diff options
| author | Stig Sandbeck Mathisen <ssm@debian.org> | 2013-12-20 08:40:56 +0100 |
|---|---|---|
| committer | Stig Sandbeck Mathisen <ssm@debian.org> | 2013-12-20 08:40:56 +0100 |
| commit | 68f6c7efced2e3fe35aefea94ea2cac160cdcd4d (patch) | |
| tree | da782122a9c9b4f8d898c664629313c9b303a507 | |
| parent | 90ffd9c9413975b3e6af205b1962364c06128f31 (diff) | |
| parent | d0f15cef98a7b77da2bef3c60eb97942b144bd17 (diff) | |
| download | puppet-upstream/3.4.0.tar.gz | |
Imported Upstream version 3.4.0upstream/3.4.0
598 files changed, 14453 insertions, 7915 deletions
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 96665c12d..681db4beb 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -47,6 +47,28 @@ top of things. * Make sure you have added the necessary tests for your changes. * Run _all_ the tests to assure nothing else was accidentally broken. +## Making Trivial Changes + +### Documentation + +For changes of a trivial nature to comments and documentation, it is not +always necessary to create a new ticket in Redmine. In this case, it is +appropriate to start the first line of a commit with '(doc)' instead of +a ticket number. + +```` + (doc) Add documentation commit example to CONTRIBUTING + + There is no example for contributing a documentation commit + to the Puppet repository. This is a problem because the contributor + is left to assume how a commit of this nature may appear. + + The first line is a real life imperative statement with '(doc)' in + place of what would have been the ticket number in a + non-documentation related commit. The body describes the nature of + the new documentation or comments added. +```` + ## Submitting Changes * Sign the [Contributor License Agreement](http://links.puppetlabs.com/cla). @@ -1,7 +1,7 @@ -source "https://rubygems.org" +source ENV['GEM_SOURCE'] || "https://rubygems.org" def location_for(place, fake_version = nil) - if place =~ /^(git:[^#]*)#(.*)/ + if place =~ /^(git[:@][^#]*)#(.*)/ [fake_version, { :git => $1, :branch => $2, :require => false }].compact elsif place =~ /^file:\/\/(.*)/ ['>= 0', { :path => File.expand_path($1), :require => false }] @@ -15,7 +15,7 @@ platforms :ruby do gem 'pry', :group => :development gem 'yard', :group => :development gem 'redcarpet', '~> 2.0', :group => :development - gem "racc", "~> 1.4", :group => :development + gem "racc", "1.4.9", :group => :development end gem "puppet", :path => File.dirname(__FILE__), :require => false @@ -37,6 +37,14 @@ group(:development, :test) do gem "mocha", "~> 0.10.5", :require => false gem "yarjuf", "~> 1.0" + + # json-schema does not support windows, so use the 'ruby' platform to exclude it on windows + platforms :ruby do + # json-schema uses multi_json, but chokes with multi_json 1.7.9, so prefer 1.7.7 + gem "multi_json", "1.7.7", :require => false + gem "json-schema", "2.1.1", :require => false + end + end group(:extra) do @@ -48,21 +56,23 @@ group(:extra) do gem "sqlite3", :require => false gem "stomp", :require => false gem "tzinfo", :require => false + gem "msgpack", :require => false end platforms :mswin, :mingw do - gem "sys-admin", "~> 1.5.6", :require => false - gem "win32-api", "~> 1.4.8", :require => false - gem "win32-dir", "~> 0.3.7", :require => false - gem "win32-eventlog", "~> 0.5.3", :require => false - gem "win32-process", "~> 0.6.5", :require => false - gem "win32-security", "~> 0.1.4", :require => false - gem "win32-service", "~> 0.7.2", :require => false - gem "win32-taskscheduler", "~> 0.2.2", :require => false - gem "win32console", "~> 1.3.2", :require => false - gem "windows-api", "~> 0.4.2", :require => false - gem "windows-pr", "~> 1.2.1", :require => false - gem "minitar", "~> 0.5.4", :require => false + gem "ffi", "1.9.0", :require => false + gem "sys-admin", "1.5.6", :require => false + gem "win32-api", "1.4.8", :require => false + gem "win32-dir", "0.4.3", :require => false + gem "win32-eventlog", "0.5.3", :require => false + gem "win32-process", "0.6.5", :require => false + gem "win32-security", "0.1.4", :require => false + gem "win32-service", "0.7.2", :require => false + gem "win32-taskscheduler", "0.2.2", :require => false + gem "win32console", "1.3.2", :require => false + gem "windows-api", "0.4.2", :require => false + gem "windows-pr", "1.2.2", :require => false + gem "minitar", "0.5.4", :require => false end if File.exists? "#{__FILE__}.local" @@ -7,32 +7,28 @@ Puppet, an automated administrative engine for your Linux, Unix, and Windows sys administrative tasks (such as adding users, installing packages, and updating server configurations) based on a centralized specification. -Documentation (and detailed installation instructions) can be found online at the -[Puppet Docs site](http://docs.puppetlabs.com). +Documentation +------------- +Documentation for Puppet and related projects can be found online at the +[Puppet Docs site](http://docs.puppetlabs.com). Installation ------------ -Generally, you need the following things installed: - -* A supported Ruby version. Ruby 1.8.7, and 1.9.3 are fully supported. +The best way to run Puppet is with [Puppet Enterprise](http://puppetlabs.com/puppet/puppet-enterprise), +which also includes orchestration features, a web console, and professional support. +[The PE documentation is available here.](http://docs.puppetlabs.com/pe/latest) -* The Ruby OpenSSL library. For some reason, this often isn't included - in the main ruby distributions. You can test for it by running - `ruby -ropenssl -e "puts :yep"`. If that errors out, you're missing the - library. +To install an open source release of Puppet, +[see the installation guide on the docs site.](http://docs.puppetlabs.com/guides/installation.html) - If your distribution doesn't come with the necessary library (e.g., on Debian - and Ubuntu you need to install libopenssl-ruby), then you'll probably have to - compile Ruby yourself, since it's part of the standard library and not - available separately. You could probably just compile and install that one - library, though. - -* Facter => 1.6.11 (available via your package manager or from the [Facter site](http://puppetlabs.com/projects/facter)). +If you need to run Puppet from source as a tester or developer, +[see the running from source guide on the docs site.](http://docs.puppetlabs.com/guides/from_source.html) Contributions ------ + Please see our [Contribution Documents](https://github.com/puppetlabs/puppet/blob/master/CONTRIBUTING.md) and our [Developer @@ -54,4 +50,4 @@ is an active #puppet channel on Freenode. HTTP API -------- -{file:api_docs/http_api_index.md HTTP API Index} +{file:api/docs/http_api_index.md HTTP API Index} diff --git a/README_DEVELOPER.md b/README_DEVELOPER.md index 7d1990899..aac8b0c25 100644 --- a/README_DEVELOPER.md +++ b/README_DEVELOPER.md @@ -693,7 +693,7 @@ When Puppet master is started from Rack, Puppet 3.x will read from ~/.puppet/puppet.conf by default. This is intended behavior. Rack configurations should start Puppet master with an explicit configuration directory using `ARGV << "--confdir" << "/etc/puppet"`. Please see the -`ext/rack/files/config.ru` file for an up-to-date example. +`ext/rack/config.ru` file for an up-to-date example. # Determining the Puppet Version @@ -64,16 +64,5 @@ task :default do end task :spec do - sh %{rspec -fd spec} -end - -namespace "ci" do - task :spec do - ENV["LOG_SPEC_ORDER"] = "true" - sh %{rspec -r yarjuf -f JUnit -o result.xml -fd spec} - end - - task :el6tests do - sh "cd acceptance/config/el6; rm -f el6.tar.gz; tar -czvf el6.tar.gz *" - end + sh %{rspec spec} end diff --git a/examples/hiera/README.md b/examples/hiera/README.md index 4fe73eb25..ecbbc944c 100644 --- a/examples/hiera/README.md +++ b/examples/hiera/README.md @@ -24,7 +24,7 @@ Module from forge with module defaults <pre> $ mv modules/data modules/data.bak -$ puppet --config etc/puppet.conf --libdir ../lib site.pp +$ puppet apply --config etc/puppet.conf site.pp notice: /Stage[main]/Ntp::Config/File[/tmp/ntp.conf]/ensure: defined content as '{md5}7045121976147a932a66c7671939a9ad' notice: /Stage[main]/Users::Common/Notify[Adding users::common]/message: defined 'message' as 'Adding users::common' $ cat /tmp/ntp.conf @@ -40,7 +40,7 @@ Site wide override data in _data::common_ <pre> $ mv modules/data.bak modules/data -$ puppet --config etc/puppet.conf --libdir ../lib site.pp +$ puppet apply --config etc/puppet.conf site.pp notice: /Stage[main]/Ntp::Config/File[/tmp/ntp.conf]/content: content changed '{md5}7045121976147a932a66c7671939a9addc2' to '{md5}8f9039fe1989a278a0a8e1836acb8d23' notice: /Stage[main]/Users::Common/Notify[Adding users::common]/message: defined 'message' as 'Adding users::common' $ cat /tmp/ntp.conf @@ -56,7 +56,7 @@ Fact driven overrides for location=dc1 * The _hiera_include()_ function includes _users::common_ and _users::dc1_ as the data file for dc1 adds that <pre> -$ FACTER_location=dc1 puppet --config etc/puppet.conf --libdir ../lib site.pp +$ FACTER_location=dc1 puppet apply --config etc/puppet.conf site.pp notice: /Stage[main]/Ntp::Config/File[/tmp/ntp.conf]/content: content changed '{md5}8f9039fe1989a278a0a8e1836acb8d23' to '{md5}074d0e2ac727f6cb9afe3345d574b578' notice: /Stage[main]/Users::Common/Notify[Adding users::common]/message: defined 'message' as 'Adding users::common' notice: /Stage[main]/Users::Dc1/Notify[Adding users::dc1]/message: defined 'message' as 'Adding users::dc1' @@ -69,7 +69,7 @@ Now simulate a machine in _dc2_, because there is no data for dc2 it uses the si does not include the _users::dc1_ class anymore <pre> -$ FACTER_location=dc2 puppet --config etc/puppet.conf --libdir ../lib site.pp +$ FACTER_location=dc2 puppet apply --config etc/puppet.conf site.pp warning: Could not find class data::dc2 for nephilim.ml.org notice: /Stage[main]/Ntp::Config/File[/tmp/ntp.conf]/content: content changed '{md5}074d0e2ac727f6cb9afe3345d574b578' to '{md5}8f9039fe1989a278a0a8e1836acb8d23' notice: /Stage[main]/Users::Common/Notify[Adding users::common]/message: defined 'message' as 'Adding users::common' diff --git a/examples/mac_automount.pp b/examples/mac_automount.pp deleted file mode 100644 index bab0136fc..000000000 --- a/examples/mac_automount.pp +++ /dev/null @@ -1,16 +0,0 @@ -#!/usr/bin/env puppet -# Jeff McCune <mccune@math.ohio-state.edu> -# -# Apple's Automounter spawns a child that sends the parent -# a SIGTERM. This makes it *very* difficult to figure out -# if the process started correctly or not. -# - -service {"automount-test": - provider => base, - hasrestart => false, - pattern => '/tmp/hometest', - start => "/usr/sbin/automount -m /tmp/home /dev/null -mnt /tmp/hometest", - stop => "ps auxww | grep '/tmp/hometest' | grep -v grep | awk '{print \$2}' | xargs kill", - ensure => running -} diff --git a/examples/mcx_dock_absent.pp b/examples/mcx_dock_absent.pp deleted file mode 100644 index ef51897e0..000000000 --- a/examples/mcx_dock_absent.pp +++ /dev/null @@ -1,4 +0,0 @@ -mcx { '/Groups/mcx_dock': - ensure => 'absent', - content => 'absent' -} diff --git a/examples/mcx_dock_default.pp b/examples/mcx_dock_default.pp deleted file mode 100644 index 78d678956..000000000 --- a/examples/mcx_dock_default.pp +++ /dev/null @@ -1,118 +0,0 @@ -mcx { '/Groups/mcx_dock': - ensure => 'present', - content => '<?xml version="1.0" encoding="UTF-8"?> -<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> -<plist version="1.0"> -<dict> - <key>com.apple.dock</key> - <dict> - <key>AppItems-Raw</key> - <dict> - <key>state</key> - <string>always</string> - <key>upk</key> - <dict> - <key>mcx_input_key_names</key> - <array> - <string>AppItems-Raw</string> - </array> - <key>mcx_output_key_name</key> - <string>static-apps</string> - <key>mcx_remove_duplicates</key> - <true/> - </dict> - <key>value</key> - <array> - <dict> - <key>mcx_typehint</key> - <integer>1</integer> - <key>tile-data</key> - <dict> - <key>file-data</key> - <dict> - <key>_CFURLString</key> - <string>/Applications/Mail.app</string> - <key>_CFURLStringType</key> - <integer>0</integer> - </dict> - <key>file-label</key> - <string>Mail</string> - </dict> - <key>tile-type</key> - <string>file-tile</string> - </dict> - <dict> - <key>mcx_typehint</key> - <integer>1</integer> - <key>tile-data</key> - <dict> - <key>file-data</key> - <dict> - <key>_CFURLString</key> - <string>/Applications/Safari.app</string> - <key>_CFURLStringType</key> - <integer>0</integer> - </dict> - <key>file-label</key> - <string>Safari</string> - </dict> - <key>tile-type</key> - <string>file-tile</string> - </dict> - </array> - </dict> - <key>DocItems-Raw</key> - <dict> - <key>state</key> - <string>always</string> - <key>upk</key> - <dict> - <key>mcx_input_key_names</key> - <array> - <string>DocItems-Raw</string> - </array> - <key>mcx_output_key_name</key> - <string>static-others</string> - <key>mcx_remove_duplicates</key> - <true/> - </dict> - <key>value</key> - <array/> - </dict> - <key>MCXDockSpecialFolders-Raw</key> - <dict> - <key>state</key> - <string>always</string> - <key>upk</key> - <dict> - <key>mcx_input_key_names</key> - <array> - <string>MCXDockSpecialFolders-Raw</string> - </array> - <key>mcx_output_key_name</key> - <string>MCXDockSpecialFolders</string> - <key>mcx_remove_duplicates</key> - <true/> - </dict> - <key>value</key> - <array/> - </dict> - <key>contents-immutable</key> - <dict> - <key>state</key> - <string>always</string> - <key>value</key> - <false/> - </dict> - <key>static-only</key> - <dict> - <key>state</key> - <string>always</string> - <key>value</key> - <false/> - </dict> - </dict> -</dict> -</plist> -' -} diff --git a/examples/mcx_dock_full.pp b/examples/mcx_dock_full.pp deleted file mode 100644 index 4fbf508fa..000000000 --- a/examples/mcx_dock_full.pp +++ /dev/null @@ -1,125 +0,0 @@ -# Mac MCX Test - -computer { "localhost": } - -mcx { - "mcx_dock": - ensure => "present", - ds_type => "group", - ds_name => "mcx_dock", - content => '<?xml version="1.0" encoding="UTF-8"?> -<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> -<plist version="1.0"> -<dict> - <key>com.apple.dock</key> - <dict> - <key>AppItems-Raw</key> - <dict> - <key>state</key> - <string>always</string> - <key>upk</key> - <dict> - <key>mcx_input_key_names</key> - <array> - <string>AppItems-Raw</string> - </array> - <key>mcx_output_key_name</key> - <string>static-apps</string> - <key>mcx_remove_duplicates</key> - <true/> - </dict> - <key>value</key> - <array> - <dict> - <key>mcx_typehint</key> - <integer>1</integer> - <key>tile-data</key> - <dict> - <key>file-data</key> - <dict> - <key>_CFURLString</key> - <string>/Applications/Mail.app</string> - <key>_CFURLStringType</key> - <integer>0</integer> - </dict> - <key>file-label</key> - <string>Mail</string> - </dict> - <key>tile-type</key> - <string>file-tile</string> - </dict> - <dict> - <key>mcx_typehint</key> - <integer>1</integer> - <key>tile-data</key> - <dict> - <key>file-data</key> - <dict> - <key>_CFURLString</key> - <string>/Applications/Safari.app</string> - <key>_CFURLStringType</key> - <integer>0</integer> - </dict> - <key>file-label</key> - <string>Safari</string> - </dict> - <key>tile-type</key> - <string>file-tile</string> - </dict> - </array> - </dict> - <key>DocItems-Raw</key> - <dict> - <key>state</key> - <string>always</string> - <key>upk</key> - <dict> - <key>mcx_input_key_names</key> - <array> - <string>DocItems-Raw</string> - </array> - <key>mcx_output_key_name</key> - <string>static-others</string> - <key>mcx_remove_duplicates</key> - <true/> - </dict> - <key>value</key> - <array/> - </dict> - <key>MCXDockSpecialFolders-Raw</key> - <dict> - <key>state</key> - <string>always</string> - <key>upk</key> - <dict> - <key>mcx_input_key_names</key> - <array> - <string>MCXDockSpecialFolders-Raw</string> - </array> - <key>mcx_output_key_name</key> - <string>MCXDockSpecialFolders</string> - <key>mcx_remove_duplicates</key> - <true/> - </dict> - <key>value</key> - <array/> - </dict> - <key>contents-immutable</key> - <dict> - <key>state</key> - <string>always</string> - <key>value</key> - <false/> - </dict> - <key>static-only</key> - <dict> - <key>state</key> - <string>always</string> - <key>value</key> - <false/> - </dict> - </dict> -</dict> -</plist> -' -} diff --git a/examples/mcx_dock_invalid.pp b/examples/mcx_dock_invalid.pp deleted file mode 100644 index 35f908177..000000000 --- a/examples/mcx_dock_invalid.pp +++ /dev/null @@ -1,9 +0,0 @@ -# Mac MCX Test - -computer { "localhost": } - -mcx { - "/Groups/mcx_dock": - ensure => "present", - content => 'invalid plist' -} diff --git a/examples/mcx_nogroup.pp b/examples/mcx_nogroup.pp deleted file mode 100644 index 67d20e605..000000000 --- a/examples/mcx_nogroup.pp +++ /dev/null @@ -1,118 +0,0 @@ -mcx { '/Groups/doesnotexist': - ensure => 'present', - content => '<?xml version="1.0" encoding="UTF-8"?> -<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> -<plist version="1.0"> -<dict> - <key>com.apple.dock</key> - <dict> - <key>AppItems-Raw</key> - <dict> - <key>state</key> - <string>always</string> - <key>upk</key> - <dict> - <key>mcx_input_key_names</key> - <array> - <string>AppItems-Raw</string> - </array> - <key>mcx_output_key_name</key> - <string>static-apps</string> - <key>mcx_remove_duplicates</key> - <true/> - </dict> - <key>value</key> - <array> - <dict> - <key>mcx_typehint</key> - <integer>1</integer> - <key>tile-data</key> - <dict> - <key>file-data</key> - <dict> - <key>_CFURLString</key> - <string>/Applications/Mail.app</string> - <key>_CFURLStringType</key> - <integer>0</integer> - </dict> - <key>file-label</key> - <string>Mail</string> - </dict> - <key>tile-type</key> - <string>file-tile</string> - </dict> - <dict> - <key>mcx_typehint</key> - <integer>1</integer> - <key>tile-data</key> - <dict> - <key>file-data</key> - <dict> - <key>_CFURLString</key> - <string>/Applications/Safari.app</string> - <key>_CFURLStringType</key> - <integer>0</integer> - </dict> - <key>file-label</key> - <string>Safari</string> - </dict> - <key>tile-type</key> - <string>file-tile</string> - </dict> - </array> - </dict> - <key>DocItems-Raw</key> - <dict> - <key>state</key> - <string>always</string> - <key>upk</key> - <dict> - <key>mcx_input_key_names</key> - <array> - <string>DocItems-Raw</string> - </array> - <key>mcx_output_key_name</key> - <string>static-others</string> - <key>mcx_remove_duplicates</key> - <true/> - </dict> - <key>value</key> - <array/> - </dict> - <key>MCXDockSpecialFolders-Raw</key> - <dict> - <key>state</key> - <string>always</string> - <key>upk</key> - <dict> - <key>mcx_input_key_names</key> - <array> - <string>MCXDockSpecialFolders-Raw</string> - </array> - <key>mcx_output_key_name</key> - <string>MCXDockSpecialFolders</string> - <key>mcx_remove_duplicates</key> - <true/> - </dict> - <key>value</key> - <array/> - </dict> - <key>contents-immutable</key> - <dict> - <key>state</key> - <string>always</string> - <key>value</key> - <false/> - </dict> - <key>static-only</key> - <dict> - <key>state</key> - <string>always</string> - <key>value</key> - <false/> - </dict> - </dict> -</dict> -</plist> -' -} diff --git a/examples/mcx_notexists_absent.pp b/examples/mcx_notexists_absent.pp deleted file mode 100644 index ef44db10e..000000000 --- a/examples/mcx_notexists_absent.pp +++ /dev/null @@ -1,4 +0,0 @@ -mcx { '/Groups/foobar': - ensure => 'absent', - content => 'absent' -} diff --git a/ext/build_defaults.yaml b/ext/build_defaults.yaml index 71636e6a0..1d117b3d4 100644 --- a/ext/build_defaults.yaml +++ b/ext/build_defaults.yaml @@ -2,14 +2,14 @@ packaging_url: 'git://github.com/puppetlabs/packaging.git --branch=master' packaging_repo: 'packaging' default_cow: 'base-squeeze-i386.cow' -cows: 'base-lucid-i386.cow base-precise-i386.cow base-quantal-i386.cow base-raring-i386.cow base-sid-i386.cow base-squeeze-i386.cow base-stable-i386.cow base-testing-i386.cow base-unstable-i386.cow base-wheezy-i386.cow' +cows: 'base-lucid-i386.cow base-precise-i386.cow base-quantal-i386.cow base-raring-i386.cow base-sid-i386.cow base-squeeze-i386.cow base-stable-i386.cow base-testing-i386.cow base-unstable-i386.cow base-wheezy-i386.cow base-saucy-i386.cow' pbuild_conf: '/etc/pbuilderrc' packager: 'puppetlabs' gpg_name: 'info@puppetlabs.com' gpg_key: '4BD6EC30' sign_tar: FALSE # a space separated list of mock configs -final_mocks: 'pl-el-5-i386 pl-el-6-i386 pl-fedora-18-i386 pl-fedora-19-i386' +final_mocks: 'pl-el-5-i386 pl-el-6-i386 pl-fedora-18-i386 pl-fedora-19-i386 pl-fedora-20-i386' yum_host: 'yum.puppetlabs.com' yum_repo_path: '/opt/repository/yum/' build_gem: TRUE diff --git a/ext/debian/changelog b/ext/debian/changelog index b94c8e5b7..eb381b0fd 100644 --- a/ext/debian/changelog +++ b/ext/debian/changelog @@ -1,8 +1,8 @@ -puppet (3.3.1-1puppetlabs1) hardy lucid natty oneiric unstable sid squeeze wheezy precise; urgency=low +puppet (3.4.0-1puppetlabs1) hardy lucid natty oneiric unstable sid squeeze wheezy precise; urgency=low - * Update to version 3.3.1-1puppetlabs1 + * Update to version 3.4.0-1puppetlabs1 - -- Puppet Labs Release <info@puppetlabs.com> Mon, 07 Oct 2013 11:00:57 -0700 + -- Puppet Labs Release <info@puppetlabs.com> Thu, 19 Dec 2013 10:53:11 -0800 puppet (3.2.3-0.1rc0puppetlabs1) lucid unstable sid squeeze wheezy precise quantal raring; urgency=low diff --git a/ext/debian/puppetmaster.init b/ext/debian/puppetmaster.init index 4ebcc5a73..d3dffac33 100644 --- a/ext/debian/puppetmaster.init +++ b/ext/debian/puppetmaster.init @@ -100,6 +100,7 @@ status_puppet_master() { else echo "" echo "puppetmaster not configured to start" + status_of_proc -p "/var/run/puppet/${NAME}.pid" "${DAEMON}" "${NAME}" fi } diff --git a/ext/debian/rules b/ext/debian/rules index 92ac1c5bb..11418e57f 100755 --- a/ext/debian/rules +++ b/ext/debian/rules @@ -66,14 +66,11 @@ install: build $(INSTALL) -m0644 ext/emacs/puppet-mode.el \ $(CURDIR)/debian/puppet-el/usr/share/emacs/site-lisp/puppet-mode.el - # Install the rack README as README.rack - $(INSTALL) -m0644 ext/rack/README \ - $(CURDIR)/debian/puppetmaster-passenger/usr/share/doc/puppetmaster-passenger/README.rack # Install the config.ru - $(INSTALL) -m0644 ext/rack/files/config.ru \ + $(INSTALL) -m0644 ext/rack/config.ru \ $(CURDIR)/debian/puppetmaster-passenger/usr/share/puppet/rack/puppetmasterd # Install apache2 site configuration template - $(INSTALL) -m0644 ext/rack/files/apache2.conf \ + $(INSTALL) -m0644 ext/rack/example-passenger-vhost.conf \ $(CURDIR)/debian/puppetmaster-passenger/usr/share/puppetmaster-passenger/apache2.site.conf.tmpl # Add ext directory diff --git a/ext/ips/puppet.p5m b/ext/ips/puppet.p5m index 06cb710ff..958a952b9 100644 --- a/ext/ips/puppet.p5m +++ b/ext/ips/puppet.p5m @@ -1,6 +1,6 @@ -set name=pkg.fmri value=pkg://puppetlabs.com/system/management/puppet@3.3.1,12.5.0-0 +set name=pkg.fmri value=pkg://puppetlabs.com/system/management/puppet@3.4.0,13.0.0-0 set name=pkg.summary value="Puppet, an automated configuration management tool" -set name=pkg.human-version value="3.3.1" +set name=pkg.human-version value="3.4.0" set name=pkg.description value="Puppet, an automated configuration management tool" set name=info.classification value="org.opensolaris.category.2008:System/Administration and Configuration" set name=org.opensolaris.consolidation value="puppet" diff --git a/ext/nagios/check_puppet.rb b/ext/nagios/check_puppet.rb index 85807f122..e614b3c77 100755 --- a/ext/nagios/check_puppet.rb +++ b/ext/nagios/check_puppet.rb @@ -99,16 +99,16 @@ class CheckPuppet process = "process #{OPTIONS[:process]} is not running" end - case @proc or @file - when 0 - status = "OK" - exitcode = 0 - when 2 + case + when (@proc == 2 or @file == 2) status = "CRITICAL" exitcode = 2 - when 3 + when (@proc == 0 and @file == 0) + status = "OK" + exitcode = 0 + else status = "UNKNOWN" - exitcide = 3 + exitcode = 3 end puts "PUPPET #{status}: #{process}, #{state}" diff --git a/ext/osx/file_mapping.yaml b/ext/osx/file_mapping.yaml index 9a1e957d7..7a1d4e7f7 100644 --- a/ext/osx/file_mapping.yaml +++ b/ext/osx/file_mapping.yaml @@ -1,6 +1,6 @@ directories: lib: - path: 'usr/lib/ruby/site_ruby/1.8' + path: 'Library/Ruby/Site' owner: 'root' group: 'wheel' perms: '0644' diff --git a/ext/osx/preflight.erb b/ext/osx/preflight.erb index d04f30560..036ac11d7 100755 --- a/ext/osx/preflight.erb +++ b/ext/osx/preflight.erb @@ -8,30 +8,45 @@ # ${3} is the destination volume so that this works correctly # when being installed to volumes other than the current OS. -<% begin %> -<% require 'rubygems' %> -<% rescue LoadError %> -<% end %> -<% require 'rake' %> +<%- ["@apple_libdir", "@apple_sbindir", "@apple_bindir", "@apple_docdir", "@package_name"].each do |i| -%> + <%- val = instance_variable_get(i) -%> + <%- raise "Critical variable #{i} is unset!" if val.nil? or val.empty? -%> +<%- end -%> -<% Dir.chdir("lib") %> -<% FileList["**/*"].select {|i| File.file?(i)}.each do |file| %> -/bin/rm -Rf "${3}<%= @apple_libdir %>/<%=file%>" -<% end %> +# remove ruby library files. Because hiera and puppet both place a "hiera" +# directory in the libdir, we have to be sure we only delete the files +# belonging to this project. In this case, we only delete files from within +# the 'hiera' directory, while deleting the entire puppet directory. +<%- Dir.chdir("lib") do -%> + <%- [@apple_old_libdir, @apple_libdir].compact.each do |libdir| -%> + <%- Dir["hiera/**/*"].select{ |i| File.file?(i) }.each do |file| -%> +/bin/rm -f "${3}<%= libdir %>/<%= file %>" + <%- end -%> + <%- Dir.glob("*").each do |file| -%> + <%- next if file == "hiera" -%> + <%- if File.directory?(file) -%> +/bin/rm -Rf "${3}<%= libdir %>/<%= file %>" + <%- else -%> +/bin/rm -f "${3}<%= libdir %>/<%= file %>" + <%- end -%> + <%- end -%> + <%- end -%> +<%- end -%> -<% Dir.chdir("../bin") %> -<% FileList["**/*"].select {|i| File.file?(i)}.each do |file| %> -/bin/rm -Rf "${3}<%= @apple_bindir %>/<%=file%>" -<% end %> -<% Dir.chdir("..") %> +# remove bin files +<%- Dir.chdir("bin") do -%> + <%- Dir.glob("*").each do |file| -%> +/bin/rm -f "${3}<%= @apple_bindir %>/<%= file %>" + <%- end -%> +<%- end -%> # remove old doc files /bin/rm -Rf "${3}<%= @apple_docdir %>/<%= @package_name %>" # These files used to live in the sbindir but were # removed in Pupppet >= 3.0. Remove them -/bin/rm -Rf "${3}<%= @apple_sbindir %>/puppetca" -/bin/rm -Rf "${3}<%= @apple_sbindir %>/puppetd" -/bin/rm -Rf "${3}<%= @apple_sbindir %>/puppetmasterd" -/bin/rm -Rf "${3}<%= @apple_sbindir %>/puppetqd" -/bin/rm -Rf "${3}<%= @apple_sbindir %>/puppetrun" +/bin/rm -f "${3}<%= @apple_sbindir %>/puppetca" +/bin/rm -f "${3}<%= @apple_sbindir %>/puppetd" +/bin/rm -f "${3}<%= @apple_sbindir %>/puppetmasterd" +/bin/rm -f "${3}<%= @apple_sbindir %>/puppetqd" +/bin/rm -f "${3}<%= @apple_sbindir %>/puppetrun" diff --git a/ext/rack/README b/ext/rack/README deleted file mode 100644 index 3a422f6a1..000000000 --- a/ext/rack/README +++ /dev/null @@ -1,58 +0,0 @@ - -PUPPETMASTER AS A RACK APPLICATION -================================== - -puppetmaster can now be hosted as a standard Rack application. A proper -config.ru is provided for this. - -For more details about rack, see http://rack.rubyforge.org/ . - -Getting started -=============== - -You'll need rack installed, version 1.0.0. Older versions are known not -to work. - - -WEBrick -------- - -WEBrick is currently not supported as a Rack host. You'll be better off -just running puppetmasterd directly. - - -Apache with Passenger (aka mod_rails) -------------------------------------- - -Make sure puppetmasterd ran at least once, so the CA & SSL certificates -got set up. - -Requirements: - Passenger version 2.2.2 or 2.2.5 or newer*** - Rack version 1.0.0 - Apache 2.x - SSL Module loaded - -Apache configuration snippet is in files/apache2.conf. You need to -edit it to reflect your servername. - -Required puppet.conf settings: - [puppetmasterd] - ssl_client_header = SSL_CLIENT_S_DN - ssl_client_verify_header = SSL_CLIENT_VERIFY - -To set up most of the boring stuff, you can use this command: - puppet --verbose --modulepath ./ext ext/rack/manifest.pp -Or use manifest.pp as a starting point for your own module. - -Note: Passenger will not let applications run as root or the Apache user, -instead an implicit setuid will be done, to the user whom owns -config.ru. Therefore, config.ru shall be owned by the puppet user. - - -*** Important note about Passenger versions: - 2.2.2 is known to work. - 2.2.3-2.2.4 are known to *NOT* work. - 2.2.5 works again when used with Puppet 0.25.2+. - Passenger installation doc: http://www.modrails.com/install.html - diff --git a/ext/rack/files/config.ru b/ext/rack/config.ru index 984017edd..984017edd 100644 --- a/ext/rack/files/config.ru +++ b/ext/rack/config.ru diff --git a/ext/rack/files/apache2.conf b/ext/rack/example-passenger-vhost.conf index 59bbd15b8..860e6f373 100644 --- a/ext/rack/files/apache2.conf +++ b/ext/rack/example-passenger-vhost.conf @@ -1,3 +1,9 @@ +# This Apache 2 virtual host config shows how to use Puppet as a Rack +# application via Passenger. See +# http://docs.puppetlabs.com/guides/passenger.html for more information. + +# You can also use the included config.ru file to run Puppet with other Rack +# servers instead of Passenger. # you probably want to tune these settings PassengerHighPerformance on diff --git a/ext/rack/manifest.pp b/ext/rack/manifest.pp deleted file mode 100644 index 7df07c0c8..000000000 --- a/ext/rack/manifest.pp +++ /dev/null @@ -1,59 +0,0 @@ - -file { ["/etc/puppet/rack", "/etc/puppet/rack/public"]: - ensure => directory, - mode => 0755, - owner => root, - group => root, -} -file { "/etc/puppet/rack/config.ru": - ensure => present, - source => "puppet:///modules/rack/config.ru", - mode => 0644, - owner => puppet, - group => root, -} -file { "/etc/apache2/conf.d/puppetmasterd": - ensure => present, - source => "puppet:///modules/rack/apache2.conf", - mode => 0644, - owner => root, - group => root, - require => [File["/etc/puppet/rack/config.ru"], File["/etc/puppet/rack/public"], Package["apache2"], Package["passenger"]], - notify => Service["apache2"], -} - -package { ["rack", "passenger"]: - ensure => installed, - provider => "gem", -} - -service { "apache2": -} - -case $lsbdistid { - "Debian": { - package { ["apache2-mpm-worker", "apache2-threaded-dev", "apache2"]: - ensure => installed, - } - file { "/etc/apache2/mods-enabled/ssl.load": - ensure => "../mods-available/ssl.load", - notify => Service["apache2"], - require => Package["apache2"], - } - Service["apache2"] { - require => Package["apache2"], - } - exec { "/var/lib/gems/1.8/bin/passenger-install-apache2-module --auto": - subscribe => Package["passenger"], - before => Service["apache2"], - require => Package[["passenger", "apache2-threaded-dev"]], - } - } -} - -notice("You need to manually enable mod_passenger.so for Apache.") -notice("Usually, you put these config stanzas into httpd.conf:") -notice(" LoadModule passenger_module /var/lib/gems/1.8/gems/passenger-2.2.2/ext/apache2/mod_passenger.so") -notice(" PassengerRoot /var/lib/gems/1.8/gems/passenger-2.2.2") -notice(" PassengerRuby /usr/bin/ruby1.8") -notice("--------------------------------------------------------") diff --git a/ext/redhat/puppet.spec b/ext/redhat/puppet.spec index b92662873..4a38607ae 100644 --- a/ext/redhat/puppet.spec +++ b/ext/redhat/puppet.spec @@ -16,8 +16,8 @@ %endif # VERSION is subbed out during rake srpm process -%global realversion 3.3.1 -%global rpmversion 3.3.1 +%global realversion 3.4.0 +%global rpmversion 3.4.0 %global confdir ext/redhat @@ -122,7 +122,8 @@ install -d -m0750 %{buildroot}%{_localstatedir}/log/puppet %if 0%{?_with_systemd} # Systemd for fedora >= 17 %{__install} -d -m0755 %{buildroot}%{_unitdir} -install -Dp -m0644 ext/systemd/puppetagent.service %{buildroot}%{_unitdir}/puppetagent.service +install -Dp -m0644 ext/systemd/puppet.service %{buildroot}%{_unitdir}/puppet.service +ln -s %{_unitdir}/puppet.service %{buildroot}%{_unitdir}/puppetagent.service install -Dp -m0644 ext/systemd/puppetmaster.service %{buildroot}%{_unitdir}/puppetmaster.service %else # Otherwise init.d for fedora < 17 or el 5, 6 @@ -189,6 +190,7 @@ cp -pr ext/puppet-nm-dispatcher \ %dir %{_sysconfdir}/NetworkManager/dispatcher.d %{_sysconfdir}/NetworkManager/dispatcher.d/98-puppet %if 0%{?_with_systemd} +%{_unitdir}/puppet.service %{_unitdir}/puppetagent.service %else %{_initrddir}/puppet @@ -289,7 +291,7 @@ if [ "$1" -ge 1 ]; then newpid="%{_localstatedir}/run/puppet/agent.pid" if [ -s "$oldpid" -a ! -s "$newpid" ]; then (kill $(< "$oldpid") && rm -f "$oldpid" && \ - /bin/systemctl start puppetagent.service) >/dev/null 2>&1 || : + /bin/systemctl start puppet.service) >/dev/null 2>&1 || : fi fi %else @@ -303,6 +305,15 @@ if [ "$1" -ge 1 ]; then (kill $(< "$oldpid") && rm -f "$oldpid" && \ /sbin/service puppet start) >/dev/null 2>&1 || : fi + + # If an old puppet process (one whose binary is located in /sbin) is running, + # kill it and then start up a fresh with the new binary. + if [ -e "$newpid" ]; then + if ps aux | grep `cat "$newpid"` | grep -v grep | awk '{ print $12 }' | grep -q sbin; then + (kill $(< "$newpid") && rm -f "$newpid" && \ + /sbin/service puppet start) >/dev/null 2>&1 || : + fi + fi fi %endif @@ -338,7 +349,9 @@ fi if [ "$1" -eq 0 ] ; then # Package removal, not upgrade /bin/systemctl --no-reload disable puppetagent.service > /dev/null 2>&1 || : + /bin/systemctl --no-reload disable puppet.service > /dev/null 2>&1 || : /bin/systemctl stop puppetagent.service > /dev/null 2>&1 || : + /bin/systemctl stop puppet.service > /dev/null 2>&1 || : /bin/systemctl daemon-reload >/dev/null 2>&1 || : fi %else @@ -391,8 +404,13 @@ fi rm -rf %{buildroot} %changelog -* Mon Oct 07 2013 Puppet Labs Release <info@puppetlabs.com> - 3.3.1-1 -- Build for 3.3.1 +* Thu Dec 19 2013 Puppet Labs Release <info@puppetlabs.com> - 3.4.0-1 +- Build for 3.4.0 + +* Wed Oct 2 2013 Jason Antman <jason@jasonantman.com> +- Move systemd service and unit file names back to "puppet" from erroneous "puppetagent" +- Add symlink to puppetagent unit file for compatibility with current bug +- Alter package removal actions to deactivate and stop both service names * Thu Jun 27 2013 Matthaus Owens <matthaus@puppetlabs.com> - 3.2.3-0.1rc0 - Bump requires on ruby-rgen to 0.6.5 diff --git a/ext/systemd/puppetagent.service b/ext/systemd/puppet.service index 6312a0509..6312a0509 100644 --- a/ext/systemd/puppetagent.service +++ b/ext/systemd/puppet.service diff --git a/lib/hiera_puppet.rb b/lib/hiera_puppet.rb index 0e6a572a8..d90b82089 100644 --- a/lib/hiera_puppet.rb +++ b/lib/hiera_puppet.rb @@ -71,12 +71,12 @@ module HieraPuppet if Puppet.settings[:hiera_config].is_a?(String) expanded_config_file = File.expand_path(Puppet.settings[:hiera_config]) - if File.exist?(expanded_config_file) + if Puppet::FileSystem::File.exist?(expanded_config_file) config_file = expanded_config_file end elsif Puppet.settings[:confdir].is_a?(String) expanded_config_file = File.expand_path(File.join(Puppet.settings[:confdir], '/hiera.yaml')) - if File.exist?(expanded_config_file) + if Puppet::FileSystem::File.exist?(expanded_config_file) config_file = expanded_config_file end end diff --git a/lib/puppet/agent.rb b/lib/puppet/agent.rb index 678fdb5d0..14c6c693b 100644 --- a/lib/puppet/agent.rb +++ b/lib/puppet/agent.rb @@ -1,4 +1,3 @@ -require 'sync' require 'puppet/application' # A general class for triggering a run of another @@ -42,7 +41,7 @@ class Puppet::Agent with_client do |client| begin client_args = client_options.merge(:pluginsync => Puppet[:pluginsync]) - sync.synchronize { lock { client.run(client_args) } } + lock { client.run(client_args) } rescue SystemExit,NoMemoryError raise rescue Exception => detail @@ -76,10 +75,6 @@ class Puppet::Agent @splayed = true end - def sync - @sync ||= Sync.new - end - def run_in_fork(forking = true) return yield unless forking or Puppet.features.windows? diff --git a/lib/puppet/application.rb b/lib/puppet/application.rb index 463c77b32..acef32134 100644 --- a/lib/puppet/application.rb +++ b/lib/puppet/application.rb @@ -381,18 +381,31 @@ class Application Puppet::Util::Log.newdestination(:console) end + set_log_level + + Puppet::Util::Log.setup_default unless options[:setdest] + end + + def set_log_level if options[:debug] Puppet::Util::Log.level = :debug elsif options[:verbose] Puppet::Util::Log.level = :info end + end - Puppet::Util::Log.setup_default unless options[:setdest] + def handle_logdest_arg(arg) + begin + Puppet::Util::Log.newdestination(arg) + options[:setdest] = true + rescue => detail + Puppet.log_exception(detail) + end end def configure_indirector_routes route_file = Puppet[:route_file] - if ::File.exists?(route_file) + if Puppet::FileSystem::File.exist?(route_file) routes = YAML.load_file(route_file) application_routes = routes[name.to_s] Puppet::Indirector.configure_routes(application_routes) if application_routes diff --git a/lib/puppet/application/agent.rb b/lib/puppet/application/agent.rb index 3a42dd83e..d86f3f47f 100644 --- a/lib/puppet/application/agent.rb +++ b/lib/puppet/application/agent.rb @@ -67,12 +67,7 @@ class Puppet::Application::Agent < Puppet::Application end option("--logdest DEST", "-l DEST") do |arg| - begin - Puppet::Util::Log.newdestination(arg) - options[:setdest] = true - rescue => detail - Puppet.log_exception(detail) - end + handle_logdest_arg(arg) end option("--waitforcert WAITFORCERT", "-w") do |arg| @@ -442,7 +437,7 @@ Copyright (c) 2011 Puppet Labs, LLC Licensed under the Apache 2.0 License def setup_listen(daemon) Puppet.warning "Puppet --listen / kick is deprecated. See http://links.puppetlabs.com/puppet-kick-deprecation" - unless FileTest.exists?(Puppet[:rest_authconfig]) + unless Puppet::FileSystem::File.exist?(Puppet[:rest_authconfig]) Puppet.err "Will not start without authorization file #{Puppet[:rest_authconfig]}" exit(14) end diff --git a/lib/puppet/application/apply.rb b/lib/puppet/application/apply.rb index 3d7370d2c..2eb4415b3 100644 --- a/lib/puppet/application/apply.rb +++ b/lib/puppet/application/apply.rb @@ -19,12 +19,7 @@ class Puppet::Application::Apply < Puppet::Application end option("--logdest LOGDEST", "-l") do |arg| - begin - Puppet::Util::Log.newdestination(arg) - options[:logset] = true - rescue => detail - $stderr.puts detail.to_s - end + handle_logdest_arg(arg) end option("--parseonly") do |args| @@ -168,7 +163,7 @@ Copyright (c) 2011 Puppet Labs, LLC Licensed under the Apache 2.0 License Puppet[:code] = options[:code] || STDIN.read else manifest = command_line.args.shift - raise "Could not find file #{manifest}" unless ::File.exist?(manifest) + raise "Could not find file #{manifest}" unless Puppet::FileSystem::File.exist?(manifest) Puppet.warning("Only one file can be applied per run. Skipping #{command_line.args.join(', ')}") if command_line.args.size > 0 Puppet[:manifest] = manifest end @@ -194,7 +189,7 @@ Copyright (c) 2011 Puppet Labs, LLC Licensed under the Apache 2.0 License # Allow users to load the classes that puppet agent creates. if options[:loadclasses] file = Puppet[:classfile] - if FileTest.exists?(file) + if Puppet::FileSystem::File.exist?(file) unless FileTest.readable?(file) $stderr.puts "#{file} is not readable" exit(63) @@ -238,7 +233,7 @@ Copyright (c) 2011 Puppet Labs, LLC Licensed under the Apache 2.0 License def setup exit(Puppet.settings.print_configs ? 0 : 1) if Puppet.settings.print_configs? - Puppet::Util::Log.newdestination(:console) unless options[:logset] + Puppet::Util::Log.newdestination(:console) unless options[:setdest] Signal.trap(:INT) do $stderr.puts "Exiting" @@ -248,10 +243,10 @@ Copyright (c) 2011 Puppet Labs, LLC Licensed under the Apache 2.0 License # we want the last report to be persisted locally Puppet::Transaction::Report.indirection.cache_class = :yaml - if options[:debug] - Puppet::Util::Log.level = :debug - elsif options[:verbose] - Puppet::Util::Log.level = :info + set_log_level + + if Puppet[:profile] + Puppet::Util::Profiler.current = Puppet::Util::Profiler::WallClock.new(Puppet.method(:debug), "apply") end end diff --git a/lib/puppet/application/cert.rb b/lib/puppet/application/cert.rb index edf15f3b9..6ad901593 100644 --- a/lib/puppet/application/cert.rb +++ b/lib/puppet/application/cert.rb @@ -1,4 +1,5 @@ require 'puppet/application' +require 'puppet/ssl/certificate_authority/interface' class Puppet::Application::Cert < Puppet::Application @@ -36,11 +37,36 @@ class Puppet::Application::Cert < Puppet::Application Puppet::Util::Log.level = :debug end - require 'puppet/ssl/certificate_authority/interface' - Puppet::SSL::CertificateAuthority::Interface::INTERFACE_METHODS.reject {|m| m == :destroy }.each do |method| - option("--#{method.to_s.gsub('_','-')}", "-#{method.to_s[0,1]}") do |arg| - self.subcommand = method - end + option("--list", "-l") do |arg| + self.subcommand = :list + end + + option("--revoke", "-r") do |arg| + self.subcommand = :revoke + end + + option("--generate", "-g") do |arg| + self.subcommand = :generate + end + + option("--sign", "-s") do |arg| + self.subcommand = :sign + end + + option("--print", "-p") do |arg| + self.subcommand = :print + end + + option("--verify", "-v") do |arg| + self.subcommand = :verify + end + + option("--fingerprint", "-f") do |arg| + self.subcommand = :fingerprint + end + + option("--reinventory") do |arg| + self.subcommand = :reinventory end option("--[no-]allow-dns-alt-names") do |value| @@ -120,6 +146,11 @@ unless the '--all' option is set. * verify: Verify the named certificate against the local CA certificate. +* reinventory: + Build an inventory of the issued certificates. This will destroy the current + inventory file specified by 'cert_inventory' and recreate it from the + certificates found in the 'certdir'. Ensure the puppet master is stopped + before running this action. OPTIONS ------- @@ -184,8 +215,8 @@ Copyright (c) 2011 Puppet Labs, LLC Licensed under the Apache 2.0 License hosts = command_line.args.collect { |h| h.downcase } end begin - @ca.apply(:revoke, options.merge(:to => hosts)) if subcommand == :destroy - @ca.apply(subcommand, options.merge(:to => hosts, :digest => @digest)) + apply(@ca, :revoke, options.merge(:to => hosts)) if subcommand == :destroy + apply(@ca, subcommand, options.merge(:to => hosts, :digest => @digest)) rescue => detail Puppet.log_exception(detail) exit(24) @@ -234,4 +265,13 @@ Copyright (c) 2011 Puppet Labs, LLC Licensed under the Apache 2.0 License end result end + + # Create and run an applicator. I wanted to build an interface where you could do + # something like 'ca.apply(:generate).to(:all) but I don't think it's really possible. + def apply(ca, method, options) + raise ArgumentError, "You must specify the hosts to apply to; valid values are an array or the symbol :all" unless options[:to] + applier = Puppet::SSL::CertificateAuthority::Interface.new(method, options) + applier.apply(ca) + end + end diff --git a/lib/puppet/application/device.rb b/lib/puppet/application/device.rb index b80691f3c..5eef49fef 100644 --- a/lib/puppet/application/device.rb +++ b/lib/puppet/application/device.rb @@ -47,12 +47,7 @@ class Puppet::Application::Device < Puppet::Application end option("--logdest DEST", "-l DEST") do |arg| - begin - Puppet::Util::Log.newdestination(arg) - options[:setdest] = true - rescue => detail - Puppet.log_exception(detail) - end + handle_logdest_arg(arg) end option("--waitforcert WAITFORCERT", "-w") do |arg| diff --git a/lib/puppet/application/face_base.rb b/lib/puppet/application/face_base.rb index 86cc6a0d8..3154d6de4 100644 --- a/lib/puppet/application/face_base.rb +++ b/lib/puppet/application/face_base.rb @@ -240,7 +240,7 @@ class Puppet::Application::FaceBase < Puppet::Application rescue SystemExit => detail status = detail.status - rescue Exception => detail + rescue => detail Puppet.log_exception(detail) Puppet.err "Try 'puppet help #{@face.name} #{@action.name}' for usage" diff --git a/lib/puppet/application/filebucket.rb b/lib/puppet/application/filebucket.rb index 389e6e4ca..509885602 100644 --- a/lib/puppet/application/filebucket.rb +++ b/lib/puppet/application/filebucket.rb @@ -129,7 +129,7 @@ Copyright (c) 2011 Puppet Labs, LLC Licensed under the Apache 2.0 License raise "You must specify a file to back up" unless args.length > 0 args.each do |file| - unless FileTest.exists?(file) + unless Puppet::FileSystem::File.exist?(file) $stderr.puts "#{file}: no such file" next end diff --git a/lib/puppet/application/inspect.rb b/lib/puppet/application/inspect.rb index 3ebeb7875..3aec9fa23 100644 --- a/lib/puppet/application/inspect.rb +++ b/lib/puppet/application/inspect.rb @@ -8,12 +8,7 @@ class Puppet::Application::Inspect < Puppet::Application option("--verbose","-v") option("--logdest LOGDEST", "-l") do |arg| - begin - Puppet::Util::Log.newdestination(arg) - options[:logset] = true - rescue => detail - $stderr.puts detail.to_s - end + handle_logdest_arg(arg) end def help @@ -86,18 +81,14 @@ Copyright (c) 2011 Puppet Labs, LLC Licensed under the Apache 2.0 License @report = Puppet::Transaction::Report.new("inspect") Puppet::Util::Log.newdestination(@report) - Puppet::Util::Log.newdestination(:console) unless options[:logset] + Puppet::Util::Log.newdestination(:console) unless options[:setdest] Signal.trap(:INT) do $stderr.puts "Exiting" exit(1) end - if options[:debug] - Puppet::Util::Log.level = :debug - elsif options[:verbose] - Puppet::Util::Log.level = :info - end + set_log_level Puppet::Transaction::Report.indirection.terminus_class = :rest Puppet::Resource::Catalog.indirection.terminus_class = :yaml diff --git a/lib/puppet/application/master.rb b/lib/puppet/application/master.rb index fafbcf3b4..b8f27994f 100644 --- a/lib/puppet/application/master.rb +++ b/lib/puppet/application/master.rb @@ -17,12 +17,7 @@ class Puppet::Application::Master < Puppet::Application end option("--logdest DEST", "-l DEST") do |arg| - begin - Puppet::Util::Log.newdestination(arg) - options[:setdest] = true - rescue => detail - Puppet.log_exception(detail) - end + handle_logdest_arg(arg) end option("--parseonly") do |args| diff --git a/lib/puppet/application/queue.rb b/lib/puppet/application/queue.rb index 87e269e33..e3820e621 100644 --- a/lib/puppet/application/queue.rb +++ b/lib/puppet/application/queue.rb @@ -112,12 +112,7 @@ Copyright (c) 2011 Puppet Labs, LLC Licensed under the Apache 2.0 License end option("--logdest DEST", "-l DEST") do |arg| - begin - Puppet::Util::Log.newdestination(arg) - options[:setdest] = true - rescue => detail - Puppet.log_exception(detail) - end + handle_logdest_arg(arg) end def main diff --git a/lib/puppet/application/resource.rb b/lib/puppet/application/resource.rb index d4944f860..35f9f155b 100644 --- a/lib/puppet/application/resource.rb +++ b/lib/puppet/application/resource.rb @@ -14,6 +14,7 @@ class Puppet::Application::Resource < Puppet::Application option("--edit","-e") option("--host HOST","-H") do |arg| + Puppet.warning("Accessing resources on the network is deprecated. See http://links.puppetlabs.com/deprecate-networked-resource") @host = arg end @@ -151,12 +152,7 @@ Copyright (c) 2011 Puppet Labs, LLC Licensed under the Apache 2.0 License def setup Puppet::Util::Log.newdestination(:console) - - if options[:debug] - Puppet::Util::Log.level = :debug - elsif options[:verbose] - Puppet::Util::Log.level = :info - end + set_log_level end private diff --git a/lib/puppet/coercion.rb b/lib/puppet/coercion.rb index f3db8f62b..5de06a363 100644 --- a/lib/puppet/coercion.rb +++ b/lib/puppet/coercion.rb @@ -26,4 +26,15 @@ module Puppet::Coercion fail('expected a boolean value') end end + + # Return the list of acceptable boolean values. + # + # This is limited to lower-case, even though boolean() is case-insensitive. + # + # @return [Array] + # @raise + # @api private + def self.boolean_values + ['true', 'false', 'yes', 'no'] + end end diff --git a/lib/puppet/configurer.rb b/lib/puppet/configurer.rb index 6547f7458..0c000c9ea 100644 --- a/lib/puppet/configurer.rb +++ b/lib/puppet/configurer.rb @@ -44,7 +44,7 @@ class Puppet::Configurer rescue => detail Puppet.log_exception(detail, "Removing corrupt state file #{Puppet[:statefile]}: #{detail}") begin - ::File.unlink(Puppet[:statefile]) + Puppet::FileSystem::File.unlink(Puppet[:statefile]) retry rescue => detail raise Puppet::Error.new("Cannot remove #{Puppet[:statefile]}: #{detail}") @@ -157,7 +157,9 @@ class Puppet::Configurer query_options = nil end end - rescue Puppet::Error, Net::HTTPError => detail + rescue SystemExit,NoMemoryError + raise + rescue Exception => detail Puppet.warning("Unable to fetch my node definition, but the agent run will continue:") Puppet.warning(detail) end @@ -199,7 +201,7 @@ class Puppet::Configurer # Between Puppet runs we need to forget the cached values. This lets us # pick up on new functions installed by gems or new modules being added # without the daemon being restarted. - Thread.current[:env_module_directories] = nil + $env_module_directories = nil Puppet::Util::Log.close(report) send_report(report) diff --git a/lib/puppet/configurer/downloader.rb b/lib/puppet/configurer/downloader.rb index f9aa161ca..56de33608 100644 --- a/lib/puppet/configurer/downloader.rb +++ b/lib/puppet/configurer/downloader.rb @@ -58,7 +58,9 @@ class Puppet::Configurer::Downloader :backup => false, :noop => false }.merge( - Puppet.features.microsoft_windows? ? {} : + Puppet.features.microsoft_windows? ? { + :source_permissions => :ignore + } : { :owner => Process.uid, :group => Process.gid diff --git a/lib/puppet/configurer/plugin_handler.rb b/lib/puppet/configurer/plugin_handler.rb index f0a93eef4..87a307b70 100644 --- a/lib/puppet/configurer/plugin_handler.rb +++ b/lib/puppet/configurer/plugin_handler.rb @@ -11,6 +11,16 @@ module Puppet::Configurer::PluginHandler Puppet[:pluginsignore], @environment ) + if Puppet.features.external_facts? + plugin_fact_downloader = Puppet::Configurer::Downloader.new( + "pluginfacts", + Puppet[:pluginfactdest], + Puppet[:pluginfactsource], + Puppet[:pluginsignore], + @environment + ) + plugin_fact_downloader.evaluate + end plugin_downloader.evaluate Puppet::Util::Autoload.reload_changed diff --git a/lib/puppet/confine.rb b/lib/puppet/confine.rb new file mode 100644 index 000000000..41b5aa0e6 --- /dev/null +++ b/lib/puppet/confine.rb @@ -0,0 +1,80 @@ +# The class that handles testing whether our providers +# actually work or not. +require 'puppet/util' + +class Puppet::Confine + include Puppet::Util + + @tests = {} + + class << self + attr_accessor :name + end + + def self.inherited(klass) + name = klass.to_s.split("::").pop.downcase.to_sym + raise "Test #{name} is already defined" if @tests.include?(name) + + klass.name = name + + @tests[name] = klass + end + + def self.test(name) + unless @tests[name] + begin + require "puppet/confine/#{name}" + rescue LoadError => detail + unless detail.to_s =~ /No such file|cannot load such file/i + warn "Could not load confine test '#{name}': #{detail}" + end + # Could not find file + end + end + @tests[name] + end + + attr_reader :values + + # Mark that this confine is used for testing binary existence. + attr_accessor :for_binary + def for_binary? + for_binary + end + + # Used for logging. + attr_accessor :label + + def initialize(values) + values = [values] unless values.is_a?(Array) + @values = values + end + + # Provide a hook for the message when there's a failure. + def message(value) + "" + end + + # Collect the results of all of them. + def result + values.collect { |value| pass?(value) } + end + + # Test whether our confine matches. + def valid? + values.each do |value| + unless pass?(value) + Puppet.debug(label + ": " + message(value)) + return false + end + end + + return true + ensure + reset + end + + # Provide a hook for subclasses. + def reset + end +end diff --git a/lib/puppet/provider/confine/exists.rb b/lib/puppet/confine/exists.rb index 09f94dfd9..95a315514 100644 --- a/lib/puppet/provider/confine/exists.rb +++ b/lib/puppet/confine/exists.rb @@ -1,12 +1,12 @@ -require 'puppet/provider/confine' +require 'puppet/confine' -class Puppet::Provider::Confine::Exists < Puppet::Provider::Confine +class Puppet::Confine::Exists < Puppet::Confine def self.summarize(confines) confines.inject([]) { |total, confine| total + confine.summary } end def pass?(value) - value && (for_binary? ? which(value) : FileTest.exist?(value)) + value && (for_binary? ? which(value) : Puppet::FileSystem::File.exist?(value)) end def message(value) diff --git a/lib/puppet/provider/confine/false.rb b/lib/puppet/confine/false.rb index 1c11dd40f..3b664df3b 100644 --- a/lib/puppet/provider/confine/false.rb +++ b/lib/puppet/confine/false.rb @@ -1,6 +1,6 @@ -require 'puppet/provider/confine' +require 'puppet/confine' -class Puppet::Provider::Confine::False < Puppet::Provider::Confine +class Puppet::Confine::False < Puppet::Confine def self.summarize(confines) confines.inject(0) { |count, confine| count + confine.summary } end diff --git a/lib/puppet/provider/confine/feature.rb b/lib/puppet/confine/feature.rb index b223b8b11..5aff48a90 100644 --- a/lib/puppet/provider/confine/feature.rb +++ b/lib/puppet/confine/feature.rb @@ -1,6 +1,6 @@ -require 'puppet/provider/confine' +require 'puppet/confine' -class Puppet::Provider::Confine::Feature < Puppet::Provider::Confine +class Puppet::Confine::Feature < Puppet::Confine def self.summarize(confines) confines.collect { |c| c.values }.flatten.uniq.find_all { |value| ! confines[0].pass?(value) } end diff --git a/lib/puppet/provider/confine/true.rb b/lib/puppet/confine/true.rb index 559f2675f..b7e347045 100644 --- a/lib/puppet/provider/confine/true.rb +++ b/lib/puppet/confine/true.rb @@ -1,6 +1,6 @@ -require 'puppet/provider/confine' +require 'puppet/confine' -class Puppet::Provider::Confine::True < Puppet::Provider::Confine +class Puppet::Confine::True < Puppet::Confine def self.summarize(confines) confines.inject(0) { |count, confine| count + confine.summary } end diff --git a/lib/puppet/provider/confine/variable.rb b/lib/puppet/confine/variable.rb index af8e5d314..9102d4577 100644 --- a/lib/puppet/provider/confine/variable.rb +++ b/lib/puppet/confine/variable.rb @@ -1,10 +1,10 @@ -require 'puppet/provider/confine' +require 'puppet/confine' # Require a specific value for a variable, either a Puppet setting # or a Facter value. This class is a bit weird because the name # is set explicitly by the ConfineCollection class -- from this class, # it's not obvious how the name would ever get set. -class Puppet::Provider::Confine::Variable < Puppet::Provider::Confine +class Puppet::Confine::Variable < Puppet::Confine # Provide a hash summary of failing confines -- the key of the hash # is the name of the confine, and the value is the missing yet required values. # Only returns failed values, not all required values. diff --git a/lib/puppet/provider/confine_collection.rb b/lib/puppet/confine_collection.rb index 46fd3baaf..7a2eba8b4 100644 --- a/lib/puppet/provider/confine_collection.rb +++ b/lib/puppet/confine_collection.rb @@ -1,8 +1,8 @@ # Manage a collection of confines, returning a boolean or # helpful information. -require 'puppet/provider/confine' +require 'puppet/confine' -class Puppet::Provider::ConfineCollection +class Puppet::ConfineCollection def confine(hash) if hash.include?(:for_binary) for_binary = true @@ -11,11 +11,11 @@ class Puppet::Provider::ConfineCollection for_binary = false end hash.each do |test, values| - if klass = Puppet::Provider::Confine.test(test) + if klass = Puppet::Confine.test(test) @confines << klass.new(values) @confines[-1].for_binary = true if for_binary else - confine = Puppet::Provider::Confine.test(:variable).new(values) + confine = Puppet::Confine.test(:variable).new(values) confine.name = test @confines << confine end diff --git a/lib/puppet/provider/confiner.rb b/lib/puppet/confiner.rb index a1a2fa593..57176e799 100644 --- a/lib/puppet/provider/confiner.rb +++ b/lib/puppet/confiner.rb @@ -1,10 +1,10 @@ -require 'puppet/provider/confine_collection' +require 'puppet/confine_collection' # The Confiner module contains methods for managing a Provider's confinement (suitability under given # conditions). The intent is to include this module in an object where confinement management is wanted. # It lazily adds an instance variable `@confine_collection` to the object where it is included. # -module Puppet::Provider::Confiner +module Puppet::Confiner # Confines a provider to be suitable only under the given conditions. # The hash describes a confine using mapping from symbols to values or predicate code. # @@ -26,11 +26,11 @@ module Puppet::Provider::Confiner confine_collection.confine(hash) end - # @return [Puppet::Provider::ConfineCollection] the collection of confines + # @return [Puppet::ConfineCollection] the collection of confines # @api private # def confine_collection - @confine_collection ||= Puppet::Provider::ConfineCollection.new(self.to_s) + @confine_collection ||= Puppet::ConfineCollection.new(self.to_s) end # Checks whether this implementation is suitable for the current platform (or returns a summary diff --git a/lib/puppet/daemon.rb b/lib/puppet/daemon.rb index 562602418..c931184f1 100755..100644 --- a/lib/puppet/daemon.rb +++ b/lib/puppet/daemon.rb @@ -149,16 +149,12 @@ class Puppet::Daemon # Create a pidfile for our daemon, so we can be stopped and others # don't try to start. def create_pidfile - Puppet::Util.synchronize_on(Puppet.run_mode.name,Sync::EX) do - raise "Could not create PID file: #{@pidfile.file_path}" unless @pidfile.lock - end + raise "Could not create PID file: #{@pidfile.file_path}" unless @pidfile.lock end # Remove the pid file for our daemon. def remove_pidfile - Puppet::Util.synchronize_on(Puppet.run_mode.name,Sync::EX) do - @pidfile.unlock - end + @pidfile.unlock end def run_event_loop diff --git a/lib/puppet/data_binding.rb b/lib/puppet/data_binding.rb index ce77a9bf8..76cc7a6f3 100644 --- a/lib/puppet/data_binding.rb +++ b/lib/puppet/data_binding.rb @@ -2,39 +2,11 @@ require 'puppet/indirector' # A class for managing data lookups class Puppet::DataBinding + class LookupError < Puppet::Error; end - # Set up indirection, so that data can be looked for in the complier + # Set up indirection, so that data can be looked for in the compiler extend Puppet::Indirector indirects(:data_binding, :terminus_setting => :data_binding_terminus, :doc => "Where to find external data bindings.") - - # A class that acts just enough like a Puppet::Parser::Scope to - # fool Hiera's puppet backend. This class doesn't actually do anything - # but it does allow people to use the puppet backend with the hiera - # data bindings withough causing problems. - class Variables - FAKE_RESOURCE = Struct.new(:name).new("fake").freeze - FAKE_CATALOG = Struct.new(:classes).new([].freeze).freeze - - def initialize(variable_bindings) - @variable_bindings = variable_bindings - end - - def [](name) - @variable_bindings[name] - end - - def resource - FAKE_RESOURCE - end - - def catalog - FAKE_CATALOG - end - - def function_include(name) - # noop - end - end end diff --git a/lib/puppet/defaults.rb b/lib/puppet/defaults.rb index 7a161fc57..9b9502f06 100644 --- a/lib/puppet/defaults.rb +++ b/lib/puppet/defaults.rb @@ -20,23 +20,27 @@ module Puppet :confdir => { :default => nil, :type => :directory, - :desc => - "The main Puppet configuration directory. The default for this setting is calculated based on the user. If the process\n" + - "is running as root or the user that Puppet is supposed to run as, it defaults to a system directory, but if it's running as any other user,\n" + - "it defaults to being in the user's home directory.", + :desc => "The main Puppet configuration directory. The default for this setting + is calculated based on the user. If the process is running as root or + the user that Puppet is supposed to run as, it defaults to a system + directory, but if it's running as any other user, it defaults to being + in the user's home directory.", }, :vardir => { :default => nil, :type => :directory, - :desc => "Where Puppet stores dynamic and growing data. The default for this setting is calculated specially, like `confdir`_.", + :owner => "service", + :group => "service", + :desc => "Where Puppet stores dynamic and growing data. The default for this + setting is calculated specially, like `confdir`_.", }, ### NOTE: this setting is usually being set to a symbol value. We don't officially have a ### setting type for that yet, but we might want to consider creating one. :name => { :default => nil, - :desc => "The name of the application, if we are running as one. The\n" + - "default is essentially $0 without the path or `.rb`.", + :desc => "The name of the application, if we are running as one. The + default is essentially $0 without the path or `.rb`.", } ) @@ -52,6 +56,15 @@ module Puppet ) define_settings(:main, + :priority => { + :default => nil, + :type => :priority, + :desc => "The scheduling priority of the process. Valid values are 'high', + 'normal', 'low', or 'idle', which are mapped to platform-specific + values. The priority can also be specified as an integer value and + will be passed as is, e.g. -5. Puppet must be running as a privileged + user in order to increase scheduling priority.", + }, :trace => { :default => false, :type => :boolean, @@ -70,9 +83,9 @@ module Puppet }, :syslogfacility => { :default => "daemon", - :desc => "What syslog facility to use when logging to\n" + - "syslog. Syslog has a fixed list of valid facilities, and you must\n" + - "choose one of those; you cannot just make one up." + :desc => "What syslog facility to use when logging to syslog. + Syslog has a fixed list of valid facilities, and you must + choose one of those; you cannot just make one up." }, :statedir => { :default => "$vardir/state", @@ -93,30 +106,33 @@ module Puppet :genconfig => { :default => false, :type => :boolean, - :desc => "Whether to just print a configuration to stdout and exit. Only makes\n" + - "sense when used interactively. Takes into account arguments specified\n" + - "on the CLI.", + :desc => "When true, causes Puppet applications to print an example config file + to stdout and exit. The example will include descriptions of each + setting, and the current (or default) value of each setting, + incorporating any settings overridden on the CLI (with the exception + of `genconfig` itself). This setting only makes sense when specified + on the command line as `--genconfig`.", }, :genmanifest => { :default => false, :type => :boolean, - :desc => "Whether to just print a manifest to stdout and exit. Only makes\n" + - "sense when used interactively. Takes into account arguments specified\n" + - "on the CLI.", + :desc => "Whether to just print a manifest to stdout and exit. Only makes + sense when specified on the command line as `--genmanifest`. Takes into account arguments specified + on the CLI.", }, :configprint => { :default => "", - :desc => "Print the value of a specific configuration setting. If the name of a\n" + - "setting is provided for this, then the value is printed and puppet\n" + - "exits. Comma-separate multiple values. For a list of all values,\n" + - "specify 'all'.", + :desc => "Print the value of a specific configuration setting. If the name of a + setting is provided for this, then the value is printed and puppet + exits. Comma-separate multiple values. For a list of all values, + specify 'all'.", }, :color => { :default => "ansi", :type => :string, - :desc => "Whether to use colors when logging to the console. Valid values are\n" + - "`ansi` (equivalent to `true`), `html`, and `false`, which produces no color.\n" + - "Defaults to false on Windows, as its console does not support ansi colors.", + :desc => "Whether to use colors when logging to the console. Valid values are + `ansi` (equivalent to `true`), `html`, and `false`, which produces no color. + Defaults to false on Windows, as its console does not support ansi colors.", }, :mkusers => { :default => false, @@ -131,14 +147,15 @@ module Puppet :onetime => { :default => false, :type => :boolean, - :desc => "Run the configuration once, rather than as a long-running\n" + - "daemon. This is useful for interactively running puppetd.", + :desc => "Perform one configuration run and exit, rather than spawning a long-running + daemon. This is useful for interactively running puppet agent, or + running puppet agent from cron.", :short => 'o', }, :path => { :default => "none", - :desc => "The shell search path. Defaults to whatever is inherited\n" + - "from the parent process.", + :desc => "The shell search path. Defaults to whatever is inherited + from the parent process.", :call_hook => :on_define_and_write, :hook => proc do |value| ENV["PATH"] = "" if ENV["PATH"].nil? @@ -153,11 +170,11 @@ module Puppet :libdir => { :type => :directory, :default => "$vardir/lib", - :desc => "An extra search path for Puppet. This is only useful\n" + - "for those files that Puppet will load on demand, and is only\n" + - "guaranteed to work for those cases. In fact, the autoload\n" + - "mechanism is responsible for making sure this directory\n" + - "is in Ruby's search path\n", + :desc => "An extra search path for Puppet. This is only useful + for those files that Puppet will load on demand, and is only + guaranteed to work for those cases. In fact, the autoload + mechanism is responsible for making sure this directory + is in Ruby's search path\n", :call_hook => :on_initialize_and_write, :hook => proc do |value| $LOAD_PATH.delete(@oldlibdir) if defined?(@oldlibdir) and $LOAD_PATH.include?(@oldlibdir) @@ -168,41 +185,44 @@ module Puppet :ignoreimport => { :default => false, :type => :boolean, - :desc => "If true, allows the parser to continue without requiring\n" + - "all files referenced with `import` statements to exist. This setting was primarily\n" + - "designed for use with commit hooks for parse-checking.", + :desc => "If true, allows the parser to continue without requiring + all files referenced with `import` statements to exist. This setting was primarily + designed for use with commit hooks for parse-checking.", }, :environment => { :default => "production", - :desc => "The environment Puppet is running in. For clients\n" + - "(e.g., `puppet agent`) this determines the environment itself, which\n" + - "is used to find modules and much more. For servers (i.e., `puppet master`)\n" + - "this provides the default environment for nodes we know nothing about." + :desc => "The environment Puppet is running in. For clients + (e.g., `puppet agent`) this determines the environment itself, which + is used to find modules and much more. For servers (i.e., `puppet master`) + this provides the default environment for nodes we know nothing about." }, :diff_args => { :default => default_diffargs, - :desc => "Which arguments to pass to the diff command when printing differences between\n" + - "files. The command to use can be chosen with the `diff` setting.", + :desc => "Which arguments to pass to the diff command when printing differences between + files. The command to use can be chosen with the `diff` setting.", }, :diff => { :default => (Puppet.features.microsoft_windows? ? "" : "diff"), - :desc => "Which diff command to use when printing differences between files. This setting\n" + - "has no default value on Windows, as standard `diff` is not available, but Puppet can use many\n" + - "third-party diff tools.", + :desc => "Which diff command to use when printing differences between files. This setting + has no default value on Windows, as standard `diff` is not available, but Puppet can use many + third-party diff tools.", }, :show_diff => { :type => :boolean, :default => false, - :desc => "Whether to log and report a contextual diff when files are being replaced. This causes\n" + - "partial file contents to pass through Puppet's normal logging and reporting system, so this setting\n" + - "should be used with caution if you are sending Puppet's reports to an insecure destination.\n" + - "This feature currently requires the `diff/lcs` Ruby library.", + :desc => "Whether to log and report a contextual diff when files are being replaced. + This causes partial file contents to pass through Puppet's normal + logging and reporting system, so this setting should be used with + caution if you are sending Puppet's reports to an insecure + destination. This feature currently requires the `diff/lcs` Ruby + library.", }, :daemonize => { :type => :boolean, :default => (Puppet.features.microsoft_windows? ? false : true), - :desc => "Whether to send the process into the background. This defaults to true on POSIX systems, - and to false on Windows (where Puppet currently cannot daemonize).", + :desc => "Whether to send the process into the background. This defaults + to true on POSIX systems, and to false on Windows (where Puppet + currently cannot daemonize).", :short => "D", :hook => proc do |value| if value and Puppet.features.microsoft_windows? @@ -212,10 +232,11 @@ module Puppet }, :maximum_uid => { :default => 4294967290, - :desc => "The maximum allowed UID. Some platforms use negative UIDs\n" + - "but then ship with tools that do not know how to handle signed ints, so the UIDs show up as\n" + - "huge numbers that can then not be fed back into the system. This is a hackish way to fail in a\n" + - "slightly more useful way when that happens.", + :desc => "The maximum allowed UID. Some platforms use negative UIDs + but then ship with tools that do not know how to handle signed ints, + so the UIDs show up as huge numbers that can then not be fed back into + the system. This is a hackish way to fail in a slightly more useful + way when that happens.", }, :route_file => { :default => "$confdir/routes.yaml", @@ -385,14 +406,21 @@ module Puppet :freeze_main => { :default => false, :type => :boolean, - :desc => "Freezes the 'main' class, disallowing any code to be added to it. This\n" + - "essentially means that you can't have any code outside of a node, class, or definition other\n" + - "than in the site manifest.", + :desc => "Freezes the 'main' class, disallowing any code to be added to it. This + essentially means that you can't have any code outside of a node, + class, or definition other than in the site manifest.", }, :stringify_facts => { :default => true, :type => :boolean, - :desc => "Flatten fact values to strings using #to_s. Means you can't have arrays or hashes as fact values.", + :desc => "Flatten fact values to strings using #to_s. Means you can't have arrays or + hashes as fact values.", + }, + :trusted_node_data => { + :default => false, + :type => :boolean, + :desc => "Stores trusted node data in a hash called $trusted. + When true also prevents $trusted from being overridden in any scope.", } ) Puppet.define_settings(:module_tool, @@ -472,6 +500,36 @@ have a pool of multiple load balanced masters, or for the same master to respond on two physically separate networks under different names. EOT }, + :csr_attributes => { + :default => "$confdir/csr_attributes.yaml", + :type => :file, + :desc => <<EOT +An optional file containing custom attributes to add to certificate signing +requests (CSRs). You should ensure that this file does not exist on your CA +puppet master; if it does, unwanted certificate extensions may leak into +certificates created with the `puppet cert generate` command. + +If present, this file must be a YAML hash containing a `custom_attributes` key +and/or an `extension_requests` key. The value of each key must be a hash, where +each key is a valid OID and each value is an object that can be cast to a string. + +Custom attributes can be used by the CA when deciding whether to sign the +certificate, but are then discarded. Attribute OIDs can be any OID value except +the standard CSR attributes (i.e. attributes described in RFC 2985 section 5.4). +This is useful for embedding a pre-shared key for autosigning policy executables +(see the `autosign` setting), often by using the `1.2.840.113549.1.9.7` +("challenge password") OID. + +Extension requests will be permanently embedded in the final certificate. +Extension OIDs must be in the "ppRegCertExt" (`1.3.6.1.4.1.34380.1.1`) or +"ppPrivCertExt" (`1.3.6.1.4.1.34380.1.2`) OID arcs. The ppRegCertExt arc is +reserved for four of the most common pieces of data to embed: `pp_uuid` (`.1`), +`pp_instance_id` (`.2`), `pp_image_name` (`.3`), and `pp_preshared_key` (`.4`) +--- in the YAML file, these can be referred to by their short descriptive names +instead of their full OID. The ppPrivCertExt arc is unregulated, and can be used +for site-specific extensions. +EOT + }, :certdir => { :default => "$ssldir/certs", :type => :directory, @@ -558,19 +616,19 @@ EOT :type => :file, :mode => 0644, :owner => "service", - :desc => "Certificate authorities who issue server certificates. SSL servers will not be \n" << - "considered authentic unless they posses a certificate issued by an authority \n" << - "listed in this file. If this setting has no value then the Puppet master's CA \n" << - "certificate (localcacert) will be used." + :desc => "Certificate authorities who issue server certificates. SSL servers will not be + considered authentic unless they posses a certificate issued by an authority + listed in this file. If this setting has no value then the Puppet master's CA + certificate (localcacert) will be used." }, :ssl_server_ca_auth => { :type => :file, :mode => 0644, :owner => "service", - :desc => "Certificate authorities who issue client certificates. SSL clients will not be \n" << - "considered authentic unless they posses a certificate issued by an authority \n" << - "listed in this file. If this setting has no value then the Puppet master's CA \n" << - "certificate (localcacert) will be used." + :desc => "Certificate authorities who issue client certificates. SSL clients will not be + considered authentic unless they posses a certificate issued by an authority + listed in this file. If this setting has no value then the Puppet master's CA + certificate (localcacert) will be used." }, :hostcrl => { :default => "$ssldir/crl.pem", @@ -583,8 +641,9 @@ EOT :certificate_revocation => { :default => true, :type => :boolean, - :desc => "Whether certificate revocation should be supported by downloading a Certificate Revocation List (CRL) - to all clients. If enabled, CA chaining will almost definitely not work.", + :desc => "Whether certificate revocation should be supported by downloading a + Certificate Revocation List (CRL) + to all clients. If enabled, CA chaining will almost definitely not work.", }, :certificate_expire_warning => { :default => "60d", @@ -681,12 +740,32 @@ EOT }, :autosign => { :default => "$confdir/autosign.conf", - :type => :file, - :mode => 0644, - :desc => "Whether to enable autosign. Valid values are true (which - autosigns any key request, and is a very bad idea), false (which - never autosigns any key request), and the path to a file, which - uses that configuration file to determine which keys to sign."}, + :type => :autosign, + :desc => "Whether (and how) to autosign certificate requests. This setting + is only relevant on a puppet master acting as a certificate authority (CA). + + Valid values are true (autosigns all certificate requests; not recommended), + false (disables autosigning certificates), or the absolute path to a file. + + The file specified in this setting may be either a **configuration file** + or a **custom policy executable.** Puppet will automatically determine + what it is: If the Puppet user (see the `user` setting) can execute the + file, it will be treated as a policy executable; otherwise, it will be + treated as a config file. + + If a custom policy executable is configured, the CA puppet master will run it + every time it receives a CSR. The executable will be passed the subject CN of the + request _as a command line argument,_ and the contents of the CSR in PEM format + _on stdin._ It should exit with a status of 0 if the cert should be autosigned + and non-zero if the cert should not be autosigned. + + If a certificate request is not autosigned, it will persist for review. An admin + user can use the `puppet cert sign` command to manually sign it, or can delete + the request. + + For info on autosign configuration files, see + [the guide to Puppet's config files](http://docs.puppetlabs.com/guides/configuring.html).", + }, :allow_duplicate_certs => { :default => false, :type => :boolean, @@ -733,10 +812,10 @@ EOT :pidfile => { :type => :file, :default => "$rundir/${run_mode}.pid", - :desc => "The file containing the PID of a running process. " << - "This file is intended to be used by service management " << - "frameworks and monitoring systems to determine if a " << - "puppet process is still in the process table.", + :desc => "The file containing the PID of a running process. + This file is intended to be used by service management frameworks + and monitoring systems to determine if a puppet process is still in + the process table.", }, :bindaddress => { :default => "0.0.0.0", @@ -825,8 +904,9 @@ EOT :modulepath => { :default => "$confdir/modules#{File::PATH_SEPARATOR}/usr/share/puppet/modules", :type => :path, - :desc => "The search path for modules, as a list of directories separated by the system path separator character. " + - "(The POSIX path separator is ':', and the Windows path separator is ';'.)", + :desc => "The search path for modules, as a list of directories separated by the system + path separator character. (The POSIX path separator is ':', and the + Windows path separator is ';'.)", }, :ssl_client_header => { :default => "HTTP_X_CLIENT_DN", @@ -1025,6 +1105,12 @@ EOT :desc => "Boolean; whether puppet agent should ignore schedules. This is useful for initial puppet agent runs.", }, + :default_schedules => { + :default => true, + :type => :boolean, + :desc => "Boolean; whether to generate the default schedule resources. Setting this to + false is useful for keeping external report processors clean of skipped schedule resources.", + }, :puppetport => { :default => 8139, :desc => "Which port puppet agent listens on.", @@ -1114,49 +1200,50 @@ EOT :agent_catalog_run_lockfile => { :default => "$statedir/agent_catalog_run.lock", :type => :string, # (#2888) Ensure this file is not added to the settings catalog. - :desc => "A lock file to indicate that a puppet agent catalog run is currently in progress. " + - "The file contains the pid of the process that holds the lock on the catalog run.", + :desc => "A lock file to indicate that a puppet agent catalog run is currently in progress. + The file contains the pid of the process that holds the lock on the catalog run.", }, :agent_disabled_lockfile => { :default => "$statedir/agent_disabled.lock", - :type => :file, - :desc => "A lock file to indicate that puppet agent runs have been administratively disabled. File contains a JSON object with state information.", + :type => :file, + :desc => "A lock file to indicate that puppet agent runs have been administratively + disabled. File contains a JSON object with state information.", }, :usecacheonfailure => { :default => true, :type => :boolean, :desc => "Whether to use the cached configuration when the remote - configuration will not compile. This option is useful for testing - new configurations, where you want to fix the broken configuration - rather than reverting to a known-good one.", + configuration will not compile. This option is useful for testing + new configurations, where you want to fix the broken configuration + rather than reverting to a known-good one.", }, :use_cached_catalog => { :default => false, :type => :boolean, :desc => "Whether to only use the cached catalog rather than compiling a new catalog - on every run. Puppet can be run with this enabled by default and then selectively - disabled when a recompile is desired.", + on every run. Puppet can be run with this enabled by default and then selectively + disabled when a recompile is desired.", }, :ignoremissingtypes => { :default => false, :type => :boolean, :desc => "Skip searching for classes and definitions that were missing during a - prior compilation. The list of missing objects is maintained per-environment and - persists until the environment is cleared or the master is restarted.", + prior compilation. The list of missing objects is maintained per-environment and + persists until the environment is cleared or the master is restarted.", }, :ignorecache => { :default => false, :type => :boolean, :desc => "Ignore cache and always recompile the configuration. This is - useful for testing new configurations, where the local cache may in - fact be stale even if the timestamps are up to date - if the facts - change or if the server changes.", + useful for testing new configurations, where the local cache may in + fact be stale even if the timestamps are up to date - if the facts + change or if the server changes.", }, :dynamicfacts => { :default => "memorysize,memoryfree,swapsize,swapfree", :desc => "(Deprecated) Facts that are dynamic; these facts will be ignored when deciding whether - changed facts should result in a recompile. Multiple facts should be - comma-separated.", + changed facts should result in a recompile. Multiple facts should be + comma-separated.", :hook => proc { |value| if value Puppet.deprecation_warning "The dynamicfacts setting is deprecated and will be ignored." @@ -1167,13 +1254,13 @@ EOT :default => "$runinterval", :type => :duration, :desc => "The maximum time to delay before runs. Defaults to being the same as the - run interval. #{AS_DURATION}", + run interval. #{AS_DURATION}", }, :splay => { :default => false, :type => :boolean, :desc => "Whether to sleep for a pseudo-random (but consistent) amount of time before - a run.", + a run.", }, :clientbucketdir => { :default => "$vardir/clientbucket", @@ -1185,8 +1272,8 @@ EOT :default => "2m", :type => :duration, :desc => "How long the client should wait for the configuration to be retrieved - before considering it a failure. This can help reduce flapping if too - many clients contact the server at one time. #{AS_DURATION}", + before considering it a failure. This can help reduce flapping if too + many clients contact the server at one time. #{AS_DURATION}", }, :report_server => { :default => "$server", @@ -1225,8 +1312,8 @@ EOT :default => false, :type => :boolean, :desc => "Whether to create dot graph files for the different - configuration graphs. These dot files can be interpreted by tools - like OmniGraffle or dot (which is part of ImageMagick).", + configuration graphs. These dot files can be interpreted by tools + like OmniGraffle or dot (which is part of ImageMagick).", }, :graphdir => { :default => "$statedir/graphs", @@ -1237,11 +1324,12 @@ EOT :default => false, :type => :boolean, :desc => "Allow http compression in REST communication with the master. - This setting might improve performance for agent -> master communications over slow WANs. - Your puppet master needs to support compression (usually by activating some settings in a reverse-proxy - in front of the puppet master, which rules out webrick). - It is harmless to activate this settings if your master doesn't support - compression, but if it supports it, this setting might reduce performance on high-speed LANs.", + This setting might improve performance for agent -> master + communications over slow WANs. Your puppet master needs to support + compression (usually by activating some settings in a reverse-proxy in + front of the puppet master, which rules out webrick). It is harmless to + activate this settings if your master doesn't support compression, but + if it supports it, this setting might reduce performance on high-speed LANs.", }, :waitforcert => { :default => "2m", @@ -1310,6 +1398,15 @@ EOT is used for retrieval, so anything that is a valid file source can be used here.", }, + :pluginfactdest => { + :type => :directory, + :default => "$vardir/facts.d", + :desc => "Where Puppet should store external facts that are being handled by pluginsync", + }, + :pluginfactsource => { + :default => "puppet://$server/pluginfacts", + :desc => "Where to retrieve external facts for pluginsync", + }, :pluginsync => { :default => true, :type => :boolean, @@ -1330,7 +1427,8 @@ EOT :type => :path, :default => "$vardir/lib/facter#{File::PATH_SEPARATOR}$vardir/facts", :desc => "Where Puppet should look for facts. Multiple directories should - be separated by the system path separator character. (The POSIX path separator is ':', and the Windows path separator is ';'.)", + be separated by the system path separator character. (The POSIX path + separator is ':', and the Windows path separator is ';'.)", :call_hook => :on_initialize_and_write, # Call our hook with the default value, so we always get the value added to facter. :hook => proc { |value| Facter.search(value) if Facter.respond_to?(:search) }} @@ -1400,27 +1498,27 @@ EOT :dbport => { :default => "", :desc => "The database password for caching. Only - used when networked databases are used. #{STORECONFIGS_ONLY}", + used when networked databases are used. #{STORECONFIGS_ONLY}", }, :dbuser => { :default => "puppet", :desc => "The database user for caching. Only - used when networked databases are used. #{STORECONFIGS_ONLY}", + used when networked databases are used. #{STORECONFIGS_ONLY}", }, :dbpassword => { :default => "puppet", :desc => "The database password for caching. Only - used when networked databases are used. #{STORECONFIGS_ONLY}", + used when networked databases are used. #{STORECONFIGS_ONLY}", }, :dbconnections => { :default => '', :desc => "The number of database connections for networked - databases. Will be ignored unless the value is a positive integer. #{STORECONFIGS_ONLY}", + databases. Will be ignored unless the value is a positive integer. #{STORECONFIGS_ONLY}", }, :dbsocket => { :default => "", :desc => "The database socket location. Only used when networked - databases are used. Will be ignored if the value is an empty string. #{STORECONFIGS_ONLY}", + databases are used. Will be ignored if the value is an empty string. #{STORECONFIGS_ONLY}", }, :railslog => { :default => "$logdir/rails.log", @@ -1434,8 +1532,8 @@ EOT :rails_loglevel => { :default => "info", :desc => "The log level for Rails connections. The value must be - a valid log level within Rails. Production environments normally use `info` - and other environments normally use `debug`. #{STORECONFIGS_ONLY}", + a valid log level within Rails. Production environments normally use `info` + and other environments normally use `debug`. #{STORECONFIGS_ONLY}", } ) @@ -1445,7 +1543,7 @@ EOT :couchdb_url => { :default => "http://127.0.0.1:5984/puppet", :desc => "The url where the puppet couchdb database will be created. - Only used when `facts_terminus` is set to `couch`.", + Only used when `facts_terminus` is set to `couch`.", } ) @@ -1454,15 +1552,15 @@ EOT :tags => { :default => "", :desc => "Tags to use to find resources. If this is set, then - only resources tagged with the specified tags will be applied. - Values must be comma-separated.", + only resources tagged with the specified tags will be applied. + Values must be comma-separated.", }, :evaltrace => { :default => false, :type => :boolean, :desc => "Whether each resource should log when it is - being evaluated. This allows you to interactively see exactly - what is being done.", + being evaluated. This allows you to interactively see exactly + what is being done.", }, :summarize => { :default => false, @@ -1476,13 +1574,13 @@ EOT :external_nodes => { :default => "none", :desc => "An external command that can produce node information. The command's output - must be a YAML dump of a hash, and that hash must have a `classes` key and/or - a `parameters` key, where `classes` is an array or hash and - `parameters` is a hash. For unknown nodes, the command should - exit with a non-zero exit code. + must be a YAML dump of a hash, and that hash must have a `classes` key and/or + a `parameters` key, where `classes` is an array or hash and + `parameters` is a hash. For unknown nodes, the command should + exit with a non-zero exit code. - This command makes it straightforward to store your node mapping - information in other data sources like databases.", + This command makes it straightforward to store your node mapping + information in other data sources like databases.", } ) @@ -1492,15 +1590,15 @@ EOT :default => false, :type => :boolean, :desc => "Whether SSL should be used when searching for nodes. - Defaults to false because SSL usually requires certificates - to be set up on the client side.", + Defaults to false because SSL usually requires certificates + to be set up on the client side.", }, :ldaptls => { :default => false, :type => :boolean, :desc => "Whether TLS should be used when searching for nodes. - Defaults to false because TLS usually requires certificates - to be set up on the client side.", + Defaults to false because TLS usually requires certificates + to be set up on the client side.", }, :ldapserver => { :default => "ldap", @@ -1518,20 +1616,20 @@ EOT :ldapclassattrs => { :default => "puppetclass", :desc => "The LDAP attributes to use to define Puppet classes. Values - should be comma-separated.", + should be comma-separated.", }, :ldapstackedattrs => { :default => "puppetvar", :desc => "The LDAP attributes that should be stacked to arrays by adding - the values in all hierarchy elements of the tree. Values - should be comma-separated.", + the values in all hierarchy elements of the tree. Values + should be comma-separated.", }, :ldapattrs => { :default => "all", :desc => "The LDAP attributes to include when querying LDAP for nodes. All - returned attributes are set as variables in the top-level scope. - Multiple values should be comma-separated. The value 'all' returns - all attributes.", + returned attributes are set as variables in the top-level scope. + Multiple values should be comma-separated. The value 'all' returns + all attributes.", }, :ldapparentattr => { :default => "parentnode", @@ -1540,7 +1638,7 @@ EOT :ldapuser => { :default => "", :desc => "The user to use to connect to LDAP. Must be specified as a - full DN.", + full DN.", }, :ldappassword => { :default => "", @@ -1549,9 +1647,9 @@ EOT :ldapbase => { :default => "", :desc => "The search base for LDAP searches. It's impossible to provide - a meaningful default here, although the LDAP libraries might - have one already set. Generally, it should be the 'ou=Hosts' - branch under your main directory.", + a meaningful default here, although the LDAP libraries might + have one already set. Generally, it should be the 'ou=Hosts' + branch under your main directory.", } ) @@ -1560,13 +1658,13 @@ EOT :default => false, :type => :boolean, :desc => "Whether to store each client's configuration, including catalogs, facts, -and related data. This also enables the import and export of resources in -the Puppet language - a mechanism for exchange resources between nodes. + and related data. This also enables the import and export of resources in + the Puppet language - a mechanism for exchange resources between nodes. -By default this uses ActiveRecord and an SQL database to store and query -the data; this, in turn, will depend on Rails being available. + By default this uses ActiveRecord and an SQL database to store and query + the data; this, in turn, will depend on Rails being available. -You can adjust the backend using the storeconfigs_backend setting.", + You can adjust the backend using the storeconfigs_backend setting.", # Call our hook with the default value, so we always get the libdir set. :call_hook => :on_initialize_and_write, :hook => proc do |value| @@ -1587,8 +1685,8 @@ You can adjust the backend using the storeconfigs_backend setting.", :type => :terminus, :default => "active_record", :desc => "Configure the backend terminus used for StoreConfigs. -By default, this uses the ActiveRecord store, which directly talks to the -database from within the Puppet Master process." + By default, this uses the ActiveRecord store, which directly talks to the + database from within the Puppet Master process." } ) @@ -1597,57 +1695,63 @@ database from within the Puppet Master process." :default => "$vardir/templates", :type => :directory, :desc => "Where Puppet looks for template files. Can be a list of colon-separated - directories.", + directories.", }, :allow_variables_with_dashes => { :default => false, :desc => <<-'EOT' -Permit hyphens (`-`) in variable names and issue deprecation warnings about -them. This setting **should always be `false`;** setting it to `true` -will cause subtle and wide-ranging bugs. It will be removed in a future version. - -Hyphenated variables caused major problems in the language, but were allowed -between Puppet 2.7.3 and 2.7.14. If you used them during this window, we -apologize for the inconvenience --- you can temporarily set this to `true` -in order to upgrade, and can rename your variables at your leisure. Please -revert it to `false` after you have renamed all affected variables. -EOT + Permit hyphens (`-`) in variable names and issue deprecation warnings about + them. This setting **should always be `false`;** setting it to `true` + will cause subtle and wide-ranging bugs. It will be removed in a future version. + + Hyphenated variables caused major problems in the language, but were allowed + between Puppet 2.7.3 and 2.7.14. If you used them during this window, we + apologize for the inconvenience --- you can temporarily set this to `true` + in order to upgrade, and can rename your variables at your leisure. Please + revert it to `false` after you have renamed all affected variables. + EOT }, :parser => { :default => "current", :desc => <<-'EOT' -Selects the parser to use for parsing puppet manifests (in puppet DSL language/'.pp' files). -Available choices are 'current' (the default), and 'future'. + Selects the parser to use for parsing puppet manifests (in puppet DSL + language/'.pp' files). Available choices are `current` (the default), + and `future`. -The 'curent' parser means that the released version of the parser should be used. + The `curent` parser means that the released version of the parser should + be used. -The 'future' parser is a "time travel to the future" allowing early exposure to new language features. -What these fatures are will vary from release to release and they may be invididually configurable. + The `future` parser is a "time travel to the future" allowing early + exposure to new language features. What these fatures are will vary from + release to release and they may be invididually configurable. -Available Since Puppet 3.2. -EOT + Available Since Puppet 3.2. + EOT }, :max_errors => { :default => 10, :desc => <<-'EOT' -Sets the max number of logged/displayed parser validation errors in case multiple errors have been detected. -A value of 0 is the same as value 1. The count is per manifest. -EOT + Sets the max number of logged/displayed parser validation errors in case + multiple errors have been detected. A value of 0 is the same as value 1. + The count is per manifest. + EOT }, :max_warnings => { :default => 10, :desc => <<-'EOT' -Sets the max number of logged/displayed parser validation warnings in case multiple errors have been detected. -A value of 0 is the same as value 1. The count is per manifest. -EOT + Sets the max number of logged/displayed parser validation warnings in + case multiple errors have been detected. A value of 0 is the same as + value 1. The count is per manifest. + EOT }, :max_deprecations => { :default => 10, :desc => <<-'EOT' -Sets the max number of logged/displayed parser validation deprecation warnings in case multiple errors have been detected. -A value of 0 is the same as value 1. The count is per manifest. -EOT + Sets the max number of logged/displayed parser validation deprecation + warnings in case multiple errors have been detected. A value of 0 is the + same as value 1. The count is per manifest. + EOT } ) @@ -1656,7 +1760,7 @@ EOT :default => false, :type => :boolean, :desc => "Whether to document all resources when using `puppet doc` to - generate manifest documentation.", + generate manifest documentation.", } ) end diff --git a/lib/puppet/error.rb b/lib/puppet/error.rb index 15952662e..34da17219 100644 --- a/lib/puppet/error.rb +++ b/lib/puppet/error.rb @@ -56,5 +56,6 @@ module Puppet # An error class for when I don't know what happened. Automatically # prints a stack trace when in debug mode. class DevError < Puppet::Error + include ExternalFileError end end diff --git a/lib/puppet/external/lock.rb b/lib/puppet/external/lock.rb deleted file mode 100644 index 024fedf3d..000000000 --- a/lib/puppet/external/lock.rb +++ /dev/null @@ -1,63 +0,0 @@ -require 'thread' -require 'sync' - -# Gotten from: -# http://path.berkeley.edu/~vjoel/ruby/solaris-bug.rb - -# Extensions to the File class for exception-safe file locking in a -# environment with multiple user threads. - -# This is here because closing a file on solaris unlocks any locks that -# other threads might have. So we have to make sure that only the last -# reader thread closes the file. -# -# The hash maps inode number to a count of reader threads -$reader_count = Hash.new(0) - -class File - # Get an exclusive (i.e., write) lock on the file, and yield to the block. - # If the lock is not available, wait for it without blocking other ruby - # threads. - def lock_exclusive - if Thread.list.size == 1 - flock(LOCK_EX) - else - # ugly hack because waiting for a lock in a Ruby thread blocks the - # process - period = 0.001 - until flock(LOCK_EX|LOCK_NB) - sleep period - period *= 2 if period < 1 - end - end - - yield self - ensure - flush - flock(LOCK_UN) - end - - # Get a shared (i.e., read) lock on the file, and yield to the block. - # If the lock is not available, wait for it without blocking other ruby - # threads. - def lock_shared - if Thread.list.size == 1 - flock(LOCK_SH) - else - # ugly hack because waiting for a lock in a Ruby thread blocks the - # process - period = 0.001 - until flock(LOCK_SH|LOCK_NB) - sleep period - period *= 2 if period < 1 - end - end - - yield self - ensure - Thread.exclusive {flock(LOCK_UN) if $reader_count[self.stat.ino] == 1} - ## for solaris, no need to unlock here--closing does it - ## but this has no effect on the bug - end -end - diff --git a/lib/puppet/external/nagios.rb b/lib/puppet/external/nagios.rb index 2ed829198..582779441 100755..100644 --- a/lib/puppet/external/nagios.rb +++ b/lib/puppet/external/nagios.rb @@ -1,5 +1,3 @@ -#!/usr/bin/env ruby -w - #-------------------- # A script to retrieve hosts from ldap and create an importable # cfservd file from them diff --git a/lib/puppet/external/nagios/base.rb b/lib/puppet/external/nagios/base.rb index ce75f66ea..0aa50b411 100755..100644 --- a/lib/puppet/external/nagios/base.rb +++ b/lib/puppet/external/nagios/base.rb @@ -297,12 +297,13 @@ class Nagios::Base def to_s str = "define #{self.type} {\n" - self.each { |param,value| + @parameters.keys.sort.each { |param| + value = @parameters[param] str += %{\t%-30s %s\n} % [ param, if value.is_a? Array - value.join(",") + value.join(",").sub(';', '\;') else - value + value.sub(';', '\;') end ] } diff --git a/lib/puppet/external/nagios/grammar.ry b/lib/puppet/external/nagios/grammar.ry index 82f0a907c..d6bbc78ec 100644 --- a/lib/puppet/external/nagios/grammar.ry +++ b/lib/puppet/external/nagios/grammar.ry @@ -1,7 +1,7 @@ # vim: syntax=ruby class Nagios::Parser -token DEFINE NAME STRING PARAM LCURLY RCURLY VALUE RETURN COMMENT INLINECOMMENT +token DEFINE NAME PARAM LCURLY RCURLY VALUE RETURN rule decls: decl { return val[0] if val[0] } @@ -20,13 +20,9 @@ decls: decl { return val[0] if val[0] } decl: object { result = [val[0]] } | RETURN { result = nil } - | comment ; -comment: COMMENT RETURN { result = nil } - ; - -object: DEFINE NAME LCURLY RETURN vars RCURLY { +object: DEFINE NAME LCURLY returns vars RCURLY { result = Nagios::Base.create(val[1],val[4]) } ; @@ -40,146 +36,211 @@ vars: var } ; -var: PARAM VALUE icomment returns { result = {val[0] => val[1]} } +var: PARAM VALUE returns { result = {val[0] => val[1]} } ; -returns: RETURN - | returns RETURN - ; - -icomment: # nothing - | INLINECOMMENT +returns: RETURN + | RETURN returns ; end ----inner +require 'strscan' class ::Nagios::Parser::SyntaxError < RuntimeError; end def parse(src) - @src = src + if src.respond_to?("force_encoding") then + src.force_encoding("ASCII-8BIT") + end + @ss = StringScanner.new(src) - # state variables - @invar = false - @inobject = false - @done = false + # state variables + @in_parameter_value = false + @in_object_definition = false + @done = false - @line = 0 - @yydebug = true + @line = 1 + @yydebug = true - do_parse + do_parse end -# The lexer. Very simple. -def token - @src.sub!(/\A\n/,'') - if $& - @line += 1 - return [ :RETURN, "\n" ] - end +# This tokenizes the outside of object definitions, +# and detects when we start defining an object. +# We ignore whitespaces, comments and inline comments. +# We yield when finding newlines, the "define" keyword, +# the object name and the opening curly bracket. +def tokenize_outside_definitions + case + when (chars = @ss.skip(/[ \t]+/)) # ignore whitespace /\s+/ + ; - if @done - return nil - end - yytext = String.new + when (text = @ss.scan(/\#.*$/)) # ignore comments + ; + when (text = @ss.scan(/;.*$/)) # ignore inline comments + ; - # remove comments from this line - @src.sub!(/\A[ \t]*;.*\n/,"\n") - if $& - return [:INLINECOMMENT, ""] - end + when (text = @ss.scan(/\n/)) # newline + [:RETURN, text] - @src.sub!(/\A#.*\n/,"\n") - if $& - return [:COMMENT, ""] - end + when (text = @ss.scan(/\b(define)\b/)) # the "define" keyword + [:DEFINE, text] - @src.sub!(/#.*/,'') + when (text = @ss.scan(/[^{ \t\n]+/)) # the name of the object being defined (everything not an opening curly bracket or a separator) + [:NAME, text] - if @src.length == 0 - @done = true - return [false, '$'] - end + when (text = @ss.scan(/\{/)) # the opening curly bracket - we enter object definition + @in_object_definition = true + [:LCURLY, text] - if @invar - @src.sub!(/\A[ \t]+/,'') - @src.sub!(/\A([^;\n]+)(\n|;)/,'\2') - if $1 - yytext += $1 - end - @invar = false - return [:VALUE, yytext] - else - @src.sub!(/\A[\t ]*(\S+)([\t ]*|$)/,'') - if $1 - yytext = $1 - case yytext - when 'define' - #puts "got define" - return [:DEFINE, yytext] - when '{' - #puts "got {" - @inobject = true - return [:LCURLY, yytext] - else - unless @inobject - #puts "got type: #{yytext}" - if yytext =~ /\W/ - giveback = yytext.dup - giveback.sub!(/^\w+/,'') - #puts "giveback " + giveback - #puts "yytext " + yytext - yytext.sub!(/\W.*$/,'') - #puts "yytext " + yytext - #puts "all [#{giveback} #{yytext} #{orig}]" - @src = giveback + @src - end - return [:NAME, yytext] - else - if yytext == '}' - #puts "got closure: #{yytext}" - @inobject = false - return [:RCURLY, '}'] - end - - unless @invar - @invar = true - return [:PARAM, $1] - else - end - end - end - end + else + text = @ss.string[@ss.pos .. -1] + raise ScanError, "can not match: '#{text}'" + end # case +end + +# This tokenizes until we find the parameter name. +def tokenize_parameter_name + case + when (chars = @ss.skip(/[ \t]+/)) # ignore whitespace /\s+/ + ; + + when (text = @ss.scan(/\#.*$/)) # ignore comments + ; + + when (text = @ss.scan(/;.*$/)) # ignore inline comments + ; + + when (text = @ss.scan(/\n/)) # newline + [:RETURN, text] + + when (text = @ss.scan(/\}/)) # closing curly bracket : end of definition + @in_object_definition = false + [:RCURLY, text] + + when (not @in_parameter_value and (text = @ss.scan(/\S+/))) # This is the name of the parameter + @in_parameter_value = true + [:PARAM, text] + + else + text = @ss.string[@ss.pos .. -1] + raise ScanError, "can not match: '#{text}'" + end # case +end + +# This tokenizes the parameter value. +# There is a special handling for lines containing semicolons : +# - unescaped semicolons are line comments (and should stop parsing of the line) +# - escaped (with backslash \) semicolons should be kept in the parameter value (without the backslash) +def tokenize_parameter_value + case + when (chars = @ss.skip(/[ \t]+/)) # ignore whitespace /\s+/ + ; + + when (text = @ss.scan(/\#.*$/)) # ignore comments + ; + + when (text = @ss.scan(/\n/)) # newline + [:RETURN, text] + + when (text = @ss.scan(/.+$/)) # Value of parameter + @in_parameter_value = false + + # Special handling of inline comments (;) and escaped semicolons (\;) + + # We split the string on escaped semicolons (\;), + # Then we rebuild it as long as there are no inline comments (;) + # We join the rebuilt string with unescaped semicolons (on purpose) + array = text.split('\;', 0) + + text = "" + + array.each do |elt| + + # Now we split at inline comments. If we have more than 1 element in the array + # it means we have an inline comment, so we are able to stop parsing + # However we still want to reconstruct the string with its first part (before the comment) + linearray = elt.split(';', 0) + + # Let's reconstruct the string with a (unescaped) semicolon + if text != "" then + text += ';' + end + text += linearray[0] + + # Now we can stop + if linearray.length > 1 then + break + end end + + + # We strip the text to remove spaces between end of string and beginning of inline comment + [:VALUE, text.strip] + + else + text = @ss.string[@ss.pos .. -1] + raise ScanError, "can not match: '#{text}'" + end # case +end + +# This tokenizes inside an object definition. +# Two cases : parameter name and parameter value +def tokenize_inside_definitions + if @in_parameter_value + tokenize_parameter_value + else + tokenize_parameter_name + end +end + +# The lexer. Very simple. +def token + text = @ss.peek(1) + @line += 1 if text == "\n" + + token = if @in_object_definition + tokenize_inside_definitions + else + tokenize_outside_definitions + end + token end def next_token - token + return if @ss.eos? + + # skips empty actions + until _next_token = token or @ss.eos?; end + _next_token end def yydebug - 1 + 1 end def yywrap - 0 + 0 end def on_error(token, value, vstack ) - msg = "" - unless value.nil? - msg = "line #{@line}: syntax error at '#{value}'" - else - msg = "line #{@line}: syntax error at '#{token}'" - end - unless @src.size > 0 - msg = "line #{@line}: Unexpected end of file" - end - if token == '$end'.intern - puts "okay, this is silly" - else - raise ::Nagios::Parser::SyntaxError, msg - end + # text = @ss.string[@ss.pos .. -1] + text = @ss.peek(20) + msg = "" + unless value.nil? + msg = "line #{@line}: syntax error at value '#{value}' : #{text}" + else + msg = "line #{@line}: syntax error at token '#{token}' : #{text}" + end + if @ss.eos? + msg = "line #{@line}: Unexpected end of file" + end + if token == '$end'.intern + puts "okay, this is silly" + else + raise ::Nagios::Parser::SyntaxError, msg + end end diff --git a/lib/puppet/external/nagios/parser.rb b/lib/puppet/external/nagios/parser.rb index e89192b3a..9a82ffdb1 100644 --- a/lib/puppet/external/nagios/parser.rb +++ b/lib/puppet/external/nagios/parser.rb @@ -8,205 +8,269 @@ require 'racc/parser.rb' module Nagios class Parser < Racc::Parser -module_eval(<<'...end grammar.ry/module_eval...', 'grammar.ry', 57) +module_eval(<<'...end grammar.ry/module_eval...', 'grammar.ry', 49) +require 'strscan' class ::Nagios::Parser::SyntaxError < RuntimeError; end def parse(src) - @src = src + if src.respond_to?("force_encoding") then + src.force_encoding("ASCII-8BIT") + end + @ss = StringScanner.new(src) - # state variables - @invar = false - @inobject = false - @done = false + # state variables + @in_parameter_value = false + @in_object_definition = false + @done = false - @line = 0 - @yydebug = true + @line = 1 + @yydebug = true - do_parse + do_parse end -# The lexer. Very simple. -def token - @src.sub!(/\A\n/,'') - if $& - @line += 1 - return [ :RETURN, "\n" ] - end +# This tokenizes the outside of object definitions, +# and detects when we start defining an object. +# We ignore whitespaces, comments and inline comments. +# We yield when finding newlines, the "define" keyword, +# the object name and the opening curly bracket. +def tokenize_outside_definitions + case + when (chars = @ss.skip(/[ \t]+/)) # ignore whitespace /\s+/ + ; - if @done - return nil - end - yytext = String.new + when (text = @ss.scan(/\#.*$/)) # ignore comments + ; + when (text = @ss.scan(/;.*$/)) # ignore inline comments + ; - # remove comments from this line - @src.sub!(/\A[ \t]*;.*\n/,"\n") - if $& - return [:INLINECOMMENT, ""] - end + when (text = @ss.scan(/\n/)) # newline + [:RETURN, text] - @src.sub!(/\A#.*\n/,"\n") - if $& - return [:COMMENT, ""] - end + when (text = @ss.scan(/\b(define)\b/)) # the "define" keyword + [:DEFINE, text] - @src.sub!(/#.*/,'') + when (text = @ss.scan(/[^{ \t\n]+/)) # the name of the object being defined (everything not an opening curly bracket or a separator) + [:NAME, text] - if @src.length == 0 - @done = true - return [false, '$'] - end + when (text = @ss.scan(/\{/)) # the opening curly bracket - we enter object definition + @in_object_definition = true + [:LCURLY, text] - if @invar - @src.sub!(/\A[ \t]+/,'') - @src.sub!(/\A([^;\n]+)(\n|;)/,'\2') - if $1 - yytext += $1 - end - @invar = false - return [:VALUE, yytext] - else - @src.sub!(/\A[\t ]*(\S+)([\t ]*|$)/,'') - if $1 - yytext = $1 - case yytext - when 'define' - #puts "got define" - return [:DEFINE, yytext] - when '{' - #puts "got {" - @inobject = true - return [:LCURLY, yytext] - else - unless @inobject - #puts "got type: #{yytext}" - if yytext =~ /\W/ - giveback = yytext.dup - giveback.sub!(/^\w+/,'') - #puts "giveback " + giveback - #puts "yytext " + yytext - yytext.sub!(/\W.*$/,'') - #puts "yytext " + yytext - #puts "all [#{giveback} #{yytext} #{orig}]" - @src = giveback + @src - end - return [:NAME, yytext] - else - if yytext == '}' - #puts "got closure: #{yytext}" - @inobject = false - return [:RCURLY, '}'] - end - - unless @invar - @invar = true - return [:PARAM, $1] - else - end - end - end - end + else + text = @ss.string[@ss.pos .. -1] + raise ScanError, "can not match: '#{text}'" + end # case +end + +# This tokenizes until we find the parameter name. +def tokenize_parameter_name + case + when (chars = @ss.skip(/[ \t]+/)) # ignore whitespace /\s+/ + ; + + when (text = @ss.scan(/\#.*$/)) # ignore comments + ; + + when (text = @ss.scan(/;.*$/)) # ignore inline comments + ; + + when (text = @ss.scan(/\n/)) # newline + [:RETURN, text] + + when (text = @ss.scan(/\}/)) # closing curly bracket : end of definition + @in_object_definition = false + [:RCURLY, text] + + when (not @in_parameter_value and (text = @ss.scan(/\S+/))) # This is the name of the parameter + @in_parameter_value = true + [:PARAM, text] + + else + text = @ss.string[@ss.pos .. -1] + raise ScanError, "can not match: '#{text}'" + end # case +end + +# This tokenizes the parameter value. +# There is a special handling for lines containing semicolons : +# - unescaped semicolons are line comments (and should stop parsing of the line) +# - escaped (with backslash \) semicolons should be kept in the parameter value (without the backslash) +def tokenize_parameter_value + case + when (chars = @ss.skip(/[ \t]+/)) # ignore whitespace /\s+/ + ; + + when (text = @ss.scan(/\#.*$/)) # ignore comments + ; + + when (text = @ss.scan(/\n/)) # newline + [:RETURN, text] + + when (text = @ss.scan(/.+$/)) # Value of parameter + @in_parameter_value = false + + # Special handling of inline comments (;) and escaped semicolons (\;) + + # We split the string on escaped semicolons (\;), + # Then we rebuild it as long as there are no inline comments (;) + # We join the rebuilt string with unescaped semicolons (on purpose) + array = text.split('\;', 0) + + text = "" + + array.each do |elt| + + # Now we split at inline comments. If we have more than 1 element in the array + # it means we have an inline comment, so we are able to stop parsing + # However we still want to reconstruct the string with its first part (before the comment) + linearray = elt.split(';', 0) + + # Let's reconstruct the string with a (unescaped) semicolon + if text != "" then + text += ';' + end + text += linearray[0] + + # Now we can stop + if linearray.length > 1 then + break + end end + + + # We strip the text to remove spaces between end of string and beginning of inline comment + [:VALUE, text.strip] + + else + text = @ss.string[@ss.pos .. -1] + raise ScanError, "can not match: '#{text}'" + end # case +end + +# This tokenizes inside an object definition. +# Two cases : parameter name and parameter value +def tokenize_inside_definitions + if @in_parameter_value + tokenize_parameter_value + else + tokenize_parameter_name + end +end + +# The lexer. Very simple. +def token + text = @ss.peek(1) + @line += 1 if text == "\n" + + token = if @in_object_definition + tokenize_inside_definitions + else + tokenize_outside_definitions + end + token end def next_token - token + return if @ss.eos? + + # skips empty actions + until _next_token = token or @ss.eos?; end + _next_token end def yydebug - 1 + 1 end def yywrap - 0 + 0 end def on_error(token, value, vstack ) - msg = "" - unless value.nil? - msg = "line #{@line}: syntax error at '#{value}'" - else - msg = "line #{@line}: syntax error at '#{token}'" - end - unless @src.size > 0 - msg = "line #{@line}: Unexpected end of file" - end - if token == '$end'.intern - puts "okay, this is silly" - else - raise ::Nagios::Parser::SyntaxError, msg - end + # text = @ss.string[@ss.pos .. -1] + text = @ss.peek(20) + msg = "" + unless value.nil? + msg = "line #{@line}: syntax error at value '#{value}' : #{text}" + else + msg = "line #{@line}: syntax error at token '#{token}' : #{text}" + end + if @ss.eos? + msg = "line #{@line}: Unexpected end of file" + end + if token == '$end'.intern + puts "okay, this is silly" + else + raise ::Nagios::Parser::SyntaxError, msg + end end ...end grammar.ry/module_eval... ##### State transition tables begin ### racc_action_table = [ - 8, 17, 7, 18, 7, 14, 12, 13, 11, 4, - 6, 4, 6, 17, 10, 20, 22, 24, 25 ] + 8, 3, 3, 14, 12, 18, 10, 4, 4, 9, + 14, 12, 6, 19, 12 ] racc_action_check = [ - 1, 15, 1, 15, 0, 13, 8, 11, 7, 1, - 1, 0, 0, 14, 6, 17, 20, 21, 23 ] + 5, 0, 5, 13, 9, 13, 8, 0, 5, 6, + 11, 12, 3, 14, 19 ] racc_action_pointer = [ - 2, 0, nil, nil, nil, nil, 5, 5, 6, nil, - nil, 1, nil, -4, 8, -4, nil, 7, nil, nil, - 5, 8, nil, 9, nil, nil ] + -1, nil, nil, 9, nil, 0, 4, nil, 6, -4, + nil, 6, 3, -1, 6, nil, nil, nil, nil, 6, + nil ] racc_action_default = [ - -15, -15, -1, -3, -4, -5, -15, -15, -15, -2, - -6, -15, 26, -15, -15, -15, -8, -15, -7, -9, - -13, -15, -14, -10, -11, -12 ] + -11, -1, -3, -11, -4, -11, -11, -2, -11, -11, + 21, -11, -9, -11, -11, -6, -10, -7, -5, -11, + -8 ] racc_goto_table = [ - 16, 19, 2, 9, 1, 15, 21, 23 ] + 11, 1, 15, 16, 17, 13, 7, 5, nil, nil, + 20 ] racc_goto_check = [ - 6, 6, 2, 2, 1, 5, 7, 8 ] + 4, 2, 6, 4, 6, 5, 2, 1, nil, nil, + 4 ] racc_goto_pointer = [ - nil, 4, 2, nil, nil, -9, -14, -14, -14 ] + nil, 7, 1, nil, -9, -6, -9 ] racc_goto_default = [ - nil, nil, nil, 3, 5, nil, nil, nil, nil ] + nil, nil, nil, 2, nil, nil, nil ] racc_reduce_table = [ 0, 0, :racc_error, - 1, 13, :_reduce_1, - 2, 13, :_reduce_2, - 1, 14, :_reduce_3, - 1, 14, :_reduce_4, + 1, 10, :_reduce_1, + 2, 10, :_reduce_2, + 1, 11, :_reduce_3, + 1, 11, :_reduce_4, + 6, 12, :_reduce_5, 1, 14, :_reduce_none, - 2, 16, :_reduce_6, - 6, 15, :_reduce_7, - 1, 17, :_reduce_none, - 2, 17, :_reduce_9, - 4, 18, :_reduce_10, - 1, 20, :_reduce_none, - 2, 20, :_reduce_none, - 0, 19, :_reduce_none, - 1, 19, :_reduce_none ] + 2, 14, :_reduce_7, + 3, 15, :_reduce_8, + 1, 13, :_reduce_none, + 2, 13, :_reduce_none ] -racc_reduce_n = 15 +racc_reduce_n = 11 -racc_shift_n = 26 +racc_shift_n = 21 racc_token_table = { false => 0, :error => 1, :DEFINE => 2, :NAME => 3, - :STRING => 4, - :PARAM => 5, - :LCURLY => 6, - :RCURLY => 7, - :VALUE => 8, - :RETURN => 9, - :COMMENT => 10, - :INLINECOMMENT => 11 } + :PARAM => 4, + :LCURLY => 5, + :RCURLY => 6, + :VALUE => 7, + :RETURN => 8 } -racc_nt_base = 12 +racc_nt_base = 9 racc_use_result_var = true @@ -231,23 +295,18 @@ Racc_token_to_s_table = [ "error", "DEFINE", "NAME", - "STRING", "PARAM", "LCURLY", "RCURLY", "VALUE", "RETURN", - "COMMENT", - "INLINECOMMENT", "$start", "decls", "decl", "object", - "comment", + "returns", "vars", - "var", - "icomment", - "returns" ] + "var" ] Racc_debug_parser = false @@ -257,82 +316,72 @@ Racc_debug_parser = false module_eval(<<'.,.,', 'grammar.ry', 6) def _reduce_1(val, _values, result) - return val[0] if val[0] + return val[0] if val[0] result end .,., module_eval(<<'.,.,', 'grammar.ry', 8) def _reduce_2(val, _values, result) - if val[1].nil? - result = val[0] - else - if val[0].nil? - result = val[1] - else - result = [ val[0], val[1] ].flatten - end - end + if val[1].nil? + result = val[0] + else + if val[0].nil? + result = val[1] + else + result = [ val[0], val[1] ].flatten + end + end + result end .,., module_eval(<<'.,.,', 'grammar.ry', 20) def _reduce_3(val, _values, result) - result = [val[0]] + result = [val[0]] result end .,., module_eval(<<'.,.,', 'grammar.ry', 21) def _reduce_4(val, _values, result) - result = nil + result = nil result end .,., -# reduce 5 omitted - module_eval(<<'.,.,', 'grammar.ry', 25) - def _reduce_6(val, _values, result) - result = nil + def _reduce_5(val, _values, result) + result = Nagios::Base.create(val[1],val[4]) + result end .,., -module_eval(<<'.,.,', 'grammar.ry', 29) - def _reduce_7(val, _values, result) - result = Nagios::Base.create(val[1],val[4]) - result - end -.,., +# reduce 6 omitted -# reduce 8 omitted - -module_eval(<<'.,.,', 'grammar.ry', 35) - def _reduce_9(val, _values, result) - val[1].each {|p,v| - val[0][p] = v - } - result = val[0] +module_eval(<<'.,.,', 'grammar.ry', 31) + def _reduce_7(val, _values, result) + val[1].each {|p,v| + val[0][p] = v + } + result = val[0] + result end .,., -module_eval(<<'.,.,', 'grammar.ry', 42) - def _reduce_10(val, _values, result) - result = {val[0] => val[1]} +module_eval(<<'.,.,', 'grammar.ry', 38) + def _reduce_8(val, _values, result) + result = {val[0] => val[1]} result end .,., -# reduce 11 omitted - -# reduce 12 omitted - -# reduce 13 omitted +# reduce 9 omitted -# reduce 14 omitted +# reduce 10 omitted def _reduce_none(val, _values, result) val[0] diff --git a/lib/puppet/external/pson/pure/parser.rb b/lib/puppet/external/pson/pure/parser.rb index f14e6161c..bcdaf7b64 100644 --- a/lib/puppet/external/pson/pure/parser.rb +++ b/lib/puppet/external/pson/pure/parser.rb @@ -118,7 +118,7 @@ module PSON else raise TypeError, "#{source.inspect} is not like a string" end - if defined?(::Encoding) + if supports_encodings?(source) if source.encoding == ::Encoding::ASCII_8BIT b = source[0, 4].bytes.to_a source = @@ -157,6 +157,14 @@ module PSON source end + def supports_encodings?(string) + # Some modules, such as REXML on 1.8.7 (see #22804) can actually create + # a top-level Encoding constant when they are misused. Therefore + # checking for just that constant is not enough, so we'll be a bit more + # robust about if we can actually support encoding transformations. + string.respond_to?(:encoding) && defined?(::Encoding) + end + # Unescape characters in strings. UNESCAPE_MAP = Hash.new { |h, k| h[k] = k.chr } diff --git a/lib/puppet/face/file/store.rb b/lib/puppet/face/file/store.rb index 6b145b8db..76ebbc15b 100644 --- a/lib/puppet/face/file/store.rb +++ b/lib/puppet/face/file/store.rb @@ -11,7 +11,7 @@ Puppet::Face.define(:file, '0.0.1') do EOT when_invoked do |path, options| - file = Puppet::FileBucket::File.new(IO.binread(path)) + file = Puppet::FileBucket::File.new(Puppet::FileSystem::File.new(path).binread) Puppet::FileBucket::File.indirection.terminus_class = :file Puppet::FileBucket::File.indirection.save file diff --git a/lib/puppet/face/module/generate.rb b/lib/puppet/face/module/generate.rb index a30a2ff59..fc9ca5f74 100644 --- a/lib/puppet/face/module/generate.rb +++ b/lib/puppet/face/module/generate.rb @@ -17,16 +17,14 @@ Puppet::Face.define(:module, '1.0.0') do $ puppet module generate puppetlabs-ssh notice: Generating module at /Users/kelseyhightower/puppetlabs-ssh puppetlabs-ssh - puppetlabs-ssh/tests - puppetlabs-ssh/tests/init.pp - puppetlabs-ssh/spec - puppetlabs-ssh/spec/spec_helper.rb - puppetlabs-ssh/spec/spec.opts - puppetlabs-ssh/README puppetlabs-ssh/Modulefile - puppetlabs-ssh/metadata.json + puppetlabs-ssh/README puppetlabs-ssh/manifests puppetlabs-ssh/manifests/init.pp + puppetlabs-ssh/spec + puppetlabs-ssh/spec/spec_helper.rb + puppetlabs-ssh/tests + puppetlabs-ssh/tests/init.pp EOT arguments "<name>" diff --git a/lib/puppet/face/parser.rb b/lib/puppet/face/parser.rb index 3242faca7..e42919091 100644 --- a/lib/puppet/face/parser.rb +++ b/lib/puppet/face/parser.rb @@ -35,17 +35,27 @@ Puppet::Face.define(:parser, '0.0.1') do if files.empty? if not STDIN.tty? Puppet[:code] = STDIN.read - Puppet::Node::Environment.new(Puppet[:environment]).known_resource_types.clear + validate_manifest else files << Puppet[:manifest] Puppet.notice "No manifest specified. Validating the default manifest #{Puppet[:manifest]}" end end + missing_files = [] files.each do |file| + missing_files << file if ! Puppet::FileSystem::File.exist?(file) Puppet[:manifest] = file - Puppet::Node::Environment.new(Puppet[:environment]).known_resource_types.clear + validate_manifest end + raise Puppet::Error, "One or more file(s) specified did not exist:\n#{missing_files.collect {|f| " " * 3 + f + "\n"}}" if ! missing_files.empty? nil end end + + def validate_manifest + Puppet::Node::Environment.new(Puppet[:environment]).known_resource_types.clear + rescue => detail + Puppet.log_exception(detail) + exit(1) + end end diff --git a/lib/puppet/face/plugin.rb b/lib/puppet/face/plugin.rb index 76c79f38d..13ea32aa9 100644 --- a/lib/puppet/face/plugin.rb +++ b/lib/puppet/face/plugin.rb @@ -42,6 +42,12 @@ Puppet::Face.define(:plugin, '0.0.1') do Puppet[:plugindest], Puppet[:pluginsource], Puppet[:pluginsignore]).evaluate + if Puppet.features.external_facts? + Puppet::Configurer::Downloader.new("pluginfacts", + Puppet[:pluginfactdest], + Puppet[:pluginfactsource], + Puppet[:pluginsignore]).evaluate + end end when_rendering :console do |value| diff --git a/lib/puppet/feature/base.rb b/lib/puppet/feature/base.rb index dcf454298..e30aed313 100644 --- a/lib/puppet/feature/base.rb +++ b/lib/puppet/feature/base.rb @@ -70,3 +70,19 @@ Puppet.features.add(:sqlite, :libs => ["sqlite3"]) Puppet.features.add(:hiera, :libs => ["hiera"]) Puppet.features.add(:minitar, :libs => ["archive/tar/minitar"]) + +# We can manage symlinks +Puppet.features.add(:manages_symlinks) do + if ! Puppet::Util::Platform.windows? + true + else + begin + require 'Win32API' + Win32API.new('kernel32', 'CreateSymbolicLink', 'SSL', 'B') + true + rescue LoadError => err + Puppet.debug("CreateSymbolicLink is not available") + false + end + end +end diff --git a/lib/puppet/feature/external_facts.rb b/lib/puppet/feature/external_facts.rb new file mode 100644 index 000000000..1e2ad575e --- /dev/null +++ b/lib/puppet/feature/external_facts.rb @@ -0,0 +1,5 @@ +require 'facter/util/config' + +Puppet.features.add(:external_facts) { + Facter::Util::Config.respond_to?(:external_facts_dirs=) +} diff --git a/lib/puppet/feature/libuser.rb b/lib/puppet/feature/libuser.rb index bf34e20da..29a745de4 100644 --- a/lib/puppet/feature/libuser.rb +++ b/lib/puppet/feature/libuser.rb @@ -4,5 +4,5 @@ require 'puppet/util/libuser' Puppet.features.add(:libuser) { File.executable?("/usr/sbin/lgroupadd") and File.executable?("/usr/sbin/luseradd") and - File.exists?(Puppet::Util::Libuser.getconf) + Puppet::FileSystem::File.exist?(Puppet::Util::Libuser.getconf) } diff --git a/lib/puppet/feature/msgpack.rb b/lib/puppet/feature/msgpack.rb new file mode 100644 index 000000000..8c7b7f963 --- /dev/null +++ b/lib/puppet/feature/msgpack.rb @@ -0,0 +1 @@ +Puppet.features.add(:msgpack, :libs => ["msgpack"]) diff --git a/lib/puppet/feature/rails.rb b/lib/puppet/feature/rails.rb index 9e2ce4fe6..c1537effb 100644 --- a/lib/puppet/feature/rails.rb +++ b/lib/puppet/feature/rails.rb @@ -24,11 +24,11 @@ Puppet.features.add(:rails) do require 'active_record' require 'active_record/version' rescue LoadError - if FileTest.exists?("/usr/share/rails") + if Puppet::FileSystem::File.exist?("/usr/share/rails") count = 0 Dir.entries("/usr/share/rails").each do |dir| libdir = File.join("/usr/share/rails", dir, "lib") - if FileTest.exists?(libdir) and ! $LOAD_PATH.include?(libdir) + if Puppet::FileSystem::File.exist?(libdir) and ! $LOAD_PATH.include?(libdir) count += 1 $LOAD_PATH << libdir end diff --git a/lib/puppet/file_bucket/dipper.rb b/lib/puppet/file_bucket/dipper.rb index c675f09a4..5d4d9382a 100644 --- a/lib/puppet/file_bucket/dipper.rb +++ b/lib/puppet/file_bucket/dipper.rb @@ -31,8 +31,9 @@ class Puppet::FileBucket::Dipper # Back up a file to our bucket def backup(file) - raise(ArgumentError, "File #{file} does not exist") unless ::File.exist?(file) - contents = IO.binread(file) + file_handle = Puppet::FileSystem::File.new(file) + raise(ArgumentError, "File #{file} does not exist") unless file_handle.exist? + contents = file_handle.binread begin file_bucket_file = Puppet::FileBucket::File.new(contents, :bucket_path => @local_path) files_original_path = absolutize_path(file) @@ -65,8 +66,9 @@ class Puppet::FileBucket::Dipper # Restore the file def restore(file,sum) restore = true - if FileTest.exists?(file) - cursum = Digest::MD5.hexdigest(IO.binread(file)) + file_handle = Puppet::FileSystem::File.new(file) + if file_handle.exist? + cursum = Digest::MD5.hexdigest(file_handle.binread) # if the checksum has changed... # this might be extra effort @@ -79,8 +81,8 @@ class Puppet::FileBucket::Dipper if newcontents = getfile(sum) newsum = Digest::MD5.hexdigest(newcontents) changed = nil - if FileTest.exists?(file) and ! FileTest.writable?(file) - changed = ::File.stat(file).mode + if file_handle.exist? and ! file_handle.writable? + changed = Puppet::FileSystem::File.new(file).stat.mode ::File.chmod(changed | 0200, file) end ::File.open(file, ::File::WRONLY|::File::TRUNC|::File::CREAT) { |of| diff --git a/lib/puppet/file_bucket/file.rb b/lib/puppet/file_bucket/file.rb index 15e1cd76b..50e5d4d92 100644 --- a/lib/puppet/file_bucket/file.rb +++ b/lib/puppet/file_bucket/file.rb @@ -2,6 +2,7 @@ require 'puppet/file_bucket' require 'puppet/indirector' require 'puppet/util/checksums' require 'digest/md5' +require 'stringio' class Puppet::FileBucket::File # This class handles the abstract notion of a file in a filebucket. @@ -34,6 +35,16 @@ class Puppet::FileBucket::File raise ArgumentError.new("Unknown option(s): #{options.keys.join(', ')}") unless options.empty? end + # @return [Num] The size of the contents + def size + contents.size + end + + # @return [IO] A stream that reads the contents + def stream + StringIO.new(contents) + end + def checksum_type 'md5' end @@ -60,7 +71,11 @@ class Puppet::FileBucket::File def to_pson Puppet.deprecation_warning("Serializing Puppet::FileBucket::File objects to pson is deprecated.") - { "contents" => contents }.to_pson + to_data_hash.to_pson + end + + def to_data_hash + { "contents" => contents } end # This method is deprecated, but cannot be removed for awhile, otherwise @@ -69,4 +84,5 @@ class Puppet::FileBucket::File Puppet.deprecation_warning("Deserializing Puppet::FileBucket::File objects from pson is deprecated. Upgrade to a newer version.") self.new(pson["contents"]) end + end diff --git a/lib/puppet/file_serving/base.rb b/lib/puppet/file_serving/base.rb index 93b5abee2..f04a7a453 100644 --- a/lib/puppet/file_serving/base.rb +++ b/lib/puppet/file_serving/base.rb @@ -21,11 +21,18 @@ class Puppet::FileServing::Base # Return the full path to our file. Fails if there's no path set. def full_path(dummy_argument=:work_arround_for_ruby_GC_bug) - (if relative_path.nil? or relative_path == "" or relative_path == "." - path + if relative_path.nil? or relative_path == "" or relative_path == "." + full_path = path else - File.join(path, relative_path) - end).gsub(%r{//+}, "/") + full_path = File.join(path, relative_path) + end + + if Puppet.features.microsoft_windows? + # Replace multiple slashes as long as they aren't at the beginning of a filename + full_path.gsub(%r{(./)/+}, '\1') + else + full_path.gsub(%r{//+}, '/') + end end def initialize(path, options = {}) @@ -61,17 +68,21 @@ class Puppet::FileServing::Base # Stat our file, using the appropriate link-sensitive method. def stat @stat_method ||= self.links == :manage ? :lstat : :stat - File.send(@stat_method, full_path) + Puppet::FileSystem::File.new(full_path).send(@stat_method) + end + + def to_data_hash + { + 'path' => @path, + 'relative_path' => @relative_path, + 'links' => @links + } end def to_pson_data_hash { # No 'document_type' since we don't send these bare - 'data' => { - 'path' => @path, - 'relative_path' => @relative_path, - 'links' => @links - }, + 'data' => to_data_hash, 'metadata' => { 'api_version' => 1 } diff --git a/lib/puppet/file_serving/configuration.rb b/lib/puppet/file_serving/configuration.rb index d3ed15d61..c386a7e0a 100644 --- a/lib/puppet/file_serving/configuration.rb +++ b/lib/puppet/file_serving/configuration.rb @@ -1,20 +1,16 @@ -require 'monitor' require 'puppet' require 'puppet/file_serving' require 'puppet/file_serving/mount' require 'puppet/file_serving/mount/file' require 'puppet/file_serving/mount/modules' require 'puppet/file_serving/mount/plugins' +require 'puppet/file_serving/mount/pluginfacts' class Puppet::FileServing::Configuration require 'puppet/file_serving/configuration/parser' - extend MonitorMixin - def self.configuration - synchronize do - @configuration ||= new - end + @configuration ||= new end Mount = Puppet::FileServing::Mount @@ -84,13 +80,15 @@ class Puppet::FileServing::Configuration @mounts["modules"].allow('*') if @mounts["modules"].empty? @mounts["plugins"] ||= Mount::Plugins.new("plugins") @mounts["plugins"].allow('*') if @mounts["plugins"].empty? + @mounts["pluginfacts"] ||= Mount::PluginFacts.new("pluginfacts") + @mounts["pluginfacts"].allow('*') if @mounts["pluginfacts"].empty? end # Read the configuration file. def readconfig(check = true) config = Puppet[:fileserverconfig] - return unless FileTest.exists?(config) + return unless Puppet::FileSystem::File.exist?(config) @parser ||= Puppet::FileServing::Configuration::Parser.new(config) diff --git a/lib/puppet/file_serving/configuration/parser.rb b/lib/puppet/file_serving/configuration/parser.rb index 3bd9c9ebd..aa09aab50 100644 --- a/lib/puppet/file_serving/configuration/parser.rb +++ b/lib/puppet/file_serving/configuration/parser.rb @@ -7,7 +7,7 @@ class Puppet::FileServing::Configuration::Parser # Parse our configuration file. def parse - raise("File server configuration #{@file} does not exist") unless FileTest.exists?(@file) + raise("File server configuration #{@file} does not exist") unless Puppet::FileSystem::File.exist?(@file) raise("Cannot read file server configuration #{@file}") unless FileTest.readable?(@file) @mounts = {} diff --git a/lib/puppet/file_serving/content.rb b/lib/puppet/file_serving/content.rb index a6706bfcc..fc5a70ea5 100644 --- a/lib/puppet/file_serving/content.rb +++ b/lib/puppet/file_serving/content.rb @@ -34,7 +34,7 @@ class Puppet::FileServing::Content < Puppet::FileServing::Base # This stat can raise an exception, too. raise(ArgumentError, "Cannot read the contents of links unless following links") if stat.ftype == "symlink" - @content = IO.binread(full_path) + @content = Puppet::FileSystem::File.new(full_path).binread end @content end diff --git a/lib/puppet/file_serving/fileset.rb b/lib/puppet/file_serving/fileset.rb index 1360a7974..20957831f 100644 --- a/lib/puppet/file_serving/fileset.rb +++ b/lib/puppet/file_serving/fileset.rb @@ -77,7 +77,7 @@ class Puppet::FileServing::Fileset links = links.to_sym raise(ArgumentError, "Invalid :links value '#{links}'") unless [:manage, :follow].include?(links) @links = links - @stat_method = File.method(@links == :manage ? :lstat : :stat) + @stat_method = @links == :manage ? :lstat : :stat end private @@ -133,7 +133,7 @@ class Puppet::FileServing::Fileset end def directory? - stat_method.call(path).directory? + Puppet::FileSystem::File.new(path).send(stat_method).directory? rescue Errno::ENOENT, Errno::EACCES false end @@ -159,7 +159,7 @@ class Puppet::FileServing::Fileset end def valid?(path) - @stat_method.call(path) + Puppet::FileSystem::File.new(path).send(@stat_method) true rescue Errno::ENOENT, Errno::EACCES false diff --git a/lib/puppet/file_serving/metadata.rb b/lib/puppet/file_serving/metadata.rb index 877f5d258..2073c53ca 100644 --- a/lib/puppet/file_serving/metadata.rb +++ b/lib/puppet/file_serving/metadata.rb @@ -65,8 +65,8 @@ class Puppet::FileServing::Metadata < Puppet::FileServing::Base end # Retrieve the attributes for this file, relative to a base directory. - # Note that File.stat raises Errno::ENOENT if the file is absent and this - # method does not catch that exception. + # Note that Puppet::FileSystem::File.new(path).stat raises Errno::ENOENT + # if the file is absent and this method does not catch that exception. def collect real_path = full_path @@ -85,7 +85,7 @@ class Puppet::FileServing::Metadata < Puppet::FileServing::Base @checksum_type = "ctime" @checksum = ("{#{@checksum_type}}") + send("#{@checksum_type}_file", path).to_s when "link" - @destination = File.readlink(real_path) + @destination = Puppet::FileSystem::File.new(real_path).readlink @checksum = ("{#{@checksum_type}}") + send("#{@checksum_type}_file", real_path).to_s rescue nil else raise ArgumentError, "Cannot manage files of type #{stat.ftype}" @@ -106,25 +106,29 @@ class Puppet::FileServing::Metadata < Puppet::FileServing::Base super(path,data) end - PSON.register_document_type('FileMetadata',self) - def to_pson_data_hash - { - 'document_type' => 'FileMetadata', - - 'data' => super['data'].update( - { - 'owner' => owner, - 'group' => group, - 'mode' => mode, - 'checksum' => { - 'type' => checksum_type, - 'value' => checksum + def to_data_hash + super.update( + { + 'owner' => owner, + 'group' => group, + 'mode' => mode, + 'checksum' => { + 'type' => checksum_type, + 'value' => checksum }, 'type' => ftype, 'destination' => destination, - }), - 'metadata' => { + } + ) + end + + PSON.register_document_type('FileMetadata',self) + def to_pson_data_hash + { + 'document_type' => 'FileMetadata', + 'data' => to_data_hash, + 'metadata' => { 'api_version' => 1 } } diff --git a/lib/puppet/file_serving/mount/file.rb b/lib/puppet/file_serving/mount/file.rb index 7f5af7f52..535a90bde 100644 --- a/lib/puppet/file_serving/mount/file.rb +++ b/lib/puppet/file_serving/mount/file.rb @@ -22,7 +22,7 @@ class Puppet::FileServing::Mount::File < Puppet::FileServing::Mount file = ::File.join(full_path, relative_path) - if !(FileTest.exist?(file) or FileTest.symlink?(file)) + if !(Puppet::FileSystem::File.exist?(file) or Puppet::FileSystem::File.new(file).symlink?) Puppet.info("File does not exist or is not accessible: #{file}") return nil end diff --git a/lib/puppet/file_serving/mount/pluginfacts.rb b/lib/puppet/file_serving/mount/pluginfacts.rb new file mode 100644 index 000000000..5ba8f7146 --- /dev/null +++ b/lib/puppet/file_serving/mount/pluginfacts.rb @@ -0,0 +1,35 @@ +require 'puppet/file_serving/mount' + +# Find files in the modules' pluginfacts directories. +# This is a very strange mount because it merges +# many directories into one. +class Puppet::FileServing::Mount::PluginFacts < Puppet::FileServing::Mount + # Return an instance of the appropriate class. + def find(relative_path, request) + return nil unless mod = request.environment.modules.find { |mod| mod.pluginfact(relative_path) } + + path = mod.pluginfact(relative_path) + + path + end + + def search(relative_path, request) + # We currently only support one kind of search on plugins - return + # them all. + Puppet.debug("Warning: calling Plugins.search with empty module path.") if request.environment.modules.empty? + paths = request.environment.modules.find_all { |mod| mod.pluginfacts? }.collect { |mod| mod.plugin_fact_directory } + if paths.empty? + # If the modulepath is valid then we still need to return a valid root + # directory for the search, but make sure nothing inside it is + # returned. + request.options[:recurse] = false + request.environment.modulepath.empty? ? nil : request.environment.modulepath + else + paths + end + end + + def valid? + true + end +end diff --git a/lib/puppet/file_system.rb b/lib/puppet/file_system.rb index d9e5b8fd3..78e0f71db 100644 --- a/lib/puppet/file_system.rb +++ b/lib/puppet/file_system.rb @@ -1,3 +1,6 @@ module Puppet::FileSystem require 'puppet/file_system/path_pattern' + require 'puppet/file_system/file' + require 'puppet/file_system/memory_file' + require 'puppet/file_system/tempfile' end diff --git a/lib/puppet/file_system/file.rb b/lib/puppet/file_system/file.rb new file mode 100644 index 000000000..d46b6846b --- /dev/null +++ b/lib/puppet/file_system/file.rb @@ -0,0 +1,261 @@ +# An abstraction over the ruby file system operations for a single file. +# +# For the time being this is being kept private so that we can evolve it for a +# while. +# +# @api private +class Puppet::FileSystem::File + attr_reader :path + + IMPL = if RUBY_VERSION =~ /^1\.8/ + require 'puppet/file_system/file18' + Puppet::FileSystem::File18 + elsif Puppet::Util::Platform.windows? + require 'puppet/file_system/file19windows' + Puppet::FileSystem::File19Windows + else + require 'puppet/file_system/file19' + Puppet::FileSystem::File19 + end + + @remembered = {} + + def self.new(path) + if @remembered.include?(path.to_s) + @remembered[path.to_s] + else + file = IMPL.allocate + file.send(:initialize, path) + file + end + end + + # Run a block of code with a file accessible in the filesystem. + # @note This API should only be used for testing + # + # @param file [Object] an object that conforms to the Puppet::FileSystem::File interface + # @api private + def self.overlay(file, &block) + remember(file) + yield + ensure + forget(file) + end + + # Create a binding between a filename and a particular instance of a file object. + # @note This API should only be used for testing + # + # @param file [Object] an object that conforms to the Puppet::FileSystem::File interface + # @api private + def self.remember(file) + @remembered[file.path.to_s] = file + end + + # Forget a remembered file + # @note This API should only be used for testing + # + # @param file [Object] an object that conforms to the Puppet::FileSystem::File interface + # @api private + def self.forget(file) + @remembered.delete(file.path.to_s) + end + + def initialize(path) + if path.is_a?(Pathname) + @path = path + else + @path = Pathname.new(path) + end + end + + def open(mode, options, &block) + ::File.open(@path, options, mode, &block) + end + + # @return [Puppet::FileSystem::File] The directory of this file + # @api public + def dir + Puppet::FileSystem::File.new(@path.dirname) + end + + # @return [Num] The size of this file + # @api public + def size + @path.size + end + + # Allows exclusive updates to a file to be made by excluding concurrent + # access using flock. This means that if the file is on a filesystem that + # does not support flock, this method will provide no protection. + # + # While polling to aquire the lock the process will wait ever increasing + # amounts of time in order to prevent multiple processes from wasting + # resources. + # + # @param mode [Integer] The mode to apply to the file if it is created + # @param options [Integer] Extra file operation mode information to use + # (defaults to read-only mode) + # @param timeout [Integer] Number of seconds to wait for the lock (defaults to 300) + # @yield The file handle, in read-write mode + # @return [Void] + # @raise [Timeout::Error] If the timeout is exceeded while waiting to aquire the lock + # @api public + def exclusive_open(mode, options = 'r', timeout = 300, &block) + wait = 0.001 + (Kernel.rand / 1000) + written = false + while !written + ::File.open(@path, options, mode) do |rf| + if rf.flock(::File::LOCK_EX|::File::LOCK_NB) + yield rf + written = true + else + sleep wait + timeout -= wait + wait *= 2 + if timeout < 0 + raise Timeout::Error, "Timeout waiting for exclusive lock on #{@path}" + end + end + end + end + end + + def each_line(&block) + ::File.open(@path) do |f| + f.each_line do |line| + yield line + end + end + end + + # @return [String] The contents of the file + def read + @path.read + end + + # @return [String] The binary contents of the file + def binread + raise NotImplementedError + end + + # Determine if a file exists by verifying that the file can be stat'd. + # Will follow symlinks and verify that the actual target path exists. + # + # @return [Boolean] true if the named file exists. + def self.exist?(path) + return IMPL.exist?(path) if IMPL.method(:exist?) != self.method(:exist?) + File.exist?(path) + end + + # Determine if a file exists by verifying that the file can be stat'd. + # Will follow symlinks and verify that the actual target path exists. + # + # @return [Boolean] true if the path of this file is present + def exist? + self.class.exist?(@path) + end + + # Determine if a file is executable. + # + # @todo Should this take into account extensions on the windows platform? + # + # @return [Boolean] true if this file can be executed + def executable? + ::File.executable?(@path) + end + + # @return [Boolean] Whether the file is writable by the current + # process + def writable? + @path.writable? + end + + # Touches the file. On most systems this updates the mtime of the file. + def touch + ::FileUtils.touch(@path) + end + + # Create the entire path as directories + def mkpath + @path.mkpath + end + + # Creates a symbolic link dest which points to the current file. + # If dest already exists: + # + # * and is a file, will raise Errno::EEXIST + # * and is a directory, will return 0 but perform no action + # * and is a symlink referencing a file, will raise Errno::EEXIST + # * and is a symlink referencing a directory, will return 0 but perform no action + # + # With the :force option set to true, when dest already exists: + # + # * and is a file, will replace the existing file with a symlink (DANGEROUS) + # * and is a directory, will return 0 but perform no action + # * and is a symlink referencing a file, will modify the existing symlink + # * and is a symlink referencing a directory, will return 0 but perform no action + # + # @param dest [String] The path to create the new symlink at + # @param [Hash] options the options to create the symlink with + # @option options [Boolean] :force overwrite dest + # @option options [Boolean] :noop do not perform the operation + # @option options [Boolean] :verbose verbose output + # + # @raise [Errno::EEXIST] dest already exists as a file and, :force is not set + # + # @return [Integer] 0 + def symlink(dest, options = {}) + FileUtils.symlink(@path, dest, options) + end + + # @return [Boolean] true if the file is a symbolic link. + def symlink? + File.symlink?(@path) + end + + # @return [String] the name of the file referenced by the given link. + def readlink + File.readlink(@path) + end + + # Deletes the named files, returning the number of names passed as arguments. + # See also Dir::rmdir. + # + # @raise an exception on any error. + # + # @return [Integer] the number of names passed as arguments + def self.unlink(*file_names) + return IMPL.unlink(*file_names) if IMPL.method(:unlink) != self.method(:unlink) + File.unlink(*file_names) + end + + # Deletes the file. + # See also Dir::rmdir. + # + # @raise an exception on any error. + # + # @return [Integer] the number of names passed as arguments, in this case 1 + def unlink + self.class.unlink(@path) + end + + # @return [File::Stat] object for the named file. + def stat + File.stat(@path) + end + + # @return [File::Stat] Same as stat, but does not follow the last symbolic + # link. Instead, reports on the link itself. + def lstat + File.lstat(@path) + end + + # Compare the contents of this file against the contents of a stream. + # @param stream [IO] The stream to compare the contents against + # @return [Boolean] Whether the contents were the same + def compare_stream(stream) + open(0, 'rb') do |this| + FileUtils.compare_stream(this, stream) + end + end +end diff --git a/lib/puppet/file_system/file18.rb b/lib/puppet/file_system/file18.rb new file mode 100644 index 000000000..99c2d5e06 --- /dev/null +++ b/lib/puppet/file_system/file18.rb @@ -0,0 +1,5 @@ +class Puppet::FileSystem::File18 < Puppet::FileSystem::File + def binread + ::File.open(@path, 'rb') { |f| f.read } + end +end diff --git a/lib/puppet/file_system/file19.rb b/lib/puppet/file_system/file19.rb new file mode 100644 index 000000000..ca011613e --- /dev/null +++ b/lib/puppet/file_system/file19.rb @@ -0,0 +1,5 @@ +class Puppet::FileSystem::File19 < Puppet::FileSystem::File + def binread + @path.binread + end +end diff --git a/lib/puppet/file_system/file19windows.rb b/lib/puppet/file_system/file19windows.rb new file mode 100644 index 000000000..87f9915fe --- /dev/null +++ b/lib/puppet/file_system/file19windows.rb @@ -0,0 +1,113 @@ +require 'puppet/file_system/file19' +require 'puppet/util/windows' + +class Puppet::FileSystem::File19Windows < Puppet::FileSystem::File19 + + def self.exist?(path) + if ! Puppet.features.manages_symlinks? + return ::File.exist?(path) + end + + path = path.to_str if path.respond_to?(:to_str) # support WatchedFile + path = path.to_s # support String and Pathname + + begin + if Puppet::Util::Windows::File.symlink?(path) + path = Puppet::Util::Windows::File.readlink(path) + end + ! Puppet::Util::Windows::File.stat(path).nil? + rescue # generally INVALID_HANDLE_VALUE which means 'file not found' + false + end + end + + def exist? + self.class.exist?(@path) + end + + def symlink(dest, options = {}) + raise_if_symlinks_unsupported + + dest_exists = self.class.exist?(dest) # returns false on dangling symlink + dest_stat = Puppet::Util::Windows::File.stat(dest) if dest_exists + dest_symlink = Puppet::Util::Windows::File.symlink?(dest) + + # silent fail to preserve semantics of original FileUtils + return 0 if dest_exists && dest_stat.ftype == 'directory' + + if dest_exists && dest_stat.ftype == 'file' && options[:force] != true + raise(Errno::EEXIST, "#{dest} already exists and the :force option was not specified") + end + + if options[:noop] != true + ::File.delete(dest) if dest_exists # can only be file + Puppet::Util::Windows::File.symlink(@path, dest) + end + + 0 + end + + def symlink? + return false if ! Puppet.features.manages_symlinks? + Puppet::Util::Windows::File.symlink?(@path) + end + + def readlink + raise_if_symlinks_unsupported + Puppet::Util::Windows::File.readlink(@path) + end + + def self.unlink(*file_names) + if ! Puppet.features.manages_symlinks? + return ::File.unlink(*file_names) + end + + file_names.each do |file_name| + file_name = file_name.to_s # handle PathName + stat = Puppet::Util::Windows::File.stat(file_name) rescue nil + + # sigh, Ruby + Windows :( + if stat && stat.ftype == 'directory' + if Puppet::Util::Windows::File.symlink?(file_name) + Dir.rmdir(file_name) + else + raise Errno::EPERM.new(file_name) + end + else + ::File.unlink(file_name) + end + end + + file_names.length + end + + def unlink + self.class.unlink(@path) + end + + def stat + if ! Puppet.features.manages_symlinks? + return super + end + Puppet::Util::Windows::File.stat(@path) + end + + def lstat + if ! Puppet.features.manages_symlinks? + return Puppet::Util::Windows::File.stat(@path) + end + Puppet::Util::Windows::File.lstat(@path) + end + + private + def raise_if_symlinks_unsupported + if ! Puppet.features.manages_symlinks? + msg = "This version of Windows does not support symlinks. Windows Vista / 2008 or higher is required." + raise Puppet::Util::Windows::Error.new(msg) + end + + if ! Puppet::Util::Windows::Process.process_privilege_symlink? + Puppet.warning "The current user does not have the necessary permission to manage symlinks." + end + end +end diff --git a/lib/puppet/file_system/memory_file.rb b/lib/puppet/file_system/memory_file.rb new file mode 100644 index 000000000..4605a7a01 --- /dev/null +++ b/lib/puppet/file_system/memory_file.rb @@ -0,0 +1,31 @@ +# An in-memory file abstraction. Commonly used with Puppet::FileSystem::File#overlay +# @api private +class Puppet::FileSystem::MemoryFile + attr_reader :path + + def self.a_missing_file(path) + new(path, :exist? => false, :executable? => false) + end + + def self.a_regular_file_containing(path, content) + new(path, :exist? => true, :executable? => false, :content => content) + end + + def self.an_executable(path) + new(path, :exist? => true, :executable? => true) + end + + def initialize(path, options) + @path = Pathname.new(path) + @exist = options[:exist?] + @executable = options[:executable?] + @content = options[:content] + end + + def exist?; @exist; end + def executable?; @executable; end + + def each_line(&block) + StringIO.new(@content).each_line(&block) + end +end diff --git a/lib/puppet/file_system/tempfile.rb b/lib/puppet/file_system/tempfile.rb new file mode 100644 index 000000000..6766a7aec --- /dev/null +++ b/lib/puppet/file_system/tempfile.rb @@ -0,0 +1,20 @@ +require 'tempfile' + +class Puppet::FileSystem::Tempfile + + # Variation of Tempfile.open which ensures that the tempfile is closed and + # unlinked before returning + # + # @param identifier [String] additional part of generated pathname + # @yieldparam file [File] the temporary file object + # @return result of the passed block + # @api private + def self.open(identifier) + file = ::Tempfile.new(identifier) + + yield file + + ensure + file.close! + end +end diff --git a/lib/puppet/indirector/active_record.rb b/lib/puppet/indirector/active_record.rb index a9f05d683..32c4dd4dc 100644 --- a/lib/puppet/indirector/active_record.rb +++ b/lib/puppet/indirector/active_record.rb @@ -1,3 +1,4 @@ +require 'puppet/rails' require 'puppet/indirector' class Puppet::Indirector::ActiveRecord < Puppet::Indirector::Terminus diff --git a/lib/puppet/indirector/catalog/compiler.rb b/lib/puppet/indirector/catalog/compiler.rb index 9b1cc0305..b2a3c251a 100644 --- a/lib/puppet/indirector/catalog/compiler.rb +++ b/lib/puppet/indirector/catalog/compiler.rb @@ -41,6 +41,7 @@ class Puppet::Resource::Catalog::Compiler < Puppet::Indirector::Code extract_facts_from_request(request) node = node_from_request(request) + node.trusted_data = trusted_hash_from_request(request) if catalog = compile(node) return catalog @@ -51,6 +52,7 @@ class Puppet::Resource::Catalog::Compiler < Puppet::Indirector::Code end end + # filter-out a catalog to remove exported resources def filter(catalog) return catalog.filter { |r| r.virtual? } if catalog.respond_to?(:filter) @@ -70,6 +72,32 @@ class Puppet::Resource::Catalog::Compiler < Puppet::Indirector::Code private + # Produces a deeply frozen hash with trusted information + # The key :authenticated is always present in the result with one of the values + # :remote, :local, false, where :remote is authenticated via cert, :local is trusted by virtue + # of running on the same machine (not a remove request), and false is an unauthenticated remot request. + # When the trusted hash value for :authenticated == false, there is no other values set in the hash. + # + def trusted_hash_from_request(request) + if request.remote? + if request.authenticated? + trust_authenticated = 'remote'.freeze + client_cert = request.node + else + trust_authenticated = false + client_cert = nil + end + else + trust_authenticated = 'local'.freeze + # Always trust local data by picking up the available parameters. + request_node = request.options[:use_node] + client_cert = request_node ? request_node.parameters['clientcert'] : nil + end + + # TODO nil or undef for client_cert missing? + trusted_hash = { 'authenticated' => trust_authenticated, 'certname' => client_cert }.freeze + end + # Add any extra data necessary to the node. def add_node_data(node) # Merge in our server-side facts, so they can be used during compilation. diff --git a/lib/puppet/indirector/certificate_request/memory.rb b/lib/puppet/indirector/certificate_request/memory.rb new file mode 100644 index 000000000..c60a1f9f1 --- /dev/null +++ b/lib/puppet/indirector/certificate_request/memory.rb @@ -0,0 +1,6 @@ +require 'puppet/ssl/certificate_request' +require 'puppet/indirector/memory' + +class Puppet::SSL::CertificateRequest::Memory < Puppet::Indirector::Memory + desc "Store certificate requests in memory. This is used for testing puppet." +end diff --git a/lib/puppet/indirector/data_binding/hiera.rb b/lib/puppet/indirector/data_binding/hiera.rb index 5271b997a..9ff640b2e 100644 --- a/lib/puppet/indirector/data_binding/hiera.rb +++ b/lib/puppet/indirector/data_binding/hiera.rb @@ -1,6 +1,50 @@ -require 'puppet/indirector/hiera' +require 'puppet/indirector/code' +require 'hiera/scope' -class Puppet::DataBinding::Hiera < Puppet::Indirector::Hiera +class Puppet::DataBinding::Hiera < Puppet::Indirector::Code desc "Retrieve data using Hiera." + + def initialize(*args) + if ! Puppet.features.hiera? + raise "Hiera terminus not supported without hiera library" + end + super + end + + if defined?(::Psych::SyntaxError) + DataBindingExceptions = [::StandardError, ::Psych::SyntaxError] + else + DataBindingExceptions = [::StandardError] + end + + def find(request) + hiera.lookup(request.key, nil, Hiera::Scope.new(request.options[:variables]), nil, nil) + rescue *DataBindingExceptions => detail + raise Puppet::DataBinding::LookupError.new(detail.message, detail) + end + + private + + def self.hiera_config + hiera_config = Puppet.settings[:hiera_config] + config = {} + + if Puppet::FileSystem::File.exist?(hiera_config) + config = Hiera::Config.load(hiera_config) + else + Puppet.warning "Config file #{hiera_config} not found, using Hiera defaults" + end + + config[:logger] = 'puppet' + config + end + + def self.hiera + @hiera ||= Hiera.new(:config => hiera_config) + end + + def hiera + self.class.hiera + end end diff --git a/lib/puppet/indirector/direct_file_server.rb b/lib/puppet/indirector/direct_file_server.rb index 62234e360..dba4d60bd 100644 --- a/lib/puppet/indirector/direct_file_server.rb +++ b/lib/puppet/indirector/direct_file_server.rb @@ -6,14 +6,14 @@ class Puppet::Indirector::DirectFileServer < Puppet::Indirector::Terminus include Puppet::FileServing::TerminusHelper def find(request) - return nil unless FileTest.exists?(request.key) + return nil unless Puppet::FileSystem::File.exist?(request.key) instance = model.new(request.key) instance.links = request.options[:links] if request.options[:links] instance end def search(request) - return nil unless FileTest.exists?(request.key) + return nil unless Puppet::FileSystem::File.exist?(request.key) path2instances(request, request.key) end end diff --git a/lib/puppet/indirector/facts/facter.rb b/lib/puppet/indirector/facts/facter.rb index a4ee18ad3..4b44b5812 100644 --- a/lib/puppet/indirector/facts/facter.rb +++ b/lib/puppet/indirector/facts/facter.rb @@ -6,6 +6,8 @@ class Puppet::Node::Facts::Facter < Puppet::Indirector::Code between Puppet and Facter. It's only `somewhat` abstract because it always returns the local host's facts, regardless of what you attempt to find." + private + def self.reload_facter Facter.clear Facter.loadfacts @@ -24,6 +26,26 @@ class Puppet::Node::Facts::Facter < Puppet::Indirector::Code end end + def self.setup_external_facts(request) + # Add any per-module fact directories to the factpath + external_facts_dirs = [] + request.environment.modules.each do |m| + if m.has_external_facts? + Puppet.info "Loading external facts from #{m.plugin_fact_directory}" + external_facts_dirs << m.plugin_fact_directory + end + end + + # Add system external fact directory if it exists + if File.directory?(Puppet[:pluginfactdest]) + external_facts_dirs << Puppet[:pluginfactdest] + end + + # Add to facter config + Facter::Util::Config.external_facts_dirs += external_facts_dirs + + end + def self.load_facts_in_dir(dir) return unless FileTest.directory?(dir) @@ -44,12 +66,15 @@ class Puppet::Node::Facts::Facter < Puppet::Indirector::Code end end + public + def destroy(facts) raise Puppet::DevError, "You cannot destroy facts in the code store; it is only used for getting facts from Facter" end # Look a host's facts up in Facter. def find(request) + self.class.setup_external_facts(request) if Puppet.features.external_facts? self.class.reload_facter self.class.load_fact_plugins result = Puppet::Node::Facts.new(request.key, Facter.to_hash) diff --git a/lib/puppet/indirector/file_bucket_file/file.rb b/lib/puppet/indirector/file_bucket_file/file.rb index a8fd79b7e..59be12451 100644 --- a/lib/puppet/indirector/file_bucket_file/file.rb +++ b/lib/puppet/indirector/file_bucket_file/file.rb @@ -9,41 +9,40 @@ module Puppet::FileBucketFile desc "Store files in a directory set based on their checksums." - def initialize - Puppet.settings.use(:filebucket) - end - - def find( request ) - checksum, files_original_path = request_to_checksum_and_path( request ) - dir_path = path_for(request.options[:bucket_path], checksum) - file_path = ::File.join(dir_path, 'contents') - - return nil unless ::File.exists?(file_path) - return nil unless path_match(dir_path, files_original_path) - - if request.options[:diff_with] - file2_path = path_for(request.options[:bucket_path], request.options[:diff_with], 'contents') - raise "could not find diff_with #{request.options[:diff_with]}" unless ::File.exists?(file2_path) - return `diff #{file_path.inspect} #{file2_path.inspect}` + def find(request) + checksum, files_original_path = request_to_checksum_and_path(request) + contents_file = path_for(request.options[:bucket_path], checksum, 'contents') + paths_file = path_for(request.options[:bucket_path], checksum, 'paths') + + if contents_file.exist? && matches(paths_file, files_original_path) + if request.options[:diff_with] + other_contents_file = path_for(request.options[:bucket_path], request.options[:diff_with], 'contents') + raise "could not find diff_with #{request.options[:diff_with]}" unless other_contents_file.exist? + return `diff #{contents_file.path.to_s.inspect} #{other_contents_file.path.to_s.inspect}` + else + Puppet.info "FileBucket read #{checksum}" + model.new(contents_file.binread) + end else - contents = IO.binread(file_path) - Puppet.info "FileBucket read #{checksum}" - model.new(contents) + nil end end def head(request) checksum, files_original_path = request_to_checksum_and_path(request) - dir_path = path_for(request.options[:bucket_path], checksum) + contents_file = path_for(request.options[:bucket_path], checksum, 'contents') + paths_file = path_for(request.options[:bucket_path], checksum, 'paths') - ::File.exists?(::File.join(dir_path, 'contents')) and path_match(dir_path, files_original_path) + contents_file.exist? && matches(paths_file, files_original_path) end - def save( request ) + def save(request) instance = request.instance - checksum, files_original_path = request_to_checksum_and_path(request) + _, files_original_path = request_to_checksum_and_path(request) + contents_file = path_for(instance.bucket_path, instance.checksum_data, 'contents') + paths_file = path_for(instance.bucket_path, instance.checksum_data, 'paths') - save_to_disk(instance, files_original_path) + save_to_disk(instance, files_original_path, contents_file, paths_file) # don't echo the request content back to the agent model.new('') @@ -55,57 +54,46 @@ module Puppet::FileBucketFile private - def path_match(dir_path, files_original_path) + def matches(paths_file, files_original_path) + paths_file.open(0640, 'a+') do |f| + path_match(f, files_original_path) + end + end + + def path_match(file_handle, files_original_path) return true unless files_original_path # if no path was provided, it's a match - paths_path = ::File.join(dir_path, 'paths') - return false unless ::File.exists?(paths_path) - ::File.open(paths_path) do |f| - f.each_line do |line| - return true if line.chomp == files_original_path - end + file_handle.rewind + file_handle.each_line do |line| + return true if line.chomp == files_original_path end return false end - def save_to_disk( bucket_file, files_original_path ) - filename = path_for(bucket_file.bucket_path, bucket_file.checksum_data, 'contents') - dir_path = path_for(bucket_file.bucket_path, bucket_file.checksum_data) - paths_path = ::File.join(dir_path, 'paths') - - # If the file already exists, touch it. - if ::File.exist?(filename) - verify_identical_file!(bucket_file) - ::FileUtils.touch(filename) - else - # Make the directories if necessary. - unless ::File.directory?(dir_path) - Puppet::Util.withumask(0007) do - ::FileUtils.mkdir_p(dir_path) - end + def save_to_disk(bucket_file, files_original_path, contents_file, paths_file) + Puppet::Util.withumask(0007) do + unless paths_file.dir.exist? + paths_file.dir.mkpath end - Puppet.info "FileBucket adding #{bucket_file.checksum}" - - # Write the file to disk. - Puppet::Util.withumask(0007) do - ::File.open(filename, ::File::WRONLY|::File::CREAT, 0440) do |of| - of.binmode - of.print bucket_file.contents + paths_file.exclusive_open(0640, 'a+') do |f| + if contents_file.exist? + verify_identical_file!(contents_file, bucket_file) + contents_file.touch + else + contents_file.open(0440, 'wb') do |of| + of.write(bucket_file.contents) + end end - ::File.open(paths_path, ::File::WRONLY|::File::CREAT, 0640) do |of| - # path will be written below - end - end - end - unless path_match(dir_path, files_original_path) - ::File.open(paths_path, 'a') do |f| - f.puts(files_original_path) + unless path_match(f, files_original_path) + f.seek(0, IO::SEEK_END) + f.puts(files_original_path) + end end end end - def request_to_checksum_and_path( request ) + def request_to_checksum_and_path(request) checksum_type, checksum, path = request.key.split(/\//, 3) if path == '' # Treat "md5/<checksum>/" like "md5/<checksum>" path = nil @@ -121,22 +109,20 @@ module Puppet::FileBucketFile dir = ::File.join(digest[0..7].split("")) basedir = ::File.join(bucket_path, dir, digest) - return basedir unless subfile - ::File.join(basedir, subfile) + Puppet::FileSystem::File.new(subfile ? ::File.join(basedir, subfile) : basedir) end - # If conflict_check is enabled, verify that the passed text is - # the same as the text in our file. - def verify_identical_file!(bucket_file) - disk_contents = IO.binread(path_for(bucket_file.bucket_path, bucket_file.checksum_data, 'contents')) + def verify_identical_file!(contents_file, bucket_file) + if bucket_file.contents.size == contents_file.size + if contents_file.compare_stream(bucket_file.stream) + Puppet.info "FileBucket got a duplicate file #{bucket_file.checksum}" + return + end + end - # If the contents don't match, then we've found a conflict. + # If the contents or sizes don't match, then we've found a conflict. # Unlikely, but quite bad. - if disk_contents != bucket_file.contents - raise Puppet::FileBucket::BucketError, "Got passed new contents for sum #{bucket_file.checksum}" - else - Puppet.info "FileBucket got a duplicate file #{bucket_file.checksum}" - end + raise Puppet::FileBucket::BucketError, "Got passed new contents for sum #{bucket_file.checksum}" end end end diff --git a/lib/puppet/indirector/hiera.rb b/lib/puppet/indirector/hiera.rb deleted file mode 100644 index 9cceb1da2..000000000 --- a/lib/puppet/indirector/hiera.rb +++ /dev/null @@ -1,39 +0,0 @@ -require 'puppet/indirector/terminus' - -class Puppet::Indirector::Hiera < Puppet::Indirector::Terminus - def initialize(*args) - if ! Puppet.features.hiera? - raise "Hiera terminus not supported without hiera library" - end - super - end - - def find(request) - hiera.lookup(request.key, nil, request.options[:variables], nil, nil) - end - - private - - def self.hiera_config - hiera_config = Puppet.settings[:hiera_config] - config = {} - - if File.exist?(hiera_config) - config = Hiera::Config.load(hiera_config) - else - Puppet.warning "Config file #{hiera_config} not found, using Hiera defaults" - end - - config[:logger] = 'puppet' - config - end - - def self.hiera - @hiera ||= Hiera.new(:config => hiera_config) - end - - def hiera - self.class.hiera - end -end - diff --git a/lib/puppet/indirector/indirection.rb b/lib/puppet/indirector/indirection.rb index a07bddcce..0c0cb2075 100644 --- a/lib/puppet/indirector/indirection.rb +++ b/lib/puppet/indirector/indirection.rb @@ -85,7 +85,7 @@ class Puppet::Indirector::Indirection def doc text = "" - text += scrub(@doc) + "\n\n" if @doc + text << scrub(@doc) << "\n\n" if @doc text << "* **Indirected Class**: `#{@indirected_class}`\n"; if terminus_setting @@ -180,6 +180,10 @@ class Puppet::Indirector::Indirection cache.save(request(:save, nil, instance, options)) end + def allow_remote_requests? + terminus.allow_remote_requests? + end + # Search for an instance in the appropriate terminus, caching the # results if caching is configured.. def find(key, options={}) diff --git a/lib/puppet/indirector/json.rb b/lib/puppet/indirector/json.rb index 1a8db8025..515ad33b6 100644 --- a/lib/puppet/indirector/json.rb +++ b/lib/puppet/indirector/json.rb @@ -21,7 +21,7 @@ class Puppet::Indirector::JSON < Puppet::Indirector::Terminus end def destroy(request) - File.unlink(path(request.key)) + Puppet::FileSystem::File.unlink(path(request.key)) rescue => detail unless detail.is_a? Errno::ENOENT raise Puppet::Error, "Could not destroy #{self.name} #{request.key}: #{detail}" diff --git a/lib/puppet/indirector/key/ca.rb b/lib/puppet/indirector/key/ca.rb index 056d037dd..d2c3482fb 100644 --- a/lib/puppet/indirector/key/ca.rb +++ b/lib/puppet/indirector/key/ca.rb @@ -9,4 +9,8 @@ class Puppet::SSL::Key::Ca < Puppet::Indirector::SslFile store_in :privatekeydir store_ca_at :cakey + + def allow_remote_requests? + false + end end diff --git a/lib/puppet/indirector/key/file.rb b/lib/puppet/indirector/key/file.rb index 1990f1a46..40f8a331d 100644 --- a/lib/puppet/indirector/key/file.rb +++ b/lib/puppet/indirector/key/file.rb @@ -7,6 +7,10 @@ class Puppet::SSL::Key::File < Puppet::Indirector::SslFile store_in :privatekeydir store_ca_at :cakey + def allow_remote_requests? + false + end + # Where should we store the public key? def public_key_path(name) if ca?(name) @@ -20,10 +24,10 @@ class Puppet::SSL::Key::File < Puppet::Indirector::SslFile def destroy(request) super - return unless FileTest.exist?(public_key_path(request.key)) + return unless Puppet::FileSystem::File.exist?(public_key_path(request.key)) begin - File.unlink(public_key_path(request.key)) + Puppet::FileSystem::File.unlink(public_key_path(request.key)) rescue => detail raise Puppet::Error, "Could not remove #{request.key} public key: #{detail}" end @@ -34,7 +38,7 @@ class Puppet::SSL::Key::File < Puppet::Indirector::SslFile super begin - Puppet.settings.writesub(:publickeydir, public_key_path(request.key)) { |f| f.print request.instance.content.public_key.to_pem } + Puppet.settings.setting(:publickeydir).open_file(public_key_path(request.key), 'w') { |f| f.print request.instance.content.public_key.to_pem } rescue => detail raise Puppet::Error, "Could not write #{request.key}: #{detail}" end diff --git a/lib/puppet/indirector/key/memory.rb b/lib/puppet/indirector/key/memory.rb new file mode 100644 index 000000000..527863e03 --- /dev/null +++ b/lib/puppet/indirector/key/memory.rb @@ -0,0 +1,6 @@ +require 'puppet/ssl/key' +require 'puppet/indirector/memory' + +class Puppet::SSL::Key::Memory < Puppet::Indirector::Memory + desc "Store keys in memory. This is used for testing puppet." +end diff --git a/lib/puppet/indirector/node/write_only_yaml.rb b/lib/puppet/indirector/node/write_only_yaml.rb index b30bdc1db..3f97e72e8 100644 --- a/lib/puppet/indirector/node/write_only_yaml.rb +++ b/lib/puppet/indirector/node/write_only_yaml.rb @@ -17,7 +17,7 @@ class Puppet::Node::WriteOnlyYaml < Puppet::Indirector::Yaml # Overridden to always return nil. This is a write only terminus. # @param [Object] request Ignored. # @return [nil] This implementation always return nil' - # @api + # @api public def find(request) nil end @@ -25,7 +25,7 @@ class Puppet::Node::WriteOnlyYaml < Puppet::Indirector::Yaml # Overridden to always return nil. This is a write only terminus. # @param [Object] request Ignored. # @return [nil] This implementation always return nil - # @api + # @api public def search(request) nil end diff --git a/lib/puppet/indirector/request.rb b/lib/puppet/indirector/request.rb index 9f95b2f2e..4eacee11b 100644 --- a/lib/puppet/indirector/request.rb +++ b/lib/puppet/indirector/request.rb @@ -37,16 +37,12 @@ class Puppet::Indirector::Request request end - def to_pson(*args) + def to_data_hash result = { - 'document_type' => 'IndirectorRequest', - 'data' => { - 'type' => indirection_name, - 'method' => method, - 'key' => key - } + 'type' => indirection_name, + 'method' => method, + 'key' => key } - data = result['data'] attributes = {} OPTION_ATTRIBUTES.each do |key| next unless value = send(key) @@ -57,10 +53,20 @@ class Puppet::Indirector::Request attributes[opt] = value end - data['attributes'] = attributes unless attributes.empty? - data['instance'] = instance if instance + result['attributes'] = attributes unless attributes.empty? + result['instance'] = instance if instance + result + end - result.to_pson(*args) + def to_pson_data_hash + { + 'document_type' => 'IndirectorRequest', + 'data' => to_data_hash, + } + end + + def to_pson(*args) + to_pson_data_hash.to_pson(*args) end # Is this an authenticated request? diff --git a/lib/puppet/indirector/resource/ral.rb b/lib/puppet/indirector/resource/ral.rb index 30c8623c5..5a366a329 100644 --- a/lib/puppet/indirector/resource/ral.rb +++ b/lib/puppet/indirector/resource/ral.rb @@ -5,6 +5,11 @@ class Puppet::Resource::Ral < Puppet::Indirector::Code desc "Manipulate resources with the resource abstraction layer. Only used internally." + def allow_remote_requests? + Puppet.deprecation_warning("Accessing resources on the network is deprecated. See http://links.puppetlabs.com/deprecate-networked-resource") + super + end + def find( request ) # find by name res = type(request).instances.find { |o| o.name == resource_name(request) } diff --git a/lib/puppet/indirector/resource/rest.rb b/lib/puppet/indirector/resource/rest.rb index 824af41d1..9992fc057 100644 --- a/lib/puppet/indirector/resource/rest.rb +++ b/lib/puppet/indirector/resource/rest.rb @@ -1,6 +1,7 @@ require 'puppet/indirector/status' require 'puppet/indirector/rest' +# @deprecated class Puppet::Resource::Rest < Puppet::Indirector::REST desc "Maniuplate resources remotely? Undocumented." diff --git a/lib/puppet/indirector/resource/store_configs.rb b/lib/puppet/indirector/resource/store_configs.rb index 45ffdbd5d..0c249d60b 100644 --- a/lib/puppet/indirector/resource/store_configs.rb +++ b/lib/puppet/indirector/resource/store_configs.rb @@ -6,4 +6,8 @@ class Puppet::Resource::StoreConfigs < Puppet::Indirector::StoreConfigs desc %q{Part of the "storeconfigs" feature. Should not be directly set by end users.} + def allow_remote_requests? + Puppet.deprecation_warning("Accessing resources on the network is deprecated. See http://links.puppetlabs.com/deprecate-networked-resource") + super + end end diff --git a/lib/puppet/indirector/rest.rb b/lib/puppet/indirector/rest.rb index 44c26e78b..d70b43bdf 100644 --- a/lib/puppet/indirector/rest.rb +++ b/lib/puppet/indirector/rest.rb @@ -55,7 +55,8 @@ class Puppet::Indirector::REST < Puppet::Indirector::Terminus end def network(request) - Puppet::Network::HTTP::Connection.new(request.server || self.class.server, request.port || self.class.port) + Puppet::Network::HttpPool.http_instance(request.server || self.class.server, + request.port || self.class.port) end def http_get(request, path, headers = nil, *args) diff --git a/lib/puppet/indirector/ssl_file.rb b/lib/puppet/indirector/ssl_file.rb index df72ab4f5..a4ca4bd77 100644 --- a/lib/puppet/indirector/ssl_file.rb +++ b/lib/puppet/indirector/ssl_file.rb @@ -70,11 +70,11 @@ class Puppet::Indirector::SslFile < Puppet::Indirector::Terminus # Remove our file. def destroy(request) path = path(request.key) - return false unless FileTest.exist?(path) + return false unless Puppet::FileSystem::File.exist?(path) Puppet.notice "Removing file #{model} #{request.key} at '#{path}'" begin - File.unlink(path) + Puppet::FileSystem::File.unlink(path) rescue => detail raise Puppet::Error, "Could not remove #{request.key}: #{detail}" end @@ -135,10 +135,10 @@ class Puppet::Indirector::SslFile < Puppet::Indirector::Terminus # which we'll be EOL'ing at some point. This method was added at 20080702 # and should be removed at some point. def rename_files_with_uppercase(file) - return file if FileTest.exist?(file) + return file if Puppet::FileSystem::File.exist?(file) dir, short = File.split(file) - return nil unless FileTest.exist?(dir) + return nil unless Puppet::FileSystem::File.exist?(dir) raise ArgumentError, "Tried to fix SSL files to a file containing uppercase" unless short.downcase == short real_file = Dir.entries(dir).reject { |f| f =~ /^\./ }.find do |other| @@ -159,12 +159,12 @@ class Puppet::Indirector::SslFile < Puppet::Indirector::Terminus # the work or opening a filehandle manually. def write(name, path) if ca?(name) and ca_location - Puppet.settings.write(self.class.ca_setting) { |f| yield f } + Puppet.settings.setting(self.class.ca_setting).open('w') { |f| yield f } elsif file_location - Puppet.settings.write(self.class.file_setting) { |f| yield f } + Puppet.settings.setting(self.class.file_setting).open('w') { |f| yield f } elsif setting = self.class.directory_setting begin - Puppet.settings.writesub(setting, path) { |f| yield f } + Puppet.settings.setting(setting).open_file(path, 'w') { |f| yield f } rescue => detail raise Puppet::Error, "Could not write #{path} to #{setting}: #{detail}" end diff --git a/lib/puppet/indirector/terminus.rb b/lib/puppet/indirector/terminus.rb index 4e74efffd..b05ac3d8c 100644 --- a/lib/puppet/indirector/terminus.rb +++ b/lib/puppet/indirector/terminus.rb @@ -140,6 +140,10 @@ class Puppet::Indirector::Terminus self.class.name end + def allow_remote_requests? + true + end + def terminus_type self.class.terminus_type end diff --git a/lib/puppet/indirector/yaml.rb b/lib/puppet/indirector/yaml.rb index 014994b9d..9c4e6f102 100644 --- a/lib/puppet/indirector/yaml.rb +++ b/lib/puppet/indirector/yaml.rb @@ -6,7 +6,7 @@ class Puppet::Indirector::Yaml < Puppet::Indirector::Terminus # Read a given name's file in and convert it from YAML. def find(request) file = path(request.key) - return nil unless FileTest.exist?(file) + return nil unless Puppet::FileSystem::File.exist?(file) begin return Puppet::Util::Yaml.load_file(file) @@ -24,7 +24,7 @@ class Puppet::Indirector::Yaml < Puppet::Indirector::Terminus basedir = File.dirname(file) # This is quite likely a bad idea, since we're not managing ownership or modes. - Dir.mkdir(basedir) unless FileTest.exist?(basedir) + Dir.mkdir(basedir) unless Puppet::FileSystem::File.exist?(basedir) begin Puppet::Util::Yaml.dump(request.instance, file) @@ -46,7 +46,7 @@ class Puppet::Indirector::Yaml < Puppet::Indirector::Terminus def destroy(request) file_path = path(request.key) - File.unlink(file_path) if File.exists?(file_path) + Puppet::FileSystem::File.unlink(file_path) if Puppet::FileSystem::File.exist?(file_path) end def search(request) diff --git a/lib/puppet/interface/documentation.rb b/lib/puppet/interface/documentation.rb index ec312512e..d90a68a62 100644 --- a/lib/puppet/interface/documentation.rb +++ b/lib/puppet/interface/documentation.rb @@ -1,19 +1,12 @@ class Puppet::Interface # @api private module DocGen + require 'puppet/util/docs' + # @api private def self.strip_whitespace(text) - text.gsub!(/[ \t\f]+$/, '') - - # We need to identify an indent: the minimum number of whitespace - # characters at the start of any line in the text. - indent = text.split(/\n/).map {|x| x.index(/[^\s]/) }.compact.min - - if indent > 0 then - text.gsub!(/^[ \t\f]{0,#{indent}}/, '') - end - - return text + # I don't want no... + Puppet::Util::Docs.scrub(text) end # The documentation attributes all have some common behaviours; previously diff --git a/lib/puppet/module.rb b/lib/puppet/module.rb index 8d5434edd..39274c044 100644 --- a/lib/puppet/module.rb +++ b/lib/puppet/module.rb @@ -20,6 +20,7 @@ class Puppet::Module "files" => "files", "templates" => "templates", "plugins" => "lib", + "pluginfacts" => "facts.d", } # Find and return the +module+ that +path+ belongs to. If +path+ is @@ -53,10 +54,14 @@ class Puppet::Module def has_metadata? return false unless metadata_file - return false unless FileTest.exist?(metadata_file) - - metadata = PSON.parse File.read(metadata_file) + return false unless Puppet::FileSystem::File.exist?(metadata_file) + begin + metadata = PSON.parse(File.read(metadata_file)) + rescue PSON::PSONError => e + Puppet.debug("#{name} has an invalid and unparsable metadata.json file. The parse error: #{e.message}") + return false + end return metadata.is_a?(Hash) && !metadata.keys.empty? end @@ -66,7 +71,7 @@ class Puppet::Module # we have files of a given type. define_method(type +'?') do type_subpath = subpath(location) - unless FileTest.exist?(type_subpath) + unless Puppet::FileSystem::File.exist?(type_subpath) Puppet.debug("No #{type} found in subpath '#{type_subpath}' " + "(file / directory does not exist)") return false @@ -89,7 +94,7 @@ class Puppet::Module full_path = subpath(location) end - return nil unless FileTest.exist?(full_path) + return nil unless Puppet::FileSystem::File.exist?(full_path) return full_path end @@ -148,7 +153,7 @@ class Puppet::Module end def all_manifests - return [] unless File.exists?(manifests) + return [] unless Puppet::FileSystem::File.exist?(manifests) Dir.glob(File.join(manifests, '**', '*.{rb,pp}')) end @@ -169,6 +174,14 @@ class Puppet::Module subpath("lib") end + def plugin_fact_directory + subpath("facts.d") + end + + def has_external_facts? + File.directory?(plugin_fact_directory) + end + def supports(name, version = nil) @supports ||= [] @supports << [name, version] diff --git a/lib/puppet/module_tool/applications/builder.rb b/lib/puppet/module_tool/applications/builder.rb index d454a7bd4..65923c1d8 100644 --- a/lib/puppet/module_tool/applications/builder.rb +++ b/lib/puppet/module_tool/applications/builder.rb @@ -60,7 +60,7 @@ module Puppet::ModuleTool when *Puppet::ModuleTool::ARTIFACTS next else - FileUtils.cp_r path, build_path + FileUtils.cp_r path, build_path, :preserve => true end end end diff --git a/lib/puppet/module_tool/applications/installer.rb b/lib/puppet/module_tool/applications/installer.rb index e53f6308b..772d7a831 100644 --- a/lib/puppet/module_tool/applications/installer.rb +++ b/lib/puppet/module_tool/applications/installer.rb @@ -32,7 +32,7 @@ module Puppet::ModuleTool if is_module_package?(@name) @source = :filesystem @filename = File.expand_path(@name) - raise MissingPackageError, :requested_package => @filename unless File.exist?(@filename) + raise MissingPackageError, :requested_package => @filename unless Puppet::FileSystem::File.exist?(@filename) parsed = parse_filename(@filename) @module_name = parsed[:module_name] diff --git a/lib/puppet/module_tool/checksums.rb b/lib/puppet/module_tool/checksums.rb index 0985b71e4..044357a8f 100644 --- a/lib/puppet/module_tool/checksums.rb +++ b/lib/puppet/module_tool/checksums.rb @@ -16,7 +16,7 @@ module Puppet::ModuleTool # Return checksum for the +Pathname+. def checksum(pathname) - return Digest::MD5.hexdigest(IO.binread(pathname)) + return Digest::MD5.hexdigest(Puppet::FileSystem::File.new(pathname).binread) end # Return checksums for object's +Pathname+, generate if it's needed. diff --git a/lib/puppet/module_tool/dependency.rb b/lib/puppet/module_tool/dependency.rb index 847a2e3c1..222e714c8 100644 --- a/lib/puppet/module_tool/dependency.rb +++ b/lib/puppet/module_tool/dependency.rb @@ -15,12 +15,16 @@ module Puppet::ModuleTool @repository = repository ? Puppet::Forge::Repository.new(repository) : nil end - # Return PSON representation of this data. - def to_pson(*args) + def to_data_hash result = { :name => @full_module_name } result[:version_requirement] = @version_requirement if @version_requirement && ! @version_requirement.nil? result[:repository] = @repository.to_s if @repository && ! @repository.nil? - result.to_pson(*args) + result + end + + # Return PSON representation of this data. + def to_pson(*args) + to_data_hash.to_pson(*args) end end end diff --git a/lib/puppet/module_tool/metadata.rb b/lib/puppet/module_tool/metadata.rb index 37a753626..650043802 100644 --- a/lib/puppet/module_tool/metadata.rb +++ b/lib/puppet/module_tool/metadata.rb @@ -129,7 +129,7 @@ module Puppet::ModuleTool end end - def to_hash() + def to_data_hash() return extra_metadata.merge({ 'name' => @full_module_name, 'version' => @version, @@ -145,9 +145,13 @@ module Puppet::ModuleTool }) end + def to_hash() + to_data_hash + end + # Return the PSON record representing this instance. def to_pson(*args) - return to_hash.to_pson(*args) + return to_data_hash.to_pson(*args) end end end diff --git a/lib/puppet/module_tool/tar.rb b/lib/puppet/module_tool/tar.rb index 4f9f87ed2..6b3257cf4 100644 --- a/lib/puppet/module_tool/tar.rb +++ b/lib/puppet/module_tool/tar.rb @@ -4,7 +4,8 @@ module Puppet::ModuleTool::Tar require 'puppet/module_tool/tar/mini' def self.instance(module_name) - if Facter.value('osfamily') == 'Solaris' && Puppet::Util.which('gtar') && ! Puppet::Util::Platform.windows? + gtar_platforms = ['Solaris', 'OpenBSD'] + if gtar_platforms.include?(Facter.value('osfamily')) && Puppet::Util.which('gtar') Solaris.new elsif Puppet::Util.which('tar') && ! Puppet::Util::Platform.windows? Gnu.new diff --git a/lib/puppet/module_tool/tar/gnu.rb b/lib/puppet/module_tool/tar/gnu.rb index 0e663b7fa..d8fc3378f 100644 --- a/lib/puppet/module_tool/tar/gnu.rb +++ b/lib/puppet/module_tool/tar/gnu.rb @@ -1,8 +1,12 @@ class Puppet::ModuleTool::Tar::Gnu + def initialize(command = "tar") + @command = command + end + def unpack(sourcefile, destdir, owner) - Puppet::Util::Execution.execute("tar xzf #{sourcefile} --no-same-permissions --no-same-owner -C #{destdir}") + Puppet::Util::Execution.execute("#{@command} xzf #{sourcefile} --no-same-owner -C #{destdir}") Puppet::Util::Execution.execute("find #{destdir} -type d -exec chmod 755 {} +") - Puppet::Util::Execution.execute("find #{destdir} -type f -exec chmod 644 {} +") + Puppet::Util::Execution.execute("find #{destdir} -type f -exec chmod a-wst {} +") Puppet::Util::Execution.execute("chown -R #{owner} #{destdir}") end diff --git a/lib/puppet/module_tool/tar/mini.rb b/lib/puppet/module_tool/tar/mini.rb index 8288876ae..f64577ea0 100644 --- a/lib/puppet/module_tool/tar/mini.rb +++ b/lib/puppet/module_tool/tar/mini.rb @@ -7,6 +7,8 @@ class Puppet::ModuleTool::Tar::Mini Zlib::GzipReader.open(sourcefile) do |reader| Archive::Tar::Minitar.unpack(reader, destdir) do |action, name, stats| case action + when :file_done + File.chmod(0444, "#{destdir}/#{name}") when :dir, :file_start validate_entry(destdir, name) Puppet.debug("extracting #{destdir}/#{name}") diff --git a/lib/puppet/module_tool/tar/solaris.rb b/lib/puppet/module_tool/tar/solaris.rb index c618fbf67..fb3e58bbf 100644 --- a/lib/puppet/module_tool/tar/solaris.rb +++ b/lib/puppet/module_tool/tar/solaris.rb @@ -1,8 +1,5 @@ class Puppet::ModuleTool::Tar::Solaris < Puppet::ModuleTool::Tar::Gnu - def unpack(sourcefile, destdir, owner) - Puppet::Util::Execution.execute("gtar xzf #{sourcefile} --no-same-permissions --no-same-owner -C #{destdir}") - Puppet::Util::Execution.execute("find #{destdir} -type d -exec chmod 755 {} +") - Puppet::Util::Execution.execute("find #{destdir} -type f -exec chmod 644 {} +") - Puppet::Util::Execution.execute("chown -R #{owner} #{destdir}") + def initialize + super("gtar") end end diff --git a/lib/puppet/network/authconfig.rb b/lib/puppet/network/authconfig.rb index 1c3eaede6..527774598 100644 --- a/lib/puppet/network/authconfig.rb +++ b/lib/puppet/network/authconfig.rb @@ -3,8 +3,6 @@ require 'puppet/network/rights' module Puppet class ConfigurationError < Puppet::Error; end class Network::AuthConfig - - extend MonitorMixin attr_accessor :rights DEFAULT_ACL = [ diff --git a/lib/puppet/network/authentication.rb b/lib/puppet/network/authentication.rb index 91e16db72..f10e461d3 100644 --- a/lib/puppet/network/authentication.rb +++ b/lib/puppet/network/authentication.rb @@ -14,7 +14,7 @@ module Puppet::Network::Authentication certs << Puppet::SSL::CertificateAuthority.instance.host.certificate if Puppet::SSL::CertificateAuthority.ca? # Always check the host cert if we have one, this will be the agent or master cert depending on the run mode - certs << Puppet::SSL::Host.localhost.certificate if FileTest.exist?(Puppet[:hostcert]) + certs << Puppet::SSL::Host.localhost.certificate if Puppet::FileSystem::File.exist?(Puppet[:hostcert]) # Remove nil values for caller convenience certs.compact.each do |cert| diff --git a/lib/puppet/network/authstore.rb b/lib/puppet/network/authstore.rb index 90498002d..ba86ed90b 100755..100644 --- a/lib/puppet/network/authstore.rb +++ b/lib/puppet/network/authstore.rb @@ -82,21 +82,20 @@ module Puppet end def interpolate(match) - Thread.current[:declarations] = @declarations.collect { |ace| ace.interpolate(match) }.sort + @modified_declarations = @declarations.collect { |ace| ace.interpolate(match) }.sort end def reset_interpolation - Thread.current[:declarations] = nil + @modified_declarations = nil end private - # returns our ACEs list, but if we have a modification of it - # in our current thread, let's return it - # this is used if we want to override the this purely immutable list - # by a modified version in a multithread safe way. + # Returns our ACEs list, but if we have a modification of it, let's return + # it. This is used if we want to override the this purely immutable list + # by a modified version. def declarations - Thread.current[:declarations] || @declarations + @modified_declarations || @declarations end # Store the results of a pattern into our hash. Basically just diff --git a/lib/puppet/network/format.rb b/lib/puppet/network/format.rb index 69895c344..e50dfd32a 100644 --- a/lib/puppet/network/format.rb +++ b/lib/puppet/network/format.rb @@ -1,10 +1,9 @@ -require 'puppet/provider' -require 'puppet/provider/confiner' +require 'puppet/confiner' # A simple class for modeling encoding formats for moving # instances around the network. class Puppet::Network::Format - include Puppet::Provider::Confiner + include Puppet::Confiner attr_reader :name, :mime attr_accessor :intern_method, :render_method, :intern_multiple_method, :render_multiple_method, :weight, :required_methods, :extension diff --git a/lib/puppet/network/format_handler.rb b/lib/puppet/network/format_handler.rb index 880b58b3c..d341b4335 100644 --- a/lib/puppet/network/format_handler.rb +++ b/lib/puppet/network/format_handler.rb @@ -5,6 +5,8 @@ require 'puppet/network/format' module Puppet::Network::FormatHandler class FormatError < Puppet::Error; end + ALL_MEDIA_TYPES = '*/*'.freeze + @formats = {} def self.create(*args, &block) @@ -70,24 +72,20 @@ module Puppet::Network::FormatHandler # that generally conforms to an HTTP Accept header. Any quality specifiers # are ignored and instead the formats are simply in strict preference order # (most preferred is first) - # @param supported [Array<Symbol>] the names of the supported formats (order - # does not matter) + # @param supported [Array<Symbol>] the names of the supported formats (the + # most preferred format is first) # @return [Puppet::Network::Format, nil] the most suitable format # @api private def self.most_suitable_format_for(accepted, supported) format_name = accepted.collect do |accepted| accepted.to_s.sub(/;q=.*$/, '') end.collect do |accepted| - begin - if accepted == '*/*' - formats - else - format_to_canonical_name(accepted) - end - rescue ArgumentError - nil + if accepted == ALL_MEDIA_TYPES + supported.first + else + format_to_canonical_name_or_nil(accepted) end - end.flatten.find do |accepted| + end.find do |accepted| supported.include?(accepted) end @@ -95,6 +93,13 @@ module Puppet::Network::FormatHandler format_for(format_name) end end + + # @api private + def self.format_to_canonical_name_or_nil(format) + format_to_canonical_name(format) + rescue ArgumentError + nil + end end require 'puppet/network/formats' diff --git a/lib/puppet/network/format_support.rb b/lib/puppet/network/format_support.rb index 7cc6cc001..79f7fe665 100644 --- a/lib/puppet/network/format_support.rb +++ b/lib/puppet/network/format_support.rb @@ -1,6 +1,7 @@ require 'puppet/network/format_handler' # Provides network serialization support when included +# @api public module Puppet::Network::FormatSupport def self.included(klass) klass.extend(ClassMethods) @@ -83,6 +84,10 @@ module Puppet::Network::FormatSupport end end + def to_msgpack(*args) + to_data_hash.to_msgpack(*args) + end + def render(format = nil) format ||= self.class.default_format @@ -102,5 +107,14 @@ module Puppet::Network::FormatSupport def support_format?(name) self.class.support_format?(name) end + + # @comment Document to_data_hash here as it is called as a hook from to_msgpack if it exists + # @!method to_data_hash(*args) + # @api public + # @abstract + # This method may be implemented to return a hash object that is used for serializing. + # The object returned by this method should contain all the info needed to instantiate it again. + # If the method exists it will be called from to_msgpack and other serialization methods. + # @return [Hash] end diff --git a/lib/puppet/network/formats.rb b/lib/puppet/network/formats.rb index 4aaeddedd..62e40d376 100644 --- a/lib/puppet/network/formats.rb +++ b/lib/puppet/network/formats.rb @@ -1,5 +1,31 @@ require 'puppet/network/format_handler' +Puppet::Network::FormatHandler.create_serialized_formats(:msgpack, :weight => 20, :mime => "application/x-msgpack", :required_methods => [:render_method, :intern_method]) do + def intern(klass, text) + data = MessagePack.unpack(text) + return data if data.is_a?(klass) + klass.from_pson(data) + end + + def intern_multiple(klass, text) + MessagePack.unpack(text).collect do |data| + klass.from_pson(data) + end + end + + def render(instance) + instance.to_msgpack + end + + def render_multiple(instances) + instances.to_msgpack + end + + def supported?(klass) + Puppet.features.msgpack? && klass.method_defined?(:to_msgpack) + end +end + Puppet::Network::FormatHandler.create_serialized_formats(:yaml) do def intern(klass, text) data = YAML.load(text, :safe => true, :deserialize_symbols => true) diff --git a/lib/puppet/network/http/connection.rb b/lib/puppet/network/http/connection.rb index c75bab97c..adb166439 100644 --- a/lib/puppet/network/http/connection.rb +++ b/lib/puppet/network/http/connection.rb @@ -24,17 +24,17 @@ module Puppet::Network::HTTP OPTION_DEFAULTS = { :use_ssl => true, - :verify_peer => true, + :verify => nil, :redirect_limit => 10 } - # Creates a new HTTP client connection to `host`:`port`. + # Creates a new HTTP client connection to `host`:`port`. # @param host [String] the host to which this client will connect to # @param port [Fixnum] the port to which this client will connect to # @param options [Hash] options influencing the properties of the created connection, # the following options are recognized: # :use_ssl [Boolean] true to connect with SSL, false otherwise, defaults to true - # :verify_peer [Boolean] true to verify the peer's certificate, false otherwise, defaults to true + # :verify [#setup_connection] An object that will configure any verification to do on the connection # :redirect_limit [Fixnum] the number of allowed redirections, defaults to 10 # passing any other option in the options hash results in a Puppet::Error exception # @note the HTTP connection itself happens lazily only when {#request}, or one of the {#get}, {#post}, {#delete}, {#head} or {#put} is called @@ -48,7 +48,7 @@ module Puppet::Network::HTTP options = OPTION_DEFAULTS.merge(options) @use_ssl = options[:use_ssl] - @verify_peer = options[:verify_peer] + @verify = options[:verify] @redirect_limit = options[:redirect_limit] end @@ -128,23 +128,19 @@ module Puppet::Network::HTTP end def execute_request(method, *args) - ssl_validator = Puppet::SSL::Validator.new(:ssl_configuration => ssl_configuration) - # Perform our own validation of the SSL connection in addition to OpenSSL - ssl_validator.register_verify_callback(connection) - response = connection.send(method, *args) # Check the peer certs and warn if they're nearing expiration. - warn_if_near_expiration(*ssl_validator.peer_certs) + warn_if_near_expiration(*@verify.peer_certs) response rescue OpenSSL::SSL::SSLError => error if error.message.include? "certificate verify failed" msg = error.message - msg << ": [" + ssl_validator.verify_errors.join('; ') + "]" + msg << ": [" + @verify.verify_errors.join('; ') + "]" raise Puppet::Error, msg elsif error.message =~ /hostname (\w+ )?not match/ - leaf_ssl_cert = ssl_validator.peer_certs.last + leaf_ssl_cert = @verify.peer_certs.last valid_certnames = [leaf_ssl_cert.name, *leaf_ssl_cert.subject_alt_names].uniq msg = valid_certnames.length > 1 ? "one of #{valid_certnames.join(', ')}" : valid_certnames.first @@ -181,23 +177,7 @@ module Puppet::Network::HTTP # Use cert information from a Puppet client to set up the http object. def cert_setup - if @verify_peer and FileTest.exist?(Puppet[:hostcert]) and FileTest.exist?(ssl_configuration.ca_auth_file) - @connection.cert_store = ssl_host.ssl_store - @connection.ca_file = ssl_configuration.ca_auth_file - @connection.cert = ssl_host.certificate.content - @connection.verify_mode = OpenSSL::SSL::VERIFY_PEER - @connection.key = ssl_host.key.content - else - # We don't have the local certificates, so we don't do any verification - # or setup at this early stage. REVISIT: Shouldn't we supply the local - # certificate details if we have them? The original code didn't. - # --daniel 2012-06-03 - - # Ruby 1.8 defaulted to this, but 1.9 defaults to peer verify, - # and we almost always talk to a dedicated, not-standard CA that - # isn't trusted out of the box. This forces the expected state. - @connection.verify_mode = OpenSSL::SSL::VERIFY_NONE - end + @verify.setup_connection(@connection) end # This method largely exists for testing purposes, so that we can @@ -205,18 +185,5 @@ module Puppet::Network::HTTP def create_connection(*args) Net::HTTP.new(*args) end - - # Use the global localhost instance. - def ssl_host - Puppet::SSL::Host.localhost - end - - def ssl_configuration - @ssl_configuration ||= Puppet::SSL::Configuration.new( - Puppet[:localcacert], - :ca_chain_file => Puppet[:ssl_client_ca_chain], - :ca_auth_file => Puppet[:ssl_client_ca_auth] - ) - end end end diff --git a/lib/puppet/network/http/handler.rb b/lib/puppet/network/http/handler.rb index b8abd80e4..3b86195d0 100644 --- a/lib/puppet/network/http/handler.rb +++ b/lib/puppet/network/http/handler.rb @@ -89,15 +89,20 @@ module Puppet::Network::HTTP::Handler configure_profiler(request_headers, request_params) Puppet::Util::Profiler.profile("Processed request #{request_method} #{request_path}") do - indirection, method, key, params = uri2indirection(request_method, request_path, request_params) + indirection_name, method, key, params = uri2indirection(request_method, request_path, request_params) - check_authorization(indirection, method, key, params) + check_authorization(indirection_name, method, key, params) warn_if_near_expiration(client_cert(request)) + indirection = Puppet::Indirector::Indirection.instance(indirection_name.to_sym) + raise ArgumentError, "Could not find indirection '#{indirection_name}'" unless indirection + + if !indirection.allow_remote_requests? + raise HTTPNotFoundError, "No handler for #{indirection.name}" + end + send("do_#{method}", indirection, key, params, request, response) end - rescue SystemExit,NoMemoryError - raise rescue HTTPError => e return do_http_control_exception(response, e) rescue Exception => e @@ -129,19 +134,13 @@ module Puppet::Network::HTTP::Handler set_response(response, exception.to_s, status) end - def model(indirection_name) - raise ArgumentError, "Could not find indirection '#{indirection_name}'" unless indirection = Puppet::Indirector::Indirection.instance(indirection_name.to_sym) - indirection.model - end - # Execute our find. - def do_find(indirection_name, key, params, request, response) - model_class = model(indirection_name) - unless result = model_class.indirection.find(key, params) - raise HTTPNotFoundError, "Could not find #{indirection_name} #{key}" + def do_find(indirection, key, params, request, response) + unless result = indirection.find(key, params) + raise HTTPNotFoundError, "Could not find #{indirection.name} #{key}" end - format = accepted_response_formatter_for(model_class, request) + format = accepted_response_formatter_for(indirection.model, request) set_content_type(response, format) rendered_result = result @@ -157,9 +156,9 @@ module Puppet::Network::HTTP::Handler end # Execute our head. - def do_head(indirection_name, key, params, request, response) - unless self.model(indirection_name).indirection.head(key, params) - raise HTTPNotFoundError, "Could not find #{indirection_name} #{key}" + def do_head(indirection, key, params, request, response) + unless indirection.head(key, params) + raise HTTPNotFoundError, "Could not find #{indirection.name} #{key}" end # No need to set a response because no response is expected from a @@ -167,38 +166,35 @@ module Puppet::Network::HTTP::Handler end # Execute our search. - def do_search(indirection_name, key, params, request, response) - model = self.model(indirection_name) - result = model.indirection.search(key, params) + def do_search(indirection, key, params, request, response) + result = indirection.search(key, params) if result.nil? - raise HTTPNotFoundError, "Could not find instances in #{indirection_name} with '#{key}'" + raise HTTPNotFoundError, "Could not find instances in #{indirection.name} with '#{key}'" end - format = accepted_response_formatter_for(model, request) + format = accepted_response_formatter_for(indirection.model, request) set_content_type(response, format) - set_response(response, model.render_multiple(format, result)) + set_response(response, indirection.model.render_multiple(format, result)) end # Execute our destroy. - def do_destroy(indirection_name, key, params, request, response) - model_class = model(indirection_name) - formatter = accepted_response_formatter_or_yaml_for(model_class, request) + def do_destroy(indirection, key, params, request, response) + formatter = accepted_response_formatter_or_yaml_for(indirection.model, request) - result = model_class.indirection.destroy(key, params) + result = indirection.destroy(key, params) set_content_type(response, formatter) set_response(response, formatter.render(result)) end # Execute our save. - def do_save(indirection_name, key, params, request, response) - model_class = model(indirection_name) - formatter = accepted_response_formatter_or_yaml_for(model_class, request) - sent_object = read_body_into_model(model_class, request) + def do_save(indirection, key, params, request, response) + formatter = accepted_response_formatter_or_yaml_for(indirection.model, request) + sent_object = read_body_into_model(indirection.model, request) - result = model_class.indirection.save(sent_object, key) + result = indirection.save(sent_object, key) set_content_type(response, formatter) set_response(response, formatter.render(result)) diff --git a/lib/puppet/network/http/webrick.rb b/lib/puppet/network/http/webrick.rb index 123b493c0..869900dad 100644 --- a/lib/puppet/network/http/webrick.rb +++ b/lib/puppet/network/http/webrick.rb @@ -10,7 +10,6 @@ require 'puppet/ssl/configuration' class Puppet::Network::HTTP::WEBrick def initialize @listening = false - @mutex = Mutex.new end def listen(address, port) @@ -25,34 +24,28 @@ class Puppet::Network::HTTP::WEBrick @server.mount('/', Puppet::Network::HTTP::WEBrickREST, :this_value_is_apparently_necessary_but_unused) - @mutex.synchronize do - raise "WEBrick server is already listening" if @listening - @listening = true - @thread = Thread.new { - @server.start { |sock| - raise "Client disconnected before connection could be established" unless IO.select([sock],nil,nil,6.2) - sock.accept - @server.run(sock) - } - } - sleep 0.1 until @server.status == :Running + raise "WEBrick server is already listening" if @listening + @listening = true + @thread = Thread.new do + @server.start do |sock| + raise "Client disconnected before connection could be established" unless IO.select([sock],nil,nil,6.2) + sock.accept + @server.run(sock) + end end + sleep 0.1 until @server.status == :Running end def unlisten - @mutex.synchronize do - raise "WEBrick server is not listening" unless @listening - @server.shutdown - wait_for_shutdown - @server = nil - @listening = false - end + raise "WEBrick server is not listening" unless @listening + @server.shutdown + wait_for_shutdown + @server = nil + @listening = false end def listening? - @mutex.synchronize do - @listening - end + @listening end def wait_for_shutdown diff --git a/lib/puppet/network/http_pool.rb b/lib/puppet/network/http_pool.rb index f037f318e..97094c9ad 100644 --- a/lib/puppet/network/http_pool.rb +++ b/lib/puppet/network/http_pool.rb @@ -2,18 +2,52 @@ require 'puppet/network/http/connection' module Puppet::Network; end -# This class is basically a placeholder for managing a pool of HTTP connections; -# at present it does not actually attempt to pool them. Historically, it did -# attempt to do so, but this didn't work well based on Puppet's threading model. -# The pooling functionality has been removed, but this abstraction is still here -# because the API is used in various places and because it could be useful -# should we decide to implement pooling at some point in the future. +# This module contains the factory methods that should be used for getting a +# Puppet::Network::HTTP::Connection instance. +# +# The name "HttpPool" is a misnomer, and a leftover of history, but we would +# like to make this cache connections in the future. +# +# @api public +# module Puppet::Network::HttpPool - # Retrieve a cached http instance if caching is enabled, else return - # a new one. + # Retrieve a connection for the given host and port. + # + # @param host [String] The hostname to connect to + # @param port [Integer] The port on the host to connect to + # @param use_ssl [Boolean] Whether to use an SSL connection + # @param verify_peer [Boolean] Whether to verify the peer credentials, if possible. Verification will not take place if the CA certificate is missing. + # @return [Puppet::Network::HTTP::Connection] + # + # @api public + # def self.http_instance(host, port, use_ssl = true, verify_peer = true) - Puppet::Network::HTTP::Connection.new(host, port, :use_ssl => use_ssl, :verify_peer => verify_peer) + verifier = if verify_peer + Puppet::SSL::Validator.default_validator() + else + Puppet::SSL::Validator.no_validator() + end + + Puppet::Network::HTTP::Connection.new(host, port, + :use_ssl => use_ssl, + :verify => verifier) end + # Get an http connection that will be secured with SSL and have the + # connection verified with the given verifier + # + # @param host [String] the DNS name to connect to + # @param port [Integer] the port to connect to + # @param verifier [#setup_connection, #peer_certs, #verify_errors] An object that will setup the appropriate + # verification on a Net::HTTP instance and report any errors and the certificates used. + # @return [Puppet::Network::HTTP::Connection] + # + # @api public + # + def self.http_ssl_instance(host, port, verifier = Puppet::SSL::Validator.default_validator()) + Puppet::Network::HTTP::Connection.new(host, port, + :use_ssl => true, + :verify => verifier) + end end diff --git a/lib/puppet/network/rights.rb b/lib/puppet/network/rights.rb index f7420a90e..f7420a90e 100755..100644 --- a/lib/puppet/network/rights.rb +++ b/lib/puppet/network/rights.rb diff --git a/lib/puppet/node.rb b/lib/puppet/node.rb index b01a4665d..09b307927 100644 --- a/lib/puppet/node.rb +++ b/lib/puppet/node.rb @@ -16,7 +16,7 @@ class Puppet::Node indirects :node, :terminus_setting => :node_terminus, :doc => "Where to find node information. A node is composed of its name, its facts, and its environment." - attr_accessor :name, :classes, :source, :ipaddress, :parameters + attr_accessor :name, :classes, :source, :ipaddress, :parameters, :trusted_data attr_reader :time, :facts ::PSON.register_document_type('Node',self) @@ -31,17 +31,25 @@ class Puppet::Node node end - def to_pson(*args) + def to_data_hash result = { + 'name' => name, + 'environment' => environment.name, + } + result['classes'] = classes unless classes.empty? + result['parameters'] = parameters unless parameters.empty? + result + end + + def to_pson_data_hash(*args) + { 'document_type' => "Node", - 'data' => {} + 'data' => to_data_hash, } - result['data']['name'] = name - result['data']['classes'] = classes unless classes.empty? - result['data']['parameters'] = parameters unless parameters.empty? - result['data']['environment'] = environment.name + end - result.to_pson(*args) + def to_pson(*args) + to_pson_data_hash.to_pson(*args) end def environment @@ -84,6 +92,7 @@ class Puppet::Node # Merge the node facts with parameters from the node source. def fact_merge if @facts = Puppet::Node::Facts.indirection.find(name, :environment => environment) + @facts.sanitize merge(@facts.values) end rescue => detail @@ -143,4 +152,11 @@ class Puppet::Node end tmp.reverse end + + # Ensures the data is frozen + # + def trusted_data=(data) + Puppet.warning("Trusted node data modified for node #{name}") unless @trusted_data.nil? + @trusted_data = data.freeze + end end diff --git a/lib/puppet/node/environment.rb b/lib/puppet/node/environment.rb index 25b454638..f79b8b35b 100644 --- a/lib/puppet/node/environment.rb +++ b/lib/puppet/node/environment.rb @@ -10,20 +10,18 @@ end # Puppet::Node::Environment acts as a container for all configuration # that is expected to vary between environments. # -# ## Thread local variables +# ## Global variables # -# The Puppet::Node::Environment uses a number of `Thread.current` variables. -# Since all web servers that Puppet runs on are single threaded these -# variables are effectively global. +# The Puppet::Node::Environment uses a number of global variables. # -# ### `Thread.current[:environment]` +# ### `$environment` # -# The 'environment' thread variable represents the current environment that's +# The 'environment' global variable represents the current environment that's # being used in the compiler. # -# ### `Thread.current[:known_resource_types]` +# ### `$known_resource_types` # -# The 'known_resource_types' thread variable represents a singleton instance +# The 'known_resource_types' global variable represents a singleton instance # of the Puppet::Resource::TypeCollection class. The variable is discarded # and regenerated if it is accessed by an environment that doesn't match the # environment of the 'known_resource_types' @@ -129,7 +127,7 @@ class Puppet::Node::Environment # @return [Puppet::Node::Environment] the currently set environment if one # has been explicitly set, else it will return the '*root*' environment def self.current - Thread.current[:environment] || root + $environment || root end # Set the environment for the current thread @@ -145,7 +143,7 @@ class Puppet::Node::Environment # # @param env [Puppet::Node::Environment] def self.current=(env) - Thread.current[:environment] = new(env) + $environment = new(env) end @@ -164,7 +162,7 @@ class Puppet::Node::Environment # @api private def self.clear @seen.clear - Thread.current[:environment] = nil + $environment = nil end # @!attribute [r] name @@ -192,17 +190,16 @@ class Puppet::Node::Environment # @param name [Symbol] The environment name def initialize(name) @name = name - extend MonitorMixin end # The current global TypeCollection # # @note The environment is loosely coupled with the {Puppet::Resource::TypeCollection} # class. While there is a 1:1 relationship between an environment and a - # TypeCollection instance, there is only one TypeCollection instance available - # at any given time. It is stored in the Thread.current collection as - # 'known_resource_types'. 'known_resource_types' is accessed as an instance - # method, but is global to all environment variables. + # TypeCollection instance, there is only one TypeCollection instance + # available at any given time. It is stored in `$known_resource_types`. + # `$known_resource_types` is accessed as an instance method, but is global + # to all environment variables. # # @api public # @return [Puppet::Resource::TypeCollection] The current global TypeCollection @@ -212,14 +209,15 @@ class Puppet::Node::Environment # always just return our thread's known-resource types. Only at the start # of a compilation (after our thread var has been set to nil) or when the # environment has changed do we delve deeper. - Thread.current[:known_resource_types] = nil if (krt = Thread.current[:known_resource_types]) && krt.environment != self - Thread.current[:known_resource_types] ||= synchronize { + $known_resource_types = nil if $known_resource_types && $known_resource_types.environment != self + $known_resource_types ||= if @known_resource_types.nil? or @known_resource_types.require_reparse? @known_resource_types = Puppet::Resource::TypeCollection.new(self) @known_resource_types.import_ast(perform_initial_import, '') + @known_resource_types + else + @known_resource_types end - @known_resource_types - } end # Yields each modules' plugin directory if the plugin directory (modulename/lib) diff --git a/lib/puppet/node/facts.rb b/lib/puppet/node/facts.rb index b18a1e88d..2be4e68b9 100755..100644 --- a/lib/puppet/node/facts.rb +++ b/lib/puppet/node/facts.rb @@ -84,16 +84,33 @@ class Puppet::Node::Facts new_facts end - def to_pson(*args) + def to_data_hash result = { 'name' => name, 'values' => strip_internal, } - result['timestamp'] = timestamp if timestamp - result['expiration'] = expiration if expiration + if timestamp + if timestamp.is_a? Time + result['timestamp'] = timestamp.iso8601(9) + else + result['timestamp'] = timestamp + end + end + + if expiration + if expiration.is_a? Time + result['expiration'] = expiration.iso8601(9) + else + result['expiration'] = expiration + end + end - result.to_pson(*args) + result + end + + def to_pson(*args) + to_data_hash.to_pson(*args) end # Add internal data to the facts for storage. @@ -109,8 +126,6 @@ class Puppet::Node::Facts self.values['_timestamp'] end - private - # Strip out that internal data. def strip_internal newvals = values.dup @@ -118,6 +133,8 @@ class Puppet::Node::Facts newvals end + private + def sanitize_fact(fact) if fact.is_a? Hash then ret = {} diff --git a/lib/puppet/parameter.rb b/lib/puppet/parameter.rb index cce9fb57e..f265a99ae 100644 --- a/lib/puppet/parameter.rb +++ b/lib/puppet/parameter.rb @@ -116,10 +116,13 @@ class Puppet::Parameter @doc ||= "" unless defined?(@addeddocvals) - @doc += value_collection.doc + @doc = Puppet::Util::Docs.scrub(@doc) + if vals = value_collection.doc + @doc << "\n\n#{vals}" + end if f = self.required_features - @doc += " Requires features #{f.flatten.collect { |f| f.to_s }.join(" ")}." + @doc << "\n\nRequires features #{f.flatten.collect { |f| f.to_s }.join(" ")}." end @addeddocvals = true end @@ -567,6 +570,16 @@ class Puppet::Parameter "'#{value}'" end end + + # @comment Document post_compile_hook here as it does not exist anywhere (called from type if implemented) + # @!method post_compile() + # @since 3.4.0 + # @api public + # @abstract A subclass may implement this - it is not implemented in the Parameter class + # This method may be implemented by a parameter in order to perform actions during compilation + # after all resources have been added to the catalog. + # @see Puppet::Type#finish + # @see Puppet::Parser::Compiler#finish end require 'puppet/parameter/path' diff --git a/lib/puppet/parameter/boolean.rb b/lib/puppet/parameter/boolean.rb index 86f0c05dd..11ad80729 100644 --- a/lib/puppet/parameter/boolean.rb +++ b/lib/puppet/parameter/boolean.rb @@ -7,4 +7,9 @@ class Puppet::Parameter::Boolean < Puppet::Parameter def unsafe_munge(value) Puppet::Coercion.boolean(value) end + + def self.initvars + super + @value_collection.newvalues(*Puppet::Coercion.boolean_values) + end end diff --git a/lib/puppet/parameter/value_collection.rb b/lib/puppet/parameter/value_collection.rb index 7e2bee331..4cbd95d6a 100644 --- a/lib/puppet/parameter/value_collection.rb +++ b/lib/puppet/parameter/value_collection.rb @@ -33,17 +33,19 @@ class Puppet::Parameter::ValueCollection unless defined?(@doc) @doc = "" unless values.empty? - @doc += " Valid values are " - @doc += @strings.collect do |value| + @doc << "Valid values are " + @doc << @strings.collect do |value| if aliases = value.aliases and ! aliases.empty? "`#{value.name}` (also called `#{aliases.join(", ")}`)" else "`#{value.name}`" end - end.join(", ") + "." + end.join(", ") << ". " end - @doc += " Values can match `" + regexes.join("`, `") + "`." unless regexes.empty? + unless regexes.empty? + @doc << "Values can match `#{regexes.join("`, `")}`." + end end @doc diff --git a/lib/puppet/parser/ast/resourceparam.rb b/lib/puppet/parser/ast/resourceparam.rb index a3be0a674..04e945c52 100644 --- a/lib/puppet/parser/ast/resourceparam.rb +++ b/lib/puppet/parser/ast/resourceparam.rb @@ -11,9 +11,10 @@ class Puppet::Parser::AST # Return the parameter and the value. def evaluate(scope) + value = @value.safeevaluate(scope) return Puppet::Parser::Resource::Param.new( :name => @param, - :value => @value.safeevaluate(scope), + :value => value.nil? ? :undef : value, :source => scope.source, :line => self.line, :file => self.file, :add => self.add ) diff --git a/lib/puppet/parser/compiler.rb b/lib/puppet/parser/compiler.rb index 20f668504..2ea34c1de 100644 --- a/lib/puppet/parser/compiler.rb +++ b/lib/puppet/parser/compiler.rb @@ -17,13 +17,8 @@ class Puppet::Parser::Compiler include Puppet::Resource::TypeCollectionHelper def self.compile(node) - # We get these from the environment and only cache them in a thread - # variable for the duration of the compilation. If nothing else is using - # the thread, though, we can leave 'em hanging round with no ill effects, - # and this is safer than cleaning them at the end and assuming that will - # stick until the next entry to this function. - Thread.current[:known_resource_types] = nil - Thread.current[:env_module_directories] = nil + $known_resource_types = nil + $env_module_directories = nil # ...and we actually do the compile now we have caching ready. new(node).compile.to_resource @@ -143,8 +138,27 @@ class Puppet::Parser::Compiler end # Evaluate all of the classes specified by the node. + # Classes with parameters are evaluated as if they were declared. + # Classes without parameters or with an empty set of parameters are evaluated + # as if they were included. This means classes with an empty set of + # parameters won't conflict even if the class has already been included. def evaluate_node_classes - evaluate_classes(@node.classes, @node_scope || topscope) + if @node.classes.is_a? Hash + classes_with_params, classes_without_params = @node.classes.partition {|name,params| params and !params.empty?} + + # The results from Hash#partition are arrays of pairs rather than hashes, + # so we have to convert to the forms evaluate_classes expects (Hash, and + # Array of class names) + classes_with_params = Hash[classes_with_params] + classes_without_params.map!(&:first) + else + classes_with_params = {} + classes_without_params = @node.classes + end + + evaluate_classes(classes_without_params, @node_scope || topscope) + + evaluate_classes(classes_with_params, @node_scope || topscope) end # Evaluate each specified class in turn. If there are any classes we can't @@ -487,10 +501,12 @@ class Puppet::Parser::Compiler node.parameters.each do |param, value| @topscope[param.to_s] = value end - # These might be nil. catalog.client_version = node.parameters["clientversion"] catalog.server_version = node.parameters["serverversion"] + if Puppet[:trusted_node_data] + @topscope.set_trusted(node.trusted_data) + end end def create_settings_scope diff --git a/lib/puppet/parser/files.rb b/lib/puppet/parser/files.rb index 1d5b64966..49f36019f 100644 --- a/lib/puppet/parser/files.rb +++ b/lib/puppet/parser/files.rb @@ -41,7 +41,7 @@ module Puppet; module Parser; module Files template_paths.collect { |path| File::join(path, template) }.each do |f| - return f if FileTest.exist?(f) + return f if Puppet::FileSystem::File.exist?(f) end end diff --git a/lib/puppet/parser/functions.rb b/lib/puppet/parser/functions.rb index 7015ec4d9..360387727 100644 --- a/lib/puppet/parser/functions.rb +++ b/lib/puppet/parser/functions.rb @@ -1,6 +1,5 @@ require 'puppet/util/autoload' require 'puppet/parser/scope' -require 'monitor' # A module for managing parser functions. Each specified function # is added to a central module that then gets included into the Scope @@ -18,8 +17,8 @@ module Puppet::Parser::Functions # # @api private def self.reset - @functions = Hash.new { |h,k| h[k] = {} }.extend(MonitorMixin) - @modules = Hash.new.extend(MonitorMixin) + @functions = Hash.new { |h,k| h[k] = {} } + @modules = Hash.new # Runs a newfunction to create a function for each of the log levels Puppet::Util::Log.levels.each do |level| @@ -46,9 +45,7 @@ module Puppet::Parser::Functions if env and ! env.is_a?(Puppet::Node::Environment) env = Puppet::Node::Environment.new(env) end - @modules.synchronize { - @modules[ (env || Environment.current || Environment.root).name ] ||= Module.new - } + @modules[ (env || Environment.current || Environment.root).name ] ||= Module.new end # Create a new Puppet DSL function. @@ -170,11 +167,9 @@ module Puppet::Parser::Functions name = name.intern func = nil - @functions.synchronize do - unless func = get_function(name) - autoloader.load(name, Environment.current) - func = get_function(name) - end + unless func = get_function(name) + autoloader.load(name, Environment.current) + func = get_function(name) end if func @@ -190,14 +185,14 @@ module Puppet::Parser::Functions ret = "" merged_functions.sort { |a,b| a[0].to_s <=> b[0].to_s }.each do |name, hash| - ret += "#{name}\n#{"-" * name.to_s.length}\n" + ret << "#{name}\n#{"-" * name.to_s.length}\n" if hash[:doc] - ret += Puppet::Util::Docs.scrub(hash[:doc]) + ret << Puppet::Util::Docs.scrub(hash[:doc]) else - ret += "Undocumented.\n" + ret << "Undocumented.\n" end - ret += "\n\n- *Type*: #{hash[:type]}\n\n" + ret << "\n\n- *Type*: #{hash[:type]}\n\n" end ret @@ -229,9 +224,7 @@ module Puppet::Parser::Functions private def merged_functions - @functions.synchronize { - @functions[Environment.root].merge(@functions[Environment.current]) - } + @functions[Environment.root].merge(@functions[Environment.current]) end def get_function(name) @@ -241,9 +234,7 @@ module Puppet::Parser::Functions def add_function(name, func) name = name.intern - @functions.synchronize { - @functions[Environment.current][name] = func - } + @functions[Environment.current][name] = func end end diff --git a/lib/puppet/parser/functions/collect.rb b/lib/puppet/parser/functions/collect.rb index 5485b36af..e30a80bb1 100644 --- a/lib/puppet/parser/functions/collect.rb +++ b/lib/puppet/parser/functions/collect.rb @@ -1,44 +1,15 @@ -require 'puppet/parser/ast/lambda' - Puppet::Parser::Functions::newfunction( :collect, :type => :rvalue, :arity => 2, :doc => <<-'ENDHEREDOC') do |args| - Applies a parameterized block to each element in a sequence of entries from the first - argument and returns an array with the result of each invocation of the parameterized block. - - This function takes two mandatory arguments: the first should be an Array or a Hash, and the second - a parameterized block as produced by the puppet syntax: - - $a.collect |$x| { ... } - - When the first argument `$a` is an Array, the block is called with each entry in turn. When the first argument - is a hash the entry is an array with `[key, value]`. - - *Examples* + The 'collect' function has been renamed to 'map'. Please update your manifests. - # Turns hash into array of values - $a.collect |$x|{ $x[1] } - - # Turns hash into array of keys - $a.collect |$x| { $x[0] } - - - Since 3.2 + The collect function is reserved for future use. + - Removed as of 3.4 - requires `parser = future`. ENDHEREDOC - receiver = args[0] - pblock = args[1] - - raise ArgumentError, ("collect(): wrong argument type (#{pblock.class}; must be a parameterized block.") unless pblock.is_a? Puppet::Parser::AST::Lambda - - case receiver - when Array - when Hash - else - raise ArgumentError, ("collect(): wrong argument type (#{receiver.class}; must be an Array or a Hash.") - end - - receiver.to_a.collect {|x| pblock.call(self, x) } -end + raise NotImplementedError, + "The 'collect' function has been renamed to 'map'. Please update your manifests." +end
\ No newline at end of file diff --git a/lib/puppet/parser/functions/contain.rb b/lib/puppet/parser/functions/contain.rb new file mode 100644 index 000000000..8eb514561 --- /dev/null +++ b/lib/puppet/parser/functions/contain.rb @@ -0,0 +1,26 @@ +# Called within a class definition, establishes a containment +# relationship with another class + +Puppet::Parser::Functions::newfunction( + :contain, + :arity => -2, + :doc => "Contain one or more classes inside the current class. If any of +these classes are undeclared, they will be declared as if called with the +`include` function. Accepts a class name, an array of class names, or a +comma-separated list of class names. + +A contained class will not be applied before the containing class is +begun, and will be finished before the containing class is finished. +" +) do |classes| + scope = self + + scope.function_include(classes) + + classes.each do |class_name| + class_resource = scope.catalog.resource("Class", class_name) + if ! scope.catalog.edge?(scope.resource, class_resource) + scope.catalog.add_edge(scope.resource, class_resource) + end + end +end diff --git a/lib/puppet/parser/functions/create_resources.rb b/lib/puppet/parser/functions/create_resources.rb index 733dc4511..1c3b910b0 100644 --- a/lib/puppet/parser/functions/create_resources.rb +++ b/lib/puppet/parser/functions/create_resources.rb @@ -44,6 +44,11 @@ Puppet::Parser::Functions::newfunction(:create_resources, :arity => -3, :doc => ENDHEREDOC raise ArgumentError, ("create_resources(): wrong number of arguments (#{args.length}; must be 2 or 3)") if args.length > 3 + raise ArgumentError, ('create_resources(): second argument must be a hash') unless args[1].is_a?(Hash) + if args.length == 3 + raise ArgumentError, ('create_resources(): third argument, if provided, must be a hash') unless args[2].is_a?(Hash) + end + type, instances, defaults = args defaults ||= {} diff --git a/lib/puppet/parser/functions/extlookup.rb b/lib/puppet/parser/functions/extlookup.rb index 2a81ccc4e..293a9ea62 100644 --- a/lib/puppet/parser/functions/extlookup.rb +++ b/lib/puppet/parser/functions/extlookup.rb @@ -100,7 +100,7 @@ This is for back compatibility to interpolate variables with %. % interpolation # if we got a custom data file, put it first in the array of search files if datafile != "" - datafiles << extlookup_datadir + "/#{datafile}.csv" if File.exists?(extlookup_datadir + "/#{datafile}.csv") + datafiles << extlookup_datadir + "/#{datafile}.csv" if Puppet::FileSystem::File.exist?(extlookup_datadir + "/#{datafile}.csv") end extlookup_precedence.each do |d| @@ -111,7 +111,7 @@ This is for back compatibility to interpolate variables with %. % interpolation datafiles.each do |file| if desired.nil? - if File.exists?(file) + if Puppet::FileSystem::File.exist?(file) result = CSV.read(file).find_all do |r| r[0] == key end diff --git a/lib/puppet/parser/functions/file.rb b/lib/puppet/parser/functions/file.rb index 569266a3b..89d78f8ba 100644 --- a/lib/puppet/parser/functions/file.rb +++ b/lib/puppet/parser/functions/file.rb @@ -10,7 +10,7 @@ Puppet::Parser::Functions::newfunction( unless Puppet::Util.absolute_path?(file) raise Puppet::ParseError, "Files must be fully qualified" end - if FileTest.exists?(file) + if Puppet::FileSystem::File.exist?(file) ret = File.read(file) break end diff --git a/lib/puppet/parser/functions/reject.rb b/lib/puppet/parser/functions/filter.rb index d19471102..7894fa48f 100644 --- a/lib/puppet/parser/functions/reject.rb +++ b/lib/puppet/parser/functions/filter.rb @@ -1,47 +1,48 @@ require 'puppet/parser/ast/lambda' Puppet::Parser::Functions::newfunction( -:reject, +:filter, :type => :rvalue, :arity => 2, :doc => <<-'ENDHEREDOC') do |args| Applies a parameterized block to each element in a sequence of entries from the first - argument and returns an array with the entires for which the block did *not* evaluate to true. + argument and returns an array or hash (same type as left operand) + with the entries for which the block evaluates to true. This function takes two mandatory arguments: the first should be an Array or a Hash, and the second a parameterized block as produced by the puppet syntax: - $a.reject |$x| { ... } + $a.filter |$x| { ... } When the first argument is an Array, the block is called with each entry in turn. When the first argument - is a hash the entry is an array with `[key, value]`. + is a Hash the entry is an array with `[key, value]`. The returned filtered object is of the same type as the receiver. *Examples* - # selects all that does not end with berry - $a = ["rasberry", "blueberry", "orange"] - $a.reject |$x| { $x =~ /berry$/ } + # selects all that end with berry + $a = ["raspberry", "blueberry", "orange"] + $a.filter |$x| { $x =~ /berry$/ } - - Since 3.2 + - Since 3.4 - requires `parser = future`. ENDHEREDOC receiver = args[0] pblock = args[1] - raise ArgumentError, ("reject(): wrong argument type (#{pblock.class}; must be a parameterized block.") unless pblock.is_a? Puppet::Parser::AST::Lambda + raise ArgumentError, ("filter(): wrong argument type (#{pblock.class}; must be a parameterized block.") unless pblock.is_a? Puppet::Parser::AST::Lambda case receiver when Array - receiver.reject {|x| pblock.call(self, x) } + receiver.select {|x| pblock.call(self, x) } when Hash - result = receiver.reject {|x, y| pblock.call(self, [x, y]) } + result = receiver.select {|x, y| pblock.call(self, [x, y]) } # Ruby 1.8.7 returns Array result = Hash[result] unless result.is_a? Hash result else - raise ArgumentError, ("reject(): wrong argument type (#{receiver.class}; must be an Array or a Hash.") + raise ArgumentError, ("filter(): wrong argument type (#{receiver.class}; must be an Array or a Hash.") end end diff --git a/lib/puppet/parser/functions/foreach.rb b/lib/puppet/parser/functions/foreach.rb deleted file mode 100644 index 113e96a58..000000000 --- a/lib/puppet/parser/functions/foreach.rb +++ /dev/null @@ -1,95 +0,0 @@ -Puppet::Parser::Functions::newfunction( -:foreach, -:type => :rvalue, -:arity => 2, -:doc => <<-'ENDHEREDOC') do |args| - Applies a parameterized block to each element in a sequence of selected entries from the first - argument and returns the first argument. - - This function takes two mandatory arguments: the first should be an Array or a Hash, and the second - a parameterized block as produced by the puppet syntax: - - $a.foreach {|$x| ... } - - When the first argument is an Array, the parameterized block should define one or two block parameters. - For each application of the block, the next element from the array is selected, and it is passed to - the block if the block has one parameter. If the block has two parameters, the first is the elements - index, and the second the value. The index starts from 0. - - $a.foreach {|$index, $value| ... } - - When the first argument is a Hash, the parameterized block should define one or two parameters. - When one parameter is defined, the iteration is performed with each entry as an array of `[key, value]`, - and when two parameters are defined the iteration is performed with key and value. - - $a.foreach {|$entry| ..."key ${$entry[0]}, value ${$entry[1]}" } - $a.foreach {|$key, $value| ..."key ${key}, value ${value}" } - - - Since 3.2 - - requires `parser = future`. - ENDHEREDOC - require 'puppet/parser/ast/lambda' - - def foreach_Array(o, scope, pblock) - return nil unless pblock - - serving_size = pblock.parameter_count - if serving_size == 0 - raise ArgumentError, "Block must define at least one parameter; value." - end - if serving_size > 2 - raise ArgumentError, "Block must define at most two parameters; index, value" - end - enumerator = o.each - index = 0 - if serving_size == 1 - (o.size).times do - pblock.call(scope, enumerator.next) - end - else - (o.size).times do - pblock.call(scope, index, enumerator.next) - index = index +1 - end - end - o - end - - def foreach_Hash(o, scope, pblock) - return nil unless pblock - serving_size = pblock.parameter_count - case serving_size - when 0 - raise ArgumentError, "Block must define at least one parameter (for hash entry key)." - when 1 - when 2 - else - raise ArgumentError, "Block must define at most two parameters (for hash entry key and value)." - end - enumerator = o.each_pair - if serving_size == 1 - (o.size).times do - pblock.call(scope, enumerator.next) - end - else - (o.size).times do - pblock.call(scope, *enumerator.next) - end - end - o - end - - raise ArgumentError, ("foreach(): wrong number of arguments (#{args.length}; must be 2)") if args.length != 2 - receiver = args[0] - pblock = args[1] - raise ArgumentError, ("foreach(): wrong argument type (#{args[1].class}; must be a parameterized block.") unless pblock.is_a? Puppet::Parser::AST::Lambda - - case receiver - when Array - foreach_Array(receiver, self, pblock) - when Hash - foreach_Hash(receiver, self, pblock) - else - raise ArgumentError, ("foreach(): wrong argument type (#{args[0].class}; must be an Array or a Hash.") - end -end diff --git a/lib/puppet/parser/functions/fqdn_rand.rb b/lib/puppet/parser/functions/fqdn_rand.rb index f9bd2d23e..ffdb64e20 100644 --- a/lib/puppet/parser/functions/fqdn_rand.rb +++ b/lib/puppet/parser/functions/fqdn_rand.rb @@ -1,12 +1,20 @@ require 'digest/md5' Puppet::Parser::Functions::newfunction(:fqdn_rand, :arity => -2, :type => :rvalue, :doc => - "Generates random numbers based on the node's fqdn. Generated random values - will be a range from 0 up to and excluding n, where n is the first parameter. - The second argument specifies a number to add to the seed and is optional, for example: + "Usage: `fqdn_rand(MAX, [SEED])`. MAX is required and must be a positive + integer; SEED is optional and may be any number or string. - $random_number = fqdn_rand(30) - $random_number_seed = fqdn_rand(30,30)") do |args| + Generates a random whole number greater than or equal to 0 and less than MAX, + combining the `$fqdn` fact and the value of SEED for repeatable randomness. + (That is, each node will get a different random number from this function, but + a given node's result will be the same every time unless its hostname changes.) + + This function is usually used for spacing out runs of resource-intensive cron + tasks that run on many nodes, which could cause a thundering herd or degrade + other services if they all fire at once. Adding a SEED can be useful when you + have more than one such task and need several unrelated random numbers per + node. (For example, `fqdn_rand(30)`, `fqdn_rand(30, 'expensive job 1')`, and + `fqdn_rand(30, 'expensive job 2')` will produce totally different numbers.)") do |args| max = args.shift.to_i seed = Digest::MD5.hexdigest([self['::fqdn'],args].join(':')).hex Puppet::Util.deterministic_rand(seed,max) diff --git a/lib/puppet/parser/functions/include.rb b/lib/puppet/parser/functions/include.rb index ca8819176..ea7acd936 100644 --- a/lib/puppet/parser/functions/include.rb +++ b/lib/puppet/parser/functions/include.rb @@ -1,5 +1,22 @@ # Include the specified classes -Puppet::Parser::Functions::newfunction(:include, :arity => -2, :doc => "Evaluate one or more classes.") do |vals| +Puppet::Parser::Functions::newfunction(:include, :arity => -2, :doc => "Declares one or more classes, causing the resources in them to be +evaluated and added to the catalog. Accepts a class name, an array of class +names, or a comma-separated list of class names. + +The `include` function can be used multiple times on the same class and will +only declare a given class once. If a class declared with `include` has any +parameters, Puppet will automatically look up values for them in Hiera, using +`<class name>::<parameter name>` as the lookup key. + +Contrast this behavior with resource-like class declarations +(`class {'name': parameter => 'value',}`), which must be used in only one place +per class and can directly set parameters. You should avoid using both `include` +and resource-like declarations with the same class. + +The `include` function does not cause classes to be contained in the class +where they are declared. For that, see the `contain` function. It also +does not create a dependency relationship between the declared class and th +surrounding class; for that, see the `require` function.") do |vals| if vals.is_a?(Array) # Protect against array inside array vals = vals.flatten diff --git a/lib/puppet/parser/functions/map.rb b/lib/puppet/parser/functions/map.rb new file mode 100644 index 000000000..3c871bc85 --- /dev/null +++ b/lib/puppet/parser/functions/map.rb @@ -0,0 +1,44 @@ +require 'puppet/parser/ast/lambda' + +Puppet::Parser::Functions::newfunction( +:map, +:type => :rvalue, +:arity => 2, +:doc => <<-'ENDHEREDOC') do |args| + Applies a parameterized block to each element in a sequence of entries from the first + argument and returns an array with the result of each invocation of the parameterized block. + + This function takes two mandatory arguments: the first should be an Array or a Hash, and the second + a parameterized block as produced by the puppet syntax: + + $a.map |$x| { ... } + + When the first argument `$a` is an Array, the block is called with each entry in turn. When the first argument + is a hash the entry is an array with `[key, value]`. + + *Examples* + + # Turns hash into array of values + $a.map |$x|{ $x[1] } + + # Turns hash into array of keys + $a.map |$x| { $x[0] } + + - Since 3.4 + - requires `parser = future`. + ENDHEREDOC + + receiver = args[0] + pblock = args[1] + + raise ArgumentError, ("map(): wrong argument type (#{pblock.class}; must be a parameterized block.") unless pblock.is_a? Puppet::Parser::AST::Lambda + + case receiver + when Array + when Hash + else + raise ArgumentError, ("map(): wrong argument type (#{receiver.class}; must be an Array or a Hash.") + end + + receiver.to_a.map {|x| pblock.call(self, x) } +end diff --git a/lib/puppet/parser/functions/select.rb b/lib/puppet/parser/functions/select.rb index d5cee8e5c..659f2013c 100644 --- a/lib/puppet/parser/functions/select.rb +++ b/lib/puppet/parser/functions/select.rb @@ -1,47 +1,15 @@ -require 'puppet/parser/ast/lambda' - Puppet::Parser::Functions::newfunction( :select, :type => :rvalue, :arity => 2, :doc => <<-'ENDHEREDOC') do |args| - Applies a parameterized block to each element in a sequence of entries from the first - argument and returns an array with the entires for which the block evaluates to true. - - This function takes two mandatory arguments: the first should be an Array or a Hash, and the second - a parameterized block as produced by the puppet syntax: - - $a.select |$x| { ... } - - When the first argument is an Array, the block is called with each entry in turn. When the first argument - is a hash the entry is an array with `[key, value]`. - - The returned filtered object is of the same type as the receiver. + The 'select' function has been renamed to 'filter'. Please update your manifests. - *Examples* - - # selects all that end with berry - $a = ["raspberry", "blueberry", "orange"] - $a.select |$x| { $x =~ /berry$/ } - - - Since 3.2 + The select function is reserved for future use. + - Removed as of 3.4 - requires `parser = future`. ENDHEREDOC - receiver = args[0] - pblock = args[1] - - raise ArgumentError, ("select(): wrong argument type (#{pblock.class}; must be a parameterized block.") unless pblock.is_a? Puppet::Parser::AST::Lambda - - case receiver - when Array - receiver.select {|x| pblock.call(self, x) } - when Hash - result = receiver.select {|x, y| pblock.call(self, [x, y]) } - # Ruby 1.8.7 returns Array - result = Hash[result] unless result.is_a? Hash - result - else - raise ArgumentError, ("select(): wrong argument type (#{receiver.class}; must be an Array or a Hash.") - end -end + raise NotImplementedError, + "The 'select' function has been renamed to 'filter'. Please update your manifests." +end
\ No newline at end of file diff --git a/lib/puppet/parser/lexer.rb b/lib/puppet/parser/lexer.rb index fee38d946..65cbef92e 100644 --- a/lib/puppet/parser/lexer.rb +++ b/lib/puppet/parser/lexer.rb @@ -347,7 +347,7 @@ class Puppet::Parser::Lexer def file=(file) @file = file @line = 1 - contents = File.exists?(file) ? File.read(file) : "" + contents = Puppet::FileSystem::File.exist?(file) ? File.read(file) : "" @scanner = StringScanner.new(contents) end diff --git a/lib/puppet/parser/parser_support.rb b/lib/puppet/parser/parser_support.rb index 2fb231ff4..b67f3c752 100644 --- a/lib/puppet/parser/parser_support.rb +++ b/lib/puppet/parser/parser_support.rb @@ -82,7 +82,7 @@ class Puppet::Parser::Parser def_delegators :@lexer, :file, :string= def file=(file) - unless FileTest.exist?(file) + unless Puppet::FileSystem::File.exist?(file) unless file =~ /\.pp$/ file = file + ".pp" end diff --git a/lib/puppet/parser/resource.rb b/lib/puppet/parser/resource.rb index 6d9f996e7..0b4c6677b 100644 --- a/lib/puppet/parser/resource.rb +++ b/lib/puppet/parser/resource.rb @@ -1,12 +1,9 @@ -require 'forwardable' require 'puppet/resource' # The primary difference between this class and its # parent is that this class has rules on who can set # parameters class Puppet::Parser::Resource < Puppet::Resource - extend Forwardable - require 'puppet/parser/resource/param' require 'puppet/util/tagging' require 'puppet/parser/yaml_trimmer' @@ -18,7 +15,6 @@ class Puppet::Parser::Resource < Puppet::Resource include Puppet::Util::MethodHelper include Puppet::Util::Errors include Puppet::Util::Logging - include Puppet::Util::Tagging include Puppet::Parser::YamlTrimmer attr_accessor :source, :scope, :collector_id @@ -56,7 +52,9 @@ class Puppet::Parser::Resource < Puppet::Resource end end - def_delegator :scope, :environment + def environment + scope.environment + end # Process the stage metaparameter for a class. A containment edge # is drawn from the class to the stage. The stage for containment @@ -188,47 +186,10 @@ class Puppet::Parser::Resource < Puppet::Resource end end - - # Create a Puppet::Resource instance from this parser resource. - # We plan, at some point, on not needing to do this conversion, but - # it's sufficient for now. - def to_resource - result = Puppet::Resource.new(type, title) - - to_hash.each do |p, v| - if v.is_a?(Puppet::Resource) - v = Puppet::Resource.new(v.type, v.title) - elsif v.is_a?(Array) - # flatten resource references arrays - v = v.flatten if v.flatten.find { |av| av.is_a?(Puppet::Resource) } - v = v.collect do |av| - av = Puppet::Resource.new(av.type, av.title) if av.is_a?(Puppet::Resource) - av - end - end - - # If the value is an array with only one value, then - # convert it to a single value. This is largely so that - # the database interaction doesn't have to worry about - # whether it returns an array or a string. - result[p] = if v.is_a?(Array) and v.length == 1 - v[0] - else - v - end - end - - result.file = self.file - result.line = self.line - result.exported = self.exported - result.virtual = self.virtual - result.tag(*self.tags) - - result - end - # Convert this resource to a RAL resource. - def_delegator :to_resource, :to_ral + def to_ral + copy_as_resource.to_ral + end private diff --git a/lib/puppet/parser/scope.rb b/lib/puppet/parser/scope.rb index c249e7566..2cad0c1dd 100644 --- a/lib/puppet/parser/scope.rb +++ b/lib/puppet/parser/scope.rb @@ -116,6 +116,9 @@ class Puppet::Parser::Scope compiler.node.name end + # TODO: 19514 - this is smelly; who uses this? functions? templates? + # What about trusted facts ? Should untrusted facts be removed from facts? + # def facts compiler.node.facts end @@ -195,8 +198,6 @@ class Puppet::Parser::Scope extend_with_functions_module - @tags = [] - # The symbol table for this scope. This is where we store variables. @symtable = Ephemeral.new(nil, true) @@ -461,6 +462,8 @@ class Puppet::Parser::Scope } end + RESERVED_VARIABLE_NAMES = ['trusted'].freeze + # Set a variable in the current scope. This will override settings # in scopes above, but will not allow variables in the current scope # to be reassigned. @@ -474,6 +477,11 @@ class Puppet::Parser::Scope raise Puppet::ParseError, "Scope variable name #{name.inspect} is a #{name.class}, not a string" end + # Check for reserved variable names + if Puppet[:trusted_node_data] && !options[:privileged] && RESERVED_VARIABLE_NAMES.include?(name) + raise Puppet::ParseError, "Attempt to assign to a reserved variable name: '#{name}'" + end + table = effective_symtable options[:ephemeral] if table.bound?(name) if options[:append] @@ -494,6 +502,29 @@ class Puppet::Parser::Scope table[name] end + def set_trusted(hash) + setvar('trusted', deep_freeze(hash), :privileged => true) + end + + # Deeply freezes the given object. The object and its content must be of the types: + # Array, Hash, Numeric, Boolean, Symbol, Regexp, NilClass, or String. All other types raises an Error. + # (i.e. if they are assignable to Puppet::Pops::Types::Data type). + # + def deep_freeze(object) + case object + when Hash + object.each {|k, v| deep_freeze(k); deep_freeze(v) } + when NilClass + # do nothing + when String + object.freeze + else + raise Puppet::Error, "Unsupported data type: '#{object.class}" + end + object + end + private :deep_freeze + # Return the effective "table" for setting variables. # This method returns the first ephemeral "table" that acts as a local scope, or this # scope's symtable. If the parameter `use_ephemeral` is true, the "top most" ephemeral "table" diff --git a/lib/puppet/parser/type_loader.rb b/lib/puppet/parser/type_loader.rb index 132242f88..4ace9f90a 100644 --- a/lib/puppet/parser/type_loader.rb +++ b/lib/puppet/parser/type_loader.rb @@ -7,61 +7,6 @@ class Puppet::Parser::TypeLoader extend Forwardable include Puppet::Node::Environment::Helper - # Helper class that makes sure we don't try to import the same file - # more than once from either the same thread or different threads. - class Helper - include MonitorMixin - def initialize - super - # These hashes are indexed by filename - @state = {} # :doing or :done - @thread = {} # if :doing, thread that's doing the parsing - @cond_var = {} # if :doing, condition var that will be signaled when done. - end - - # Execute the supplied block exactly once per file, no matter how - # many threads have asked for it to run. If another thread is - # already executing it, wait for it to finish. If this thread is - # already executing it, return immediately without executing the - # block. - # - # Note: the reason for returning immediately if this thread is - # already executing the block is to handle the case of a circular - # import--when this happens, we attempt to recursively re-parse a - # file that we are already in the process of parsing. To prevent - # an infinite regress we need to simply do nothing when the - # recursive import is attempted. - def do_once(file) - need_to_execute = synchronize do - case @state[file] - when :doing - if @thread[file] != Thread.current - @cond_var[file].wait - end - false - when :done - false - else - @state[file] = :doing - @thread[file] = Thread.current - @cond_var[file] = new_cond - true - end - end - if need_to_execute - begin - yield - ensure - synchronize do - @state[file] = :done - @thread.delete(file) - @cond_var.delete(file).broadcast - end - end - end - end - end - # Import manifest files that match a given file glob pattern. # # @param pattern [String] the file glob to apply when determining which files @@ -103,7 +48,6 @@ class Puppet::Parser::TypeLoader def initialize(env) self.environment = env - @loading_helper = Helper.new end # Try to load the object with the given fully qualified name. @@ -148,11 +92,11 @@ class Puppet::Parser::TypeLoader end def load_files(modname, files) + @loaded ||= {} loaded_asts = [] - files.each do |file| - @loading_helper.do_once(file) do - loaded_asts << parse_file(file) - end + files.reject { |file| @loaded[file] }.each do |file| + loaded_asts << parse_file(file) + @loaded[file] = true end loaded_asts.collect do |ast| diff --git a/lib/puppet/pops/binder/bindings_loader.rb b/lib/puppet/pops/binder/bindings_loader.rb index 44c14adec..eca05c1e9 100644 --- a/lib/puppet/pops/binder/bindings_loader.rb +++ b/lib/puppet/pops/binder/bindings_loader.rb @@ -35,7 +35,7 @@ class Puppet::Pops::Binder::BindingsLoader def self.loadable?(basedir, name) # note, "lib" is added by the autoloader # - paths_for_name(name).find {|p| File.exists?(File.join(basedir, "lib/puppet/bindings", p)+'.rb') } + paths_for_name(name).find {|p| Puppet::FileSystem::File.exist?(File.join(basedir, "lib/puppet/bindings", p)+'.rb') } end private diff --git a/lib/puppet/pops/binder/config/binder_config.rb b/lib/puppet/pops/binder/config/binder_config.rb index aa5c45e54..a4fb7796a 100644 --- a/lib/puppet/pops/binder/config/binder_config.rb +++ b/lib/puppet/pops/binder/config/binder_config.rb @@ -72,20 +72,20 @@ module Puppet::Pops::Binder::Config rootdir = confdir if rootdir.is_a?(String) expanded_config_file = File.expand_path(File.join(rootdir, '/binder_config.yaml')) - if File.exist?(expanded_config_file) + if Puppet::FileSystem::File.exist?(expanded_config_file) @config_file = expanded_config_file end else raise ArgumentError, "No Puppet settings 'confdir', or it is not a String" end when String - unless File.exist?(@config_file) + unless Puppet::FileSystem::File.exist?(@config_file) raise ArgumentError, "Cannot find the given binder configuration file '#{@config_file}'" end else raise ArgumentError, "The setting binder_config is expected to be a String, got: #{@config_file.class.name}." end - unless @config_file.is_a?(String) && File.exist?(@config_file) + unless @config_file.is_a?(String) && Puppet::FileSystem::File.exist?(@config_file) @config_file = nil # use defaults end diff --git a/lib/puppet/pops/binder/hiera2/bindings_provider.rb b/lib/puppet/pops/binder/hiera2/bindings_provider.rb index 3cfc41aeb..d0e5d9a5e 100644 --- a/lib/puppet/pops/binder/hiera2/bindings_provider.rb +++ b/lib/puppet/pops/binder/hiera2/bindings_provider.rb @@ -35,7 +35,7 @@ module Puppet::Pops::Binder::Hiera2 precedence = [] @config.hierarchy.each do |key, value, path| - source_file = File.join(@config.module_dir, 'hiera.config.yaml') + source_file = File.join(@config.module_dir, 'hiera.yaml') category_value = @parser.evaluate_string(scope, @parser.quote(value), source_file) hierarchy[key] = { diff --git a/lib/puppet/pops/binder/scheme_handler/confdir_hiera_scheme.rb b/lib/puppet/pops/binder/scheme_handler/confdir_hiera_scheme.rb index 8be9f9018..d2ae152f7 100644 --- a/lib/puppet/pops/binder/scheme_handler/confdir_hiera_scheme.rb +++ b/lib/puppet/pops/binder/scheme_handler/confdir_hiera_scheme.rb @@ -43,7 +43,7 @@ class Puppet::Pops::Binder::SchemeHandler::ConfdirHieraScheme < Puppetx::Puppet: end def config_exist?(uri, composer) - File.exist?(File.join(composer.confdir, uri.path, 'hiera.yaml')) + Puppet::FileSystem::File.exist?(File.join(composer.confdir, uri.path, 'hiera.yaml')) end # A hiera.yaml that exists, is readable, can be loaded, and does not have version >= 2 set is ignored. diff --git a/lib/puppet/pops/binder/scheme_handler/module_hiera_scheme.rb b/lib/puppet/pops/binder/scheme_handler/module_hiera_scheme.rb index 38f57d033..0663ee2cb 100644 --- a/lib/puppet/pops/binder/scheme_handler/module_hiera_scheme.rb +++ b/lib/puppet/pops/binder/scheme_handler/module_hiera_scheme.rb @@ -36,7 +36,7 @@ class Puppet::Pops::Binder::SchemeHandler::ModuleHieraScheme < Puppetx::Puppet:: when '*' # create new URIs, one per module name that has a hiera.yaml file relative to its root composer.name_to_module.each_pair do | name, mod | - if File.exist?(File.join(mod.path, split_path[ 2..-1 ], 'hiera.yaml' )) + if Puppet::FileSystem::File.exist?(File.join(mod.path, split_path[ 2..-1 ], 'hiera.yaml' )) path_parts =["", name] + split_path[2..-1] result << URI.parse('module-hiera:'+File.join(path_parts)) end @@ -47,7 +47,7 @@ class Puppet::Pops::Binder::SchemeHandler::ModuleHieraScheme < Puppetx::Puppet:: # If uri has query that is empty, or the text 'optional' skip this uri if it does not exist if query = uri.query() if query == '' || query == 'optional' - if File.exist?(File.join(mod.path, split_path[ 2..-1 ], 'hiera.yaml' )) + if Puppet::FileSystem::File.exist?(File.join(mod.path, split_path[ 2..-1 ], 'hiera.yaml' )) result << URI.parse('module-hiera:' + uri.path) end end diff --git a/lib/puppet/pops/issues.rb b/lib/puppet/pops/issues.rb index 0276f4022..61663b167 100644 --- a/lib/puppet/pops/issues.rb +++ b/lib/puppet/pops/issues.rb @@ -195,6 +195,10 @@ module Puppet::Pops::Issues "Illegal +> operation on attribute #{name}. This operator can not be used in #{label.a_an(parent)}" end + ILLEGAL_NAME = hard_issue :ILLEGAL_NAME, :name do + "Illegal name. The given name #{name} does not conform to the naming rule \\A((::)?[a-z0-9]\w*)(::[a-z0-9]\w*)*\\z" + end + # In case a model is constructed programmatically, it must create valid type references. # ILLEGAL_CLASSREF = hard_issue :ILLEGAL_CLASSREF, :name do diff --git a/lib/puppet/pops/model/ast_transformer.rb b/lib/puppet/pops/model/ast_transformer.rb index b16d951bc..2d4504050 100644 --- a/lib/puppet/pops/model/ast_transformer.rb +++ b/lib/puppet/pops/model/ast_transformer.rb @@ -439,7 +439,10 @@ class Puppet::Pops::Model::AstTransformer args = { :code => transform(o.body) } - args[:parent] = transform(o.parent) unless is_nop?(o.parent) + args[:parent] = hostname(o.parent) unless is_nop?(o.parent) + if(args[:parent].is_a?(Array)) + raise "Illegal expression - unacceptable as a node parent" + end Puppet::Parser::AST::Node.new(hostname(o.host_matches), merge_location(args, o)) end diff --git a/lib/puppet/pops/model/model_label_provider.rb b/lib/puppet/pops/model/model_label_provider.rb index b48e7b441..469130de4 100644 --- a/lib/puppet/pops/model/model_label_provider.rb +++ b/lib/puppet/pops/model/model_label_provider.rb @@ -15,7 +15,7 @@ class Puppet::Pops::Model::ModelLabelProvider < Puppet::Pops::LabelProvider def label_Factory o ; label(o.current) end def label_Array o ; "Array Object" end def label_LiteralNumber o ; "Literal Number" end - def label_ArithmeticExpression o ; "'#{o_operator}' expression" end + def label_ArithmeticExpression o ; "'#{o.operator}' expression" end def label_AccessExpression o ; "'[]' expression" end def label_MatchExpression o ; "'#{o.operator}' expression" end def label_CollectExpression o ; label(o.query) end diff --git a/lib/puppet/pops/parser/egrammar.ra b/lib/puppet/pops/parser/egrammar.ra index eb8357d58..b45cd2cc3 100644 --- a/lib/puppet/pops/parser/egrammar.ra +++ b/lib/puppet/pops/parser/egrammar.ra @@ -14,7 +14,7 @@ token NAME SEMIC CASE DEFAULT AT LCOLLECT RCOLLECT CLASSREF token NOT OR AND UNDEF PARROW PLUS MINUS TIMES DIV LSHIFT RSHIFT UMINUS token MATCH NOMATCH REGEX IN_EDGE OUT_EDGE IN_EDGE_SUB OUT_EDGE_SUB token IN UNLESS PIPE -token LAMBDA SELBRACE +token SELBRACE token LOW prechigh @@ -203,14 +203,11 @@ call_method_with_lambda_expression # # This is a temporary switch while experimenting with concrete syntax # One should be picked for inclusion in puppet. -lambda - : lambda_j8 - | lambda_ruby -# Java8-like lambda with parameters to the left of the body -lambda_j8 - : lambda_parameter_list optional_farrow lambda_rest { - result = Factory.LAMBDA(val[0], val[2]) +# Lambda with parameters to the left of the body +lambda + : lambda_parameter_list lambda_rest { + result = Factory.LAMBDA(val[0], val[1]) # loc result, val[1] # TODO } @@ -218,22 +215,6 @@ lambda_rest : LBRACE statements RBRACE { result = val[1] } | LBRACE RBRACE { result = nil } -optional_farrow - : nil - | FARROW - -# Ruby-like lambda with parameters inside the body -# -lambda_ruby - : LAMBDA lambda_parameter_list statements RBRACE { - result = Factory.LAMBDA(val[1], val[2]) - loc result, val[0], val[3] - } - | LAMBDA lambda_parameter_list RBRACE { - result = Factory.LAMBDA(val[1], nil) - loc result, val[0], val[2] - } - # Produces Array<Model::Parameter> lambda_parameter_list : PIPE PIPE { result = [] } diff --git a/lib/puppet/pops/parser/eparser.rb b/lib/puppet/pops/parser/eparser.rb index e476e0358..6d71e7b7e 100644 --- a/lib/puppet/pops/parser/eparser.rb +++ b/lib/puppet/pops/parser/eparser.rb @@ -20,7 +20,7 @@ module Puppet module Parser class Parser < Racc::Parser -module_eval(<<'...end egrammar.ra/module_eval...', 'egrammar.ra', 718) +module_eval(<<'...end egrammar.ra/module_eval...', 'egrammar.ra', 699) # Make emacs happy # Local Variables: @@ -30,125 +30,64 @@ module_eval(<<'...end egrammar.ra/module_eval...', 'egrammar.ra', 718) ##### State transition tables begin ### clist = [ -'68,-132,112,207,220,235,344,51,53,87,88,84,79,90,234,94,-130,89,51,53', -'80,82,81,83,234,283,224,269,222,115,233,223,321,114,207,234,244,204', -'93,-198,-207,-132,86,85,54,299,72,73,75,74,77,78,68,70,71,54,-130,316', -'202,315,69,87,88,84,79,90,59,94,76,89,51,53,80,82,81,83,245,59,115,-198', -'-207,301,114,115,115,51,53,114,114,115,93,330,291,114,86,85,105,104', -'72,73,75,74,77,78,68,70,71,120,225,227,122,226,69,87,88,84,79,90,68', -'94,76,89,54,297,80,82,81,83,68,59,115,90,268,94,114,89,243,51,53,105', -'104,90,93,94,128,89,86,85,68,267,72,73,75,74,77,78,93,70,71,84,79,90', -'68,94,69,89,93,306,80,82,81,83,76,192,54,90,316,94,315,89,309,70,71', -'105,104,310,93,207,69,51,53,85,68,168,72,73,75,74,77,78,93,70,71,84', -'79,90,313,94,69,89,51,53,80,82,81,83,76,105,68,68,317,51,53,229,228', -'319,120,63,263,122,93,90,90,94,94,89,89,241,72,73,75,74,77,78,243,70', -'71,120,59,326,122,327,69,68,241,91,93,93,120,267,76,122,87,88,84,79', -'90,259,94,59,89,70,71,80,82,81,83,68,69,63,59,64,66,65,67,134,258,257', -'336,79,90,93,94,243,89,86,85,80,243,72,73,75,74,77,78,116,70,71,241', -'339,217,341,106,69,217,93,282,286,319,346,347,76,348,72,73,75,74,77', -'78,68,70,71,349,99,352,353,354,69,285,63,60,79,90,361,94,76,89,362,363', -'80,364,,,68,,,,,,,,,,,,79,90,93,94,,89,,,80,,72,73,75,74,77,78,68,70', -'71,,,,,,69,,93,,,90,,94,76,89,72,73,75,74,77,78,68,70,71,,,,,,69,,,', -'79,90,93,94,76,89,,,80,,72,73,75,74,77,78,68,70,71,,,,,,69,,93,,,90', -',94,76,89,72,73,75,74,77,78,68,70,71,,,,,,69,87,88,84,79,90,93,94,76', -'89,,,80,82,81,83,68,,,,,70,71,,,,,,69,90,93,94,,89,86,85,,,72,73,75', -'74,77,78,68,70,71,,,,,,69,,93,,,90,,94,76,89,72,73,75,74,77,78,68,70', -'71,,,,,,69,,,,,90,93,94,76,89,,,,,72,73,75,74,,,,70,71,,,,,,69,,93,', -',,,,76,,72,73,75,74,,,68,70,71,,,,,,69,87,88,84,79,90,239,94,76,89,', -',80,82,81,83,,,,,,,,,,,,,,,93,,,,86,85,,,72,73,75,74,77,78,68,70,71', -',,,,,69,87,88,84,79,90,,94,76,89,,,80,82,81,83,,,,,,,,,,,,,,,93,,,,86', -'85,,,72,73,75,74,77,78,68,70,71,,,,,,69,87,88,84,79,90,,94,76,89,68', -',80,82,81,83,,,,,,,,90,,94,,89,,,93,,,,86,85,,,72,73,75,74,77,78,,70', -'71,,93,,,,69,,,,,,75,74,76,,68,70,71,,,,,,69,87,88,84,79,90,,94,76,89', -'68,,80,82,81,83,,,,,,,,90,,94,,89,,,93,,,,86,85,,,72,73,75,74,77,78', -',70,71,,93,,,,69,,,,,,75,74,76,,68,70,71,,,,,,69,87,88,84,79,90,,94', -'76,89,,,80,82,81,83,,,,,,,,,,,,,,,93,,,,86,85,,,72,73,75,74,77,78,68', -'70,71,,,,,,69,87,88,84,79,90,,94,76,89,,,80,82,81,83,,,,,,,,,,,,,,,93', -',,,86,85,,,72,73,75,74,77,78,68,70,71,,,,,,69,87,88,84,79,90,,94,76', -'89,,,80,82,81,83,,,,,,,,,,,,,,,93,,,,86,85,,,72,73,75,74,77,78,68,70', -'71,,,,,,69,87,88,84,79,90,,94,76,89,,,80,82,81,83,,,,,,,,,,,,,,,93,', -',,86,85,,,72,73,75,74,77,78,68,70,71,,,,,,69,87,88,84,79,90,,94,76,89', +'68,215,228,229,-126,230,202,229,238,87,88,84,79,90,290,94,262,89,-124', +'68,80,82,81,83,68,217,51,53,51,53,224,223,90,239,94,-192,89,90,93,94', +'199,89,86,85,-126,212,72,73,75,74,77,78,276,70,71,51,53,93,-124,68,69', +'202,93,117,-201,54,119,76,87,88,84,79,90,240,94,-192,89,70,71,80,82', +'81,83,68,69,59,229,59,51,53,292,212,117,109,312,119,90,93,94,112,89', +'86,85,111,-201,72,73,75,74,77,78,294,70,71,59,51,53,279,68,69,112,93', +'51,53,111,54,76,87,88,84,79,90,307,94,306,89,70,71,80,82,81,83,112,69', +'112,219,111,59,111,321,218,278,117,112,112,119,93,111,111,117,86,85', +'119,275,72,73,75,74,77,78,220,70,71,221,59,68,68,307,69,306,238,59,189', +'299,300,76,84,79,90,90,94,94,89,89,301,80,82,81,83,51,53,202,68,165', +'51,53,284,64,66,65,67,125,304,93,93,90,236,94,85,89,308,72,73,75,74', +'77,78,310,70,71,222,261,68,236,238,69,54,317,318,260,93,54,76,84,79', +'90,260,94,63,89,63,131,80,82,81,83,68,102,254,327,253,198,113,252,330', +'102,103,238,102,90,93,94,334,89,310,336,337,338,72,73,75,74,77,78,339', +'70,71,99,342,343,344,68,69,91,93,236,63,60,351,76,87,88,84,79,90,352', +'94,353,89,70,71,80,82,81,83,68,69,354,,,,,,,,,,79,90,93,94,,89,86,85', +'80,,72,73,75,74,77,78,,70,71,,,,,,69,,93,,,,,76,68,,72,73,75,74,77,78', +',70,71,,79,90,,94,69,89,,,80,,,76,68,,,,,,,,,,,,79,90,93,94,,89,,,80', +',72,73,75,74,77,78,,70,71,,,,,,69,,93,,,,,76,68,,72,73,75,74,77,78,', +'70,71,,79,90,,94,69,89,,,80,,,76,68,,,,,,,,,,,,,90,93,94,,89,,,,,72', +'73,75,74,77,78,,70,71,,,,,,69,,93,,,,,76,,,72,73,75,74,77,78,,70,71', +',,,,68,69,,,,,,,76,87,88,84,79,90,,94,,89,,,80,82,81,83,68,,,,,,,,,', +',,,90,93,94,,89,86,85,,,72,73,75,74,77,78,,70,71,,,,,,69,,93,,,,68,76', +',,72,73,75,74,77,78,,70,71,90,,94,,89,69,,,,,,68,76,,,,,,,,,,,,90,93', +'94,,89,,,,,72,73,75,74,,,,70,71,,,,,,69,,93,,,,,76,,,72,73,75,74,,,', +'70,71,,,,,68,69,,,,,,,76,87,88,84,79,90,234,94,,89,,,80,82,81,83,,,', +',,,,,,,,,,,93,,,,86,85,,,72,73,75,74,77,78,,70,71,,,,,68,69,,,,,,,76', +'87,88,84,79,90,,94,,89,,,80,82,81,83,,,,,,,,,,,,,,,93,,,,86,85,,,72', +'73,75,74,77,78,,70,71,,,,,68,69,,,,,,,76,87,88,84,79,90,,94,,89,,,80', +'82,81,83,68,,,,,,,,,,,,,90,93,94,,89,86,85,,,72,73,75,74,77,78,,70,71', +',,,,,69,,93,,,,,76,,,,,75,74,,,,70,71,,,,,68,69,,,,,,,76,87,88,84,79', +'90,,94,,89,,,80,82,81,83,68,,,,,,,,,,,,,90,93,94,,89,86,85,,,72,73,75', +'74,77,78,,70,71,,,,,,69,,93,,,,,76,,,,,75,74,,,,70,71,,,,,68,69,,,,', +',,76,87,88,84,79,90,,94,,89,,,80,82,81,83,,,,,,,,,,,,,,,93,,,,86,85', +',,72,73,75,74,77,78,,70,71,,,,,68,69,,,,,,,76,87,88,84,79,90,,94,,89', ',,80,82,81,83,,,,,,,,,,,,,,,93,,,,86,85,,,72,73,75,74,77,78,,70,71,', -',,,,69,68,,213,,,,,76,,87,88,84,79,90,,94,,89,,,80,82,81,83,,,,,,,,', -',,,,,,93,,,,86,85,,,72,73,75,74,77,78,,70,71,,,,,,69,68,,212,,,,,76', -',87,88,84,79,90,,94,,89,,,80,82,81,83,,,,,,,,,,,,,,,93,,,,86,85,,,72', -'73,75,74,77,78,,70,71,,,,,,69,68,,211,,,,,76,,87,88,84,79,90,,94,,89', -',,80,82,81,83,,,,,,,,,,,,,,,93,,,,86,85,,,72,73,75,74,77,78,,70,71,', -',,,,69,68,,210,,,,,76,,87,88,84,79,90,,94,,89,,,80,82,81,83,,,,,,,,', -',,,,,,93,,,,86,85,,,72,73,75,74,77,78,68,70,71,,,,,,69,87,88,84,79,90', -',94,76,89,,197,80,82,81,83,,,,,,,,,,,,,,,93,,,,86,85,,,72,73,75,74,77', -'78,,70,71,51,53,,,47,69,48,,,,,,,76,,,,,,,,,13,,,,,,38,,44,,46,96,,45', -'58,54,,40,57,,,,55,12,,,56,51,53,11,,47,,48,,,,59,,,,,,39,,,167,,,13', -',,,,,170,187,181,188,46,182,190,183,179,177,,172,185,,,,55,12,191,186', -'184,51,53,11,,47,,48,333,,,59,,,,,189,171,,,,,,13,,,,,,38,,44,,46,42', -',45,58,54,,40,57,43,,,55,12,51,53,56,,47,11,48,322,,,,,,59,,,,,,39,', -',13,,,,,,38,,44,,46,96,,45,58,54,,40,57,,,,55,12,51,53,56,,47,11,48', -',,,,,,59,,,,,,39,,,13,,,,,,38,,44,,46,96,,45,58,54,,40,57,,,,55,12,51', -'53,56,,47,11,48,,,,,,,59,,,,,,39,,,13,,,,,,38,,44,,46,96,,45,58,54,', -'40,57,,,,55,12,51,53,56,,47,11,48,,,,,,,59,,,,,,39,,,13,,,,,,38,,44', -',46,96,,45,58,54,,40,57,,,,55,12,51,53,56,,47,11,48,,,,,,,59,,,,,,39', -',,13,,,,,,38,,44,,46,96,,45,58,54,,40,57,,,,55,12,51,53,56,,47,11,48', -',,,,,,59,,,,,,39,,,13,,,,,,38,,44,,46,96,,45,58,54,,40,57,,,,55,12,51', -'53,56,,47,11,48,,,,,,,59,,,,,,39,,,13,,,,,,38,,44,,46,96,,45,58,54,', -'40,57,,,,55,12,51,53,56,,47,11,48,,,,,,,59,,,,,,39,,,13,,,,,,38,,44', -',46,96,,45,58,54,,40,57,,,,55,12,51,53,56,,47,11,48,,,,,,,59,,,,,,39', -',,13,,,,,,38,,44,,46,96,,45,58,54,,40,57,,,,55,12,51,53,56,,47,11,48', -',,,,,,59,,,,,,39,,,13,,,,,,38,,44,,46,96,,45,58,54,,40,57,,,,55,12,51', -'53,56,,47,11,48,,,,,,,59,,,,,,39,,,13,,,,,,38,,44,,46,96,,45,58,54,', -'40,57,,,,55,12,51,53,56,,47,11,48,,,,,,,59,,,,,,39,,,13,,,,,,38,,44', -',46,96,,45,58,54,,40,57,,,,55,12,51,53,56,,47,11,48,,,,,,,59,,,,,,39', -',,13,,,,,,38,,44,,46,96,,45,58,54,,40,57,,,,55,12,51,53,56,,47,11,48', -',,,,,,59,,,,,,39,,,13,,,,,,38,,44,,46,96,,45,58,54,,40,57,,,,55,12,51', -'53,56,,47,11,48,,,,,,,59,,,,,,39,,,13,,,,,,38,,44,,46,96,,45,58,54,', -'40,57,,,,55,12,51,53,56,,47,11,48,,,,,,,59,,,,,,39,,,13,,,,,,38,,44', -',46,96,,45,58,54,,40,57,,,,55,12,51,53,56,,47,11,48,,,,,,,59,,,,,,39', -',,13,,,,,,38,,44,,46,96,,45,58,54,,40,57,,,,55,12,51,53,56,,47,11,48', -',,,,,,59,,,,,,39,,,13,,,,,,38,,44,,46,96,,45,58,54,,40,57,,,,55,12,51', -'53,56,,47,11,48,,,,,,,59,,,,,,39,,,13,,,,,,38,,44,,46,96,,45,58,54,', -'40,57,,,,55,12,51,53,56,,47,11,48,,,,,,,59,,,,,,39,,,13,,,,,,38,,44', -',46,96,,45,58,54,,40,57,,,,55,12,51,53,56,,47,11,48,,,,,,,59,,,,,,39', -',,13,,,,,,170,187,181,188,46,182,190,183,179,177,,172,185,,,,55,12,191', -'186,184,51,53,11,,47,,48,308,,,59,,,,,189,171,,,,,,13,,,,,,38,,44,,46', -'42,,45,58,54,,40,57,43,,,55,12,51,53,56,,47,11,48,,,,,,,59,,,,,,39,', -',13,,,,,,38,,44,,46,96,,45,58,54,,40,57,,,,55,12,51,53,56,,47,11,48', -',,,,,,59,,,,,,39,,,13,,,,,,38,,44,,46,96,,45,58,54,,40,57,,,,55,12,51', -'53,56,,47,11,48,,,,,,,59,,,,,,39,,,13,,,,,,38,,44,,46,96,,45,58,54,', -'40,57,,,,55,12,51,53,56,,47,11,48,,,,,,,59,,,,,,39,,,13,,,,,,38,,44', -',46,96,,45,58,54,,40,57,,,,55,12,51,53,56,,47,11,48,,,,,,,59,,,,,,39', -',,13,,,,,,38,,44,,46,96,,45,58,54,,40,57,,,,55,12,51,53,56,,47,11,48', -',,,,,,59,,,,,,39,,,13,,,,,,38,,44,,46,42,,45,58,54,,40,57,43,,,55,12', -'51,53,56,,47,11,48,,,,,,,59,,,,,,39,,,13,199,,,,,38,,44,,46,96,,45,58', -'54,,40,57,,,,55,12,51,53,56,,47,11,48,,,,,,,59,,,,,,39,,,13,,,,,,38', -',44,,46,96,,45,58,54,,40,57,,,,55,12,51,53,56,,47,11,48,,,,,,,59,,,', -',,39,,,13,,,,,,38,,44,,46,96,,45,58,54,,40,57,,,,55,12,51,53,56,,47', -'11,48,,,,,,,59,,,,,,39,,,13,,,,,,38,,44,,46,96,,45,58,54,,40,57,,,,55', -'12,51,53,56,,47,11,48,,,,,,,59,,,,,,39,,,13,209,,,,,38,,44,,46,96,,45', -'58,54,,40,57,,,,55,12,51,53,56,,47,11,48,,,,,,,59,,,,,,39,,,13,,,,,', -'38,,44,,46,42,,45,58,54,,40,57,43,,,55,12,51,53,56,,47,11,48,,,,,,,59', -',,,,,39,,,13,,,,,,38,,44,,46,42,,45,58,54,,40,57,43,,,55,12,51,53,56', -',47,11,48,,,,,,,59,,,,,,39,,,13,,,,,,38,,44,,46,42,,45,58,54,,40,57', -'43,,,55,12,51,53,56,,47,11,48,,,,,,,59,,,,,,39,,,13,,,,,,38,,44,,46', -'42,,45,58,54,,40,57,43,,,55,12,51,53,56,,47,11,48,,,,,,,59,,,,,,39,', -',13,,,,,,38,,44,,46,96,,45,58,54,,40,57,,,,55,12,51,53,56,,47,11,48', -',,,,,,59,,,,,,39,,,13,,,,,,38,,44,,46,96,,45,58,54,,40,57,,,,55,12,51', -'53,56,,47,11,48,,,,,,,59,,,,,,39,,,13,,,,,,38,,44,,46,96,,45,58,54,', -'40,57,,,,55,12,,,56,51,53,11,,47,290,48,,,,59,,,,,,39,,,,,,13,,,,,,38', -',44,,46,96,,45,58,54,,40,57,,,,55,12,51,53,56,,47,11,48,335,,,,,,59', -',,,,,39,,,13,,,,,,38,,44,,46,42,,45,58,54,,40,57,43,,,55,12,51,53,56', -',47,11,48,,,,,,,59,,,,,,39,,,13,,,,,,38,,44,,46,96,,45,58,54,,40,57', -',,,55,12,51,53,56,,47,11,48,,,,,,,59,,,,,,39,,,13,,,,,,38,,44,,46,96', -',45,58,54,,40,57,,,,55,12,51,53,56,,47,11,48,273,,,,,,59,,,,,,39,,,13', -',,,,,38,,44,,46,42,,45,58,54,,40,57,43,,,55,12,51,53,56,,47,11,48,,', -',,,,59,,,,,,39,,,13,,,,,,38,,44,,46,42,,45,58,54,,40,57,43,,,55,12,51', -'53,56,,47,11,48,271,,,,,,59,,,,,,39,,,13,,,,,,38,,44,,46,42,,45,58,54', -',40,57,43,,,55,12,51,53,56,,47,11,48,265,,,,,,59,,,,,,39,,,13,,,,,,38', -',44,,46,42,,45,58,54,,40,57,43,,,55,12,51,53,56,,47,11,48,,,,,,,59,', -',,,,39,,,13,,,,,,38,,44,,46,96,,45,58,54,,40,57,,,,55,12,51,53,56,,47', -'11,48,,,,,,,59,,,,,,39,,,13,,,,,,38,,44,,46,96,,45,58,54,,40,57,,,,55', -'12,,,56,51,53,11,,47,126,48,,,,59,,,,,,39,,,,,,13,,,,,,38,,44,,46,96', -',45,58,54,,40,57,,,,55,12,51,53,56,,47,11,48,,,,,,,59,,,,,,39,,,13,', -',,,,38,,44,,46,96,,45,58,54,,40,57,,,,55,12,51,53,56,,47,11,48,,,,,', -',59,,,,,,39,,,13,,,,,,38,,44,,46,96,,45,58,54,,40,57,,,,55,12,51,53', +',,,68,69,,,,,,,76,87,88,84,79,90,,94,,89,,,80,82,81,83,,,,,,,,,,,,,', +',93,,,,86,85,,,72,73,75,74,77,78,,70,71,,,,,68,69,,,,,,,76,87,88,84', +'79,90,,94,,89,,,80,82,81,83,,,,,,,,,,,,,,,93,,,,86,85,,,72,73,75,74', +'77,78,,70,71,,,,,68,69,,,,,,,76,87,88,84,79,90,,94,,89,,,80,82,81,83', +',,,,,,,,,,,,,,93,,,,86,85,,,72,73,75,74,77,78,,70,71,,,,,68,69,208,', +',,,,76,87,88,84,79,90,,94,,89,,,80,82,81,83,,,,,,,,,,,,,,,93,,,,86,85', +',,72,73,75,74,77,78,,70,71,,,,,68,69,207,,,,,,76,87,88,84,79,90,,94', +',89,,,80,82,81,83,,,,,,,,,,,,,,,93,,,,86,85,,,72,73,75,74,77,78,,70', +'71,,,,,68,69,206,,,,,,76,87,88,84,79,90,,94,,89,,,80,82,81,83,,,,,,', +',,,,,,,,93,,,,86,85,,,72,73,75,74,77,78,,70,71,,,,,68,69,205,,,,,,76', +'87,88,84,79,90,,94,,89,,,80,82,81,83,,,,,,,,,,,,,,,93,,,,86,85,,,72', +'73,75,74,77,78,,70,71,,,,,68,69,,,,,,,76,87,88,84,79,90,,94,,89,,194', +'80,82,81,83,,,,,,,,,,51,53,,,47,93,48,,,86,85,,,72,73,75,74,77,78,,70', +'71,13,,,,,69,38,,44,,46,96,76,45,58,54,,40,57,,,,55,12,51,53,56,,47', +'11,48,,,,,,,59,,,,,,39,,164,13,,,,,,167,184,178,185,46,179,187,180,176', +'174,,169,182,,,,55,12,188,183,181,51,53,11,,47,,48,324,,,59,,,,,186', +'168,,,,,,13,,,,,,38,,44,,46,42,,45,58,54,,40,57,43,,,55,12,51,53,56', +',47,11,48,313,,,,,,59,,,,,,39,,,13,,,,,,38,,44,,46,96,,45,58,54,,40', +'57,,,,55,12,51,53,56,,47,11,48,,,,,,,59,,,,,,39,,,13,,,,,,38,,44,,46', +'96,,45,58,54,,40,57,,,,55,12,51,53,56,,47,11,48,,,,,,,59,,,,,,39,,,13', +',,,,,38,,44,,46,96,,45,58,54,,40,57,,,,55,12,51,53,56,,47,11,48,,,,', +',,59,,,,,,39,,,13,,,,,,38,,44,,46,96,,45,58,54,,40,57,,,,55,12,51,53', '56,,47,11,48,,,,,,,59,,,,,,39,,,13,,,,,,38,,44,,46,96,,45,58,54,,40', '57,,,,55,12,51,53,56,,47,11,48,,,,,,,59,,,,,,39,,,13,,,,,,38,,44,,46', '96,,45,58,54,,40,57,,,,55,12,51,53,56,,47,11,48,,,,,,,59,,,,,,39,,,13', @@ -156,24 +95,85 @@ clist = [ ',,59,,,,,,39,,,13,,,,,,38,,44,,46,96,,45,58,54,,40,57,,,,55,12,51,53', '56,,47,11,48,,,,,,,59,,,,,,39,,,13,,,,,,38,,44,,46,96,,45,58,54,,40', '57,,,,55,12,51,53,56,,47,11,48,,,,,,,59,,,,,,39,,,13,,,,,,38,,44,,46', -'96,,45,58,54,,40,57,,,,55,12,51,53,56,,47,11,48,351,,,,,,59,,,,,,39', -',,13,,,,,,38,,44,,46,42,,45,58,54,,40,57,43,,,55,12,51,53,56,,47,11', +'96,,45,58,54,,40,57,,,,55,12,51,53,56,,47,11,48,,,,,,,59,,,,,,39,,,13', +',,,,,38,,44,,46,96,,45,58,54,,40,57,,,,55,12,51,53,56,,47,11,48,,,,', +',,59,,,,,,39,,,13,,,,,,38,,44,,46,96,,45,58,54,,40,57,,,,55,12,51,53', +'56,,47,11,48,,,,,,,59,,,,,,39,,,13,,,,,,38,,44,,46,96,,45,58,54,,40', +'57,,,,55,12,51,53,56,,47,11,48,,,,,,,59,,,,,,39,,,13,,,,,,38,,44,,46', +'96,,45,58,54,,40,57,,,,55,12,51,53,56,,47,11,48,,,,,,,59,,,,,,39,,,13', +',,,,,38,,44,,46,96,,45,58,54,,40,57,,,,55,12,51,53,56,,47,11,48,,,,', +',,59,,,,,,39,,,13,,,,,,38,,44,,46,96,,45,58,54,,40,57,,,,55,12,51,53', +'56,,47,11,48,,,,,,,59,,,,,,39,,,13,,,,,,38,,44,,46,96,,45,58,54,,40', +'57,,,,55,12,51,53,56,,47,11,48,,,,,,,59,,,,,,39,,,13,,,,,,38,,44,,46', +'96,,45,58,54,,40,57,,,,55,12,51,53,56,,47,11,48,,,,,,,59,,,,,,39,,,13', +',,,,,38,,44,,46,96,,45,58,54,,40,57,,,,55,12,51,53,56,,47,11,48,,,,', +',,59,,,,,,39,,,13,,,,,,38,,44,,46,96,,45,58,54,,40,57,,,,55,12,51,53', +'56,,47,11,48,,,,,,,59,,,,,,39,,,13,,,,,,167,184,178,185,46,179,187,180', +'176,174,,169,182,,,,55,12,188,183,181,51,53,11,,47,,48,,,,59,,,,,186', +'168,,,,,,13,,,,,,38,,44,,46,96,,45,58,54,,40,57,,,,55,12,51,53,56,,47', +'11,48,,,,,,,59,,,,,,39,,,13,,,,,,38,,44,,46,96,,45,58,54,,40,57,,,,55', +'12,51,53,56,,47,11,48,,,,,,,59,,,,,,39,,,13,,,,,,38,,44,,46,96,,45,58', +'54,,40,57,,,,55,12,51,53,56,,47,11,48,,,,,,,59,,,,,,39,,,13,,,,,,38', +',44,,46,96,,45,58,54,,40,57,,,,55,12,51,53,56,,47,11,48,,,,,,,59,,,', +',,39,,,13,,,,,,38,,44,,46,96,,45,58,54,,40,57,,,,55,12,51,53,56,,47', +'11,48,,,,,,,59,,,,,,39,,,13,,,,,,38,,44,,46,96,,45,58,54,,40,57,,,,55', +'12,51,53,56,,47,11,48,,,,,,,59,,,,,,39,,,13,,,,,,38,,44,,46,42,,45,58', +'54,,40,57,43,,,55,12,51,53,56,,47,11,48,,,,,,,59,,,,,,39,,,13,196,,', +',,38,,44,,46,96,,45,58,54,,40,57,,,,55,12,51,53,56,,47,11,48,,,,,,,59', +',,,,,39,,,13,,,,,,38,,44,,46,96,,45,58,54,,40,57,,,,55,12,51,53,56,', +'47,11,48,,,,,,,59,,,,,,39,,,13,,,,,,38,,44,,46,96,,45,58,54,,40,57,', +',,55,12,51,53,56,,47,11,48,,,,,,,59,,,,,,39,,,13,204,,,,,38,,44,,46', +'96,,45,58,54,,40,57,,,,55,12,51,53,56,,47,11,48,,,,,,,59,,,,,,39,,,13', +',,,,,38,,44,,46,42,,45,58,54,,40,57,43,,,55,12,51,53,56,,47,11,48,,', +',,,,59,,,,,,39,,,13,,,,,,38,,44,,46,42,,45,58,54,,40,57,43,,,55,12,51', +'53,56,,47,11,48,,,,,,,59,,,,,,39,,,13,,,,,,38,,44,,46,42,,45,58,54,', +'40,57,43,,,55,12,51,53,56,,47,11,48,,,,,,,59,,,,,,39,,,13,,,,,,38,,44', +',46,42,,45,58,54,,40,57,43,,,55,12,51,53,56,,47,11,48,,,,,,,59,,,,,', +'39,,,13,,,,,,38,,44,,46,96,,45,58,54,,40,57,,,,55,12,51,53,56,,47,11', '48,,,,,,,59,,,,,,39,,,13,,,,,,38,,44,,46,96,,45,58,54,,40,57,,,,55,12', '51,53,56,,47,11,48,,,,,,,59,,,,,,39,,,13,,,,,,38,,44,,46,96,,45,58,54', -',40,57,,,,55,12,51,53,56,,47,11,48,,,,,,,59,,,,,,39,,,13,,,,,,38,,44', -',46,96,,45,58,54,,40,57,,,,55,12,51,53,56,,47,11,48,356,,,,,,59,,,,', -',39,,,13,,,,,,38,,44,,46,42,,45,58,54,,40,57,43,,,55,12,51,53,56,,47', -'11,48,358,,,,,,59,,,,,,39,,,13,,,,,,38,,44,,46,42,,45,58,54,,40,57,43', -',,55,12,51,53,56,,47,11,48,,,,,,,59,,,,,,39,,,13,,,,,,38,,44,,46,42', -',45,58,54,61,40,57,43,,,55,12,51,53,56,,47,11,48,360,,,,,,59,,,,,,39', +',40,57,,,,55,12,,,56,51,53,11,,47,283,48,,,,59,,,,,,39,,,,,,13,,,,,', +'38,,44,,46,96,,45,58,54,,40,57,,,,55,12,51,53,56,,47,11,48,326,,,,,', +'59,,,,,,39,,,13,,,,,,38,,44,,46,42,,45,58,54,,40,57,43,,,55,12,51,53', +'56,,47,11,48,,,,,,,59,,,,,,39,,,13,,,,,,38,,44,,46,96,,45,58,54,,40', +'57,,,,55,12,51,53,56,,47,11,48,,,,,,,59,,,,,,39,,,13,,,,,,38,,44,,46', +'96,,45,58,54,,40,57,,,,55,12,51,53,56,,47,11,48,266,,,,,,59,,,,,,39', ',,13,,,,,,38,,44,,46,42,,45,58,54,,40,57,43,,,55,12,51,53,56,,47,11', '48,,,,,,,59,,,,,,39,,,13,,,,,,38,,44,,46,42,,45,58,54,,40,57,43,,,55', '12,51,53,56,,47,11,48,,,,,,,59,,,,,,39,,,13,,,,,,38,,44,,46,96,,45,58', -'54,,40,57,,,,55,12,,,56,,,11,,,,253,187,252,188,59,250,190,254,248,247', -'39,249,251,,,,,,191,186,255,253,187,252,188,,250,190,254,248,247,,249', -'251,,,189,256,,191,186,255,253,187,252,188,,250,190,254,248,247,,249', -'251,,,189,256,,191,186,255,,,,,,,,,,,,,,,,189,256' ] - racc_action_table = arr = ::Array.new(4804, nil) +'54,,40,57,,,,55,12,51,53,56,,47,11,48,258,,,,,,59,,,,,,39,,,13,,,,,', +'38,,44,,46,42,,45,58,54,,40,57,43,,,55,12,51,53,56,,47,11,48,,,,,,,59', +',,,,,39,,,13,,,,,,38,,44,,46,96,,45,58,54,,40,57,,,,55,12,51,53,56,', +'47,11,48,,,,,,,59,,,,,,39,,,13,,,,,,38,,44,,46,96,,45,58,54,,40,57,', +',,55,12,,,56,51,53,11,,47,123,48,,,,59,,,,,,39,,,,,,13,,,,,,38,,44,', +'46,96,,45,58,54,,40,57,,,,55,12,51,53,56,,47,11,48,,,,,,,59,,,,,,39', +',,13,,,,,,38,,44,,46,96,,45,58,54,,40,57,,,,55,12,51,53,56,,47,11,48', +',,,,,,59,,,,,,39,,,13,,,,,,38,,44,,46,96,,45,58,54,,40,57,,,,55,12,51', +'53,56,,47,11,48,,,,,,,59,,,,,,39,,,13,,,,,,38,,44,,46,96,,45,58,54,', +'40,57,,,,55,12,51,53,56,,47,11,48,,,,,,,59,,,,,,39,,,13,,,,,,38,,44', +',46,96,,45,58,54,,40,57,,,,55,12,51,53,56,,47,11,48,,,,,,,59,,,,,,39', +',,13,,,,,,38,,44,,46,96,,45,58,54,,40,57,,,,55,12,51,53,56,,47,11,48', +',,,,,,59,,,,,,39,,,13,,,,,,38,,44,,46,96,,45,58,54,,40,57,,,,55,12,51', +'53,56,,47,11,48,,,,,,,59,,,,,,39,,,13,,,,,,38,,44,,46,96,,45,58,54,', +'40,57,,,,55,12,51,53,56,,47,11,48,,,,,,,59,,,,,,39,,,13,,,,,,38,,44', +',46,96,,45,58,54,,40,57,,,,55,12,51,53,56,,47,11,48,341,,,,,,59,,,,', +',39,,,13,,,,,,38,,44,,46,42,,45,58,54,,40,57,43,,,55,12,51,53,56,,47', +'11,48,,,,,,,59,,,,,,39,,,13,,,,,,38,,44,,46,96,,45,58,54,,40,57,,,,55', +'12,51,53,56,,47,11,48,,,,,,,59,,,,,,39,,,13,,,,,,38,,44,,46,96,,45,58', +'54,,40,57,,,,55,12,51,53,56,,47,11,48,,,,,,,59,,,,,,39,,,13,,,,,,38', +',44,,46,96,,45,58,54,,40,57,,,,55,12,51,53,56,,47,11,48,346,,,,,,59', +',,,,,39,,,13,,,,,,38,,44,,46,42,,45,58,54,,40,57,43,,,55,12,51,53,56', +',47,11,48,348,,,,,,59,,,,,,39,,,13,,,,,,38,,44,,46,42,,45,58,54,,40', +'57,43,,,55,12,51,53,56,,47,11,48,,,,,,,59,,,,,,39,,,13,,,,,,38,,44,', +'46,42,,45,58,54,61,40,57,43,,,55,12,51,53,56,,47,11,48,350,,,,,,59,', +',,,,39,,,13,,,,,,38,,44,,46,42,,45,58,54,,40,57,43,,,55,12,51,53,56', +',47,11,48,,,,,,,59,,,,,,39,,,13,,,,,,38,,44,,46,42,,45,58,54,,40,57', +'43,,,55,12,51,53,56,,47,11,48,264,,,,,,59,,,,,,39,,,13,,,,,,38,,44,', +'46,42,,45,58,54,,40,57,43,,,55,12,,,56,,,11,,,,248,184,247,185,59,245', +'187,249,243,242,39,244,246,,,,,,188,183,250,248,184,247,185,,245,187', +'249,243,242,,244,246,,,186,251,,188,183,250,248,184,247,185,,245,187', +'249,243,242,,244,246,,,186,251,,188,183,250,,,,,,,,,,,,,,,,186,251' ] + racc_action_table = arr = ::Array.new(4874, nil) idx = 0 clist.each do |str| str.split(',', -1).each do |i| @@ -183,167 +183,167 @@ clist = [ end clist = [ -'164,179,42,105,118,164,316,70,70,164,164,164,164,164,208,164,177,164', -'71,71,164,164,164,164,274,217,125,208,118,42,141,125,274,42,217,141', -'180,105,164,185,184,179,164,164,70,240,164,164,164,164,164,164,163,164', -'164,71,177,271,103,271,164,163,163,163,163,163,70,163,164,163,220,220', -'163,163,163,163,180,71,96,185,184,242,96,182,282,226,226,182,282,181', -'163,282,226,181,163,163,306,306,163,163,163,163,163,163,162,163,163', -'220,127,130,220,127,163,162,162,162,162,162,97,162,163,162,226,236,162', -'162,162,162,151,220,44,97,207,97,44,97,246,48,48,199,199,151,162,151', -'48,151,162,162,161,205,162,162,162,162,162,162,97,162,162,161,161,161', -'142,161,162,161,151,260,161,161,161,161,162,92,48,142,313,142,313,142', -'264,151,151,36,36,266,161,267,151,183,183,161,160,90,161,161,161,161', -'161,161,142,161,161,160,160,160,270,160,161,160,45,45,160,160,160,160', -'161,104,150,95,272,222,222,133,133,273,183,135,200,183,160,150,95,150', -'95,150,95,277,160,160,160,160,160,160,278,160,160,45,183,279,45,280', -'160,10,214,10,150,95,222,284,160,222,10,10,10,10,10,198,10,45,10,150', -'150,10,10,10,10,159,150,62,222,7,7,7,7,60,196,194,296,159,159,10,159', -'174,159,10,10,159,298,10,10,10,10,10,10,43,10,10,173,305,113,307,37', -'10,117,159,215,219,317,319,320,10,324,159,159,159,159,159,159,158,159', -'159,325,35,331,332,334,159,218,5,1,158,158,350,158,159,158,355,357,158', -'359,,,157,,,,,,,,,,,,157,157,158,157,,157,,,157,,158,158,158,158,158', -'158,155,158,158,,,,,,158,,157,,,155,,155,158,155,157,157,157,157,157', -'157,156,157,157,,,,,,157,,,,156,156,155,156,157,156,,,156,,155,155,155', -'155,155,155,149,155,155,,,,,,155,,156,,,149,,149,155,149,156,156,156', -'156,156,156,312,156,156,,,,,,156,312,312,312,312,312,149,312,156,312', -',,312,312,312,312,154,,,,,149,149,,,,,,149,154,312,154,,154,312,312', -',,312,312,312,312,312,312,153,312,312,,,,,,312,,154,,,153,,153,312,153', -'154,154,154,154,154,154,152,154,154,,,,,,154,,,,,152,153,152,154,152', -',,,,153,153,153,153,,,,153,153,,,,,,153,,152,,,,,,153,,152,152,152,152', -',,169,152,152,,,,,,152,169,169,169,169,169,169,169,152,169,,,169,169', -'169,169,,,,,,,,,,,,,,,169,,,,169,169,,,169,169,169,169,169,169,304,169', -'169,,,,,,169,304,304,304,304,304,,304,169,304,,,304,304,304,304,,,,', -',,,,,,,,,,304,,,,304,304,,,304,304,304,304,304,304,303,304,304,,,,,', -'304,303,303,303,303,303,,303,304,303,148,,303,303,303,303,,,,,,,,148', -',148,,148,,,303,,,,303,303,,,303,303,303,303,303,303,,303,303,,148,', -',,303,,,,,,148,148,303,,295,148,148,,,,,,148,295,295,295,295,295,,295', -'148,295,147,,295,295,295,295,,,,,,,,147,,147,,147,,,295,,,,295,295,', -',295,295,295,295,295,295,,295,295,,147,,,,295,,,,,,147,147,295,,293', -'147,147,,,,,,147,293,293,293,293,293,,293,147,293,,,293,293,293,293', -',,,,,,,,,,,,,,293,,,,293,293,,,293,293,293,293,293,293,193,293,293,', -',,,,293,193,193,193,193,193,,193,293,193,,,193,193,193,193,,,,,,,,,', -',,,,,193,,,,193,193,,,193,193,193,193,193,193,289,193,193,,,,,,193,289', -'289,289,289,289,,289,193,289,,,289,289,289,289,,,,,,,,,,,,,,,289,,,', -'289,289,,,289,289,289,289,289,289,131,289,289,,,,,,289,131,131,131,131', -'131,,131,289,131,,,131,131,131,131,,,,,,,,,,,,,,,131,,,,131,131,,,131', -'131,131,131,131,131,124,131,131,,,,,,131,124,124,124,124,124,,124,131', -'124,,,124,124,124,124,,,,,,,,,,,,,,,124,,,,124,124,,,124,124,124,124', -'124,124,,124,124,,,,,,124,111,,111,,,,,124,,111,111,111,111,111,,111', -',111,,,111,111,111,111,,,,,,,,,,,,,,,111,,,,111,111,,,111,111,111,111', -'111,111,,111,111,,,,,,111,110,,110,,,,,111,,110,110,110,110,110,,110', -',110,,,110,110,110,110,,,,,,,,,,,,,,,110,,,,110,110,,,110,110,110,110', -'110,110,,110,110,,,,,,110,109,,109,,,,,110,,109,109,109,109,109,,109', -',109,,,109,109,109,109,,,,,,,,,,,,,,,109,,,,109,109,,,109,109,109,109', -'109,109,,109,109,,,,,,109,107,,107,,,,,109,,107,107,107,107,107,,107', -',107,,,107,107,107,107,,,,,,,,,,,,,,,107,,,,107,107,,,107,107,107,107', -'107,107,98,107,107,,,,,,107,98,98,98,98,98,,98,107,98,,98,98,98,98,98', -',,,,,,,,,,,,,,98,,,,98,98,,,98,98,98,98,98,98,,98,98,89,89,,,89,98,89', -',,,,,,98,,,,,,,,,89,,,,,,89,,89,,89,89,,89,89,89,,89,89,,,,89,89,,,89', -'213,213,89,,213,,213,,,,89,,,,,,89,,,89,,,213,,,,,,213,213,213,213,213', -'213,213,213,213,213,,213,213,,,,213,213,213,213,213,285,285,213,,285', -',285,285,,,213,,,,,213,213,,,,,,285,,,,,,285,,285,,285,285,,285,285', -'285,,285,285,285,,,285,285,275,275,285,,275,285,275,275,,,,,,285,,,', -',,285,,,275,,,,,,275,,275,,275,275,,275,275,275,,275,275,,,,275,275', -'72,72,275,,72,275,72,,,,,,,275,,,,,,275,,,72,,,,,,72,,72,,72,72,,72', -'72,72,,72,72,,,,72,72,73,73,72,,73,72,73,,,,,,,72,,,,,,72,,,73,,,,,', -'73,,73,,73,73,,73,73,73,,73,73,,,,73,73,74,74,73,,74,73,74,,,,,,,73', -',,,,,73,,,74,,,,,,74,,74,,74,74,,74,74,74,,74,74,,,,74,74,75,75,74,', -'75,74,75,,,,,,,74,,,,,,74,,,75,,,,,,75,,75,,75,75,,75,75,75,,75,75,', -',,75,75,76,76,75,,76,75,76,,,,,,,75,,,,,,75,,,76,,,,,,76,,76,,76,76', -',76,76,76,,76,76,,,,76,76,77,77,76,,77,76,77,,,,,,,76,,,,,,76,,,77,', -',,,,77,,77,,77,77,,77,77,77,,77,77,,,,77,77,78,78,77,,78,77,78,,,,,', -',77,,,,,,77,,,78,,,,,,78,,78,,78,78,,78,78,78,,78,78,,,,78,78,79,79', -'78,,79,78,79,,,,,,,78,,,,,,78,,,79,,,,,,79,,79,,79,79,,79,79,79,,79', -'79,,,,79,79,80,80,79,,80,79,80,,,,,,,79,,,,,,79,,,80,,,,,,80,,80,,80', -'80,,80,80,80,,80,80,,,,80,80,81,81,80,,81,80,81,,,,,,,80,,,,,,80,,,81', -',,,,,81,,81,,81,81,,81,81,81,,81,81,,,,81,81,82,82,81,,82,81,82,,,,', -',,81,,,,,,81,,,82,,,,,,82,,82,,82,82,,82,82,82,,82,82,,,,82,82,83,83', -'82,,83,82,83,,,,,,,82,,,,,,82,,,83,,,,,,83,,83,,83,83,,83,83,83,,83', -'83,,,,83,83,84,84,83,,84,83,84,,,,,,,83,,,,,,83,,,84,,,,,,84,,84,,84', -'84,,84,84,84,,84,84,,,,84,84,85,85,84,,85,84,85,,,,,,,84,,,,,,84,,,85', -',,,,,85,,85,,85,85,,85,85,85,,85,85,,,,85,85,86,86,85,,86,85,86,,,,', -',,85,,,,,,85,,,86,,,,,,86,,86,,86,86,,86,86,86,,86,86,,,,86,86,167,167', -'86,,167,86,167,,,,,,,86,,,,,,86,,,167,,,,,,167,,167,,167,167,,167,167', -'167,,167,167,,,,167,167,88,88,167,,88,167,88,,,,,,,167,,,,,,167,,,88', -',,,,,88,,88,,88,88,,88,88,88,,88,88,,,,88,88,68,68,88,,68,88,68,,,,', -',,88,,,,,,88,,,68,,,,,,68,,68,,68,68,,68,68,68,,68,68,,,,68,68,268,268', -'68,,268,68,268,,,,,,,68,,,,,,68,,,268,,,,,,268,,268,,268,268,,268,268', -'268,,268,268,,,,268,268,91,91,268,,91,268,91,,,,,,,268,,,,,,268,,,91', -',,,,,91,91,91,91,91,91,91,91,91,91,,91,91,,,,91,91,91,91,91,263,263', -'91,,263,,263,263,,,91,,,,,91,91,,,,,,263,,,,,,263,,263,,263,263,,263', -'263,263,,263,263,263,,,263,263,93,93,263,,93,263,93,,,,,,,263,,,,,,263', -',,93,,,,,,93,,93,,93,93,,93,93,93,,93,93,,,,93,93,94,94,93,,94,93,94', -',,,,,,93,,,,,,93,,,94,,,,,,94,,94,,94,94,,94,94,94,,94,94,,,,94,94,259', -'259,94,,259,94,259,,,,,,,94,,,,,,94,,,259,,,,,,259,,259,,259,259,,259', -'259,259,,259,259,,,,259,259,245,245,259,,245,259,245,,,,,,,259,,,,,', -'259,,,245,,,,,,245,,245,,245,245,,245,245,245,,245,245,,,,245,245,244', -'244,245,,244,245,244,,,,,,,245,,,,,,245,,,244,,,,,,244,,244,,244,244', -',244,244,244,,244,244,,,,244,244,67,67,244,,67,244,67,,,,,,,244,,,,', -',244,,,67,,,,,,67,,67,,67,67,,67,67,67,,67,67,67,,,67,67,99,99,67,,99', -'67,99,,,,,,,67,,,,,,67,,,99,99,,,,,99,,99,,99,99,,99,99,99,,99,99,,', -',99,99,241,241,99,,241,99,241,,,,,,,99,,,,,,99,,,241,,,,,,241,,241,', -'241,241,,241,241,241,,241,241,,,,241,241,235,235,241,,235,241,235,,', -',,,,241,,,,,,241,,,235,,,,,,235,,235,,235,235,,235,235,235,,235,235', -',,,235,235,234,234,235,,234,235,234,,,,,,,235,,,,,,235,,,234,,,,,,234', -',234,,234,234,,234,234,234,,234,234,,,,234,234,106,106,234,,106,234', -'106,,,,,,,234,,,,,,234,,,106,106,,,,,106,,106,,106,106,,106,106,106', -',106,106,,,,106,106,66,66,106,,66,106,66,,,,,,,106,,,,,,106,,,66,,,', -',,66,,66,,66,66,,66,66,66,,66,66,66,,,66,66,65,65,66,,65,66,65,,,,,', -',66,,,,,,66,,,65,,,,,,65,,65,,65,65,,65,65,65,,65,65,65,,,65,65,64,64', -'65,,64,65,64,,,,,,,65,,,,,,65,,,64,,,,,,64,,64,,64,64,,64,64,64,,64', -'64,64,,,64,64,63,63,64,,63,64,63,,,,,,,64,,,,,,64,,,63,,,,,,63,,63,', -'63,63,,63,63,63,,63,63,63,,,63,63,112,112,63,,112,63,112,,,,,,,63,,', -',,,63,,,112,,,,,,112,,112,,112,112,,112,112,112,,112,112,,,,112,112', -'232,232,112,,232,112,232,,,,,,,112,,,,,,112,,,232,,,,,,232,,232,,232', -'232,,232,232,232,,232,232,,,,232,232,227,227,232,,227,232,227,,,,,,', -'232,,,,,,232,,,227,,,,,,227,,227,,227,227,,227,227,227,,227,227,,,,227', -'227,,,227,223,223,227,,223,223,223,,,,227,,,,,,227,,,,,,223,,,,,,223', -',223,,223,223,,223,223,223,,223,223,,,,223,223,286,286,223,,286,223', -'286,286,,,,,,223,,,,,,223,,,286,,,,,,286,,286,,286,286,,286,286,286', -',286,286,286,,,286,286,69,69,286,,69,286,69,,,,,,,286,,,,,,286,,,69', -',,,,,69,,69,,69,69,,69,69,69,,69,69,,,,69,69,212,212,69,,212,69,212', -',,,,,,69,,,,,,69,,,212,,,,,,212,,212,,212,212,,212,212,212,,212,212', -',,,212,212,211,211,212,,211,212,211,211,,,,,,212,,,,,,212,,,211,,,,', -',211,,211,,211,211,,211,211,211,,211,211,211,,,211,211,61,61,211,,61', -'211,61,,,,,,,211,,,,,,211,,,61,,,,,,61,,61,,61,61,,61,61,61,,61,61,61', -',,61,61,210,210,61,,210,61,210,210,,,,,,61,,,,,,61,,,210,,,,,,210,,210', -',210,210,,210,210,210,,210,210,210,,,210,210,203,203,210,,203,210,203', -'203,,,,,,210,,,,,,210,,,203,,,,,,203,,203,,203,203,,203,203,203,,203', -'203,203,,,203,203,52,52,203,,52,203,52,,,,,,,203,,,,,,203,,,52,,,,,', -'52,,52,,52,52,,52,52,52,,52,52,,,,52,52,172,172,52,,172,52,172,,,,,', -',52,,,,,,52,,,172,,,,,,172,,172,,172,172,,172,172,172,,172,172,,,,172', -'172,,,172,47,47,172,,47,47,47,,,,172,,,,,,172,,,,,,47,,,,,,47,,47,,47', -'47,,47,47,47,,47,47,,,,47,47,297,297,47,,297,47,297,,,,,,,47,,,,,,47', -',,297,,,,,,297,,297,,297,297,,297,297,297,,297,297,,,,297,297,171,171', -'297,,171,297,171,,,,,,,297,,,,,,297,,,171,,,,,,171,,171,,171,171,,171', -'171,171,,171,171,,,,171,171,170,170,171,,170,171,170,,,,,,,171,,,,,', -'171,,,170,,,,,,170,,170,,170,170,,170,170,170,,170,170,,,,170,170,41', -'41,170,,41,170,41,,,,,,,170,,,,,,170,,,41,,,,,,41,,41,,41,41,,41,41', -'41,,41,41,,,,41,41,40,40,41,,40,41,40,,,,,,,41,,,,,,41,,,40,,,,,,40', -',40,,40,40,,40,40,40,,40,40,,,,40,40,39,39,40,,39,40,39,,,,,,,40,,,', -',,40,,,39,,,,,,39,,39,,39,39,,39,39,39,,39,39,,,,39,39,38,38,39,,38', -'39,38,,,,,,,39,,,,,,39,,,38,,,,,,38,,38,,38,38,,38,38,38,,38,38,,,,38', -'38,315,315,38,,315,38,315,,,,,,,38,,,,,,38,,,315,,,,,,315,,315,,315', -'315,,315,315,315,,315,315,,,,315,315,327,327,315,,327,315,327,327,,', -',,,315,,,,,,315,,,327,,,,,,327,,327,,327,327,,327,327,327,,327,327,327', -',,327,327,13,13,327,,13,327,13,,,,,,,327,,,,,,327,,,13,,,,,,13,,13,', -'13,13,,13,13,13,,13,13,,,,13,13,12,12,13,,12,13,12,,,,,,,13,,,,,,13', -',,12,,,,,,12,,12,,12,12,,12,12,12,,12,12,,,,12,12,11,11,12,,11,12,11', -',,,,,,12,,,,,,12,,,11,,,,,,11,,11,,11,11,,11,11,11,,11,11,,,,11,11,344', -'344,11,,344,11,344,344,,,,,,11,,,,,,11,,,344,,,,,,344,,344,,344,344', -',344,344,344,,344,344,344,,,344,344,346,346,344,,346,344,346,346,,,', -',,344,,,,,,344,,,346,,,,,,346,,346,,346,346,,346,346,346,,346,346,346', -',,346,346,4,4,346,,4,346,4,,,,,,,346,,,,,,346,,,4,,,,,,4,,4,,4,4,,4', -'4,4,4,4,4,4,,,4,4,347,347,4,,347,4,347,347,,,,,,4,,,,,,4,,,347,,,,,', -'347,,347,,347,347,,347,347,347,,347,347,347,,,347,347,0,0,347,,0,347', -'0,,,,,,,347,,,,,,347,,,0,,,,,,0,,0,,0,0,,0,0,0,,0,0,0,,,0,0,87,87,0', -',87,0,87,,,,,,,0,,,,,,0,,,87,,,,,,87,,87,,87,87,,87,87,87,,87,87,,,', -'87,87,,,87,,,87,,,,192,192,192,192,87,192,192,192,192,192,87,192,192', -',,,,,192,192,192,238,238,238,238,,238,238,238,238,238,,238,238,,,192', -'192,,238,238,238,243,243,243,243,,243,243,243,243,243,,243,243,,,238', -'238,,243,243,243,,,,,,,,,,,,,,,,243,243' ] - racc_action_check = arr = ::Array.new(4804, nil) +'161,115,138,203,176,161,102,138,291,161,161,161,161,161,231,161,203', +'161,174,139,161,161,161,161,148,115,215,215,71,71,130,130,139,177,139', +'182,139,148,161,148,102,148,161,161,176,110,161,161,161,161,161,161', +'212,161,161,217,217,139,174,160,161,212,148,215,181,71,215,161,160,160', +'160,160,160,177,160,182,160,148,148,160,160,160,160,147,148,215,267', +'71,70,70,235,114,217,42,267,217,147,160,147,96,147,160,160,96,181,160', +'160,160,160,160,160,237,160,160,217,180,180,214,159,160,42,147,45,45', +'42,70,160,159,159,159,159,159,264,159,264,159,147,147,159,159,159,159', +'275,147,179,122,275,70,179,275,122,213,180,44,178,180,159,44,178,45', +'159,159,45,210,159,159,159,159,159,159,124,159,159,124,180,158,97,304', +'159,304,241,45,92,255,257,159,158,158,158,97,158,97,158,97,259,158,158', +'158,158,221,221,260,95,90,48,48,221,7,7,7,7,48,263,158,97,95,209,95', +'158,95,265,158,158,158,158,158,158,266,158,158,127,202,157,270,271,158', +'221,272,273,200,95,48,158,157,157,157,277,157,132,157,62,60,157,157', +'157,157,146,196,195,289,193,101,43,191,298,299,37,171,36,146,157,146', +'307,146,308,310,311,315,157,157,157,157,157,157,316,157,157,35,322,323', +'325,10,157,10,146,170,5,1,340,157,10,10,10,10,10,345,10,347,10,146,146', +'10,10,10,10,156,146,349,,,,,,,,,,156,156,10,156,,156,10,10,156,,10,10', +'10,10,10,10,,10,10,,,,,,10,,156,,,,,10,155,,156,156,156,156,156,156', +',156,156,,155,155,,155,156,155,,,155,,,156,154,,,,,,,,,,,,154,154,155', +'154,,154,,,154,,155,155,155,155,155,155,,155,155,,,,,,155,,154,,,,,155', +'153,,154,154,154,154,154,154,,154,154,,153,153,,153,154,153,,,153,,', +'154,152,,,,,,,,,,,,,152,153,152,,152,,,,,153,153,153,153,153,153,,153', +'153,,,,,,153,,152,,,,,153,,,152,152,152,152,152,152,,152,152,,,,,303', +'152,,,,,,,152,303,303,303,303,303,,303,,303,,,303,303,303,303,151,,', +',,,,,,,,,,151,303,151,,151,303,303,,,303,303,303,303,303,303,,303,303', +',,,,,303,,151,,,,150,303,,,151,151,151,151,151,151,,151,151,150,,150', +',150,151,,,,,,149,151,,,,,,,,,,,,149,150,149,,149,,,,,150,150,150,150', +',,,150,150,,,,,,150,,149,,,,,150,,,149,149,149,149,,,,149,149,,,,,166', +'149,,,,,,,149,166,166,166,166,166,166,166,,166,,,166,166,166,166,,,', +',,,,,,,,,,,166,,,,166,166,,,166,166,166,166,166,166,,166,166,,,,,297', +'166,,,,,,,166,297,297,297,297,297,,297,,297,,,297,297,297,297,,,,,,', +',,,,,,,,297,,,,297,297,,,297,297,297,297,297,297,,297,297,,,,,296,297', +',,,,,,297,296,296,296,296,296,,296,,296,,,296,296,296,296,145,,,,,,', +',,,,,,145,296,145,,145,296,296,,,296,296,296,296,296,296,,296,296,,', +',,,296,,145,,,,,296,,,,,145,145,,,,145,145,,,,,288,145,,,,,,,145,288', +'288,288,288,288,,288,,288,,,288,288,288,288,144,,,,,,,,,,,,,144,288', +'144,,144,288,288,,,288,288,288,288,288,288,,288,288,,,,,,288,,144,,', +',,288,,,,,144,144,,,,144,144,,,,,286,144,,,,,,,144,286,286,286,286,286', +',286,,286,,,286,286,286,286,,,,,,,,,,,,,,,286,,,,286,286,,,286,286,286', +'286,286,286,,286,286,,,,,190,286,,,,,,,286,190,190,190,190,190,,190', +',190,,,190,190,190,190,,,,,,,,,,,,,,,190,,,,190,190,,,190,190,190,190', +'190,190,,190,190,,,,,282,190,,,,,,,190,282,282,282,282,282,,282,,282', +',,282,282,282,282,,,,,,,,,,,,,,,282,,,,282,282,,,282,282,282,282,282', +'282,,282,282,,,,,128,282,,,,,,,282,128,128,128,128,128,,128,,128,,,128', +'128,128,128,,,,,,,,,,,,,,,128,,,,128,128,,,128,128,128,128,128,128,', +'128,128,,,,,121,128,,,,,,,128,121,121,121,121,121,,121,,121,,,121,121', +'121,121,,,,,,,,,,,,,,,121,,,,121,121,,,121,121,121,121,121,121,,121', +'121,,,,,108,121,108,,,,,,121,108,108,108,108,108,,108,,108,,,108,108', +'108,108,,,,,,,,,,,,,,,108,,,,108,108,,,108,108,108,108,108,108,,108', +'108,,,,,107,108,107,,,,,,108,107,107,107,107,107,,107,,107,,,107,107', +'107,107,,,,,,,,,,,,,,,107,,,,107,107,,,107,107,107,107,107,107,,107', +'107,,,,,106,107,106,,,,,,107,106,106,106,106,106,,106,,106,,,106,106', +'106,106,,,,,,,,,,,,,,,106,,,,106,106,,,106,106,106,106,106,106,,106', +'106,,,,,104,106,104,,,,,,106,104,104,104,104,104,,104,,104,,,104,104', +'104,104,,,,,,,,,,,,,,,104,,,,104,104,,,104,104,104,104,104,104,,104', +'104,,,,,98,104,,,,,,,104,98,98,98,98,98,,98,,98,,98,98,98,98,98,,,,', +',,,,,89,89,,,89,98,89,,,98,98,,,98,98,98,98,98,98,,98,98,89,,,,,98,89', +',89,,89,89,98,89,89,89,,89,89,,,,89,89,208,208,89,,208,89,208,,,,,,', +'89,,,,,,89,,89,208,,,,,,208,208,208,208,208,208,208,208,208,208,,208', +'208,,,,208,208,208,208,208,278,278,208,,278,,278,278,,,208,,,,,208,208', +',,,,,278,,,,,,278,,278,,278,278,,278,278,278,,278,278,278,,,278,278', +'268,268,278,,268,278,268,268,,,,,,278,,,,,,278,,,268,,,,,,268,,268,', +'268,268,,268,268,268,,268,268,,,,268,268,72,72,268,,72,268,72,,,,,,', +'268,,,,,,268,,,72,,,,,,72,,72,,72,72,,72,72,72,,72,72,,,,72,72,73,73', +'72,,73,72,73,,,,,,,72,,,,,,72,,,73,,,,,,73,,73,,73,73,,73,73,73,,73', +'73,,,,73,73,74,74,73,,74,73,74,,,,,,,73,,,,,,73,,,74,,,,,,74,,74,,74', +'74,,74,74,74,,74,74,,,,74,74,75,75,74,,75,74,75,,,,,,,74,,,,,,74,,,75', +',,,,,75,,75,,75,75,,75,75,75,,75,75,,,,75,75,76,76,75,,76,75,76,,,,', +',,75,,,,,,75,,,76,,,,,,76,,76,,76,76,,76,76,76,,76,76,,,,76,76,77,77', +'76,,77,76,77,,,,,,,76,,,,,,76,,,77,,,,,,77,,77,,77,77,,77,77,77,,77', +'77,,,,77,77,78,78,77,,78,77,78,,,,,,,77,,,,,,77,,,78,,,,,,78,,78,,78', +'78,,78,78,78,,78,78,,,,78,78,79,79,78,,79,78,79,,,,,,,78,,,,,,78,,,79', +',,,,,79,,79,,79,79,,79,79,79,,79,79,,,,79,79,80,80,79,,80,79,80,,,,', +',,79,,,,,,79,,,80,,,,,,80,,80,,80,80,,80,80,80,,80,80,,,,80,80,81,81', +'80,,81,80,81,,,,,,,80,,,,,,80,,,81,,,,,,81,,81,,81,81,,81,81,81,,81', +'81,,,,81,81,82,82,81,,82,81,82,,,,,,,81,,,,,,81,,,82,,,,,,82,,82,,82', +'82,,82,82,82,,82,82,,,,82,82,83,83,82,,83,82,83,,,,,,,82,,,,,,82,,,83', +',,,,,83,,83,,83,83,,83,83,83,,83,83,,,,83,83,84,84,83,,84,83,84,,,,', +',,83,,,,,,83,,,84,,,,,,84,,84,,84,84,,84,84,84,,84,84,,,,84,84,85,85', +'84,,85,84,85,,,,,,,84,,,,,,84,,,85,,,,,,85,,85,,85,85,,85,85,85,,85', +'85,,,,85,85,86,86,85,,86,85,86,,,,,,,85,,,,,,85,,,86,,,,,,86,,86,,86', +'86,,86,86,86,,86,86,,,,86,86,87,87,86,,87,86,87,,,,,,,86,,,,,,86,,,87', +',,,,,87,,87,,87,87,,87,87,87,,87,87,,,,87,87,88,88,87,,88,87,88,,,,', +',,87,,,,,,87,,,88,,,,,,88,,88,,88,88,,88,88,88,,88,88,,,,88,88,68,68', +'88,,68,88,68,,,,,,,88,,,,,,88,,,68,,,,,,68,,68,,68,68,,68,68,68,,68', +'68,,,,68,68,261,261,68,,261,68,261,,,,,,,68,,,,,,68,,,261,,,,,,261,', +'261,,261,261,,261,261,261,,261,261,,,,261,261,91,91,261,,91,261,91,', +',,,,,261,,,,,,261,,,91,,,,,,91,91,91,91,91,91,91,91,91,91,,91,91,,,', +'91,91,91,91,91,254,254,91,,254,,254,,,,91,,,,,91,91,,,,,,254,,,,,,254', +',254,,254,254,,254,254,254,,254,254,,,,254,254,93,93,254,,93,254,93', +',,,,,,254,,,,,,254,,,93,,,,,,93,,93,,93,93,,93,93,93,,93,93,,,,93,93', +'94,94,93,,94,93,94,,,,,,,93,,,,,,93,,,94,,,,,,94,,94,,94,94,,94,94,94', +',94,94,,,,94,94,240,240,94,,240,94,240,,,,,,,94,,,,,,94,,,240,,,,,,240', +',240,,240,240,,240,240,240,,240,240,,,,240,240,239,239,240,,239,240', +'239,,,,,,,240,,,,,,240,,,239,,,,,,239,,239,,239,239,,239,239,239,,239', +'239,,,,239,239,236,236,239,,236,239,236,,,,,,,239,,,,,,239,,,236,,,', +',,236,,236,,236,236,,236,236,236,,236,236,,,,236,236,67,67,236,,67,236', +'67,,,,,,,236,,,,,,236,,,67,,,,,,67,,67,,67,67,,67,67,67,,67,67,67,,', +'67,67,99,99,67,,99,67,99,,,,,,,67,,,,,,67,,,99,99,,,,,99,,99,,99,99', +',99,99,99,,99,99,,,,99,99,230,230,99,,230,99,230,,,,,,,99,,,,,,99,,', +'230,,,,,,230,,230,,230,230,,230,230,230,,230,230,,,,230,230,229,229', +'230,,229,230,229,,,,,,,230,,,,,,230,,,229,,,,,,229,,229,,229,229,,229', +'229,229,,229,229,,,,229,229,103,103,229,,103,229,103,,,,,,,229,,,,,', +'229,,,103,103,,,,,103,,103,,103,103,,103,103,103,,103,103,,,,103,103', +'66,66,103,,66,103,66,,,,,,,103,,,,,,103,,,66,,,,,,66,,66,,66,66,,66', +'66,66,,66,66,66,,,66,66,65,65,66,,65,66,65,,,,,,,66,,,,,,66,,,65,,,', +',,65,,65,,65,65,,65,65,65,,65,65,65,,,65,65,64,64,65,,64,65,64,,,,,', +',65,,,,,,65,,,64,,,,,,64,,64,,64,64,,64,64,64,,64,64,64,,,64,64,63,63', +'64,,63,64,63,,,,,,,64,,,,,,64,,,63,,,,,,63,,63,,63,63,,63,63,63,,63', +'63,63,,,63,63,109,109,63,,109,63,109,,,,,,,63,,,,,,63,,,109,,,,,,109', +',109,,109,109,,109,109,109,,109,109,,,,109,109,227,227,109,,227,109', +'227,,,,,,,109,,,,,,109,,,227,,,,,,227,,227,,227,227,,227,227,227,,227', +'227,,,,227,227,222,222,227,,222,227,222,,,,,,,227,,,,,,227,,,222,,,', +',,222,,222,,222,222,,222,222,222,,222,222,,,,222,222,,,222,218,218,222', +',218,218,218,,,,222,,,,,,222,,,,,,218,,,,,,218,,218,,218,218,,218,218', +'218,,218,218,,,,218,218,279,279,218,,279,218,279,279,,,,,,218,,,,,,218', +',,279,,,,,,279,,279,,279,279,,279,279,279,,279,279,279,,,279,279,69', +'69,279,,69,279,69,,,,,,,279,,,,,,279,,,69,,,,,,69,,69,,69,69,,69,69', +'69,,69,69,,,,69,69,207,207,69,,207,69,207,,,,,,,69,,,,,,69,,,207,,,', +',,207,,207,,207,207,,207,207,207,,207,207,,,,207,207,206,206,207,,206', +'207,206,206,,,,,,207,,,,,,207,,,206,,,,,,206,,206,,206,206,,206,206', +'206,,206,206,206,,,206,206,61,61,206,,61,206,61,,,,,,,206,,,,,,206,', +',61,,,,,,61,,61,,61,61,,61,61,61,,61,61,61,,,61,61,164,164,61,,164,61', +'164,,,,,,,61,,,,,,61,,,164,,,,,,164,,164,,164,164,,164,164,164,,164', +'164,,,,164,164,198,198,164,,198,164,198,198,,,,,,164,,,,,,164,,,198', +',,,,,198,,198,,198,198,,198,198,198,,198,198,198,,,198,198,52,52,198', +',52,198,52,,,,,,,198,,,,,,198,,,52,,,,,,52,,52,,52,52,,52,52,52,,52', +'52,,,,52,52,169,169,52,,169,52,169,,,,,,,52,,,,,,52,,,169,,,,,,169,', +'169,,169,169,,169,169,169,,169,169,,,,169,169,,,169,47,47,169,,47,47', +'47,,,,169,,,,,,169,,,,,,47,,,,,,47,,47,,47,47,,47,47,47,,47,47,,,,47', +'47,290,290,47,,290,47,290,,,,,,,47,,,,,,47,,,290,,,,,,290,,290,,290', +'290,,290,290,290,,290,290,,,,290,290,168,168,290,,168,290,168,,,,,,', +'290,,,,,,290,,,168,,,,,,168,,168,,168,168,,168,168,168,,168,168,,,,168', +'168,167,167,168,,167,168,167,,,,,,,168,,,,,,168,,,167,,,,,,167,,167', +',167,167,,167,167,167,,167,167,,,,167,167,41,41,167,,41,167,41,,,,,', +',167,,,,,,167,,,41,,,,,,41,,41,,41,41,,41,41,41,,41,41,,,,41,41,40,40', +'41,,40,41,40,,,,,,,41,,,,,,41,,,40,,,,,,40,,40,,40,40,,40,40,40,,40', +'40,,,,40,40,39,39,40,,39,40,39,,,,,,,40,,,,,,40,,,39,,,,,,39,,39,,39', +'39,,39,39,39,,39,39,,,,39,39,38,38,39,,38,39,38,,,,,,,39,,,,,,39,,,38', +',,,,,38,,38,,38,38,,38,38,38,,38,38,,,,38,38,306,306,38,,306,38,306', +',,,,,,38,,,,,,38,,,306,,,,,,306,,306,,306,306,,306,306,306,,306,306', +',,,306,306,318,318,306,,318,306,318,318,,,,,,306,,,,,,306,,,318,,,,', +',318,,318,,318,318,,318,318,318,,318,318,318,,,318,318,13,13,318,,13', +'318,13,,,,,,,318,,,,,,318,,,13,,,,,,13,,13,,13,13,,13,13,13,,13,13,', +',,13,13,12,12,13,,12,13,12,,,,,,,13,,,,,,13,,,12,,,,,,12,,12,,12,12', +',12,12,12,,12,12,,,,12,12,11,11,12,,11,12,11,,,,,,,12,,,,,,12,,,11,', +',,,,11,,11,,11,11,,11,11,11,,11,11,,,,11,11,334,334,11,,334,11,334,334', +',,,,,11,,,,,,11,,,334,,,,,,334,,334,,334,334,,334,334,334,,334,334,334', +',,334,334,336,336,334,,336,334,336,336,,,,,,334,,,,,,334,,,336,,,,,', +'336,,336,,336,336,,336,336,336,,336,336,336,,,336,336,4,4,336,,4,336', +'4,,,,,,,336,,,,,,336,,,4,,,,,,4,,4,,4,4,,4,4,4,4,4,4,4,,,4,4,337,337', +'4,,337,4,337,337,,,,,,4,,,,,,4,,,337,,,,,,337,,337,,337,337,,337,337', +'337,,337,337,337,,,337,337,0,0,337,,0,337,0,,,,,,,337,,,,,,337,,,0,', +',,,,0,,0,,0,0,,0,0,0,,0,0,0,,,0,0,205,205,0,,205,0,205,205,,,,,,0,,', +',,,0,,,205,,,,,,205,,205,,205,205,,205,205,205,,205,205,205,,,205,205', +',,205,,,205,,,,233,233,233,233,205,233,233,233,233,233,205,233,233,', +',,,,233,233,233,189,189,189,189,,189,189,189,189,189,,189,189,,,233', +'233,,189,189,189,238,238,238,238,,238,238,238,238,238,,238,238,,,189', +'189,,238,238,238,,,,,,,,,,,,,,,,238,238' ] + racc_action_check = arr = ::Array.new(4874, nil) idx = 0 clist.each do |str| str.split(',', -1).each do |i| @@ -353,225 +353,228 @@ clist = [ end racc_action_pointer = [ - 4621, 340, nil, nil, 4529, 327, nil, 219, nil, nil, - 247, 4391, 4345, 4299, nil, nil, nil, nil, nil, nil, + 4691, 297, nil, nil, 4599, 284, nil, 145, nil, nil, + 285, 4461, 4415, 4369, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, - nil, nil, nil, nil, nil, 309, 115, 288, 4161, 4115, - 4069, 4023, -6, 262, 95, 208, nil, 3839, 135, nil, - nil, nil, 3744, nil, nil, nil, nil, nil, nil, nil, - 285, 3606, 267, 3189, 3143, 3097, 3051, 2775, 2358, 3468, - 5, 16, 1576, 1622, 1668, 1714, 1760, 1806, 1852, 1898, - 1944, 1990, 2036, 2082, 2128, 2174, 2220, 4667, 2312, 1386, - 154, 2450, 165, 2545, 2591, 213, 43, 112, 1327, 2821, - nil, nil, nil, 47, 149, -31, 3005, 1275, nil, 1214, - 1153, 1092, 3235, 286, nil, nil, nil, 290, -8, nil, - nil, nil, nil, nil, 1031, 19, nil, 99, nil, nil, - 98, 979, nil, 219, nil, 215, nil, nil, nil, nil, - nil, 23, 156, nil, nil, nil, nil, 771, 701, 427, - 212, 122, 527, 503, 475, 375, 399, 347, 323, 271, - 186, 142, 98, 46, -6, nil, nil, 2266, nil, 579, - 3977, 3931, 3790, 268, 281, nil, nil, 5, nil, -10, - 25, 54, 48, 187, 29, 28, nil, nil, nil, nil, - nil, nil, 4694, 875, 242, nil, 264, nil, 255, 71, - 220, nil, nil, 3698, nil, 137, nil, 117, 2, nil, - 3652, 3560, 3514, 1435, 214, 280, nil, 0, 330, 309, - 68, nil, 219, 3376, nil, nil, 83, 3327, nil, nil, - nil, nil, 3281, nil, 2959, 2913, 111, nil, 4715, nil, - 36, 2867, 72, 4736, 2729, 2683, 124, nil, nil, nil, - nil, nil, nil, nil, nil, nil, nil, nil, nil, 2637, - 142, nil, nil, 2499, 171, nil, 117, 153, 2404, nil, - 197, 26, 211, 194, 12, 1530, nil, 197, 232, 240, - 243, nil, 49, nil, 247, 1484, 3422, nil, nil, 927, - nil, nil, nil, 823, nil, 753, 279, 3885, 286, nil, - nil, nil, nil, 683, 631, 300, 28, 302, nil, nil, - nil, nil, 451, 145, nil, 4207, -2, 287, nil, 311, - 312, nil, nil, nil, 313, 323, nil, 4253, nil, nil, - nil, 309, 326, nil, 327, nil, nil, nil, nil, nil, - nil, nil, nil, nil, 4437, nil, 4483, 4575, nil, nil, - 334, nil, nil, nil, nil, 338, nil, 339, nil, 341, + nil, nil, nil, nil, nil, 263, 200, 242, 4231, 4185, + 4139, 4093, 85, 219, 118, 120, nil, 3909, 202, nil, + nil, nil, 3814, nil, nil, nil, nil, nil, nil, nil, + 251, 3676, 238, 3259, 3213, 3167, 3121, 2891, 2474, 3538, + 86, 26, 1692, 1738, 1784, 1830, 1876, 1922, 1968, 2014, + 2060, 2106, 2152, 2198, 2244, 2290, 2336, 2382, 2428, 1505, + 164, 2566, 174, 2661, 2707, 196, 64, 170, 1468, 2937, + nil, 253, -28, 3075, 1409, nil, 1350, 1291, 1232, 3305, + 21, nil, nil, nil, 67, -11, nil, nil, nil, nil, + nil, 1173, 138, nil, 161, nil, nil, 219, 1114, nil, + 26, nil, 236, nil, nil, nil, nil, nil, -5, 13, + nil, nil, nil, nil, 878, 795, 250, 77, 18, 594, + 570, 528, 445, 421, 377, 353, 309, 226, 169, 112, + 53, -6, nil, nil, 3722, nil, 653, 4047, 4001, 3860, + 255, 255, nil, nil, 7, nil, -7, 22, 119, 109, + 113, 53, 24, nil, nil, nil, nil, nil, nil, 4785, + 996, 218, nil, 238, nil, 246, 189, nil, 3768, nil, + 227, nil, 216, -9, nil, 4737, 3630, 3584, 1551, 176, + 127, nil, 27, 143, 109, 24, nil, 53, 3446, nil, + nil, 197, 3397, nil, nil, nil, nil, 3351, nil, 3029, + 2983, 2, nil, 4764, nil, 81, 2845, 102, 4806, 2799, + 2753, 168, nil, nil, nil, nil, nil, nil, nil, nil, + nil, nil, nil, nil, 2615, 158, nil, 175, nil, 126, + 167, 2520, nil, 203, 101, 211, 196, 74, 1646, nil, + 193, 222, 228, 230, nil, 107, nil, 234, 1600, 3492, + nil, nil, 1055, nil, nil, nil, 937, nil, 854, 250, + 3955, -4, nil, nil, nil, nil, 771, 712, 255, 197, + nil, nil, nil, 504, 146, nil, 4277, 264, 243, nil, + 267, 268, nil, nil, nil, 268, 275, nil, 4323, nil, + nil, nil, 263, 280, nil, 281, nil, nil, nil, nil, + nil, nil, nil, nil, 4507, nil, 4553, 4645, nil, nil, + 289, nil, nil, nil, nil, 296, nil, 298, nil, 308, nil, nil, nil, nil, nil ] racc_action_default = [ - -209, -210, -1, -2, -3, -4, -7, -9, -10, -15, - -109, -210, -210, -210, -43, -44, -45, -46, -47, -48, + -203, -204, -1, -2, -3, -4, -7, -9, -10, -15, + -103, -204, -204, -204, -43, -44, -45, -46, -47, -48, -49, -50, -51, -52, -53, -54, -55, -56, -57, -58, - -59, -60, -61, -62, -63, -68, -69, -73, -210, -210, - -210, -210, -210, -119, -210, -210, -164, -210, -210, -174, - -175, -176, -210, -178, -185, -186, -187, -188, -189, -190, - -210, -210, -6, -210, -210, -210, -210, -210, -210, -210, - -210, -210, -210, -210, -210, -210, -210, -210, -210, -210, - -210, -210, -210, -210, -210, -210, -210, -210, -210, -210, - -210, -127, -122, -209, -209, -27, -210, -34, -210, -210, - -70, -75, -76, -209, -210, -210, -210, -210, -86, -210, - -210, -210, -210, -209, -153, -154, -120, -209, -209, -145, - -147, -148, -149, -150, -41, -210, -167, -210, -170, -171, - -210, -182, -177, -210, 365, -5, -8, -11, -12, -13, - -14, -210, -17, -18, -162, -163, -19, -20, -21, -22, - -23, -24, -25, -26, -28, -29, -30, -31, -32, -33, - -35, -36, -37, -38, -210, -39, -104, -210, -74, -210, - -202, -208, -196, -193, -191, -117, -128, -185, -131, -189, - -210, -199, -197, -205, -187, -188, -195, -200, -201, -203, - -204, -206, -127, -126, -210, -125, -210, -40, -191, -65, - -210, -80, -81, -210, -84, -191, -158, -161, -210, -72, - -210, -210, -210, -127, -193, -209, -155, -210, -210, -210, - -210, -151, -210, -210, -165, -168, -210, -210, -179, -180, - -181, -183, -210, -16, -210, -210, -191, -106, -127, -116, - -210, -194, -210, -192, -210, -210, -191, -130, -132, -196, - -197, -198, -199, -202, -205, -207, -208, -123, -124, -192, - -210, -67, -77, -210, -210, -83, -210, -192, -210, -71, - -210, -89, -210, -95, -210, -210, -99, -193, -191, -210, - -210, -139, -210, -156, -191, -210, -210, -146, -152, -42, - -166, -169, -172, -173, -184, -108, -210, -192, -191, -112, - -118, -113, -129, -133, -134, -210, -64, -210, -79, -82, - -85, -159, -160, -89, -88, -210, -210, -95, -94, -210, - -210, -103, -98, -100, -210, -210, -114, -210, -140, -141, - -142, -210, -210, -136, -210, -144, -105, -107, -115, -121, - -66, -78, -87, -90, -210, -93, -210, -210, -110, -111, - -210, -138, -157, -135, -143, -210, -92, -210, -97, -210, - -102, -137, -91, -96, -101 ] + -59, -60, -61, -62, -63, -68, -69, -73, -204, -204, + -204, -204, -204, -113, -204, -204, -158, -204, -204, -168, + -169, -170, -204, -172, -179, -180, -181, -182, -183, -184, + -204, -204, -6, -204, -204, -204, -204, -204, -204, -204, + -204, -204, -204, -204, -204, -204, -204, -204, -204, -204, + -204, -204, -204, -204, -204, -204, -204, -204, -204, -204, + -204, -121, -116, -203, -203, -27, -204, -34, -204, -204, + -70, -204, -204, -204, -204, -80, -204, -204, -204, -204, + -203, -147, -148, -114, -203, -203, -139, -141, -142, -143, + -144, -41, -204, -161, -204, -164, -165, -204, -176, -171, + -204, 355, -5, -8, -11, -12, -13, -14, -204, -17, + -18, -156, -157, -19, -20, -21, -22, -23, -24, -25, + -26, -28, -29, -30, -31, -32, -33, -35, -36, -37, + -38, -204, -39, -98, -204, -74, -204, -196, -202, -190, + -187, -185, -111, -122, -179, -125, -183, -204, -193, -191, + -199, -181, -182, -189, -194, -195, -197, -198, -200, -121, + -120, -204, -119, -204, -40, -185, -65, -75, -204, -78, + -185, -152, -155, -204, -72, -204, -204, -204, -121, -187, + -203, -149, -204, -204, -204, -204, -145, -204, -204, -159, + -162, -204, -204, -173, -174, -175, -177, -204, -16, -204, + -204, -185, -100, -121, -110, -204, -188, -204, -186, -204, + -204, -185, -124, -126, -190, -191, -192, -193, -196, -199, + -201, -202, -117, -118, -186, -204, -67, -204, -77, -204, + -186, -204, -71, -204, -83, -204, -89, -204, -204, -93, + -187, -185, -204, -204, -133, -204, -150, -185, -204, -204, + -140, -146, -42, -160, -163, -166, -167, -178, -102, -204, + -186, -185, -106, -112, -107, -123, -127, -128, -204, -64, + -76, -79, -153, -154, -83, -82, -204, -204, -89, -88, + -204, -204, -97, -92, -94, -204, -204, -108, -204, -134, + -135, -136, -204, -204, -130, -204, -138, -99, -101, -109, + -115, -66, -81, -84, -204, -87, -204, -204, -104, -105, + -204, -132, -151, -129, -137, -204, -86, -204, -91, -204, + -96, -131, -85, -90, -95 ] racc_goto_table = [ - 2, 117, 100, 95, 97, 98, 3, 132, 129, 166, - 174, 240, 130, 205, 314, 318, 123, 121, 215, 173, - 1, 276, 218, 320, 287, 62, 288, 242, 194, 196, - 107, 109, 110, 111, 145, 145, 125, 143, 146, 124, - 214, 144, 144, 236, 131, 137, 138, 139, 140, 275, - 300, 260, 279, 238, 343, 302, 342, 141, 266, 345, - 124, 142, 262, 200, 147, 148, 149, 150, 151, 152, - 153, 154, 155, 156, 157, 158, 159, 160, 161, 162, - 163, 164, 135, 169, 323, 193, 193, 237, 198, 296, - 280, 124, 328, 219, 203, 208, 311, 127, 124, 305, - 165, 136, 231, 232, 169, 230, nil, nil, nil, 201, - nil, 246, nil, nil, nil, 324, nil, nil, nil, 216, - nil, nil, nil, 216, 221, 284, nil, nil, nil, nil, - nil, 325, 278, nil, nil, nil, nil, 331, 117, nil, - nil, 277, nil, nil, nil, nil, nil, nil, nil, nil, - nil, 338, nil, nil, 123, 121, nil, 298, nil, 164, - nil, nil, 107, 109, 110, 261, nil, nil, nil, nil, - nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, - nil, nil, nil, nil, nil, nil, 292, 294, nil, nil, - 130, 123, 121, 123, 121, nil, nil, nil, nil, nil, - nil, nil, nil, 264, 124, 169, nil, nil, nil, nil, - 270, 272, nil, nil, nil, 289, nil, 337, nil, 293, - nil, 281, nil, nil, 131, nil, 289, 295, nil, nil, - nil, nil, nil, 169, nil, nil, 303, 304, nil, 329, - nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, - nil, 289, nil, nil, nil, nil, nil, nil, nil, nil, - 312, nil, nil, 307, nil, nil, nil, 124, nil, nil, - nil, nil, 340, nil, nil, nil, nil, nil, nil, nil, - nil, nil, nil, nil, nil, 332, 334, nil, nil, 164, + 2, 3, 100, 95, 97, 98, 114, 163, 129, 126, + 170, 171, 127, 200, 118, 305, 309, 120, 235, 210, + 280, 311, 281, 213, 237, 231, 269, 293, 209, 233, + 104, 106, 107, 108, 142, 142, 62, 140, 143, 121, + 191, 193, 141, 141, 128, 268, 295, 333, 255, 134, + 135, 136, 137, 259, 197, 332, 273, 272, 335, 319, + 121, 139, 214, 162, 144, 145, 146, 147, 148, 149, + 150, 151, 152, 153, 154, 155, 156, 157, 158, 159, + 160, 161, 232, 166, 289, 190, 190, 314, 302, 122, + 124, 121, 133, 132, 298, 121, 1, 226, 227, 225, + nil, 166, nil, nil, nil, nil, nil, nil, nil, 241, + 138, 211, nil, nil, nil, 211, 216, nil, 315, nil, + nil, nil, nil, 277, 316, nil, nil, 270, 271, nil, + 322, nil, nil, nil, nil, nil, nil, nil, nil, nil, + 114, 195, nil, nil, 329, 203, nil, nil, nil, 118, + nil, nil, 120, 291, nil, nil, 161, nil, nil, 104, + 106, 107, 256, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, - nil, nil, nil, nil, nil, nil, nil, 107, nil, nil, + nil, nil, 285, 287, 118, 127, 118, 120, nil, 120, + nil, nil, nil, nil, nil, nil, nil, nil, 257, 121, + 166, nil, nil, nil, nil, 263, 265, nil, 328, nil, + 282, 274, nil, nil, 286, nil, nil, nil, nil, 128, + nil, 282, 288, nil, nil, nil, nil, nil, 166, nil, + nil, 296, 297, nil, nil, nil, nil, 320, nil, nil, + nil, nil, nil, nil, nil, nil, 282, nil, nil, nil, + nil, nil, nil, 303, nil, nil, nil, nil, nil, nil, + 121, nil, nil, nil, nil, 331, nil, nil, nil, nil, + nil, nil, nil, nil, nil, nil, nil, nil, 323, 325, + nil, nil, 161, nil, nil, nil, nil, nil, nil, nil, + nil, nil, nil, nil, nil, nil, nil, nil, 104, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, - nil, nil, nil, nil, nil, nil, nil, 350, nil, nil, + nil, nil, nil, nil, nil, nil, nil, nil, 340, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, - nil, nil, nil, nil, 355, nil, 357, 359 ] + nil, nil, nil, nil, 345, nil, 347, 349 ] racc_goto_check = [ - 2, 65, 37, 9, 9, 9, 3, 78, 74, 52, - 57, 56, 31, 45, 47, 48, 30, 35, 66, 55, - 1, 50, 66, 51, 71, 5, 71, 36, 61, 61, - 9, 9, 9, 9, 31, 31, 11, 12, 12, 9, - 55, 30, 30, 53, 9, 7, 7, 7, 7, 49, - 58, 36, 56, 59, 46, 62, 47, 11, 36, 48, - 9, 9, 44, 43, 9, 9, 9, 9, 9, 9, + 2, 3, 37, 9, 9, 9, 62, 49, 75, 71, + 52, 54, 31, 42, 35, 44, 45, 30, 53, 63, + 68, 48, 68, 63, 36, 50, 47, 55, 52, 56, + 9, 9, 9, 9, 31, 31, 5, 12, 12, 9, + 58, 58, 30, 30, 9, 46, 59, 43, 36, 7, + 7, 7, 7, 36, 41, 44, 64, 53, 45, 65, + 9, 9, 67, 13, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, - 9, 9, 5, 9, 50, 9, 9, 52, 11, 36, - 67, 9, 68, 70, 42, 11, 72, 73, 9, 36, - 13, 6, 79, 80, 9, 82, nil, nil, nil, 3, - nil, 57, nil, nil, nil, 56, nil, nil, nil, 3, - nil, nil, nil, 3, 3, 45, nil, nil, nil, nil, - nil, 36, 57, nil, nil, nil, nil, 36, 65, nil, - nil, 55, nil, nil, nil, nil, nil, nil, nil, nil, - nil, 36, nil, nil, 30, 35, nil, 57, nil, 9, - nil, nil, 9, 9, 9, 37, nil, nil, nil, nil, + 9, 9, 49, 9, 36, 9, 9, 47, 69, 11, + 70, 9, 6, 5, 36, 9, 1, 76, 77, 79, + nil, 9, nil, nil, nil, nil, nil, nil, nil, 54, + 11, 3, nil, nil, nil, 3, 3, nil, 53, nil, + nil, nil, nil, 42, 36, nil, nil, 52, 54, nil, + 36, nil, nil, nil, nil, nil, nil, nil, nil, nil, + 62, 11, nil, nil, 36, 11, nil, nil, nil, 35, + nil, nil, 30, 54, nil, nil, 9, nil, nil, 9, + 9, 9, 37, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, - nil, nil, nil, nil, nil, nil, 74, 78, nil, nil, - 31, 30, 35, 30, 35, nil, nil, nil, nil, nil, - nil, nil, nil, 2, 9, 9, nil, nil, nil, nil, - 2, 2, nil, nil, nil, 9, nil, 52, nil, 9, - nil, 3, nil, nil, 9, nil, 9, 9, nil, nil, - nil, nil, nil, 9, nil, nil, 9, 9, nil, 65, + nil, nil, 71, 75, 35, 31, 35, 30, nil, 30, + nil, nil, nil, nil, nil, nil, nil, nil, 2, 9, + 9, nil, nil, nil, nil, 2, 2, nil, 49, nil, + 9, 3, nil, nil, 9, nil, nil, nil, nil, 9, + nil, 9, 9, nil, nil, nil, nil, nil, 9, nil, + nil, 9, 9, nil, nil, nil, nil, 62, nil, nil, + nil, nil, nil, nil, nil, nil, 9, nil, nil, nil, + nil, nil, nil, 9, nil, nil, nil, nil, nil, nil, + 9, nil, nil, nil, nil, 37, nil, nil, nil, nil, + nil, nil, nil, nil, nil, nil, nil, nil, 2, 2, + nil, nil, 9, nil, nil, nil, nil, nil, nil, nil, + nil, nil, nil, nil, nil, nil, nil, nil, 9, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, - nil, 9, nil, nil, nil, nil, nil, nil, nil, nil, - 9, nil, nil, 2, nil, nil, nil, 9, nil, nil, - nil, nil, 37, nil, nil, nil, nil, nil, nil, nil, - nil, nil, nil, nil, nil, 2, 2, nil, nil, 9, - nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, - nil, nil, nil, nil, nil, nil, nil, 9, nil, nil, - nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, - nil, nil, nil, nil, nil, nil, nil, 2, nil, nil, + nil, nil, nil, nil, nil, nil, nil, nil, 2, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, 2, nil, 2, 2 ] racc_goto_pointer = [ - nil, 20, 0, 6, nil, 21, 38, -19, nil, -8, - nil, -11, -33, 11, nil, nil, nil, nil, nil, nil, + nil, 96, 0, 1, nil, 32, 29, -15, nil, -8, + nil, 42, -33, -26, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, - -29, -36, nil, nil, nil, -28, -147, -34, nil, nil, - nil, nil, -10, -40, -138, -92, -261, -257, -258, -163, - -191, -251, -80, -124, nil, -72, -162, -81, -191, -116, - nil, -65, -188, nil, nil, -43, -95, -125, -190, nil, - -25, -196, -171, 49, -40, nil, nil, nil, -45, -31, - -30, nil, -28 ] + -28, -36, nil, nil, nil, -31, -147, -34, nil, nil, + nil, -47, -89, -259, -249, -250, -162, -181, -246, -82, + -139, nil, -81, -152, -80, -209, -137, nil, -53, -192, + nil, nil, -38, -91, -154, -216, nil, -53, -195, -172, + 42, -39, nil, nil, nil, -44, -33, -32, nil, -31 ] racc_goto_default = [ - nil, nil, nil, 195, 4, 5, 6, 7, 8, 10, - 9, 274, nil, nil, 14, 35, 15, 16, 17, 18, + nil, nil, nil, 192, 4, 5, 6, 7, 8, 10, + 9, 267, nil, nil, 14, 35, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, nil, nil, 36, 37, - 101, 102, 103, nil, nil, nil, 108, nil, nil, nil, - nil, nil, nil, nil, 41, nil, nil, nil, 175, nil, - 92, nil, 176, 180, 178, 113, nil, nil, nil, 118, - nil, 119, 206, nil, nil, 49, 50, 52, nil, nil, - nil, 133, nil ] + 101, nil, nil, 105, nil, nil, nil, nil, nil, nil, + nil, 41, nil, nil, nil, 172, nil, 92, nil, 173, + 177, 175, 110, nil, nil, nil, 115, nil, 116, 201, + nil, nil, 49, 50, 52, nil, nil, nil, 130, nil ] racc_reduce_table = [ 0, 0, :racc_error, - 1, 78, :_reduce_1, - 1, 78, :_reduce_none, - 1, 79, :_reduce_3, - 1, 81, :_reduce_4, - 3, 81, :_reduce_5, - 2, 81, :_reduce_6, - 1, 82, :_reduce_7, - 3, 82, :_reduce_8, - 1, 83, :_reduce_none, - 1, 84, :_reduce_10, - 3, 84, :_reduce_11, - 3, 84, :_reduce_12, - 3, 84, :_reduce_13, - 3, 84, :_reduce_14, + 1, 77, :_reduce_1, + 1, 77, :_reduce_none, + 1, 78, :_reduce_3, + 1, 80, :_reduce_4, + 3, 80, :_reduce_5, + 2, 80, :_reduce_6, + 1, 81, :_reduce_7, + 3, 81, :_reduce_8, + 1, 82, :_reduce_none, + 1, 83, :_reduce_10, + 3, 83, :_reduce_11, + 3, 83, :_reduce_12, + 3, 83, :_reduce_13, + 3, 83, :_reduce_14, + 1, 85, :_reduce_none, + 4, 85, :_reduce_16, + 3, 85, :_reduce_17, + 3, 85, :_reduce_18, + 3, 85, :_reduce_19, + 3, 85, :_reduce_20, + 3, 85, :_reduce_21, + 3, 85, :_reduce_22, + 3, 85, :_reduce_23, + 3, 85, :_reduce_24, + 3, 85, :_reduce_25, + 3, 85, :_reduce_26, + 2, 85, :_reduce_27, + 3, 85, :_reduce_28, + 3, 85, :_reduce_29, + 3, 85, :_reduce_30, + 3, 85, :_reduce_31, + 3, 85, :_reduce_32, + 3, 85, :_reduce_33, + 2, 85, :_reduce_34, + 3, 85, :_reduce_35, + 3, 85, :_reduce_36, + 3, 85, :_reduce_37, + 3, 85, :_reduce_38, + 3, 85, :_reduce_39, + 3, 85, :_reduce_40, + 1, 87, :_reduce_41, + 3, 87, :_reduce_42, 1, 86, :_reduce_none, - 4, 86, :_reduce_16, - 3, 86, :_reduce_17, - 3, 86, :_reduce_18, - 3, 86, :_reduce_19, - 3, 86, :_reduce_20, - 3, 86, :_reduce_21, - 3, 86, :_reduce_22, - 3, 86, :_reduce_23, - 3, 86, :_reduce_24, - 3, 86, :_reduce_25, - 3, 86, :_reduce_26, - 2, 86, :_reduce_27, - 3, 86, :_reduce_28, - 3, 86, :_reduce_29, - 3, 86, :_reduce_30, - 3, 86, :_reduce_31, - 3, 86, :_reduce_32, - 3, 86, :_reduce_33, - 2, 86, :_reduce_34, - 3, 86, :_reduce_35, - 3, 86, :_reduce_36, - 3, 86, :_reduce_37, - 3, 86, :_reduce_38, - 3, 86, :_reduce_39, - 3, 86, :_reduce_40, - 1, 88, :_reduce_41, - 3, 88, :_reduce_42, - 1, 87, :_reduce_none, - 1, 92, :_reduce_none, + 1, 91, :_reduce_none, + 1, 91, :_reduce_none, + 1, 91, :_reduce_none, + 1, 91, :_reduce_none, + 1, 91, :_reduce_none, + 1, 91, :_reduce_none, + 1, 91, :_reduce_none, + 1, 91, :_reduce_none, + 1, 91, :_reduce_none, + 1, 91, :_reduce_none, 1, 92, :_reduce_none, 1, 92, :_reduce_none, 1, 92, :_reduce_none, @@ -580,167 +583,152 @@ racc_reduce_table = [ 1, 92, :_reduce_none, 1, 92, :_reduce_none, 1, 92, :_reduce_none, - 1, 92, :_reduce_none, - 1, 93, :_reduce_none, - 1, 93, :_reduce_none, - 1, 93, :_reduce_none, - 1, 93, :_reduce_none, - 1, 93, :_reduce_none, - 1, 93, :_reduce_none, - 1, 93, :_reduce_none, - 1, 93, :_reduce_none, - 1, 108, :_reduce_62, - 1, 108, :_reduce_63, - 5, 91, :_reduce_64, - 3, 91, :_reduce_65, - 6, 91, :_reduce_66, - 4, 91, :_reduce_67, - 1, 91, :_reduce_68, - 1, 95, :_reduce_69, - 2, 95, :_reduce_70, - 4, 115, :_reduce_71, - 3, 115, :_reduce_72, - 1, 115, :_reduce_73, - 3, 116, :_reduce_74, - 1, 114, :_reduce_none, - 1, 114, :_reduce_none, - 3, 117, :_reduce_77, - 3, 121, :_reduce_78, - 2, 121, :_reduce_79, - 1, 120, :_reduce_none, - 1, 120, :_reduce_none, - 4, 118, :_reduce_82, - 3, 118, :_reduce_83, - 2, 119, :_reduce_84, - 4, 119, :_reduce_85, - 2, 98, :_reduce_86, - 5, 123, :_reduce_87, - 4, 123, :_reduce_88, - 0, 124, :_reduce_none, - 2, 124, :_reduce_90, - 4, 124, :_reduce_91, - 3, 124, :_reduce_92, - 6, 99, :_reduce_93, - 5, 99, :_reduce_94, - 0, 125, :_reduce_none, - 4, 125, :_reduce_96, - 3, 125, :_reduce_97, - 5, 97, :_reduce_98, - 1, 126, :_reduce_99, - 2, 126, :_reduce_100, - 5, 127, :_reduce_101, - 4, 127, :_reduce_102, - 1, 128, :_reduce_103, - 1, 90, :_reduce_none, - 4, 90, :_reduce_105, - 1, 130, :_reduce_106, - 3, 130, :_reduce_107, - 3, 129, :_reduce_108, - 1, 85, :_reduce_109, - 6, 85, :_reduce_110, - 6, 85, :_reduce_111, - 5, 85, :_reduce_112, - 5, 85, :_reduce_113, - 5, 85, :_reduce_114, - 4, 135, :_reduce_115, - 1, 136, :_reduce_116, - 1, 132, :_reduce_117, - 3, 132, :_reduce_118, - 1, 131, :_reduce_119, - 2, 131, :_reduce_120, - 6, 96, :_reduce_121, - 2, 96, :_reduce_122, - 3, 137, :_reduce_123, - 3, 137, :_reduce_124, - 1, 138, :_reduce_none, - 1, 138, :_reduce_none, - 0, 134, :_reduce_127, - 1, 134, :_reduce_128, - 3, 134, :_reduce_129, - 1, 140, :_reduce_none, - 1, 140, :_reduce_none, - 1, 140, :_reduce_none, - 3, 139, :_reduce_133, - 3, 139, :_reduce_134, - 6, 100, :_reduce_135, - 5, 100, :_reduce_136, - 7, 101, :_reduce_137, - 6, 101, :_reduce_138, - 1, 144, :_reduce_none, - 2, 144, :_reduce_140, - 1, 145, :_reduce_none, - 1, 145, :_reduce_none, - 6, 102, :_reduce_143, - 5, 102, :_reduce_144, - 1, 146, :_reduce_145, - 3, 146, :_reduce_146, - 1, 148, :_reduce_147, - 1, 148, :_reduce_148, - 1, 148, :_reduce_149, - 1, 148, :_reduce_none, - 1, 147, :_reduce_none, - 2, 147, :_reduce_152, - 1, 142, :_reduce_153, - 1, 142, :_reduce_154, - 1, 143, :_reduce_155, - 2, 143, :_reduce_156, - 4, 143, :_reduce_157, - 1, 122, :_reduce_158, - 3, 122, :_reduce_159, - 3, 149, :_reduce_160, - 1, 149, :_reduce_161, + 1, 107, :_reduce_62, + 1, 107, :_reduce_63, + 5, 90, :_reduce_64, + 3, 90, :_reduce_65, + 6, 90, :_reduce_66, + 4, 90, :_reduce_67, + 1, 90, :_reduce_68, + 1, 94, :_reduce_69, + 2, 94, :_reduce_70, + 4, 114, :_reduce_71, + 3, 114, :_reduce_72, + 1, 114, :_reduce_73, + 3, 115, :_reduce_74, + 2, 113, :_reduce_75, + 3, 117, :_reduce_76, + 2, 117, :_reduce_77, + 2, 116, :_reduce_78, + 4, 116, :_reduce_79, + 2, 97, :_reduce_80, + 5, 119, :_reduce_81, + 4, 119, :_reduce_82, + 0, 120, :_reduce_none, + 2, 120, :_reduce_84, + 4, 120, :_reduce_85, + 3, 120, :_reduce_86, + 6, 98, :_reduce_87, + 5, 98, :_reduce_88, + 0, 121, :_reduce_none, + 4, 121, :_reduce_90, + 3, 121, :_reduce_91, + 5, 96, :_reduce_92, + 1, 122, :_reduce_93, + 2, 122, :_reduce_94, + 5, 123, :_reduce_95, + 4, 123, :_reduce_96, + 1, 124, :_reduce_97, 1, 89, :_reduce_none, - 1, 89, :_reduce_none, - 1, 94, :_reduce_164, - 3, 103, :_reduce_165, - 4, 103, :_reduce_166, - 2, 103, :_reduce_167, - 3, 106, :_reduce_168, - 4, 106, :_reduce_169, - 2, 106, :_reduce_170, - 1, 150, :_reduce_171, - 3, 150, :_reduce_172, - 3, 151, :_reduce_173, - 1, 112, :_reduce_none, - 1, 112, :_reduce_none, - 1, 152, :_reduce_176, - 2, 153, :_reduce_177, - 1, 154, :_reduce_178, - 1, 156, :_reduce_179, - 1, 157, :_reduce_180, - 2, 155, :_reduce_181, - 1, 158, :_reduce_182, - 1, 159, :_reduce_183, - 2, 159, :_reduce_184, - 1, 111, :_reduce_185, - 1, 109, :_reduce_186, - 1, 110, :_reduce_187, - 1, 105, :_reduce_188, - 1, 104, :_reduce_189, - 1, 107, :_reduce_190, - 0, 113, :_reduce_none, - 1, 113, :_reduce_192, - 0, 133, :_reduce_none, - 1, 133, :_reduce_none, - 1, 141, :_reduce_none, - 1, 141, :_reduce_none, - 1, 141, :_reduce_none, - 1, 141, :_reduce_none, - 1, 141, :_reduce_none, - 1, 141, :_reduce_none, - 1, 141, :_reduce_none, - 1, 141, :_reduce_none, - 1, 141, :_reduce_none, - 1, 141, :_reduce_none, - 1, 141, :_reduce_none, - 1, 141, :_reduce_none, + 4, 89, :_reduce_99, + 1, 126, :_reduce_100, + 3, 126, :_reduce_101, + 3, 125, :_reduce_102, + 1, 84, :_reduce_103, + 6, 84, :_reduce_104, + 6, 84, :_reduce_105, + 5, 84, :_reduce_106, + 5, 84, :_reduce_107, + 5, 84, :_reduce_108, + 4, 131, :_reduce_109, + 1, 132, :_reduce_110, + 1, 128, :_reduce_111, + 3, 128, :_reduce_112, + 1, 127, :_reduce_113, + 2, 127, :_reduce_114, + 6, 95, :_reduce_115, + 2, 95, :_reduce_116, + 3, 133, :_reduce_117, + 3, 133, :_reduce_118, + 1, 134, :_reduce_none, + 1, 134, :_reduce_none, + 0, 130, :_reduce_121, + 1, 130, :_reduce_122, + 3, 130, :_reduce_123, + 1, 136, :_reduce_none, + 1, 136, :_reduce_none, + 1, 136, :_reduce_none, + 3, 135, :_reduce_127, + 3, 135, :_reduce_128, + 6, 99, :_reduce_129, + 5, 99, :_reduce_130, + 7, 100, :_reduce_131, + 6, 100, :_reduce_132, + 1, 140, :_reduce_none, + 2, 140, :_reduce_134, 1, 141, :_reduce_none, 1, 141, :_reduce_none, - 0, 80, :_reduce_209 ] - -racc_reduce_n = 210 - -racc_shift_n = 365 + 6, 101, :_reduce_137, + 5, 101, :_reduce_138, + 1, 142, :_reduce_139, + 3, 142, :_reduce_140, + 1, 144, :_reduce_141, + 1, 144, :_reduce_142, + 1, 144, :_reduce_143, + 1, 144, :_reduce_none, + 1, 143, :_reduce_none, + 2, 143, :_reduce_146, + 1, 138, :_reduce_147, + 1, 138, :_reduce_148, + 1, 139, :_reduce_149, + 2, 139, :_reduce_150, + 4, 139, :_reduce_151, + 1, 118, :_reduce_152, + 3, 118, :_reduce_153, + 3, 145, :_reduce_154, + 1, 145, :_reduce_155, + 1, 88, :_reduce_none, + 1, 88, :_reduce_none, + 1, 93, :_reduce_158, + 3, 102, :_reduce_159, + 4, 102, :_reduce_160, + 2, 102, :_reduce_161, + 3, 105, :_reduce_162, + 4, 105, :_reduce_163, + 2, 105, :_reduce_164, + 1, 146, :_reduce_165, + 3, 146, :_reduce_166, + 3, 147, :_reduce_167, + 1, 111, :_reduce_none, + 1, 111, :_reduce_none, + 1, 148, :_reduce_170, + 2, 149, :_reduce_171, + 1, 150, :_reduce_172, + 1, 152, :_reduce_173, + 1, 153, :_reduce_174, + 2, 151, :_reduce_175, + 1, 154, :_reduce_176, + 1, 155, :_reduce_177, + 2, 155, :_reduce_178, + 1, 110, :_reduce_179, + 1, 108, :_reduce_180, + 1, 109, :_reduce_181, + 1, 104, :_reduce_182, + 1, 103, :_reduce_183, + 1, 106, :_reduce_184, + 0, 112, :_reduce_none, + 1, 112, :_reduce_186, + 0, 129, :_reduce_none, + 1, 129, :_reduce_none, + 1, 137, :_reduce_none, + 1, 137, :_reduce_none, + 1, 137, :_reduce_none, + 1, 137, :_reduce_none, + 1, 137, :_reduce_none, + 1, 137, :_reduce_none, + 1, 137, :_reduce_none, + 1, 137, :_reduce_none, + 1, 137, :_reduce_none, + 1, 137, :_reduce_none, + 1, 137, :_reduce_none, + 1, 137, :_reduce_none, + 1, 137, :_reduce_none, + 1, 137, :_reduce_none, + 0, 79, :_reduce_203 ] + +racc_reduce_n = 204 + +racc_shift_n = 355 racc_token_table = { false => 0, @@ -812,16 +800,15 @@ racc_token_table = { :IN => 66, :UNLESS => 67, :PIPE => 68, - :LAMBDA => 69, - :SELBRACE => 70, - :LOW => 71, - :HIGH => 72, - :CALL => 73, - :MODULO => 74, - :TITLE_COLON => 75, - :CASE_COLON => 76 } + :SELBRACE => 69, + :LOW => 70, + :HIGH => 71, + :CALL => 72, + :MODULO => 73, + :TITLE_COLON => 74, + :CASE_COLON => 75 } -racc_nt_base = 77 +racc_nt_base = 76 racc_use_result_var = true @@ -911,7 +898,6 @@ Racc_token_to_s_table = [ "IN", "UNLESS", "PIPE", - "LAMBDA", "SELBRACE", "LOW", "HIGH", @@ -959,10 +945,7 @@ Racc_token_to_s_table = [ "lambda", "call_method_expression", "named_access", - "lambda_j8", - "lambda_ruby", "lambda_parameter_list", - "optional_farrow", "lambda_rest", "parameters", "if_part", @@ -1429,71 +1412,45 @@ module_eval(<<'.,.,', 'egrammar.ra', 197) end .,., -# reduce 75 omitted - -# reduce 76 omitted - -module_eval(<<'.,.,', 'egrammar.ra', 212) - def _reduce_77(val, _values, result) - result = Factory.LAMBDA(val[0], val[2]) +module_eval(<<'.,.,', 'egrammar.ra', 209) + def _reduce_75(val, _values, result) + result = Factory.LAMBDA(val[0], val[1]) # loc result, val[1] # TODO result end .,., -module_eval(<<'.,.,', 'egrammar.ra', 217) - def _reduce_78(val, _values, result) +module_eval(<<'.,.,', 'egrammar.ra', 214) + def _reduce_76(val, _values, result) result = val[1] result end .,., -module_eval(<<'.,.,', 'egrammar.ra', 218) - def _reduce_79(val, _values, result) +module_eval(<<'.,.,', 'egrammar.ra', 215) + def _reduce_77(val, _values, result) result = nil result end .,., -# reduce 80 omitted - -# reduce 81 omitted - -module_eval(<<'.,.,', 'egrammar.ra', 228) - def _reduce_82(val, _values, result) - result = Factory.LAMBDA(val[1], val[2]) - loc result, val[0], val[3] - - result - end -.,., - -module_eval(<<'.,.,', 'egrammar.ra', 232) - def _reduce_83(val, _values, result) - result = Factory.LAMBDA(val[1], nil) - loc result, val[0], val[2] - - result - end -.,., - -module_eval(<<'.,.,', 'egrammar.ra', 238) - def _reduce_84(val, _values, result) +module_eval(<<'.,.,', 'egrammar.ra', 219) + def _reduce_78(val, _values, result) result = [] result end .,., -module_eval(<<'.,.,', 'egrammar.ra', 239) - def _reduce_85(val, _values, result) +module_eval(<<'.,.,', 'egrammar.ra', 220) + def _reduce_79(val, _values, result) result = val[1] result end .,., -module_eval(<<'.,.,', 'egrammar.ra', 249) - def _reduce_86(val, _values, result) +module_eval(<<'.,.,', 'egrammar.ra', 230) + def _reduce_80(val, _values, result) result = val[1] loc(result, val[0], val[1]) @@ -1501,8 +1458,8 @@ module_eval(<<'.,.,', 'egrammar.ra', 249) end .,., -module_eval(<<'.,.,', 'egrammar.ra', 256) - def _reduce_87(val, _values, result) +module_eval(<<'.,.,', 'egrammar.ra', 237) + def _reduce_81(val, _values, result) result = Factory.IF(val[0], Factory.block_or_expression(*val[2]), val[4]) loc(result, val[0], (val[4] ? val[4] : val[3])) @@ -1510,8 +1467,8 @@ module_eval(<<'.,.,', 'egrammar.ra', 256) end .,., -module_eval(<<'.,.,', 'egrammar.ra', 260) - def _reduce_88(val, _values, result) +module_eval(<<'.,.,', 'egrammar.ra', 241) + def _reduce_82(val, _values, result) result = Factory.IF(val[0], nil, val[3]) loc(result, val[0], (val[3] ? val[3] : val[2])) @@ -1519,10 +1476,10 @@ module_eval(<<'.,.,', 'egrammar.ra', 260) end .,., -# reduce 89 omitted +# reduce 83 omitted -module_eval(<<'.,.,', 'egrammar.ra', 268) - def _reduce_90(val, _values, result) +module_eval(<<'.,.,', 'egrammar.ra', 249) + def _reduce_84(val, _values, result) result = val[1] loc(result, val[0], val[1]) @@ -1530,8 +1487,8 @@ module_eval(<<'.,.,', 'egrammar.ra', 268) end .,., -module_eval(<<'.,.,', 'egrammar.ra', 272) - def _reduce_91(val, _values, result) +module_eval(<<'.,.,', 'egrammar.ra', 253) + def _reduce_85(val, _values, result) result = Factory.block_or_expression(*val[2]) loc result, val[0], val[3] @@ -1539,16 +1496,16 @@ module_eval(<<'.,.,', 'egrammar.ra', 272) end .,., -module_eval(<<'.,.,', 'egrammar.ra', 276) - def _reduce_92(val, _values, result) +module_eval(<<'.,.,', 'egrammar.ra', 257) + def _reduce_86(val, _values, result) result = nil # don't think a nop is needed here either result end .,., -module_eval(<<'.,.,', 'egrammar.ra', 285) - def _reduce_93(val, _values, result) +module_eval(<<'.,.,', 'egrammar.ra', 266) + def _reduce_87(val, _values, result) result = Factory.UNLESS(val[1], Factory.block_or_expression(*val[3]), val[5]) loc result, val[0], val[4] @@ -1556,8 +1513,8 @@ module_eval(<<'.,.,', 'egrammar.ra', 285) end .,., -module_eval(<<'.,.,', 'egrammar.ra', 289) - def _reduce_94(val, _values, result) +module_eval(<<'.,.,', 'egrammar.ra', 270) + def _reduce_88(val, _values, result) result = Factory.UNLESS(val[1], nil, nil) loc result, val[0], val[4] @@ -1565,10 +1522,10 @@ module_eval(<<'.,.,', 'egrammar.ra', 289) end .,., -# reduce 95 omitted +# reduce 89 omitted -module_eval(<<'.,.,', 'egrammar.ra', 299) - def _reduce_96(val, _values, result) +module_eval(<<'.,.,', 'egrammar.ra', 280) + def _reduce_90(val, _values, result) result = Factory.block_or_expression(*val[2]) loc result, val[0], val[3] @@ -1576,16 +1533,16 @@ module_eval(<<'.,.,', 'egrammar.ra', 299) end .,., -module_eval(<<'.,.,', 'egrammar.ra', 303) - def _reduce_97(val, _values, result) +module_eval(<<'.,.,', 'egrammar.ra', 284) + def _reduce_91(val, _values, result) result = nil # don't think a nop is needed here either result end .,., -module_eval(<<'.,.,', 'egrammar.ra', 311) - def _reduce_98(val, _values, result) +module_eval(<<'.,.,', 'egrammar.ra', 292) + def _reduce_92(val, _values, result) result = Factory.CASE(val[1], *val[3]) loc result, val[0], val[4] @@ -1593,22 +1550,22 @@ module_eval(<<'.,.,', 'egrammar.ra', 311) end .,., -module_eval(<<'.,.,', 'egrammar.ra', 317) - def _reduce_99(val, _values, result) +module_eval(<<'.,.,', 'egrammar.ra', 298) + def _reduce_93(val, _values, result) result = [val[0]] result end .,., -module_eval(<<'.,.,', 'egrammar.ra', 318) - def _reduce_100(val, _values, result) +module_eval(<<'.,.,', 'egrammar.ra', 299) + def _reduce_94(val, _values, result) result = val[0].push val[1] result end .,., -module_eval(<<'.,.,', 'egrammar.ra', 323) - def _reduce_101(val, _values, result) +module_eval(<<'.,.,', 'egrammar.ra', 304) + def _reduce_95(val, _values, result) result = Factory.WHEN(val[0], val[3]) loc result, val[1], val[4] @@ -1616,8 +1573,8 @@ module_eval(<<'.,.,', 'egrammar.ra', 323) end .,., -module_eval(<<'.,.,', 'egrammar.ra', 327) - def _reduce_102(val, _values, result) +module_eval(<<'.,.,', 'egrammar.ra', 308) + def _reduce_96(val, _values, result) result = Factory.WHEN(val[0], nil) loc result, val[1], val[3] @@ -1625,54 +1582,54 @@ module_eval(<<'.,.,', 'egrammar.ra', 327) end .,., -module_eval(<<'.,.,', 'egrammar.ra', 331) - def _reduce_103(val, _values, result) +module_eval(<<'.,.,', 'egrammar.ra', 312) + def _reduce_97(val, _values, result) result = val[0] result end .,., -# reduce 104 omitted +# reduce 98 omitted -module_eval(<<'.,.,', 'egrammar.ra', 342) - def _reduce_105(val, _values, result) +module_eval(<<'.,.,', 'egrammar.ra', 323) + def _reduce_99(val, _values, result) result = val[1] result end .,., -module_eval(<<'.,.,', 'egrammar.ra', 347) - def _reduce_106(val, _values, result) +module_eval(<<'.,.,', 'egrammar.ra', 328) + def _reduce_100(val, _values, result) result = [val[0]] result end .,., -module_eval(<<'.,.,', 'egrammar.ra', 348) - def _reduce_107(val, _values, result) +module_eval(<<'.,.,', 'egrammar.ra', 329) + def _reduce_101(val, _values, result) result = val[0].push val[2] result end .,., -module_eval(<<'.,.,', 'egrammar.ra', 353) - def _reduce_108(val, _values, result) +module_eval(<<'.,.,', 'egrammar.ra', 334) + def _reduce_102(val, _values, result) result = Factory.MAP(val[0], val[2]) ; loc result, val[1] result end .,., -module_eval(<<'.,.,', 'egrammar.ra', 369) - def _reduce_109(val, _values, result) +module_eval(<<'.,.,', 'egrammar.ra', 350) + def _reduce_103(val, _values, result) result = val[0] result end .,., -module_eval(<<'.,.,', 'egrammar.ra', 372) - def _reduce_110(val, _values, result) +module_eval(<<'.,.,', 'egrammar.ra', 353) + def _reduce_104(val, _values, result) result = case Factory.resource_shape(val[1]) when :resource, :class tmp = Factory.RESOURCE(Factory.fqn(token_text(val[1])), val[3]) @@ -1691,8 +1648,8 @@ module_eval(<<'.,.,', 'egrammar.ra', 372) end .,., -module_eval(<<'.,.,', 'egrammar.ra', 387) - def _reduce_111(val, _values, result) +module_eval(<<'.,.,', 'egrammar.ra', 368) + def _reduce_105(val, _values, result) result = case Factory.resource_shape(val[1]) when :resource, :class error "Defaults are not virtualizable" @@ -1708,8 +1665,8 @@ module_eval(<<'.,.,', 'egrammar.ra', 387) end .,., -module_eval(<<'.,.,', 'egrammar.ra', 399) - def _reduce_112(val, _values, result) +module_eval(<<'.,.,', 'egrammar.ra', 380) + def _reduce_106(val, _values, result) result = case Factory.resource_shape(val[0]) when :resource, :class Factory.RESOURCE(Factory.fqn(token_text(val[0])), val[2]) @@ -1726,8 +1683,8 @@ module_eval(<<'.,.,', 'egrammar.ra', 399) end .,., -module_eval(<<'.,.,', 'egrammar.ra', 412) - def _reduce_113(val, _values, result) +module_eval(<<'.,.,', 'egrammar.ra', 393) + def _reduce_107(val, _values, result) result = case Factory.resource_shape(val[0]) when :resource, :class # This catches deprecated syntax. @@ -1746,8 +1703,8 @@ module_eval(<<'.,.,', 'egrammar.ra', 412) end .,., -module_eval(<<'.,.,', 'egrammar.ra', 427) - def _reduce_114(val, _values, result) +module_eval(<<'.,.,', 'egrammar.ra', 408) + def _reduce_108(val, _values, result) result = Factory.RESOURCE(Factory.fqn(token_text(val[0])), val[2]) loc result, val[0], val[4] @@ -1755,50 +1712,50 @@ module_eval(<<'.,.,', 'egrammar.ra', 427) end .,., -module_eval(<<'.,.,', 'egrammar.ra', 432) - def _reduce_115(val, _values, result) +module_eval(<<'.,.,', 'egrammar.ra', 413) + def _reduce_109(val, _values, result) result = Factory.RESOURCE_BODY(val[0], val[2]) result end .,., -module_eval(<<'.,.,', 'egrammar.ra', 434) - def _reduce_116(val, _values, result) +module_eval(<<'.,.,', 'egrammar.ra', 415) + def _reduce_110(val, _values, result) result = val[0] result end .,., -module_eval(<<'.,.,', 'egrammar.ra', 437) - def _reduce_117(val, _values, result) +module_eval(<<'.,.,', 'egrammar.ra', 418) + def _reduce_111(val, _values, result) result = [val[0]] result end .,., -module_eval(<<'.,.,', 'egrammar.ra', 438) - def _reduce_118(val, _values, result) +module_eval(<<'.,.,', 'egrammar.ra', 419) + def _reduce_112(val, _values, result) result = val[0].push val[2] result end .,., -module_eval(<<'.,.,', 'egrammar.ra', 443) - def _reduce_119(val, _values, result) +module_eval(<<'.,.,', 'egrammar.ra', 424) + def _reduce_113(val, _values, result) result = :virtual result end .,., -module_eval(<<'.,.,', 'egrammar.ra', 444) - def _reduce_120(val, _values, result) +module_eval(<<'.,.,', 'egrammar.ra', 425) + def _reduce_114(val, _values, result) result = :exported result end .,., -module_eval(<<'.,.,', 'egrammar.ra', 456) - def _reduce_121(val, _values, result) +module_eval(<<'.,.,', 'egrammar.ra', 437) + def _reduce_115(val, _values, result) result = Factory.COLLECT(val[0], val[1], val[3]) loc result, val[0], val[5] @@ -1806,8 +1763,8 @@ module_eval(<<'.,.,', 'egrammar.ra', 456) end .,., -module_eval(<<'.,.,', 'egrammar.ra', 460) - def _reduce_122(val, _values, result) +module_eval(<<'.,.,', 'egrammar.ra', 441) + def _reduce_116(val, _values, result) result = Factory.COLLECT(val[0], val[1], []) loc result, val[0], val[1] @@ -1815,53 +1772,53 @@ module_eval(<<'.,.,', 'egrammar.ra', 460) end .,., -module_eval(<<'.,.,', 'egrammar.ra', 465) - def _reduce_123(val, _values, result) +module_eval(<<'.,.,', 'egrammar.ra', 446) + def _reduce_117(val, _values, result) result = Factory.VIRTUAL_QUERY(val[1]) ; loc result, val[0], val[2] result end .,., -module_eval(<<'.,.,', 'egrammar.ra', 466) - def _reduce_124(val, _values, result) +module_eval(<<'.,.,', 'egrammar.ra', 447) + def _reduce_118(val, _values, result) result = Factory.EXPORTED_QUERY(val[1]) ; loc result, val[0], val[2] result end .,., -# reduce 125 omitted +# reduce 119 omitted -# reduce 126 omitted +# reduce 120 omitted -module_eval(<<'.,.,', 'egrammar.ra', 479) - def _reduce_127(val, _values, result) +module_eval(<<'.,.,', 'egrammar.ra', 460) + def _reduce_121(val, _values, result) result = [] result end .,., -module_eval(<<'.,.,', 'egrammar.ra', 480) - def _reduce_128(val, _values, result) +module_eval(<<'.,.,', 'egrammar.ra', 461) + def _reduce_122(val, _values, result) result = [val[0]] result end .,., -module_eval(<<'.,.,', 'egrammar.ra', 481) - def _reduce_129(val, _values, result) +module_eval(<<'.,.,', 'egrammar.ra', 462) + def _reduce_123(val, _values, result) result = val[0].push(val[2]) result end .,., -# reduce 130 omitted +# reduce 124 omitted -# reduce 131 omitted +# reduce 125 omitted -# reduce 132 omitted +# reduce 126 omitted -module_eval(<<'.,.,', 'egrammar.ra', 497) - def _reduce_133(val, _values, result) +module_eval(<<'.,.,', 'egrammar.ra', 478) + def _reduce_127(val, _values, result) result = Factory.ATTRIBUTE_OP(val[0][:value], :'=>', val[2]) loc result, val[0], val[2] @@ -1869,8 +1826,8 @@ module_eval(<<'.,.,', 'egrammar.ra', 497) end .,., -module_eval(<<'.,.,', 'egrammar.ra', 501) - def _reduce_134(val, _values, result) +module_eval(<<'.,.,', 'egrammar.ra', 482) + def _reduce_128(val, _values, result) result = Factory.ATTRIBUTE_OP(val[0][:value], :'+>', val[2]) loc result, val[0], val[2] @@ -1878,8 +1835,8 @@ module_eval(<<'.,.,', 'egrammar.ra', 501) end .,., -module_eval(<<'.,.,', 'egrammar.ra', 511) - def _reduce_135(val, _values, result) +module_eval(<<'.,.,', 'egrammar.ra', 492) + def _reduce_129(val, _values, result) result = Factory.DEFINITION(classname(val[1][:value]), val[2], val[4]) loc result, val[0], val[5] @lexer.indefine = false @@ -1888,8 +1845,8 @@ module_eval(<<'.,.,', 'egrammar.ra', 511) end .,., -module_eval(<<'.,.,', 'egrammar.ra', 516) - def _reduce_136(val, _values, result) +module_eval(<<'.,.,', 'egrammar.ra', 497) + def _reduce_130(val, _values, result) result = Factory.DEFINITION(classname(val[1][:value]), val[2], nil) loc result, val[0], val[4] @lexer.indefine = false @@ -1898,8 +1855,8 @@ module_eval(<<'.,.,', 'egrammar.ra', 516) end .,., -module_eval(<<'.,.,', 'egrammar.ra', 531) - def _reduce_137(val, _values, result) +module_eval(<<'.,.,', 'egrammar.ra', 512) + def _reduce_131(val, _values, result) @lexer.namepop result = Factory.HOSTCLASS(classname(val[1][:value]), val[2], token_text(val[3]), val[5]) loc result, val[0], val[6] @@ -1908,8 +1865,8 @@ module_eval(<<'.,.,', 'egrammar.ra', 531) end .,., -module_eval(<<'.,.,', 'egrammar.ra', 536) - def _reduce_138(val, _values, result) +module_eval(<<'.,.,', 'egrammar.ra', 517) + def _reduce_132(val, _values, result) @lexer.namepop result = Factory.HOSTCLASS(classname(val[1][:value]), val[2], token_text(val[3]), nil) loc result, val[0], val[5] @@ -1918,21 +1875,21 @@ module_eval(<<'.,.,', 'egrammar.ra', 536) end .,., -# reduce 139 omitted +# reduce 133 omitted -module_eval(<<'.,.,', 'egrammar.ra', 544) - def _reduce_140(val, _values, result) +module_eval(<<'.,.,', 'egrammar.ra', 525) + def _reduce_134(val, _values, result) result = val[1] result end .,., -# reduce 141 omitted +# reduce 135 omitted -# reduce 142 omitted +# reduce 136 omitted -module_eval(<<'.,.,', 'egrammar.ra', 561) - def _reduce_143(val, _values, result) +module_eval(<<'.,.,', 'egrammar.ra', 542) + def _reduce_137(val, _values, result) result = Factory.NODE(val[1], val[2], val[4]) loc result, val[0], val[5] @@ -1940,8 +1897,8 @@ module_eval(<<'.,.,', 'egrammar.ra', 561) end .,., -module_eval(<<'.,.,', 'egrammar.ra', 565) - def _reduce_144(val, _values, result) +module_eval(<<'.,.,', 'egrammar.ra', 546) + def _reduce_138(val, _values, result) result = Factory.NODE(val[1], val[2], nil) loc result, val[0], val[4] @@ -1949,307 +1906,319 @@ module_eval(<<'.,.,', 'egrammar.ra', 565) end .,., -module_eval(<<'.,.,', 'egrammar.ra', 575) - def _reduce_145(val, _values, result) +module_eval(<<'.,.,', 'egrammar.ra', 556) + def _reduce_139(val, _values, result) result = [result] result end .,., -module_eval(<<'.,.,', 'egrammar.ra', 576) - def _reduce_146(val, _values, result) +module_eval(<<'.,.,', 'egrammar.ra', 557) + def _reduce_140(val, _values, result) result = val[0].push(val[2]) result end .,., -module_eval(<<'.,.,', 'egrammar.ra', 581) - def _reduce_147(val, _values, result) +module_eval(<<'.,.,', 'egrammar.ra', 562) + def _reduce_141(val, _values, result) result = Factory.fqn(val[0][:value]); loc result, val[0] result end .,., -module_eval(<<'.,.,', 'egrammar.ra', 582) - def _reduce_148(val, _values, result) +module_eval(<<'.,.,', 'egrammar.ra', 563) + def _reduce_142(val, _values, result) result = val[0] result end .,., -module_eval(<<'.,.,', 'egrammar.ra', 583) - def _reduce_149(val, _values, result) +module_eval(<<'.,.,', 'egrammar.ra', 564) + def _reduce_143(val, _values, result) result = Factory.literal(:default); loc result, val[0] result end .,., -# reduce 150 omitted +# reduce 144 omitted -# reduce 151 omitted +# reduce 145 omitted -module_eval(<<'.,.,', 'egrammar.ra', 589) - def _reduce_152(val, _values, result) +module_eval(<<'.,.,', 'egrammar.ra', 570) + def _reduce_146(val, _values, result) result = val[1] result end .,., -module_eval(<<'.,.,', 'egrammar.ra', 594) - def _reduce_153(val, _values, result) +module_eval(<<'.,.,', 'egrammar.ra', 575) + def _reduce_147(val, _values, result) result = val[0] result end .,., -module_eval(<<'.,.,', 'egrammar.ra', 595) - def _reduce_154(val, _values, result) +module_eval(<<'.,.,', 'egrammar.ra', 576) + def _reduce_148(val, _values, result) result = val[0] result end .,., -module_eval(<<'.,.,', 'egrammar.ra', 599) - def _reduce_155(val, _values, result) +module_eval(<<'.,.,', 'egrammar.ra', 580) + def _reduce_149(val, _values, result) result = [] result end .,., -module_eval(<<'.,.,', 'egrammar.ra', 600) - def _reduce_156(val, _values, result) +module_eval(<<'.,.,', 'egrammar.ra', 581) + def _reduce_150(val, _values, result) result = [] result end .,., -module_eval(<<'.,.,', 'egrammar.ra', 601) - def _reduce_157(val, _values, result) +module_eval(<<'.,.,', 'egrammar.ra', 582) + def _reduce_151(val, _values, result) result = val[1] result end .,., -module_eval(<<'.,.,', 'egrammar.ra', 605) - def _reduce_158(val, _values, result) +module_eval(<<'.,.,', 'egrammar.ra', 586) + def _reduce_152(val, _values, result) result = [val[0]] result end .,., -module_eval(<<'.,.,', 'egrammar.ra', 606) - def _reduce_159(val, _values, result) +module_eval(<<'.,.,', 'egrammar.ra', 587) + def _reduce_153(val, _values, result) result = val[0].push(val[2]) result end .,., -module_eval(<<'.,.,', 'egrammar.ra', 610) - def _reduce_160(val, _values, result) +module_eval(<<'.,.,', 'egrammar.ra', 591) + def _reduce_154(val, _values, result) result = Factory.PARAM(val[0][:value], val[2]) ; loc result, val[0] result end .,., -module_eval(<<'.,.,', 'egrammar.ra', 611) - def _reduce_161(val, _values, result) +module_eval(<<'.,.,', 'egrammar.ra', 592) + def _reduce_155(val, _values, result) result = Factory.PARAM(val[0][:value]); loc result, val[0] result end .,., -# reduce 162 omitted +# reduce 156 omitted -# reduce 163 omitted +# reduce 157 omitted -module_eval(<<'.,.,', 'egrammar.ra', 624) - def _reduce_164(val, _values, result) +module_eval(<<'.,.,', 'egrammar.ra', 605) + def _reduce_158(val, _values, result) result = Factory.fqn(val[0][:value]).var ; loc result, val[0] result end .,., -module_eval(<<'.,.,', 'egrammar.ra', 630) - def _reduce_165(val, _values, result) +module_eval(<<'.,.,', 'egrammar.ra', 611) + def _reduce_159(val, _values, result) result = Factory.LIST(val[1]); loc result, val[0], val[2] result end .,., -module_eval(<<'.,.,', 'egrammar.ra', 631) - def _reduce_166(val, _values, result) +module_eval(<<'.,.,', 'egrammar.ra', 612) + def _reduce_160(val, _values, result) result = Factory.LIST(val[1]); loc result, val[0], val[3] result end .,., -module_eval(<<'.,.,', 'egrammar.ra', 632) - def _reduce_167(val, _values, result) +module_eval(<<'.,.,', 'egrammar.ra', 613) + def _reduce_161(val, _values, result) result = Factory.literal([]) ; loc result, val[0] result end .,., -module_eval(<<'.,.,', 'egrammar.ra', 635) - def _reduce_168(val, _values, result) +module_eval(<<'.,.,', 'egrammar.ra', 616) + def _reduce_162(val, _values, result) result = Factory.HASH(val[1]); loc result, val[0], val[2] result end .,., -module_eval(<<'.,.,', 'egrammar.ra', 636) - def _reduce_169(val, _values, result) +module_eval(<<'.,.,', 'egrammar.ra', 617) + def _reduce_163(val, _values, result) result = Factory.HASH(val[1]); loc result, val[0], val[3] result end .,., -module_eval(<<'.,.,', 'egrammar.ra', 637) - def _reduce_170(val, _values, result) +module_eval(<<'.,.,', 'egrammar.ra', 618) + def _reduce_164(val, _values, result) result = Factory.literal({}) ; loc result, val[0], val[3] result end .,., -module_eval(<<'.,.,', 'egrammar.ra', 640) - def _reduce_171(val, _values, result) +module_eval(<<'.,.,', 'egrammar.ra', 621) + def _reduce_165(val, _values, result) result = [val[0]] result end .,., -module_eval(<<'.,.,', 'egrammar.ra', 641) - def _reduce_172(val, _values, result) +module_eval(<<'.,.,', 'egrammar.ra', 622) + def _reduce_166(val, _values, result) result = val[0].push val[2] result end .,., -module_eval(<<'.,.,', 'egrammar.ra', 644) - def _reduce_173(val, _values, result) +module_eval(<<'.,.,', 'egrammar.ra', 625) + def _reduce_167(val, _values, result) result = Factory.KEY_ENTRY(val[0], val[2]); loc result, val[1] result end .,., -# reduce 174 omitted +# reduce 168 omitted -# reduce 175 omitted +# reduce 169 omitted -module_eval(<<'.,.,', 'egrammar.ra', 650) - def _reduce_176(val, _values, result) +module_eval(<<'.,.,', 'egrammar.ra', 631) + def _reduce_170(val, _values, result) result = Factory.literal(val[0][:value]) ; loc result, val[0] result end .,., -module_eval(<<'.,.,', 'egrammar.ra', 651) - def _reduce_177(val, _values, result) +module_eval(<<'.,.,', 'egrammar.ra', 632) + def _reduce_171(val, _values, result) result = Factory.string(val[0], *val[1]) ; loc result, val[0], val[1][-1] result end .,., -module_eval(<<'.,.,', 'egrammar.ra', 652) - def _reduce_178(val, _values, result) +module_eval(<<'.,.,', 'egrammar.ra', 633) + def _reduce_172(val, _values, result) result = Factory.literal(val[0][:value]); loc result, val[0] result end .,., -module_eval(<<'.,.,', 'egrammar.ra', 653) - def _reduce_179(val, _values, result) +module_eval(<<'.,.,', 'egrammar.ra', 634) + def _reduce_173(val, _values, result) result = Factory.literal(val[0][:value]); loc result, val[0] result end .,., -module_eval(<<'.,.,', 'egrammar.ra', 654) - def _reduce_180(val, _values, result) +module_eval(<<'.,.,', 'egrammar.ra', 635) + def _reduce_174(val, _values, result) result = Factory.literal(val[0][:value]); loc result, val[0] result end .,., -module_eval(<<'.,.,', 'egrammar.ra', 655) - def _reduce_181(val, _values, result) +module_eval(<<'.,.,', 'egrammar.ra', 636) + def _reduce_175(val, _values, result) result = [val[0]] + val[1] result end .,., -module_eval(<<'.,.,', 'egrammar.ra', 656) - def _reduce_182(val, _values, result) +module_eval(<<'.,.,', 'egrammar.ra', 637) + def _reduce_176(val, _values, result) result = Factory.TEXT(val[0]) result end .,., -module_eval(<<'.,.,', 'egrammar.ra', 659) - def _reduce_183(val, _values, result) +module_eval(<<'.,.,', 'egrammar.ra', 640) + def _reduce_177(val, _values, result) result = [val[0]] result end .,., -module_eval(<<'.,.,', 'egrammar.ra', 660) - def _reduce_184(val, _values, result) +module_eval(<<'.,.,', 'egrammar.ra', 641) + def _reduce_178(val, _values, result) result = [val[0]] + val[1] result end .,., -module_eval(<<'.,.,', 'egrammar.ra', 662) - def _reduce_185(val, _values, result) +module_eval(<<'.,.,', 'egrammar.ra', 643) + def _reduce_179(val, _values, result) result = Factory.QNAME_OR_NUMBER(val[0][:value]) ; loc result, val[0] result end .,., -module_eval(<<'.,.,', 'egrammar.ra', 663) - def _reduce_186(val, _values, result) +module_eval(<<'.,.,', 'egrammar.ra', 644) + def _reduce_180(val, _values, result) result = Factory.QREF(val[0][:value]) ; loc result, val[0] result end .,., -module_eval(<<'.,.,', 'egrammar.ra', 664) - def _reduce_187(val, _values, result) +module_eval(<<'.,.,', 'egrammar.ra', 645) + def _reduce_181(val, _values, result) result = Factory.literal(:undef); loc result, val[0] result end .,., -module_eval(<<'.,.,', 'egrammar.ra', 665) - def _reduce_188(val, _values, result) +module_eval(<<'.,.,', 'egrammar.ra', 646) + def _reduce_182(val, _values, result) result = Factory.literal(:default); loc result, val[0] result end .,., -module_eval(<<'.,.,', 'egrammar.ra', 670) - def _reduce_189(val, _values, result) +module_eval(<<'.,.,', 'egrammar.ra', 651) + def _reduce_183(val, _values, result) result = Factory.literal(val[0][:value]) ; loc result, val[0] result end .,., -module_eval(<<'.,.,', 'egrammar.ra', 673) - def _reduce_190(val, _values, result) +module_eval(<<'.,.,', 'egrammar.ra', 654) + def _reduce_184(val, _values, result) result = Factory.literal(val[0][:value]); loc result, val[0] result end .,., -# reduce 191 omitted +# reduce 185 omitted -module_eval(<<'.,.,', 'egrammar.ra', 679) - def _reduce_192(val, _values, result) +module_eval(<<'.,.,', 'egrammar.ra', 660) + def _reduce_186(val, _values, result) result = nil result end .,., +# reduce 187 omitted + +# reduce 188 omitted + +# reduce 189 omitted + +# reduce 190 omitted + +# reduce 191 omitted + +# reduce 192 omitted + # reduce 193 omitted # reduce 194 omitted @@ -2270,20 +2239,8 @@ module_eval(<<'.,.,', 'egrammar.ra', 679) # reduce 202 omitted -# reduce 203 omitted - -# reduce 204 omitted - -# reduce 205 omitted - -# reduce 206 omitted - -# reduce 207 omitted - -# reduce 208 omitted - -module_eval(<<'.,.,', 'egrammar.ra', 702) - def _reduce_209(val, _values, result) +module_eval(<<'.,.,', 'egrammar.ra', 683) + def _reduce_203(val, _values, result) result = nil result end diff --git a/lib/puppet/pops/parser/lexer.rb b/lib/puppet/pops/parser/lexer.rb index 1e090f7bc..994c4643f 100644 --- a/lib/puppet/pops/parser/lexer.rb +++ b/lib/puppet/pops/parser/lexer.rb @@ -148,8 +148,8 @@ class Puppet::Pops::Parser::Lexer TOKENS.add_tokens( '[' => :LBRACK, ']' => :RBRACK, - # '{' => :LBRACE, # Specialized to handle lambda - '}' => :RBRACE, + # '{' => :LBRACE, # Specialized to handle lambda and brace count + # '}' => :RBRACE, # Specialized to handle brace count '(' => :LPAREN, ')' => :RPAREN, '=' => :EQUALS, @@ -194,7 +194,6 @@ class Puppet::Pops::Parser::Lexer "<dqstring between two interpolations>" => :DQMID, "<dqstring after final interpolation>" => :DQPOST, "<boolean>" => :BOOLEAN, - "<lambda start>" => :LAMBDA, # A LBRACE followed by '|' "<select start>" => :SELBRACE # A QMARK followed by '{' ) @@ -214,10 +213,6 @@ class Puppet::Pops::Parser::Lexer REGEX_INTRODUCING_TOKENS.include? context[:after] end - IN_STRING_INTERPOLATION = Proc.new do |context| - context[:string_interpolation_depth] > 0 - end - DASHED_VARIABLES_ALLOWED = Proc.new do |context| Puppet[:allow_variables_with_dashes] end @@ -227,20 +222,6 @@ class Puppet::Pops::Parser::Lexer end end - # LBRACE needs look ahead to differentiate between '{' and a '{' - # followed by a '|' (start of lambda) The racc grammar can only do one - # token lookahead. - # - TOKENS.add_token :LBRACE, /\{/ do | lexer, value | - if lexer.match?(/[ \t\r]*\|/) - [TOKENS[:LAMBDA], value] - elsif lexer.lexing_context[:after] == :QMARK - [TOKENS[:SELBRACE], value] - else - [TOKENS[:LBRACE], value] - end - end - # Numbers are treated separately from names, so that they may contain dots. TOKENS.add_token :NUMBER, %r{\b(?:0[xX][0-9A-Fa-f]+|0?\d+(?:\.\d+)?(?:[eE]-?\d+)?)\b} do |lexer, value| lexer.assert_numeric(value) @@ -305,10 +286,31 @@ class Puppet::Pops::Parser::Lexer lexer.tokenize_interpolated_string(DQ_initial_token_types) end - TOKENS.add_token :DQCONT, /\}/ do |lexer, value| - lexer.tokenize_interpolated_string(DQ_continuation_token_types) + + # LBRACE needs look ahead to differentiate between '{' and a '{' + # followed by a '|' (start of lambda) The racc grammar can only do one + # token lookahead. + # + TOKENS.add_token :LBRACE, "{" do |lexer, value| + lexer.lexing_context[:brace_count] += 1 + if lexer.lexing_context[:after] == :QMARK + [TOKENS[:SELBRACE], value] + else + [TOKENS[:LBRACE], value] + end + end + + # RBRACE needs to differentiate between a regular brace that is part of + # syntax and one that is the ending of a string interpolation. + TOKENS.add_token :RBRACE, "}" do |lexer, value| + context = lexer.lexing_context + if context[:interpolation_stack].empty? || context[:brace_count] != context[:interpolation_stack][-1] + context[:brace_count] -= 1 + [TOKENS[:RBRACE], value] + else + lexer.tokenize_interpolated_string(DQ_continuation_token_types) + end end - TOKENS[:DQCONT].acceptable_when Contextual::IN_STRING_INTERPOLATION TOKENS.add_token :DOLLAR_VAR_WITH_DASH, %r{\$(?:::)?(?:[-\w]+::)*[-\w]+} do |lexer, value| lexer.warn_if_variable_has_hyphen(value) @@ -339,9 +341,21 @@ class Puppet::Pops::Parser::Lexer # reference. # if lexer.match?(%r{[ \t\r]*\(}) - [TOKENS[:NAME],value] + # followed by ( is a function call + [TOKENS[:NAME], value] + + elsif kwd_token = KEYWORDS.lookup(value) + # true, false, if, unless, case, and undef are keywords that cannot be used as variables + # but node, and several others are variables + if [ :TRUE, :FALSE ].include?(kwd_token.name) + [ TOKENS[:BOOLEAN], eval(value) ] + elsif [ :IF, :UNLESS, :CASE, :UNDEF ].include?(kwd_token.name) + [kwd_token, value] + else + [TOKENS[:VARIABLE], value] + end else - [TOKENS[:VARIABLE],value] + [TOKENS[:VARIABLE], value] end end @@ -404,7 +418,7 @@ class Puppet::Pops::Parser::Lexer def file=(file) @file = file - contents = File.exists?(file) ? File.read(file) : "" + contents = Puppet::FileSystem::File.exist?(file) ? File.read(file) : "" @scanner = StringScanner.new(contents) @locator = Locator.new(contents, multibyte?) end @@ -501,7 +515,8 @@ class Puppet::Pops::Parser::Lexer :start_of_line => true, :offset => 0, # byte offset before where token starts :end_offset => 0, # byte offset after scanned token - :string_interpolation_depth => 0 + :brace_count => 0, # nested depth of braces + :interpolation_stack => [] # matching interpolation brace level } end @@ -592,8 +607,11 @@ class Puppet::Pops::Parser::Lexer end lexing_context[:after] = final_token.name unless newline - lexing_context[:string_interpolation_depth] += 1 if final_token.name == :DQPRE - lexing_context[:string_interpolation_depth] -= 1 if final_token.name == :DQPOST + if final_token.name == :DQPRE + lexing_context[:interpolation_stack] << lexing_context[:brace_count] + elsif final_token.name == :DQPOST + lexing_context[:interpolation_stack].pop + end value = token_value[:value] diff --git a/lib/puppet/pops/parser/parser_support.rb b/lib/puppet/pops/parser/parser_support.rb index db35aad01..a22de7161 100644 --- a/lib/puppet/pops/parser/parser_support.rb +++ b/lib/puppet/pops/parser/parser_support.rb @@ -63,7 +63,7 @@ class Puppet::Pops::Parser::Parser # Parses a file expected to contain pp DSL logic. def parse_file(file) - unless FileTest.exist?(file) + unless Puppet::FileSystem::File.exist?(file) unless file =~ /\.pp$/ file = file + ".pp" end diff --git a/lib/puppet/pops/patterns.rb b/lib/puppet/pops/patterns.rb index d18384fcb..b1c76ad43 100644 --- a/lib/puppet/pops/patterns.rb +++ b/lib/puppet/pops/patterns.rb @@ -18,18 +18,18 @@ module Puppet::Pops::Patterns # NAME matches a name the same way as the lexer. # This name includes hyphen, which may be illegal in variables, and names in general. - NAME = %r{((::)?[a-z0-9][-\w]*)(::[a-z0-9][-\w]*)*} + NAME = %r{\A((::)?[a-z0-9]\w*)(::[a-z0-9]\w*)*\z} # CLASSREF_EXT matches a class reference the same way as the lexer - i.e. the external source form # where each part must start with a capital letter A-Z. # This name includes hyphen, which may be illegal in some cases. # - CLASSREF_EXT = %r{((::){0,1}[A-Z][-\w]*)+} + CLASSREF_EXT = %r{\A((::){0,1}[A-Z][-\w]*)+\z} - # CLASSREF matches a class reference the way it is represented internall in the + # CLASSREF matches a class reference the way it is represented internally in the # model (i.e. in lower case). # This name includes hyphen, which may be illegal in some cases. # - CLASSREF = %r{((::){0,1}[a-z][-\w]*)+} + CLASSREF = %r{\A((::){0,1}[a-z][-\w]*)+\z} end diff --git a/lib/puppet/pops/utils.rb b/lib/puppet/pops/utils.rb index 104a26951..01a540432 100644 --- a/lib/puppet/pops/utils.rb +++ b/lib/puppet/pops/utils.rb @@ -35,7 +35,7 @@ module Puppet::Pops::Utils radix = 10 if match[1].to_s.length > 0 radix = 16 - elsif match[2].to_s.length > 0 && match[2][0] == '0' + elsif match[2].to_s.length > 1 && match[2][0] == '0' radix = 8 end [Integer(match[0], radix), radix] diff --git a/lib/puppet/pops/validation/checker3_1.rb b/lib/puppet/pops/validation/checker3_1.rb index 65cbbbeaf..839df5044 100644 --- a/lib/puppet/pops/validation/checker3_1.rb +++ b/lib/puppet/pops/validation/checker3_1.rb @@ -20,7 +20,7 @@ class Puppet::Pops::Validation::Checker3_1 def initialize(diagnostics_producer) @@check_visitor ||= Puppet::Pops::Visitor.new(nil, "check", 0, 0) @@rvalue_visitor ||= Puppet::Pops::Visitor.new(nil, "rvalue", 0, 0) - @@hostname_visitor ||= Puppet::Pops::Visitor.new(nil, "hostname", 1, 1) + @@hostname_visitor ||= Puppet::Pops::Visitor.new(nil, "hostname", 1, 2) @@assignment_visitor ||= Puppet::Pops::Visitor.new(nil, "assign", 0, 1) @@query_visitor ||= Puppet::Pops::Visitor.new(nil, "query", 0, 0) @@top_visitor ||= Puppet::Pops::Visitor.new(nil, "top", 1, 1) @@ -45,8 +45,9 @@ class Puppet::Pops::Validation::Checker3_1 end # Performs check if this is a vaid hostname expression - def hostname(o, semantic) - @@hostname_visitor.visit_this(self, o, semantic) + # @param single_feature_name [String, nil] the name of a single valued hostname feature of the value's container. e.g. 'parent' + def hostname(o, semantic, single_feature_name = nil) + @@hostname_visitor.visit_this(self, o, semantic, single_feature_name) end # Performs check if this is valid as a query @@ -125,7 +126,7 @@ class Puppet::Pops::Validation::Checker3_1 case o.left_expr when Model::QualifiedName # allows many keys, but the name should really be a QualifiedReference - acceptor.accept(Issues::DEPRECATED_NAME_AS_TYPE, o, :name => o.value) + acceptor.accept(Issues::DEPRECATED_NAME_AS_TYPE, o, :name => o.left_expr.value) when Model::QualifiedReference # ok, allows many - this is a resource reference @@ -255,6 +256,7 @@ class Puppet::Pops::Validation::Checker3_1 def check_NodeDefinition(o) # Check that hostnames are valid hostnames (or regular expressons) hostname(o.host_matches, o) + hostname(o.parent, o, 'parent') unless o.parent.nil? top(o.eContainer, o) end @@ -385,11 +387,14 @@ class Puppet::Pops::Validation::Checker3_1 #--- HOSTNAME CHECKS # Transforms Array of host matching expressions into a (Ruby) array of AST::HostName - def hostname_Array(o, semantic) - o.each {|x| hostname x, semantic } + def hostname_Array(o, semantic, single_feature_name) + if single_feature_name + acceptor.accept(Issues::ILLEGAL_EXPRESSION, o, {:feature=>single_feature_name, :container=>semantic}) + end + o.each {|x| hostname(x, semantic, false) } end - def hostname_String(o, semantic) + def hostname_String(o, semantic, single_feature_name) # The 3.x checker only checks for illegal characters - if matching /[^-\w.]/ the name is invalid, # but this allows pathological names like "a..b......c", "----" # TODO: Investigate if more illegal hostnames should be flagged. @@ -399,11 +404,11 @@ class Puppet::Pops::Validation::Checker3_1 end end - def hostname_LiteralValue(o, semantic) - hostname_String(o.value.to_s, o) + def hostname_LiteralValue(o, semantic, single_feature_name) + hostname_String(o.value.to_s, o, single_feature_name) end - def hostname_ConcatenatedString(o, semantic) + def hostname_ConcatenatedString(o, semantic, single_feature_name) # Puppet 3.1. only accepts a concatenated string without interpolated expressions if the_expr = o.segments.index {|s| s.is_a?(Model::TextExpression) } acceptor.accept(Issues::ILLEGAL_HOSTNAME_INTERPOLATION, o.segments[the_expr].expr) @@ -413,32 +418,32 @@ class Puppet::Pops::Validation::Checker3_1 else # corner case, may be ok, but lexer may have replaced with plain string, this is # here if it does not - hostname_String(o.segments[0], o.segments[0]) + hostname_String(o.segments[0], o.segments[0], false) end end - def hostname_QualifiedName(o, semantic) - hostname_String(o.value.to_s, o) + def hostname_QualifiedName(o, semantic, single_feature_name) + hostname_String(o.value.to_s, o, single_feature_name) end - def hostname_QualifiedReference(o, semantic) - hostname_String(o.value.to_s, o) + def hostname_QualifiedReference(o, semantic, single_feature_name) + hostname_String(o.value.to_s, o, single_feature_name) end - def hostname_LiteralNumber(o, semantic) + def hostname_LiteralNumber(o, semantic, single_feature_name) # always ok end - def hostname_LiteralDefault(o, semantic) + def hostname_LiteralDefault(o, semantic, single_feature_name) # always ok end - def hostname_LiteralRegularExpression(o, semantic) + def hostname_LiteralRegularExpression(o, semantic, single_feature_name) # always ok end - def hostname_Object(o, semantic) - acceptor.accept(Issues::ILLEGAL_EXPRESSION, o, {:feature=>'hostname', :container=>semantic}) + def hostname_Object(o, semantic, single_feature_name) + acceptor.accept(Issues::ILLEGAL_EXPRESSION, o, {:feature=> single_feature_name || 'hostname', :container=>semantic}) end #---QUERY CHECKS diff --git a/lib/puppet/provider.rb b/lib/puppet/provider.rb index f875e3df0..d88eebea3 100644 --- a/lib/puppet/provider.rb +++ b/lib/puppet/provider.rb @@ -40,10 +40,10 @@ class Puppet::Provider include Puppet::Util::Warnings extend Puppet::Util::Warnings - require 'puppet/provider/confiner' + require 'puppet/confiner' require 'puppet/provider/command' - extend Puppet::Provider::Confiner + extend Puppet::Confiner Puppet::Util.logmethods(self, true) @@ -148,6 +148,7 @@ class Puppet::Provider # @raise [Puppet::DevError] if the name does not reference an existing command. # @return [String] the absolute path to the found executable for the command # @see which + # @api public def self.command(name) name = name.intern @@ -163,7 +164,7 @@ class Puppet::Provider end # Confines this provider to be suitable only on hosts where the given commands are present. - # Also see {Puppet::Provider::Confiner#confine} for other types of confinement of a provider by use of other types of + # Also see {Puppet::Confiner#confine} for other types of confinement of a provider by use of other types of # predicates. # # @note It is preferred if the commands are not entered with absolute paths as this allows puppet @@ -173,6 +174,7 @@ class Puppet::Provider # be executing on the system. Each command is specified with a name and the path of the executable. # @return [void] # @see optional_commands + # @api public # def self.commands(command_specs) command_specs.each do |name, path| @@ -189,6 +191,7 @@ class Puppet::Provider # be executing on the system. Each command is specified with a name and the path of the executable. # (@see #has_command) # @see commands + # @api public def self.optional_commands(hash) hash.each do |name, target| has_command(name, target) do @@ -221,6 +224,7 @@ class Puppet::Provider # @comment a yield [ ] produces {|| ...} in the signature, do not remove the space. # @note the name ´has_command´ looks odd in an API context, but makes more sense when seen in the internal # DSL context where a Provider is declaratively defined. + # @api public # def self.has_command(name, path, &block) name = name.intern @@ -485,7 +489,7 @@ class Puppet::Provider if @defaults.length > 0 return "Default for " + @defaults.collect do |f, v| "`#{f}` == `#{[v].flatten.join(', ')}`" - end.join(" and ") + "." + end.sort.join(" and ") + "." end end @@ -493,7 +497,7 @@ class Puppet::Provider if @commands.length > 0 return "Required binaries: " + @commands.collect do |n, c| "`#{c}`" - end.join(", ") + "." + end.sort.join(", ") + "." end end @@ -501,7 +505,7 @@ class Puppet::Provider if features.length > 0 return "Supported features: " + features.collect do |f| "`#{f}`" - end.join(", ") + "." + end.sort.join(", ") + "." end end @@ -602,6 +606,18 @@ class Puppet::Provider # fetched state (i.e. what is returned from the {instances} method). # @param resources_hash [Hash<{String => Puppet::Resource}>] map from name to resource of resources to prefetch # @return [void] + # @api public + + # @comment Document post_resource_eval here as it does not exist anywhere else (called from transaction if implemented) + # @!method self.post_resource_eval() + # @since 3.4.0 + # @api public + # @abstract A subclass may implement this - it is not implemented in the Provider class + # This method may be implemented by a provider in order to perform any + # cleanup actions needed. It will be called at the end of the transaction if + # any resources in the catalog make use of the provider, regardless of + # whether the resources are changed or not and even if resource failures occur. + # @return [void] # @comment Document flush here as it does not exist anywhere (called from transaction if implemented) # @!method flush() @@ -609,5 +625,6 @@ class Puppet::Provider # This method may be implemented by a provider in order to flush properties that has not been individually # applied to the managed entity's current state. # @return [void] + # @api public end diff --git a/lib/puppet/provider/aixobject.rb b/lib/puppet/provider/aixobject.rb index ed27b4e52..ed27b4e52 100755..100644 --- a/lib/puppet/provider/aixobject.rb +++ b/lib/puppet/provider/aixobject.rb diff --git a/lib/puppet/provider/augeas/augeas.rb b/lib/puppet/provider/augeas/augeas.rb index 2246636da..f0369a420 100644 --- a/lib/puppet/provider/augeas/augeas.rb +++ b/lib/puppet/provider/augeas/augeas.rb @@ -125,7 +125,7 @@ Puppet::Type.type(:augeas).provide(:augeas) do end fail("missing string argument #{narg} for #{cmd}") unless argline[-1] elsif f == :comparator - argline << sc.scan(/(==|!=|=~|<|<=|>|>=)/) + argline << sc.scan(/(==|!=|=~|<=|>=|<|>)/) unless argline[-1] puts sc.rest fail("invalid comparator for command #{cmd}") @@ -198,6 +198,17 @@ Puppet::Type.type(:augeas).provide(:augeas) do end end + def is_numeric?(s) + case s + when Fixnum + true + when String + s.match(/\A[+-]?\d+?(\.\d+)?\Z/n) == nil ? false : true + else + false + end + end + # Used by the need_to_run? method to process get filters. Returns # true if there is a match, false if otherwise # Assumes a syntax of get /files/path [COMPARATOR] value @@ -213,10 +224,15 @@ Puppet::Type.type(:augeas).provide(:augeas) do #check the value in augeas result = @aug.get(path) || '' - case comparator - when "!=" + + if ['<', '<=', '>=', '>'].include? comparator and is_numeric?(result) and + is_numeric?(arg) + resultf = result.to_f + argf = arg.to_f + return_value = (resultf.send(comparator, argf)) + elsif comparator == "!=" return_value = (result != arg) - when "=~" + elsif comparator == "=~" regex = Regexp.new(arg) return_value = (result =~ regex) else @@ -292,7 +308,7 @@ Puppet::Type.type(:augeas).provide(:augeas) do load_path.flatten! end - if File.exists?("#{Puppet[:libdir]}/augeas/lenses") + if Puppet::FileSystem::File.exist?("#{Puppet[:libdir]}/augeas/lenses") load_path << "#{Puppet[:libdir]}/augeas/lenses" end diff --git a/lib/puppet/provider/confine.rb b/lib/puppet/provider/confine.rb index b28d07df3..4804465b0 100644 --- a/lib/puppet/provider/confine.rb +++ b/lib/puppet/provider/confine.rb @@ -1,80 +1,6 @@ -# The class that handles testing whether our providers -# actually work or not. -require 'puppet/util' +# Confines have been moved out of the provider as they are also used for other things. +# This provides backwards compatibility for people still including this old location. +require 'puppet/provider' +require 'puppet/confine' -class Puppet::Provider::Confine - include Puppet::Util - - @tests = {} - - class << self - attr_accessor :name - end - - def self.inherited(klass) - name = klass.to_s.split("::").pop.downcase.to_sym - raise "Test #{name} is already defined" if @tests.include?(name) - - klass.name = name - - @tests[name] = klass - end - - def self.test(name) - unless @tests[name] - begin - require "puppet/provider/confine/#{name}" - rescue LoadError => detail - unless detail.to_s =~ /No such file|cannot load such file/i - warn "Could not load confine test '#{name}': #{detail}" - end - # Could not find file - end - end - @tests[name] - end - - attr_reader :values - - # Mark that this confine is used for testing binary existence. - attr_accessor :for_binary - def for_binary? - for_binary - end - - # Used for logging. - attr_accessor :label - - def initialize(values) - values = [values] unless values.is_a?(Array) - @values = values - end - - # Provide a hook for the message when there's a failure. - def message(value) - "" - end - - # Collect the results of all of them. - def result - values.collect { |value| pass?(value) } - end - - # Test whether our confine matches. - def valid? - values.each do |value| - unless pass?(value) - Puppet.debug(label + ": " + message(value)) - return false - end - end - - return true - ensure - reset - end - - # Provide a hook for subclasses. - def reset - end -end +Puppet::Provider::Confine = Puppet::Confine diff --git a/lib/puppet/provider/cron/crontab.rb b/lib/puppet/provider/cron/crontab.rb index 91047af78..91047af78 100755..100644 --- a/lib/puppet/provider/cron/crontab.rb +++ b/lib/puppet/provider/cron/crontab.rb diff --git a/lib/puppet/provider/exec.rb b/lib/puppet/provider/exec.rb index d5cd552fb..b0db9ff94 100644 --- a/lib/puppet/provider/exec.rb +++ b/lib/puppet/provider/exec.rb @@ -47,18 +47,17 @@ class Puppet::Provider::Exec < Puppet::Provider end end - Timeout::timeout(resource[:timeout]) do # note that we are passing "false" for the "override_locale" parameter, which ensures that the user's # default/system locale will be respected. Callers may override this behavior by setting locale-related # environment variables (LANG, LC_ALL, etc.) in their 'environment' configuration. - output, status = Puppet::Util::SUIDManager. - run_and_capture(command, resource[:user], resource[:group], - :override_locale => false, - :custom_environment => environment) + output = Puppet::Util::Execution.execute(command, :failonfail => false, :combine => true, + :uid => resource[:user], :gid => resource[:group], + :override_locale => false, + :custom_environment => environment) end # The shell returns 127 if the command is missing. - if status.exitstatus == 127 + if output.exitstatus == 127 raise ArgumentError, output end @@ -67,7 +66,10 @@ class Puppet::Provider::Exec < Puppet::Provider self.fail detail.to_s end - return output, status + # Return output twice as processstatus was returned before, but only exitstatus was ever called. + # Output has the exitstatus on it so it is returned instead. This is here twice as changing this + # would result in a change to the underlying API. + return output, output end def extractexe(command) diff --git a/lib/puppet/provider/exec/posix.rb b/lib/puppet/provider/exec/posix.rb index 82d6068ea..c552f9a03 100644 --- a/lib/puppet/provider/exec/posix.rb +++ b/lib/puppet/provider/exec/posix.rb @@ -1,6 +1,7 @@ require 'puppet/provider/exec' Puppet::Type.type(:exec).provide :posix, :parent => Puppet::Provider::Exec do + has_feature :umask confine :feature => :posix defaultfor :feature => :posix @@ -16,7 +17,7 @@ Puppet::Type.type(:exec).provide :posix, :parent => Puppet::Provider::Exec do exe = extractexe(command) if File.expand_path(exe) == exe - if !File.exists?(exe) + if !Puppet::FileSystem::File.exist?(exe) raise ArgumentError, "Could not find command '#{exe}'" elsif !File.file?(exe) raise ArgumentError, "'#{exe}' is a #{File.ftype(exe)}, not a file" @@ -36,4 +37,12 @@ Puppet::Type.type(:exec).provide :posix, :parent => Puppet::Provider::Exec do # distinguish not found from not executable raise ArgumentError, "Could not find command '#{exe}'" end + + def run(command, check = false) + if resource[:umask] + Puppet::Util::withumask(resource[:umask]) { super(command, check) } + else + super(command, check) + end + end end diff --git a/lib/puppet/provider/exec/windows.rb b/lib/puppet/provider/exec/windows.rb index 05b9af9e9..1c727ef0b 100644 --- a/lib/puppet/provider/exec/windows.rb +++ b/lib/puppet/provider/exec/windows.rb @@ -36,7 +36,7 @@ Puppet::Type.type(:exec).provide :windows, :parent => Puppet::Provider::Exec do exe = extractexe(command) if absolute_path?(exe) - if !File.exists?(exe) + if !Puppet::FileSystem::File.exist?(exe) raise ArgumentError, "Could not find command '#{exe}'" elsif !File.file?(exe) raise ArgumentError, "'#{exe}' is a #{File.ftype(exe)}, not a file" diff --git a/lib/puppet/provider/file/posix.rb b/lib/puppet/provider/file/posix.rb index 5ab84b48b..629a380dd 100644 --- a/lib/puppet/provider/file/posix.rb +++ b/lib/puppet/provider/file/posix.rb @@ -2,6 +2,7 @@ Puppet::Type.type(:file).provide :posix do desc "Uses POSIX functionality to manage file ownership and permissions." confine :feature => :posix + has_features :manages_symlinks include Puppet::Util::POSIX include Puppet::Util::Warnings diff --git a/lib/puppet/provider/file/windows.rb b/lib/puppet/provider/file/windows.rb index b3475ebe1..350948c47 100644 --- a/lib/puppet/provider/file/windows.rb +++ b/lib/puppet/provider/file/windows.rb @@ -2,6 +2,7 @@ Puppet::Type.type(:file).provide :windows do desc "Uses Microsoft Windows functionality to manage file ownership and permissions." confine :operatingsystem => :windows + has_feature :manages_symlinks if Puppet.features.manages_symlinks? include Puppet::Util::Warnings @@ -35,33 +36,37 @@ Puppet::Type.type(:file).provide :windows do alias :name2uid :name2id def owner - return :absent unless resource.exist? + return :absent unless resource.stat get_owner(resource[:path]) end def owner=(should) begin - set_owner(should, resource[:path]) + path = resource[:links] == :manage ? file.path.to_s : file.readlink + + set_owner(should, path) rescue => detail raise Puppet::Error, "Failed to set owner to '#{should}': #{detail}" end end def group - return :absent unless resource.exist? + return :absent unless resource.stat get_group(resource[:path]) end def group=(should) begin - set_group(should, resource[:path]) + path = resource[:links] == :manage ? file.path.to_s : file.readlink + + set_group(should, path) rescue => detail raise Puppet::Error, "Failed to set group to '#{should}': #{detail}" end end def mode - if resource.exist? + if resource.stat mode = get_mode(resource[:path]) mode ? mode.to_s(8) : :absent else @@ -85,4 +90,10 @@ Puppet::Type.type(:file).provide :windows do resource.fail("Can only manage owner, group, and mode on filesystems that support Windows ACLs, such as NTFS") end end + + attr_reader :file + private + def file + @file ||= Puppet::FileSystem::File.new(resource[:path]) + end end diff --git a/lib/puppet/provider/group/aix.rb b/lib/puppet/provider/group/aix.rb index 666748378..666748378 100755..100644 --- a/lib/puppet/provider/group/aix.rb +++ b/lib/puppet/provider/group/aix.rb diff --git a/lib/puppet/provider/group/windows_adsi.rb b/lib/puppet/provider/group/windows_adsi.rb index 5811fc593..56c0c175b 100644 --- a/lib/puppet/provider/group/windows_adsi.rb +++ b/lib/puppet/provider/group/windows_adsi.rb @@ -1,13 +1,45 @@ require 'puppet/util/adsi' Puppet::Type.type(:group).provide :windows_adsi do - desc "Local group management for Windows. Nested groups are not supported." + desc "Local group management for Windows. Group members can be both users and groups. + Additionally, local groups can contain domain users." defaultfor :operatingsystem => :windows confine :operatingsystem => :windows has_features :manages_members + def members_insync?(current, should) + return false unless current + + # By comparing account SIDs we don't have to worry about case + # sensitivity, or canonicalization of account names. + + # Cannot use munge of the group property to canonicalize @should + # since the default array_matching comparison is not commutative + should_empty = should.nil? or should.empty? + + return false if current.empty? != should_empty + + # dupes automatically weeded out when hashes built + Puppet::Util::ADSI::Group.name_sid_hash(current) == Puppet::Util::ADSI::Group.name_sid_hash(should) + end + + def members_to_s(users) + return '' if users.nil? or !users.kind_of?(Array) + users = users.map do |user_name| + sid = Puppet::Util::Windows::Security.name_to_sid_object(user_name) + if sid.account =~ /\\/ + account, _ = Puppet::Util::ADSI::User.parse_name(sid.account) + else + account = sid.account + end + resource.debug("#{sid.domain}\\#{account} (#{sid.to_s})") + "#{sid.domain}\\#{account}" + end + return users.join(',') + end + def group @group ||= Puppet::Util::ADSI::Group.new(@resource[:name]) end diff --git a/lib/puppet/provider/macauthorization/macauthorization.rb b/lib/puppet/provider/macauthorization/macauthorization.rb index fe9c56985..e410d6f43 100644 --- a/lib/puppet/provider/macauthorization/macauthorization.rb +++ b/lib/puppet/provider/macauthorization/macauthorization.rb @@ -17,7 +17,7 @@ Puppet::Type.type(:macauthorization).provide :macauthorization, :parent => Puppe # This should be confined based on macosx_productversion # but puppet resource doesn't make the facts available and # that interface is heavily used with this provider. - if FileTest.exists?("/usr/bin/sw_vers") + if Puppet::FileSystem::File.exist?("/usr/bin/sw_vers") product_version = sw_vers "-productVersion" confine :true => unless /^10\.[0-4]/.match(product_version) diff --git a/lib/puppet/provider/mailalias/aliases.rb b/lib/puppet/provider/mailalias/aliases.rb index 87ee5b465..87ee5b465 100755..100644 --- a/lib/puppet/provider/mailalias/aliases.rb +++ b/lib/puppet/provider/mailalias/aliases.rb diff --git a/lib/puppet/provider/maillist/mailman.rb b/lib/puppet/provider/maillist/mailman.rb index e070a25dd..e070a25dd 100755..100644 --- a/lib/puppet/provider/maillist/mailman.rb +++ b/lib/puppet/provider/maillist/mailman.rb diff --git a/lib/puppet/provider/mount/parsed.rb b/lib/puppet/provider/mount/parsed.rb index d935d7d13..d935d7d13 100755..100644 --- a/lib/puppet/provider/mount/parsed.rb +++ b/lib/puppet/provider/mount/parsed.rb diff --git a/lib/puppet/provider/nameservice/directoryservice.rb b/lib/puppet/provider/nameservice/directoryservice.rb index f0cbecea4..e8fe9016f 100644 --- a/lib/puppet/provider/nameservice/directoryservice.rb +++ b/lib/puppet/provider/nameservice/directoryservice.rb @@ -251,7 +251,7 @@ class Puppet::Provider::NameService::DirectoryService < Puppet::Provider::NameSe Please check your password and try again.") end - if File.exists?("#{users_plist_dir}/#{resource_name}.plist") + if Puppet::FileSystem::File.exist?("#{users_plist_dir}/#{resource_name}.plist") # If a plist already exists in /var/db/dslocal/nodes/Default/users, then # we will need to extract the binary plist from the 'ShadowHashData' # key, log the new password into the resultant plist's 'SALTED-SHA512' @@ -296,7 +296,7 @@ class Puppet::Provider::NameService::DirectoryService < Puppet::Provider::NameSe if (Puppet::Util::Package.versioncmp(get_macosx_version_major, '10.7') == -1) password_hash = nil password_hash_file = "#{password_hash_dir}/#{guid}" - if File.exists?(password_hash_file) and File.file?(password_hash_file) + if Puppet::FileSystem::File.exist?(password_hash_file) and File.file?(password_hash_file) fail("Could not read password hash file at #{password_hash_file}") if not File.readable?(password_hash_file) f = File.new(password_hash_file) password_hash = f.read @@ -304,7 +304,7 @@ class Puppet::Provider::NameService::DirectoryService < Puppet::Provider::NameSe end password_hash else - if File.exists?("#{users_plist_dir}/#{username}.plist") + if Puppet::FileSystem::File.exist?("#{users_plist_dir}/#{username}.plist") # If a plist exists in /var/db/dslocal/nodes/Default/users, we will # extract the binary plist from the 'ShadowHashData' key, decode the # salted-SHA512 password hash, and then return it. diff --git a/lib/puppet/provider/package/appdmg.rb b/lib/puppet/provider/package/appdmg.rb index 2910c8599..aaf5a7be2 100644 --- a/lib/puppet/provider/package/appdmg.rb +++ b/lib/puppet/provider/package/appdmg.rb @@ -93,7 +93,7 @@ Puppet::Type.type(:package).provide(:appdmg, :parent => Puppet::Provider::Packag end def query - FileTest.exists?("/var/db/.puppet_appdmg_installed_#{@resource[:name]}") ? {:name => @resource[:name], :ensure => :present} : nil + Puppet::FileSystem::File.exist?("/var/db/.puppet_appdmg_installed_#{@resource[:name]}") ? {:name => @resource[:name], :ensure => :present} : nil end def install diff --git a/lib/puppet/provider/package/apple.rb b/lib/puppet/provider/package/apple.rb index 362ba60c7..a603d0783 100755..100644 --- a/lib/puppet/provider/package/apple.rb +++ b/lib/puppet/provider/package/apple.rb @@ -33,7 +33,7 @@ Puppet::Type.type(:package).provide :apple, :parent => Puppet::Provider::Package end def query - FileTest.exists?("/Library/Receipts/#{@resource[:name]}.pkg") ? {:name => @resource[:name], :ensure => :present} : nil + Puppet::FileSystem::File.exist?("/Library/Receipts/#{@resource[:name]}.pkg") ? {:name => @resource[:name], :ensure => :present} : nil end def install diff --git a/lib/puppet/provider/package/apt.rb b/lib/puppet/provider/package/apt.rb index 16618d359..df5cd725a 100755..100644 --- a/lib/puppet/provider/package/apt.rb +++ b/lib/puppet/provider/package/apt.rb @@ -84,7 +84,7 @@ Puppet::Type.type(:package).provide :apt, :parent => :dpkg, :source => :dpkg do # preseeds answers to dpkg-set-selection from the "responsefile" # def run_preseed - if response = @resource[:responsefile] and FileTest.exist?(response) + if response = @resource[:responsefile] and Puppet::FileSystem::File.exist?(response) self.info("Preseeding #{response} to debconf-set-selections") preseed response diff --git a/lib/puppet/provider/package/aptitude.rb b/lib/puppet/provider/package/aptitude.rb index d3ab0da52..d3ab0da52 100755..100644 --- a/lib/puppet/provider/package/aptitude.rb +++ b/lib/puppet/provider/package/aptitude.rb diff --git a/lib/puppet/provider/package/blastwave.rb b/lib/puppet/provider/package/blastwave.rb index 4200e5b2b..fc0698e1a 100755..100644 --- a/lib/puppet/provider/package/blastwave.rb +++ b/lib/puppet/provider/package/blastwave.rb @@ -18,7 +18,7 @@ Puppet::Type.type(:package).provide :blastwave, :parent => :sun, :source => :sun "The pkg-get command is missing; blastwave packaging unavailable" end - unless FileTest.exists?("/var/pkg-get/admin") + unless Puppet::FileSystem::File.exist?("/var/pkg-get/admin") Puppet.notice "It is highly recommended you create '/var/pkg-get/admin'." Puppet.notice "See /var/pkg-get/admin-fullauto" end diff --git a/lib/puppet/provider/package/dpkg.rb b/lib/puppet/provider/package/dpkg.rb index c8528cc4b..b65ef07bf 100755..100644 --- a/lib/puppet/provider/package/dpkg.rb +++ b/lib/puppet/provider/package/dpkg.rb @@ -40,7 +40,7 @@ Puppet::Type.type(:package).provide :dpkg, :parent => Puppet::Provider::Package private # Note: self:: is required here to keep these constants in the context of what will - # eventually become this Puppet:Type::Package::ProviderDpkg class. + # eventually become this Puppet::Type::Package::ProviderDpkg class. self::DPKG_DESCRIPTION_DELIMITER = ':DESC:' self::DPKG_QUERY_FORMAT_STRING = %Q{'${Status} ${Package} ${Version} #{self::DPKG_DESCRIPTION_DELIMITER} ${Description}\\n#{self::DPKG_DESCRIPTION_DELIMITER}\\n'} self::FIELDS_REGEX = %r{^(\S+) +(\S+) +(\S+) (\S+) (\S*) #{self::DPKG_DESCRIPTION_DELIMITER} (.*)$} diff --git a/lib/puppet/provider/package/fink.rb b/lib/puppet/provider/package/fink.rb index 7efa131b9..cbf141462 100755..100644 --- a/lib/puppet/provider/package/fink.rb +++ b/lib/puppet/provider/package/fink.rb @@ -56,7 +56,7 @@ Puppet::Type.type(:package).provide :fink, :parent => :dpkg, :source => :dpkg do # preseeds answers to dpkg-set-selection from the "responsefile" # def run_preseed - if response = @resource[:responsefile] and FileTest.exists?(response) + if response = @resource[:responsefile] and Puppet::FileSystem::File.exist?(response) self.info("Preseeding #{response} to debconf-set-selections") preseed response diff --git a/lib/puppet/provider/package/freebsd.rb b/lib/puppet/provider/package/freebsd.rb index adb72c607..adb72c607 100755..100644 --- a/lib/puppet/provider/package/freebsd.rb +++ b/lib/puppet/provider/package/freebsd.rb diff --git a/lib/puppet/provider/package/gem.rb b/lib/puppet/provider/package/gem.rb index a13bbf35f..a13bbf35f 100755..100644 --- a/lib/puppet/provider/package/gem.rb +++ b/lib/puppet/provider/package/gem.rb diff --git a/lib/puppet/provider/package/macports.rb b/lib/puppet/provider/package/macports.rb index b410d6fc3..b410d6fc3 100755..100644 --- a/lib/puppet/provider/package/macports.rb +++ b/lib/puppet/provider/package/macports.rb diff --git a/lib/puppet/provider/package/msi.rb b/lib/puppet/provider/package/msi.rb index 34da206cc..46617f13f 100644 --- a/lib/puppet/provider/package/msi.rb +++ b/lib/puppet/provider/package/msi.rb @@ -74,22 +74,18 @@ Puppet::Type.type(:package).provide(:msi, :parent => Puppet::Provider::Package) # because of the special quoting we need to do around the MSI # properties to use. command = ['msiexec.exe', '/qn', '/norestart', '/i', shell_quote(resource[:source]), install_options].flatten.compact.join(' ') - execute(command, :failonfail => false, :combine => true) + output = execute(command, :failonfail => false, :combine => true) - check_result(exit_status) + check_result(output.exitstatus) end def uninstall fail("The productcode property is missing.") unless properties[:productcode] command = ['msiexec.exe', '/qn', '/norestart', '/x', properties[:productcode], uninstall_options].flatten.compact.join(' ') - execute(command, :failonfail => false, :combine => true) + output = execute(command, :failonfail => false, :combine => true) - check_result(exit_status) - end - - def exit_status - $CHILD_STATUS.exitstatus + check_result(output.exitstatus) end # (Un)install may "fail" because the package requested a reboot, the system requested a @@ -102,8 +98,6 @@ Puppet::Type.type(:package).provide(:msi, :parent => Puppet::Provider::Package) case hr when 0 # yeah - when 194 - warning("The package requested a reboot to finish the operation.") when 1641 warning("The package #{operation}ed successfully and the system is rebooting now.") when 3010 diff --git a/lib/puppet/provider/package/nim.rb b/lib/puppet/provider/package/nim.rb index 61d4cbcbf..1869cf57f 100644 --- a/lib/puppet/provider/package/nim.rb +++ b/lib/puppet/provider/package/nim.rb @@ -151,10 +151,10 @@ Puppet::Type.type(:package).provide :nim, :parent => :aix, :source => :aix do # I spent a lot of time trying to figure out a solution that didn't # require parsing the `nimclient -o showres` output and was unable to # do so. - HEADER_LINE_REGEX = /^([^\s]+)\s+[^@]+@@(I|R):(\1)\s+[^\s]+$/ - PACKAGE_LINE_REGEX = /^.*@@(I|R):(.*)$/ - RPM_PACKAGE_REGEX = /^(.*)-(.*-\d+) \2$/ - INSTALLP_PACKAGE_REGEX = /^(.*) (.*)$/ + self::HEADER_LINE_REGEX = /^([^\s]+)\s+[^@]+@@(I|R):(\1)\s+[^\s]+$/ + self::PACKAGE_LINE_REGEX = /^.*@@(I|R):(.*)$/ + self::RPM_PACKAGE_REGEX = /^(.*)-(.*-\d+) \2$/ + self::INSTALLP_PACKAGE_REGEX = /^(.*) (.*)$/ # Here is some sample output that shows what the above regexes will be up # against: @@ -205,13 +205,13 @@ Puppet::Type.type(:package).provide :nim, :parent => :aix, :source => :aix do # meant to validate that the header line for the package listing output # looks sane, so we know we're dealing with the kind of output that we # are capable of handling. - unless line.match(HEADER_LINE_REGEX) + unless line.match(self.class::HEADER_LINE_REGEX) self.fail "Unable to parse output from nimclient showres: line does not match expected package header format:\n'#{line}'" end end def parse_installp_package_string(package_string) - unless match = package_string.match(INSTALLP_PACKAGE_REGEX) + unless match = package_string.match(self.class::INSTALLP_PACKAGE_REGEX) self.fail "Unable to parse output from nimclient showres: package string does not match expected installp package string format:\n'#{package_string}'" end package_name = match.captures[0] @@ -220,7 +220,7 @@ Puppet::Type.type(:package).provide :nim, :parent => :aix, :source => :aix do end def parse_rpm_package_string(package_string) - unless match = package_string.match(RPM_PACKAGE_REGEX) + unless match = package_string.match(self.class::RPM_PACKAGE_REGEX) self.fail "Unable to parse output from nimclient showres: package string does not match expected rpm package string format:\n'#{package_string}'" end package_name = match.captures[0] @@ -229,7 +229,7 @@ Puppet::Type.type(:package).provide :nim, :parent => :aix, :source => :aix do end def parse_showres_package_line(line) - unless match = line.match(PACKAGE_LINE_REGEX) + unless match = line.match(self.class::PACKAGE_LINE_REGEX) self.fail "Unable to parse output from nimclient showres: line does not match expected package line format:\n'#{line}'" end diff --git a/lib/puppet/provider/package/openbsd.rb b/lib/puppet/provider/package/openbsd.rb index 75c01ec83..d51bc8f04 100755..100644 --- a/lib/puppet/provider/package/openbsd.rb +++ b/lib/puppet/provider/package/openbsd.rb @@ -54,7 +54,7 @@ Puppet::Type.type(:package).provide :openbsd, :parent => Puppet::Provider::Packa def parse_pkgconf unless @resource[:source] - if File.exist?("/etc/pkg.conf") + if Puppet::FileSystem::File.exist?("/etc/pkg.conf") File.open("/etc/pkg.conf", "rb").readlines.each do |line| if matchdata = line.match(/^installpath\s*=\s*(.+)\s*$/i) @resource[:source] = matchdata[1] diff --git a/lib/puppet/provider/package/opkg.rb b/lib/puppet/provider/package/opkg.rb index 439afeb01..439afeb01 100755..100644 --- a/lib/puppet/provider/package/opkg.rb +++ b/lib/puppet/provider/package/opkg.rb diff --git a/lib/puppet/provider/package/pacman.rb b/lib/puppet/provider/package/pacman.rb index 0b47a4e8b..f811aa5a8 100644 --- a/lib/puppet/provider/package/pacman.rb +++ b/lib/puppet/provider/package/pacman.rb @@ -6,7 +6,7 @@ Puppet::Type.type(:package).provide :pacman, :parent => Puppet::Provider::Packag commands :pacman => "/usr/bin/pacman" # Yaourt is a common AUR helper which, if installed, we can use to query the AUR - commands :yaourt => "/usr/bin/yaourt" if File.exists? '/usr/bin/yaourt' + commands :yaourt => "/usr/bin/yaourt" if Puppet::FileSystem::File.exist? '/usr/bin/yaourt' confine :operatingsystem => :archlinux defaultfor :operatingsystem => :archlinux @@ -14,7 +14,7 @@ Puppet::Type.type(:package).provide :pacman, :parent => Puppet::Provider::Packag # If yaourt is installed, we can make use of it def yaourt? - return File.exists? '/usr/bin/yaourt' + return Puppet::FileSystem::File.exist? '/usr/bin/yaourt' end # Install a package using 'pacman', or 'yaourt' if available. diff --git a/lib/puppet/provider/package/pkgdmg.rb b/lib/puppet/provider/package/pkgdmg.rb index 15c3639de..f14b53158 100644 --- a/lib/puppet/provider/package/pkgdmg.rb +++ b/lib/puppet/provider/package/pkgdmg.rb @@ -113,7 +113,7 @@ Puppet::Type.type(:package).provide :pkgdmg, :parent => Puppet::Provider::Packag end def query - if FileTest.exists?("/var/db/.puppet_pkgdmg_installed_#{@resource[:name]}") + if Puppet::FileSystem::File.exist?("/var/db/.puppet_pkgdmg_installed_#{@resource[:name]}") Puppet.debug "/var/db/.puppet_pkgdmg_installed_#{@resource[:name]} found" return {:name => @resource[:name], :ensure => :present} else diff --git a/lib/puppet/provider/package/pkgutil.rb b/lib/puppet/provider/package/pkgutil.rb index 157066415..c114fa949 100755..100644 --- a/lib/puppet/provider/package/pkgutil.rb +++ b/lib/puppet/provider/package/pkgutil.rb @@ -14,7 +14,7 @@ Puppet::Type.type(:package).provide :pkgutil, :parent => :sun, :source => :sun d end def self.healthcheck() - unless FileTest.exists?("/var/opt/csw/pkgutil/admin") + unless Puppet::FileSystem::File.exist?("/var/opt/csw/pkgutil/admin") Puppet.notice "It is highly recommended you create '/var/opt/csw/pkgutil/admin'." Puppet.notice "See /var/opt/csw/pkgutil" end diff --git a/lib/puppet/provider/package/ports.rb b/lib/puppet/provider/package/ports.rb index 9141e30f5..9141e30f5 100755..100644 --- a/lib/puppet/provider/package/ports.rb +++ b/lib/puppet/provider/package/ports.rb diff --git a/lib/puppet/provider/package/rpm.rb b/lib/puppet/provider/package/rpm.rb index e99e51c7b..6b89eaa62 100755..100644 --- a/lib/puppet/provider/package/rpm.rb +++ b/lib/puppet/provider/package/rpm.rb @@ -3,15 +3,22 @@ require 'puppet/provider/package' # RPM packaging. Should work anywhere that has rpm installed. Puppet::Type.type(:package).provide :rpm, :source => :rpm, :parent => Puppet::Provider::Package do desc "RPM packaging support; should work anywhere with a working `rpm` - binary." + binary. + + This provider supports the `install_options` attribute, which allows + command-line flags to be passed to the RPM binary. Install options should be + specified as an array, where each element is either a string or a + `{'--flag' => 'value'}` hash. (That hash example would be equivalent to a + `'--flag=value'` string; the hash syntax is available as a convenience.)" has_feature :versionable + has_feature :install_options # Note: self:: is required here to keep these constants in the context of what will - # eventually become this Puppet:Type::Package::ProviderRpm class. + # eventually become this Puppet::Type::Package::ProviderRpm class. self::RPM_DESCRIPTION_DELIMITER = ':DESC:' # The query format by which we identify installed packages - self::NEVRA_FORMAT = %Q{'%{NAME} %|EPOCH?{%{EPOCH}}:{0}| %{VERSION} %{RELEASE} %{ARCH} #{self::RPM_DESCRIPTION_DELIMITER} %{SUMMARY}\\n'} + self::NEVRA_FORMAT = %Q{%{NAME} %|EPOCH?{%{EPOCH}}:{0}| %{VERSION} %{RELEASE} %{ARCH} #{self::RPM_DESCRIPTION_DELIMITER} %{SUMMARY}\\n} self::NEVRA_REGEX = %r{^(\S+) (\S+) (\S+) (\S+) (\S+)(?: #{self::RPM_DESCRIPTION_DELIMITER} ?(.*))?$} self::RPM_PACKAGE_NOT_FOUND_REGEX = /package .+ is not installed/ self::NEVRA_FIELDS = [:name, :epoch, :version, :release, :arch, :description] @@ -49,7 +56,7 @@ Puppet::Type.type(:package).provide :rpm, :source => :rpm, :parent => Puppet::Pr # list out all of the packages begin - execpipe("#{command(:rpm)} -qa #{nosignature} #{nodigest} --qf #{self::NEVRA_FORMAT}") { |process| + execpipe("#{command(:rpm)} -qa #{nosignature} #{nodigest} --qf '#{self::NEVRA_FORMAT}'") { |process| # now turn each returned line into a package object process.each_line { |line| hash = nevra_to_hash(line) @@ -107,9 +114,10 @@ Puppet::Type.type(:package).provide :rpm, :source => :rpm, :parent => Puppet::Pr return end - flag = "-i" + flag = ["-i"] flag = ["-U", "--oldpackage"] if @property_hash[:ensure] and @property_hash[:ensure] != :absent + flag = flag + install_options rpm flag, source end @@ -139,8 +147,36 @@ Puppet::Type.type(:package).provide :rpm, :source => :rpm, :parent => Puppet::Pr self.install end + def install_options + join_options(resource[:install_options]) + end + private + # Turns a array of options into flags to be passed to rpm install(8) and + # The options can be passed as a string or hash. Note that passing a hash + # should only be used in case -Dfoo=bar must be passed, + # which can be accomplished with: + # install_options => [ { '-Dfoo' => 'bar' } ] + # Regular flags like '-L' must be passed as a string. + # @param options [Array] + # @return Concatenated list of options + # @api private + def join_options(options) + return [] unless options + + options.collect do |val| + case val + when Hash + val.keys.sort.collect do |k| + "#{k}=#{val[k]}" + end.join(' ') + else + val + end + end + end + # @param line [String] one line of rpm package query information # @return [Hash] of NEVRA_FIELDS strings parsed from package info # if we failed to parse diff --git a/lib/puppet/provider/package/sun.rb b/lib/puppet/provider/package/sun.rb index 5f15ad13e..87a13a2a5 100755..100644 --- a/lib/puppet/provider/package/sun.rb +++ b/lib/puppet/provider/package/sun.rb @@ -15,7 +15,7 @@ Puppet::Type.type(:package).provide :sun, :parent => Puppet::Provider::Package d has_feature :install_options - Namemap = { + self::Namemap = { "PKGINST" => :name, "CATEGORY" => :category, "ARCH" => :platform, @@ -26,8 +26,8 @@ Puppet::Type.type(:package).provide :sun, :parent => Puppet::Provider::Package d } def self.namemap(hash) - Namemap.keys.inject({}) do |hsh,k| - hsh.merge(Namemap[k] => hash[k]) + self::Namemap.keys.inject({}) do |hsh,k| + hsh.merge(self::Namemap[k] => hash[k]) end end diff --git a/lib/puppet/provider/package/sunfreeware.rb b/lib/puppet/provider/package/sunfreeware.rb index 118d36b66..118d36b66 100755..100644 --- a/lib/puppet/provider/package/sunfreeware.rb +++ b/lib/puppet/provider/package/sunfreeware.rb diff --git a/lib/puppet/provider/package/windows.rb b/lib/puppet/provider/package/windows.rb index 7ca022c8e..d4936c89b 100644 --- a/lib/puppet/provider/package/windows.rb +++ b/lib/puppet/provider/package/windows.rb @@ -22,6 +22,7 @@ Puppet::Type.type(:package).provide(:windows, :parent => Puppet::Provider::Packa has_feature :uninstallable has_feature :install_options has_feature :uninstall_options + has_feature :versionable attr_accessor :package @@ -37,9 +38,7 @@ Puppet::Type.type(:package).provide(:windows, :parent => Puppet::Provider::Packa def self.to_hash(pkg) { :name => pkg.name, - # we're not versionable, so we can't set the ensure - # parameter to the currently installed version - :ensure => :installed, + :ensure => pkg.version || :installed, :provider => :windows } end @@ -59,26 +58,22 @@ Puppet::Type.type(:package).provide(:windows, :parent => Puppet::Provider::Packa installer = Puppet::Provider::Package::Windows::Package.installer_class(resource) command = [installer.install_command(resource), install_options].flatten.compact.join(' ') - execute(command, :failonfail => false, :combine => true) + output = execute(command, :failonfail => false, :combine => true) - check_result(exit_status) + check_result(output.exitstatus) end def uninstall command = [package.uninstall_command, uninstall_options].flatten.compact.join(' ') - execute(command, :failonfail => false, :combine => true) + output = execute(command, :failonfail => false, :combine => true) - check_result(exit_status) - end - - def exit_status - $CHILD_STATUS.exitstatus + check_result(output.exitstatus) end # http://msdn.microsoft.com/en-us/library/windows/desktop/aa368542(v=vs.85).aspx - ERROR_SUCCESS = 0 - ERROR_SUCCESS_REBOOT_INITIATED = 1641 - ERROR_SUCCESS_REBOOT_REQUIRED = 3010 + self::ERROR_SUCCESS = 0 + self::ERROR_SUCCESS_REBOOT_INITIATED = 1641 + self::ERROR_SUCCESS_REBOOT_REQUIRED = 3010 # (Un)install may "fail" because the package requested a reboot, the system requested a # reboot, or something else entirely. Reboot requests mean the package was installed @@ -87,13 +82,11 @@ Puppet::Type.type(:package).provide(:windows, :parent => Puppet::Provider::Packa operation = resource[:ensure] == :absent ? 'uninstall' : 'install' case hr - when ERROR_SUCCESS + when self.class::ERROR_SUCCESS # yeah - when 194 - warning("The package requested a reboot to finish the operation.") - when ERROR_SUCCESS_REBOOT_INITIATED + when self.class::ERROR_SUCCESS_REBOOT_INITIATED warning("The package #{operation}ed successfully and the system is rebooting now.") - when ERROR_SUCCESS_REBOOT_REQUIRED + when self.class::ERROR_SUCCESS_REBOOT_REQUIRED warning("The package #{operation}ed successfully, but the system must be rebooted.") else raise Puppet::Util::Windows::Error.new("Failed to #{operation}", hr) diff --git a/lib/puppet/provider/package/windows/package.rb b/lib/puppet/provider/package/windows/package.rb index fd007de6c..e26c6773c 100644 --- a/lib/puppet/provider/package/windows/package.rb +++ b/lib/puppet/provider/package/windows/package.rb @@ -58,7 +58,7 @@ class Puppet::Provider::Package::Windows # REMIND: what about msp, etc MsiPackage when /\.exe"?\Z/i - fail("The source does not exist: '#{resource[:source]}'") unless File.exists?(resource[:source]) + fail("The source does not exist: '#{resource[:source]}'") unless Puppet::FileSystem::File.exist?(resource[:source]) ExePackage else fail("Don't know how to install '#{resource[:source]}'") diff --git a/lib/puppet/provider/package/yum.rb b/lib/puppet/provider/package/yum.rb index dd41aa470..c32052d3d 100644 --- a/lib/puppet/provider/package/yum.rb +++ b/lib/puppet/provider/package/yum.rb @@ -11,7 +11,7 @@ Puppet::Type.type(:package).provide :yum, :parent => :rpm, :source => :rpm do commands :yum => "yum", :rpm => "rpm", :python => "python" - YUMHELPER = File::join(File::dirname(__FILE__), "yumhelper.py") + self::YUMHELPER = File::join(File::dirname(__FILE__), "yumhelper.py") attr_accessor :latest_info @@ -34,7 +34,7 @@ Puppet::Type.type(:package).provide :yum, :parent => :rpm, :source => :rpm do # collect our 'latest' info updates = {} - python(YUMHELPER).each_line do |l| + python(self::YUMHELPER).each_line do |l| l.chomp! next if l.empty? if l[0,4] == "_pkg" diff --git a/lib/puppet/provider/parsedfile.rb b/lib/puppet/provider/parsedfile.rb index 1a13c8df3..1a13c8df3 100755..100644 --- a/lib/puppet/provider/parsedfile.rb +++ b/lib/puppet/provider/parsedfile.rb diff --git a/lib/puppet/provider/port/parsed.rb b/lib/puppet/provider/port/parsed.rb index 5c973b6af..5c973b6af 100755..100644 --- a/lib/puppet/provider/port/parsed.rb +++ b/lib/puppet/provider/port/parsed.rb diff --git a/lib/puppet/provider/service/base.rb b/lib/puppet/provider/service/base.rb index 0d960854f..0d960854f 100755..100644 --- a/lib/puppet/provider/service/base.rb +++ b/lib/puppet/provider/service/base.rb diff --git a/lib/puppet/provider/service/bsd.rb b/lib/puppet/provider/service/bsd.rb index 85e7824fa..a19608a51 100644 --- a/lib/puppet/provider/service/bsd.rb +++ b/lib/puppet/provider/service/bsd.rb @@ -20,13 +20,13 @@ Puppet::Type.type(:service).provide :bsd, :parent => :init do # remove service file from rc.conf.d to disable it def disable rcfile = File.join(rcconf_dir, @model[:name]) - File.delete(rcfile) if File.exists?(rcfile) + File.delete(rcfile) if Puppet::FileSystem::File.exist?(rcfile) end # if the service file exists in rc.conf.d then it's already enabled def enabled? rcfile = File.join(rcconf_dir, @model[:name]) - return :true if File.exists?(rcfile) + return :true if Puppet::FileSystem::File.exist?(rcfile) :false end @@ -34,7 +34,7 @@ Puppet::Type.type(:service).provide :bsd, :parent => :init do # enable service by creating a service file under rc.conf.d with the # proper contents def enable - Dir.mkdir(rcconf_dir) if not File.exists?(rcconf_dir) + Dir.mkdir(rcconf_dir) if not Puppet::FileSystem::File.exist?(rcconf_dir) rcfile = File.join(rcconf_dir, @model[:name]) open(rcfile, 'w') { |f| f << "%s_enable=\"YES\"\n" % @model[:name] } end diff --git a/lib/puppet/provider/service/daemontools.rb b/lib/puppet/provider/service/daemontools.rb index f7784cc61..32c0e0ff7 100644 --- a/lib/puppet/provider/service/daemontools.rb +++ b/lib/puppet/provider/service/daemontools.rb @@ -48,7 +48,7 @@ Puppet::Type.type(:service).provide :daemontools, :parent => :base do def defpath(dummy_argument=:work_arround_for_ruby_GC_bug) unless @defpath ["/var/lib/service", "/etc"].each do |path| - if FileTest.exist?(path) + if Puppet::FileSystem::File.exist?(path) @defpath = path break end @@ -74,7 +74,7 @@ Puppet::Type.type(:service).provide :daemontools, :parent => :base do # or don't contain a run file Dir.entries(path).reject { |e| fullpath = File.join(path, e) - e =~ /^\./ or ! FileTest.directory?(fullpath) or ! FileTest.exist?(File.join(fullpath,"run")) + e =~ /^\./ or ! FileTest.directory?(fullpath) or ! Puppet::FileSystem::File.exist?(File.join(fullpath,"run")) }.collect do |name| new(:name => name, :path => path) end @@ -89,7 +89,7 @@ Puppet::Type.type(:service).provide :daemontools, :parent => :base do def servicedir unless @servicedir ["/service", "/etc/service","/var/lib/svscan"].each do |path| - if FileTest.exist?(path) + if Puppet::FileSystem::File.exist?(path) @servicedir = path break end @@ -142,7 +142,7 @@ Puppet::Type.type(:service).provide :daemontools, :parent => :base do return :true else # the service is enabled if it is linked - return FileTest.symlink?(self.service) ? :true : :false + return Puppet::FileSystem::File.new(self.service).symlink? ? :true : :false end end @@ -152,9 +152,9 @@ Puppet::Type.type(:service).provide :daemontools, :parent => :base do self.setupservice end if self.daemon - if ! FileTest.symlink?(self.service) + if ! Puppet::FileSystem::File.new(self.service).symlink? Puppet.notice "Enabling #{self.service}: linking #{self.daemon} -> #{self.service}" - File.symlink(self.daemon, self.service) + Puppet::FileSystem::File.new(self.daemon).symlink(self.service) end end rescue Puppet::ExecutionFailure @@ -168,9 +168,9 @@ Puppet::Type.type(:service).provide :daemontools, :parent => :base do self.setupservice end if self.daemon - if FileTest.symlink?(self.service) + if Puppet::FileSystem::File.new(self.service).symlink? Puppet.notice "Disabling #{self.service}: removing link #{self.daemon} -> #{self.service}" - File.unlink(self.service) + Puppet::FileSystem::File.unlink(self.service) end end rescue Puppet::ExecutionFailure diff --git a/lib/puppet/provider/service/debian.rb b/lib/puppet/provider/service/debian.rb index 7d14eaa3c..7d14eaa3c 100755..100644 --- a/lib/puppet/provider/service/debian.rb +++ b/lib/puppet/provider/service/debian.rb diff --git a/lib/puppet/provider/service/freebsd.rb b/lib/puppet/provider/service/freebsd.rb index 8cf07bab5..36de850c5 100644 --- a/lib/puppet/provider/service/freebsd.rb +++ b/lib/puppet/provider/service/freebsd.rb @@ -71,7 +71,7 @@ Puppet::Type.type(:service).provide :freebsd, :parent => :init do success = false # Replace in all files, not just in the first found with a match [rcconf, rcconf_local, rcconf_dir + "/#{service}"].each do |filename| - if File.exists?(filename) + if Puppet::FileSystem::File.exist?(filename) s = File.read(filename) if s.gsub!(/^(#{rcvar}(_enable)?)=\"?(YES|NO)\"?/, "\\1=\"#{yesno}\"") File.open(filename, File::WRONLY) { |f| f << s } @@ -87,14 +87,14 @@ Puppet::Type.type(:service).provide :freebsd, :parent => :init do def rc_add(service, rcvar, yesno) append = "\# Added by Puppet\n#{rcvar}_enable=\"#{yesno}\"\n" # First, try the one-file-per-service style - if File.exists?(rcconf_dir) + if Puppet::FileSystem::File.exist?(rcconf_dir) File.open(rcconf_dir + "/#{service}", File::WRONLY | File::APPEND | File::CREAT, 0644) { |f| f << append self.debug("Appended to #{f.path}") } else # Else, check the local rc file first, but don't create it - if File.exists?(rcconf_local) + if Puppet::FileSystem::File.exist?(rcconf_local) File.open(rcconf_local, File::WRONLY | File::APPEND) { |f| f << append self.debug("Appended to #{f.path}") diff --git a/lib/puppet/provider/service/init.rb b/lib/puppet/provider/service/init.rb index 23ad824b2..d46cc8772 100755..100644 --- a/lib/puppet/provider/service/init.rb +++ b/lib/puppet/provider/service/init.rb @@ -99,7 +99,7 @@ Puppet::Type.type(:service).provide :init, :parent => :base do if File.directory?(path) true else - if File.exist?(path) + if Puppet::FileSystem::File.exist?(path) self.debug "Search path #{path} is not a directory" else self.debug "Search path #{path} does not exist" @@ -112,7 +112,7 @@ Puppet::Type.type(:service).provide :init, :parent => :base do def search(name) paths.each do |path| fqname = File.join(path,name) - if File.exist? fqname + if Puppet::FileSystem::File.exist? fqname return fqname else self.debug("Could not find #{name} in #{path}") @@ -121,7 +121,7 @@ Puppet::Type.type(:service).provide :init, :parent => :base do paths.each do |path| fqname_sh = File.join(path,"#{name}.sh") - if File.exist? fqname_sh + if Puppet::FileSystem::File.exist? fqname_sh return fqname_sh else self.debug("Could not find #{name}.sh in #{path}") @@ -154,7 +154,8 @@ Puppet::Type.type(:service).provide :init, :parent => :base do private def self.is_init?(script = initscript) - !File.symlink?(script) || File.readlink(script) != "/lib/init/upstart-job" + file = Puppet::FileSystem::File.new(script) + !file.symlink? || file.readlink != "/lib/init/upstart-job" end end diff --git a/lib/puppet/provider/service/launchd.rb b/lib/puppet/provider/service/launchd.rb index 206676aec..316fdbf46 100644 --- a/lib/puppet/provider/service/launchd.rb +++ b/lib/puppet/provider/service/launchd.rb @@ -112,40 +112,51 @@ Puppet::Type.type(:service).provide :launchd, :parent => :base do array_of_files.compact end + # Get a hash of all launchd plists, keyed by label. This value is cached, but + # the cache will be refreshed if refresh is true. + # + # @api private + def self.make_label_to_path_map(refresh=false) + return @label_to_path_map if @label_to_path_map and not refresh + @label_to_path_map = {} + launchd_paths.each do |path| + return_globbed_list_of_file_paths(path).each do |filepath| + job = read_plist(filepath) + next if job.nil? + if job.has_key?("Label") + @label_to_path_map[job["Label"]] = filepath + else + Puppet.warning("The #{filepath} plist does not contain a 'label' key; " + + "Puppet is skipping it") + next + end + end + end + @label_to_path_map + end + # Sets a class instance variable with a hash of all launchd plist files that # are found on the system. The key of the hash is the job id and the value # is the path to the file. If a label is passed, we return the job id and # path for that specific job. def self.jobsearch(label=nil) - @label_to_path_map ||= {} - if @label_to_path_map.empty? - launchd_paths.each do |path| - return_globbed_list_of_file_paths(path).each do |filepath| - job = read_plist(filepath) - next if job.nil? - if job.has_key?("Label") - if job["Label"] == label - return { label => filepath } - else - @label_to_path_map[job["Label"]] = filepath - end - else - Puppet.warning("The #{filepath} plist does not contain a 'label' key; " + - "Puppet is skipping it") - next - end - end - end - end + by_label = make_label_to_path_map if label - if @label_to_path_map.has_key? label - return { label => @label_to_path_map[label] } + if by_label.has_key? label + return { label => by_label[label] } else - raise Puppet::Error.new("Unable to find launchd plist for job: #{label}") + # try refreshing the map, in case a plist has been added in the interim + by_label = make_label_to_path_map(true) + if by_label.has_key? label + return { label => by_label[label] } + else + raise Puppet::Error, "Unable to find launchd plist for job: #{label}" + end end else - @label_to_path_map + # caller wants the whole map + by_label end end diff --git a/lib/puppet/provider/service/openbsd.rb b/lib/puppet/provider/service/openbsd.rb new file mode 100644 index 000000000..8fe68955e --- /dev/null +++ b/lib/puppet/provider/service/openbsd.rb @@ -0,0 +1,23 @@ +Puppet::Type.type(:service).provide :openbsd, :parent => :init do + + desc "Provider for OpenBSD's rc.d daemon control scripts" + + confine :operatingsystem => :openbsd + defaultfor :operatingsystem => :openbsd + + def self.defpath + ["/etc/rc.d"] + end + + def startcmd + [self.initscript, "-f", :start] + end + + def restartcmd + (@resource[:hasrestart] == :true) && [self.initscript, "-f", :restart] + end + + def statuscmd + [self.initscript, :check] + end +end diff --git a/lib/puppet/provider/service/redhat.rb b/lib/puppet/provider/service/redhat.rb index c1c6401c1..c1c6401c1 100755..100644 --- a/lib/puppet/provider/service/redhat.rb +++ b/lib/puppet/provider/service/redhat.rb diff --git a/lib/puppet/provider/service/runit.rb b/lib/puppet/provider/service/runit.rb index d326b1ebf..8d835d2db 100644 --- a/lib/puppet/provider/service/runit.rb +++ b/lib/puppet/provider/service/runit.rb @@ -42,7 +42,7 @@ Puppet::Type.type(:service).provide :runit, :parent => :daemontools do def defpath(dummy_argument=:work_arround_for_ruby_GC_bug) unless @defpath ["/etc/sv", "/var/lib/service"].each do |path| - if FileTest.exist?(path) + if Puppet::FileSystem::File.exist?(path) @defpath = path break end @@ -57,7 +57,7 @@ Puppet::Type.type(:service).provide :runit, :parent => :daemontools do def servicedir unless @servicedir ["/service", "/etc/service","/var/service"].each do |path| - if FileTest.exist?(path) + if Puppet::FileSystem::File.exist?(path) @servicedir = path break end @@ -105,7 +105,7 @@ Puppet::Type.type(:service).provide :runit, :parent => :daemontools do # before a disable def disable # unlink the daemon symlink to disable it - File.unlink(self.service) if FileTest.symlink?(self.service) + Puppet::FileSystem::File.unlink(self.service) if Puppet::FileSystem::File.new(self.service).symlink? end end diff --git a/lib/puppet/provider/service/smf.rb b/lib/puppet/provider/service/smf.rb index 25ae551de..25ae551de 100755..100644 --- a/lib/puppet/provider/service/smf.rb +++ b/lib/puppet/provider/service/smf.rb diff --git a/lib/puppet/provider/service/src.rb b/lib/puppet/provider/service/src.rb index 8028751ff..8028751ff 100755..100644 --- a/lib/puppet/provider/service/src.rb +++ b/lib/puppet/provider/service/src.rb diff --git a/lib/puppet/provider/service/systemd.rb b/lib/puppet/provider/service/systemd.rb index 1ff070b86..1ff070b86 100755..100644 --- a/lib/puppet/provider/service/systemd.rb +++ b/lib/puppet/provider/service/systemd.rb diff --git a/lib/puppet/provider/service/upstart.rb b/lib/puppet/provider/service/upstart.rb index 04665c971..c7fd58c4e 100755..100644 --- a/lib/puppet/provider/service/upstart.rb +++ b/lib/puppet/provider/service/upstart.rb @@ -71,7 +71,7 @@ Puppet::Type.type(:service).provide :upstart, :parent => :debian do paths.each do |path| service_name = name.match(/^(\S+)/)[1] fqname = File.join(path, service_name + suffix) - if File.exists?(fqname) + if Puppet::FileSystem::File.exist?(fqname) return fqname end @@ -148,7 +148,7 @@ Puppet::Type.type(:service).provide :upstart, :parent => :debian do private def is_upstart?(script = initscript) - File.exists?(script) && script.match(/\/etc\/init\/\S+\.conf/) + Puppet::FileSystem::File.exist?(script) && script.match(/\/etc\/init\/\S+\.conf/) end def version_is_pre_0_6_7 @@ -256,7 +256,7 @@ private end def read_override_file - if File.exists?(overscript) + if Puppet::FileSystem::File.exist?(overscript) read_script_from(overscript) else "" diff --git a/lib/puppet/provider/ssh_authorized_key/parsed.rb b/lib/puppet/provider/ssh_authorized_key/parsed.rb index 44fef458e..cb08b593f 100644 --- a/lib/puppet/provider/ssh_authorized_key/parsed.rb +++ b/lib/puppet/provider/ssh_authorized_key/parsed.rb @@ -41,7 +41,7 @@ Puppet::Type.type(:ssh_authorized_key).provide( end def user - uid = File.stat(target).uid + uid = Puppet::FileSystem::File.new(target).stat.uid Etc.getpwuid(uid).name end @@ -55,7 +55,7 @@ Puppet::Type.type(:ssh_authorized_key).provide( self.class.backup_target(target) Puppet::Util::SUIDManager.asuser(@resource.should(:user)) do - unless File.exist?(dir = File.dirname(target)) + unless Puppet::FileSystem::File.exist?(dir = File.dirname(target)) Puppet.debug "Creating #{dir}" Dir.mkdir(dir, dir_perm) end diff --git a/lib/puppet/provider/sshkey/parsed.rb b/lib/puppet/provider/sshkey/parsed.rb index f874683b7..f874683b7 100755..100644 --- a/lib/puppet/provider/sshkey/parsed.rb +++ b/lib/puppet/provider/sshkey/parsed.rb diff --git a/lib/puppet/provider/user/aix.rb b/lib/puppet/provider/user/aix.rb index 0831f2e26..0831f2e26 100755..100644 --- a/lib/puppet/provider/user/aix.rb +++ b/lib/puppet/provider/user/aix.rb diff --git a/lib/puppet/provider/user/directoryservice.rb b/lib/puppet/provider/user/directoryservice.rb index c9110808d..6a59c4bee 100644 --- a/lib/puppet/provider/user/directoryservice.rb +++ b/lib/puppet/provider/user/directoryservice.rb @@ -238,7 +238,7 @@ Puppet::Type.type(:user).provide :directoryservice do def self.get_sha1(guid) password_hash = nil password_hash_file = "#{password_hash_dir}/#{guid}" - if File.exists?(password_hash_file) and File.file?(password_hash_file) + if Puppet::FileSystem::File.exist?(password_hash_file) and File.file?(password_hash_file) raise Puppet::Error, "Could not read password hash file at #{password_hash_file}" if not File.readable?(password_hash_file) f = File.new(password_hash_file) password_hash = f.read diff --git a/lib/puppet/provider/user/useradd.rb b/lib/puppet/provider/user/useradd.rb index 741d0c1d7..2c6552c73 100644 --- a/lib/puppet/provider/user/useradd.rb +++ b/lib/puppet/provider/user/useradd.rb @@ -160,7 +160,7 @@ Puppet::Type.type(:user).provide :useradd, :parent => Puppet::Provider::NameServ else cmd = [command(:add)] end - if not @resource.should(gid) and Puppet::Util.gid(@resource[:name]) + if not @resource.should(:gid) and Puppet::Util.gid(@resource[:name]) cmd += ["-g", @resource[:name]] end cmd += add_properties diff --git a/lib/puppet/provider/zone/solaris.rb b/lib/puppet/provider/zone/solaris.rb index 6e0d14c31..2cd0e99bb 100644 --- a/lib/puppet/provider/zone/solaris.rb +++ b/lib/puppet/provider/zone/solaris.rb @@ -261,7 +261,7 @@ Puppet::Type.type(:zone).provide(:solaris) do # which makes zoneadmd mount the zone root zoneadm :ready unless File.directory?(zoneetc) - unless File.exists?(sysidcfg) + unless Puppet::FileSystem::File.exist?(sysidcfg) begin File.open(sysidcfg, "w", 0600) do |f| f.puts cfg diff --git a/lib/puppet/rails/benchmark.rb b/lib/puppet/rails/benchmark.rb index 741b6d5bd..8d88280c3 100644 --- a/lib/puppet/rails/benchmark.rb +++ b/lib/puppet/rails/benchmark.rb @@ -52,7 +52,7 @@ module Puppet::Rails::Benchmark file = "/tmp/time_debugging.yaml" - if FileTest.exist?(file) + if Puppet::FileSystem::File.exist?(file) data = YAML.load_file(file) else data = {} diff --git a/lib/puppet/reference/configuration.rb b/lib/puppet/reference/configuration.rb index e7f495403..c1580fe35 100644 --- a/lib/puppet/reference/configuration.rb +++ b/lib/puppet/reference/configuration.rb @@ -14,14 +14,13 @@ config = Puppet::Util::Reference.newreference(:configuration, :depth => 1, :doc # Print the doc string itself begin - str << object.desc.gsub(/\n/, " ") + str << Puppet::Util::Docs.scrub(object.desc) rescue => detail Puppet.log_exception(detail) end str << "\n\n" # Now print the data about the item. - str << "" val = object.default if name.to_s == "vardir" val = "/var/lib/puppet" diff --git a/lib/puppet/reference/indirection.rb b/lib/puppet/reference/indirection.rb index e997c88e9..b1685eca1 100644 --- a/lib/puppet/reference/indirection.rb +++ b/lib/puppet/reference/indirection.rb @@ -10,16 +10,14 @@ reference = Puppet::Util::Reference.newreference :indirection, :doc => "Indirect name = indirection.to_s.capitalize text << "## " + indirection.to_s + "\n\n" - text << ind.doc + "\n\n" - - text << "### Termini\n\n" + text << Puppet::Util::Docs.scrub(ind.doc) + "\n\n" Puppet::Indirector::Terminus.terminus_classes(ind.name).sort { |a,b| a.to_s <=> b.to_s }.each do |terminus| terminus_name = terminus.to_s term_class = Puppet::Indirector::Terminus.terminus_class(ind.name, terminus) if term_class terminus_doc = Puppet::Util::Docs.scrub(term_class.doc) - text << markdown_definitionlist(terminus_name, terminus_doc) + text << markdown_header("`#{terminus_name}` terminus", 3) << terminus_doc << "\n\n" else Puppet.warning "Could not build docs for indirector #{name.inspect}, terminus #{terminus_name.inspect}: could not locate terminus." end @@ -31,7 +29,7 @@ end reference.header = <<HEADER -# About Indirection +## About Indirection Puppet's indirector support pluggable backends (termini) for a variety of key-value stores (indirections). Each indirection type corresponds to a particular Ruby class (the "Indirected Class" below) and values are instances of that class. @@ -40,15 +38,15 @@ The termini can be local (e.g., on-disk files) or remote (e.g., using a REST int An indirector has five methods, which are mapped into HTTP verbs for the REST interface: - * `find(key)` - get a single value (mapped to GET or POST with a singular endpoint) - * `search(key)` - get a list of matching values (mapped to GET with a plural endpoint) - * `head(key)` - return true if the key exists (mapped to HEAD) - * `destroy(key)` - remove the key van value (mapped to DELETE) - * `save(instance)` - write the instance to the store, using the instance's name as the key (mapped to PUT) +* `find(key)` - get a single value (mapped to GET or POST with a singular endpoint) +* `search(key)` - get a list of matching values (mapped to GET with a plural endpoint) +* `head(key)` - return true if the key exists (mapped to HEAD) +* `destroy(key)` - remove the key van value (mapped to DELETE) +* `save(instance)` - write the instance to the store, using the instance's name as the key (mapped to PUT) -These methods are available via the `indirection` class method on the indirected classes. For example:: +These methods are available via the `indirection` class method on the indirected classes. For example: - foo_cert = Puppet::SSL::Certificate.indirection.find('foo.example.com') + foo_cert = Puppet::SSL::Certificate.indirection.find('foo.example.com') At startup, each indirection is configured with a terminus. In most cases, this is the default terminus defined by the indirected class, but it can be overridden by the application or face, or overridden with the `route_file` configuration. @@ -58,12 +56,12 @@ Indirections can also have a cache, represented by a second terminus. This is a write-through cache: modifications are written both to the cache and to the primary terminus. Values fetched from the terminus are written to the cache. -## Interaction with REST +### Interaction with REST REST endpoints have the form `/{environment}/{indirection}/{key}`, where the indirection can be singular or plural, following normal English spelling rules. On the server side, REST responses are generated from the locally-configured endpoints. -## Indirections and Termini +### Indirections and Termini Below is the list of all indirections, their associated terminus classes, and how you select between them. diff --git a/lib/puppet/relationship.rb b/lib/puppet/relationship.rb index 2ffcd298f..ebac97e7e 100755..100644 --- a/lib/puppet/relationship.rb +++ b/lib/puppet/relationship.rb @@ -1,5 +1,3 @@ -#!/usr/bin/env ruby - # subscriptions are permanent associations determining how different # objects react to an event @@ -72,7 +70,7 @@ class Puppet::Relationship "{ #{source} => #{target} }" end - def to_pson_data_hash + def to_data_hash data = { 'source' => source.to_s, 'target' => target.to_s @@ -85,8 +83,13 @@ class Puppet::Relationship data end + # This doesn't include document type as it is part of a catalog + def to_pson_data_hash + to_data_hash + end + def to_pson(*args) - to_pson_data_hash.to_pson(*args) + to_data_hash.to_pson(*args) end def to_s diff --git a/lib/puppet/reports.rb b/lib/puppet/reports.rb index be6e8203f..e219971fb 100755..100644 --- a/lib/puppet/reports.rb +++ b/lib/puppet/reports.rb @@ -72,9 +72,9 @@ class Puppet::Reports instance_loader(:report).loadall loaded_instances(:report).sort { |a,b| a.to_s <=> b.to_s }.each do |name| mod = self.report(name) - docs += "#{name}\n#{"-" * name.to_s.length}\n" + docs << "#{name}\n#{"-" * name.to_s.length}\n" - docs += Puppet::Util::Docs.scrub(mod.doc) + "\n\n" + docs << Puppet::Util::Docs.scrub(mod.doc) << "\n\n" end docs diff --git a/lib/puppet/reports/rrdgraph.rb b/lib/puppet/reports/rrdgraph.rb index f283f7c27..e55d653c4 100644 --- a/lib/puppet/reports/rrdgraph.rb +++ b/lib/puppet/reports/rrdgraph.rb @@ -114,7 +114,7 @@ Puppet::Reports.register_report(:rrdgraph) do metric.graph end - mkhtml unless FileTest.exists?(File.join(hostdir, "index.html")) + mkhtml unless Puppet::FileSystem::File.exist?(File.join(hostdir, "index.html")) end # Unfortunately, RRD does not deal well with changing lists of values, diff --git a/lib/puppet/reports/store.rb b/lib/puppet/reports/store.rb index 7d318ffca..27d649355 100644 --- a/lib/puppet/reports/store.rb +++ b/lib/puppet/reports/store.rb @@ -17,7 +17,7 @@ Puppet::Reports.register_report(:store) do dir = File.join(Puppet[:reportdir], host) - if ! FileTest.exists?(dir) + if ! Puppet::FileSystem::File.exist?(dir) FileUtils.mkdir_p(dir) FileUtils.chmod_R(0750, dir) end @@ -54,11 +54,11 @@ Puppet::Reports.register_report(:store) do dir = File.join(Puppet[:reportdir], host) - if File.exists?(dir) + if Puppet::FileSystem::File.exist?(dir) Dir.entries(dir).each do |file| next if ['.','..'].include?(file) file = File.join(dir, file) - File.unlink(file) if File.file?(file) + Puppet::FileSystem::File.unlink(file) if File.file?(file) end Dir.rmdir(dir) end diff --git a/lib/puppet/reports/tagmail.rb b/lib/puppet/reports/tagmail.rb index 528b6fa38..a07f98986 100644 --- a/lib/puppet/reports/tagmail.rb +++ b/lib/puppet/reports/tagmail.rb @@ -108,7 +108,7 @@ Puppet::Reports.register_report(:tagmail) do # Process the report. This just calls the other associated messages. def process - unless FileTest.exists?(Puppet[:tagmap]) + unless Puppet::FileSystem::File.exist?(Puppet[:tagmap]) Puppet.notice "Cannot send tagmail report; no tagmap file #{Puppet[:tagmap]}" return end @@ -158,7 +158,7 @@ Puppet::Reports.register_report(:tagmail) do p.puts "From: #{Puppet[:reportfrom]}" p.puts "Subject: Puppet Report for #{self.host}" p.puts "To: " + emails.join(", ") - + p.puts p.puts messages end end diff --git a/lib/puppet/resource.rb b/lib/puppet/resource.rb index eae59f389..d4eac6f6d 100644 --- a/lib/puppet/resource.rb +++ b/lib/puppet/resource.rb @@ -56,7 +56,7 @@ class Puppet::Resource "#{@type}[#{@title}]#{to_hash.inspect}" end - def to_pson_data_hash + def to_data_hash data = ([:type, :title, :tags] + ATTRIBUTES).inject({}) do |hash, param| next hash unless value = self.send(param) hash[param.to_s] = value @@ -80,6 +80,11 @@ class Puppet::Resource data end + # This doesn't include document type as it is part of a catalog + def to_pson_data_hash + to_data_hash + end + def self.value_to_pson_data(value) if value.is_a? Array value.map{|v| value_to_pson_data(v) } @@ -102,8 +107,20 @@ class Puppet::Resource end end + YAML_ATTRIBUTES = [:@file, :@line, :@exported, :@type, :@title, :@tags, :@parameters] + + # Explicitly list the instance variables that should be serialized when + # converting to YAML. + # + # @api private + # @return [Array<Symbol>] The intersection of our explicit variable list and + # all of the instance variables defined on this class. + def to_yaml_properties + YAML_ATTRIBUTES & super + end + def to_pson(*args) - to_pson_data_hash.to_pson(*args) + to_data_hash.to_pson(*args) end # Proxy these methods to the parameters hash. It's likely they'll @@ -328,10 +345,7 @@ class Puppet::Resource result = scope.compiler.injector.lookup(scope, name) end if result.nil? - Puppet::DataBinding.indirection.find( - name, - :environment => scope.environment.to_s, - :variables => Puppet::DataBinding::Variables.new(scope)) + lookup_with_databinding(name, scope) else result end @@ -339,6 +353,19 @@ class Puppet::Resource private :lookup_external_default_for + def lookup_with_databinding(name, scope) + begin + Puppet::DataBinding.indirection.find( + name, + :environment => scope.environment.to_s, + :variables => scope) + rescue Puppet::DataBinding::LookupError => e + raise Puppet::Error.new("Error from DataBinding '#{Puppet[:data_binding_terminus]}' while looking up '#{name}': #{e.message}", e) + end + end + + private :lookup_with_databinding + def set_default_parameters(scope) return [] unless resource_type and resource_type.respond_to?(:arguments) @@ -362,8 +389,39 @@ class Puppet::Resource end.compact end - def to_resource - self + def copy_as_resource + result = Puppet::Resource.new(type, title) + + to_hash.each do |p, v| + if v.is_a?(Puppet::Resource) + v = Puppet::Resource.new(v.type, v.title) + elsif v.is_a?(Array) + # flatten resource references arrays + v = v.flatten if v.flatten.find { |av| av.is_a?(Puppet::Resource) } + v = v.collect do |av| + av = Puppet::Resource.new(av.type, av.title) if av.is_a?(Puppet::Resource) + av + end + end + + # If the value is an array with only one value, then + # convert it to a single value. This is largely so that + # the database interaction doesn't have to worry about + # whether it returns an array or a string. + result[p] = if v.is_a?(Array) and v.length == 1 + v[0] + else + v + end + end + + result.file = self.file + result.line = self.line + result.exported = self.exported + result.virtual = self.virtual + result.tag(*self.tags) + + result end def valid_parameter?(name) diff --git a/lib/puppet/resource/catalog.rb b/lib/puppet/resource/catalog.rb index 3120d3ff7..61b4f0b81 100644 --- a/lib/puppet/resource/catalog.rb +++ b/lib/puppet/resource/catalog.rb @@ -365,19 +365,23 @@ class Puppet::Resource::Catalog < Puppet::Graph::SimpleGraph result.add_edge(edge) end + def to_data_hash + { + 'tags' => tags, + 'name' => name, + 'version' => version, + 'environment' => environment.to_s, + 'resources' => @resources.collect { |v| @resource_table[v].to_pson_data_hash }, + 'edges' => edges. collect { |e| e.to_pson_data_hash }, + 'classes' => classes + } + end + PSON.register_document_type('Catalog',self) def to_pson_data_hash { 'document_type' => 'Catalog', - 'data' => { - 'tags' => tags, - 'name' => name, - 'version' => version, - 'environment' => environment.to_s, - 'resources' => @resources.collect { |v| @resource_table[v].to_pson_data_hash }, - 'edges' => edges. collect { |e| e.to_pson_data_hash }, - 'classes' => classes - }, + 'data' => to_data_hash, 'metadata' => { 'api_version' => 1 } @@ -465,7 +469,7 @@ class Puppet::Resource::Catalog < Puppet::Graph::SimpleGraph # Verify that the given resource isn't declared elsewhere. def fail_on_duplicate_type_and_title(resource) - # Short-curcuit the common case, + # Short-circuit the common case, return unless existing_resource = @resource_table[title_key_for_ref(resource.ref)] # If we've gotten this far, it's a real conflict @@ -492,22 +496,11 @@ class Puppet::Resource::Catalog < Puppet::Graph::SimpleGraph next if virtual_not_exported?(resource) next if block_given? and yield resource - #This is hackity hack for 1094 - #Aliases aren't working in the ral catalog because the current instance of the resource - #has a reference to the catalog being converted. . . So, give it a reference to the new one - #problem solved. . . - if resource.class == Puppet::Resource - resource = resource.dup - resource.catalog = result - elsif resource.is_a?(Puppet::Parser::Resource) - resource = resource.to_resource - resource.catalog = result - end + newres = resource.copy_as_resource + newres.catalog = result - if resource.is_a?(Puppet::Resource) and convert.to_s == "to_resource" - newres = resource - else - newres = resource.send(convert) + if convert != :to_resource + newres = newres.to_ral end # We can't guarantee that resources don't munge their names diff --git a/lib/puppet/resource/status.rb b/lib/puppet/resource/status.rb index 6bfe73b2b..eae5aeed9 100644 --- a/lib/puppet/resource/status.rb +++ b/lib/puppet/resource/status.rb @@ -1,10 +1,12 @@ require 'time' +require 'puppet/network/format_support' module Puppet class Resource class Status include Puppet::Util::Tagging include Puppet::Util::Logging + include Puppet::Network::FormatSupport attr_accessor :resource, :node, :file, :line, :current_values, :status, :evaluation_time @@ -65,7 +67,7 @@ module Puppet # will always be accompanied by an event with some explanatory power. This # is useful for reporting/diagnostics/etc. So synthesize an event here # with the exception detail as the message. - add_event(@real_resource.event(:status => "failure", :message => detail.to_s)) + add_event(@real_resource.event(:name => :resource_error, :status => "failure", :message => detail.to_s)) end def initialize(resource) @@ -100,7 +102,7 @@ module Puppet @evaluation_time = data['evaluation_time'] @change_count = data['change_count'] @out_of_sync_count = data['out_of_sync_count'] - @tags = data['tags'] + @tags = Puppet::Util::TagSet.new(data['tags']) @time = data['time'] @time = Time.parse(@time) if @time.is_a? String @out_of_sync = data['out_of_sync'] @@ -113,7 +115,7 @@ module Puppet end end - def to_pson + def to_data_hash { 'title' => @title, 'file' => @file, @@ -131,7 +133,11 @@ module Puppet 'change_count' => @change_count, 'out_of_sync_count' => @out_of_sync_count, 'events' => @events, - }.to_pson + } + end + + def to_pson(*args) + to_data_hash.to_pson(*args) end def to_yaml_properties diff --git a/lib/puppet/run.rb b/lib/puppet/run.rb index 762244119..1d89304be 100644 --- a/lib/puppet/run.rb +++ b/lib/puppet/run.rb @@ -94,11 +94,15 @@ class Puppet::Run new(options) end - def to_pson + def to_data_hash { :options => @options, :background => @background, :status => @status - }.to_pson + } + end + + def to_pson(*args) + to_data_hash.to_pson(*args) end end diff --git a/lib/puppet/settings.rb b/lib/puppet/settings.rb index b47039b66..ca017ae1e 100644 --- a/lib/puppet/settings.rb +++ b/lib/puppet/settings.rb @@ -1,5 +1,4 @@ require 'puppet' -require 'sync' require 'getoptlong' require 'puppet/util/watched_file' require 'puppet/util/command_line/puppet_option_parser' @@ -18,6 +17,8 @@ class Puppet::Settings require 'puppet/settings/boolean_setting' require 'puppet/settings/terminus_setting' require 'puppet/settings/duration_setting' + require 'puppet/settings/priority_setting' + require 'puppet/settings/autosign_setting' require 'puppet/settings/config_file' require 'puppet/settings/value_translator' @@ -74,9 +75,6 @@ class Puppet::Settings @created = [] @searchpath = nil - # Mutex-like thing to protect @values - @sync = Sync.new - # Keep track of set values. @values = Hash.new { |hash, key| hash[key] = {} } @@ -95,6 +93,12 @@ class Puppet::Settings @config_file_parser = Puppet::Settings::ConfigFile.new(@translate) end + # @param name [Symbol] The name of the setting to fetch + # @return [Puppet::Settings::BaseSetting] The setting object + def setting(name) + @config[name] + end + # Retrieve a config value def [](param) value(param) @@ -135,9 +139,7 @@ class Puppet::Settings # Remove all set values, potentially skipping cli values. def clear - @sync.synchronize do - unsafe_clear - end + unsafe_clear end # Remove all set values, potentially skipping cli values. @@ -170,9 +172,7 @@ class Puppet::Settings # this method must be called to clear out the caches so that updated # objects will be returned. def flush_cache - @sync.synchronize do - unsafe_flush_cache - end + unsafe_flush_cache end def unsafe_flush_cache @@ -507,9 +507,7 @@ class Puppet::Settings # Parse the configuration file. Just provides thread safety. def parse_config_files - @sync.synchronize do - unsafe_parse(which_configuration_file) - end + unsafe_parse(which_configuration_file) call_hooks_deferred_to_application_initialization :ignore_interpolation_dependency_errors => true end @@ -552,7 +550,7 @@ class Puppet::Settings def unsafe_parse(file) # build up a single data structure that contains the values from all of the parsed files. data = {} - if FileTest.exist?(file) + if Puppet::FileSystem::File.exist?(file) begin file_data = parse_file(file) @@ -602,7 +600,11 @@ class Puppet::Settings # This results in extra work, but so few of the settings # will have associated hooks that it ends up being less work this # way overall. - setting.handle(self.value(setting.name, env)) + if setting.call_hook_on_initialize? + @hooks_to_call_on_application_initialization << setting + else + setting.handle(self.value(setting.name, env)) + end break end end @@ -636,6 +638,8 @@ class Puppet::Settings :terminus => TerminusSetting, :duration => DurationSetting, :enum => EnumSetting, + :priority => PrioritySetting, + :autosign => AutosignSetting, } # Create a new setting. The value is passed in because it's used to determine @@ -692,7 +696,7 @@ class Puppet::Settings return @files if @files @files = [] [main_config_file, user_config_file].each do |path| - if FileTest.exist?(path) + if Puppet::FileSystem::File.exist?(path) @files << Puppet::Util::WatchedFile.new(path) end end @@ -713,11 +717,9 @@ class Puppet::Settings def reuse return unless defined?(@used) - @sync.synchronize do # yay, thread-safe - new = @used - @used = [] - self.use(*new) - end + new = @used + @used = [] + self.use(*new) end # The order in which to search for values. @@ -788,12 +790,8 @@ class Puppet::Settings setting.handle(value) if setting.has_hook? and not options[:dont_trigger_handles] - @sync.synchronize do # yay, thread-safe - - @values[type][param] = value - unsafe_flush_cache - - end + @values[type][param] = value + unsafe_flush_cache value end @@ -839,7 +837,7 @@ class Puppet::Settings def define_settings(section, defs) section = section.to_sym call = [] - defs.each { |name, hash| + defs.each do |name, hash| raise ArgumentError, "setting definition for '#{name}' is not a hash!" unless hash.is_a? Hash name = name.to_sym @@ -858,9 +856,12 @@ class Puppet::Settings # Collect the settings that need to have their hooks called immediately. # We have to collect them so that we can be sure we're fully initialized before # the hook is called. - call << tryconfig if tryconfig.call_hook_on_define? - @hooks_to_call_on_application_initialization << tryconfig if tryconfig.call_hook_on_initialize? - } + if tryconfig.call_hook_on_define? + call << tryconfig + elsif tryconfig.call_hook_on_initialize? + @hooks_to_call_on_application_initialization << tryconfig + end + end call.each { |setting| setting.handle(self.value(setting.name)) } end @@ -927,29 +928,27 @@ Generated on #{Time.now}. # you can 'use' a section as many times as you want. def use(*sections) sections = sections.collect { |s| s.to_sym } - @sync.synchronize do # yay, thread-safe - sections = sections.reject { |s| @used.include?(s) } + sections = sections.reject { |s| @used.include?(s) } - return if sections.empty? + return if sections.empty? - begin - catalog = to_catalog(*sections).to_ral - rescue => detail - Puppet.log_and_raise(detail, "Could not create resources for managing Puppet's files and directories in sections #{sections.inspect}: #{detail}") - end + begin + catalog = to_catalog(*sections).to_ral + rescue => detail + Puppet.log_and_raise(detail, "Could not create resources for managing Puppet's files and directories in sections #{sections.inspect}: #{detail}") + end - catalog.host_config = false - catalog.apply do |transaction| - if transaction.any_failed? - report = transaction.report - failures = report.logs.find_all { |log| log.level == :err } - raise "Got #{failures.length} failure(s) while initializing: #{failures.collect { |l| l.to_s }.join("; ")}" - end + catalog.host_config = false + catalog.apply do |transaction| + if transaction.any_failed? + report = transaction.report + failures = report.logs.find_all { |log| log.level == :err } + raise "Got #{failures.length} failure(s) while initializing: #{failures.collect { |l| l.to_s }.join("; ")}" end - - sections.each { |s| @used << s } - @used.uniq! end + + sections.each { |s| @used << s } + @used.uniq! end def valid?(param) @@ -974,9 +973,7 @@ Generated on #{Time.now}. each_source(environment) do |source| # Look for the value. We have to test the hash for whether # it exists, because the value might be false. - @sync.synchronize do - return @values[source][param] if @values[source].include?(param) - end + return @values[source][param] if @values[source].include?(param) end return nil end @@ -1033,70 +1030,6 @@ Generated on #{Time.now}. val end - # Open a file with the appropriate user, group, and mode - def write(default, *args, &bloc) - obj = get_config_file_default(default) - writesub(default, value(obj.name), *args, &bloc) - end - - # Open a non-default file under a default dir with the appropriate user, - # group, and mode - def writesub(default, file, *args, &bloc) - obj = get_config_file_default(default) - chown = nil - if Puppet.features.root? - chown = [obj.owner, obj.group] - else - chown = [nil, nil] - end - - Puppet::Util::SUIDManager.asuser(*chown) do - mode = obj.mode ? obj.mode.to_i : 0640 - args << "w" if args.empty? - - args << mode - - # Update the umask to make non-executable files - Puppet::Util.withumask(File.umask ^ 0111) do - File.open(file, *args) do |file| - yield file - end - end - end - end - - def readwritelock(default, *args, &bloc) - file = value(get_config_file_default(default).name) - tmpfile = file + ".tmp" - sync = Sync.new - raise Puppet::DevError, "Cannot create #{file}; directory #{File.dirname(file)} does not exist" unless FileTest.directory?(File.dirname(tmpfile)) - - sync.synchronize(Sync::EX) do - File.open(file, ::File::CREAT|::File::RDWR, 0600) do |rf| - rf.lock_exclusive do - if File.exist?(tmpfile) - raise Puppet::Error, ".tmp file already exists for #{file}; Aborting locked write. Check the .tmp file and delete if appropriate" - end - - # If there's a failure, remove our tmpfile - begin - writesub(default, tmpfile, *args, &bloc) - rescue - File.unlink(tmpfile) if FileTest.exist?(tmpfile) - raise - end - - begin - File.rename(tmpfile, file) - rescue => detail - Puppet.err "Could not rename #{file} to #{tmpfile}: #{detail}" - File.unlink(tmpfile) if FileTest.exist?(tmpfile) - end - end - end - end - end - private def get_config_file_default(default) @@ -1166,9 +1099,7 @@ Generated on #{Time.now}. def set_metadata(meta) meta.each do |var, values| values.each do |param, value| - @sync.synchronize do # yay, thread-safe - @config[var].send(param.to_s + "=", value) - end + @config[var].send(param.to_s + "=", value) end end end @@ -1177,11 +1108,9 @@ Generated on #{Time.now}. # # @return nil def clear_everything_for_tests() - @sync.synchronize do - unsafe_clear(true, true) - @global_defaults_initialized = false - @app_defaults_initialized = false - end + unsafe_clear(true, true) + @global_defaults_initialized = false + @app_defaults_initialized = false end private :clear_everything_for_tests diff --git a/lib/puppet/settings/autosign_setting.rb b/lib/puppet/settings/autosign_setting.rb new file mode 100644 index 000000000..f13824ecf --- /dev/null +++ b/lib/puppet/settings/autosign_setting.rb @@ -0,0 +1,20 @@ +require 'puppet/settings/base_setting' + +class Puppet::Settings::AutosignSetting < Puppet::Settings::BaseSetting + + def type + :autosign + end + + def munge(value) + if ['true', true].include? value + true + elsif ['false', false, nil].include? value + false + elsif Puppet::Util.absolute_path?(value) + value + else + raise Puppet::Settings::ValidationError, "Invalid autosign value #{value}: must be 'true'/'false' or an absolute path" + end + end +end diff --git a/lib/puppet/settings/base_setting.rb b/lib/puppet/settings/base_setting.rb index 92c904e90..ac38b35f3 100644 --- a/lib/puppet/settings/base_setting.rb +++ b/lib/puppet/settings/base_setting.rb @@ -2,17 +2,13 @@ require 'puppet/settings/errors' # The base setting type class Puppet::Settings::BaseSetting - attr_accessor :name, :section, :default, :call_on_define, :call_hook - attr_reader :desc, :short + attr_accessor :name, :desc, :section, :default, :call_on_define, :call_hook + attr_reader :short def self.available_call_hook_values [:on_define_and_write, :on_initialize_and_write, :on_write_only] end - def desc=(value) - @desc = value.gsub(/^\s*/, '') - end - def call_on_define Puppet.deprecation_warning "call_on_define has been deprecated. Please use call_hook_on_define?" call_hook_on_define? @@ -130,10 +126,12 @@ class Puppet::Settings::BaseSetting # Convert the object to a config statement. def to_config - str = @desc.gsub(/^/, "# ") + "\n" + require 'puppet/util/docs' + # Scrub any funky indentation; comment out description. + str = Puppet::Util::Docs.scrub(@desc).gsub(/^/, "# ") + "\n" # Add in a statement about the default. - str += "# The default value is '#{default(true)}'.\n" if default(true) + str << "# The default value is '#{default(true)}'.\n" if default(true) # If the value has not been overridden, then print it out commented # and unconverted, so it's clear that that's the default and how it @@ -146,8 +144,9 @@ class Puppet::Settings::BaseSetting line = "# #{@name} = #{@default}" end - str += line + "\n" + str << (line + "\n") + # Indent str.gsub(/^/, " ") end diff --git a/lib/puppet/settings/directory_setting.rb b/lib/puppet/settings/directory_setting.rb index 2a9472f92..9f67f96ab 100644 --- a/lib/puppet/settings/directory_setting.rb +++ b/lib/puppet/settings/directory_setting.rb @@ -2,4 +2,12 @@ class Puppet::Settings::DirectorySetting < Puppet::Settings::FileSetting def type :directory end + + # @api private + def open_file(filename, option = 'r', &block) + file = Puppet::FileSystem::File.new(filename) + controlled_access do |mode| + file.open(mode, option, &block) + end + end end diff --git a/lib/puppet/settings/file_setting.rb b/lib/puppet/settings/file_setting.rb index 265bc0613..243805edb 100644 --- a/lib/puppet/settings/file_setting.rb +++ b/lib/puppet/settings/file_setting.rb @@ -128,7 +128,7 @@ class Puppet::Settings::FileSetting < Puppet::Settings::StringSetting # Make sure the paths are fully qualified. path = File.expand_path(path) - return nil unless type == :directory or create_files? or File.exist?(path) + return nil unless type == :directory or create_files? or Puppet::FileSystem::File.exist?(path) return nil if path =~ /^\/dev/ or path =~ /^[A-Z]:\/dev/i resource = Puppet::Resource.new(:file, path) @@ -178,8 +178,42 @@ class Puppet::Settings::FileSetting < Puppet::Settings::StringSetting } end + # @api private + def exclusive_open(option = 'r', &block) + controlled_access do |mode| + file.exclusive_open(mode, option, &block) + end + end + + # @api private + def open(option = 'r', &block) + controlled_access do |mode| + file.open(mode, option, &block) + end + end + private + def file + Puppet::FileSystem::File.new(value) + end + def unknown_value(parameter, value) raise SettingError, "The #{parameter} parameter for the setting '#{name}' must be either 'root' or 'service', not '#{value}'" end + + def controlled_access(&block) + chown = nil + if Puppet.features.root? + chown = [owner, group] + else + chown = [nil, nil] + end + + Puppet::Util::SUIDManager.asuser(*chown) do + # Update the umask to make non-executable files + Puppet::Util.withumask(File.umask ^ 0111) do + yield mode ? mode.to_i : 0640 + end + end + end end diff --git a/lib/puppet/settings/priority_setting.rb b/lib/puppet/settings/priority_setting.rb new file mode 100644 index 000000000..707b8ab82 --- /dev/null +++ b/lib/puppet/settings/priority_setting.rb @@ -0,0 +1,42 @@ +require 'puppet/settings/base_setting' + +# A setting that represents a scheduling priority, and evaluates to an +# OS-specific priority level. +class Puppet::Settings::PrioritySetting < Puppet::Settings::BaseSetting + PRIORITY_MAP = + if Puppet::Util::Platform.windows? + require 'win32/process' + { + :high => Process::HIGH_PRIORITY_CLASS, + :normal => Process::NORMAL_PRIORITY_CLASS, + :low => Process::BELOW_NORMAL_PRIORITY_CLASS, + :idle => Process::IDLE_PRIORITY_CLASS + } + else + { + :high => -10, + :normal => 0, + :low => 10, + :idle => 19 + } + end + + def type + :priority + end + + def munge(value) + return unless value + + case + when value.is_a?(Integer) + value + when (value.is_a?(String) and value =~ /\d+/) + value.to_i + when (value.is_a?(String) and PRIORITY_MAP[value.to_sym]) + PRIORITY_MAP[value.to_sym] + else + raise Puppet::Settings::ValidationError, "Invalid priority format '#{value.inspect}' for parameter: #{@name}" + end + end +end diff --git a/lib/puppet/ssl.rb b/lib/puppet/ssl.rb index 8f71ba8a4..596feb933 100644 --- a/lib/puppet/ssl.rb +++ b/lib/puppet/ssl.rb @@ -5,4 +5,8 @@ require 'openssl' module Puppet::SSL # :nodoc: CA_NAME = "ca" require 'puppet/ssl/host' + require 'puppet/ssl/oids' + require 'puppet/ssl/validator' + require 'puppet/ssl/validator/no_validator' + require 'puppet/ssl/validator/default_validator' end diff --git a/lib/puppet/ssl/certificate.rb b/lib/puppet/ssl/certificate.rb index a49267ac7..a8ed118fa 100644 --- a/lib/puppet/ssl/certificate.rb +++ b/lib/puppet/ssl/certificate.rb @@ -44,4 +44,22 @@ DOC def unmunged_name self.class.name_from_subject(content.subject) end + + # Any extensions registered with custom OIDs as defined in module + # Puppet::SSL::Oids may be looked up here. + # + # A cert with a 'pp_uuid' extension having the value 'abcd' would return: + # + # [{ 'oid' => 'pp_uuid', 'value' => 'abcd'}] + # + # @return [Array<Hash{String => String}>] An array of two element hashes, + # with key/value pairs for the extension's oid, and its value. + def custom_extensions + custom_exts = content.extensions.select do |ext| + Puppet::SSL::Oids.subtree_of?('ppRegCertExt', ext.oid) or + Puppet::SSL::Oids.subtree_of?('ppPrivCertExt', ext.oid) + end + + custom_exts.map { |ext| {'oid' => ext.oid, 'value' => ext.value} } + end end diff --git a/lib/puppet/ssl/certificate_authority.rb b/lib/puppet/ssl/certificate_authority.rb index 83ff6a114..3ff4d5eb5 100644 --- a/lib/puppet/ssl/certificate_authority.rb +++ b/lib/puppet/ssl/certificate_authority.rb @@ -1,4 +1,3 @@ -require 'monitor' require 'puppet/ssl/host' require 'puppet/ssl/certificate_request' require 'puppet/ssl/certificate_signer' @@ -26,10 +25,9 @@ class Puppet::SSL::CertificateAuthority require 'puppet/ssl/inventory' require 'puppet/ssl/certificate_revocation_list' require 'puppet/ssl/certificate_authority/interface' + require 'puppet/ssl/certificate_authority/autosign_command' require 'puppet/network/authstore' - extend MonitorMixin - class CertificateVerificationError < RuntimeError attr_accessor :error_code @@ -39,9 +37,7 @@ class Puppet::SSL::CertificateAuthority end def self.singleton_instance - synchronize do - @singleton_instance ||= new - end + @singleton_instance ||= new end class CertificateSigningError < RuntimeError @@ -53,67 +49,56 @@ class Puppet::SSL::CertificateAuthority end def self.ca? - return false unless Puppet[:ca] - return false unless Puppet.run_mode.master? - true + # running as ca? - ensure boolean answer + !!(Puppet[:ca] && Puppet.run_mode.master?) end - # If this process can function as a CA, then return a singleton - # instance. + # If this process can function as a CA, then return a singleton instance. def self.instance - return nil unless ca? - - singleton_instance + ca? ? singleton_instance : nil end attr_reader :name, :host - # Create and run an applicator. I wanted to build an interface where you could do - # something like 'ca.apply(:generate).to(:all) but I don't think it's really possible. - def apply(method, options) - raise ArgumentError, "You must specify the hosts to apply to; valid values are an array or the symbol :all" unless options[:to] - applier = Interface.new(method, options) - applier.apply(self) - end - - # If autosign is configured, then autosign all CSRs that match our configuration. - def autosign - return unless auto = autosign? - - store = nil - store = autosign_store(auto) if auto != true - - Puppet::SSL::CertificateRequest.indirection.search("*").each do |csr| - if auto == true or store.allowed?(csr.name, "127.1.1.1") - Puppet.info "Autosigning #{csr.name}" - sign(csr.name) - end + # If autosign is configured, autosign the csr we are passed. + # @param csr [Puppet::SSL::CertificateRequest] The csr to sign. + # @return [Void] + # @api private + def autosign(csr) + if autosign?(csr) + Puppet.info "Autosigning #{csr.name}" + sign(csr.name) end end - # Do we autosign? This returns true, false, or a filename. - def autosign? + # Determine if a CSR can be autosigned by the autosign store or autosign command + # + # @param csr [Puppet::SSL::CertificateRequest] The CSR to check + # @return [true, false] + # @api private + def autosign?(csr) auto = Puppet[:autosign] - return false if ['false', false].include?(auto) - return true if ['true', true].include?(auto) - raise ArgumentError, "The autosign configuration '#{auto}' must be a fully qualified file" unless Puppet::Util.absolute_path?(auto) - FileTest.exist?(auto) && auto - end - - # Create an AuthStore for autosigning. - def autosign_store(file) - auth = Puppet::Network::AuthStore.new - File.readlines(file).each do |line| - next if line =~ /^\s*#/ - next if line =~ /^\s*$/ - auth.allow(line.chomp) - end + decider = case auto + when 'false', false, nil + AutosignNever.new + when 'true', true + AutosignAlways.new + else + file = Puppet::FileSystem::File.new(auto) + if file.executable? + Puppet::SSL::CertificateAuthority::AutosignCommand.new(auto) + elsif file.exist? + AutosignConfig.new(file) + else + AutosignNever.new + end + end - auth + decider.allowed?(csr) end - # Retrieve (or create, if necessary) the certificate revocation list. + # Retrieves (or creates, if necessary) the certificate revocation list. def crl unless defined?(@crl) unless @crl = Puppet::SSL::CertificateRevocationList.indirection.find(Puppet::SSL::CA_NAME) @@ -125,12 +110,12 @@ class Puppet::SSL::CertificateAuthority @crl end - # Delegate this to our Host class. + # Delegates this to our Host class. def destroy(name) Puppet::SSL::Host.destroy(name) end - # Generate a new certificate. + # Generates a new certificate. # @return Puppet::SSL::Certificate def generate(name, options = {}) raise ArgumentError, "A Certificate already exists for #{name}" if Puppet::SSL::Certificate.indirection.find(name) @@ -187,7 +172,7 @@ class Puppet::SSL::CertificateAuthority 20.times { pass += (rand(74) + 48).chr } begin - Puppet.settings.write(:capass) { |f| f.print pass } + Puppet.settings.setting(:capass).open('w') { |f| f.print pass } rescue Errno::EACCES => detail raise Puppet::Error, "Could not write CA password: #{detail}" end @@ -222,26 +207,27 @@ class Puppet::SSL::CertificateAuthority # Read the next serial from the serial file, and increment the # file so this one is considered used. def next_serial - serial = nil - - # This is slightly odd. If the file doesn't exist, our readwritelock creates - # it, but with a mode we can't actually read in some cases. So, use - # a default before the lock. - serial = 0x1 unless FileTest.exist?(Puppet[:serial]) + serial = 1 + Puppet.settings.setting(:serial).exclusive_open('a+') do |f| + f.rewind + serial = f.read.chomp.hex + if serial == 0 + serial = 1 + end - Puppet.settings.readwritelock(:serial) { |f| - serial ||= File.read(Puppet.settings[:serial]).chomp.hex if FileTest.exist?(Puppet[:serial]) + f.truncate(0) + f.rewind # We store the next valid serial, not the one we just used. f << "%04X" % (serial + 1) - } + end serial end # Does the password file exist? def password? - FileTest.exist? Puppet[:capass] + Puppet::FileSystem::File.exist? Puppet[:capass] end # Print a given host's certificate as text. @@ -323,8 +309,11 @@ class Puppet::SSL::CertificateAuthority def check_internal_signing_policies(hostname, csr, allow_dns_alt_names) # Reject unknown request extensions. - unknown_req = csr.request_extensions. - reject {|x| RequestExtensionWhitelist.include? x["oid"] } + unknown_req = csr.request_extensions.reject do |x| + RequestExtensionWhitelist.include? x["oid"] or + Puppet::SSL::Oids.subtree_of?('ppRegCertExt', x["oid"], true) or + Puppet::SSL::Oids.subtree_of?('ppPrivCertExt', x["oid"], true) + end if unknown_req and not unknown_req.empty? names = unknown_req.map {|x| x["oid"] }.sort.uniq.join(", ") @@ -393,7 +382,7 @@ class Puppet::SSL::CertificateAuthority # # @return [OpenSSL::X509::Store] def x509_store(options = {}) - if (options[:cache]) + if (options[:cache]) return @x509store unless @x509store.nil? @x509store = create_x509_store else @@ -407,11 +396,13 @@ class Puppet::SSL::CertificateAuthority # # @return [OpenSSL::X509::Store] def create_x509_store - store = OpenSSL::X509::Store.new - store.add_file Puppet[:cacert] - store.add_crl crl.content if self.crl + store = OpenSSL::X509::Store.new() + store.add_file(Puppet[:cacert]) + store.add_crl(crl.content) if self.crl store.purpose = OpenSSL::X509::PURPOSE_SSL_CLIENT - store.flags = OpenSSL::X509::V_FLAG_CRL_CHECK_ALL|OpenSSL::X509::V_FLAG_CRL_CHECK if Puppet.settings[:certificate_revocation] + if Puppet.settings[:certificate_revocation] + store.flags = OpenSSL::X509::V_FLAG_CRL_CHECK_ALL | OpenSSL::X509::V_FLAG_CRL_CHECK + end store end private :create_x509_store @@ -472,4 +463,42 @@ class Puppet::SSL::CertificateAuthority def waiting? Puppet::SSL::CertificateRequest.indirection.search("*").collect { |r| r.name } end + + # @api private + class AutosignAlways + def allowed?(csr) + true + end + end + + # @api private + class AutosignNever + def allowed?(csr) + false + end + end + + # @api private + class AutosignConfig + def initialize(config_file) + @config = config_file + end + + def allowed?(csr) + autosign_store.allowed?(csr.name, '127.1.1.1') + end + + private + + def autosign_store + auth = Puppet::Network::AuthStore.new + @config.each_line do |line| + next if line =~ /^\s*#/ + next if line =~ /^\s*$/ + auth.allow(line.chomp) + end + + auth + end + end end diff --git a/lib/puppet/ssl/certificate_authority/autosign_command.rb b/lib/puppet/ssl/certificate_authority/autosign_command.rb new file mode 100644 index 000000000..52c066555 --- /dev/null +++ b/lib/puppet/ssl/certificate_authority/autosign_command.rb @@ -0,0 +1,44 @@ +require 'puppet/ssl/certificate_authority' + +# This class wraps a given command and invokes it with a CSR name and body to +# determine if the given CSR should be autosigned +# +# @api private +class Puppet::SSL::CertificateAuthority::AutosignCommand + + class CheckFailure < Puppet::Error; end + + def initialize(path) + @path = path + end + + # Run the autosign command with the given CSR name as an argument and the + # CSR body on stdin. + # + # @param name [String] The CSR name to check for autosigning + # @return [true, false] If the CSR should be autosigned + def allowed?(csr) + name = csr.name + cmd = [@path, name] + + output = Puppet::FileSystem::Tempfile.open('puppet-csr') do |csr_file| + csr_file.write(csr.to_s) + csr_file.flush + + execute_options = {:stdinfile => csr_file.path, :combine => true, :failonfail => false} + Puppet::Util::Execution.execute(cmd, execute_options) + end + + output.chomp! + + Puppet.debug "Autosign command '#{@path}' exit status: #{output.exitstatus}" + Puppet.debug "Autosign command '#{@path}' output: #{output}" + + case output.exitstatus + when 0 + true + else + false + end + end +end diff --git a/lib/puppet/ssl/certificate_authority/interface.rb b/lib/puppet/ssl/certificate_authority/interface.rb index 4e7a14ac8..b68368b8d 100644 --- a/lib/puppet/ssl/certificate_authority/interface.rb +++ b/lib/puppet/ssl/certificate_authority/interface.rb @@ -4,7 +4,9 @@ module Puppet # This class is basically a hidden class that knows how to act on the # CA. Its job is to provide a CLI-like interface to the CA class. class Interface - INTERFACE_METHODS = [:destroy, :list, :revoke, :generate, :sign, :print, :verify, :fingerprint] + INTERFACE_METHODS = [:destroy, :list, :revoke, :generate, :sign, :print, :verify, :fingerprint, :reinventory] + + SUBJECTLESS_METHODS = [:list, :reinventory] class InterfaceError < ArgumentError; end @@ -12,14 +14,17 @@ module Puppet # Actually perform the work. def apply(ca) - unless subjects or method == :list + unless subjects || SUBJECTLESS_METHODS.include?(method) raise ArgumentError, "You must provide hosts or --all when using #{method}" end - return send(method, ca) if respond_to?(method) - - (subjects == :all ? ca.list : subjects).each do |host| - ca.send(method, host) + # if the interface implements the method, use it instead of the ca's method + if respond_to?(method) + send(method, ca) + else + (subjects == :all ? ca.list : subjects).each do |host| + ca.send(method, host) + end end end @@ -66,14 +71,11 @@ module Puppet end if verify_error - cert = Puppet::SSL::Certificate.indirection.find(host) - certs[:invalid][host] = [cert, verify_error] + certs[:invalid][host] = [ Puppet::SSL::Certificate.indirection.find(host), verify_error ] elsif signed.include?(host) - cert = Puppet::SSL::Certificate.indirection.find(host) - certs[:signed][host] = cert + certs[:signed][host] = Puppet::SSL::Certificate.indirection.find(host) else - req = Puppet::SSL::CertificateRequest.indirection.find(host) - certs[:request][host] = req + certs[:request][host] = Puppet::SSL::CertificateRequest.indirection.find(host) end end @@ -147,7 +149,7 @@ module Puppet end end - # Sign a given certificate. + # Signs given certificates or waiting of subjects == :all def sign(ca) list = subjects == :all ? ca.waiting? : subjects raise InterfaceError, "No waiting certificate requests to sign" if list.empty? @@ -156,15 +158,17 @@ module Puppet end end + def reinventory(ca) + ca.inventory.rebuild + end + # Set the list of hosts we're operating on. Also supports keywords. def subjects=(value) - unless value == :all or value == :signed or value.is_a?(Array) + unless value == :all || value == :signed || value.is_a?(Array) raise ArgumentError, "Subjects must be an array or :all; not #{value}" end - value = nil if value.is_a?(Array) and value.empty? - - @subjects = value + @subjects = (value == []) ? nil : value end end end diff --git a/lib/puppet/ssl/certificate_factory.rb b/lib/puppet/ssl/certificate_factory.rb index 1e628ed83..c6d01026c 100644 --- a/lib/puppet/ssl/certificate_factory.rb +++ b/lib/puppet/ssl/certificate_factory.rb @@ -66,17 +66,7 @@ module Puppet::SSL::CertificateFactory inject({}) {|ret, val| ret.merge(val) } cert.extensions = exts.map do |oid, val| - val, crit = *val - val = val.join(', ') unless val.is_a? String - - # Enforce the X509v3 rules about subjectAltName being critical: - # specifically, it SHOULD NOT be critical if we have a subject, which we - # always do. --daniel 2011-10-18 - crit = false if oid == "subjectAltName" - - # val can be either a string, or [string, critical], and this does the - # right thing regardless of what we get passed. - ef.create_ext(oid, val, crit) + generate_extension(ef, oid, *val) end end @@ -144,5 +134,41 @@ module Puppet::SSL::CertificateFactory "nsCertType" => "client,email", } end -end + # Generate an extension with the given OID, value, and critical state + # + # @param oid [String] The numeric value or short name of a given OID. X509v3 + # extensions must be passed by short name or long name, while custom + # extensions may be passed by short name, long name, oid numeric OID. + # @param ef [OpenSSL::X509::ExtensionFactory] The extension factory to use + # when generating the extension. + # @param val [String, Array<String>] The extension value. + # @param crit [true, false] Whether the given extension is critical, defaults + # to false. + # + # @return [OpenSSL::X509::Extension] + # + # @api private + def self.generate_extension(ef, oid, val, crit = false) + + val = val.join(', ') unless val.is_a? String + + # Enforce the X509v3 rules about subjectAltName being critical: + # specifically, it SHOULD NOT be critical if we have a subject, which we + # always do. --daniel 2011-10-18 + crit = false if oid == "subjectAltName" + + if Puppet::SSL::Oids.subtree_of?('id-ce', oid) or Puppet::SSL::Oids.subtree_of?('id-pkix', oid) + # Attempt to create a X509v3 certificate extension. Standard certificate + # extensions may need access to the associated subject certificate and + # issuing certificate, so must be created by the OpenSSL::X509::ExtensionFactory + # which provides that context. + ef.create_ext(oid, val, crit) + else + # This is not an X509v3 extension which means that the extension + # factory cannot generate it. We need to generate the extension + # manually. + OpenSSL::X509::Extension.new(oid, val, crit) + end + end +end diff --git a/lib/puppet/ssl/certificate_request.rb b/lib/puppet/ssl/certificate_request.rb index cc27eebbe..cd68eb34a 100644 --- a/lib/puppet/ssl/certificate_request.rb +++ b/lib/puppet/ssl/certificate_request.rb @@ -1,7 +1,30 @@ require 'puppet/ssl/base' require 'puppet/ssl/certificate_signer' -# Manage certificate requests. +# This class creates and manages X509 certificate signing requests. +# +# ## CSR attributes +# +# CSRs may contain a set of attributes that includes supplementary information +# about the CSR or information for the signed certificate. +# +# PKCS#9/RFC 2985 section 5.4 formally defines the "Challenge password", +# "Extension request", and "Extended-certificate attributes", but this +# implementation only handles the "Extension request" attribute. Other +# attributes may be defined on a CSR, but the RFC doesn't define behavior for +# any other attributes so we treat them as only informational. +# +# ## CSR Extension request attribute +# +# CSRs may contain an optional set of extension requests, which allow CSRs to +# include additional information that may be included in the signed +# certificate. Any additional information that should be copied from the CSR +# to the signed certificate MUST be included in this attribute. +# +# This behavior is dictated by PKCS#9/RFC 2985 section 5.4.2. +# +# @see http://tools.ietf.org/html/rfc2985 "RFC 2985 Section 5.4.2 Extension request" +# class Puppet::SSL::CertificateRequest < Puppet::SSL::Base wraps OpenSSL::X509::Request @@ -14,7 +37,7 @@ class Puppet::SSL::CertificateRequest < Puppet::SSL::Base # Try to autosign the CSR. if ca = Puppet::SSL::CertificateAuthority.instance - ca.autosign + ca.autosign(instance) end end end @@ -34,7 +57,22 @@ DOC @ef ||= OpenSSL::X509::ExtensionFactory.new end - # How to create a certificate request with our system defaults. + # Create a certificate request with our system settings. + # + # @param key [OpenSSL::X509::Key, Puppet::SSL::Key] The key pair associated + # with this CSR. + # @param opts [Hash] + # @options opts [String] :dns_alt_names A comma separated list of + # Subject Alternative Names to include in the CSR extension request. + # @options opts [Hash<String, String, Array<String>>] :csr_attributes A hash + # of OIDs and values that are either a string or array of strings. + # @options opts [Array<String, String>] :extension_requests A hash of + # certificate extensions to add to the CSR extReq attribute, excluding + # the Subject Alternative Names extension. + # + # @raise [Puppet::Error] If the generated CSR signature couldn't be verified + # + # @return [OpenSSL::X509::Request] The generated CSR def generate(key, options = {}) Puppet.info "Creating a new SSL certificate request for #{name}" @@ -51,16 +89,12 @@ DOC csr.subject = OpenSSL::X509::Name.new([["CN", common_name]]) csr.public_key = key.public_key - if options[:dns_alt_names] then - names = options[:dns_alt_names].split(/\s*,\s*/).map(&:strip) + [name] - names = names.sort.uniq.map {|name| "DNS:#{name}" }.join(", ") - names = extension_factory.create_extension("subjectAltName", names, false) - - extReq = OpenSSL::ASN1::Set([OpenSSL::ASN1::Sequence([names])]) + if options[:csr_attributes] + add_csr_attributes(csr, options[:csr_attributes]) + end - # We only support the standard request extensions. If you really need - # msExtReq support, let us know and we can restore them. --daniel 2011-10-10 - csr.add_attribute(OpenSSL::X509::Attribute.new("extReq", extReq)) + if (ext_req_attribute = extension_request_attribute(options)) + csr.add_attribute(ext_req_attribute) end signer = Puppet::SSL::CertificateSigner.new @@ -74,72 +108,192 @@ DOC end # Return the set of extensions requested on this CSR, in a form designed to - # be useful to Ruby: a hash. Which, not coincidentally, you can pass + # be useful to Ruby: an array of hashes. Which, not coincidentally, you can pass # successfully to the OpenSSL constructor later, if you want. + # + # @return [Array<Hash{String => String}>] An array of two or three element + # hashes, with key/value pairs for the extension's oid, its value, and + # optionally its critical state. def request_extensions raise Puppet::Error, "CSR needs content to extract fields" unless @content # Prefer the standard extReq, but accept the Microsoft specific version as # a fallback, if the standard version isn't found. - ext = @content.attributes.find {|x| x.oid == "extReq" } or - @content.attributes.find {|x| x.oid == "msExtReq" } - return [] unless ext - - # Assert the structure and extract the names into an array of arrays. - unless ext.value.is_a? OpenSSL::ASN1::Set - raise Puppet::Error, "In #{ext.oid}, expected Set but found #{ext.value.class}" - end - - unless ext.value.value.is_a? Array - raise Puppet::Error, "In #{ext.oid}, expected Set[Array] but found #{ext.value.value.class}" - end - - unless ext.value.value.length == 1 - raise Puppet::Error, "In #{ext.oid}, expected Set[Array[...]], but found #{ext.value.value.length} items in the array" - end + attribute = @content.attributes.find {|x| x.oid == "extReq" } + attribute ||= @content.attributes.find {|x| x.oid == "msExtReq" } + return [] unless attribute - san = ext.value.value.first - unless san.is_a? OpenSSL::ASN1::Sequence - raise Puppet::Error, "In #{ext.oid}, expected Set[Array[Sequence[...]]], but found #{san.class}" - end - san = san.value + extensions = unpack_extension_request(attribute) - # OK, now san should be the array of items, validate that... index = -1 - san.map do |name| + extensions.map do |ext_values| index += 1 - - unless name.is_a? OpenSSL::ASN1::Sequence - raise Puppet::Error, "In #{ext.oid}, expected request extension record #{index} to be a Sequence, but found #{name.class}" - end - name = name.value + context = "#{attribute.oid} extension index #{index}" # OK, turn that into an extension, to unpack the content. Lovely that # we have to swap the order of arguments to the underlying method, or # perhaps that the ASN.1 representation chose to pack them in a # strange order where the optional component comes *earlier* than the # fixed component in the sequence. - case name.length + case ext_values.length when 2 - ev = OpenSSL::X509::Extension.new(name[0].value, name[1].value) + ev = OpenSSL::X509::Extension.new(ext_values[0].value, ext_values[1].value) { "oid" => ev.oid, "value" => ev.value } when 3 - ev = OpenSSL::X509::Extension.new(name[0].value, name[2].value, name[1].value) + ev = OpenSSL::X509::Extension.new(ext_values[0].value, ext_values[2].value, ext_values[1].value) { "oid" => ev.oid, "value" => ev.value, "critical" => ev.critical? } else - raise Puppet::Error, "In #{ext.oid}, expected extension record #{index} to have two or three items, but found #{name.length}" + raise Puppet::Error, "In #{attribute.oid}, expected extension record #{index} to have two or three items, but found #{ext_values.length}" end - end.flatten + end end def subject_alt_names @subject_alt_names ||= request_extensions. - select {|x| x["oid"] = "subjectAltName" }. + select {|x| x["oid"] == "subjectAltName" }. map {|x| x["value"].split(/\s*,\s*/) }. flatten. sort. uniq end + + # Return all user specified attributes attached to this CSR as a hash. IF an + # OID has a single value it is returned as a string, otherwise all values are + # returned as an array. + # + # The format of CSR attributes is specified in PKCS#10/RFC 2986 + # + # @see http://tools.ietf.org/html/rfc2986 "RFC 2986 Certification Request Syntax Specification" + # + # @api public + # + # @return [Hash<String, String>] + def custom_attributes + x509_attributes = @content.attributes.reject do |attr| + PRIVATE_CSR_ATTRIBUTES.include? attr.oid + end + + x509_attributes.map do |attr| + {"oid" => attr.oid, "value" => attr.value.first.value} + end + end + + private + + # Exclude OIDs that may conflict with how Puppet creates CSRs. + # + # We only have nominal support for Microsoft extension requests, but since we + # ultimately respect that field when looking for extension requests in a CSR + # we need to prevent that field from being written to directly. + PRIVATE_CSR_ATTRIBUTES = [ + 'extReq', '1.2.840.113549.1.9.14', + 'msExtReq', '1.3.6.1.4.1.311.2.1.14', + ] + + def add_csr_attributes(csr, csr_attributes) + csr_attributes.each do |oid, value| + begin + if PRIVATE_CSR_ATTRIBUTES.include? oid + raise ArgumentError, "Cannot specify CSR attribute #{oid}: conflicts with internally used CSR attribute" + end + + encoded = OpenSSL::ASN1::PrintableString.new(value.to_s) + + attr_set = OpenSSL::ASN1::Set.new([encoded]) + csr.add_attribute(OpenSSL::X509::Attribute.new(oid, attr_set)) + Puppet.debug("Added csr attribute: #{oid} => #{attr_set.inspect}") + rescue OpenSSL::X509::AttributeError => e + raise Puppet::Error, "Cannot create CSR with attribute #{oid}: #{e.message}" + end + end + end + + private + + PRIVATE_EXTENSIONS = [ + 'subjectAltName', '2.5.29.17', + ] + + # @api private + def extension_request_attribute(options) + extensions = [] + + if options[:extension_requests] + options[:extension_requests].each_pair do |oid, value| + begin + if PRIVATE_EXTENSIONS.include? oid + raise Puppet::Error, "Cannot specify CSR extension request #{oid}: conflicts with internally used extension request" + end + + ext = OpenSSL::X509::Extension.new(oid, value.to_s, false) + extensions << ext + rescue OpenSSL::X509::ExtensionError => e + raise Puppet::Error, "Cannot create CSR with extension request #{oid}: #{e.message}" + end + end + end + + if options[:dns_alt_names] + names = options[:dns_alt_names].split(/\s*,\s*/).map(&:strip) + [name] + names = names.sort.uniq.map {|name| "DNS:#{name}" }.join(", ") + alt_names_ext = extension_factory.create_extension("subjectAltName", names, false) + + extensions << alt_names_ext + end + + unless extensions.empty? + seq = OpenSSL::ASN1::Sequence(extensions) + ext_req = OpenSSL::ASN1::Set([seq]) + OpenSSL::X509::Attribute.new("extReq", ext_req) + end + end + + # Unpack the extReq attribute into an array of Extensions. + # + # The extension request attribute is structured like + # `Set[Sequence[Extensions]]` where the outer Set only contains a single + # sequence. + # + # In addition the Ruby implementation of ASN1 requires that all ASN1 values + # contain a single value, so Sets and Sequence have to contain an array + # that in turn holds the elements. This is why we have to unpack an array + # every time we unpack a Set/Seq. + # + # @see http://tools.ietf.org/html/rfc2985#ref-10 5.4.2 CSR Extension Request structure + # @see http://tools.ietf.org/html/rfc5280 4.1 Certificate Extension structure + # + # @api private + # + # @param attribute [OpenSSL::X509::Attribute] The X509 extension request + # + # @return [Array<Array<Object>>] A array of arrays containing the extension + # OID the critical state if present, and the extension value. + def unpack_extension_request(attribute) + + unless attribute.value.is_a? OpenSSL::ASN1::Set + raise Puppet::Error, "In #{attribute.oid}, expected Set but found #{attribute.value.class}" + end + + unless attribute.value.value.is_a? Array + raise Puppet::Error, "In #{attribute.oid}, expected Set[Array] but found #{attribute.value.value.class}" + end + + unless attribute.value.value.size == 1 + raise Puppet::Error, "In #{attribute.oid}, expected Set[Array] with one value but found #{attribute.value.value.size} elements" + end + + unless attribute.value.value.first.is_a? OpenSSL::ASN1::Sequence + raise Puppet::Error, "In #{attribute.oid}, expected Set[Array[Sequence[...]]], but found #{extension.class}" + end + + unless attribute.value.value.first.value.is_a? Array + raise Puppet::Error, "In #{attribute.oid}, expected Set[Array[Sequence[Array[...]]]], but found #{extension.value.class}" + end + + extensions = attribute.value.value.first.value + + extensions.map(&:value) + end end diff --git a/lib/puppet/ssl/certificate_request_attributes.rb b/lib/puppet/ssl/certificate_request_attributes.rb new file mode 100644 index 000000000..e65b01443 --- /dev/null +++ b/lib/puppet/ssl/certificate_request_attributes.rb @@ -0,0 +1,37 @@ +require 'puppet/ssl' +require 'puppet/util/yaml' + +# This class transforms simple key/value pairs into the equivalent ASN1 +# structures. Values may be strings or arrays of strings. +# +# @api private +class Puppet::SSL::CertificateRequestAttributes + + attr_reader :path, :custom_attributes, :extension_requests + + def initialize(path) + @path = path + @custom_attributes = {} + @extension_requests = {} + end + + # Attempt to load a yaml file at the given @path. + # @return true if we are able to load the file, false otherwise + # @raise [Puppet::Error] if there are unexpected attribute keys + def load + Puppet.info("csr_attributes file loading from #{path}") + if Puppet::FileSystem::File.exist?(path) + hash = Puppet::Util::Yaml.load_file(path, {}) + if ! hash.is_a?(Hash) + raise Puppet::Error, "invalid CSR attributes, expected instance of Hash, received instance of #{hash.class}" + end + @custom_attributes = hash.delete('custom_attributes') || {} + @extension_requests = hash.delete('extension_requests') || {} + if not hash.keys.empty? + raise Puppet::Error, "unexpected attributes #{hash.keys.inspect} in #{@path.inspect}" + end + return true + end + return false + end +end diff --git a/lib/puppet/ssl/certificate_revocation_list.rb b/lib/puppet/ssl/certificate_revocation_list.rb index bb6595477..e19534764 100644 --- a/lib/puppet/ssl/certificate_revocation_list.rb +++ b/lib/puppet/ssl/certificate_revocation_list.rb @@ -49,7 +49,7 @@ DOC Puppet.notice "Revoked certificate with serial #{serial}" time = Time.now - add_certitificate_revocation_for(serial, reason, time) + add_certificate_revocation_for(serial, reason, time) update_to_next_crl_number update_valid_time_range_to_start_at(time) sign_with(cakey) @@ -69,7 +69,7 @@ private @content.extensions = [crl_number_of(0)] end - def add_certitificate_revocation_for(serial, reason, time) + def add_certificate_revocation_for(serial, reason, time) revoked = OpenSSL::X509::Revoked.new revoked.serial = serial revoked.time = time diff --git a/lib/puppet/ssl/host.rb b/lib/puppet/ssl/host.rb index e97ffe6c0..f30b4dee7 100644 --- a/lib/puppet/ssl/host.rb +++ b/lib/puppet/ssl/host.rb @@ -4,6 +4,7 @@ require 'puppet/ssl/key' require 'puppet/ssl/certificate' require 'puppet/ssl/certificate_request' require 'puppet/ssl/certificate_revocation_list' +require 'puppet/ssl/certificate_request_attributes' # The class that manages all aspects of our SSL certificates -- # private keys, public keys, requests, etc. @@ -173,6 +174,12 @@ DOC end end + csr_attributes = Puppet::SSL::CertificateRequestAttributes.new(Puppet[:csr_attributes]) + if csr_attributes.load + options[:csr_attributes] = csr_attributes.custom_attributes + options[:extension_requests] = csr_attributes.extension_requests + end + @certificate_request = CertificateRequest.new(name) @certificate_request.generate(key.content, options) begin @@ -264,14 +271,14 @@ ERROR_STRING @ssl_store end - def to_pson(*args) + def to_data_hash my_cert = Puppet::SSL::Certificate.indirection.find(name) - pson_hash = { :name => name } + result = { :name => name } my_state = state - pson_hash[:state] = my_state - pson_hash[:desired_state] = desired_state if desired_state + result[:state] = my_state + result[:desired_state] = desired_state if desired_state thing_to_use = (my_state == 'requested') ? certificate_request : my_cert @@ -280,7 +287,7 @@ ERROR_STRING # pson[:fingerprints][:default] # It appears that we have no internal consumers of this api # --jeffweiss 30 aug 2012 - pson_hash[:fingerprint] = thing_to_use.fingerprint + result[:fingerprint] = thing_to_use.fingerprint # The above fingerprint doesn't tell us what message digest algorithm was used # No problem, except that the default is changing between 2.7 and 3.0. Also, as @@ -289,15 +296,19 @@ ERROR_STRING # So, when we add the newer fingerprints, we're explicit about the hashing # algorithm used. # --jeffweiss 31 july 2012 - pson_hash[:fingerprints] = {} - pson_hash[:fingerprints][:default] = thing_to_use.fingerprint + result[:fingerprints] = {} + result[:fingerprints][:default] = thing_to_use.fingerprint suitable_message_digest_algorithms.each do |md| - pson_hash[:fingerprints][md] = thing_to_use.fingerprint md + result[:fingerprints][md] = thing_to_use.fingerprint md end - pson_hash[:dns_alt_names] = thing_to_use.subject_alt_names + result[:dns_alt_names] = thing_to_use.subject_alt_names - pson_hash.to_pson(*args) + result + end + + def to_pson(*args) + to_data_hash.to_pson(*args) end # eventually we'll probably want to move this somewhere else or make it diff --git a/lib/puppet/ssl/inventory.rb b/lib/puppet/ssl/inventory.rb index c210fdc35..3aae02a65 100644 --- a/lib/puppet/ssl/inventory.rb +++ b/lib/puppet/ssl/inventory.rb @@ -8,11 +8,7 @@ class Puppet::SSL::Inventory # Add a certificate to our inventory. def add(cert) cert = cert.content if cert.is_a?(Puppet::SSL::Certificate) - - # Create our file, if one does not already exist. - rebuild unless FileTest.exist?(@path) - - Puppet.settings.write(:cert_inventory, "a") do |f| + Puppet.settings.setting(:cert_inventory).open("a") do |f| f.print format(cert) end end @@ -32,16 +28,16 @@ class Puppet::SSL::Inventory def rebuild Puppet.notice "Rebuilding inventory file" - Puppet.settings.write(:cert_inventory) do |f| - f.print "# Inventory of signed certificates\n# SERIAL NOT_BEFORE NOT_AFTER SUBJECT\n" + Puppet.settings.setting(:cert_inventory).open('w') do |f| + Puppet::SSL::Certificate.indirection.search("*").each do |cert| + f.print format(cert.content) + end end - - Puppet::SSL::Certificate.indirection.search("*").each { |cert| add(cert) } end # Find the serial number for a given certificate. def serial(name) - return nil unless FileTest.exist?(@path) + return nil unless Puppet::FileSystem::File.exist?(@path) File.readlines(@path).each do |line| next unless line =~ /^(\S+).+\/CN=#{name}$/ diff --git a/lib/puppet/ssl/key.rb b/lib/puppet/ssl/key.rb index 569fb706d..b64fde544 100644 --- a/lib/puppet/ssl/key.rb +++ b/lib/puppet/ssl/key.rb @@ -36,7 +36,7 @@ DOC end def password - return nil unless password_file and FileTest.exist?(password_file) + return nil unless password_file and Puppet::FileSystem::File.exist?(password_file) ::File.read(password_file) end diff --git a/lib/puppet/ssl/oids.rb b/lib/puppet/ssl/oids.rb new file mode 100644 index 000000000..6d0a8d0d9 --- /dev/null +++ b/lib/puppet/ssl/oids.rb @@ -0,0 +1,78 @@ +require 'puppet/ssl' + +# This module defines OIDs for use within Puppet. +# +# == ASN.1 Definition +# +# The following is the formal definition of OIDs specified in this file. +# +# puppetCertExtensions OBJECT IDENTIFIER ::= {iso(1) identified-organization(3) +# dod(6) internet(1) private(4) enterprise(1) 34380 1} +# +# -- the tree under registeredExtensions 'belongs' to puppetlabs +# -- privateExtensions can be extended by enterprises to suit their own needs +# registeredExtensions OBJECT IDENTIFIER ::= { puppetCertExtensions 1 } +# privateExtensions OBJECT IDENTIFIER ::= { puppetCertExtensions 2 } +# +# -- subtree of common registered extensions +# -- The short names for these OIDs are intentionally lowercased and formatted +# -- since they may be exposed inside the Puppet DSL as variables. +# pp_uuid OBJECT IDENTIFIER ::= { registeredExtensions 1 } +# pp_instance_id OBJECT IDENTIFIER ::= { registeredExtensions 2 } +# pp_image_name OBJECT IDENTIFIER ::= { registeredExtensions 3 } +# pp_preshared_key OBJECT IDENTIFIER ::= { registeredExtensions 4 } +# +# @api private +module Puppet::SSL::Oids + + PUPPET_OIDS = [ + ["1.3.6.1.4.1.34380", 'puppetlabs', 'Puppet Labs'], + ["1.3.6.1.4.1.34380.1", 'ppCertExt', 'Puppet Certificate Extension'], + + ["1.3.6.1.4.1.34380.1.1", 'ppRegCertExt', 'Puppet Registered Certificate Extension'], + + ["1.3.6.1.4.1.34380.1.1.1", 'pp_uuid', 'Puppet Node UUID'], + ["1.3.6.1.4.1.34380.1.1.2", 'pp_instance_id', 'Puppet Node Instance ID'], + ["1.3.6.1.4.1.34380.1.1.3", 'pp_image_name', 'Puppet Node Image Name'], + ["1.3.6.1.4.1.34380.1.1.4", 'pp_preshared_key', 'Puppet Node Preshared Key'], + + ["1.3.6.1.4.1.34380.1.2", 'ppPrivCertExt', 'Puppet Private Certificate Extension'], + ] + + PUPPET_OIDS.each do |oid_defn| + OpenSSL::ASN1::ObjectId.register(*oid_defn) + end + + # Determine if the first OID contains the second OID + # + # @param first [String] The containing OID, in dotted form or as the short name + # @param second [String] The contained OID, in dotted form or as the short name + # @param exclusive [true, false] If an OID should not be considered as a subtree of itself + # + # @example Comparing two dotted OIDs + # Puppet::SSL::Oids.subtree_of?('1.3.6.1', '1.3.6.1.4.1') #=> true + # Puppet::SSL::Oids.subtree_of?('1.3.6.1', '1.3.6') #=> false + # + # @example Comparing an OID short name with a dotted OID + # Puppet::SSL::Oids.subtree_of?('IANA', '1.3.6.1.4.1') #=> true + # Puppet::SSL::Oids.subtree_of?('1.3.6.1', 'enterprises') #=> true + # + # @example Comparing an OID against itself + # Puppet::SSL::Oids.subtree_of?('IANA', 'IANA') #=> true + # Puppet::SSL::Oids.subtree_of?('IANA', 'IANA', true) #=> false + # + # @return [true, false] + def self.subtree_of?(first, second, exclusive = false) + first_oid = OpenSSL::ASN1::ObjectId.new(first).oid + second_oid = OpenSSL::ASN1::ObjectId.new(second).oid + + + if exclusive and first_oid == second_oid + false + else + second_oid.index(first_oid) == 0 + end + rescue OpenSSL::ASN1::ASN1Error + false + end +end diff --git a/lib/puppet/ssl/validator.rb b/lib/puppet/ssl/validator.rb index 18255fb3c..754a74233 100644 --- a/lib/puppet/ssl/validator.rb +++ b/lib/puppet/ssl/validator.rb @@ -1,116 +1,60 @@ -require 'puppet/ssl' require 'openssl' -module Puppet -module SSL -class Validator - attr_reader :peer_certs - attr_reader :verify_errors - attr_reader :ssl_configuration - ## - # @param [Hash] opts the options to initialze the instance with. - # - # @option opts [Puppet::SSL::Configuration] :ssl_configuration to use for - # authorizing the peer certificate chain. - def initialize(opts = {}) - reset! - @ssl_configuration = opts[:ssl_configuration] or raise ArgumentError, ":ssl_configuration is required" - end +# API for certificate verification +# +# @api public +class Puppet::SSL::Validator - ## - # reset to the initial state. - def reset! - @peer_certs = [] - @verify_errors = [] + # Factory method for creating an instance of a null/no validator. + # This method does not have to be implemented by concrete implementations of this API. + # + # @return [Puppet::SSL::Validator] produces a validator that performs no validation + # + # @api public + # + def self.no_validator() + @@no_validator_cache ||= Puppet::SSL::Validator::NoValidator.new() end - ## - # call performs verification of the SSL connection and collection of the - # certificates for use in constructing the error message if the verification - # failed. This callback will be executed once for each certificate in a - # chain being verified. + # Factory method for creating an instance of the default Puppet validator. + # This method does not have to be implemented by concrete implementations of this API. # - # From the [OpenSSL - # documentation](http://www.openssl.org/docs/ssl/SSL_CTX_set_verify.html): - # The `verify_callback` function is used to control the behaviour when the - # SSL_VERIFY_PEER flag is set. It must be supplied by the application and - # receives two arguments: preverify_ok indicates, whether the verification of - # the certificate in question was passed (preverify_ok=1) or not - # (preverify_ok=0). x509_ctx is a pointer to the complete context used for - # the certificate chain verification. + # @return [Puppet::SSL::Validator] produces a validator that performs no validation # - # See {Puppet::Network::HTTP::Connection} for more information and where this - # class is intended to be used. + # @api public # - # @param [Boolean] preverify_ok indicates whether the verification of the - # certificate in question was passed (preverify_ok=true) - # @param [OpenSSL::SSL::SSLContext] ssl_context holds the SSLContext for the - # chain being verified. - # - # @return [Boolean] false if the peer is invalid, true otherwise. - def call(preverify_ok, ssl_context) - # We must make a copy since the scope of the ssl_context will be lost - # across invocations of this method. - current_cert = ssl_context.current_cert - @peer_certs << Puppet::SSL::Certificate.from_instance(current_cert) - - if preverify_ok - # If we've copied all of the certs in the chain out of the SSL library - if @peer_certs.length == ssl_context.chain.length - # (#20027) The peer cert must be issued by a specific authority - preverify_ok = valid_peer? - end - else - if ssl_context.error_string - @verify_errors << "#{ssl_context.error_string} for #{current_cert.subject}" - end - end - preverify_ok - rescue => ex - @verify_errors << ex.message - false + def self.default_validator() + Puppet::SSL::Validator::DefaultValidator.new() end - ## - # Register the instance's call method with the connection. + # Array of peer certificates + # @return [Array<Puppet::SSL::Certificate>] peer certificates # - # @param [Net::HTTP] connection The connection to velidate + # @api public # - # @return [void] - def register_verify_callback(connection) - connection.verify_callback = self + def peer_certs + raise NotImplementedError, "Concrete class should have implemented this method" end - ## - # Validate the peer certificates against the authorized certificates. - def valid_peer? - descending_cert_chain = @peer_certs.reverse.map {|c| c.content } - authz_ca_certs = ssl_configuration.ca_auth_certificates - - if not has_authz_peer_cert(descending_cert_chain, authz_ca_certs) - msg = "The server presented a SSL certificate chain which does not include a " << - "CA listed in the ssl_client_ca_auth file. " - msg << "Authorized Issuers: #{authz_ca_certs.collect {|c| c.subject}.join(', ')} " << - "Peer Chain: #{descending_cert_chain.collect {|c| c.subject}.join(' => ')}" - @verify_errors << msg - false - else - true - end + # Contains the result of validation + # @return [Array<String>, nil] nil, empty Array, or Array with messages + # + # @api public + # + def verify_errors + raise NotImplementedError, "Concrete class should have implemented this method" end - ## - # checks if the set of peer_certs contains at least one certificate issued - # by a certificate listed in authz_certs + # Registers the connection to validate. + # + # @param [Net::HTTP] connection The connection to validate + # + # @return [void] # - # @return [Boolean] - def has_authz_peer_cert(peer_certs, authz_certs) - peer_certs.any? do |peer_cert| - authz_certs.any? do |authz_cert| - peer_cert.verify(authz_cert.public_key) - end - end + # @api public + # + def setup_connection(connection) + raise NotImplementedError, "Concrete class should have implemented this method" end end -end -end + diff --git a/lib/puppet/ssl/validator/default_validator.rb b/lib/puppet/ssl/validator/default_validator.rb new file mode 100644 index 000000000..1f238f4c4 --- /dev/null +++ b/lib/puppet/ssl/validator/default_validator.rb @@ -0,0 +1,153 @@ +require 'openssl' + +# Perform peer certificate verification against the known CA. +# If there is no CA information known, then no verification is performed +# +# @api private +# +class Puppet::SSL::Validator::DefaultValidator #< class Puppet::SSL::Validator + attr_reader :peer_certs + attr_reader :verify_errors + attr_reader :ssl_configuration + + # Creates a new DefaultValidator, optionally with an SSL Configuration and SSL Host. + # + # @param [Puppet::SSL::Configuration] (a default configuration) ssl_configuration the SSL configuration to use + # @param [Puppet::SSL::Host] (Puppet::SSL::Host.localhost) the SSL host to use + # + # @api private + # + def initialize( + ssl_configuration = Puppet::SSL::Configuration.new( + Puppet[:localcacert], { + :ca_chain_file => Puppet[:ssl_client_ca_chain], + :ca_auth_file => Puppet[:ssl_client_ca_auth] + }), + ssl_host = Puppet::SSL::Host.localhost) + + reset! + @ssl_configuration = ssl_configuration + @ssl_host = ssl_host + end + + + # Resets this validator to its initial validation state. The ssl configuration is not changed. + # + # @api private + # + def reset! + @peer_certs = [] + @verify_errors = [] + end + + # Performs verification of the SSL connection and collection of the + # certificates for use in constructing the error message if the verification + # failed. This callback will be executed once for each certificate in a + # chain being verified. + # + # From the [OpenSSL + # documentation](http://www.openssl.org/docs/ssl/SSL_CTX_set_verify.html): + # The `verify_callback` function is used to control the behaviour when the + # SSL_VERIFY_PEER flag is set. It must be supplied by the application and + # receives two arguments: preverify_ok indicates, whether the verification of + # the certificate in question was passed (preverify_ok=1) or not + # (preverify_ok=0). x509_ctx is a pointer to the complete context used for + # the certificate chain verification. + # + # See {Puppet::Network::HTTP::Connection} for more information and where this + # class is intended to be used. + # + # @param [Boolean] preverify_ok indicates whether the verification of the + # certificate in question was passed (preverify_ok=true) + # @param [OpenSSL::SSL::SSLContext] ssl_context holds the SSLContext for the + # chain being verified. + # + # @return [Boolean] false if the peer is invalid, true otherwise. + # + # @api private + # + def call(preverify_ok, ssl_context) + # We must make a copy since the scope of the ssl_context will be lost + # across invocations of this method. + current_cert = ssl_context.current_cert + @peer_certs << Puppet::SSL::Certificate.from_instance(current_cert) + + if preverify_ok + # If we've copied all of the certs in the chain out of the SSL library + if @peer_certs.length == ssl_context.chain.length + # (#20027) The peer cert must be issued by a specific authority + preverify_ok = valid_peer? + end + else + if ssl_context.error_string + @verify_errors << "#{ssl_context.error_string} for #{current_cert.subject}" + end + end + preverify_ok + rescue => ex + @verify_errors << ex.message + false + end + + # Registers the instance's call method with the connection. + # + # @param [Net::HTTP] connection The connection to validate + # + # @return [void] + # + # @api private + # + def setup_connection(connection) + if ssl_certificates_are_present? + connection.cert_store = @ssl_host.ssl_store + connection.ca_file = @ssl_configuration.ca_auth_file + connection.cert = @ssl_host.certificate.content + connection.key = @ssl_host.key.content + connection.verify_mode = OpenSSL::SSL::VERIFY_PEER + connection.verify_callback = self + else + connection.verify_mode = OpenSSL::SSL::VERIFY_NONE + end + end + + # Validates the peer certificates against the authorized certificates. + # + # @api private + # + def valid_peer? + descending_cert_chain = @peer_certs.reverse.map {|c| c.content } + authz_ca_certs = ssl_configuration.ca_auth_certificates + + if not has_authz_peer_cert(descending_cert_chain, authz_ca_certs) + msg = "The server presented a SSL certificate chain which does not include a " << + "CA listed in the ssl_client_ca_auth file. " + msg << "Authorized Issuers: #{authz_ca_certs.collect {|c| c.subject}.join(', ')} " << + "Peer Chain: #{descending_cert_chain.collect {|c| c.subject}.join(' => ')}" + @verify_errors << msg + false + else + true + end + end + + # Checks if the set of peer_certs contains at least one certificate issued + # by a certificate listed in authz_certs + # + # @return [Boolean] + # + # @api private + # + def has_authz_peer_cert(peer_certs, authz_certs) + peer_certs.any? do |peer_cert| + authz_certs.any? do |authz_cert| + peer_cert.verify(authz_cert.public_key) + end + end + end + + # @api private + # + def ssl_certificates_are_present? + Puppet::FileSystem::File.exist?(Puppet[:hostcert]) && Puppet::FileSystem::File.exist?(@ssl_configuration.ca_auth_file) + end +end diff --git a/lib/puppet/ssl/validator/no_validator.rb b/lib/puppet/ssl/validator/no_validator.rb new file mode 100644 index 000000000..1141b6952 --- /dev/null +++ b/lib/puppet/ssl/validator/no_validator.rb @@ -0,0 +1,17 @@ +# Performs no SSL verification +# @api private +# +class Puppet::SSL::Validator::NoValidator < Puppet::SSL::Validator + + def setup_connection(connection) + connection.verify_mode = OpenSSL::SSL::VERIFY_NONE + end + + def peer_certs + [] + end + + def verify_errors + [] + end +end diff --git a/lib/puppet/status.rb b/lib/puppet/status.rb index 9ad55668b..0b26ae22e 100644 --- a/lib/puppet/status.rb +++ b/lib/puppet/status.rb @@ -10,6 +10,10 @@ class Puppet::Status @status = status || {"is_alive" => true} end + def to_data_hash + @status + end + def to_pson(*args) @status.to_pson end diff --git a/lib/puppet/test/test_helper.rb b/lib/puppet/test/test_helper.rb index c09a1ade2..f80e699e7 100644 --- a/lib/puppet/test/test_helper.rb +++ b/lib/puppet/test/test_helper.rb @@ -1,3 +1,5 @@ +require 'puppet/indirector/data_binding/hiera' + module Puppet::Test # This class is intended to provide an API to be used by external projects # when they are running tests that depend on puppet core. This should @@ -84,8 +86,11 @@ module Puppet::Test Puppet::Node::Environment.clear Puppet::Parser::Functions.reset Puppet::Application.clear! + Puppet::Util::Profiler.clear Puppet.clear_deprecation_warnings + + Puppet::DataBinding::Hiera.instance_variable_set("@hiera", nil) end # Call this method once per test, after execution of each individual test. diff --git a/lib/puppet/transaction.rb b/lib/puppet/transaction.rb index a37ec59ad..ab74ed385 100644 --- a/lib/puppet/transaction.rb +++ b/lib/puppet/transaction.rb @@ -2,6 +2,7 @@ require 'puppet' require 'puppet/util/tagging' require 'puppet/application' require 'digest/sha1' +require 'set' # the class that actually walks our resource/property tree, collects the changes, # and performs them @@ -58,7 +59,11 @@ class Puppet::Transaction continue_while = lambda { !stop_processing? } + post_evalable_providers = Set.new pre_process = lambda do |resource| + prov_class = resource.provider.class + post_evalable_providers << prov_class if prov_class.respond_to?(:post_resource_eval) + prefetch_if_necessary(resource) # If we generated resources, we don't know what they are now @@ -90,6 +95,14 @@ class Puppet::Transaction providerless_types.uniq.each do |type| Puppet.err "Could not find a suitable provider for #{type}" end + + post_evalable_providers.each do |provider| + begin + provider.post_resource_eval + rescue => detail + Puppet.log_exception(detail, "post_resource_eval failed for provider #{provider}") + end + end end relationship_graph.traverse(:while => continue_while, diff --git a/lib/puppet/transaction/event.rb b/lib/puppet/transaction/event.rb index c832665c6..ca4255937 100644 --- a/lib/puppet/transaction/event.rb +++ b/lib/puppet/transaction/event.rb @@ -2,17 +2,18 @@ require 'puppet/transaction' require 'puppet/util/tagging' require 'puppet/util/logging' require 'puppet/util/methodhelper' +require 'puppet/network/format_support' # A simple struct for storing what happens on the system. class Puppet::Transaction::Event include Puppet::Util::MethodHelper include Puppet::Util::Tagging include Puppet::Util::Logging + include Puppet::Network::FormatSupport ATTRIBUTES = [:name, :resource, :property, :previous_value, :desired_value, :historical_value, :status, :message, :file, :line, :source_description, :audited, :invalidate_refreshes] YAML_ATTRIBUTES = %w{@audited @property @previous_value @desired_value @historical_value @message @name @status @time}.map(&:to_sym) attr_accessor *ATTRIBUTES - attr_writer :tags attr_accessor :time attr_reader :default_log_level @@ -44,7 +45,7 @@ class Puppet::Transaction::Event @time = Time.parse(@time) if @time.is_a? String end - def to_pson + def to_data_hash { 'audited' => @audited, 'property' => @property, @@ -55,7 +56,11 @@ class Puppet::Transaction::Event 'name' => @name, 'status' => @status, 'time' => @time.iso8601(9), - }.to_pson + } + end + + def to_pson(*args) + to_data_hash.to_pson(*args) end def property=(prop) diff --git a/lib/puppet/transaction/report.rb b/lib/puppet/transaction/report.rb index 92e685f42..0469965cd 100644 --- a/lib/puppet/transaction/report.rb +++ b/lib/puppet/transaction/report.rb @@ -216,7 +216,7 @@ class Puppet::Transaction::Report end end - def to_pson + def to_data_hash { 'host' => @host, 'time' => @time.iso8601(9), @@ -231,7 +231,11 @@ class Puppet::Transaction::Report 'logs' => @logs, 'metrics' => @metrics, 'resource_statuses' => @resource_statuses, - }.to_pson + } + end + + def to_pson + to_data_hash.to_pson end # @return [String] the host name diff --git a/lib/puppet/transaction/resource_harness.rb b/lib/puppet/transaction/resource_harness.rb index 99ac751b5..3efcabaf6 100644 --- a/lib/puppet/transaction/resource_harness.rb +++ b/lib/puppet/transaction/resource_harness.rb @@ -6,16 +6,50 @@ class Puppet::Transaction::ResourceHarness attr_reader :transaction - def allow_changes?(resource) - if resource.purging? and resource.deleting? and deps = relationship_graph.dependents(resource) \ - and ! deps.empty? and deps.detect { |d| ! d.deleting? } - deplabel = deps.collect { |r| r.ref }.join(",") - plurality = deps.length > 1 ? "":"s" - resource.warning "#{deplabel} still depend#{plurality} on me -- not purging" - false - else - true + def initialize(transaction) + @transaction = transaction + end + + def evaluate(resource) + status = Puppet::Resource::Status.new(resource) + + begin + context = ResourceApplicationContext.from_resource(resource, status) + perform_changes(resource, context) + + if status.changed? && ! resource.noop? + cache(resource, :synced, Time.now) + resource.flush if resource.respond_to?(:flush) + end + rescue => detail + status.failed_because(detail) + ensure + status.evaluation_time = Time.now - status.time + end + + status + end + + def scheduled?(resource) + return true if Puppet[:ignoreschedules] + return true unless schedule = schedule(resource) + + # We use 'checked' here instead of 'synced' because otherwise we'll + # end up checking most resources most times, because they will generally + # have been synced a long time ago (e.g., a file only gets updated + # once a month on the server and its schedule is daily; the last sync time + # will have been a month ago, so we'd end up checking every run). + schedule.match?(cached(resource, :checked).to_i) + end + + def schedule(resource) + unless resource.catalog + resource.warning "Cannot schedule without a schedule-containing catalog" + return nil end + + return nil unless name = resource[:schedule] + resource.catalog.resource(:schedule, name) || resource.fail("Could not find schedule #{name}") end # Used mostly for scheduling and auditing at this point. @@ -28,153 +62,177 @@ class Puppet::Transaction::ResourceHarness Puppet::Util::Storage.cache(resource)[name] = value end - def perform_changes(resource) - current_values = resource.retrieve_resource.to_hash + private + def perform_changes(resource, context) cache(resource, :checked, Time.now) return [] if ! allow_changes?(resource) - historical_values = Puppet::Util::Storage.cache(resource).dup - desired_values = {} - resource.properties.each do |property| - desired_values[property.name] = property.should - end - audited_params = (resource[:audit] || []).map { |p| p.to_sym } - synced_params = [] - # Record the current state in state.yml. - audited_params.each do |param| - cache(resource, param, current_values[param]) + context.audited_params.each do |param| + cache(resource, param, context.current_values[param]) end - # Update the machine state & create logs/events - events = [] - ensure_param = resource.parameter(:ensure) - if desired_values[:ensure] && !ensure_param.safe_insync?(current_values[:ensure]) - events << apply_parameter(ensure_param, current_values[:ensure], audited_params.include?(:ensure), historical_values[:ensure]) - synced_params << :ensure - elsif current_values[:ensure] != :absent - work_order = resource.properties # Note: only the resource knows what order to apply changes in - work_order.each do |param| - if desired_values[param.name] && !param.safe_insync?(current_values[param.name]) - events << apply_parameter(param, current_values[param.name], audited_params.include?(param.name), historical_values[param.name]) - synced_params << param.name + managed_via_ensure = manage_via_ensure_if_possible(resource, context) + + if !managed_via_ensure + if context.resource_present? + resource.properties.each do |param| + sync_if_needed(param, context) end + else + resource.debug("Nothing to manage: no ensure and the resource doesn't exist") end end - # Add more events to capture audit results - audited_params.each do |param_name| - if historical_values.include?(param_name) - if historical_values[param_name] != current_values[param_name] && !synced_params.include?(param_name) - event = create_change_event(resource.parameter(param_name), current_values[param_name], true, historical_values[param_name]) - event.send_log - events << event + capture_audit_events(resource, context) + end + + def allow_changes?(resource) + if resource.purging? and resource.deleting? and deps = relationship_graph.dependents(resource) \ + and ! deps.empty? and deps.detect { |d| ! d.deleting? } + deplabel = deps.collect { |r| r.ref }.join(",") + plurality = deps.length > 1 ? "":"s" + resource.warning "#{deplabel} still depend#{plurality} on me -- not purging" + false + else + true + end + end + + def manage_via_ensure_if_possible(resource, context) + ensure_param = resource.parameter(:ensure) + if ensure_param && ensure_param.should + sync_if_needed(ensure_param, context) + else + false + end + end + + def sync_if_needed(param, context) + historical_value = context.historical_values[param.name] + current_value = context.current_values[param.name] + do_audit = context.audited_params.include?(param.name) + + begin + if param.should && !param.safe_insync?(current_value) + event = create_change_event(param, current_value, historical_value) + if do_audit + event = audit_event(event, param) + end + + brief_audit_message = audit_message(param, do_audit, historical_value, current_value) + + if param.noop + noop(event, param, current_value, brief_audit_message) + else + sync(event, param, current_value, brief_audit_message) end + + true else - resource.property(param_name).notice "audit change: newly-recorded value #{current_values[param_name]}" + false end - end + rescue => detail + # Execution will continue on StandardErrors, just store the event + Puppet.log_exception(detail) - events + event = create_change_event(param, current_value, historical_value) + event.status = "failure" + event.message = "change from #{param.is_to_s(current_value)} to #{param.should_to_s(param.should)} failed: #{detail}" + false + rescue Exception => detail + # Execution will halt on Exceptions, they get raised to the application + event = create_change_event(param, current_value, historical_value) + event.status = "failure" + event.message = "change from #{param.is_to_s(current_value)} to #{param.should_to_s(param.should)} failed: #{detail}" + raise + ensure + if event + context.record(event) + event.send_log + context.synced_params << param.name + end + end end - def create_change_event(property, current_value, do_audit, historical_value) + def create_change_event(property, current_value, historical_value) event = property.event event.previous_value = current_value event.desired_value = property.should event.historical_value = historical_value - if do_audit - event.audited = true - event.status = "audit" - if historical_value != current_value - event.message = "audit change: previously recorded value #{property.is_to_s(historical_value)} has been changed to #{property.is_to_s(current_value)}" - end + event + end + + def audit_event(event, property) + event.audited = true + event.status = "audit" + if event.historical_value != event.previous_value + event.message = "audit change: previously recorded value #{property.is_to_s(event.historical_value)} has been changed to #{property.is_to_s(event.previous_value)}" end event end - def apply_parameter(property, current_value, do_audit, historical_value) - event = create_change_event(property, current_value, do_audit, historical_value) - + def audit_message(param, do_audit, historical_value, current_value) if do_audit && historical_value && historical_value != current_value - brief_audit_message = " (previously recorded value was #{property.is_to_s(historical_value)})" + " (previously recorded value was #{param.is_to_s(historical_value)})" else - brief_audit_message = "" + "" end - - if property.noop - event.message = "current_value #{property.is_to_s(current_value)}, should be #{property.should_to_s(property.should)} (noop)#{brief_audit_message}" - event.status = "noop" - else - property.sync - event.message = [ property.change_to_s(current_value, property.should), brief_audit_message ].join - event.status = "success" - end - event - rescue => detail - # Execution will continue on StandardErrors, just store the event - Puppet.log_exception(detail) - event.status = "failure" - - event.message = "change from #{property.is_to_s(current_value)} to #{property.should_to_s(property.should)} failed: #{detail}" - event - rescue Exception => detail - # Execution will halt on Exceptions, they get raised to the application - event.status = "failure" - event.message = "change from #{property.is_to_s(current_value)} to #{property.should_to_s(property.should)} failed: #{detail}" - raise - ensure - event.send_log end - def evaluate(resource) - status = Puppet::Resource::Status.new(resource) + def noop(event, param, current_value, audit_message) + event.message = "current_value #{param.is_to_s(current_value)}, should be #{param.should_to_s(param.should)} (noop)#{audit_message}" + event.status = "noop" + end - begin - perform_changes(resource).each do |event| - status << event - end + def sync(event, param, current_value, audit_message) + param.sync + event.message = "#{param.change_to_s(current_value, param.should)}#{audit_message}" + event.status = "success" + end - if status.changed? && ! resource.noop? - cache(resource, :synced, Time.now) - resource.flush if resource.respond_to?(:flush) + def capture_audit_events(resource, context) + context.audited_params.each do |param_name| + if context.historical_values.include?(param_name) + if context.historical_values[param_name] != context.current_values[param_name] && !context.synced_params.include?(param_name) + parameter = resource.parameter(param_name) + event = audit_event(create_change_event(parameter, + context.current_values[param_name], + context.historical_values[param_name]), + parameter) + event.send_log + context.record(event) + end + else + resource.property(param_name).notice "audit change: newly-recorded value #{context.current_values[param_name]}" end - rescue => detail - status.failed_because(detail) - ensure - status.evaluation_time = Time.now - status.time end - - status end - def initialize(transaction) - @transaction = transaction - end - - def scheduled?(resource) - return true if Puppet[:ignoreschedules] - return true unless schedule = schedule(resource) - - # We use 'checked' here instead of 'synced' because otherwise we'll - # end up checking most resources most times, because they will generally - # have been synced a long time ago (e.g., a file only gets updated - # once a month on the server and its schedule is daily; the last sync time - # will have been a month ago, so we'd end up checking every run). - schedule.match?(cached(resource, :checked).to_i) - end + # @api private + ResourceApplicationContext = Struct.new(:current_values, + :historical_values, + :audited_params, + :synced_params, + :status) do + def self.from_resource(resource, status) + ResourceApplicationContext.new(resource.retrieve_resource.to_hash, + Puppet::Util::Storage.cache(resource).dup, + (resource[:audit] || []).map { |p| p.to_sym }, + [], + status) + end - def schedule(resource) - unless resource.catalog - resource.warning "Cannot schedule without a schedule-containing catalog" - return nil + def resource_present? + current_values[:ensure] != :absent end - return nil unless name = resource[:schedule] - resource.catalog.resource(:schedule, name) || resource.fail("Could not find schedule #{name}") + def record(event) + status << event + end end end diff --git a/lib/puppet/type.rb b/lib/puppet/type.rb index 55b4a5fad..941696d28 100644 --- a/lib/puppet/type.rb +++ b/lib/puppet/type.rb @@ -692,6 +692,20 @@ class Type } end + # Return the parameters, metaparams, and properties that have a value or were set by a default. Properties are + # included since they are a subclass of parameter. + # @return [Array<Puppet::Parameter>] Array of parameter objects ( or subclass thereof ) + def parameters_with_value + self.class.allattrs.collect { |attr| parameter(attr) }.compact + end + + # Iterates over all parameters with value currently set. + # @yieldparam parameter [Puppet::Parameter] or a subclass thereof + # @return [void] + def eachparameter + parameters_with_value.each { |parameter| yield parameter } + end + # Creates a transaction event. # Called by Transaction or by a property. # Merges the given options with the options `:resource`, `:file`, `:line`, and `:tags`, initialized from @@ -2278,6 +2292,13 @@ class Type # @return [Array<Puppet::Parameter>] the validated list/set of attributes # def finish + # Call post_compile hook on every parameter that implements it. This includes all subclasses + # of parameter including, but not limited to, regular parameters, metaparameters, relationship + # parameters, and properties. + eachparameter do |parameter| + parameter.post_compile if parameter.respond_to? :post_compile + end + # Make sure all of our relationships are valid. Again, must be done # when the entire catalog is instantiated. self.class.relationship_params.collect do |klass| @@ -2297,25 +2318,21 @@ class Type self[:name] end - # Returns the parent of this in the catalog. - # In case of an erroneous catalog where multiple parents have been produced, the first found (non deterministic) - # parent is returned. - # @return [???, nil] WHAT (which types can be the parent of a resource in a catalog?), or nil if there - # is no catalog. - # + # Returns the parent of this in the catalog. In case of an erroneous catalog + # where multiple parents have been produced, the first found (non + # deterministic) parent is returned. + # @return [Puppet::Type, nil] the + # containing resource or nil if there is no catalog or no containing + # resource. def parent return nil unless catalog - unless defined?(@parent) + @parent ||= if parents = catalog.adjacent(self, :direction => :in) - # We should never have more than one parent, so let's just ignore - # it if we happen to. - @parent = parents.shift + parents.shift else - @parent = nil + nil end - end - @parent end # Returns a reference to this as a string in "Type[name]" format. diff --git a/lib/puppet/type/augeas.rb b/lib/puppet/type/augeas.rb index 75c8142f2..930235f0c 100644 --- a/lib/puppet/type/augeas.rb +++ b/lib/puppet/type/augeas.rb @@ -96,52 +96,18 @@ Puppet::Type.newtype(:augeas) do desc "The changes which should be applied to the filesystem. This can be a command or an array of commands. The following commands are supported: - `set <PATH> <VALUE>` - : Sets the value `VALUE` at loction `PATH` - - - `setm <PATH> <SUB> <VALUE>` - : Sets multiple nodes (matching `SUB` relative to `PATH`) to `VALUE` - - - `rm <PATH>` - : Removes the node at location `PATH` - - - `remove <PATH>` - : Synonym for `rm` - - - `clear <PATH>` - : Sets the node at `PATH` to `NULL`, creating it if needed - - - `clearm <PATH> <SUB>` - : Sets multiple nodes (matching `SUB` relative to `PATH`) to `NULL` - - - `ins <LABEL> (before|after) <PATH>` - : Inserts an empty node `LABEL` either before or after `PATH`. - - - `insert <LABEL> <WHERE> <PATH>` - : Synonym for `ins` - - - `mv <PATH> <OTHER PATH>` - : Moves a node at `PATH` to the new location `OTHER PATH` - - - `move <PATH> <OTHER PATH>` - : Synonym for `mv` - - - `defvar <NAME> <PATH>` - : Sets Augeas variable `$NAME` to `PATH` - - - `defnode <NAME> <PATH> <VALUE>` - : Sets Augeas variable `$NAME` to `PATH`, creating it with `VALUE` if needed + * `set <PATH> <VALUE>` --- Sets the value `VALUE` at loction `PATH` + * `setm <PATH> <SUB> <VALUE>` --- Sets multiple nodes (matching `SUB` relative to `PATH`) to `VALUE` + * `rm <PATH>` --- Removes the node at location `PATH` + * `remove <PATH>` --- Synonym for `rm` + * `clear <PATH>` --- Sets the node at `PATH` to `NULL`, creating it if needed + * `clearm <PATH> <SUB>` --- Sets multiple nodes (matching `SUB` relative to `PATH`) to `NULL` + * `ins <LABEL> (before|after) <PATH>` --- Inserts an empty node `LABEL` either before or after `PATH`. + * `insert <LABEL> <WHERE> <PATH>` --- Synonym for `ins` + * `mv <PATH> <OTHER PATH>` --- Moves a node at `PATH` to the new location `OTHER PATH` + * `move <PATH> <OTHER PATH>` --- Synonym for `mv` + * `defvar <NAME> <PATH>` --- Sets Augeas variable `$NAME` to `PATH` + * `defnode <NAME> <PATH> <VALUE>` --- Sets Augeas variable `$NAME` to `PATH`, creating it with `VALUE` if needed If the `context` parameter is set, that value is prepended to any relative `PATH`s." end diff --git a/lib/puppet/type/component.rb b/lib/puppet/type/component.rb index 4783ef023..255d1a9d8 100644 --- a/lib/puppet/type/component.rb +++ b/lib/puppet/type/component.rb @@ -34,13 +34,7 @@ Puppet::Type.newtype(:component) do # Component paths are special because they function as containers. def pathbuilder if reference.type == "Class" - # 'main' is the top class, so we want to see '//' instead of - # its name. - if reference.title.to_s.downcase == "main" - myname = "" - else - myname = reference.title - end + myname = reference.title else myname = reference.to_s end diff --git a/lib/puppet/type/cron.rb b/lib/puppet/type/cron.rb index 198d6c171..198d6c171 100755..100644 --- a/lib/puppet/type/cron.rb +++ b/lib/puppet/type/cron.rb diff --git a/lib/puppet/type/exec.rb b/lib/puppet/type/exec.rb index 85724cc30..d3e83e948 100755..100644 --- a/lib/puppet/type/exec.rb +++ b/lib/puppet/type/exec.rb @@ -220,6 +220,18 @@ module Puppet end end + newparam(:umask, :required_feature => :umask) do + desc "Sets the umask to be used while executing this command" + + munge do |value| + if value =~ /^0?[0-7]{1,4}$/ + return value.to_i(8) + else + raise Puppet::Error, "The umask specification is invalid: #{value.inspect}" + end + end + end + newparam(:timeout) do desc "The maximum time the command should take. If the command takes longer than the timeout, the command is considered to have failed @@ -341,7 +353,7 @@ module Puppet # If the file exists, return false (i.e., don't run the command), # else return true def check(value) - ! FileTest.exists?(value) + ! Puppet::FileSystem::File.exist?(value) end end diff --git a/lib/puppet/type/file.rb b/lib/puppet/type/file.rb index 862ab41ba..df6af7847 100644 --- a/lib/puppet/type/file.rb +++ b/lib/puppet/type/file.rb @@ -34,6 +34,9 @@ Puppet::Type.newtype(:file) do file, the file resource will autorequire them. If Puppet is managing any parent directories of a file, the file resource will autorequire them." + feature :manages_symlinks, + "The provider can manage symbolic links." + def self.title_patterns [ [ /^(.*?)\/*\Z/m, [ [ :path ] ] ] ] end @@ -54,7 +57,12 @@ Puppet::Type.newtype(:file) do end munge do |value| - ::File.join(::File.split(::File.expand_path(value))) + if value.start_with?('//') and ::File.basename(value) == "/" + # This is a UNC path pointing to a share, so don't add a trailing slash + ::File.expand_path(value) + else + ::File.join(::File.split(::File.expand_path(value))) + end end end @@ -118,15 +126,16 @@ Puppet::Type.newtype(:file) do end newparam(:recurse) do - desc "Whether and how deeply to do recursive - management. Options are: + desc "Whether and how to do recursive file management. Options are: * `inf,true` --- Regular style recursion on both remote and local - directory structure. - * `remote` --- Descends recursively into the remote directory - but not the local directory. Allows copying of + directory structure. See `recurselimit` to specify a limit to the + recursion depth. + * `remote` --- Descends recursively into the remote (source) directory + but not the local (destination) directory. Allows copying of a few files into a directory containing many unmanaged files without scanning all the local files. + This can only be used when a source parameter is specified. * `false` --- Default of no recursion. " @@ -682,7 +691,7 @@ Puppet::Type.newtype(:file) do end @stat = begin - ::File.send(method, self[:path]) + Puppet::FileSystem::File.new(self[:path]).send(method) rescue Errno::ENOENT => error nil rescue Errno::ENOTDIR => error @@ -707,7 +716,7 @@ Puppet::Type.newtype(:file) do use_temporary_file = write_temporary_file? if use_temporary_file path = "#{self[:path]}.puppettmp_#{rand(10000)}" - path = "#{self[:path]}.puppettmp_#{rand(10000)}" while ::File.exists?(path) or ::File.symlink?(path) + path = "#{self[:path]}.puppettmp_#{rand(10000)}" while Puppet::FileSystem::File.exist?(path) or Puppet::FileSystem::File.new(path).symlink? else path = self[:path] end @@ -727,7 +736,7 @@ Puppet::Type.newtype(:file) do fail "Could not rename temporary file #{path} to #{self[:path]}: #{detail}" ensure # Make sure the created file gets removed - ::File.unlink(path) if FileTest.exists?(path) + Puppet::FileSystem::File.unlink(path) if Puppet::FileSystem::File.exist?(path) end end @@ -777,7 +786,7 @@ Puppet::Type.newtype(:file) do # @api private def remove_file(current_type, wanted_type) debug "Removing existing #{current_type} for replacement with #{wanted_type}" - ::File.unlink(self[:path]) + Puppet::FileSystem::File.unlink(self[:path]) stat_needed true end diff --git a/lib/puppet/type/file/checksum.rb b/lib/puppet/type/file/checksum.rb index 3fd37d455..3fd37d455 100755..100644 --- a/lib/puppet/type/file/checksum.rb +++ b/lib/puppet/type/file/checksum.rb diff --git a/lib/puppet/type/file/content.rb b/lib/puppet/type/file/content.rb index 5807d1885..6a2eef676 100755..100644 --- a/lib/puppet/type/file/content.rb +++ b/lib/puppet/type/file/content.rb @@ -133,6 +133,9 @@ module Puppet # Make sure we're also managing the checksum property. def should=(value) + # treat the value as a bytestring, in Ruby versions that support it, regardless of the encoding + # in which it has been supplied + value = value.clone.force_encoding(Encoding::ASCII_8BIT) if value.respond_to?(:force_encoding) @resource.newattr(:checksum) unless @resource.parameter(:checksum) super end diff --git a/lib/puppet/type/file/ensure.rb b/lib/puppet/type/file/ensure.rb index de19e10eb..d75c8f6ac 100755..100644 --- a/lib/puppet/type/file/ensure.rb +++ b/lib/puppet/type/file/ensure.rb @@ -5,17 +5,35 @@ module Puppet require 'puppet/util/symbolic_file_mode' include Puppet::Util::SymbolicFileMode - desc <<-'EOT' - Whether to create files that don't currently exist. - Possible values are `absent`, `present`, `file`, `directory`, and `link`. - Specifying `present` will match any form of file existence, and - if the file is missing will create an empty file. Specifying - `absent` will delete the file (or directory, if `recurse => true` and - `force => true`). Specifying `link` requires that you also set the `target` - attribute; note that symlinks cannot be managed on Windows. - - If you specify the path to another file as the ensure value, it is - equivalent to specifying `link` and using that path as the `target`: + desc <<-EOT + Whether the file should exist, and if so what kind of file it should be. + Possible values are `present`, `absent`, `file`, `directory`, and `link`. + + * `present` will accept any form of file existence, and will create a + normal file if the file is missing. (The file will have no content + unless the `content` or `source` attribute is used.) + * `absent` will make sure the file doesn't exist, deleting it + if necessary. + * `file` will make sure it's a normal file, and enables use of the + `content` or `source` attribute. + * `directory` will make sure it's a directory, and enables use of the + `source`, `recurse`, `recurselimit`, `ignore`, and `purge` attributes. + * `link` will make sure the file is a symlink, and **requires** that you + also set the `target` attribute. Symlinks are supported on all Posix + systems and on Windows Vista / 2008 and higher. On Windows, managing + symlinks requires puppet agent's user account to have the "Create + Symbolic Links" privilege; this can be configured in the "User Rights + Assignment" section in the Windows policy editor. By default, puppet + agent runs as the Administrator account, which does have this privilege. + + Puppet avoids destroying directories unless the `force` attribute is set + to `true`. This means that if a file is currently a directory, setting + `ensure` to anything but `directory` or `present` will cause Puppet to + skip managing the resource and log either a notice or an error. + + There is one other non-standard value for `ensure`. If you specify the + path to another file as the ensure value, it is equivalent to specifying + `link` and using that path as the `target`: # Equivalent resources: @@ -36,7 +54,7 @@ module Puppet nodefault newvalue(:absent) do - File.unlink(@resource[:path]) + Puppet::FileSystem::File.unlink(@resource[:path]) end aliasvalue(:false, :absent) @@ -61,7 +79,7 @@ module Puppet newvalue(:directory, :event => :directory_created) do mode = @resource.should(:mode) parent = File.dirname(@resource[:path]) - unless FileTest.exists? parent + unless Puppet::FileSystem::File.exist? parent raise Puppet::Error, "Cannot create #{@resource[:path]}; parent directory #{parent} does not exist" end @@ -77,7 +95,7 @@ module Puppet end - newvalue(:link, :event => :link_created) do + newvalue(:link, :event => :link_created, :required_features => :manages_symlinks) do fail "Cannot create a symlink without a target" unless property = resource.property(:target) property.retrieve property.mklink @@ -121,7 +139,7 @@ module Puppet def check basedir = File.dirname(@resource[:path]) - if ! FileTest.exists?(basedir) + if ! Puppet::FileSystem::File.exist?(basedir) raise Puppet::Error, "Can not create #{@resource.title}; parent directory does not exist" elsif ! FileTest.directory?(basedir) diff --git a/lib/puppet/type/file/group.rb b/lib/puppet/type/file/group.rb index a1ae66518..a1ae66518 100755..100644 --- a/lib/puppet/type/file/group.rb +++ b/lib/puppet/type/file/group.rb diff --git a/lib/puppet/type/file/mode.rb b/lib/puppet/type/file/mode.rb index b6b4becf2..682e744cd 100755..100644 --- a/lib/puppet/type/file/mode.rb +++ b/lib/puppet/type/file/mode.rb @@ -39,8 +39,12 @@ module Puppet * g (group's current permissions) * o (other's current permissions) - Thus, mode `0664` could be represented symbolically as either `a=r,ug+w` or - `ug=rw,o=r`. See the manual page for GNU or BSD `chmod` for more details + Thus, mode `0664` could be represented symbolically as either `a=r,ug+w` + or `ug=rw,o=r`. However, symbolic modes are more expressive than numeric + modes: a mode only affects the specified bits, so `mode => 'ug+w'` will + set the user and group write bits, without affecting any other bits. + + See the manual page for GNU or BSD `chmod` for more details on numeric and symbolic modes. On Windows, permissions are translated as follows: diff --git a/lib/puppet/type/file/owner.rb b/lib/puppet/type/file/owner.rb index 3b61b400c..3b61b400c 100755..100644 --- a/lib/puppet/type/file/owner.rb +++ b/lib/puppet/type/file/owner.rb diff --git a/lib/puppet/type/file/source.rb b/lib/puppet/type/file/source.rb index 000636b6b..7f88e692a 100755..100644 --- a/lib/puppet/type/file/source.rb +++ b/lib/puppet/type/file/source.rb @@ -1,4 +1,3 @@ - require 'puppet/file_serving/content' require 'puppet/file_serving/metadata' @@ -85,7 +84,7 @@ module Puppet def change_to_s(currentvalue, newvalue) # newvalue = "{md5}#{@metadata.checksum}" - if @resource.property(:ensure).retrieve == :absent + if resource.property(:ensure).retrieve == :absent return "creating from source #{metadata.source} with contents #{metadata.checksum}" else return "replacing from source #{metadata.source} with contents #{metadata.checksum}" @@ -111,28 +110,48 @@ module Puppet def copy_source_values devfail "Somehow got asked to copy source values without any metadata" unless metadata + # conditionally copy :checksum + if metadata.ftype != "directory" && !(metadata.ftype == "link" && metadata.links == :manage) + copy_source_value(:checksum) + end + # Take each of the stats and set them as states on the local file # if a value has not already been provided. - [:owner, :mode, :group, :checksum].each do |metadata_method| - param_name = (metadata_method == :checksum) ? :content : metadata_method + [:owner, :mode, :group].each do |metadata_method| next if metadata_method == :owner and !Puppet.features.root? - next if metadata_method == :checksum and metadata.ftype == "directory" - next if metadata_method == :checksum and metadata.ftype == "link" and metadata.links == :manage + next if metadata_method == :group and !Puppet.features.root? if Puppet.features.microsoft_windows? - next if [:owner, :group].include?(metadata_method) and !local? + # Warn on Windows if source permissions are being used and the file resource + # does not have mode owner and group all set (which would take precedence). + if [:use, :use_when_creating].include?(resource[:source_permissions]) && + (resource[:owner] == nil || resource[:group] == nil || resource[:mode] == nil) + + warning = "Copying %s from the source" << + " file on Windows is deprecated;" << + " use source_permissions => ignore." + Puppet.deprecation_warning(warning % 'owner/mode/group') + resource.debug(warning % metadata_method.to_s) + end + # But never try to copy remote owner/group on Windows + next if [:owner, :group].include?(metadata_method) && !local? end - if resource[param_name].nil? or resource[param_name] == :absent - resource[param_name] = metadata.send(metadata_method) + case resource[:source_permissions] + when :ignore + next + when :use_when_creating + next if Puppet::FileSystem::File.exist?(resource[:path]) end + + copy_source_value(metadata_method) end if resource[:ensure] == :absent # We know all we need to elsif metadata.ftype != "link" resource[:ensure] = metadata.ftype - elsif @resource[:links] == :follow + elsif resource[:links] == :follow resource[:ensure] = :present else resource[:ensure] = "link" @@ -140,10 +159,6 @@ module Puppet end end - def found? - ! (metadata.nil? or metadata.ftype.nil?) - end - attr_writer :metadata # Provide, and retrieve if necessary, the metadata for this file. Fail @@ -195,5 +210,41 @@ module Puppet def uri @uri ||= URI.parse(URI.escape(metadata.source)) end + + private + def found? + ! (metadata.nil? or metadata.ftype.nil?) + end + + def copy_source_value(metadata_method) + param_name = (metadata_method == :checksum) ? :content : metadata_method + if resource[param_name].nil? or resource[param_name] == :absent + resource[param_name] = metadata.send(metadata_method) + end + end + end + + Puppet::Type.type(:file).newparam(:source_permissions) do + desc <<-'EOT' + Whether (and how) Puppet should copy owner, group, and mode permissions from + the `source` to `file` resources when the permissions are not explicitly + specified. (In all cases, explicit permissions will take precedence.) + Valid values are `use`, `use_when_creating`, and `ignore`: + + * `use` (the default) will cause Puppet to apply the owner, group, + and mode from the `source` to any files it is managing. + * `use_when_creating` will only apply the owner, group, and mode from the + `source` when creating a file; existing files will not have their permissions + overwritten. + * `ignore` will never apply the owner, group, or mode from the `source` when + managing a file. When creating new files without explicit permissions, + the permissions they receive will depend on platform-specific behavior. + On POSIX, Puppet will use the umask of the user it is running as. On + Windows, Puppet will use the default DACL associated with the user it is + running as. + EOT + + defaultto :use + newvalues(:use, :use_when_creating, :ignore) end end diff --git a/lib/puppet/type/file/target.rb b/lib/puppet/type/file/target.rb index e1dbdeae2..08a2a97df 100644 --- a/lib/puppet/type/file/target.rb +++ b/lib/puppet/type/file/target.rb @@ -32,7 +32,7 @@ module Puppet # Create our link. def mklink - raise Puppet::Error, "Cannot symlink on Microsoft Windows" if Puppet.features.microsoft_windows? + raise Puppet::Error, "Cannot symlink on this platform version" if !provider.feature?(:manages_symlinks) target = self.should @@ -40,17 +40,17 @@ module Puppet # it doesn't determine what's removed. @resource.remove_existing(target) - raise Puppet::Error, "Could not remove existing file" if FileTest.exists?(@resource[:path]) + raise Puppet::Error, "Could not remove existing file" if Puppet::FileSystem::File.exist?(@resource[:path]) Dir.chdir(File.dirname(@resource[:path])) do Puppet::Util::SUIDManager.asuser(@resource.asuser) do mode = @resource.should(:mode) if mode Puppet::Util.withumask(000) do - File.symlink(target, @resource[:path]) + Puppet::FileSystem::File.new(target).symlink(@resource[:path]) end else - File.symlink(target, @resource[:path]) + Puppet::FileSystem::File.new(target).symlink(@resource[:path]) end end @@ -63,7 +63,7 @@ module Puppet def insync?(currentvalue) if [:nochange, :notlink].include?(self.should) or @resource.recurse? return true - elsif ! @resource.replace? and File.exists?(@resource[:path]) + elsif ! @resource.replace? and Puppet::FileSystem::File.exist?(@resource[:path]) return true else return super(currentvalue) @@ -74,7 +74,7 @@ module Puppet def retrieve if stat = @resource.stat if stat.ftype == "link" - return File.readlink(@resource[:path]) + return Puppet::FileSystem::File.new(@resource[:path]).readlink else return :notlink end diff --git a/lib/puppet/type/file/type.rb b/lib/puppet/type/file/type.rb index 38f301573..38f301573 100755..100644 --- a/lib/puppet/type/file/type.rb +++ b/lib/puppet/type/file/type.rb diff --git a/lib/puppet/type/filebucket.rb b/lib/puppet/type/filebucket.rb index cbb551464..cbb551464 100755..100644 --- a/lib/puppet/type/filebucket.rb +++ b/lib/puppet/type/filebucket.rb diff --git a/lib/puppet/type/group.rb b/lib/puppet/type/group.rb index a66a29452..cf9eb2382 100755..100644 --- a/lib/puppet/type/group.rb +++ b/lib/puppet/type/group.rb @@ -87,6 +87,24 @@ module Puppet newvalue = newvalue.join(",") super(currentvalue, newvalue) end + + def insync?(current) + if provider.respond_to?(:members_insync?) + return provider.members_insync?(current, @should) + end + + super(current) + end + + def is_to_s(currentvalue) + if provider.respond_to?(:members_to_s) + currentvalue = '' if currentvalue.nil? + return provider.members_to_s(currentvalue.split(',')) + end + + super(currentvalue) + end + alias :should_to_s :is_to_s end newparam(:auth_membership) do diff --git a/lib/puppet/type/host.rb b/lib/puppet/type/host.rb index d63b37d1a..d63b37d1a 100755..100644 --- a/lib/puppet/type/host.rb +++ b/lib/puppet/type/host.rb diff --git a/lib/puppet/type/k5login.rb b/lib/puppet/type/k5login.rb index b2fff2793..a87b3e7d8 100644 --- a/lib/puppet/type/k5login.rb +++ b/lib/puppet/type/k5login.rb @@ -37,7 +37,7 @@ Puppet::Type.newtype(:k5login) do # Does this file exist? def exists? - File.exists?(@resource[:name]) + Puppet::FileSystem::File.exist?(@resource[:name]) end # create the file @@ -51,12 +51,12 @@ Puppet::Type.newtype(:k5login) do # remove the file def destroy - File.unlink(@resource[:name]) + Puppet::FileSystem::File.unlink(@resource[:name]) end # Return the principals def principals(dummy_argument=:work_arround_for_ruby_GC_bug) - if File.exists?(@resource[:name]) + if Puppet::FileSystem::File.exist?(@resource[:name]) File.readlines(@resource[:name]).collect { |line| line.chomp } else :absent @@ -70,7 +70,7 @@ Puppet::Type.newtype(:k5login) do # Return the mode as an octal string, not as an integer def mode - "%o" % (File.stat(@resource[:name]).mode & 007777) + "%o" % (Puppet::FileSystem::File.new(@resource[:name]).stat.mode & 007777) end # Set the file mode, converting from a string to an integer. diff --git a/lib/puppet/type/mailalias.rb b/lib/puppet/type/mailalias.rb index ce7ca790b..ce7ca790b 100755..100644 --- a/lib/puppet/type/mailalias.rb +++ b/lib/puppet/type/mailalias.rb diff --git a/lib/puppet/type/maillist.rb b/lib/puppet/type/maillist.rb index 4e0542c83..4e0542c83 100755..100644 --- a/lib/puppet/type/maillist.rb +++ b/lib/puppet/type/maillist.rb diff --git a/lib/puppet/type/mount.rb b/lib/puppet/type/mount.rb index 27624e31b..acad4927d 100755..100644 --- a/lib/puppet/type/mount.rb +++ b/lib/puppet/type/mount.rb @@ -8,7 +8,11 @@ module Puppet on the value of the 'ensure' parameter. Note that if a `mount` receives an event from another resource, - it will try to remount the filesystems if `ensure` is set to `mounted`." + it will try to remount the filesystems if `ensure` is set to `mounted`. + + **Autorequires:** If Puppet is managing any parents of a mount resource --- + that is, other mount points higher up in the filesystem --- the child + mount will autorequire them." feature :refreshable, "The provider can remount the filesystem.", :methods => [:remount] @@ -268,5 +272,15 @@ module Puppet return property.value end end + + # Ensure that mounts higher up in the filesystem are mounted first + autorequire(:mount) do + dependencies = [] + Pathname.new(@parameters[:name].value).ascend do |parent| + dependencies.unshift parent.to_s + end + dependencies[0..-2] + end + end end diff --git a/lib/puppet/type/package.rb b/lib/puppet/type/package.rb index 5500f4499..e7ea1125b 100644 --- a/lib/puppet/type/package.rb +++ b/lib/puppet/type/package.rb @@ -58,7 +58,7 @@ module Puppet retrieve by specifying a version number or `latest` as the ensure value. On packaging systems that manage configuration files separately from "normal" system files, you can uninstall config files by - specifying `purged` as the ensure value. + specifying `purged` as the ensure value. This defaults to `installed`. EOT attr_accessor :latest @@ -225,6 +225,12 @@ module Puppet " isnamevar + + validate do |value| + if !value.is_a?(String) + raise ArgumentError, "Name must be a String not #{value.class}" + end + end end newparam(:source) do diff --git a/lib/puppet/type/port.rb b/lib/puppet/type/port.rb index e19988515..e19988515 100755..100644 --- a/lib/puppet/type/port.rb +++ b/lib/puppet/type/port.rb diff --git a/lib/puppet/type/schedule.rb b/lib/puppet/type/schedule.rb index e1586cbdb..d67831e89 100755..100644 --- a/lib/puppet/type/schedule.rb +++ b/lib/puppet/type/schedule.rb @@ -356,10 +356,10 @@ module Puppet of the range, not necessarily the day that it is when it matches. For example, consider this schedule: - schedule { 'maintenance_window': - range => '22:00 - 04:00', - weekday => 'Saturday', - } + schedule { 'maintenance_window': + range => '22:00 - 04:00', + weekday => 'Saturday', + } This will match at 11 PM on Saturday and 2 AM on Sunday, but not at 2 AM on Saturday. @@ -418,6 +418,11 @@ module Puppet def self.mkdefaultschedules result = [] + unless Puppet[:default_schedules] + Puppet.debug "Not creating default schedules: default_schedules is false" + return result + end + Puppet.debug "Creating default schedules" result << self.new( diff --git a/lib/puppet/type/service.rb b/lib/puppet/type/service.rb index 0a74649cf..e4876664e 100644 --- a/lib/puppet/type/service.rb +++ b/lib/puppet/type/service.rb @@ -180,7 +180,7 @@ module Puppet automatically, usually by looking for the service in the process table. - [lsb-exit-codes]: http://refspecs.freestandards.org/LSB_3.1.1/LSB-Core-generic/LSB-Core-generic/iniscrptact.html" + [lsb-exit-codes]: http://refspecs.linuxfoundation.org/LSB_4.1.0/LSB-Core-generic/LSB-Core-generic/iniscrptact.html" end newparam(:stop) do diff --git a/lib/puppet/type/sshkey.rb b/lib/puppet/type/sshkey.rb index 41948ed98..41948ed98 100755..100644 --- a/lib/puppet/type/sshkey.rb +++ b/lib/puppet/type/sshkey.rb diff --git a/lib/puppet/type/tidy.rb b/lib/puppet/type/tidy.rb index 8297cf938..83ac3322d 100755..100644 --- a/lib/puppet/type/tidy.rb +++ b/lib/puppet/type/tidy.rb @@ -312,7 +312,7 @@ Puppet::Type.newtype(:tidy) do def stat(path) begin - ::File.lstat(path) + Puppet::FileSystem::File.new(path).lstat rescue Errno::ENOENT => error info "File does not exist" return nil diff --git a/lib/puppet/type/user.rb b/lib/puppet/type/user.rb index 9ec6e922f..d0ca0f78c 100755..100644 --- a/lib/puppet/type/user.rb +++ b/lib/puppet/type/user.rb @@ -124,8 +124,9 @@ module Puppet newproperty(:gid) do desc "The user's primary group. Can be specified numerically or by name. - Note that users on Windows systems do not have a primary group; manage groups - with the `groups` attribute instead." + This attribute is not supported on Windows systems; use the `groups` + attribute instead. (On Windows, designating a primary group is only + meaningful for domain accounts, which Puppet does not currently manage.)" munge do |value| if value.is_a?(String) and value =~ /^[-0-9]+$/ @@ -163,6 +164,9 @@ module Puppet newproperty(:comment) do desc "A description of the user. Generally the user's full name." + munge do |v| + v.respond_to?(:encode) ? v.encode(Encoding::ASCII_8BIT) : v + end end newproperty(:shell) do diff --git a/lib/puppet/type/yumrepo.rb b/lib/puppet/type/yumrepo.rb index bd52121c9..f71736045 100644 --- a/lib/puppet/type/yumrepo.rb +++ b/lib/puppet/type/yumrepo.rb @@ -170,7 +170,7 @@ module Puppet unless Puppet[:noop] target_mode = 0644 # FIXME: should be configurable inifile.each_file do |file| - current_mode = ::File.stat(file).mode & 0777 + current_mode = Puppet::FileSystem::File.new(file).stat.mode & 0777 unless current_mode == target_mode Puppet::info "changing mode of #{file} from %03o to %03o" % [current_mode, target_mode] ::File.chmod(target_mode, file) @@ -332,7 +332,7 @@ module Puppet end newproperty(:cost, :parent => Puppet::IniProperty) do - desc "Cost of this repository.\n#{ABSENT_DOC}" + desc "Cost of this repository. #{ABSENT_DOC}" newvalue(:absent) { self.should = :absent } newvalue(%r{\d+}) { } end @@ -364,28 +364,30 @@ module Puppet newproperty(:sslcacert, :parent => Puppet::IniProperty) do desc "Path to the directory containing the databases of the - certificate authorities yum should use to verify SSL certificates.\n#{ABSENT_DOC}" + certificate authorities yum should use to verify SSL certificates. + #{ABSENT_DOC}" newvalue(:absent) { self.should = :absent } newvalue(/.*/) { } end newproperty(:sslverify, :parent => Puppet::IniProperty) do desc "Should yum verify SSL certificates/hosts at all. - Possible values are 'True' or 'False'.\n#{ABSENT_DOC}" + Possible values are 'True' or 'False'. + #{ABSENT_DOC}" newvalue(:absent) { self.should = :absent } newvalue(%r(True|False)) { } end newproperty(:sslclientcert, :parent => Puppet::IniProperty) do desc "Path to the SSL client certificate yum should use to connect - to repos/remote sites.\n#{ABSENT_DOC}" + to repos/remote sites. #{ABSENT_DOC}" newvalue(:absent) { self.should = :absent } newvalue(/.*/) { } end newproperty(:sslclientkey, :parent => Puppet::IniProperty) do desc "Path to the SSL client key yum should use to connect - to repos/remote sites.\n#{ABSENT_DOC}" + to repos/remote sites. #{ABSENT_DOC}" newvalue(:absent) { self.should = :absent } newvalue(/.*/) { } end diff --git a/lib/puppet/type/zpool.rb b/lib/puppet/type/zpool.rb index 043deecc4..043deecc4 100755..100644 --- a/lib/puppet/type/zpool.rb +++ b/lib/puppet/type/zpool.rb diff --git a/lib/puppet/util.rb b/lib/puppet/util.rb index c1a82340e..4c04f37aa 100644 --- a/lib/puppet/util.rb +++ b/lib/puppet/util.rb @@ -1,12 +1,9 @@ # A module to collect utility functions. require 'English' -require 'puppet/external/lock' require 'puppet/error' require 'puppet/util/execution_stub' require 'uri' -require 'sync' -require 'monitor' require 'tempfile' require 'pathname' require 'ostruct' @@ -26,9 +23,6 @@ module Util extend Puppet::Util::SymbolicFileMode - @@sync_objects = {}.extend MonitorMixin - - def self.activerecord_version if (defined?(::ActiveRecord) and defined?(::ActiveRecord::VERSION) and defined?(::ActiveRecord::VERSION::MAJOR) and defined?(::ActiveRecord::VERSION::MINOR)) ([::ActiveRecord::VERSION::MAJOR, ::ActiveRecord::VERSION::MINOR].join('.').to_f) @@ -67,20 +61,6 @@ module Util end - def self.synchronize_on(x,type) - sync_object,users = 0,1 - begin - @@sync_objects.synchronize { - (@@sync_objects[x] ||= [Sync.new,0])[users] += 1 - } - @@sync_objects[x][sync_object].synchronize(type) { yield } - ensure - @@sync_objects.synchronize { - @@sync_objects.delete(x) unless (@@sync_objects[x][users] -= 1) > 0 - } - end - end - # Change the process to a different user def self.chuser if group = Puppet[:group] @@ -154,7 +134,6 @@ module Util end end - def benchmark(*args) msg = args.pop level = args.pop @@ -187,6 +166,7 @@ module Util yield end end + module_function :benchmark # Resolve a path for an executable to the absolute path. This tries to behave # in the same manner as the unix `which` command and uses the `PATH` @@ -270,7 +250,7 @@ module Util if Puppet.features.microsoft_windows? path = path.gsub(/\\/, '/') - if unc = /^\/\/([^\/]+)(\/[^\/]+)/.match(path) + if unc = /^\/\/([^\/]+)(\/.+)/.match(path) params[:host] = unc[1] path = unc[2] elsif path =~ /^[a-z]:\//i @@ -320,13 +300,6 @@ module Util end module_function :safe_posix_fork - # Create an exclusive lock. - def threadlock(resource, type = Sync::EX) - Puppet::Util.synchronize_on(resource,type) { yield } - end - - module_function :benchmark - def memory unless defined?(@pmap) @pmap = which('pmap') @@ -361,6 +334,7 @@ module Util # Because IO#binread is only available in 1.9 def binread(file) + Puppet.deprecation_warning("Puppet::Util.binread is deprecated. Read the file without this method as it will be removed in a future version.") File.open(file, 'rb') { |f| f.read } end module_function :binread @@ -467,7 +441,7 @@ module Util # This might race, but there are enough possible cases that there # isn't a good, solid "better" way to do this, and the next call # should fail in the same way anyhow. - raise if have_retried or File.exist?(file) + raise if have_retried or Puppet::FileSystem::File.exist?(file) have_retried = true # OK, so, we can't replace a file that doesn't exist, so let us put @@ -499,7 +473,6 @@ module Util end module_function :replace_file - # Executes a block of code, wrapped with some special exception handling. Causes the ruby interpreter to # exit if the block throws an exception. # diff --git a/lib/puppet/util/adsi.rb b/lib/puppet/util/adsi.rb index a4b02d537..98639eabd 100644 --- a/lib/puppet/util/adsi.rb +++ b/lib/puppet/util/adsi.rb @@ -41,6 +41,11 @@ module Puppet::Util::ADSI "winmgmts:{impersonationLevel=impersonate}!//#{host}/root/cimv2" end + def sid_uri(sid) + raise Puppet::Error.new( "Must use a valid SID object" ) if !sid.kind_of?(Win32::Security::SID) + "WinNT://#{sid.to_s}" + end + def uri(resource_name, resource_type, host = '.') "#{computer_uri(host)}/#{resource_name},#{resource_type}" end @@ -64,22 +69,40 @@ module Puppet::Util::ADSI extend Enumerable attr_accessor :native_user - attr_reader :name + attr_reader :name, :sid def initialize(name, native_user = nil) @name = name @native_user = native_user end + def self.parse_name(name) + if name =~ /\// + raise Puppet::Error.new( "Value must be in DOMAIN\\user style syntax" ) + end + + matches = name.scan(/((.*)\\)?(.*)/) + domain = matches[0][1] || '.' + account = matches[0][2] + + return account, domain + end + def native_user - @native_user ||= Puppet::Util::ADSI.connect(uri) + @native_user ||= Puppet::Util::ADSI.connect(self.class.uri(*self.class.parse_name(@name))) + end + + def sid + @sid ||= Puppet::Util::Windows::Security.octet_string_to_sid_object(native_user.objectSID) end def self.uri(name, host = '.') + host = '.' if ['NT AUTHORITY', 'BUILTIN', Socket.gethostname].include?(host) + Puppet::Util::ADSI.uri(name, 'user', host) end def uri - self.class.uri(name) + self.class.uri(sid.account, sid.domain) end def self.logon(name, password) @@ -131,14 +154,14 @@ module Puppet::Util::ADSI def add_to_groups(*group_names) group_names.each do |group_name| - Puppet::Util::ADSI::Group.new(group_name).add_member(@name) + Puppet::Util::ADSI::Group.new(group_name).add_member_sids(sid) end end alias add_to_group add_to_groups def remove_from_groups(*group_names) group_names.each do |group_name| - Puppet::Util::ADSI::Group.new(group_name).remove_member(@name) + Puppet::Util::ADSI::Group.new(group_name).remove_member_sids(sid) end end alias remove_from_group remove_from_groups @@ -167,7 +190,7 @@ module Puppet::Util::ADSI end def self.exists?(name) - Puppet::Util::ADSI::connectable?(User.uri(name)) + Puppet::Util::ADSI::connectable?(User.uri(*User.parse_name(name))) end def self.delete(name) @@ -175,7 +198,7 @@ module Puppet::Util::ADSI end def self.each(&block) - wql = Puppet::Util::ADSI.execquery("select name from win32_useraccount") + wql = Puppet::Util::ADSI.execquery('select name from win32_useraccount where localaccount = "TRUE"') users = [] wql.each do |u| @@ -233,20 +256,44 @@ module Puppet::Util::ADSI self end - def add_members(*names) - names.each do |name| - native_group.Add(Puppet::Util::ADSI::User.uri(name, Puppet::Util::ADSI.computer_name)) + def self.name_sid_hash(names) + return [] if names.nil? or names.empty? + + sids = names.map do |name| + sid = Puppet::Util::Windows::Security.name_to_sid_object(name) + raise Puppet::Error.new( "Could not resolve username: #{name}" ) if !sid + [sid.to_s, sid] end + + Hash[ sids ] + end + + def add_members(*names) + Puppet.deprecation_warning('Puppet::Util::ADSI::Group#add_members is deprecated; please use Puppet::Util::ADSI::Group#add_member_sids') + sids = self.class.name_sid_hash(names) + add_member_sids(*sids.values) end alias add_member add_members def remove_members(*names) - names.each do |name| - native_group.Remove(Puppet::Util::ADSI::User.uri(name, Puppet::Util::ADSI.computer_name)) - end + Puppet.deprecation_warning('Puppet::Util::ADSI::Group#remove_members is deprecated; please use Puppet::Util::ADSI::Group#remove_member_sids') + sids = self.class.name_sid_hash(names) + remove_member_sids(*sids.values) end alias remove_member remove_members + def add_member_sids(*sids) + sids.each do |sid| + native_group.Add(Puppet::Util::ADSI.sid_uri(sid)) + end + end + + def remove_member_sids(*sids) + sids.each do |sid| + native_group.Remove(Puppet::Util::ADSI.sid_uri(sid)) + end + end + def members # WIN32OLE objects aren't enumerable, so no map members = [] @@ -254,18 +301,27 @@ module Puppet::Util::ADSI members end + def member_sids + sids = [] + native_group.Members.each do |m| + sids << Puppet::Util::Windows::Security.octet_string_to_sid_object(m.objectSID) + end + sids + end + def set_members(desired_members) return if desired_members.nil? or desired_members.empty? - current_members = self.members + current_hash = Hash[ self.member_sids.map { |sid| [sid.to_s, sid] } ] + desired_hash = self.class.name_sid_hash(desired_members) # First we add all missing members - members_to_add = desired_members - current_members - add_members(*members_to_add) + members_to_add = (desired_hash.keys - current_hash.keys).map { |sid| desired_hash[sid] } + add_member_sids(*members_to_add) # Then we remove all extra members - members_to_remove = current_members - desired_members - remove_members(*members_to_remove) + members_to_remove = (current_hash.keys - desired_hash.keys).map { |sid| current_hash[sid] } + remove_member_sids(*members_to_remove) end def self.create(name) @@ -283,7 +339,7 @@ module Puppet::Util::ADSI end def self.each(&block) - wql = Puppet::Util::ADSI.execquery( "select name from win32_group" ) + wql = Puppet::Util::ADSI.execquery( 'select name from win32_group where localaccount = "TRUE"' ) groups = [] wql.each do |g| diff --git a/lib/puppet/util/autoload.rb b/lib/puppet/util/autoload.rb index fc407a971..82e0560e8 100644 --- a/lib/puppet/util/autoload.rb +++ b/lib/puppet/util/autoload.rb @@ -85,7 +85,7 @@ class Puppet::Util::Autoload # returns nil if no file is found def get_file(name, env=nil) name = name + '.rb' unless name =~ /\.rb$/ - path = search_directories(env).find { |dir| File.exist?(File.join(dir, name)) } + path = search_directories(env).find { |dir| Puppet::FileSystem::File.exist?(File.join(dir, name)) } path and File.join(path, name) end @@ -112,7 +112,7 @@ class Puppet::Util::Autoload # We're using a per-thread cache of module directories so that we don't # scan the filesystem each time we try to load something. This is reset # at the beginning of compilation and at the end of an agent run. - Thread.current[:env_module_directories] ||= {} + $env_module_directories ||= {} # This is a little bit of a hack. Basically, the autoloader is being @@ -136,7 +136,7 @@ class Puppet::Util::Autoload # --cprice 2012-03-16 if Puppet.settings.app_defaults_initialized? # if the app defaults have been initialized then it should be safe to access the module path setting. - Thread.current[:env_module_directories][real_env] ||= real_env.modulepath.collect do |dir| + $env_module_directories[real_env] ||= real_env.modulepath.collect do |dir| Dir.entries(dir).reject { |f| f =~ /^\./ }.collect { |f| File.join(dir, f) } end.flatten.collect { |d| File.join(d, "lib") }.find_all do |d| FileTest.directory?(d) diff --git a/lib/puppet/util/backups.rb b/lib/puppet/util/backups.rb index b1daf78fa..8ae14c190 100644 --- a/lib/puppet/util/backups.rb +++ b/lib/puppet/util/backups.rb @@ -10,7 +10,7 @@ module Puppet::Util::Backups # let the path be specified file ||= self[:path] - return true unless FileTest.exists?(file) + return true unless Puppet::FileSystem::File.exist?(file) return(self.bucket ? perform_backup_with_bucket(file) : perform_backup_with_backuplocal(file, self[:backup])) end @@ -19,7 +19,7 @@ module Puppet::Util::Backups def perform_backup_with_bucket(fileobj) file = (fileobj.class == String) ? fileobj : fileobj.name - case File.lstat(file).ftype + case Puppet::FileSystem::File.new(file).lstat.ftype when "directory" # we don't need to backup directories when recurse is on return true if self[:recurse] @@ -58,7 +58,7 @@ module Puppet::Util::Backups end begin - stat = File.send(method, newfile) + stat = Puppet::FileSystem::File.new(newfile).send(method) rescue Errno::ENOENT return end @@ -70,7 +70,7 @@ module Puppet::Util::Backups info "Removing old backup of type #{stat.ftype}" begin - File.unlink(newfile) + Puppet::FileSystem::File.unlink(newfile) rescue => detail message = "Could not remove old backup: #{detail}" self.log_exception(detail, message) diff --git a/lib/puppet/util/cacher.rb b/lib/puppet/util/cacher.rb index 136c9973e..24017de32 100644 --- a/lib/puppet/util/cacher.rb +++ b/lib/puppet/util/cacher.rb @@ -1,5 +1,3 @@ -require 'monitor' - module Puppet::Util::Cacher # Our module has been extended in a class; we can only add the Instance methods, # which become *class* methods in the class. @@ -33,10 +31,8 @@ module Puppet::Util::Cacher define_method(name.to_s + "=") do |value| # Make sure the cache timestamp is set - value_cache.synchronize do - value_cache[name] = value - set_expiration(name) - end + value_cache[name] = value + set_expiration(name) end end @@ -55,13 +51,11 @@ module Puppet::Util::Cacher private def cached_value(name) - value_cache.synchronize do - if value_cache[name].nil? or expired_by_ttl?(name) - value_cache[name] = send("init_#{name}") - set_expiration(name) - end - value_cache[name] + if value_cache[name].nil? or expired_by_ttl?(name) + value_cache[name] = send("init_#{name}") + set_expiration(name) end + value_cache[name] end def expired_by_ttl?(name) @@ -74,7 +68,7 @@ module Puppet::Util::Cacher end def value_cache - @value_cache ||= {}.extend(MonitorMixin) + @value_cache ||= {} end end end diff --git a/lib/puppet/util/checksums.rb b/lib/puppet/util/checksums.rb index a505bfc72..1f28fe8a7 100644 --- a/lib/puppet/util/checksums.rb +++ b/lib/puppet/util/checksums.rb @@ -56,7 +56,7 @@ module Puppet::Util::Checksums # Return the :mtime timestamp of a file. def mtime_file(filename) - File.stat(filename).send(:mtime) + Puppet::FileSystem::File.new(filename).stat.send(:mtime) end # by definition this doesn't exist @@ -102,7 +102,7 @@ module Puppet::Util::Checksums # Return the :ctime of a file. def ctime_file(filename) - File.stat(filename).send(:ctime) + Puppet::FileSystem::File.new(filename).stat.send(:ctime) end alias :ctime_stream :mtime_stream diff --git a/lib/puppet/util/classgen.rb b/lib/puppet/util/classgen.rb index 8785f87b3..ee1ea5f46 100644 --- a/lib/puppet/util/classgen.rb +++ b/lib/puppet/util/classgen.rb @@ -1,3 +1,5 @@ +require 'puppet/util/methodhelper' + module Puppet class ConstantAlreadyDefined < Error; end class SubclassAlreadyDefined < Error; end @@ -67,7 +69,7 @@ module Puppet::Util::ClassGen options = symbolize_options(options) const = genconst_string(name, options) retval = false - if const_defined?(const) + if is_constant_defined?(const) remove_const(const) retval = true end diff --git a/lib/puppet/util/colors.rb b/lib/puppet/util/colors.rb index 37bcddf4a..a10e240a1 100644 --- a/lib/puppet/util/colors.rb +++ b/lib/puppet/util/colors.rb @@ -82,6 +82,7 @@ module Puppet::Util::Colors if Puppet::Util::Platform.windows? # We're on windows, need win32console for color to work begin + require 'Win32API' require 'win32console' require 'windows/wide_string' diff --git a/lib/puppet/util/command_line.rb b/lib/puppet/util/command_line.rb index 1612691e8..a334ef8db 100644 --- a/lib/puppet/util/command_line.rb +++ b/lib/puppet/util/command_line.rb @@ -13,6 +13,7 @@ require 'puppet' require 'puppet/util' require "puppet/util/plugins" require "puppet/util/rubygems" +require "puppet/util/limits" module Puppet module Util @@ -20,6 +21,8 @@ module Puppet # is basically where the bootstrapping process / lifecycle of an app # begins. class CommandLine + include Puppet::Util::Limits + OPTION_OR_MANIFEST_FILE = /^-|\.pp$|\.rb$/ # @param zero [String] the name of the executable @@ -83,6 +86,8 @@ module Puppet Puppet.initialize_settings(args) end + setpriority(Puppet[:priority]) + find_subcommand.run end diff --git a/lib/puppet/util/docs.rb b/lib/puppet/util/docs.rb index 0e9277f97..0c854b76c 100644 --- a/lib/puppet/util/docs.rb +++ b/lib/puppet/util/docs.rb @@ -20,10 +20,10 @@ module Puppet::Util::Docs def doc extra = methods.find_all { |m| m.to_s =~ /^dochook_.+/ }.sort.collect { |m| self.send(m) - }.delete_if {|r| r.nil? }.join(" ") + }.delete_if {|r| r.nil? }.collect {|r| "* #{r}"}.join("\n") if @doc - @doc + (extra.empty? ? '' : "\n\n" + extra) + scrub(@doc) + (extra.empty? ? '' : "\n\n#{extra}") else extra end @@ -63,6 +63,7 @@ module Puppet::Util::Docs str + "\n" end + # There is nothing that would ever set this. It gets read in reference/type.rb, but will never have any value but nil. attr_reader :nodoc def nodoc? nodoc @@ -89,33 +90,38 @@ module Puppet::Util::Docs str << "\n" end - # Handle the inline indentation in the docs. + # Strip indentation and trailing whitespace from embedded doc fragments. + # + # Multi-line doc fragments are sometimes indented in order to preserve the + # formatting of the code they're embedded in. Since indents are syntactic + # elements in Markdown, we need to make sure we remove any indent that was + # added solely to preserve surrounding code formatting, but LEAVE any indent + # that delineates a Markdown element (code blocks, multi-line bulleted list + # items). We can do this by removing the *least common indent* from each line. + # + # Least common indent is defined as follows: + # + # * Find the smallest amount of leading space on any line... + # * ...excluding the first line (which may have zero indent without affecting + # the common indent)... + # * ...and excluding lines that consist solely of whitespace. + # * The least common indent may be a zero-length string, if the fragment is + # not indented to match code. + # * If there are hard tabs for some dumb reason, we assume they're at least + # consistent within this doc fragment. + # + # See tests in spec/unit/util/docs_spec.rb for examples. def scrub(text) - # Stupid markdown - #text = text.gsub("<%=", "<%=") - # For text with no carriage returns, there's nothing to do. - return text if text !~ /\n/ - indent = nil - - # If we can match an indentation, then just remove that same level of - # indent from every line. However, ignore any indentation on the - # first line, since that can be inconsistent. - text = text.lstrip - text.gsub!(/^([\t]+)/) { |s| " "*8*s.length; } # Expand leading tabs - # Find first non-empty line after the first line: - line2start = (text =~ /(\n?\s*\n)/) - line2start += $1.length - if (text[line2start..-1] =~ /^([ ]+)\S/) == 0 - indent = Regexp.quote($1) - begin - return text.gsub(/^#{indent}/,'') - rescue => detail - Puppet.log_exception(detail) - end - else - return text + # One-liners are easy! + return text.strip if text !~ /\n/ + excluding_first_line = text.partition("\n").last + indent = excluding_first_line.scan(/^[ \t]*(?=\S)/).min || '' # prevent nil + # Clean hanging indent, if any + if indent.length > 0 + text = text.gsub(/^#{indent}/, '') end - + # Clean trailing space + text.lines.map{|line|line.rstrip}.join("\n").rstrip end module_function :scrub diff --git a/lib/puppet/util/execution.rb b/lib/puppet/util/execution.rb index 5f0089303..e931816f6 100644 --- a/lib/puppet/util/execution.rb +++ b/lib/puppet/util/execution.rb @@ -1,15 +1,33 @@ module Puppet require 'rbconfig' - # A command failed to execute. require 'puppet/error' + # A command failed to execute. + # @api public class ExecutionFailure < Puppet::Error end +end # This module defines methods for execution of system commands. It is intented for inclusion # in classes that needs to execute system commands. # @api public -module Util::Execution +module Puppet::Util::Execution + + # This is the full output from a process. The object itself (a String) is the + # stdout of the process. + # + # @api public + class ProcessOutput < String + # @return [Integer] The exit status of the process + # @api public + attr_reader :exitstatus + + # @api private + def initialize(value,exitstatus) + super(value) + @exitstatus = exitstatus + end + end # Executes the provided command with STDIN connected to a pipe, yielding the # pipe object. @@ -27,17 +45,12 @@ module Util::Execution # @yield [pipe] to a block executing a subprocess # @yieldparam pipe [IO] the opened pipe # @yieldreturn [String] the output to return - # @raise [ExecutionFailure] if the executed chiled process did not exit with status == 0 and `failonfail` is + # @raise [Puppet::ExecutionFailure] if the executed chiled process did not exit with status == 0 and `failonfail` is # `true`. # @return [String] a string with the output from the subprocess executed by the given block + # @api public # def self.execpipe(command, failonfail = true) - if respond_to? :debug - debug "Executing '#{command}'" - else - Puppet.debug "Executing '#{command}'" - end - # Paste together an array with spaces. We used to paste directly # together, no spaces, which made for odd invocations; the user had to # include whitespace between arguments. @@ -46,13 +59,20 @@ module Util::Execution # shell anyhow, while no spaces makes for a small developer cost every # time this is invoked. --daniel 2012-02-13 command_str = command.respond_to?(:join) ? command.join(' ') : command + + if respond_to? :debug + debug "Executing '#{command_str}'" + else + Puppet.debug "Executing '#{command_str}'" + end + output = open("| #{command_str} 2>&1") do |pipe| yield pipe end if failonfail unless $CHILD_STATUS == 0 - raise ExecutionFailure, output + raise Puppet::ExecutionFailure, output end end @@ -62,10 +82,11 @@ module Util::Execution # Wraps execution of {execute} with mapping of exception to given exception (and output as argument). # @raise [exception] under same conditions as {execute}, but raises the given `exception` with the output as argument # @return (see execute) + # @api public def self.execfail(command, exception) output = execute(command) return output - rescue ExecutionFailure + rescue Puppet::ExecutionFailure raise exception, output end @@ -80,8 +101,8 @@ module Util::Execution # @param options [Hash] a Hash of options # @option options [Boolean] :failonfail if this value is set to true, then this method will raise an error if the # command is not executed successfully. - # @option options [?] :uid (nil) the user id of the user that the process should be run as - # @option options [?] :gid (nil) the group id of the group that the process should be run as + # @option options [Integer, String] :uid (nil) the user id of the user that the process should be run as + # @option options [Integer, String] :gid (nil) the group id of the group that the process should be run as # @option options [Boolean] :combine sets whether or not to combine stdout/stderr in the output # @option options [String] :stdinfile (nil) sets a file that can be used for stdin. Passing a string for stdin is not currently # supported. @@ -92,13 +113,16 @@ module Util::Execution # Passing in a value of false for this option will allow the command to be executed using the user/system locale. # @option options [Hash<{String => String}>] :custom_environment ({}) a hash of key/value pairs to set as environment variables for the duration # of the command. - # @return [String] output as specified by options + # @return [Puppet::Util::Execution::ProcessOutput] output as specified by options + # @raise [Puppet::ExecutionFailure] if the executed chiled process did not exit with status == 0 and `failonfail` is + # `true`. # @note Unfortunately, the default behavior for failonfail and combine (since # 0.22.4 and 0.24.7, respectively) depend on whether options are specified # or not. If specified, then failonfail and combine default to false (even # when the options specified are neither failonfail nor combine). If no # options are specified, then failonfail and combine default to true. # @comment See commits efe9a833c and d32d7f30 + # @api public # def self.execute(command, options = NoOptionsSpecified) # specifying these here rather than in the method signature to allow callers to pass in a partial @@ -147,8 +171,8 @@ module Util::Execution begin exit_status = Puppet::Util::Windows::Process.wait_process(process_info.process_handle) ensure - Process.CloseHandle(process_info.process_handle) - Process.CloseHandle(process_info.thread_handle) + Puppet::Util::Windows::Process.CloseHandle(process_info.process_handle) + Puppet::Util::Windows::Process.CloseHandle(process_info.thread_handle) end end @@ -161,15 +185,16 @@ module Util::Execution end if options[:failonfail] and exit_status != 0 - raise ExecutionFailure, "Execution of '#{str}' returned #{exit_status}: #{output}" + raise Puppet::ExecutionFailure, "Execution of '#{str}' returned #{exit_status}: #{output}" end - output + Puppet::Util::Execution::ProcessOutput.new(output || '', exit_status) end # Returns the path to the ruby executable (available via Config object, even if # it's not in the PATH... so this is slightly safer than just using Puppet::Util.which) # @return [String] the path to the Ruby executable + # @api private # def self.ruby_path() File.join(RbConfig::CONFIG['bindir'], @@ -261,7 +286,7 @@ module Util::Execution # about a race condition because all of the places that we call this from are preceded by a call to "waitpid2", # meaning that the processes responsible for writing the file have completed before we get here.) 2.times do |try| - if File.exists?(stdout.path) + if Puppet::FileSystem::File.exist?(stdout.path) stdout.open begin return stdout.read @@ -279,4 +304,3 @@ module Util::Execution end private_class_method :wait_for_output end -end diff --git a/lib/puppet/util/filetype.rb b/lib/puppet/util/filetype.rb index c65f8026e..7f43f8c10 100755..100644 --- a/lib/puppet/util/filetype.rb +++ b/lib/puppet/util/filetype.rb @@ -98,12 +98,12 @@ class Puppet::Util::FileType newfiletype(:flat) do # Back the file up before replacing it. def backup - bucket.backup(@path) if File.exists?(@path) + bucket.backup(@path) if Puppet::FileSystem::File.exist?(@path) end # Read the file. def read - if File.exist?(@path) + if Puppet::FileSystem::File.exist?(@path) File.read(@path) else return nil @@ -112,7 +112,7 @@ class Puppet::Util::FileType # Remove the file. def remove - File.unlink(@path) if File.exist?(@path) + Puppet::FileSystem::File.unlink(@path) if Puppet::FileSystem::File.exist?(@path) end # Overwrite the file. diff --git a/lib/puppet/util/instance_loader.rb b/lib/puppet/util/instance_loader.rb index 7c2e18486..c6d6b71d4 100755..100644 --- a/lib/puppet/util/instance_loader.rb +++ b/lib/puppet/util/instance_loader.rb @@ -42,9 +42,9 @@ module Puppet::Util::InstanceLoader # Use this method so they all get loaded loaded_instances(type).sort { |a,b| a.to_s <=> b.to_s }.each do |name| mod = self.loaded_instance(name) - docs += "#{name}\n#{"-" * name.to_s.length}\n" + docs << "#{name}\n#{"-" * name.to_s.length}\n" - docs += Puppet::Util::Docs.scrub(mod.doc) + "\n\n" + docs << Puppet::Util::Docs.scrub(mod.doc) << "\n\n" end docs diff --git a/lib/puppet/util/instrumentation.rb b/lib/puppet/util/instrumentation.rb index eb522437e..94a75ac03 100644 --- a/lib/puppet/util/instrumentation.rb +++ b/lib/puppet/util/instrumentation.rb @@ -5,7 +5,6 @@ require 'puppet/util/instance_loader' class Puppet::Util::Instrumentation extend Puppet::Util::ClassGen extend Puppet::Util::InstanceLoader - extend MonitorMixin # we're using a ruby lazy autoloader to prevent a loop when requiring listeners # since this class sets up an indirection which is also used in Puppet::Indirector::Indirection @@ -71,11 +70,9 @@ class Puppet::Util::Instrumentation end def self.each_listener(label) - synchronize { - @listeners_of[label] ||= @listeners.select do |k,l| - l.listen_to?(label) - end - }.each do |l| + @listeners_of[label] ||= @listeners.select do |k,l| + l.listen_to?(label) + end.each do |l| yield l end end @@ -105,67 +102,51 @@ class Puppet::Util::Instrumentation end def self.subscribe(listener, label_pattern, event) - synchronize { - raise "Listener #{listener.name} is already subscribed" if @listeners.include?(listener.name) - Puppet.debug "registering instrumentation listener #{listener.name}" - @listeners[listener.name] = Listener.new(listener, label_pattern, event) - listener.subscribed if listener.respond_to?(:subscribed) - rehash - } + raise "Listener #{listener.name} is already subscribed" if @listeners.include?(listener.name) + Puppet.debug "registering instrumentation listener #{listener.name}" + @listeners[listener.name] = Listener.new(listener, label_pattern, event) + listener.subscribed if listener.respond_to?(:subscribed) + rehash end def self.unsubscribe(listener) - synchronize { - Puppet.warning("#{listener.name} hasn't been registered but asked to be unregistered") unless @listeners.include?(listener.name) - Puppet.info "unregistering instrumentation listener #{listener.name}" - @listeners.delete(listener.name) - listener.unsubscribed if listener.respond_to?(:unsubscribed) - rehash - } + Puppet.warning("#{listener.name} hasn't been registered but asked to be unregistered") unless @listeners.include?(listener.name) + Puppet.info "unregistering instrumentation listener #{listener.name}" + @listeners.delete(listener.name) + listener.unsubscribed if listener.respond_to?(:unsubscribed) + rehash end def self.init # let's init our probe indirection require 'puppet/util/instrumentation/indirection_probe' - synchronize { - @listeners ||= {} - @listeners_of ||= {} - instance_loader(:listener).loadall - } + @listeners ||= {} + @listeners_of ||= {} + instance_loader(:listener).loadall end def self.clear - synchronize { - @listeners = {} - @listeners_of = {} - @id = 0 - } + @listeners = {} + @listeners_of = {} + @id = 0 end def self.[](key) - synchronize { - @listeners[key.intern] - } + @listeners[key.intern] end def self.[]=(key, value) - synchronize { - @listeners[key.intern] = value - rehash - } + @listeners[key.intern] = value + rehash end private - # should be called only under the guard - # self.synchronize def self.rehash @listeners_of = {} end def self.next_id - synchronize { - @id = (@id || 0) + 1 - } + @id = (@id || 0) + 1 end end diff --git a/lib/puppet/util/instrumentation/data.rb b/lib/puppet/util/instrumentation/data.rb index 9157f58fc..48e595432 100644 --- a/lib/puppet/util/instrumentation/data.rb +++ b/lib/puppet/util/instrumentation/data.rb @@ -20,12 +20,19 @@ class Puppet::Util::Instrumentation::Data @listener.name end - def to_pson(*args) - result = { + def to_data_hash + { :name => name }.merge(@listener.respond_to?(:data) ? @listener.data : {}) + end + + def to_pson_data_hash + { 'document_type' => "Puppet::Util::Instrumentation::Data", - 'data' => { :name => name }.merge(@listener.respond_to?(:data) ? @listener.data : {}) + 'data' => to_data_hash, } - result.to_pson(*args) + end + + def to_pson(*args) + to_pson_data_hash.to_pson(*args) end def self.from_pson(data) diff --git a/lib/puppet/util/instrumentation/indirection_probe.rb b/lib/puppet/util/instrumentation/indirection_probe.rb index ad9323a38..237d8dbc8 100644 --- a/lib/puppet/util/instrumentation/indirection_probe.rb +++ b/lib/puppet/util/instrumentation/indirection_probe.rb @@ -15,12 +15,19 @@ class Puppet::Util::Instrumentation::IndirectionProbe @probe_name = probe_name end - def to_pson(*args) - result = { + def to_data_hash + { :name => probe_name } + end + + def to_pson_data_hash + { :document_type => "Puppet::Util::Instrumentation::IndirectionProbe", - :data => { :name => probe_name } + :data => to_data_hash, } - result.to_pson(*args) + end + + def to_pson(*args) + to_pson_data_hash.to_pson(*args) end def self.from_pson(data) diff --git a/lib/puppet/util/instrumentation/instrumentable.rb b/lib/puppet/util/instrumentation/instrumentable.rb index 4c6d2f97e..01c75ff77 100644 --- a/lib/puppet/util/instrumentation/instrumentable.rb +++ b/lib/puppet/util/instrumentation/instrumentable.rb @@ -1,4 +1,3 @@ -require 'monitor' require 'puppet/util/instrumentation' # This is the central point of all declared probes. @@ -15,7 +14,7 @@ require 'puppet/util/instrumentation' # end # end module Puppet::Util::Instrumentation::Instrumentable - INSTRUMENTED_CLASSES = {}.extend(MonitorMixin) + INSTRUMENTED_CLASSES = {} attr_reader :probes @@ -101,10 +100,8 @@ module Puppet::Util::Instrumentation::Instrumentable # end # def probe(method, options = {}) - INSTRUMENTED_CLASSES.synchronize { - (@probes ||= []) << Probe.new(method, self, options) - INSTRUMENTED_CLASSES[self] = @probes - } + (@probes ||= []) << Probe.new(method, self, options) + INSTRUMENTED_CLASSES[self] = @probes end def self.probes @@ -126,18 +123,14 @@ module Puppet::Util::Instrumentation::Instrumentable end def self.clear_probes - INSTRUMENTED_CLASSES.synchronize { - INSTRUMENTED_CLASSES.clear - } + INSTRUMENTED_CLASSES.clear nil # do not leak our probes to the exterior world end def self.each_probe - INSTRUMENTED_CLASSES.synchronize { - INSTRUMENTED_CLASSES.each_key do |klass| - klass.probes.each { |probe| yield probe } - end - } + INSTRUMENTED_CLASSES.each_key do |klass| + klass.probes.each { |probe| yield probe } + end nil # do not leak our probes to the exterior world end end diff --git a/lib/puppet/util/instrumentation/listener.rb b/lib/puppet/util/instrumentation/listener.rb index 42ec0c0e9..b965e976f 100644 --- a/lib/puppet/util/instrumentation/listener.rb +++ b/lib/puppet/util/instrumentation/listener.rb @@ -41,16 +41,23 @@ class Puppet::Util::Instrumentation::Listener { :data => @listener.data } end - def to_pson(*args) - result = { + def to_data_hash + { + :name => name, + :pattern => pattern, + :enabled => enabled? + } + end + + def to_pson_data_hash + { :document_type => "Puppet::Util::Instrumentation::Listener", - :data => { - :name => name, - :pattern => pattern, - :enabled => enabled? - } + :data => to_data_hash, } - result.to_pson(*args) + end + + def to_pson(*args) + to_pson_data_hash.to_pson(*args) end def self.from_pson(data) diff --git a/lib/puppet/util/instrumentation/listeners/log.rb b/lib/puppet/util/instrumentation/listeners/log.rb index fcff14621..72797d628 100644 --- a/lib/puppet/util/instrumentation/listeners/log.rb +++ b/lib/puppet/util/instrumentation/listeners/log.rb @@ -1,5 +1,3 @@ -require 'monitor' - # This is an example instrumentation listener that stores the last # 20 instrumented probe run time. Puppet::Util::Instrumentation.new_listener(:log) do @@ -9,21 +7,17 @@ Puppet::Util::Instrumentation.new_listener(:log) do attr_accessor :last_logs def initialize - @last_logs = {}.extend(MonitorMixin) + @last_logs = {} end def notify(label, event, data) return if event == :start log_line = "#{label} took #{data[:finished] - data[:started]}" - @last_logs.synchronize { - (@last_logs[label] ||= []) << log_line - @last_logs[label].shift if @last_logs[label].length > SIZE - } + (@last_logs[label] ||= []) << log_line + @last_logs[label].shift if @last_logs[label].length > SIZE end def data - @last_logs.synchronize { - @last_logs.dup - } + @last_logs.dup end end diff --git a/lib/puppet/util/instrumentation/listeners/performance.rb b/lib/puppet/util/instrumentation/listeners/performance.rb index 3ad51b7de..5bf3e2c5b 100644 --- a/lib/puppet/util/instrumentation/listeners/performance.rb +++ b/lib/puppet/util/instrumentation/listeners/performance.rb @@ -1,30 +1,24 @@ -require 'monitor' - Puppet::Util::Instrumentation.new_listener(:performance) do attr_reader :samples def initialize - @samples = {}.extend(MonitorMixin) + @samples = {} end def notify(label, event, data) return if event == :start duration = data[:finished] - data[:started] - samples.synchronize do - @samples[label] ||= { :count => 0, :max => 0, :min => nil, :sum => 0, :average => 0 } - @samples[label][:count] += 1 - @samples[label][:sum] += duration - @samples[label][:max] = [ @samples[label][:max], duration ].max - @samples[label][:min] = [ @samples[label][:min], duration ].reject { |val| val.nil? }.min - @samples[label][:average] = @samples[label][:sum] / @samples[label][:count] - end + @samples[label] ||= { :count => 0, :max => 0, :min => nil, :sum => 0, :average => 0 } + @samples[label][:count] += 1 + @samples[label][:sum] += duration + @samples[label][:max] = [ @samples[label][:max], duration ].max + @samples[label][:min] = [ @samples[label][:min], duration ].reject { |val| val.nil? }.min + @samples[label][:average] = @samples[label][:sum] / @samples[label][:count] end def data - samples.synchronize do - @samples.dup - end + @samples.dup end end diff --git a/lib/puppet/util/limits.rb b/lib/puppet/util/limits.rb new file mode 100644 index 000000000..e2f805b00 --- /dev/null +++ b/lib/puppet/util/limits.rb @@ -0,0 +1,12 @@ +require 'puppet/util' + +module Puppet::Util::Limits + # @api private + def setpriority(priority) + return unless priority + + Process.setpriority(0, Process.pid, priority) + rescue Errno::EACCES, NotImplementedError + Puppet.warning("Failed to set process priority to '#{priority}'") + end +end diff --git a/lib/puppet/util/lockfile.rb b/lib/puppet/util/lockfile.rb index 75bc97213..4f1ad5716 100644 --- a/lib/puppet/util/lockfile.rb +++ b/lib/puppet/util/lockfile.rb @@ -31,7 +31,7 @@ class Puppet::Util::Lockfile def unlock if locked? - File.unlink(@file_path) + Puppet::FileSystem::File.unlink(@file_path) true else false @@ -56,7 +56,7 @@ class Puppet::Util::Lockfile # being overridden by child classes. # @return [boolean] true if the file is locked, false if it is not. def file_locked?() - File.exists? @file_path + Puppet::FileSystem::File.exist? @file_path end private :file_locked? end diff --git a/lib/puppet/util/log.rb b/lib/puppet/util/log.rb index 3d34bba2c..808e0631d 100644 --- a/lib/puppet/util/log.rb +++ b/lib/puppet/util/log.rb @@ -1,5 +1,6 @@ require 'puppet/util/tagging' require 'puppet/util/classgen' +require 'puppet/network/format_support' # Pass feedback to the user. Log levels are modeled after syslog's, and it is # expected that that will be the most common log destination. Supports @@ -8,6 +9,7 @@ class Puppet::Util::Log include Puppet::Util extend Puppet::Util::ClassGen include Puppet::Util::Tagging + include Puppet::Network::FormatSupport @levels = [:debug,:info,:notice,:warning,:err,:alert,:emerg,:crit] @loglevel = 2 @@ -165,9 +167,7 @@ class Puppet::Util::Log queuemessage(msg) if @destinations.length == 0 @destinations.each do |name, dest| - threadlock(dest) do - dest.handle(msg) - end + dest.handle(msg) end end @@ -265,7 +265,7 @@ class Puppet::Util::Log @level = data['level'].intern @message = data['message'] @source = data['source'] - @tags = data['tags'] + @tags = Puppet::Util::TagSet.new(data['tags']) @time = data['time'] if @time.is_a? String @time = Time.parse(@time) @@ -274,7 +274,11 @@ class Puppet::Util::Log @line = data['line'] if data['line'] end - def to_pson + def to_hash + self.to_data_hash + end + + def to_data_hash { 'level' => @level, 'message' => @message, @@ -283,7 +287,11 @@ class Puppet::Util::Log 'time' => @time.iso8601(9), 'file' => @file, 'line' => @line, - }.to_pson + } + end + + def to_pson(*args) + to_data_hash.to_pson(*args) end def message=(msg) diff --git a/lib/puppet/util/log/destinations.rb b/lib/puppet/util/log/destinations.rb index e33cfd9a8..ecb19c66b 100644 --- a/lib/puppet/util/log/destinations.rb +++ b/lib/puppet/util/log/destinations.rb @@ -68,7 +68,7 @@ Puppet::Util::Log.newdesttype :file do # first make sure the directory exists # We can't just use 'Config.use' here, because they've # specified a "special" destination. - unless FileTest.exist?(File.dirname(path)) + unless Puppet::FileSystem::File.exist?(File.dirname(path)) FileUtils.mkdir_p(File.dirname(path), :mode => 0755) Puppet.info "Creating log directory #{File.dirname(path)}" end @@ -95,6 +95,28 @@ Puppet::Util::Log.newdesttype :file do end end +Puppet::Util::Log.newdesttype :logstash_event do + require 'time' + + def format(msg) + # logstash_event format is documented at + # https://logstash.jira.com/browse/LOGSTASH-675 + + data = {} + data = msg.to_hash + data['version'] = 1 + data['@timestamp'] = data['time'] + data.delete('time') + + data + end + + def handle(msg) + message = format(msg) + $stdout.puts message.to_pson + end +end + Puppet::Util::Log.newdesttype :console do require 'puppet/util/colors' include Puppet::Util::Colors diff --git a/lib/puppet/util/metric.rb b/lib/puppet/util/metric.rb index 49d4edfc3..f37bbcfea 100644 --- a/lib/puppet/util/metric.rb +++ b/lib/puppet/util/metric.rb @@ -1,8 +1,10 @@ # included so we can test object types require 'puppet' +require 'puppet/network/format_support' # A class for handling metrics. This is currently ridiculously hackish. class Puppet::Util::Metric + include Puppet::Network::FormatSupport attr_accessor :type, :name, :value, :label attr_writer :values @@ -15,12 +17,16 @@ class Puppet::Util::Metric metric end - def to_pson + def to_data_hash { 'name' => @name, 'label' => @label, 'values' => @values - }.to_pson + } + end + + def to_pson(*args) + to_data_hash.to_pson(*args) end # Return a specific value @@ -155,7 +161,7 @@ class Puppet::Util::Metric Puppet.warning "RRD library is missing; cannot store metrics" return end - self.create(time - 5) unless FileTest.exists?(self.path) + self.create(time - 5) unless Puppet::FileSystem::File.exist?(self.path) if Puppet.features.rrd_legacy? && ! Puppet.features.rrd? @rrd ||= RRDtool.new(self.path) diff --git a/lib/puppet/util/monkey_patches.rb b/lib/puppet/util/monkey_patches.rb index b593ae3d3..3f0e68e8a 100644 --- a/lib/puppet/util/monkey_patches.rb +++ b/lib/puppet/util/monkey_patches.rb @@ -105,6 +105,7 @@ class IO end def self.binread(name, length = nil, offset = 0) + Puppet.deprecation_warning("This is a monkey-patched implementation of IO.binread on ruby 1.8 and is deprecated. Read the file without this method as it will be removed in a future version.") File.open(name, 'rb') do |f| f.seek(offset) if offset > 0 f.read(length) @@ -194,8 +195,12 @@ if Puppet::Util::Platform.windows? def set_default_paths # This can be removed once openssl integrates with windows # cert store, see http://rt.openssl.org/Ticket/Display.html?id=2158 - Puppet::Util::Windows::RootCerts.instance.each do |x509| - add_cert(x509) + Puppet::Util::Windows::RootCerts.instance.to_a.uniq.each do |x509| + begin + add_cert(x509) + rescue OpenSSL::X509::StoreError => e + warn "Failed to add #{x509.subject.to_s}" + end end __original_set_default_paths diff --git a/lib/puppet/util/network_device/config.rb b/lib/puppet/util/network_device/config.rb index ef49dd393..fe355708a 100644 --- a/lib/puppet/util/network_device/config.rb +++ b/lib/puppet/util/network_device/config.rb @@ -15,7 +15,7 @@ class Puppet::Util::NetworkDevice::Config attr_reader :devices def exists? - FileTest.exists?(@file) + Puppet::FileSystem::File.exist?(@file) end def initialize diff --git a/lib/puppet/util/plugins.rb b/lib/puppet/util/plugins.rb index 0bea67d05..dde496d35 100644 --- a/lib/puppet/util/plugins.rb +++ b/lib/puppet/util/plugins.rb @@ -37,7 +37,7 @@ module Puppet def self.known Paths[Loaded.length...Paths.length].each { |path| file = File.join(path,'plugin_init.rb') - Loaded << (File.exist?(file) && new(file)) + Loaded << (Puppet::FileSystem::File.exist?(file) && new(file)) } Loaded.compact end diff --git a/lib/puppet/util/posix.rb b/lib/puppet/util/posix.rb index 2af0e4b91..2af0e4b91 100755..100644 --- a/lib/puppet/util/posix.rb +++ b/lib/puppet/util/posix.rb diff --git a/lib/puppet/util/profiler.rb b/lib/puppet/util/profiler.rb index 0c3a3768f..c8d1762f5 100644 --- a/lib/puppet/util/profiler.rb +++ b/lib/puppet/util/profiler.rb @@ -10,14 +10,19 @@ module Puppet::Util::Profiler NONE = Puppet::Util::Profiler::None.new + # Reset the profiling system to the original state + def self.clear + @profiler = nil + end + # @return This thread's configured profiler def self.current - Thread.current[:profiler] || NONE + @profiler || NONE end # @param profiler [#profile] A profiler for the current thread def self.current=(profiler) - Thread.current[:profiler] = profiler + @profiler = profiler end # @param message [String] A description of the profiled event diff --git a/lib/puppet/util/provider_features.rb b/lib/puppet/util/provider_features.rb index d557c0380..f8c0c0aa6 100644 --- a/lib/puppet/util/provider_features.rb +++ b/lib/puppet/util/provider_features.rb @@ -84,7 +84,7 @@ module Puppet::Util::ProviderFeatures names = @features.keys.sort { |a,b| a.to_s <=> b.to_s } names.each do |name| doc = @features[name].docs.gsub(/\n\s+/, " ") - str += "- *#{name}*: #{doc}\n" + str << "- *#{name}*: #{doc}\n" end if providers.length > 0 @@ -101,7 +101,7 @@ module Puppet::Util::ProviderFeatures end end end - str += doctable(headers, data) + str << doctable(headers, data) end str end diff --git a/lib/puppet/util/rdoc.rb b/lib/puppet/util/rdoc.rb index becfa4ba3..49784956b 100644 --- a/lib/puppet/util/rdoc.rb +++ b/lib/puppet/util/rdoc.rb @@ -5,44 +5,42 @@ module Puppet::Util::RDoc # launch a rdoc documenation process # with the files/dir passed in +files+ def rdoc(outputdir, files, charset = nil) - unless Puppet.features.rdoc1? - raise "the version of RDoc included in Ruby #{::RUBY_VERSION} is not supported" - end - - begin - Puppet[:ignoreimport] = true + Puppet[:ignoreimport] = true - # then rdoc - require 'rdoc/rdoc' - require 'rdoc/options' + # then rdoc + require 'rdoc/rdoc' + require 'rdoc/options' - # load our parser - require 'puppet/util/rdoc/parser' + # load our parser + require 'puppet/util/rdoc/parser' - r = RDoc::RDoc.new + r = RDoc::RDoc.new + if Puppet.features.rdoc1? RDoc::RDoc::GENERATORS["puppet"] = RDoc::RDoc::Generator.new( - "puppet/util/rdoc/generators/puppet_generator.rb", - :PuppetGenerator, - "puppet" - ) - - # specify our own format & where to output - options = [ "--fmt", "puppet", - "--quiet", - "--exclude", "/modules/[^/]*/files/.*$", - "--exclude", "/modules/[^/]*/templates/.*$", - "--op", outputdir ] + "puppet/util/rdoc/generators/puppet_generator.rb", + :PuppetGenerator, + "puppet" + ) + end - options << "--force-update" if Options::OptionList.options.any? { |o| o[0] == "--force-update" } - options += [ "--charset", charset] if charset - options += files + # specify our own format & where to output + options = [ "--fmt", "puppet", + "--quiet", + "--exclude", "/modules/[^/]*/spec/.*$", + "--exclude", "/modules/[^/]*/files/.*$", + "--exclude", "/modules/[^/]*/tests/.*$", + "--exclude", "/modules/[^/]*/templates/.*$", + "--op", outputdir ] - # launch the documentation process - r.document(options) - rescue RDoc::RDocError => e - raise Puppet::ParseError.new("RDoc error #{e}") + if !Puppet.features.rdoc1? || ::Options::OptionList.options.any? { |o| o[0] == "--force-update" } # Options is a root object in the rdoc1 namespace... + options << "--force-update" end + options += [ "--charset", charset] if charset + options += files + + # launch the documentation process + r.document(options) end # launch an output to console manifest doc diff --git a/lib/puppet/util/rdoc/code_objects.rb b/lib/puppet/util/rdoc/code_objects.rb index 3c789a0c5..7edd4134e 100644 --- a/lib/puppet/util/rdoc/code_objects.rb +++ b/lib/puppet/util/rdoc/code_objects.rb @@ -9,25 +9,20 @@ module RDoc # PuppetGenerator. # PuppetTopLevel is a top level (usually a .pp/.rb file) - class PuppetTopLevel < TopLevel + module PuppetTopLevel attr_accessor :module_name, :global + end - # will contain all plugins - @@all_plugins = {} - - # contains all cutoms facts - @@all_facts = {} - - def initialize(toplevel) - super(toplevel.file_relative_name) - end - - def self.all_plugins - @@all_plugins.values - end - - def self.all_facts - @@all_facts.values + # Add top level comments to a class or module regardless of whether we are + # using rdoc1 or rdoc2+ + # @api private + module AddClassModuleComment + def add_comment(comment, location = nil) + if PUPPET_RDOC_VERSION == 1 + self.comment = comment + else + super + end end end @@ -35,28 +30,67 @@ module RDoc # This is mapped to an HTMLPuppetModule # it leverage the RDoc (ruby) module infrastructure class PuppetModule < NormalModule + include AddClassModuleComment + attr_accessor :facts, :plugins def initialize(name,superclass=nil) @facts = [] @plugins = [] + @nodes = {} super(name,superclass) end - def initialize_classes_and_modules - super - @nodes = {} + def add_plugin(plugin) + if PUPPET_RDOC_VERSION == 1 + add_to(@plugins, plugin) + else + add_plugin_rdoc2(plugin) + end end - def add_plugin(plugin) - add_to(@plugins, plugin) + def add_plugin_rdoc2(plugin) + name = plugin.name + type = plugin.type + meth = AnyMethod.new("*args", name) + meth.params = "(*args)" + meth.visibility = :public + meth.document_self = true + meth.singleton = false + meth.comment = plugin.comment + if type == 'function' + @function_container ||= add_module(NormalModule, "__functions__") + @function_container.add_method(meth) + elsif type == 'type' + @type_container ||= add_module(NormalModule, "__types__") + @type_container.add_method(meth) + end end def add_fact(fact) - add_to(@facts, fact) + if PUPPET_RDOC_VERSION == 1 + add_to(@facts, fact) + else + add_fact_rdoc2(fact) + end + end + + def add_fact_rdoc2(fact) + @fact_container ||= add_module(NormalModule, "__facts__") + confine_str = fact.confine.empty? ? '' : fact.confine.to_s + const = Constant.new(fact.name, confine_str, fact.comment) + @fact_container.add_constant(const) + end + + def add_node(name, superclass) + if PUPPET_RDOC_VERSION == 1 + add_node_rdoc1(name, superclass) + else + add_node_rdoc2(name, superclass) + end end - def add_node(name,superclass) + def add_node_rdoc1(name, superclass) cls = @nodes[name] unless cls cls = PuppetNode.new(name, superclass) @@ -67,6 +101,18 @@ module RDoc cls end + # Adds a module called __nodes__ and adds nodes to it as classes + # + def add_node_rdoc2(name,superclass) + if cls = @nodes[name] + return cls + end + @node_container ||= add_module(NormalModule, "__nodes__") + cls = @node_container.add_class(PuppetNode, name, superclass) + @nodes[name] = cls if !@done_documenting + cls + end + def each_fact @facts.each {|c| yield c} end @@ -88,6 +134,8 @@ module RDoc # It is mapped to a HTMLPuppetClass for display # It leverages RDoc (ruby) Class class PuppetClass < ClassModule + include AddClassModuleComment + attr_accessor :resource_list, :requires, :childs, :realizes def initialize(name, superclass) @@ -130,7 +178,7 @@ module RDoc # but are written class1::class2::define we need to perform the lookup by # ourselves. def find_symbol(symbol, method=nil) - result = super + result = super(symbol) if not result and symbol =~ /::/ modules = symbol.split(/::/) unless modules.empty? @@ -169,6 +217,8 @@ module RDoc # It is mapped to a HTMLPuppetNode for display # A node is just a variation of a class class PuppetNode < PuppetClass + include AddClassModuleComment + def initialize(name, superclass) super(name,superclass) end diff --git a/lib/puppet/util/rdoc/generators/puppet_generator.rb b/lib/puppet/util/rdoc/generators/puppet_generator.rb index 5c6aca28e..142124769 100644 --- a/lib/puppet/util/rdoc/generators/puppet_generator.rb +++ b/lib/puppet/util/rdoc/generators/puppet_generator.rb @@ -246,7 +246,7 @@ module Generators end def gen_composite_index(collection, template, filename)\ - return if FileTest.exists?(filename) + return if Puppet::FileSystem::File.exist?(filename) template = TemplatePage.new(RDoc::Page::FR_INDEX_BODY, template) res1 = [] diff --git a/lib/puppet/util/rdoc/parser.rb b/lib/puppet/util/rdoc/parser.rb index 6b5ad740b..6ecd4921c 100644 --- a/lib/puppet/util/rdoc/parser.rb +++ b/lib/puppet/util/rdoc/parser.rb @@ -6,492 +6,17 @@ # rdoc mandatory includes require "rdoc/code_objects" require "puppet/util/rdoc/code_objects" -require "rdoc/tokenstream" -if ::RUBY_VERSION =~ /^1.8/ - require "rdoc/markup/simple_markup/preprocess" - require "rdoc/parsers/parserfactory" -else - require "rdoc/markup/preprocess" - require "rdoc/parser" -end - -module RDoc - -class Parser - extend ParserFactory if ::RUBY_VERSION =~ /^1.8/ - - SITE = "__site__" - - attr_accessor :input_file_name, :top_level - - # parser registration into RDoc - parse_files_matching(/\.(rb|pp)$/) - - # called with the top level file - def initialize(top_level, file_name, content, options, stats) - @options = options - @stats = stats - @input_file_name = file_name - @top_level = PuppetTopLevel.new(top_level) - @progress = $stderr unless options.quiet - end - - # main entry point - def scan - environment = Puppet::Node::Environment.new - @known_resource_types = environment.known_resource_types - unless environment.known_resource_types.watching_file?(@input_file_name) - Puppet.info "rdoc: scanning #{@input_file_name}" - if @input_file_name =~ /\.pp$/ - @parser = Puppet::Parser::Parser.new(environment) - @parser.file = @input_file_name - @parser.parse.instantiate('').each do |type| - @known_resource_types.add type - end - end - end - - scan_top_level(@top_level) - @top_level - end - - # Due to a bug in RDoc, we need to roll our own find_module_named - # The issue is that RDoc tries harder by asking the parent for a class/module - # of the name. But by doing so, it can mistakenly use a module of same name - # but from which we are not descendant. - def find_object_named(container, name) - return container if container.name == name - container.each_classmodule do |m| - return m if m.name == name - end - nil - end - - # walk down the namespace and lookup/create container as needed - def get_class_or_module(container, name) - - # class ::A -> A is in the top level - if name =~ /^::/ - container = @top_level - end - - names = name.split('::') - - final_name = names.pop - names.each do |name| - prev_container = container - container = find_object_named(container, name) - container ||= prev_container.add_class(PuppetClass, name, nil) - end - [container, final_name] - end - - # split_module tries to find if +path+ belongs to the module path - # if it does, it returns the module name, otherwise if we are sure - # it is part of the global manifest path, "__site__" is returned. - # And finally if this path couldn't be mapped anywhere, nil is returned. - def split_module(path) - # find a module - fullpath = File.expand_path(path) - Puppet.debug "rdoc: testing #{fullpath}" - if fullpath =~ /(.*)\/([^\/]+)\/(?:manifests|plugins|lib)\/.+\.(pp|rb)$/ - modpath = $1 - name = $2 - Puppet.debug "rdoc: module #{name} into #{modpath} ?" - Puppet::Node::Environment.new.modulepath.each do |mp| - if File.identical?(modpath,mp) - Puppet.debug "rdoc: found module #{name}" - return name - end - end - end - if fullpath =~ /\.(pp|rb)$/ - # there can be paths we don't want to scan under modules - # imagine a ruby or manifest that would be distributed as part as a module - # but we don't want those to be hosted under <site> - Puppet::Node::Environment.new.modulepath.each do |mp| - # check that fullpath is a descendant of mp - dirname = fullpath - previous = dirname - while (dirname = File.dirname(previous)) != previous - previous = dirname - return nil if File.identical?(dirname,mp) - end - end - end - # we are under a global manifests - Puppet.debug "rdoc: global manifests" - SITE - end - - # create documentation for the top level +container+ - def scan_top_level(container) - # use the module README as documentation for the module - comment = "" - %w{README README.rdoc}.each do |rfile| - readme = File.join(File.dirname(File.dirname(@input_file_name)), rfile) - comment = File.open(readme,"r") { |f| f.read } if FileTest.readable?(readme) - end - look_for_directives_in(container, comment) unless comment.empty? - - # infer module name from directory - name = split_module(@input_file_name) - if name.nil? - # skip .pp files that are not in manifests directories as we can't guarantee they're part - # of a module or the global configuration. - container.document_self = false - return - end - - Puppet.debug "rdoc: scanning for #{name}" - - container.module_name = name - container.global=true if name == SITE - - @stats.num_modules += 1 - container, name = get_class_or_module(container,name) - mod = container.add_module(PuppetModule, name) - mod.record_location(@top_level) - mod.comment = comment - - if @input_file_name =~ /\.pp$/ - parse_elements(mod) - elsif @input_file_name =~ /\.rb$/ - parse_plugins(mod) - end - end - - # create documentation for include statements we can find in +code+ - # and associate it with +container+ - def scan_for_include_or_require(container, code) - code = [code] unless code.is_a?(Array) - code.each do |stmt| - scan_for_include_or_require(container,stmt.children) if stmt.is_a?(Puppet::Parser::AST::ASTArray) - - if stmt.is_a?(Puppet::Parser::AST::Function) and ['include','require'].include?(stmt.name) - stmt.arguments.each do |included| - Puppet.debug "found #{stmt.name}: #{included}" - container.send("add_#{stmt.name}",Include.new(included.to_s, stmt.doc)) - end - end - end - end - - # create documentation for realize statements we can find in +code+ - # and associate it with +container+ - def scan_for_realize(container, code) - code = [code] unless code.is_a?(Array) - code.each do |stmt| - scan_for_realize(container,stmt.children) if stmt.is_a?(Puppet::Parser::AST::ASTArray) - - if stmt.is_a?(Puppet::Parser::AST::Function) and stmt.name == 'realize' - stmt.arguments.each do |realized| - Puppet.debug "found #{stmt.name}: #{realized}" - container.add_realize(Include.new(realized.to_s, stmt.doc)) - end - end - end - end - - # create documentation for global variables assignements we can find in +code+ - # and associate it with +container+ - def scan_for_vardef(container, code) - code = [code] unless code.is_a?(Array) - code.each do |stmt| - scan_for_vardef(container,stmt.children) if stmt.is_a?(Puppet::Parser::AST::ASTArray) - - if stmt.is_a?(Puppet::Parser::AST::VarDef) - Puppet.debug "rdoc: found constant: #{stmt.name} = #{stmt.value}" - container.add_constant(Constant.new(stmt.name.to_s, stmt.value.to_s, stmt.doc)) - end - end - end - - # create documentation for resources we can find in +code+ - # and associate it with +container+ - def scan_for_resource(container, code) - code = [code] unless code.is_a?(Array) - code.each do |stmt| - scan_for_resource(container,stmt.children) if stmt.is_a?(Puppet::Parser::AST::ASTArray) - - if stmt.is_a?(Puppet::Parser::AST::Resource) and !stmt.type.nil? - begin - type = stmt.type.split("::").collect { |s| s.capitalize }.join("::") - stmt.instances.each do |inst| - title = inst.title.is_a?(Puppet::Parser::AST::ASTArray) ? inst.title.to_s.gsub(/\[(.*)\]/,'\1') : inst.title.to_s - Puppet.debug "rdoc: found resource: #{type}[#{title}]" - - param = [] - inst.parameters.children.each do |p| - res = {} - res["name"] = p.param - res["value"] = "#{p.value.to_s}" unless p.value.nil? - - param << res - end - - container.add_resource(PuppetResource.new(type, title, stmt.doc, param)) - end - rescue => detail - raise Puppet::ParseError, "impossible to parse resource in #{stmt.file} at line #{stmt.line}: #{detail}" - end - end - end - end - - def resource_stmt_to_ref(stmt) - type = stmt.type.split("::").collect { |s| s.capitalize }.join("::") - title = stmt.title.is_a?(Puppet::Parser::AST::ASTArray) ? stmt.title.to_s.gsub(/\[(.*)\]/,'\1') : stmt.title.to_s - - param = stmt.params.children.collect do |p| - {"name" => p.param, "value" => p.value.to_s} - end - PuppetResource.new(type, title, stmt.doc, param) - end - - # create documentation for a class named +name+ - def document_class(name, klass, container) - Puppet.debug "rdoc: found new class #{name}" - container, name = get_class_or_module(container, name) - - superclass = klass.parent - superclass = "" if superclass.nil? or superclass.empty? - - @stats.num_classes += 1 - comment = klass.doc - look_for_directives_in(container, comment) unless comment.empty? - cls = container.add_class(PuppetClass, name, superclass) - # it is possible we already encountered this class, while parsing some namespaces - # from other classes of other files. But at that time we couldn't know this class superclass - # so, now we know it and force it. - cls.superclass = superclass - cls.record_location(@top_level) - - # scan class code for include - code = klass.code.children if klass.code.is_a?(Puppet::Parser::AST::ASTArray) - code ||= klass.code - unless code.nil? - scan_for_include_or_require(cls, code) - scan_for_realize(cls, code) - scan_for_resource(cls, code) if Puppet.settings[:document_all] - end - - cls.comment = comment - rescue => detail - raise Puppet::ParseError, "impossible to parse class '#{name}' in #{klass.file} at line #{klass.line}: #{detail}" - end - - # create documentation for a node - def document_node(name, node, container) - Puppet.debug "rdoc: found new node #{name}" - superclass = node.parent - superclass = "" if superclass.nil? or superclass.empty? - - comment = node.doc - look_for_directives_in(container, comment) unless comment.empty? - n = container.add_node(name, superclass) - n.record_location(@top_level) - - code = node.code.children if node.code.is_a?(Puppet::Parser::AST::ASTArray) - code ||= node.code - unless code.nil? - scan_for_include_or_require(n, code) - scan_for_realize(n, code) - scan_for_vardef(n, code) - scan_for_resource(n, code) if Puppet.settings[:document_all] - end - - n.comment = comment - rescue => detail - raise Puppet::ParseError, "impossible to parse node '#{name}' in #{node.file} at line #{node.line}: #{detail}" - end - - # create documentation for a define - def document_define(name, define, container) - Puppet.debug "rdoc: found new definition #{name}" - # find superclas if any - @stats.num_methods += 1 - - # find the parent - # split define name by :: to find the complete module hierarchy - container, name = get_class_or_module(container,name) - - # build up declaration - declaration = "" - define.arguments.each do |arg,value| - declaration << "\$#{arg}" - unless value.nil? - declaration << " => " - case value - when Puppet::Parser::AST::Leaf - declaration << "'#{value.value}'" - when Puppet::Parser::AST::ASTArray - declaration << "[#{value.children.collect { |v| "'#{v}'" }.join(", ")}]" - else - declaration << "#{value.to_s}" - end - end - declaration << ", " - end - declaration.chop!.chop! if declaration.size > 1 - - # register method into the container - meth = AnyMethod.new(declaration, name) - meth.comment = define.doc - container.add_method(meth) - look_for_directives_in(container, meth.comment) unless meth.comment.empty? - meth.params = "( #{declaration} )" - meth.visibility = :public - meth.document_self = true - meth.singleton = false - rescue => detail - raise Puppet::ParseError, "impossible to parse definition '#{name}' in #{define.file} at line #{define.line}: #{detail}" - end - - # Traverse the AST tree and produce code-objects node - # that contains the documentation - def parse_elements(container) - Puppet.debug "rdoc: scanning manifest" - - @known_resource_types.hostclasses.values.sort { |a,b| a.name <=> b.name }.each do |klass| - name = klass.name - if klass.file == @input_file_name - unless name.empty? - document_class(name,klass,container) - else # on main class document vardefs - code = klass.code.children if klass.code.is_a?(Puppet::Parser::AST::ASTArray) - code ||= klass.code - scan_for_vardef(container, code) unless code.nil? - end - end - end - - @known_resource_types.definitions.each do |name, define| - if define.file == @input_file_name - document_define(name,define,container) - end - end - - @known_resource_types.nodes.each do |name, node| - if node.file == @input_file_name - document_node(name.to_s,node,container) - end - end - end - - # create documentation for plugins - def parse_plugins(container) - Puppet.debug "rdoc: scanning plugin or fact" - if @input_file_name =~ /\/facter\/[^\/]+\.rb$/ - parse_fact(container) - else - parse_puppet_plugin(container) - end - end - - # this is a poor man custom fact parser :-) - def parse_fact(container) - comments = "" - current_fact = nil - File.open(@input_file_name) do |of| - of.each do |line| - # fetch comments - if line =~ /^[ \t]*# ?(.*)$/ - comments += $1 + "\n" - elsif line =~ /^[ \t]*Facter.add\(['"](.*?)['"]\)/ - current_fact = Fact.new($1,{}) - look_for_directives_in(container, comments) unless comments.empty? - current_fact.comment = comments - container.add_fact(current_fact) - current_fact.record_location(@top_level) - comments = "" - Puppet.debug "rdoc: found custom fact #{current_fact.name}" - elsif line =~ /^[ \t]*confine[ \t]*:(.*?)[ \t]*=>[ \t]*(.*)$/ - current_fact.confine = { :type => $1, :value => $2 } unless current_fact.nil? - else # unknown line type - comments ="" - end - end - end - end - - # this is a poor man puppet plugin parser :-) - # it doesn't extract doc nor desc :-( - def parse_puppet_plugin(container) - comments = "" - current_plugin = nil - - File.open(@input_file_name) do |of| - of.each do |line| - # fetch comments - if line =~ /^[ \t]*# ?(.*)$/ - comments += $1 + "\n" - elsif line =~ /^[ \t]*newfunction[ \t]*\([ \t]*:(.*?)[ \t]*,[ \t]*:type[ \t]*=>[ \t]*(:rvalue|:lvalue)\)/ - current_plugin = Plugin.new($1, "function") - container.add_plugin(current_plugin) - look_for_directives_in(container, comments) unless comments.empty? - current_plugin.comment = comments - current_plugin.record_location(@top_level) - comments = "" - Puppet.debug "rdoc: found new function plugins #{current_plugin.name}" - elsif line =~ /^[ \t]*Puppet::Type.newtype[ \t]*\([ \t]*:(.*?)\)/ - current_plugin = Plugin.new($1, "type") - container.add_plugin(current_plugin) - look_for_directives_in(container, comments) unless comments.empty? - current_plugin.comment = comments - current_plugin.record_location(@top_level) - comments = "" - Puppet.debug "rdoc: found new type plugins #{current_plugin.name}" - elsif line =~ /module Puppet::Parser::Functions/ - # skip - else # unknown line type - comments ="" - end - end - end - end - - # look_for_directives_in scans the current +comment+ for RDoc directives - def look_for_directives_in(context, comment) - preprocess = SM::PreProcess.new(@input_file_name, @options.rdoc_include) - - preprocess.handle(comment) do |directive, param| - case directive - when "stopdoc" - context.stop_doc - "" - when "startdoc" - context.start_doc - context.force_documentation = true - "" - when "enddoc" - #context.done_documenting = true - #"" - throw :enddoc - when "main" - options = Options.instance - options.main_page = param - "" - when "title" - options = Options.instance - options.title = param - "" - when "section" - context.set_current_section(param, comment) - comment.replace("") # 1.8 doesn't support #clear - break - else - warn "Unrecognized directive '#{directive}'" - break - end - end - remove_private_comments(comment) - end - - def remove_private_comments(comment) - comment.gsub!(/^#--.*?^#\+\+/m, '') - comment.sub!(/^#--.*/m, '') - end -end +begin + # Rdoc 1 imports + require "rdoc/tokenstream" + require "rdoc/markup/simple_markup/preprocess" + require "rdoc/parsers/parserfactory" + require "puppet/util/rdoc/parser/puppet_parser_rdoc1.rb" +rescue LoadError + # Current version imports + require "rdoc/token_stream" + require "rdoc/markup/pre_process" + require "rdoc/parser" + require "puppet/util/rdoc/parser/puppet_parser_rdoc2.rb" end diff --git a/lib/puppet/util/rdoc/parser/puppet_parser_core.rb b/lib/puppet/util/rdoc/parser/puppet_parser_core.rb new file mode 100644 index 000000000..dd7d03caf --- /dev/null +++ b/lib/puppet/util/rdoc/parser/puppet_parser_core.rb @@ -0,0 +1,477 @@ +# Functionality common to both our RDoc version 1 and 2 parsers. +module RDoc::PuppetParserCore + + SITE = "__site__" + + def self.included(base) + base.class_eval do + attr_accessor :input_file_name, :top_level + + # parser registration into RDoc + parse_files_matching(/\.(rb|pp)$/) + end + end + + # called with the top level file + def initialize(top_level, file_name, body, options, stats) + @options = options + @stats = stats + @input_file_name = file_name + @top_level = top_level + @top_level.extend(RDoc::PuppetTopLevel) + @progress = $stderr unless options.quiet + end + + # main entry point + def scan + environment = Puppet::Node::Environment.new + @known_resource_types = environment.known_resource_types + unless environment.known_resource_types.watching_file?(@input_file_name) + Puppet.info "rdoc: scanning #{@input_file_name}" + if @input_file_name =~ /\.pp$/ + @parser = Puppet::Parser::Parser.new(environment) + @parser.file = @input_file_name + @parser.parse.instantiate('').each do |type| + @known_resource_types.add type + end + end + end + + scan_top_level(@top_level) + @top_level + end + + # Due to a bug in RDoc, we need to roll our own find_module_named + # The issue is that RDoc tries harder by asking the parent for a class/module + # of the name. But by doing so, it can mistakenly use a module of same name + # but from which we are not descendant. + def find_object_named(container, name) + return container if container.name == name + container.each_classmodule do |m| + return m if m.name == name + end + nil + end + + # walk down the namespace and lookup/create container as needed + def get_class_or_module(container, name) + + # class ::A -> A is in the top level + if name =~ /^::/ + container = @top_level + end + + names = name.split('::') + + final_name = names.pop + names.each do |name| + prev_container = container + container = find_object_named(container, name) + container ||= prev_container.add_class(RDoc::PuppetClass, name, nil) + end + [container, final_name] + end + + # split_module tries to find if +path+ belongs to the module path + # if it does, it returns the module name, otherwise if we are sure + # it is part of the global manifest path, "__site__" is returned. + # And finally if this path couldn't be mapped anywhere, nil is returned. + def split_module(path) + # find a module + fullpath = File.expand_path(path) + Puppet.debug "rdoc: testing #{fullpath}" + if fullpath =~ /(.*)\/([^\/]+)\/(?:manifests|plugins|lib)\/.+\.(pp|rb)$/ + modpath = $1 + name = $2 + Puppet.debug "rdoc: module #{name} into #{modpath} ?" + Puppet::Node::Environment.new.modulepath.each do |mp| + if File.identical?(modpath,mp) + Puppet.debug "rdoc: found module #{name}" + return name + end + end + end + if fullpath =~ /\.(pp|rb)$/ + # there can be paths we don't want to scan under modules + # imagine a ruby or manifest that would be distributed as part as a module + # but we don't want those to be hosted under <site> + Puppet::Node::Environment.new.modulepath.each do |mp| + # check that fullpath is a descendant of mp + dirname = fullpath + previous = dirname + while (dirname = File.dirname(previous)) != previous + previous = dirname + return nil if File.identical?(dirname,mp) + end + end + end + # we are under a global manifests + Puppet.debug "rdoc: global manifests" + SITE + end + + # create documentation for the top level +container+ + def scan_top_level(container) + # use the module README as documentation for the module + comment = "" + %w{README README.rdoc}.each do |rfile| + readme = File.join(File.dirname(File.dirname(@input_file_name)), rfile) + comment = File.open(readme,"r") { |f| f.read } if FileTest.readable?(readme) + end + look_for_directives_in(container, comment) unless comment.empty? + + # infer module name from directory + name = split_module(@input_file_name) + if name.nil? + # skip .pp files that are not in manifests directories as we can't guarantee they're part + # of a module or the global configuration. + container.document_self = false + return + end + + Puppet.debug "rdoc: scanning for #{name}" + + container.module_name = name + container.global=true if name == SITE + + container, name = get_class_or_module(container,name) + mod = container.add_module(RDoc::PuppetModule, name) + mod.record_location(@top_level) + mod.add_comment(comment, @input_file_name) + + if @input_file_name =~ /\.pp$/ + parse_elements(mod) + elsif @input_file_name =~ /\.rb$/ + parse_plugins(mod) + end + end + + # create documentation for include statements we can find in +code+ + # and associate it with +container+ + def scan_for_include_or_require(container, code) + code = [code] unless code.is_a?(Array) + code.each do |stmt| + scan_for_include_or_require(container,stmt.children) if stmt.is_a?(Puppet::Parser::AST::BlockExpression) + + if stmt.is_a?(Puppet::Parser::AST::Function) and ['include','require'].include?(stmt.name) + stmt.arguments.each do |included| + Puppet.debug "found #{stmt.name}: #{included}" + container.send("add_#{stmt.name}", RDoc::Include.new(included.to_s, stmt.doc)) + end + end + end + end + + # create documentation for realize statements we can find in +code+ + # and associate it with +container+ + def scan_for_realize(container, code) + code = [code] unless code.is_a?(Array) + code.each do |stmt| + scan_for_realize(container,stmt.children) if stmt.is_a?(Puppet::Parser::AST::BlockExpression) + + if stmt.is_a?(Puppet::Parser::AST::Function) and stmt.name == 'realize' + stmt.arguments.each do |realized| + Puppet.debug "found #{stmt.name}: #{realized}" + container.add_realize( RDoc::Include.new(realized.to_s, stmt.doc)) + end + end + end + end + + # create documentation for global variables assignements we can find in +code+ + # and associate it with +container+ + def scan_for_vardef(container, code) + code = [code] unless code.is_a?(Array) + code.each do |stmt| + scan_for_vardef(container,stmt.children) if stmt.is_a?(Puppet::Parser::AST::BlockExpression) + + if stmt.is_a?(Puppet::Parser::AST::VarDef) + Puppet.debug "rdoc: found constant: #{stmt.name} = #{stmt.value}" + container.add_constant(RDoc::Constant.new(stmt.name.to_s, stmt.value.to_s, stmt.doc)) + end + end + end + + # create documentation for resources we can find in +code+ + # and associate it with +container+ + def scan_for_resource(container, code) + code = [code] unless code.is_a?(Array) + code.each do |stmt| + scan_for_resource(container,stmt.children) if stmt.is_a?(Puppet::Parser::AST::BlockExpression) + + if stmt.is_a?(Puppet::Parser::AST::Resource) and !stmt.type.nil? + begin + type = stmt.type.split("::").collect { |s| s.capitalize }.join("::") + stmt.instances.each do |inst| + title = inst.title.is_a?(Puppet::Parser::AST::ASTArray) ? inst.title.to_s.gsub(/\[(.*)\]/,'\1') : inst.title.to_s + Puppet.debug "rdoc: found resource: #{type}[#{title}]" + + param = [] + inst.parameters.children.each do |p| + res = {} + res["name"] = p.param + res["value"] = "#{p.value.to_s}" unless p.value.nil? + + param << res + end + + container.add_resource(RDoc::PuppetResource.new(type, title, stmt.doc, param)) + end + rescue => detail + raise Puppet::ParseError, "impossible to parse resource in #{stmt.file} at line #{stmt.line}: #{detail}" + end + end + end + end + + # create documentation for a class named +name+ + def document_class(name, klass, container) + Puppet.debug "rdoc: found new class #{name}" + container, name = get_class_or_module(container, name) + + superclass = klass.parent + superclass = "" if superclass.nil? or superclass.empty? + + comment = klass.doc + look_for_directives_in(container, comment) unless comment.empty? + cls = container.add_class(RDoc::PuppetClass, name, superclass) + # it is possible we already encountered this class, while parsing some namespaces + # from other classes of other files. But at that time we couldn't know this class superclass + # so, now we know it and force it. + cls.superclass = superclass + cls.record_location(@top_level) + + # scan class code for include + code = klass.code.children if klass.code.is_a?(Puppet::Parser::AST::BlockExpression) + code ||= klass.code + unless code.nil? + scan_for_include_or_require(cls, code) + scan_for_realize(cls, code) + scan_for_resource(cls, code) if Puppet.settings[:document_all] + end + + cls.add_comment(comment, klass.file) + rescue => detail + raise Puppet::ParseError, "impossible to parse class '#{name}' in #{klass.file} at line #{klass.line}: #{detail}" + end + + # create documentation for a node + def document_node(name, node, container) + Puppet.debug "rdoc: found new node #{name}" + superclass = node.parent + superclass = "" if superclass.nil? or superclass.empty? + + comment = node.doc + look_for_directives_in(container, comment) unless comment.empty? + n = container.add_node(name, superclass) + n.record_location(@top_level) + + code = node.code.children if node.code.is_a?(Puppet::Parser::AST::BlockExpression) + code ||= node.code + unless code.nil? + scan_for_include_or_require(n, code) + scan_for_realize(n, code) + scan_for_vardef(n, code) + scan_for_resource(n, code) if Puppet.settings[:document_all] + end + + n.add_comment(comment, node.file) + rescue => detail + raise Puppet::ParseError, "impossible to parse node '#{name}' in #{node.file} at line #{node.line}: #{detail}" + end + + # create documentation for a define + def document_define(name, define, container) + Puppet.debug "rdoc: found new definition #{name}" + # find superclas if any + + # find the parent + # split define name by :: to find the complete module hierarchy + container, name = get_class_or_module(container,name) + + # build up declaration + declaration = "" + define.arguments.each do |arg,value| + declaration << "\$#{arg}" + unless value.nil? + declaration << " => " + case value + when Puppet::Parser::AST::Leaf + declaration << "'#{value.value}'" + when Puppet::Parser::AST::BlockExpression + declaration << "[#{value.children.collect { |v| "'#{v}'" }.join(", ")}]" + else + declaration << "#{value.to_s}" + end + end + declaration << ", " + end + declaration.chop!.chop! if declaration.size > 1 + + # register method into the container + meth = RDoc::AnyMethod.new(declaration, name) + meth.comment = define.doc + container.add_method(meth) + look_for_directives_in(container, meth.comment) unless meth.comment.empty? + meth.params = "( #{declaration} )" + meth.visibility = :public + meth.document_self = true + meth.singleton = false + rescue => detail + raise Puppet::ParseError, "impossible to parse definition '#{name}' in #{define.file} at line #{define.line}: #{detail}" + end + + # Traverse the AST tree and produce code-objects node + # that contains the documentation + def parse_elements(container) + Puppet.debug "rdoc: scanning manifest" + + @known_resource_types.hostclasses.values.sort { |a,b| a.name <=> b.name }.each do |klass| + name = klass.name + if klass.file == @input_file_name + unless name.empty? + document_class(name,klass,container) + else # on main class document vardefs + code = klass.code.children if klass.code.is_a?(Puppet::Parser::AST::BlockExpression) + code ||= klass.code + scan_for_vardef(container, code) unless code.nil? + end + end + end + + @known_resource_types.definitions.each do |name, define| + if define.file == @input_file_name + document_define(name,define,container) + end + end + + @known_resource_types.nodes.each do |name, node| + if node.file == @input_file_name + document_node(name.to_s,node,container) + end + end + end + + # create documentation for plugins + def parse_plugins(container) + Puppet.debug "rdoc: scanning plugin or fact" + if @input_file_name =~ /\/facter\/[^\/]+\.rb$/ + parse_fact(container) + else + parse_puppet_plugin(container) + end + end + + # this is a poor man custom fact parser :-) + def parse_fact(container) + comments = "" + current_fact = nil + parsed_facts = [] + File.open(@input_file_name) do |of| + of.each do |line| + # fetch comments + if line =~ /^[ \t]*# ?(.*)$/ + comments += $1 + "\n" + elsif line =~ /^[ \t]*Facter.add\(['"](.*?)['"]\)/ + current_fact = RDoc::Fact.new($1,{}) + look_for_directives_in(container, comments) unless comments.empty? + current_fact.comment = comments + parsed_facts << current_fact + comments = "" + Puppet.debug "rdoc: found custom fact #{current_fact.name}" + elsif line =~ /^[ \t]*confine[ \t]*:(.*?)[ \t]*=>[ \t]*(.*)$/ + current_fact.confine = { :type => $1, :value => $2 } unless current_fact.nil? + else # unknown line type + comments ="" + end + end + end + parsed_facts.each do |f| + container.add_fact(f) + f.record_location(@top_level) + end + end + + # this is a poor man puppet plugin parser :-) + # it doesn't extract doc nor desc :-( + def parse_puppet_plugin(container) + comments = "" + current_plugin = nil + + File.open(@input_file_name) do |of| + of.each do |line| + # fetch comments + if line =~ /^[ \t]*# ?(.*)$/ + comments += $1 + "\n" + elsif line =~ /^[ \t]*(?:Puppet::Parser::Functions::)?newfunction[ \t]*\([ \t]*:(.*?)[ \t]*,[ \t]*:type[ \t]*=>[ \t]*(:rvalue|:lvalue)/ + current_plugin = RDoc::Plugin.new($1, "function") + look_for_directives_in(container, comments) unless comments.empty? + current_plugin.comment = comments + current_plugin.record_location(@top_level) + container.add_plugin(current_plugin) + comments = "" + Puppet.debug "rdoc: found new function plugins #{current_plugin.name}" + elsif line =~ /^[ \t]*Puppet::Type.newtype[ \t]*\([ \t]*:(.*?)\)/ + current_plugin = RDoc::Plugin.new($1, "type") + look_for_directives_in(container, comments) unless comments.empty? + current_plugin.comment = comments + current_plugin.record_location(@top_level) + container.add_plugin(current_plugin) + comments = "" + Puppet.debug "rdoc: found new type plugins #{current_plugin.name}" + elsif line =~ /module Puppet::Parser::Functions/ + # skip + else # unknown line type + comments ="" + end + end + end + end + + # New instance of the appropriate PreProcess for our RDoc version. + def create_rdoc_preprocess + raise(NotImplementedError, "This method must be overwritten for whichever version of RDoc this parser is working with") + end + + # look_for_directives_in scans the current +comment+ for RDoc directives + def look_for_directives_in(context, comment) + preprocess = create_rdoc_preprocess + + preprocess.handle(comment) do |directive, param| + case directive + when "stopdoc" + context.stop_doc + "" + when "startdoc" + context.start_doc + context.force_documentation = true + "" + when "enddoc" + #context.done_documenting = true + #"" + throw :enddoc + when "main" + options = Options.instance + options.main_page = param + "" + when "title" + options = Options.instance + options.title = param + "" + when "section" + context.set_current_section(param, comment) + comment.replace("") # 1.8 doesn't support #clear + break + else + warn "Unrecognized directive '#{directive}'" + break + end + end + remove_private_comments(comment) + end + + def remove_private_comments(comment) + comment.gsub!(/^#--.*?^#\+\+/m, '') + comment.sub!(/^#--.*/m, '') + end +end diff --git a/lib/puppet/util/rdoc/parser/puppet_parser_rdoc1.rb b/lib/puppet/util/rdoc/parser/puppet_parser_rdoc1.rb new file mode 100644 index 000000000..656d2f7e7 --- /dev/null +++ b/lib/puppet/util/rdoc/parser/puppet_parser_rdoc1.rb @@ -0,0 +1,19 @@ +require 'puppet/util/rdoc/parser/puppet_parser_core.rb' + +module RDoc + PUPPET_RDOC_VERSION = 1 + + # @api private + class PuppetParserRDoc1 + extend ParserFactory + include PuppetParserCore + + def create_rdoc_preprocess + preprocess = SM::PreProcess.new(@input_file_name, @options.rdoc_include) + end + end + + # For backwards compatibility + # @api private + Parser = PuppetParserRDoc1 +end diff --git a/lib/puppet/util/rdoc/parser/puppet_parser_rdoc2.rb b/lib/puppet/util/rdoc/parser/puppet_parser_rdoc2.rb new file mode 100644 index 000000000..75a5cdac3 --- /dev/null +++ b/lib/puppet/util/rdoc/parser/puppet_parser_rdoc2.rb @@ -0,0 +1,14 @@ +require 'puppet/util/rdoc/parser/puppet_parser_core.rb' + +module RDoc + PUPPET_RDOC_VERSION = 2 + + # @api private + class PuppetParserRDoc2 < Parser + include PuppetParserCore + + def create_rdoc_preprocess + preprocess = Markup::PreProcess.new(@input_file_name, @options.rdoc_include) + end + end +end diff --git a/lib/puppet/util/reference.rb b/lib/puppet/util/reference.rb index 2e2ee78f6..491e39b09 100644 --- a/lib/puppet/util/reference.rb +++ b/lib/puppet/util/reference.rb @@ -46,7 +46,7 @@ class Puppet::Util::Reference # There used to be an attempt to use secure_open / replace_file to secure # the target, too, but that did nothing: the race was still here. We can # get exactly the same benefit from running this effort: - File.unlink('/tmp/puppetdoc.tex') rescue nil + Puppet::FileSystem::File.unlink('/tmp/puppetdoc.tex') rescue nil output = %x{#{cmd}} unless $CHILD_STATUS == 0 $stderr.puts "rst2latex failed" diff --git a/lib/puppet/util/resource_template.rb b/lib/puppet/util/resource_template.rb index b12b125b5..bed585b21 100644 --- a/lib/puppet/util/resource_template.rb +++ b/lib/puppet/util/resource_template.rb @@ -44,7 +44,7 @@ class Puppet::Util::ResourceTemplate end def initialize(file, resource) - raise ArgumentError, "Template #{file} does not exist" unless FileTest.exist?(file) + raise ArgumentError, "Template #{file} does not exist" unless Puppet::FileSystem::File.exist?(file) @file = file @resource = resource end diff --git a/lib/puppet/util/selinux.rb b/lib/puppet/util/selinux.rb index 0b712be0d..78c3c4dfa 100644 --- a/lib/puppet/util/selinux.rb +++ b/lib/puppet/util/selinux.rb @@ -216,7 +216,7 @@ module Puppet::Util::SELinux # # @return [File::Stat] File.lstat result def file_lstat(path) - File.lstat(path) + Puppet::FileSystem::File.new(path).lstat end private :file_lstat end diff --git a/lib/puppet/util/storage.rb b/lib/puppet/util/storage.rb index 32d744862..9df1cb501 100644 --- a/lib/puppet/util/storage.rb +++ b/lib/puppet/util/storage.rb @@ -45,7 +45,7 @@ class Puppet::Util::Storage Puppet.settings.use(:main) unless FileTest.directory?(Puppet[:statedir]) filename = Puppet[:statefile] - unless File.exists?(filename) + unless Puppet::FileSystem::File.exist?(filename) self.init if @@state.nil? return end @@ -80,7 +80,7 @@ class Puppet::Util::Storage def self.store Puppet.debug "Storing state" - Puppet.info "Creating state file #{Puppet[:statefile]}" unless FileTest.exist?(Puppet[:statefile]) + Puppet.info "Creating state file #{Puppet[:statefile]}" unless Puppet::FileSystem::File.exist?(Puppet[:statefile]) Puppet::Util.benchmark(:debug, "Stored state") do Puppet::Util::Yaml.dump(@@state, Puppet[:statefile]) diff --git a/lib/puppet/util/suidmanager.rb b/lib/puppet/util/suidmanager.rb index e2b063992..481e3864c 100644 --- a/lib/puppet/util/suidmanager.rb +++ b/lib/puppet/util/suidmanager.rb @@ -178,7 +178,7 @@ module Puppet::Util::SUIDManager # :custom_environment (default {}) -- a hash of key/value pairs to set as environment variables for the duration # of the command def run_and_capture(command, new_uid=nil, new_gid=nil, options = {}) - + Puppet.deprecation_warning("Puppet::Util::SUIDManager.run_and_capture is deprecated; please use Puppet::Util::Execution.execute instead.") # specifying these here rather than in the method signature to allow callers to pass in a partial # set of overrides without affecting the default values for options that they don't pass in default_options = { diff --git a/lib/puppet/util/tag_set.rb b/lib/puppet/util/tag_set.rb new file mode 100644 index 000000000..bd1029040 --- /dev/null +++ b/lib/puppet/util/tag_set.rb @@ -0,0 +1,29 @@ +require 'set' + +class Puppet::Util::TagSet < Set + def self.from_yaml(yaml) + self.new(YAML.load(yaml)) + end + + def to_yaml + @hash.keys.to_yaml + end + + def self.from_pson(data) + self.new(data) + end + + def to_pson(*args) + to_a.to_pson + end + + # this makes puppet serialize it as an array for backwards + # compatibility + def to_zaml(z) + to_a.to_zaml(z) + end + + def join(*args) + to_a.join(*args) + end +end diff --git a/lib/puppet/util/tagging.rb b/lib/puppet/util/tagging.rb index 4161c0291..2e788279b 100644 --- a/lib/puppet/util/tagging.rb +++ b/lib/puppet/util/tagging.rb @@ -1,30 +1,10 @@ -# Created on 2008-01-19 -# Copyright Luke Kanies +require 'puppet/util/tag_set' -# A common module to handle tagging. -# -# So, do you want the bad news or the good news first? -# -# The bad news is that using an array here is hugely costly compared to using -# a hash. Like, the same speed empty, 50 percent slower with one item, and -# 300 percent slower at 6 - one of our common peaks for tagging items. -# -# ...and that assumes an efficient implementation, just using include?. These -# methods have even more costs hidden in them. -# -# The good news is that this module has no API. Various objects directly -# interact with their `@tags` member as an array, or dump it directly in YAML, -# or whatever. -# -# So, er, you can't actually change this. No matter how much you want to be -# cause it is inefficient in both CPU and object allocation terms. -# -# Good luck, my friend. --daniel 2012-07-17 module Puppet::Util::Tagging # Add a tag to our current list. These tags will be added to all # of the objects contained in this scope. def tag(*ary) - @tags ||= [] + @tags ||= new_tags qualified = [] @@ -45,12 +25,12 @@ module Puppet::Util::Tagging # Return a copy of the tag list, so someone can't ask for our tags # and then modify them. def tags - @tags ||= [] + @tags ||= new_tags @tags.dup end def tags=(tags) - @tags = [] + @tags = new_tags return if tags.nil? or tags == "" @@ -73,4 +53,8 @@ module Puppet::Util::Tagging def valid_tag?(tag) tag.is_a?(String) and tag =~ ValidTagRegex end + + def new_tags + Puppet::Util::TagSet.new + end end diff --git a/lib/puppet/util/watched_file.rb b/lib/puppet/util/watched_file.rb index 6b28ab402..3e1195700 100755..100644 --- a/lib/puppet/util/watched_file.rb +++ b/lib/puppet/util/watched_file.rb @@ -26,7 +26,7 @@ class Puppet::Util::WatchedFile end # Allow this to be used as the name of the file being watched in various - # other methods (such as File.exist?) + # other methods (such as Puppet::FileSystem::File.exist?) def to_str @filename end diff --git a/lib/puppet/util/watcher.rb b/lib/puppet/util/watcher.rb index 78b21f8af..547c24c9e 100644 --- a/lib/puppet/util/watcher.rb +++ b/lib/puppet/util/watcher.rb @@ -7,7 +7,7 @@ module Puppet::Util::Watcher def self.file_ctime_change_watcher(filename) Puppet::Util::Watcher::ChangeWatcher.watch(lambda do begin - File.stat(filename).ctime + Puppet::FileSystem::File.new(filename).stat.ctime rescue Errno::ENOENT, Errno::ENOTDIR :absent end diff --git a/lib/puppet/util/windows.rb b/lib/puppet/util/windows.rb index 612f72b78..af440552f 100644 --- a/lib/puppet/util/windows.rb +++ b/lib/puppet/util/windows.rb @@ -8,6 +8,9 @@ module Puppet::Util::Windows require 'puppet/util/windows/process' require 'puppet/util/windows/file' require 'puppet/util/windows/root_certs' + require 'puppet/util/windows/access_control_entry' + require 'puppet/util/windows/access_control_list' + require 'puppet/util/windows/security_descriptor' end require 'puppet/util/windows/registry' end diff --git a/lib/puppet/util/windows/access_control_entry.rb b/lib/puppet/util/windows/access_control_entry.rb new file mode 100644 index 000000000..5cd052dae --- /dev/null +++ b/lib/puppet/util/windows/access_control_entry.rb @@ -0,0 +1,84 @@ +# Windows Access Control Entry +# +# Represents an access control entry, which grants or denies a subject, +# identified by a SID, rights to a securable object. +# +# @see http://msdn.microsoft.com/en-us/library/windows/desktop/aa374868(v=vs.85).aspx +# @api private +class Puppet::Util::Windows::AccessControlEntry + require 'puppet/util/windows/security' + include Puppet::Util::Windows::SID + + attr_accessor :sid + attr_reader :mask, :flags, :type + + OBJECT_INHERIT_ACE = 0x1 + CONTAINER_INHERIT_ACE = 0x2 + NO_PROPAGATE_INHERIT_ACE = 0x4 + INHERIT_ONLY_ACE = 0x8 + INHERITED_ACE = 0x10 + + ACCESS_ALLOWED_ACE_TYPE = 0x0 + ACCESS_DENIED_ACE_TYPE = 0x1 + + def initialize(sid, mask, flags = 0, type = ACCESS_ALLOWED_ACE_TYPE) + @sid = sid + @mask = mask + @flags = flags + @type = type + end + + # Returns true if this ACE is inherited from a parent. If false, + # then the ACE is set directly on the object to which it refers. + # + # @return [Boolean] true if the ACE is inherited + def inherited? + (@flags & INHERITED_ACE) == INHERITED_ACE + end + + # Returns true if this ACE only applies to children of the object. + # If false, it applies to the object. + # + # @return [Boolean] true if the ACE only applies to children and + # not the object itself. + def inherit_only? + (@flags & INHERIT_ONLY_ACE) == INHERIT_ONLY_ACE + end + + # Returns true if this ACE applies to child directories. + # + # @return [Boolean] true if the ACE applies to child direcories + def container_inherit? + (@flags & CONTAINER_INHERIT_ACE) == CONTAINER_INHERIT_ACE + end + + # Returns true if this ACE applies to child files. + # + # @return [Boolean] true if the ACE applies to child files. + def object_inherit? + (@flags & OBJECT_INHERIT_ACE) == OBJECT_INHERIT_ACE + end + + def inspect + inheritance = "" + inheritance << '(I)' if inherited? + inheritance << '(OI)' if object_inherit? + inheritance << '(CI)' if container_inherit? + inheritance << '(IO)' if inherit_only? + + left = "#{sid_to_name(sid)}:#{inheritance}" + left = left.ljust(45) + "#{left} 0x#{mask.to_s(16)}" + end + + # Returns true if this ACE is equal to +other+ + def ==(other) + self.class == other.class && + sid == other.sid && + mask == other.mask && + flags == other.flags && + type == other.type + end + + alias eql? == +end diff --git a/lib/puppet/util/windows/access_control_list.rb b/lib/puppet/util/windows/access_control_list.rb new file mode 100644 index 000000000..14a924cd9 --- /dev/null +++ b/lib/puppet/util/windows/access_control_list.rb @@ -0,0 +1,106 @@ +# Windows Access Control List +# +# Represents a list of access control entries (ACEs). +# +# @see http://msdn.microsoft.com/en-us/library/windows/desktop/aa374872(v=vs.85).aspx +# @api private +class Puppet::Util::Windows::AccessControlList + include Enumerable + + ACCESS_ALLOWED_ACE_TYPE = 0x0 + ACCESS_DENIED_ACE_TYPE = 0x1 + + # Construct an ACL. + # + # @param acl [Enumerable] A list of aces to copy from. + def initialize(acl = nil) + if acl + @aces = acl.map(&:dup) + else + @aces = [] + end + end + + # Enumerate each ACE in the list. + # + # @yieldparam ace [Hash] the ace + def each + @aces.each {|ace| yield ace} + end + + # Allow the +sid+ to access a resource with the specified access +mask+. + # + # @param sid [String] The SID that the ACE is granting access to + # @param mask [int] The access mask granted to the SID + # @param flags [int] The flags assigned to the ACE, e.g. +INHERIT_ONLY_ACE+ + def allow(sid, mask, flags = 0) + @aces << Puppet::Util::Windows::AccessControlEntry.new(sid, mask, flags, ACCESS_ALLOWED_ACE_TYPE) + end + + # Deny the +sid+ access to a resource with the specified access +mask+. + # + # @param sid [String] The SID that the ACE is denying access to + # @param mask [int] The access mask denied to the SID + # @param flags [int] The flags assigned to the ACE, e.g. +INHERIT_ONLY_ACE+ + def deny(sid, mask, flags = 0) + @aces << Puppet::Util::Windows::AccessControlEntry.new(sid, mask, flags, ACCESS_DENIED_ACE_TYPE) + end + + # Reassign all ACEs currently assigned to +old_sid+ to +new_sid+ instead. + # If an ACE is inherited or is not assigned to +old_sid+, then it will + # be copied as-is to the new ACL, preserving its order within the ACL. + # + # @param old_sid [String] The old SID, e.g. 'S-1-5-18' + # @param new_sid [String] The new SID + # @return [AccessControlList] The copied ACL. + def reassign!(old_sid, new_sid) + new_aces = [] + prepend_needed = false + aces_to_prepend = [] + + @aces.each do |ace| + new_ace = ace.dup + + if ace.sid == old_sid + if ace.inherited? + # create an explicit ACE granting or denying the + # new_sid the rights that the inherited ACE + # granted or denied the old_sid. We mask off all + # flags except those affecting inheritance of the + # ACE we're creating. + inherit_mask = Windows::Security::CONTAINER_INHERIT_ACE | + Windows::Security::OBJECT_INHERIT_ACE | + Windows::Security::INHERIT_ONLY_ACE + explicit_ace = Puppet::Util::Windows::AccessControlEntry.new(new_sid, ace.mask, ace.flags & inherit_mask, ace.type) + aces_to_prepend << explicit_ace + else + new_ace.sid = new_sid + + prepend_needed = old_sid == Win32::Security::SID::LocalSystem + end + end + new_aces << new_ace + end + + @aces = [] + + if prepend_needed + mask = Windows::Security::STANDARD_RIGHTS_ALL | Windows::Security::SPECIFIC_RIGHTS_ALL + ace = Puppet::Util::Windows::AccessControlEntry.new( + Win32::Security::SID::LocalSystem, + mask) + @aces << ace + end + + @aces.concat(aces_to_prepend) + @aces.concat(new_aces) + end + + def inspect + str = "" + @aces.each do |ace| + str << " #{ace.inspect}\n" + end + str + end +end diff --git a/lib/puppet/util/windows/file.rb b/lib/puppet/util/windows/file.rb index d4b7bc1ca..141873071 100644 --- a/lib/puppet/util/windows/file.rb +++ b/lib/puppet/util/windows/file.rb @@ -1,6 +1,7 @@ require 'puppet/util/windows' module Puppet::Util::Windows::File + require 'ffi' require 'windows/api' require 'windows/wide_string' @@ -24,4 +25,216 @@ module Puppet::Util::Windows::File new("MoveFileEx(#{source}, #{target}, #{flags.to_s(8)})") end module_function :move_file_ex + + module API + extend FFI::Library + ffi_lib 'kernel32' + ffi_convention :stdcall + + # BOOLEAN WINAPI CreateSymbolicLink( + # _In_ LPTSTR lpSymlinkFileName, - symbolic link to be created + # _In_ LPTSTR lpTargetFileName, - name of target for symbolic link + # _In_ DWORD dwFlags - 0x0 target is a file, 0x1 target is a directory + # ); + # rescue on Windows < 6.0 so that code doesn't explode + begin + attach_function :create_symbolic_link, :CreateSymbolicLinkW, + [:buffer_in, :buffer_in, :uint], :bool + rescue LoadError + end + + # DWORD WINAPI GetFileAttributes( + # _In_ LPCTSTR lpFileName + # ); + attach_function :get_file_attributes, :GetFileAttributesW, + [:buffer_in], :uint + + # HANDLE WINAPI CreateFile( + # _In_ LPCTSTR lpFileName, + # _In_ DWORD dwDesiredAccess, + # _In_ DWORD dwShareMode, + # _In_opt_ LPSECURITY_ATTRIBUTES lpSecurityAttributes, + # _In_ DWORD dwCreationDisposition, + # _In_ DWORD dwFlagsAndAttributes, + # _In_opt_ HANDLE hTemplateFile + # ); + attach_function :create_file, :CreateFileW, + [:buffer_in, :uint, :uint, :pointer, :uint, :uint, :uint], :uint + + # http://msdn.microsoft.com/en-us/library/windows/desktop/aa363216(v=vs.85).aspx + # BOOL WINAPI DeviceIoControl( + # _In_ HANDLE hDevice, + # _In_ DWORD dwIoControlCode, + # _In_opt_ LPVOID lpInBuffer, + # _In_ DWORD nInBufferSize, + # _Out_opt_ LPVOID lpOutBuffer, + # _In_ DWORD nOutBufferSize, + # _Out_opt_ LPDWORD lpBytesReturned, + # _Inout_opt_ LPOVERLAPPED lpOverlapped + # ); + attach_function :device_io_control, :DeviceIoControl, + [:uint, :uint, :pointer, :uint, :pointer, :uint, :pointer, :pointer], :bool + + MAXIMUM_REPARSE_DATA_BUFFER_SIZE = 16384 + + # REPARSE_DATA_BUFFER + # http://msdn.microsoft.com/en-us/library/cc232006.aspx + # http://msdn.microsoft.com/en-us/library/windows/hardware/ff552012(v=vs.85).aspx + # struct is always MAXIMUM_REPARSE_DATA_BUFFER_SIZE bytes + class ReparseDataBuffer < FFI::Struct + layout :reparse_tag, :uint, + :reparse_data_length, :ushort, + :reserved, :ushort, + :substitute_name_offset, :ushort, + :substitute_name_length, :ushort, + :print_name_offset, :ushort, + :print_name_length, :ushort, + :flags, :uint, + # max less above fields dword / uint 4 bytes, ushort 2 bytes + :path_buffer, [:uchar, MAXIMUM_REPARSE_DATA_BUFFER_SIZE - 20] + end + + # BOOL WINAPI CloseHandle( + # _In_ HANDLE hObject + # ); + attach_function :close_handle, :CloseHandle, [:uint], :bool + end + + def symlink(target, symlink) + flags = File.directory?(target) ? 0x1 : 0x0 + result = API.create_symbolic_link(WideString.new(symlink.to_s), + WideString.new(target.to_s), flags) + return true if result + raise Puppet::Util::Windows::Error.new( + "CreateSymbolicLink(#{symlink}, #{target}, #{flags.to_s(8)})") + end + module_function :symlink + + INVALID_FILE_ATTRIBUTES = 0xFFFFFFFF #define INVALID_FILE_ATTRIBUTES (DWORD (-1)) + def self.get_file_attributes(file_name) + result = API.get_file_attributes(WideString.new(file_name.to_s)) + return result unless result == INVALID_FILE_ATTRIBUTES + raise Puppet::Util::Windows::Error.new("GetFileAttributes(#{file_name})") + end + + INVALID_HANDLE_VALUE = -1 #define INVALID_HANDLE_VALUE ((HANDLE)(LONG_PTR)-1) + def self.create_file(file_name, desired_access, share_mode, security_attributes, + creation_disposition, flags_and_attributes, template_file_handle) + + result = API.create_file(WideString.new(file_name.to_s), + desired_access, share_mode, security_attributes, creation_disposition, + flags_and_attributes, template_file_handle) + + return result unless result == INVALID_HANDLE_VALUE + raise Puppet::Util::Windows::Error.new( + "CreateFile(#{file_name}, #{desired_access.to_s(8)}, #{share_mode.to_s(8)}, " + + "#{security_attributes}, #{creation_disposition.to_s(8)}, " + + "#{flags_and_attributes.to_s(8)}, #{template_file_handle})") + end + + def self.device_io_control(handle, io_control_code, in_buffer = nil, out_buffer = nil) + if out_buffer.nil? + raise Puppet::Util::Windows::Error.new("out_buffer is required") + end + + result = API.device_io_control( + handle, + io_control_code, + in_buffer, in_buffer.nil? ? 0 : in_buffer.size, + out_buffer, out_buffer.size, + FFI::MemoryPointer.new(:uint, 1), + nil + ) + + return out_buffer if result + raise Puppet::Util::Windows::Error.new( + "DeviceIoControl(#{handle}, #{io_control_code}, #{in_buffer}, #{in_buffer.size}, " + + "#{out_buffer}, #{out_buffer.size}") + end + + FILE_ATTRIBUTE_REPARSE_POINT = 0x400 + def symlink?(file_name) + begin + attributes = get_file_attributes(file_name) + (attributes & FILE_ATTRIBUTE_REPARSE_POINT) == FILE_ATTRIBUTE_REPARSE_POINT + rescue + # raised INVALID_FILE_ATTRIBUTES is equivalent to file not found + false + end + end + module_function :symlink? + + GENERIC_READ = 0x80000000 + FILE_SHARE_READ = 1 + OPEN_EXISTING = 3 + FILE_FLAG_OPEN_REPARSE_POINT = 0x00200000 + FILE_FLAG_BACKUP_SEMANTICS = 0x02000000 + + def self.open_symlink(link_name) + begin + yield handle = create_file( + WideString.new(link_name.to_s), + GENERIC_READ, + FILE_SHARE_READ, + nil, # security_attributes + OPEN_EXISTING, + FILE_FLAG_OPEN_REPARSE_POINT | FILE_FLAG_BACKUP_SEMANTICS, + 0) # template_file + ensure + API.close_handle(handle) if handle + end + end + + def readlink(link_name) + open_symlink(link_name) do |handle| + resolve_symlink(handle) + end + end + module_function :readlink + + def stat(file_name) + file_name = file_name.to_s # accomodate PathName or String + stat = File.stat(file_name) + if symlink?(file_name) + link_ftype = File.stat(readlink(file_name)).ftype + # sigh, monkey patch instance method for instance, and close over link_ftype + singleton_class = class << stat; self; end + singleton_class.send(:define_method, :ftype) do + link_ftype + end + end + stat + end + module_function :stat + + def lstat(file_name) + file_name = file_name.to_s # accomodate PathName or String + # monkey'ing around! + stat = File.lstat(file_name) + if symlink?(file_name) + def stat.ftype + "link" + end + end + stat + end + module_function :lstat + + private + + # http://msdn.microsoft.com/en-us/library/windows/desktop/aa364571(v=vs.85).aspx + FSCTL_GET_REPARSE_POINT = 0x900a8 + + def self.resolve_symlink(handle) + # must be multiple of 1024, min 10240 + out_buffer = FFI::MemoryPointer.new(API::ReparseDataBuffer.size) + device_io_control(handle, FSCTL_GET_REPARSE_POINT, nil, out_buffer) + + reparse_data = API::ReparseDataBuffer.new(out_buffer) + offset = reparse_data[:print_name_offset] + length = reparse_data[:print_name_length] + + result = reparse_data[:path_buffer].to_a[offset, length].pack('C*') + result.force_encoding('UTF-16LE').encode(Encoding.default_external) + end end diff --git a/lib/puppet/util/windows/process.rb b/lib/puppet/util/windows/process.rb index fc85119c3..165e90af0 100644 --- a/lib/puppet/util/windows/process.rb +++ b/lib/puppet/util/windows/process.rb @@ -8,6 +8,117 @@ module Puppet::Util::Windows::Process extend ::Windows::Handle extend ::Windows::Synchronize + module API + require 'ffi' + extend FFI::Library + ffi_convention :stdcall + + ffi_lib 'kernel32' + + # http://msdn.microsoft.com/en-us/library/windows/desktop/ms683179(v=vs.85).aspx + # HANDLE WINAPI GetCurrentProcess(void); + attach_function :get_current_process, :GetCurrentProcess, [], :uint + + # BOOL WINAPI CloseHandle( + # _In_ HANDLE hObject + # ); + attach_function :close_handle, :CloseHandle, [:uint], :bool + + ffi_lib 'advapi32' + + # http://msdn.microsoft.com/en-us/library/windows/desktop/aa379295(v=vs.85).aspx + # BOOL WINAPI OpenProcessToken( + # _In_ HANDLE ProcessHandle, + # _In_ DWORD DesiredAccess, + # _Out_ PHANDLE TokenHandle + # ); + attach_function :open_process_token, :OpenProcessToken, + [:uint, :uint, :pointer], :bool + + + # http://msdn.microsoft.com/en-us/library/windows/desktop/aa379261(v=vs.85).aspx + # typedef struct _LUID { + # DWORD LowPart; + # LONG HighPart; + # } LUID, *PLUID; + class LUID < FFI::Struct + layout :low_part, :uint, + :high_part, :int + end + + # http://msdn.microsoft.com/en-us/library/Windows/desktop/aa379180(v=vs.85).aspx + # BOOL WINAPI LookupPrivilegeValue( + # _In_opt_ LPCTSTR lpSystemName, + # _In_ LPCTSTR lpName, + # _Out_ PLUID lpLuid + # ); + attach_function :lookup_privilege_value, :LookupPrivilegeValueW, + [:buffer_in, :buffer_in, :pointer], :bool + + Token_Information = enum( + :token_user, 1, + :token_groups, + :token_privileges, + :token_owner, + :token_primary_group, + :token_default_dacl, + :token_source, + :token_type, + :token_impersonation_level, + :token_statistics, + :token_restricted_sids, + :token_session_id, + :token_groups_and_privileges, + :token_session_reference, + :token_sandbox_inert, + :token_audit_policy, + :token_origin, + :token_elevation_type, + :token_linked_token, + :token_elevation, + :token_has_restrictions, + :token_access_information, + :token_virtualization_allowed, + :token_virtualization_enabled, + :token_integrity_level, + :token_ui_access, + :token_mandatory_policy, + :token_logon_sid, + :max_token_info_class + ) + + # http://msdn.microsoft.com/en-us/library/windows/desktop/aa379263(v=vs.85).aspx + # typedef struct _LUID_AND_ATTRIBUTES { + # LUID Luid; + # DWORD Attributes; + # } LUID_AND_ATTRIBUTES, *PLUID_AND_ATTRIBUTES; + class LUID_And_Attributes < FFI::Struct + layout :luid, LUID, + :attributes, :uint + end + + # http://msdn.microsoft.com/en-us/library/windows/desktop/aa379630(v=vs.85).aspx + # typedef struct _TOKEN_PRIVILEGES { + # DWORD PrivilegeCount; + # LUID_AND_ATTRIBUTES Privileges[ANYSIZE_ARRAY]; + # } TOKEN_PRIVILEGES, *PTOKEN_PRIVILEGES; + class Token_Privileges < FFI::Struct + layout :privilege_count, :uint, + :privileges, [LUID_And_Attributes, 1] # placeholder for offset + end + + # http://msdn.microsoft.com/en-us/library/windows/desktop/aa446671(v=vs.85).aspx + # BOOL WINAPI GetTokenInformation( + # _In_ HANDLE TokenHandle, + # _In_ TOKEN_INFORMATION_CLASS TokenInformationClass, + # _Out_opt_ LPVOID TokenInformation, + # _In_ DWORD TokenInformationLength, + # _Out_ PDWORD ReturnLength + # ); + attach_function :get_token_information, :GetTokenInformation, + [:uint, Token_Information, :pointer, :uint, :pointer ], :bool + end + def execute(command, arguments, stdin, stdout, stderr) Process.create( :command_line => command, :startup_info => {:stdin => stdin, :stdout => stdout, :stderr => stderr}, :close_handles => false ) end @@ -33,4 +144,92 @@ module Puppet::Util::Windows::Process exit_status end module_function :wait_process + + def get_current_process + # this pseudo-handle does not require closing per MSDN docs + API.get_current_process + end + module_function :get_current_process + + def open_process_token(handle, desired_access) + token_handle_ptr = FFI::MemoryPointer.new(:uint, 1) + result = API.open_process_token(handle, desired_access, token_handle_ptr) + if !result + raise Puppet::Util::Windows::Error.new( + "OpenProcessToken(#{handle}, #{desired_access.to_s(8)}, #{token_handle_ptr})") + end + + begin + yield token_handle = token_handle_ptr.read_uint + ensure + API.close_handle(token_handle) + end + end + module_function :open_process_token + + def lookup_privilege_value(name, system_name = '') + luid = FFI::MemoryPointer.new(API::LUID.size) + result = API.lookup_privilege_value(WideString.new(system_name), + WideString.new(name.to_s), luid) + + return API::LUID.new(luid) if result + raise Puppet::Util::Windows::Error.new( + "LookupPrivilegeValue(#{system_name}, #{name}, #{luid})") + end + module_function :lookup_privilege_value + + def get_token_information(token_handle, token_information) + # to determine buffer size + return_length_ptr = FFI::MemoryPointer.new(:uint, 1) + result = API.get_token_information(token_handle, token_information, nil, 0, return_length_ptr) + return_length = return_length_ptr.read_uint + + if return_length <= 0 + raise Puppet::Util::Windows::Error.new( + "GetTokenInformation(#{token_handle}, #{token_information}, nil, 0, #{return_length_ptr})") + end + + # re-call API with properly sized buffer for all results + token_information_buf = FFI::MemoryPointer.new(return_length) + result = API.get_token_information(token_handle, token_information, + token_information_buf, return_length, return_length_ptr) + + if !result + raise Puppet::Util::Windows::Error.new( + "GetTokenInformation(#{token_handle}, #{token_information}, #{token_information_buf}, " + + "#{return_length}, #{return_length_ptr})") + end + + raw_privileges = API::Token_Privileges.new(token_information_buf) + privileges = { :count => raw_privileges[:privilege_count], :privileges => [] } + + offset = token_information_buf + API::Token_Privileges.offset_of(:privileges) + privilege_ptr = FFI::Pointer.new(API::LUID_And_Attributes, offset) + + # extract each instance of LUID_And_Attributes + 0.upto(privileges[:count] - 1) do |i| + privileges[:privileges] << API::LUID_And_Attributes.new(privilege_ptr[i]) + end + + privileges + end + module_function :get_token_information + + TOKEN_ALL_ACCESS = 0xF01FF + ERROR_NO_SUCH_PRIVILEGE = 1313 + def process_privilege_symlink? + handle = get_current_process + open_process_token(handle, TOKEN_ALL_ACCESS) do |token_handle| + luid = lookup_privilege_value('SeCreateSymbolicLinkPrivilege') + token_info = get_token_information(token_handle, :token_privileges) + token_info[:privileges].any? { |p| p[:luid].values == luid.values } + end + rescue Puppet::Util::Windows::Error => e + if e.code == ERROR_NO_SUCH_PRIVILEGE + false # pre-Vista + else + raise e + end + end + module_function :process_privilege_symlink? end diff --git a/lib/puppet/util/windows/root_certs.rb b/lib/puppet/util/windows/root_certs.rb index 4eae0a540..7988ab832 100644 --- a/lib/puppet/util/windows/root_certs.rb +++ b/lib/puppet/util/windows/root_certs.rb @@ -1,17 +1,16 @@ require 'puppet/util/windows' require 'openssl' -require 'Win32API' -require 'windows/msvcrt/buffer' +require 'ffi' # Represents a collection of trusted root certificates. # # @api public class Puppet::Util::Windows::RootCerts include Enumerable + extend FFI::Library - CertOpenSystemStore = Win32API.new('crypt32', 'CertOpenSystemStore', ['L','P'], 'L') - CertEnumCertificatesInStore = Win32API.new('crypt32', 'CertEnumCertificatesInStore', ['L', 'L'], 'L') - CertCloseStore = Win32API.new('crypt32', 'CertCloseStore', ['L', 'L'], 'B') + typedef :ulong, :dword + typedef :uintptr_t, :handle def initialize(roots) @roots = roots @@ -24,10 +23,6 @@ class Puppet::Util::Windows::RootCerts @roots.each {|cert| yield cert} end - class << self - include Windows::MSVCRT::Buffer - end - # Returns a new instance. # @return [Puppet::Util::Windows::RootCerts] object constructed from current root certificates def self.instance @@ -43,34 +38,12 @@ class Puppet::Util::Windows::RootCerts # This is based on a patch submitted to openssl: # http://www.mail-archive.com/openssl-dev@openssl.org/msg26958.html - context = 0 - store = CertOpenSystemStore.call(0, "ROOT") + ptr = FFI::Pointer::NULL + store = CertOpenSystemStoreA(nil, "ROOT") begin - while (context = CertEnumCertificatesInStore.call(store, context) and context != 0) - # 466 typedef struct _CERT_CONTEXT { - # 467 DWORD dwCertEncodingType; - # 468 BYTE *pbCertEncoded; - # 469 DWORD cbCertEncoded; - # 470 PCERT_INFO pCertInfo; - # 471 HCERTSTORE hCertStore; - # 472 } CERT_CONTEXT, *PCERT_CONTEXT; - - # buffer to hold struct above - ctx_buf = 0.chr * 5 * 8 - - # copy from win to ruby - memcpy(ctx_buf, context, ctx_buf.size) - - # unpack structure - arr = ctx_buf.unpack('LLLLL') - - # create buf of length cbCertEncoded - cert_buf = 0.chr * arr[2] - - # copy pbCertEncoded from win to ruby - memcpy(cert_buf, arr[1], cert_buf.length) - - # create a cert + while (ptr = CertEnumCertificatesInStore(store, ptr)) and not ptr.null? + context = CERT_CONTEXT.new(ptr) + cert_buf = context[:pbCertEncoded].read_bytes(context[:cbCertEncoded]) begin certs << OpenSSL::X509::Certificate.new(cert_buf) rescue => detail @@ -78,9 +51,51 @@ class Puppet::Util::Windows::RootCerts end end ensure - CertCloseStore.call(store, 0) + CertCloseStore(store, 0) end certs end + + private + + # typedef ULONG_PTR HCRYPTPROV_LEGACY; + # typedef void *HCERTSTORE; + + class CERT_CONTEXT < FFI::Struct + layout( + :dwCertEncodingType, :dword, + :pbCertEncoded, :pointer, + :cbCertEncoded, :dword, + :pCertInfo, :pointer, + :hCertStore, :handle + ) + end + + # HCERTSTORE + # WINAPI + # CertOpenSystemStoreA( + # __in_opt HCRYPTPROV_LEGACY hProv, + # __in LPCSTR szSubsystemProtocol + # ); + ffi_lib :crypt32 + attach_function :CertOpenSystemStoreA, [:pointer, :string], :handle + + # PCCERT_CONTEXT + # WINAPI + # CertEnumCertificatesInStore( + # __in HCERTSTORE hCertStore, + # __in_opt PCCERT_CONTEXT pPrevCertContext + # ); + ffi_lib :crypt32 + attach_function :CertEnumCertificatesInStore, [:handle, :pointer], :pointer + + # BOOL + # WINAPI + # CertCloseStore( + # __in_opt HCERTSTORE hCertStore, + # __in DWORD dwFlags + # ); + ffi_lib :crypt32 + attach_function :CertCloseStore, [:handle, :dword], :bool end diff --git a/lib/puppet/util/windows/security.rb b/lib/puppet/util/windows/security.rb index 92df1b746..dfd7ea931 100644 --- a/lib/puppet/util/windows/security.rb +++ b/lib/puppet/util/windows/security.rb @@ -53,6 +53,8 @@ # enables Puppet to detect when file/dirs are out-of-sync, # especially those that Puppet did not create, but is attempting # to manage. +# * A special case of this is S_ISYSTEM_MISSING, which is set when the +# SYSTEM permissions are *not* present on the DACL. # * On Unix, the owner and group can be modified without changing the # mode. But on Windows, an access control entry specifies which SID # it applies to. As a result, the set_owner and set_group methods @@ -61,6 +63,7 @@ require 'puppet/util/windows' require 'pathname' +require 'ffi' require 'win32/security' @@ -100,11 +103,13 @@ module Puppet::Util::Windows::Security S_IRWXO = 0000007 S_ISVTX = 0001000 S_IEXTRA = 02000000 # represents an extra ace + S_ISYSTEM_MISSING = 04000000 # constants that are missing from Windows::Security PROTECTED_DACL_SECURITY_INFORMATION = 0x80000000 UNPROTECTED_DACL_SECURITY_INFORMATION = 0x20000000 NO_INHERITANCE = 0x0 + SE_DACL_PROTECTED = 0x1000 # Set the owner of the object referenced by +path+ to the specified # +owner_sid+. The owner sid should be of the form "S-1-5-32-544" @@ -112,9 +117,12 @@ module Puppet::Util::Windows::Security # SE_RESTORE_NAME privilege in their process token can overwrite the # object's owner to something other than the current user. def set_owner(owner_sid, path) - old_sid = get_owner(path) + sd = get_security_descriptor(path) - change_sid(old_sid, owner_sid, OWNER_SECURITY_INFORMATION, path) + if owner_sid != sd.owner + sd.owner = owner_sid + set_security_descriptor(path, sd) + end end # Get the owner of the object referenced by +path+. The returned @@ -125,7 +133,7 @@ module Puppet::Util::Windows::Security def get_owner(path) return unless supports_acl?(path) - get_sid(OWNER_SECURITY_INFORMATION, path) + get_security_descriptor(path).owner end # Set the owner of the object referenced by +path+ to the specified @@ -134,9 +142,12 @@ module Puppet::Util::Windows::Security # access to the object can change the group (regardless of whether # the current user belongs to that group or not). def set_group(group_sid, path) - old_sid = get_group(path) + sd = get_security_descriptor(path) - change_sid(old_sid, group_sid, GROUP_SECURITY_INFORMATION, path) + if group_sid != sd.group + sd.group = group_sid + set_security_descriptor(path, sd) + end end # Get the group of the object referenced by +path+. The returned @@ -147,7 +158,7 @@ module Puppet::Util::Windows::Security def get_group(path) return unless supports_acl?(path) - get_sid(GROUP_SECURITY_INFORMATION, path) + get_security_descriptor(path).group end def supports_acl?(path) @@ -163,31 +174,6 @@ module Puppet::Util::Windows::Security (flags.unpack('L')[0] & Windows::File::FILE_PERSISTENT_ACLS) != 0 end - def change_sid(old_sid, new_sid, info, path) - if old_sid != new_sid - mode = get_mode(path) - - string_to_sid_ptr(new_sid) do |psid| - with_privilege(SE_RESTORE_NAME) do - open_file(path, WRITE_OWNER) do |handle| - set_security_info(handle, info, psid) - end - end - end - - # rebuild dacl now that sid has changed - set_mode(mode, path) - end - end - - def get_sid(info, path) - with_privilege(SE_BACKUP_NAME) do - open_file(path, READ_CONTROL) do |handle| - get_security_info(handle, info) - end - end - end - def get_attributes(path) attributes = GetFileAttributes(path) @@ -222,6 +208,10 @@ module Puppet::Util::Windows::Security (FILE_GENERIC_EXECUTE & ~FILE_READ_ATTRIBUTES) => S_IXOTH } + def get_aces_for_path_by_sid(path, sid) + get_security_descriptor(path).dacl.select { |ace| ace.sid == sid } + end + # Get the mode of the object referenced by +path+. The returned # integer value represents the POSIX-style read, write, and execute # modes for the user, group, and other classes, e.g. 0640. Any user @@ -231,58 +221,61 @@ module Puppet::Util::Windows::Security def get_mode(path) return unless supports_acl?(path) - owner_sid = get_owner(path) - group_sid = get_group(path) well_known_world_sid = Win32::Security::SID::Everyone well_known_nobody_sid = Win32::Security::SID::Nobody + well_known_system_sid = Win32::Security::SID::LocalSystem - with_privilege(SE_BACKUP_NAME) do - open_file(path, READ_CONTROL) do |handle| - mode = 0 - - get_dacl(handle).each do |ace| - case ace[:sid] - when owner_sid - MASK_TO_MODE.each_pair do |k,v| - if (ace[:mask] & k) == k - mode |= (v << 6) - end - end - when group_sid - MASK_TO_MODE.each_pair do |k,v| - if (ace[:mask] & k) == k - mode |= (v << 3) - end - end - when well_known_world_sid - MASK_TO_MODE.each_pair do |k,v| - if (ace[:mask] & k) == k - mode |= (v << 6) | (v << 3) | v - end - end - if File.directory?(path) and (ace[:mask] & (FILE_WRITE_DATA | FILE_EXECUTE | FILE_DELETE_CHILD)) == (FILE_WRITE_DATA | FILE_EXECUTE) - mode |= S_ISVTX; - end - when well_known_nobody_sid - if (ace[:mask] & FILE_APPEND_DATA).nonzero? - mode |= S_ISVTX - end - else - #puts "Warning, unable to map SID into POSIX mode: #{ace[:sid]}" - mode |= S_IEXTRA - end + mode = S_ISYSTEM_MISSING - # if owner and group the same, then user and group modes are the OR of both - if owner_sid == group_sid - mode |= ((mode & S_IRWXG) << 3) | ((mode & S_IRWXU) >> 3) - #puts "owner: #{group_sid}, 0x#{ace[:mask].to_s(16)}, #{mode.to_s(8)}" + sd = get_security_descriptor(path) + sd.dacl.each do |ace| + next if ace.inherit_only? + + case ace.sid + when sd.owner + MASK_TO_MODE.each_pair do |k,v| + if (ace.mask & k) == k + mode |= (v << 6) + end + end + when sd.group + MASK_TO_MODE.each_pair do |k,v| + if (ace.mask & k) == k + mode |= (v << 3) + end + end + when well_known_world_sid + MASK_TO_MODE.each_pair do |k,v| + if (ace.mask & k) == k + mode |= (v << 6) | (v << 3) | v end end + if File.directory?(path) && (ace.mask & (FILE_WRITE_DATA | FILE_EXECUTE | FILE_DELETE_CHILD)) == (FILE_WRITE_DATA | FILE_EXECUTE) + mode |= S_ISVTX; + end + when well_known_nobody_sid + if (ace.mask & FILE_APPEND_DATA).nonzero? + mode |= S_ISVTX + end + when well_known_system_sid + else + #puts "Warning, unable to map SID into POSIX mode: #{ace.sid}" + mode |= S_IEXTRA + end - #puts "get_mode: #{mode.to_s(8)}" - mode + if ace.sid == well_known_system_sid + mode &= ~S_ISYSTEM_MISSING + end + + # if owner and group the same, then user and group modes are the OR of both + if sd.owner == sd.group + mode |= ((mode & S_IRWXG) << 3) | ((mode & S_IRWXU) >> 3) + #puts "owner: #{sd.group}, 0x#{ace.mask.to_s(16)}, #{mode.to_s(8)}" end end + + #puts "get_mode: #{mode.to_s(8)}" + mode end MODE_TO_MASK = { @@ -305,15 +298,16 @@ module Puppet::Util::Windows::Security # privileges in their process token can change the mode for objects # that they do not have read and write access to. def set_mode(mode, path, protected = true) - owner_sid = get_owner(path) - group_sid = get_group(path) + sd = get_security_descriptor(path) well_known_world_sid = Win32::Security::SID::Everyone well_known_nobody_sid = Win32::Security::SID::Nobody + well_known_system_sid = Win32::Security::SID::LocalSystem owner_allow = STANDARD_RIGHTS_ALL | FILE_READ_ATTRIBUTES | FILE_WRITE_ATTRIBUTES group_allow = STANDARD_RIGHTS_READ | FILE_READ_ATTRIBUTES | SYNCHRONIZE other_allow = STANDARD_RIGHTS_READ | FILE_READ_ATTRIBUTES | SYNCHRONIZE nobody_allow = 0 + system_allow = 0 MODE_TO_MASK.each do |k,v| if ((mode >> 6) & k) == k @@ -331,22 +325,29 @@ module Puppet::Util::Windows::Security nobody_allow |= FILE_APPEND_DATA; end + # caller is NOT managing SYSTEM by using group or owner, so set to FULL + if ! [sd.owner, sd.group].include? well_known_system_sid + # we don't check S_ISYSTEM_MISSING bit, but automatically carry over existing SYSTEM perms + # by default set SYSTEM perms to full + system_allow = FILE_ALL_ACCESS + end + isdir = File.directory?(path) if isdir if (mode & (S_IWUSR | S_IXUSR)) == (S_IWUSR | S_IXUSR) owner_allow |= FILE_DELETE_CHILD end - if (mode & (S_IWGRP | S_IXGRP)) == (S_IWGRP | S_IXGRP) and (mode & S_ISVTX) == 0 + if (mode & (S_IWGRP | S_IXGRP)) == (S_IWGRP | S_IXGRP) && (mode & S_ISVTX) == 0 group_allow |= FILE_DELETE_CHILD end - if (mode & (S_IWOTH | S_IXOTH)) == (S_IWOTH | S_IXOTH) and (mode & S_ISVTX) == 0 + if (mode & (S_IWOTH | S_IXOTH)) == (S_IWOTH | S_IXOTH) && (mode & S_ISVTX) == 0 other_allow |= FILE_DELETE_CHILD end end # if owner and group the same, then map group permissions to the one owner ACE - isownergroup = owner_sid == group_sid + isownergroup = sd.owner == sd.group if isownergroup owner_allow |= group_allow end @@ -357,64 +358,37 @@ module Puppet::Util::Windows::Security remove_attributes(path, FILE_ATTRIBUTE_READONLY) end - set_acl(path, protected) do |acl| - #puts "ace: owner #{owner_sid}, mask 0x#{owner_allow.to_s(16)}" - add_access_allowed_ace(acl, owner_allow, owner_sid) - - unless isownergroup - #puts "ace: group #{group_sid}, mask 0x#{group_allow.to_s(16)}" - add_access_allowed_ace(acl, group_allow, group_sid) - end - - #puts "ace: other #{well_known_world_sid}, mask 0x#{other_allow.to_s(16)}" - add_access_allowed_ace(acl, other_allow, well_known_world_sid) + dacl = Puppet::Util::Windows::AccessControlList.new + dacl.allow(sd.owner, owner_allow) + unless isownergroup + dacl.allow(sd.group, group_allow) + end + dacl.allow(well_known_world_sid, other_allow) + dacl.allow(well_known_nobody_sid, nobody_allow) - #puts "ace: nobody #{well_known_nobody_sid}, mask 0x#{nobody_allow.to_s(16)}" - add_access_allowed_ace(acl, nobody_allow, well_known_nobody_sid) + # TODO: system should be first? + dacl.allow(well_known_system_sid, system_allow) - # add inherit-only aces for child dirs and files that are created within the dir - if isdir - inherit = INHERIT_ONLY_ACE | CONTAINER_INHERIT_ACE - add_access_allowed_ace(acl, owner_allow, Win32::Security::SID::CreatorOwner, inherit) - add_access_allowed_ace(acl, group_allow, Win32::Security::SID::CreatorGroup, inherit) + # add inherit-only aces for child dirs and files that are created within the dir + if isdir + inherit = INHERIT_ONLY_ACE | CONTAINER_INHERIT_ACE + dacl.allow(Win32::Security::SID::CreatorOwner, owner_allow, inherit) + dacl.allow(Win32::Security::SID::CreatorGroup, group_allow, inherit) - inherit = INHERIT_ONLY_ACE | OBJECT_INHERIT_ACE - add_access_allowed_ace(acl, owner_allow & ~FILE_EXECUTE, Win32::Security::SID::CreatorOwner, inherit) - add_access_allowed_ace(acl, group_allow & ~FILE_EXECUTE, Win32::Security::SID::CreatorGroup, inherit) - end + inherit = INHERIT_ONLY_ACE | OBJECT_INHERIT_ACE + dacl.allow(Win32::Security::SID::CreatorOwner, owner_allow & ~FILE_EXECUTE, inherit) + dacl.allow(Win32::Security::SID::CreatorGroup, group_allow & ~FILE_EXECUTE, inherit) end + new_sd = Puppet::Util::Windows::SecurityDescriptor.new(sd.owner, sd.group, dacl, protected) + set_security_descriptor(path, new_sd) + nil end - # setting DACL requires both READ_CONTROL and WRITE_DACL access rights, - # and their respective privileges, SE_BACKUP_NAME and SE_RESTORE_NAME. - def set_acl(path, protected = true) - with_privilege(SE_BACKUP_NAME) do - with_privilege(SE_RESTORE_NAME) do - open_file(path, READ_CONTROL | WRITE_DAC) do |handle| - acl = 0.chr * 1024 # This can be increased later as needed - - unless InitializeAcl(acl, acl.size, ACL_REVISION) - raise Puppet::Util::Windows::Error.new("Failed to initialize ACL") - end - - raise Puppet::Util::Windows::Error.new("Invalid DACL") unless IsValidAcl(acl) - - yield acl - - # protected means the object does not inherit aces from its parent - info = DACL_SECURITY_INFORMATION - info |= protected ? PROTECTED_DACL_SECURITY_INFORMATION : UNPROTECTED_DACL_SECURITY_INFORMATION - - # set the DACL - set_security_info(handle, info, acl) - end - end - end - end + def add_access_allowed_ace(acl, mask, sid, inherit = nil) + inherit ||= NO_INHERITANCE - def add_access_allowed_ace(acl, mask, sid, inherit = NO_INHERITANCE) string_to_sid_ptr(sid) do |sid_ptr| raise Puppet::Util::Windows::Error.new("Invalid SID") unless IsValidSid(sid_ptr) @@ -434,126 +408,68 @@ module Puppet::Util::Windows::Security end end - def get_dacl(handle) - get_dacl_ptr(handle) do |dacl_ptr| - # REMIND: need to handle NULL DACL - raise Puppet::Util::Windows::Error.new("Invalid DACL") unless IsValidAcl(dacl_ptr) - - # ACL structure, size and count are the important parts. The - # size includes both the ACL structure and all the ACEs. + def parse_dacl(dacl_ptr) + # REMIND: need to handle NULL DACL + raise Puppet::Util::Windows::Error.new("Invalid DACL") unless IsValidAcl(dacl_ptr) + + # ACL structure, size and count are the important parts. The + # size includes both the ACL structure and all the ACEs. + # + # BYTE AclRevision + # BYTE Padding1 + # WORD AclSize + # WORD AceCount + # WORD Padding2 + acl_buf = 0.chr * 8 + memcpy(acl_buf, dacl_ptr, acl_buf.size) + ace_count = acl_buf.unpack('CCSSS')[3] + + dacl = Puppet::Util::Windows::AccessControlList.new + + # deny all + return dacl if ace_count == 0 + + 0.upto(ace_count - 1) do |i| + ace_ptr = [0].pack('L') + + next unless GetAce(dacl_ptr, i, ace_ptr) + + # ACE structures vary depending on the type. All structures + # begin with an ACE header, which specifies the type, flags + # and size of what follows. We are only concerned with + # ACCESS_ALLOWED_ACE and ACCESS_DENIED_ACEs, which have the + # same structure: # - # BYTE AclRevision - # BYTE Padding1 - # WORD AclSize - # WORD AceCount - # WORD Padding2 - acl_buf = 0.chr * 8 - memcpy(acl_buf, dacl_ptr, acl_buf.size) - ace_count = acl_buf.unpack('CCSSS')[3] - - dacl = [] - - # deny all - return dacl if ace_count == 0 - - 0.upto(ace_count - 1) do |i| - ace_ptr = [0].pack('L') - - next unless GetAce(dacl_ptr, i, ace_ptr) - - # ACE structures vary depending on the type. All structures - # begin with an ACE header, which specifies the type, flags - # and size of what follows. We are only concerned with - # ACCESS_ALLOWED_ACE and ACCESS_DENIED_ACEs, which have the - # same structure: - # - # BYTE C AceType - # BYTE C AceFlags - # WORD S AceSize - # DWORD L ACCESS_MASK - # DWORD L Sid - # .. ... - # DWORD L Sid - - ace_buf = 0.chr * 8 - memcpy(ace_buf, ace_ptr.unpack('L')[0], ace_buf.size) - - ace_type, ace_flags, size, mask = ace_buf.unpack('CCSL') - - # skip aces that only serve to propagate inheritance - next if (ace_flags & INHERIT_ONLY_ACE).nonzero? - - case ace_type - when ACCESS_ALLOWED_ACE_TYPE - sid_ptr = ace_ptr.unpack('L')[0] + 8 # address of ace_ptr->SidStart - raise Puppet::Util::Windows::Error.new("Failed to read DACL, invalid SID") unless IsValidSid(sid_ptr) - sid = sid_ptr_to_string(sid_ptr) - dacl << {:sid => sid, :type => ace_type, :mask => mask} - else - Puppet.warning "Unsupported access control entry type: 0x#{ace_type.to_s(16)}" - end + # BYTE C AceType + # BYTE C AceFlags + # WORD S AceSize + # DWORD L ACCESS_MASK + # DWORD L Sid + # .. ... + # DWORD L Sid + + ace_buf = 0.chr * 8 + memcpy(ace_buf, ace_ptr.unpack('L')[0], ace_buf.size) + + ace_type, ace_flags, size, mask = ace_buf.unpack('CCSL') + + case ace_type + when ACCESS_ALLOWED_ACE_TYPE + sid_ptr = ace_ptr.unpack('L')[0] + 8 # address of ace_ptr->SidStart + raise Puppet::Util::Windows::Error.new("Failed to read DACL, invalid SID") unless IsValidSid(sid_ptr) + sid = sid_ptr_to_string(sid_ptr) + dacl.allow(sid, mask, ace_flags) + when ACCESS_DENIED_ACE_TYPE + sid_ptr = ace_ptr.unpack('L')[0] + 8 # address of ace_ptr->SidStart + raise Puppet::Util::Windows::Error.new("Failed to read DACL, invalid SID") unless IsValidSid(sid_ptr) + sid = sid_ptr_to_string(sid_ptr) + dacl.deny(sid, mask, ace_flags) + else + Puppet.warning "Unsupported access control entry type: 0x#{ace_type.to_s(16)}" end - - dacl end - end - def get_dacl_ptr(handle) - dacl = [0].pack('L') - sd = [0].pack('L') - - rv = GetSecurityInfo( - handle, - SE_FILE_OBJECT, - DACL_SECURITY_INFORMATION, - nil, - nil, - dacl, #dacl - nil, #sacl - sd) #sec desc - raise Puppet::Util::Windows::Error.new("Failed to get DACL") unless rv == ERROR_SUCCESS - begin - yield dacl.unpack('L')[0] - ensure - LocalFree(sd.unpack('L')[0]) - end - end - - # Set the security info on the specified handle. - def set_security_info(handle, info, ptr) - rv = SetSecurityInfo( - handle, - SE_FILE_OBJECT, - info, - (info & OWNER_SECURITY_INFORMATION) == OWNER_SECURITY_INFORMATION ? ptr : nil, - (info & GROUP_SECURITY_INFORMATION) == GROUP_SECURITY_INFORMATION ? ptr : nil, - (info & DACL_SECURITY_INFORMATION) == DACL_SECURITY_INFORMATION ? ptr : nil, - nil) - raise Puppet::Util::Windows::Error.new("Failed to set security information") unless rv == ERROR_SUCCESS - end - - # Get the SID string, e.g. "S-1-5-32-544", for the specified handle - # and type of information (owner, group). - def get_security_info(handle, info) - sid = [0].pack('L') - sd = [0].pack('L') - - rv = GetSecurityInfo( - handle, - SE_FILE_OBJECT, - info, # security info - info == OWNER_SECURITY_INFORMATION ? sid : nil, - info == GROUP_SECURITY_INFORMATION ? sid : nil, - nil, #dacl - nil, #sacl - sd) #sec desc - raise Puppet::Util::Windows::Error.new("Failed to get security information") unless rv == ERROR_SUCCESS - - begin - return sid_ptr_to_string(sid.unpack('L')[0]) - ensure - LocalFree(sd.unpack('L')[0]) - end + dacl end # Open an existing file with the specified access mode, and execute a @@ -565,7 +481,7 @@ module Puppet::Util::Windows::Security FILE_SHARE_READ | FILE_SHARE_WRITE, 0, # security_attributes OPEN_EXISTING, - FILE_FLAG_BACKUP_SEMANTICS, + FILE_FLAG_OPEN_REPARSE_POINT | FILE_FLAG_BACKUP_SEMANTICS, 0) # template raise Puppet::Util::Windows::Error.new("Failed to open '#{path}'") if handle == INVALID_HANDLE_VALUE begin @@ -620,4 +536,114 @@ module Puppet::Util::Windows::Security CloseHandle(token) end end + + def get_security_descriptor(path) + sd = nil + + with_privilege(SE_BACKUP_NAME) do + open_file(path, READ_CONTROL) do |handle| + owner_sid = [0].pack('L') + group_sid = [0].pack('L') + dacl = [0].pack('L') + ppsd = [0].pack('L') + + rv = GetSecurityInfo( + handle, + SE_FILE_OBJECT, + OWNER_SECURITY_INFORMATION | GROUP_SECURITY_INFORMATION | DACL_SECURITY_INFORMATION, + owner_sid, + group_sid, + dacl, + nil, #sacl + ppsd) #sec desc + raise Puppet::Util::Windows::Error.new("Failed to get security information") unless rv == ERROR_SUCCESS + + begin + owner = sid_ptr_to_string(owner_sid.unpack('L')[0]) + group = sid_ptr_to_string(group_sid.unpack('L')[0]) + + control = FFI::MemoryPointer.new(:uint16, 1) + revision = FFI::MemoryPointer.new(:uint32, 1) + ffsd = FFI::Pointer.new(ppsd.unpack('L')[0]) + + if ! API.get_security_descriptor_control(ffsd, control, revision) + raise Puppet::Util::Windows::Error.new("Failed to get security descriptor control") + end + + protect = (control.read_uint16 & SE_DACL_PROTECTED) == SE_DACL_PROTECTED + + dacl = parse_dacl(dacl.unpack('L')[0]) + sd = Puppet::Util::Windows::SecurityDescriptor.new(owner, group, dacl, protect) + ensure + LocalFree(ppsd.unpack('L')[0]) + end + end + end + + sd + end + + # setting DACL requires both READ_CONTROL and WRITE_DACL access rights, + # and their respective privileges, SE_BACKUP_NAME and SE_RESTORE_NAME. + def set_security_descriptor(path, sd) + # REMIND: FFI + acl = 0.chr * 1024 # This can be increased later as neede + unless InitializeAcl(acl, acl.size, ACL_REVISION) + raise Puppet::Util::Windows::Error.new("Failed to initialize ACL") + end + + raise Puppet::Util::Windows::Error.new("Invalid DACL") unless IsValidAcl(acl) + + with_privilege(SE_BACKUP_NAME) do + with_privilege(SE_RESTORE_NAME) do + open_file(path, READ_CONTROL | WRITE_DAC | WRITE_OWNER) do |handle| + string_to_sid_ptr(sd.owner) do |ownersid| + string_to_sid_ptr(sd.group) do |groupsid| + sd.dacl.each do |ace| + case ace.type + when ACCESS_ALLOWED_ACE_TYPE + #puts "ace: allow, sid #{sid_to_name(ace.sid)}, mask 0x#{ace.mask.to_s(16)}" + add_access_allowed_ace(acl, ace.mask, ace.sid, ace.flags) + when ACCESS_DENIED_ACE_TYPE + #puts "ace: deny, sid #{sid_to_name(ace.sid)}, mask 0x#{ace.mask.to_s(16)}" + add_access_denied_ace(acl, ace.mask, ace.sid) + else + raise "We should never get here" + # TODO: this should have been a warning in an earlier commit + end + end + + # protected means the object does not inherit aces from its parent + flags = OWNER_SECURITY_INFORMATION | GROUP_SECURITY_INFORMATION | DACL_SECURITY_INFORMATION + flags |= sd.protect ? PROTECTED_DACL_SECURITY_INFORMATION : UNPROTECTED_DACL_SECURITY_INFORMATION + + rv = SetSecurityInfo(handle, + SE_FILE_OBJECT, + flags, + ownersid, + groupsid, + acl, + nil) + raise Puppet::Util::Windows::Error.new("Failed to set security information") unless rv == ERROR_SUCCESS + end + end + end + end + end + end + + module API + extend FFI::Library + ffi_lib 'kernel32' + ffi_convention :stdcall + + # typedef WORD SECURITY_DESCRIPTOR_CONTROL, *PSECURITY_DESCRIPTOR_CONTROL; + # BOOL WINAPI GetSecurityDescriptorControl( + # _In_ PSECURITY_DESCRIPTOR pSecurityDescriptor, + # _Out_ PSECURITY_DESCRIPTOR_CONTROL pControl, + # _Out_ LPDWORD lpdwRevision + # ); + ffi_lib :advapi32 + attach_function :get_security_descriptor_control, :GetSecurityDescriptorControl, [:pointer, :pointer, :pointer], :bool + end end diff --git a/lib/puppet/util/windows/security_descriptor.rb b/lib/puppet/util/windows/security_descriptor.rb new file mode 100644 index 000000000..9c95cee6d --- /dev/null +++ b/lib/puppet/util/windows/security_descriptor.rb @@ -0,0 +1,62 @@ +# Windows Security Descriptor +# +# Represents a security descriptor that can be applied to any Windows securable +# object, e.g. file, registry key, service, etc. It consists of an owner, group, +# flags, DACL, and SACL. The SACL is not currently supported, though it has the +# same layout as a DACL. +# +# @see http://msdn.microsoft.com/en-us/library/windows/desktop/aa379563(v=vs.85).aspx +# @api private +class Puppet::Util::Windows::SecurityDescriptor + require 'puppet/util/windows/security' + include Puppet::Util::Windows::SID + + attr_reader :owner, :group, :dacl + attr_accessor :protect + + # Construct a security descriptor + # + # @param owner [String] The SID of the owner, e.g. 'S-1-5-18' + # @param group [String] The SID of the group + # @param dacl [AccessControlList] The ACL specifying the rights granted to + # each user for accessing the object that the security descriptor refers to. + # @param protect [Boolean] If true, then inheritable access control + # entries will be blocked, and not applied to the object. + def initialize(owner, group, dacl, protect = false) + @owner = owner + @group = group + @dacl = dacl + @protect = protect + end + + # Set the owner. Non-inherited access control entries assigned to the + # current owner will be assigned to the new owner. + # + # @param new_owner [String] The SID of the new owner, e.g. 'S-1-5-18' + def owner=(new_owner) + if @owner != new_owner + @dacl.reassign!(@owner, new_owner) + @owner = new_owner + end + end + + # Set the group. Non-inherited access control entries assigned to the + # current group will be assigned to the new group. + # + # @param new_group [String] The SID of the new group, e.g. 'S-1-0-0' + def group=(new_group) + if @group != new_group + @dacl.reassign!(@group, new_group) + @group = new_group + end + end + + def inspect + str = sid_to_name(owner) + str << "\n" + str << sid_to_name(group) + str << "\n" + str << @dacl.inspect + str + end +end diff --git a/lib/puppet/util/windows/sid.rb b/lib/puppet/util/windows/sid.rb index cd321eacb..90b48d933 100644 --- a/lib/puppet/util/windows/sid.rb +++ b/lib/puppet/util/windows/sid.rb @@ -20,17 +20,39 @@ module Puppet::Util::Windows # 'BUILTIN\Administrators', or 'S-1-5-32-544', and will return the # SID. Returns nil if the account doesn't exist. def name_to_sid(name) + sid = name_to_sid_object(name) + + sid ? sid.to_s : nil + end + + # Convert an account name, e.g. 'Administrators' into a SID object, + # e.g. 'S-1-5-32-544'. The name can be specified as 'Administrators', + # 'BUILTIN\Administrators', or 'S-1-5-32-544', and will return the + # SID object. Returns nil if the account doesn't exist. + def name_to_sid_object(name) # Apparently, we accept a symbol.. - name = name.to_s if name + name = name.to_s.strip if name - # if it's in SID string form, return it, otherwise, lookup sid - is_sid = Win32::Security::SID.string_to_sid(name) rescue nil + # if it's in SID string form, convert to user + parsed_sid = Win32::Security::SID.string_to_sid(name) rescue nil - is_sid ? name : Win32::Security::SID.new(name).to_s + parsed_sid ? Win32::Security::SID.new(parsed_sid) : Win32::Security::SID.new(name) rescue nil end + # Converts an octet string array of bytes to a SID object, + # e.g. [1, 1, 0, 0, 0, 0, 0, 5, 18, 0, 0, 0] is the representation for + # S-1-5-18, the local 'SYSTEM' account. + # Raises an Error for nil or non-array input. + def octet_string_to_sid_object(bytes) + if !bytes || !bytes.respond_to?('pack') || bytes.empty? + raise Puppet::Util::Windows::Error.new("Octet string must be an array of bytes") + end + + Win32::Security::SID.new(bytes.pack('C*')) + end + # Convert a SID string, e.g. "S-1-5-32-544" to a name, # e.g. 'BUILTIN\Administrators'. Returns nil if an account # for that SID does not exist. diff --git a/lib/puppet/util/yaml.rb b/lib/puppet/util/yaml.rb index d0c37ae77..246080843 100644 --- a/lib/puppet/util/yaml.rb +++ b/lib/puppet/util/yaml.rb @@ -9,8 +9,9 @@ module Puppet::Util::Yaml class YamlLoadError < Puppet::Error; end - def self.load_file(filename) - YAML.load_file(filename) + def self.load_file(filename, default_value = false) + yaml = YAML.load_file(filename) + yaml || default_value rescue *YamlLoadExceptions => detail raise YamlLoadError.new(detail.message, detail) end diff --git a/lib/puppet/version.rb b/lib/puppet/version.rb index 9f9f738be..b416a864b 100644 --- a/lib/puppet/version.rb +++ b/lib/puppet/version.rb @@ -7,7 +7,7 @@ module Puppet - PUPPETVERSION = '3.3.1' + PUPPETVERSION = '3.4.0' ## # version is a public API method intended to always provide a fast and @@ -81,7 +81,7 @@ module Puppet # @return [String] for example: "1.6.14-6-gea42046" or nil if the VERSION # file does not exist. def self.read_version_file(path) - if File.exists?(path) + if File.exist?(path) File.read(path).chomp end end diff --git a/spec/fixtures/releases/jamtur01-apache/lib/puppet/provider/a2mod/debian.rb b/spec/fixtures/releases/jamtur01-apache/lib/puppet/provider/a2mod/debian.rb index 82ced05b7..f0cbc446b 100644 --- a/spec/fixtures/releases/jamtur01-apache/lib/puppet/provider/a2mod/debian.rb +++ b/spec/fixtures/releases/jamtur01-apache/lib/puppet/provider/a2mod/debian.rb @@ -16,6 +16,6 @@ Puppet::Type.type(:a2mod).provide(:debian) do def exists? mod= "/etc/apache2/mods-enabled/" + resource[:name] + ".load" - File.exists?(mod) + Puppet::FileSystem::File.exist?(mod) end end diff --git a/spec/fixtures/unit/indirector/hiera/global.yaml b/spec/fixtures/unit/indirector/data_binding/hiera/global.yaml index 0853e0ec1..0853e0ec1 100644 --- a/spec/fixtures/unit/indirector/hiera/global.yaml +++ b/spec/fixtures/unit/indirector/data_binding/hiera/global.yaml diff --git a/spec/fixtures/unit/indirector/data_binding/hiera/invalid.yaml b/spec/fixtures/unit/indirector/data_binding/hiera/invalid.yaml new file mode 100644 index 000000000..9a84fa87c --- /dev/null +++ b/spec/fixtures/unit/indirector/data_binding/hiera/invalid.yaml @@ -0,0 +1 @@ +{ invalid: diff --git a/spec/fixtures/unit/module/trailing-comma.json b/spec/fixtures/unit/module/trailing-comma.json new file mode 100644 index 000000000..23750bfb2 --- /dev/null +++ b/spec/fixtures/unit/module/trailing-comma.json @@ -0,0 +1,24 @@ +{ + "name": "puppetlabs/ruby", + "version": "0.0.2", + "summary": "Manage Ruby", + "source": "git@github.com/puppetlabs/puppetlabs-ruby.git", + "project_page": "http://github.com/puppetlabs/puppetlabs-ruby", + "author": "Puppet Labs", + "license": "Apache-2.0", + "operatingsystem_support": [ + "RedHat", + "OpenSUSE", + "SLES", + "SLED", + "Debian", + "Ubuntu" + ], + "puppet_version": [ + 2.7, + 3.0, + 3.1, + 3.2, + 3.3, + ], +} diff --git a/spec/fixtures/unit/util/monkey_patches/x509.pem b/spec/fixtures/unit/util/monkey_patches/x509.pem new file mode 100644 index 000000000..f7ed7e0fc --- /dev/null +++ b/spec/fixtures/unit/util/monkey_patches/x509.pem @@ -0,0 +1,32 @@ +-----BEGIN CERTIFICATE----- +MIIFmTCCA4GgAwIBAgIQea0WoUqgpa1Mc1j0BxMuZTANBgkqhkiG9w0BAQUFADBf +MRMwEQYKCZImiZPyLGQBGRYDY29tMRkwFwYKCZImiZPyLGQBGRYJbWljcm9zb2Z0 +MS0wKwYDVQQDEyRNaWNyb3NvZnQgUm9vdCBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkw +HhcNMDEwNTA5MjMxOTIyWhcNMjEwNTA5MjMyODEzWjBfMRMwEQYKCZImiZPyLGQB +GRYDY29tMRkwFwYKCZImiZPyLGQBGRYJbWljcm9zb2Z0MS0wKwYDVQQDEyRNaWNy +b3NvZnQgUm9vdCBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkwggIiMA0GCSqGSIb3DQEB +AQUAA4ICDwAwggIKAoICAQDzXfqAZ9Rap6kMLJAg0DUIPHWEzbcHiZyJ2t7Ow2D6 +kWhanpRxKRh2fMLgyCV2lA5Y+gQ0Nubfr/eAuulYCyuT5Z0F43cikfc0ZDwikR1e +4QmQvBT+/HVYGeF5tweSo66IWQjYnwfKA1j8aCltMtfSqMtL/OELSDJP5uu4rU/k +XG8TlJnbldV126gat5SRtHdb9UgMj2p5fRRwBH1tr5D12nDYR7e/my9s5wW34RFg +rHmRFHzF1qbk4X7Vw37lktI8ALU2gt554W3ztW74nzPJy1J9c5g224uha6KVl5uj +3sJNJv8GlmclBsjnrOTuEjOVMZnINQhONMp5U9W1vmMyWUA2wKVOBE0921sHM+RY +v+8/U2TYQlk1V/0PRXwkBE2e1jh0EZcikM5oRHSSb9VLb7CG48c2QqDQ/MHAWvmj +YbkwR3GWChawkcBCle8Qfyhq4yofseTNAz93cQTHIPxJDx1FiKTXy36IrY4t7EXb +xFEEySr87IaemhGXW97OU4jm4rf9rJXCKEDb7wSQ34EzOdmyRaUjhwalVYkxuwYt +YA5BGH0fLrWXyxHrFdUkpZTvFRSJ/Utz+jJb/NEzAPlZYnAHMuouq0Ate8rdIWcb +MJmPFqojqEHRsG4RmzbE3kB0nOFYZcFgHnpbOMiPuwQmfNQWQOW2a2yqhv0Av87B +NQIDAQABo1EwTzALBgNVHQ8EBAMCAcYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4E +FgQUDqyCYEBWJ5flJRP8KuEKU5VZ5KQwEAYJKwYBBAGCNxUBBAMCAQAwDQYJKoZI +hvcNAQEFBQADggIBAMURTQM6YN1dUhF3j7K7NsiyBb+0t6jYIJ1cEwO2HCL6BhM1 +tshj1JpHbyZX0lXxBLEmX9apUGigvNK4bszD6azfGc14rFl0rGY0NsQbPmw4TDMO +MBINoyb+UVMA/69aToQNDx/kbQUuToVLjWwzb1TSZKu/UK99ejmgN+1jAw/8EwbO +FjbUVDuVG1FiOuVNF9QFOZKaJ6hbqr3su77jIIlgcWxWs6UT0G0OI36VA+1oPfLY +Y7hrTbboMLXhypRL96KqXZkwsj2nwlFsKCABJCcrSwC3nRFrcL6yEIK8DJto0I07 +JIeqmShynTNfWZC99d6TnjpiWjQ54ohVHbkGsMGJay3XacMZEjaE0Mmg2v8vaXiy +5Xra69cMwPe9Yxe4ORM4ojZbe/KFVmodZGLBOOKqv1FmopT1EpxmIhBr8rcwki3y +KfA9OxRDaKLxnCk3y844ICVtfGfzfiQSJAMIgUfspZ6X9RjXz7vV73aW7/3O21ad +laBC+ZdY4dcxItNfWeY+biIA6kOEtiXb2fMIVmjAZGsdfOy2k6JiV24u2OdYj8Qx +SSbd3ik1h/UwcXBbFDxpvYkSfesuo/7Yf56CWlIKK8FDK9kwiJ/IEPuJjeahhXUz +fmye23MTZGJppS99ypZtn/gETTCSPW4hFCHJPeDD/YprnUr90aGdmUN3P7Da +-----END CERTIFICATE----- diff --git a/spec/integration/application/apply_spec.rb b/spec/integration/application/apply_spec.rb index aa1f33d4f..43fb3cd66 100755 --- a/spec/integration/application/apply_spec.rb +++ b/spec/integration/application/apply_spec.rb @@ -26,7 +26,7 @@ describe "apply" do puppet.apply - File.should be_exist(file_to_create) + Puppet::FileSystem::File.exist?(file_to_create).should be_true File.read(file_to_create).should == "my stuff" end end diff --git a/spec/integration/application/doc_spec.rb b/spec/integration/application/doc_spec.rb index 83dd39ba9..97ef026e7 100755 --- a/spec/integration/application/doc_spec.rb +++ b/spec/integration/application/doc_spec.rb @@ -40,7 +40,7 @@ describe Puppet::Application::Doc do expect { puppet.run_command }.to exit_with 0 - File.should be_exist('doc') + Puppet::FileSystem::File.exist?('doc').should be_true ensure Dir.chdir(old_dir) end diff --git a/spec/integration/configurer_spec.rb b/spec/integration/configurer_spec.rb index 355458f02..ddb044a0b 100755 --- a/spec/integration/configurer_spec.rb +++ b/spec/integration/configurer_spec.rb @@ -9,7 +9,9 @@ describe Puppet::Configurer do describe "when downloading plugins" do it "should use the :pluginsignore setting, split on whitespace, for ignoring remote files" do resource = Puppet::Type.type(:notify).new :name => "yay" - Puppet::Type.type(:file).expects(:new).with { |args| args[:ignore] == Puppet[:pluginsignore].split(/\s+/) }.returns resource + Puppet::Type.type(:file).expects(:new).at_most(2).with do |args| + args[:ignore] == Puppet[:pluginsignore].split(/\s+/) + end.returns resource configurer = Puppet::Configurer.new configurer.stubs(:download_plugins?).returns true @@ -59,7 +61,7 @@ describe Puppet::Configurer do file_mode = Puppet.features.microsoft_windows? ? '100644' : '100666' - File.stat(Puppet[:lastrunfile]).mode.to_s(8).should == file_mode + Puppet::FileSystem::File.new(Puppet[:lastrunfile]).stat.mode.to_s(8).should == file_mode summary = nil File.open(Puppet[:lastrunfile], "r") do |fd| diff --git a/spec/integration/data_binding.rb b/spec/integration/data_binding.rb new file mode 100644 index 000000000..0e8db4dfa --- /dev/null +++ b/spec/integration/data_binding.rb @@ -0,0 +1,100 @@ +require 'spec_helper' + +require 'puppet_spec/compiler' + +describe "Data binding" do + include PuppetSpec::Files + include PuppetSpec::Compiler + + let(:dir) { tmpdir("puppetdir") } + + before do + Puppet[:data_binding_terminus] = "hiera" + Puppet[:modulepath] = dir + end + + it "looks up data from hiera" do + configure_hiera_for({ + "testing::binding::value" => "the value", + "testing::binding::calling_class" => "%{calling_class}", + "testing::binding::calling_module" => "%{calling_module}" + }) + + create_manifest_in_module("testing", "binding.pp", + <<-MANIFEST) + class testing::binding($value, + $calling_class, + $calling_module) {} + MANIFEST + + catalog = compile_to_catalog("include testing::binding") + resource = catalog.resource('Class[testing::binding]') + + expect(resource[:value]).to eq("the value") + expect(resource[:calling_class]).to eq("testing::binding") + expect(resource[:calling_module]).to eq("testing") + end + + it "works with the puppet backend configured, although it can't use it for lookup" do + configure_hiera_for_puppet + create_manifest_in_module("testing", "binding.pp", + <<-MANIFEST) + # lookup via the puppet backend to ensure it works + class testing::binding($value = hiera('variable')) {} + MANIFEST + + create_manifest_in_module("testing", "data.pp", + <<-MANIFEST) + class testing::data { + $variable = "the value" + } + MANIFEST + + catalog = compile_to_catalog("include testing::binding") + resource = catalog.resource('Class[testing::binding]') + + expect(resource[:value]).to eq("the value") + end + + def configure_hiera_for(data) + hiera_config_file = tmpfile("hiera.yaml") + + File.open(hiera_config_file, 'w') do |f| + f.write("--- + :yaml: + :datadir: #{dir} + :hierarchy: ['global'] + :logger: 'noop' + :backends: ['yaml'] + ") + end + + File.open(File.join(dir, 'global.yaml'), 'w') do |f| + f.write(YAML.dump(data)) + end + + Puppet[:hiera_config] = hiera_config_file + end + + def configure_hiera_for_puppet + hiera_config_file = tmpfile("hiera.yaml") + + File.open(hiera_config_file, 'w') do |f| + f.write("--- + :logger: 'noop' + :backends: ['puppet'] + ") + end + + Puppet[:hiera_config] = hiera_config_file + end + + def create_manifest_in_module(module_name, name, manifest) + module_dir = File.join(dir, module_name, 'manifests') + FileUtils.mkdir_p(module_dir) + + File.open(File.join(module_dir, name), 'w') do |f| + f.write(manifest) + end + end +end diff --git a/spec/integration/indirector/catalog/compiler_spec.rb b/spec/integration/indirector/catalog/compiler_spec.rb index f5c16964f..be5a415bc 100755 --- a/spec/integration/indirector/catalog/compiler_spec.rb +++ b/spec/integration/indirector/catalog/compiler_spec.rb @@ -32,33 +32,36 @@ describe Puppet::Resource::Catalog::Compiler do end it "should filter out virtual resources when finding a catalog" do - @one.virtual = true - request = stub 'request', :name => "mynode" + Puppet[:node_terminus] = :memory + Puppet::Node.indirection.save(Puppet::Node.new("mynode")) Puppet::Resource::Catalog.indirection.terminus.stubs(:extract_facts_from_request) - Puppet::Resource::Catalog.indirection.terminus.stubs(:node_from_request) Puppet::Resource::Catalog.indirection.terminus.stubs(:compile).returns(@catalog) - Puppet::Resource::Catalog.indirection.find(request).resource_refs.should == [ @two.ref ] + @one.virtual = true + + Puppet::Resource::Catalog.indirection.find("mynode").resource_refs.should == [ @two.ref ] end it "should not filter out exported resources when finding a catalog" do - @one.exported = true - request = stub 'request', :name => "mynode" + Puppet[:node_terminus] = :memory + Puppet::Node.indirection.save(Puppet::Node.new("mynode")) Puppet::Resource::Catalog.indirection.terminus.stubs(:extract_facts_from_request) - Puppet::Resource::Catalog.indirection.terminus.stubs(:node_from_request) Puppet::Resource::Catalog.indirection.terminus.stubs(:compile).returns(@catalog) - Puppet::Resource::Catalog.indirection.find(request).resource_refs.sort.should == [ @one.ref, @two.ref ] + @one.exported = true + + Puppet::Resource::Catalog.indirection.find("mynode").resource_refs.sort.should == [ @one.ref, @two.ref ] end it "should filter out virtual exported resources when finding a catalog" do - @one.exported = true - @one.virtual = true - request = stub 'request', :name => "mynode" + Puppet[:node_terminus] = :memory + Puppet::Node.indirection.save(Puppet::Node.new("mynode")) Puppet::Resource::Catalog.indirection.terminus.stubs(:extract_facts_from_request) - Puppet::Resource::Catalog.indirection.terminus.stubs(:node_from_request) Puppet::Resource::Catalog.indirection.terminus.stubs(:compile).returns(@catalog) - Puppet::Resource::Catalog.indirection.find(request).resource_refs.should == [ @two.ref ] + @one.exported = true + @one.virtual = true + + Puppet::Resource::Catalog.indirection.find("mynode").resource_refs.should == [ @two.ref ] end end diff --git a/spec/integration/indirector/direct_file_server_spec.rb b/spec/integration/indirector/direct_file_server_spec.rb index b82734845..3a6b96250 100755 --- a/spec/integration/indirector/direct_file_server_spec.rb +++ b/spec/integration/indirector/direct_file_server_spec.rb @@ -15,7 +15,7 @@ describe Puppet::Indirector::DirectFileServer, " when interacting with the files it "should return an instance of the model" do pending("porting to Windows", :if => Puppet.features.microsoft_windows?) do - FileTest.expects(:exists?).with(@filepath).returns(true) + Puppet::FileSystem::File.expects(:exist?).with(@filepath).returns(true) @terminus.find(@terminus.indirection.request(:find, "file://host#{@filepath}", nil)).should be_instance_of(Puppet::FileServing::Content) end @@ -23,11 +23,9 @@ describe Puppet::Indirector::DirectFileServer, " when interacting with the files it "should return an instance capable of returning its content" do pending("porting to Windows", :if => Puppet.features.microsoft_windows?) do - FileTest.expects(:exists?).with(@filepath).returns(true) - File.stubs(:lstat).with(@filepath).returns(stub("stat", :ftype => "file")) - IO.expects(:binread).with(@filepath).returns("my content") + filename = file_containing("testfile", "my content") - instance = @terminus.find(@terminus.indirection.request(:find, "file://host#{@filepath}", nil)) + instance = @terminus.find(@terminus.indirection.request(:find, "file://host#{filename}", nil)) instance.content.should == "my content" end diff --git a/spec/integration/indirector/file_content/file_server_spec.rb b/spec/integration/indirector/file_content/file_server_spec.rb index 00bdda412..44e2fabee 100755 --- a/spec/integration/indirector/file_content/file_server_spec.rb +++ b/spec/integration/indirector/file_content/file_server_spec.rb @@ -59,8 +59,8 @@ describe Puppet::Indirector::FileContent::FileServer, " when finding files" do end it "should find file content in files when node name expansions are used" do - FileTest.stubs(:exists?).returns true - FileTest.stubs(:exists?).with(Puppet[:fileserverconfig]).returns(true) + Puppet::FileSystem::File.stubs(:exist?).returns true + Puppet::FileSystem::File.stubs(:exist?).with(Puppet[:fileserverconfig]).returns(true) @path = tmpfile("file_server_testing") diff --git a/spec/integration/network/server/webrick_spec.rb b/spec/integration/network/server/webrick_spec.rb deleted file mode 100755 index 5ae490410..000000000 --- a/spec/integration/network/server/webrick_spec.rb +++ /dev/null @@ -1,76 +0,0 @@ -#! /usr/bin/env ruby -require 'spec_helper' -require 'puppet/network/server' -require 'puppet/ssl/certificate_authority' -require 'socket' - -describe Puppet::Network::Server, :unless => Puppet.features.microsoft_windows? do - describe "when using webrick" do - include PuppetSpec::Files - - # This reduces the odds of conflicting port numbers between concurrent runs - # of the suite on the same machine dramatically. - let(:port) { 20000 + ($$ % 40000) } - let(:address) { '127.0.0.1' } - - before :each do - Puppet[:server] = '127.0.0.1' - - # Get a safe temporary file - dir = tmpdir("webrick_integration_testing") - - Puppet.settings[:confdir] = dir - Puppet.settings[:vardir] = dir - Puppet.settings[:logdir] = dir - Puppet.settings[:group] = Process.gid - - Puppet::SSL::Host.ca_location = :local - - ca = Puppet::SSL::CertificateAuthority.new - ca.generate(Puppet[:certname]) unless Puppet::SSL::Certificate.indirection.find(Puppet[:certname]) - - @server = Puppet::Network::Server.new(address, port) - end - - after do - Puppet::SSL::Host.ca_location = :none - end - - describe "before listening" do - it "should not be reachable at the specified address and port" do - expect { TCPSocket.new('127.0.0.1', port) }.to raise_error - end - end - - describe "when listening" do - it "should be reachable on the specified address and port" do - @server.start - expect { TCPSocket.new('127.0.0.1', port) }.to_not raise_error - end - - it "should use any specified bind address" do - @server.stubs(:stop) # we're breaking listening internally, so we have to keep it from unlistening - Puppet::Network::HTTP::WEBrick.any_instance.expects(:listen).with(address, port) - @server.start - end - - it "should not allow multiple servers to listen on the same address and port" do - @server.start - server2 = Puppet::Network::Server.new(address, port) - expect { server2.start }.to raise_error - end - - after :each do - @server.stop if @server && @server.listening? - end - end - - describe "after unlistening" do - it "should not be reachable on the port and address assigned" do - @server.start - @server.stop - expect { TCPSocket.new('127.0.0.1', port) }.to raise_error(Errno::ECONNREFUSED) - end - end - end -end diff --git a/spec/integration/node/facts_spec.rb b/spec/integration/node/facts_spec.rb index d307be35a..fa9fd4905 100755 --- a/spec/integration/node/facts_spec.rb +++ b/spec/integration/node/facts_spec.rb @@ -23,7 +23,7 @@ describe Puppet::Node::Facts do terminus = Puppet::Node::Facts.indirection.terminus(:yaml) terminus.expects(:path).with("me").returns "/my/yaml/file" - FileTest.expects(:exist?).with("/my/yaml/file").returns false + Puppet::FileSystem::File.expects(:exist?).with("/my/yaml/file").returns false Puppet::Node::Facts.indirection.find("me").should be_nil end diff --git a/spec/integration/node_spec.rb b/spec/integration/node_spec.rb index 5ea3df9aa..ef862ef28 100755 --- a/spec/integration/node_spec.rb +++ b/spec/integration/node_spec.rb @@ -21,7 +21,7 @@ describe Puppet::Node do terminus.expects(:path).with(@name).returns "/my/yaml/file" - FileTest.expects(:exist?).with("/my/yaml/file").returns false + Puppet::FileSystem::File.expects(:exist?).with("/my/yaml/file").returns false Puppet::Node.indirection.find(@name).should be_nil end diff --git a/spec/integration/parser/compiler_spec.rb b/spec/integration/parser/compiler_spec.rb index 01e5c085f..eb87d03ed 100755 --- a/spec/integration/parser/compiler_spec.rb +++ b/spec/integration/parser/compiler_spec.rb @@ -1,8 +1,11 @@ #! /usr/bin/env ruby require 'spec_helper' require 'puppet/parser/parser_factory' +require 'puppet_spec/compiler' describe "Puppet::Parser::Compiler" do + include PuppetSpec::Compiler + before :each do @node = Puppet::Node.new "testnode" @@ -310,6 +313,93 @@ describe "Puppet::Parser::Compiler" do expected_subscriptions << ['b', 'c'] << ['e', 'd'] end end + + context 'when working with the trusted data hash' do + context 'and have opted in to trusted_node_data' do + before :each do + Puppet[:trusted_node_data] = true + end + + it 'should make $trusted available' do + node = Puppet::Node.new("testing") + node.trusted_data = { "data" => "value" } + + catalog = compile_to_catalog(<<-MANIFEST, node) + notify { 'test': message => $trusted[data] } + MANIFEST + + catalog.resource("Notify[test]")[:message].should == "value" + end + + it 'should not allow assignment to $trusted' do + node = Puppet::Node.new("testing") + node.trusted_data = { "data" => "value" } + + expect do + catalog = compile_to_catalog(<<-MANIFEST, node) + $trusted = 'changed' + notify { 'test': message => $trusted == 'changed' } + MANIFEST + catalog.resource("Notify[test]")[:message].should == true + end.to raise_error(Puppet::Error, /Attempt to assign to a reserved variable name: 'trusted'/) + end + + it 'should not allow addition to $trusted hash' do + node = Puppet::Node.new("testing") + node.trusted_data = { "data" => "value" } + + expect do + catalog = compile_to_catalog(<<-MANIFEST, node) + $trusted['extra'] = 'added' + notify { 'test': message => $trusted['extra'] == 'added' } + MANIFEST + catalog.resource("Notify[test]")[:message].should == true + # different errors depending on regular or future parser + end.to raise_error(Puppet::Error, /(can't modify frozen [hH]ash)|(Illegal attempt to assign)/) + end + + it 'should not allow addition to $trusted hash via Ruby inline template' do + node = Puppet::Node.new("testing") + node.trusted_data = { "data" => "value" } + + expect do + catalog = compile_to_catalog(<<-MANIFEST, node) + $dummy = inline_template("<% @trusted['extra'] = 'added' %> lol") + notify { 'test': message => $trusted['extra'] == 'added' } + MANIFEST + catalog.resource("Notify[test]")[:message].should == true + end.to raise_error(Puppet::Error, /can't modify frozen [hH]ash/) + end + end + + context 'and have not opted in to trusted_node_data' do + before :each do + Puppet[:trusted_node_data] = false + end + + it 'should not make $trusted available' do + node = Puppet::Node.new("testing") + node.trusted_data = { "data" => "value" } + + catalog = compile_to_catalog(<<-MANIFEST, node) + notify { 'test': message => $trusted == undef } + MANIFEST + + catalog.resource("Notify[test]")[:message].should == true + end + + it 'should allow assignment to $trusted' do + node = Puppet::Node.new("testing") + + catalog = compile_to_catalog(<<-MANIFEST, node) + $trusted = 'changed' + notify { 'test': message => $trusted == 'changed' } + MANIFEST + + catalog.resource("Notify[test]")[:message].should == true + end + end + end end describe 'using classic parser' do diff --git a/spec/integration/parser/functions_spec.rb b/spec/integration/parser/functions_spec.rb deleted file mode 100755 index 51cfb92c1..000000000 --- a/spec/integration/parser/functions_spec.rb +++ /dev/null @@ -1,16 +0,0 @@ -#! /usr/bin/env ruby -require 'spec_helper' - -describe Puppet::Parser::Functions do - it "should support multiple threads autoloading the same function" do - threads = [] - lambda { - 10.times { |a| - threads << Thread.new { - Puppet::Parser::Functions.function("template") - } - } - }.should_not raise_error - threads.each { |t| t.join } - end -end diff --git a/spec/integration/parser/parser_spec.rb b/spec/integration/parser/parser_spec.rb index 759643a0f..280fd040f 100755 --- a/spec/integration/parser/parser_spec.rb +++ b/spec/integration/parser/parser_spec.rb @@ -213,11 +213,11 @@ describe "Puppet::Parser::Parser" do end it 'should flag illegal use of non r-value producing <| |>' do - expect { @parser.parse("$a = file <| |>") }.to raise_error(/A Virtual Query does not produce a value at line 1:6/) + expect { @parser.parse("$a = File <| |>") }.to raise_error(/A Virtual Query does not produce a value at line 1:6/) end it 'should flag illegal use of non r-value producing <<| |>>' do - expect { @parser.parse("$a = file <<| |>>") }.to raise_error(/An Exported Query does not produce a value at line 1:6/) + expect { @parser.parse("$a = File <<| |>>") }.to raise_error(/An Exported Query does not produce a value at line 1:6/) end it 'should flag illegal use of non r-value producing define' do diff --git a/spec/integration/provider/cron/crontab_spec.rb b/spec/integration/provider/cron/crontab_spec.rb index 4681ff109..3c262c926 100644 --- a/spec/integration/provider/cron/crontab_spec.rb +++ b/spec/integration/provider/cron/crontab_spec.rb @@ -10,12 +10,12 @@ describe Puppet::Type.type(:cron).provider(:crontab), '(integration)', :unless = Puppet::Type.type(:cron).stubs(:defaultprovider).returns described_class Puppet::FileBucket::Dipper.any_instance.stubs(:backup) # Don't backup to filebucket - # I dont want to execute anything + # I don't want to execute anything described_class.stubs(:filetype).returns Puppet::Util::FileType::FileTypeFlat described_class.stubs(:default_target).returns crontab_user1 - # I dont want to stub Time.now to get a static header because I dont know - # where Time.now is used elsewere so just go with a very simple header + # I don't want to stub Time.now to get a static header because I don't know + # where Time.now is used elsewhere, so just go with a very simple header described_class.stubs(:header).returns "# HEADER: some simple\n# HEADER: header\n" FileUtils.cp(my_fixture('crontab_user1'), crontab_user1) FileUtils.cp(my_fixture('crontab_user2'), crontab_user2) @@ -191,8 +191,6 @@ describe Puppet::Type.type(:cron).provider(:crontab), '(integration)', :unless = File.read(crontab_user2).should == File.read(my_fixture('moved_cronjob_input2')) end end - - it "should not add multiple headers" end end diff --git a/spec/integration/resource/catalog_spec.rb b/spec/integration/resource/catalog_spec.rb index 3dc30560c..1469cc063 100755 --- a/spec/integration/resource/catalog_spec.rb +++ b/spec/integration/resource/catalog_spec.rb @@ -21,7 +21,7 @@ describe Puppet::Resource::Catalog do terminus = Puppet::Resource::Catalog.indirection.terminus(:yaml) terminus.expects(:path).with("me").returns "/my/yaml/file" - FileTest.expects(:exist?).with("/my/yaml/file").returns false + Puppet::FileSystem::File.expects(:exist?).with("/my/yaml/file").returns false Puppet::Resource::Catalog.indirection.find("me").should be_nil end diff --git a/spec/integration/ssl/autosign_spec.rb b/spec/integration/ssl/autosign_spec.rb new file mode 100644 index 000000000..003796ef0 --- /dev/null +++ b/spec/integration/ssl/autosign_spec.rb @@ -0,0 +1,130 @@ +require 'spec_helper' + +describe "autosigning" do + include PuppetSpec::Files + + let(:puppet_dir) { tmpdir("ca_autosigning") } + let(:csr_attributes_content) do + { + 'custom_attributes' => { + '1.3.6.1.4.1.34380.2.0' => 'hostname.domain.com', + '1.3.6.1.4.1.34380.2.1' => 'my passphrase', + '1.3.6.1.4.1.34380.2.2' => # system IPs in hex + [ 0xC0A80001, # 192.168.0.1 + 0xC0A80101 ], # 192.168.1.1 + }, + 'extension_requests' => { + 'pp_uuid' => 'abcdef', + '1.3.6.1.4.1.34380.1.1.2' => '1234', # pp_instance_id + '1.3.6.1.4.1.34380.1.2.1' => 'some-value', # private extension + }, + } + end + + let(:host) { Puppet::SSL::Host.new } + + before do + Puppet.settings[:confdir] = puppet_dir + Puppet.settings[:vardir] = puppet_dir + + # This is necessary so the terminus instances don't lie around. + Puppet::SSL::Key.indirection.termini.clear + end + + def write_csr_attributes(yaml) + File.open(Puppet.settings[:csr_attributes], 'w') do |file| + file.puts YAML.dump(yaml) + end + end + + context "when the csr_attributes file is valid, but empty" do + it "generates a CSR when the file is empty" do + Puppet::FileSystem::File.new(Puppet.settings[:csr_attributes]).touch + + host.generate_certificate_request + end + + it "generates a CSR when the file contains whitespace" do + File.open(Puppet.settings[:csr_attributes], 'w') do |file| + file.puts "\n\n" + end + + host.generate_certificate_request + end + end + + context "when the csr_attributes file doesn't contain a YAML encoded hash" do + it "raises when the file contains a string" do + write_csr_attributes('a string') + + expect { + host.generate_certificate_request + }.to raise_error(Puppet::Error, /invalid CSR attributes, expected instance of Hash, received instance of String/) + end + + it "raises when the file contains an empty array" do + write_csr_attributes([]) + + expect { + host.generate_certificate_request + }.to raise_error(Puppet::Error, /invalid CSR attributes, expected instance of Hash, received instance of Array/) + end + end + + context "with extension requests from csr_attributes file" do + let(:ca) { Puppet::SSL::CertificateAuthority.new } + + it "generates a CSR when the csr_attributes file is an empty hash" do + write_csr_attributes(csr_attributes_content) + + host.generate_certificate_request + end + + context "and subjectAltName" do + it "raises an error if you include subjectAltName in csr_attributes" do + csr_attributes_content['extension_requests']['subjectAltName'] = 'foo' + write_csr_attributes(csr_attributes_content) + expect { host.generate_certificate_request }.to raise_error(Puppet::Error, /subjectAltName.*conflicts with internally used extension request/) + end + + it "properly merges subjectAltName when in settings" do + Puppet.settings[:dns_alt_names] = 'althostname.nowhere' + write_csr_attributes(csr_attributes_content) + host.generate_certificate_request + csr = Puppet::SSL::CertificateRequest.indirection.find(host.name) + expect(csr.subject_alt_names).to include('DNS:althostname.nowhere') + end + end + + context "without subjectAltName" do + + before do + write_csr_attributes(csr_attributes_content) + host.generate_certificate_request + end + + it "pulls extension attributes from the csr_attributes file into the certificate" do + csr = Puppet::SSL::CertificateRequest.indirection.find(host.name) + expect(csr.request_extensions).to have(3).items + expect(csr.request_extensions).to include('oid' => 'pp_uuid', 'value' => 'abcdef') + expect(csr.request_extensions).to include('oid' => 'pp_instance_id', 'value' => '1234') + expect(csr.request_extensions).to include('oid' => '1.3.6.1.4.1.34380.1.2.1', 'value' => 'some-value') + end + + it "copies extension requests to certificate" do + cert = ca.sign(host.name) + expect(cert.custom_extensions).to include('oid' => 'pp_uuid', 'value' => 'abcdef') + expect(cert.custom_extensions).to include('oid' => 'pp_instance_id', 'value' => '1234') + expect(cert.custom_extensions).to include('oid' => '1.3.6.1.4.1.34380.1.2.1', 'value' => 'some-value') + end + + it "does not copy custom attributes to certificate" do + cert = ca.sign(host.name) + cert.custom_extensions.each do |ext| + expect(Puppet::SSL::Oids.subtree_of?('1.3.6.1.4.1.34380.2', ext['oid'])).to be_false + end + end + end + + end +end diff --git a/spec/integration/ssl/certificate_authority_spec.rb b/spec/integration/ssl/certificate_authority_spec.rb index 724f6b371..acbc38cbc 100755 --- a/spec/integration/ssl/certificate_authority_spec.rb +++ b/spec/integration/ssl/certificate_authority_spec.rb @@ -6,8 +6,9 @@ require 'puppet/ssl/certificate_authority' describe Puppet::SSL::CertificateAuthority, :unless => Puppet.features.microsoft_windows? do include PuppetSpec::Files + let(:ca) { @ca } + before do - # Get a safe temporary file dir = tmpdir("ca_integration_testing") Puppet.settings[:confdir] = dir @@ -15,103 +16,58 @@ describe Puppet::SSL::CertificateAuthority, :unless => Puppet.features.microsoft Puppet.settings[:group] = Process.gid Puppet::SSL::Host.ca_location = :local - @ca = Puppet::SSL::CertificateAuthority.new - end - - after { - Puppet::SSL::Host.ca_location = :none - - Puppet.settings.clear - - Puppet::SSL::CertificateAuthority.instance_variable_set("@instance", nil) - } - it "should create a CA host" do - @ca.host.should be_ca - end - - it "should be able to generate a certificate" do - @ca.generate_ca_certificate - - @ca.host.certificate.should be_instance_of(Puppet::SSL::Certificate) + # this has the side-effect of creating the various directories that we need + @ca = Puppet::SSL::CertificateAuthority.new end it "should be able to generate a new host certificate" do - @ca.generate("newhost") + ca.generate("newhost") Puppet::SSL::Certificate.indirection.find("newhost").should be_instance_of(Puppet::SSL::Certificate) end it "should be able to revoke a host certificate" do - @ca.generate("newhost") + ca.generate("newhost") - @ca.revoke("newhost") + ca.revoke("newhost") - lambda { @ca.verify("newhost") }.should raise_error - end - - it "should have a CRL" do - @ca.generate_ca_certificate - @ca.crl.should_not be_nil - end - - it "should be able to read in a previously created CRL" do - @ca.generate_ca_certificate - - # Create it to start with. - @ca.crl - - Puppet::SSL::CertificateAuthority.new.crl.should_not be_nil + expect { ca.verify("newhost") }.to raise_error(Puppet::SSL::CertificateAuthority::CertificateVerificationError, "certificate revoked") end describe "when signing certificates" do - before do - @host = Puppet::SSL::Host.new("luke.madstop.com") - - # We have to provide the key, since when we're in :ca_only mode, we can only interact - # with the CA key. - key = Puppet::SSL::Key.new(@host.name) - key.generate - - @host.key = key - @host.generate_certificate_request - - path = File.join(Puppet[:requestdir], "luke.madstop.com.pem") - end - - it "should be able to sign certificates" do - @ca.sign("luke.madstop.com") - end - it "should save the signed certificate" do - @ca.sign("luke.madstop.com") + host = certificate_request_for("luke.madstop.com") + + ca.sign("luke.madstop.com") Puppet::SSL::Certificate.indirection.find("luke.madstop.com").should be_instance_of(Puppet::SSL::Certificate) end it "should be able to sign multiple certificates" do - @other = Puppet::SSL::Host.new("other.madstop.com") - okey = Puppet::SSL::Key.new(@other.name) - okey.generate - @other.key = okey - @other.generate_certificate_request + host = certificate_request_for("luke.madstop.com") + other = certificate_request_for("other.madstop.com") - @ca.sign("luke.madstop.com") - @ca.sign("other.madstop.com") + ca.sign("luke.madstop.com") + ca.sign("other.madstop.com") Puppet::SSL::Certificate.indirection.find("other.madstop.com").should be_instance_of(Puppet::SSL::Certificate) Puppet::SSL::Certificate.indirection.find("luke.madstop.com").should be_instance_of(Puppet::SSL::Certificate) end it "should save the signed certificate to the :signeddir" do - @ca.sign("luke.madstop.com") + host = certificate_request_for("luke.madstop.com") + + ca.sign("luke.madstop.com") client_cert = File.join(Puppet[:signeddir], "luke.madstop.com.pem") File.read(client_cert).should == Puppet::SSL::Certificate.indirection.find("luke.madstop.com").content.to_s end it "should save valid certificates" do - @ca.sign("luke.madstop.com") + host = certificate_request_for("luke.madstop.com") + + ca.sign("luke.madstop.com") unless ssl = Puppet::Util::which('openssl') pending "No ssl available" @@ -124,21 +80,58 @@ describe Puppet::SSL::CertificateAuthority, :unless => Puppet.features.microsoft end it "should verify proof of possession when signing certificates" do - csr = @host.certificate_request - wrong_key = Puppet::SSL::Key.new(@host.name) + host = certificate_request_for("luke.madstop.com") + csr = host.certificate_request + wrong_key = Puppet::SSL::Key.new(host.name) wrong_key.generate csr.content.public_key = wrong_key.content.public_key # The correct key has to be removed so we can save the incorrect one - Puppet::SSL::CertificateRequest.indirection.destroy(@host.name) + Puppet::SSL::CertificateRequest.indirection.destroy(host.name) Puppet::SSL::CertificateRequest.indirection.save(csr) expect { - @ca.sign(@host.name) + ca.sign(host.name) }.to raise_error( Puppet::SSL::CertificateAuthority::CertificateSigningError, "CSR contains a public key that does not correspond to the signing key" ) end end + + it "allows autosigning certificates concurrently", :unless => Puppet::Util::Platform.windows? do + Puppet[:autosign] = true + hosts = (0..4).collect { |i| certificate_request_for("host#{i}") } + + run_in_parallel(5) do |i| + ca.autosign(Puppet::SSL::CertificateRequest.indirection.find(hosts[i].name)) + end + + certs = hosts.collect { |host| Puppet::SSL::Certificate.indirection.find(host.name).content } + serial_numbers = certs.collect(&:serial) + + serial_numbers.sort.should == [2, 3, 4, 5, 6] # serial 1 is the ca certificate + end + + def certificate_request_for(hostname) + key = Puppet::SSL::Key.new(hostname) + key.generate + + host = Puppet::SSL::Host.new(hostname) + host.key = key + host.generate_certificate_request + + host + end + + def run_in_parallel(number) + children = [] + number.times do |i| + children << Kernel.fork do + yield i + end + end + + children.each { |pid| Process.wait(pid) } + end end diff --git a/spec/integration/ssl/certificate_revocation_list_spec.rb b/spec/integration/ssl/certificate_revocation_list_spec.rb index add481151..530f03ed9 100755 --- a/spec/integration/ssl/certificate_revocation_list_spec.rb +++ b/spec/integration/ssl/certificate_revocation_list_spec.rb @@ -29,7 +29,7 @@ describe Puppet::SSL::CertificateRevocationList do it "should be able to read in written out CRLs with no revoked certificates" do ca = Puppet::SSL::CertificateAuthority.new - raise "CRL not created" unless FileTest.exist?(Puppet[:hostcrl]) + raise "CRL not created" unless Puppet::FileSystem::File.exist?(Puppet[:hostcrl]) crl = Puppet::SSL::CertificateRevocationList.new("crl_int_testing") crl.read(Puppet[:hostcrl]) diff --git a/spec/integration/ssl/host_spec.rb b/spec/integration/ssl/host_spec.rb index d8191f9b4..fb6b4e1e0 100755 --- a/spec/integration/ssl/host_spec.rb +++ b/spec/integration/ssl/host_spec.rb @@ -70,7 +70,7 @@ describe Puppet::SSL::Host do @ca = Puppet::SSL::Host.new(Puppet::SSL::Host.ca_name) @ca.generate_key - FileTest.should_not be_exist(File.join(Puppet[:privatekeydir], "ca.pem")) + Puppet::FileSystem::File.exist?(File.join(Puppet[:privatekeydir], "ca.pem")).should be_false end end diff --git a/spec/integration/transaction_spec.rb b/spec/integration/transaction_spec.rb index 4fdd69315..851954dbe 100755 --- a/spec/integration/transaction_spec.rb +++ b/spec/integration/transaction_spec.rb @@ -64,7 +64,7 @@ describe Puppet::Transaction do catalog.add_resource resource catalog.apply - FileTest.should be_exist(path) + Puppet::FileSystem::File.exist?(path).should be_true end it "should not apply virtual exported resources" do @@ -189,8 +189,8 @@ describe Puppet::Transaction do catalog = mk_catalog(file, exec1, exec2) catalog.apply - FileTest.should be_exist(file1) - FileTest.should be_exist(file2) + Puppet::FileSystem::File.exist?(file1).should be_true + Puppet::FileSystem::File.exist?(file2).should be_true end it "should not let one failed refresh result in other refreshes failing" do @@ -223,7 +223,7 @@ describe Puppet::Transaction do catalog = mk_catalog(file, exec1, exec2) catalog.apply - FileTest.should be_exists(newfile) + Puppet::FileSystem::File.exist?(newfile).should be_true end it "should still trigger skipped resources" do @@ -251,18 +251,18 @@ describe Puppet::Transaction do # Run it once catalog.apply - FileTest.should be_exists(fname) + Puppet::FileSystem::File.exist?(fname).should be_true # Now remove it, so it can get created again - File.unlink(fname) + Puppet::FileSystem::File.unlink(fname) file[:content] = "some content" catalog.apply - FileTest.should be_exists(fname) + Puppet::FileSystem::File.exist?(fname).should be_true # Now remove it, so it can get created again - File.unlink(fname) + Puppet::FileSystem::File.unlink(fname) # And tag our exec exec.tag("testrun") @@ -275,7 +275,7 @@ describe Puppet::Transaction do file[:content] = "totally different content" catalog.apply - FileTest.should be_exists(fname) + Puppet::FileSystem::File.exist?(fname).should be_true end it "should not attempt to evaluate resources with failed dependencies" do @@ -302,8 +302,8 @@ describe Puppet::Transaction do catalog = mk_catalog(exec, file1, file2) catalog.apply - FileTest.should_not be_exists(file1[:path]) - FileTest.should_not be_exists(file2[:path]) + Puppet::FileSystem::File.exist?(file1[:path]).should be_false + Puppet::FileSystem::File.exist?(file2[:path]).should be_false end it "should not trigger subscribing resources on failure" do @@ -328,8 +328,8 @@ describe Puppet::Transaction do catalog = mk_catalog(exec, create_file1, create_file2) catalog.apply - FileTest.should_not be_exists(file1) - FileTest.should_not be_exists(file2) + Puppet::FileSystem::File.exist?(file1).should be_false + Puppet::FileSystem::File.exist?(file2).should be_false end # #801 -- resources only checked in noop should be rescheduled immediately. diff --git a/spec/integration/type/exec_spec.rb b/spec/integration/type/exec_spec.rb index d31c20134..2b044473f 100755 --- a/spec/integration/type/exec_spec.rb +++ b/spec/integration/type/exec_spec.rb @@ -33,7 +33,7 @@ describe Puppet::Type.type(:exec) do catalog.add_resource exec catalog.apply - File.should_not be_exist(path) + Puppet::FileSystem::File.exist?(path).should be_false end it "should execute the command if onlyif returns zero" do @@ -72,6 +72,6 @@ describe Puppet::Type.type(:exec) do catalog.add_resource exec catalog.apply - File.should_not be_exist(path) + Puppet::FileSystem::File.exist?(path).should be_false end end diff --git a/spec/integration/type/file_spec.rb b/spec/integration/type/file_spec.rb index 5377251fd..7cd39aace 100755 --- a/spec/integration/type/file_spec.rb +++ b/spec/integration/type/file_spec.rb @@ -21,21 +21,28 @@ describe Puppet::Type.type(:file) do File.join(parent, 'file_testing') end + let(:dir) do + # we create a directory first so backups of :path that are stored in + # the same directory will also be removed after the tests + parent = tmpdir('file_spec') + File.join(parent, 'dir_testing') + end + if Puppet.features.posix? def set_mode(mode, file) File.chmod(mode, file) end def get_mode(file) - File.lstat(file).mode + Puppet::FileSystem::File.new(file).lstat.mode end def get_owner(file) - File.lstat(file).uid + Puppet::FileSystem::File.new(file).lstat.uid end def get_group(file) - File.lstat(file).gid + Puppet::FileSystem::File.new(file).lstat.gid end else class SecurityHelper @@ -57,6 +64,10 @@ describe Puppet::Type.type(:file) do def get_group(file) SecurityHelper.get_group(file) end + + def get_aces_for_path_by_sid(path, sid) + SecurityHelper.get_aces_for_path_by_sid(path, sid) + end end before do @@ -72,7 +83,7 @@ describe Puppet::Type.type(:file) do status = catalog.apply.report.resource_statuses["File[#{source}]"] status.should_not be_failed status.should_not be_changed - File.should_not be_exist(source) + Puppet::FileSystem::File.exist?(source).should be_false end describe "when ensure is absent" do @@ -81,14 +92,14 @@ describe Puppet::Type.type(:file) do catalog.add_resource(described_class.new(:path => path, :ensure => :absent, :backup => :false)) report = catalog.apply.report report.resource_statuses["File[#{path}]"].should_not be_failed - File.should_not be_exist(path) + Puppet::FileSystem::File.exist?(path).should be_false end it "should do nothing if file is not present" do catalog.add_resource(described_class.new(:path => path, :ensure => :absent, :backup => :false)) report = catalog.apply.report report.resource_statuses["File[#{path}]"].should_not be_failed - File.should_not be_exist(path) + Puppet::FileSystem::File.exist?(path).should be_false end # issue #14599 @@ -204,7 +215,7 @@ describe Puppet::Type.type(:file) do end end - describe "for links", :unless => Puppet.features.microsoft_windows? do + describe "for links", :if => described_class.defaultprovider.feature?(:manages_symlinks) do let(:link) { tmpfile('link_mode') } describe "when managing links" do @@ -214,7 +225,7 @@ describe Puppet::Type.type(:file) do FileUtils.touch(link_target) File.chmod(0444, link_target) - File.symlink(link_target, link) + Puppet::FileSystem::File.new(link_target).symlink(link) end it "should not set the executable bit on the link nor the target" do @@ -222,8 +233,8 @@ describe Puppet::Type.type(:file) do catalog.apply - (File.stat(link).mode & 07777) == 0666 - (File.lstat(link_target).mode & 07777) == 0444 + (Puppet::FileSystem::File.new(link).stat.mode & 07777) == 0666 + (Puppet::FileSystem::File.new(link_target).lstat.mode & 07777) == 0444 end it "should ignore dangling symlinks (#6856)" do @@ -232,7 +243,7 @@ describe Puppet::Type.type(:file) do catalog.add_resource described_class.new(:path => link, :ensure => :link, :mode => 0666, :target => link_target, :links => :manage) catalog.apply - File.should_not be_exist(link) + Puppet::FileSystem::File.exist?(link).should be_false end it "should create a link to the target if ensure is omitted" do @@ -240,9 +251,9 @@ describe Puppet::Type.type(:file) do catalog.add_resource described_class.new(:path => link, :target => link_target) catalog.apply - File.should be_exist link - File.lstat(link).ftype.should == 'link' - File.readlink(link).should == link_target + Puppet::FileSystem::File.exist?(link).should be_true + Puppet::FileSystem::File.new(link).lstat.ftype.should == 'link' + Puppet::FileSystem::File.new(link).readlink().should == link_target end end @@ -251,7 +262,7 @@ describe Puppet::Type.type(:file) do target = tmpfile('dangling') FileUtils.touch(target) - File.symlink(target, link) + Puppet::FileSystem::File.new(target).symlink(link) File.delete(target) catalog.add_resource described_class.new(:path => path, :source => link, :mode => 0600, :links => :follow) @@ -264,7 +275,7 @@ describe Puppet::Type.type(:file) do before :each do File.chmod(0600, link_target) - File.symlink(link_target, link) + Puppet::FileSystem::File.new(link_target).symlink(link) end after :each do @@ -327,7 +338,7 @@ describe Puppet::Type.type(:file) do before :each do FileUtils.touch(link_target) - File.symlink(link_target, link) + Puppet::FileSystem::File.new(link_target).symlink(link) end it "should create the file, not a symlink (#2817, #10315)" do @@ -357,8 +368,8 @@ describe Puppet::Type.type(:file) do File.chmod(0666, real_target) # link -> target -> real_target - File.symlink(real_target, target) - File.symlink(target, link) + Puppet::FileSystem::File.new(real_target).symlink(target) + Puppet::FileSystem::File.new(target).symlink(link) end after :each do @@ -397,13 +408,13 @@ describe Puppet::Type.type(:file) do catalog.add_resource file catalog.add_resource filebucket - File.open(file[:path], "wb") { |f| f.puts "bar" } + File.open(file[:path], "w") { |f| f.write("bar") } - md5 = Digest::MD5.hexdigest(IO.binread(file[:path])) + md5 = Digest::MD5.hexdigest("bar") catalog.apply - filebucket.bucket.getfile(md5).should == "bar\n" + filebucket.bucket.getfile(md5).should == "bar" end it "should backup files in the local directory when a backup string is provided" do @@ -415,7 +426,7 @@ describe Puppet::Type.type(:file) do catalog.apply backup = file[:path] + ".bak" - FileTest.should be_exist(backup) + Puppet::FileSystem::File.exist?(backup).should be_true File.read(backup).should == "bar\n" end @@ -437,7 +448,7 @@ describe Puppet::Type.type(:file) do File.read(file[:path]).should == "bar\n" end - it "should not backup symlinks", :unless => Puppet.features.microsoft_windows? do + it "should not backup symlinks", :if => described_class.defaultprovider.feature?(:manages_symlinks) do link = tmpfile("link") dest1 = tmpfile("dest1") dest2 = tmpfile("dest2") @@ -447,14 +458,14 @@ describe Puppet::Type.type(:file) do catalog.add_resource bucket File.open(dest1, "w") { |f| f.puts "whatever" } - File.symlink(dest1, link) + Puppet::FileSystem::File.new(dest1).symlink(link) md5 = Digest::MD5.hexdigest(File.read(file[:path])) catalog.apply - File.readlink(link).should == dest2 - File.exist?(bucket[:path]).should be_false + Puppet::FileSystem::File.new(link).readlink().should == dest2 + Puppet::FileSystem::File.exist?(bucket[:path]).should be_false end it "should backup directories to the local filesystem by copying the whole directory" do @@ -574,7 +585,7 @@ describe Puppet::Type.type(:file) do end end - it "should be able to recursively make links to other files", :unless => Puppet.features.microsoft_windows? do + it "should be able to recursively make links to other files", :if => described_class.defaultprovider.feature?(:manages_symlinks) do source = tmpfile("file_link_integration_source") build_path(source) @@ -590,13 +601,13 @@ describe Puppet::Type.type(:file) do @dirs.each do |path| link_path = path.sub(source, dest) - File.lstat(link_path).should be_directory + Puppet::FileSystem::File.new(link_path).lstat.should be_directory end @files.each do |path| link_path = path.sub(source, dest) - File.lstat(link_path).ftype.should == "link" + Puppet::FileSystem::File.new(link_path).lstat.ftype.should == "link" end end @@ -616,13 +627,13 @@ describe Puppet::Type.type(:file) do @dirs.each do |path| newpath = path.sub(source, dest) - File.lstat(newpath).should be_directory + Puppet::FileSystem::File.new(newpath).lstat.should be_directory end @files.each do |path| newpath = path.sub(source, dest) - File.lstat(newpath).ftype.should == "file" + Puppet::FileSystem::File.new(newpath).lstat.ftype.should == "file" end end @@ -685,8 +696,8 @@ describe Puppet::Type.type(:file) do catalog.apply File.should be_directory(path) - File.should_not be_exist(File.join(path, 'one')) - File.should be_exist(File.join(path, 'three', 'four')) + Puppet::FileSystem::File.exist?(File.join(path, 'one')).should be_false + Puppet::FileSystem::File.exist?(File.join(path, 'three', 'four')).should be_true end it "should recursively copy an empty directory" do @@ -707,7 +718,7 @@ describe Puppet::Type.type(:file) do catalog.apply File.should be_directory(path) - File.should_not be_exist(File.join(path, 'a')) + Puppet::FileSystem::File.exist?(File.join(path, 'a')).should be_false end it "should only recurse one level" do @@ -731,9 +742,9 @@ describe Puppet::Type.type(:file) do catalog.apply - File.should be_exist(File.join(path, 'a')) - File.should_not be_exist(File.join(path, 'a', 'b')) - File.should_not be_exist(File.join(path, 'z')) + Puppet::FileSystem::File.exist?(File.join(path, 'a')).should be_true + Puppet::FileSystem::File.exist?(File.join(path, 'a', 'b')).should be_false + Puppet::FileSystem::File.exist?(File.join(path, 'z')).should be_false end end @@ -830,10 +841,10 @@ describe Puppet::Type.type(:file) do catalog.add_resource obj catalog.apply - File.should be_exist(File.join(path, 'a')) - File.should_not be_exist(File.join(path, 'a', 'b')) - File.should be_exist(File.join(path, 'z')) - File.should_not be_exist(File.join(path, 'z', 'y')) + Puppet::FileSystem::File.exist?(File.join(path, 'a')).should be_true + Puppet::FileSystem::File.exist?(File.join(path, 'a', 'b')).should be_false + Puppet::FileSystem::File.exist?(File.join(path, 'z')).should be_true + Puppet::FileSystem::File.exist?(File.join(path, 'z', 'y')).should be_false end end end @@ -897,7 +908,7 @@ describe Puppet::Type.type(:file) do expected_mode = Puppet.features.microsoft_windows? ? 0644 : 0755 File.read(dest).should == "foo" - (File.stat(dest).mode & 007777).should == expected_mode + (Puppet::FileSystem::File.new(dest).stat.mode & 007777).should == expected_mode end it "should be able to copy individual files even if recurse has been specified" do @@ -949,7 +960,7 @@ describe Puppet::Type.type(:file) do catalog.add_resource file catalog.apply - File.should_not be_exist(dest) + Puppet::FileSystem::File.exist?(dest).should be_false end describe "when sourcing" do @@ -991,6 +1002,38 @@ describe Puppet::Type.type(:file) do end describe "on Windows systems", :if => Puppet.features.microsoft_windows? do + def expects_sid_granted_full_access_explicitly(path, sid) + inherited_ace = Windows::Security::INHERITED_ACE + + aces = get_aces_for_path_by_sid(path, sid) + aces.should_not be_empty + + aces.each do |ace| + ace.mask.should == Windows::File::FILE_ALL_ACCESS + (ace.flags & inherited_ace).should_not == inherited_ace + end + end + + def expects_system_granted_full_access_explicitly(path) + expects_sid_granted_full_access_explicitly(path, @sids[:system]) + end + + def expects_at_least_one_inherited_ace_grants_full_access(path, sid) + inherited_ace = Windows::Security::INHERITED_ACE + + aces = get_aces_for_path_by_sid(path, sid) + aces.should_not be_empty + + aces.any? do |ace| + ace.mask == Windows::File::FILE_ALL_ACCESS && + (ace.flags & inherited_ace) == inherited_ace + end.should be_true + end + + def expects_at_least_one_inherited_system_ace_grants_full_access(path) + expects_at_least_one_inherited_ace_grants_full_access(path, @sids[:system]) + end + it "should provide valid default values when ACLs are not supported" do Puppet::Util::Windows::Security.stubs(:supports_acl?).with(source).returns false @@ -1008,6 +1051,205 @@ describe Puppet::Type.type(:file) do get_group(path).should =~ /^S\-1\-0\-0.*$/ get_mode(path).should == 0644 end + + describe "when processing SYSTEM ACEs" do + before do + @sids = { + :current_user => Puppet::Util::Windows::Security.name_to_sid(Sys::Admin.get_login), + :system => Win32::Security::SID::LocalSystem, + :admin => Puppet::Util::Windows::Security.name_to_sid("Administrator"), + :guest => Puppet::Util::Windows::Security.name_to_sid("Guest"), + :users => Win32::Security::SID::BuiltinUsers, + :power_users => Win32::Security::SID::PowerUsers, + :none => Win32::Security::SID::Nobody + } + end + + describe "on files" do + before :each do + @file = described_class.new( + :path => path, + :ensure => :file, + :source => source, + :backup => false + ) + catalog.add_resource @file + end + + describe "when source permissions are ignored" do + before :each do + @file[:source_permissions] = :ignore + end + + it "preserves the inherited SYSTEM ACE" do + catalog.apply + + expects_at_least_one_inherited_system_ace_grants_full_access(path) + end + end + + describe "when permissions are insync?" do + it "preserves the explicit SYSTEM ACE" do + FileUtils.touch(path) + + sd = Puppet::Util::Windows::Security.get_security_descriptor(path) + sd.protect = true + sd.owner = @sids[:none] + sd.group = @sids[:none] + Puppet::Util::Windows::Security.set_security_descriptor(source, sd) + Puppet::Util::Windows::Security.set_security_descriptor(path, sd) + + catalog.apply + + expects_system_granted_full_access_explicitly(path) + end + end + + describe "when permissions are not insync?" do + before :each do + @file[:owner] = 'None' + @file[:group] = 'None' + end + + it "replaces inherited SYSTEM ACEs with an uninherited one for an existing file" do + FileUtils.touch(path) + + expects_at_least_one_inherited_system_ace_grants_full_access(path) + + catalog.apply + + expects_system_granted_full_access_explicitly(path) + end + + it "replaces inherited SYSTEM ACEs for a new file with an uninherited one" do + catalog.apply + + expects_system_granted_full_access_explicitly(path) + end + end + + describe "created with SYSTEM as the group" do + before :each do + @file[:owner] = @sids[:users] + @file[:group] = @sids[:system] + @file[:mode] = 0644 + + catalog.apply + end + + it "should allow the user to explicitly set the mode to 4" do + system_aces = get_aces_for_path_by_sid(path, @sids[:system]) + system_aces.should_not be_empty + + system_aces.each do |ace| + ace.mask.should == Windows::File::FILE_GENERIC_READ + end + end + + it "prepends SYSTEM ace when changing group from system to power users" do + @file[:group] = @sids[:power_users] + catalog.apply + + system_aces = get_aces_for_path_by_sid(path, @sids[:system]) + system_aces.size.should == 1 + end + end + end + + describe "on directories" do + before :each do + @directory = described_class.new( + :path => dir, + :ensure => :directory + ) + catalog.add_resource @directory + end + + describe "when source permissions are ignored" do + before :each do + @directory[:source_permissions] = :ignore + end + + it "preserves the inherited SYSTEM ACE" do + catalog.apply + + expects_at_least_one_inherited_system_ace_grants_full_access(dir) + end + end + + describe "when permissions are insync?" do + it "preserves the explicit SYSTEM ACE" do + Dir.mkdir(dir) + + source_dir = tmpdir('source_dir') + @directory[:source] = source_dir + + sd = Puppet::Util::Windows::Security.get_security_descriptor(source_dir) + sd.protect = true + sd.owner = @sids[:none] + sd.group = @sids[:none] + Puppet::Util::Windows::Security.set_security_descriptor(source_dir, sd) + Puppet::Util::Windows::Security.set_security_descriptor(dir, sd) + + catalog.apply + + expects_system_granted_full_access_explicitly(dir) + end + end + + describe "when permissions are not insync?" do + before :each do + @directory[:owner] = 'None' + @directory[:group] = 'None' + @directory[:mode] = 0444 + end + + it "replaces inherited SYSTEM ACEs with an uninherited one for an existing directory" do + FileUtils.mkdir(dir) + + expects_at_least_one_inherited_system_ace_grants_full_access(dir) + + catalog.apply + + expects_system_granted_full_access_explicitly(dir) + end + + it "replaces inherited SYSTEM ACEs with an uninherited one for an existing directory" do + catalog.apply + + expects_system_granted_full_access_explicitly(dir) + end + + describe "created with SYSTEM as the group" do + before :each do + @directory[:owner] = @sids[:users] + @directory[:group] = @sids[:system] + @directory[:mode] = 0644 + + catalog.apply + end + + it "should allow the user to explicitly set the mode to 4" do + system_aces = get_aces_for_path_by_sid(dir, @sids[:system]) + system_aces.should_not be_empty + + system_aces.each do |ace| + # unlike files, Puppet sets execute bit on directories that are readable + ace.mask.should == Windows::File::FILE_GENERIC_READ | Windows::File::FILE_GENERIC_EXECUTE + end + end + + it "prepends SYSTEM ace when changing group from system to power users" do + @directory[:group] = @sids[:power_users] + catalog.apply + + system_aces = get_aces_for_path_by_sid(dir, @sids[:system]) + system_aces.size.should == 1 + end + end + end + end + end end end @@ -1056,7 +1298,7 @@ describe Puppet::Type.type(:file) do end it "should purge files that are neither remote nor otherwise managed" do - FileTest.should_not be_exist(@purgee) + Puppet::FileSystem::File.exist?(@purgee).should be_false end end diff --git a/spec/integration/type/tidy_spec.rb b/spec/integration/type/tidy_spec.rb index 54458c651..562ae17e3 100755 --- a/spec/integration/type/tidy_spec.rb +++ b/spec/integration/type/tidy_spec.rb @@ -12,12 +12,12 @@ describe Puppet::Type.type(:tidy) do end # Testing #355. - it "should be able to remove dead links", :unless => Puppet.features.microsoft_windows? do + it "should be able to remove dead links", :if => Puppet.features.manages_symlinks? do dir = tmpfile("tidy_link_testing") link = File.join(dir, "link") target = tmpfile("no_such_file_tidy_link_testing") Dir.mkdir(dir) - File.symlink(target, link) + Puppet::FileSystem::File.new(target).symlink(link) tidy = Puppet::Type.type(:tidy).new :path => dir, :recurse => true @@ -26,6 +26,6 @@ describe Puppet::Type.type(:tidy) do catalog.apply - FileTest.should_not be_symlink(link) + Puppet::FileSystem::File.new(link).symlink?.should be_false end end diff --git a/spec/integration/util/rdoc/parser_spec.rb b/spec/integration/util/rdoc/parser_spec.rb index 0a473d042..58c0a882d 100755 --- a/spec/integration/util/rdoc/parser_spec.rb +++ b/spec/integration/util/rdoc/parser_spec.rb @@ -1,60 +1,261 @@ #! /usr/bin/env ruby require 'spec_helper' +require 'puppet/util/rdoc' -describe "RDoc::Parser", :if => Puppet.features.rdoc1? do +describe "RDoc::Parser" do require 'puppet_spec/files' include PuppetSpec::Files - before :all do - require 'puppet/resource/type_collection' - require 'puppet/util/rdoc/parser' - require 'puppet/util/rdoc' - require 'puppet/util/rdoc/code_objects' - require 'rdoc/options' - require 'rdoc/rdoc' + let(:document_all) { false } + let(:tmp_dir) { tmpdir('rdoc_parser_tmp') } + let(:doc_dir) { File.join(tmp_dir, 'doc') } + let(:manifests_dir) { File.join(tmp_dir, 'manifests') } + let(:modules_dir) { File.join(tmp_dir, 'modules') } + + let(:modules_and_manifests) do + { + :site => [ + File.join(manifests_dir, 'site.pp'), + <<-EOF +# The test class comment +class test { + # The virtual resource comment + @notify { virtual: } + # The a_notify_resource comment + notify { a_notify_resource: + message => "a_notify_resource message" + } +} + +# The includes_another class comment +class includes_another { + include another +} + +# The requires_another class comment +class requires_another { + require another +} + +# node comment +node foo { + include test + $a_var = "var_value" + realize Notify[virtual] + notify { bar: } +} + EOF + ], + :module_readme => [ + File.join(modules_dir, 'a_module', 'README'), + <<-EOF +The a_module README docs. + EOF + ], + :module_init => [ + File.join(modules_dir, 'a_module', 'manifests', 'init.pp'), + <<-EOF +# The a_module class comment +class a_module {} + +class another {} + EOF + ], + :module_type => [ + File.join(modules_dir, 'a_module', 'manifests', 'a_type.pp'), + <<-EOF +# The a_type type comment +define a_module::a_type() {} + EOF + ], + :module_plugin => [ + File.join(modules_dir, 'a_module', 'lib', 'puppet', 'type', 'a_plugin.rb'), + <<-EOF +# The a_plugin type comment +Puppet::Type.newtype(:a_plugin) do + @doc = "Not presented" +end + EOF + ], + :module_function => [ + File.join(modules_dir, 'a_module', 'lib', 'puppet', 'parser', 'a_function.rb'), + <<-EOF +# The a_function function comment +module Puppet::Parser::Functions + newfunction(:a_function, :type => :rvalue) do + return + end +end + EOF + ], + :module_fact => [ + File.join(modules_dir, 'a_module', 'lib', 'facter', 'a_fact.rb'), + <<-EOF +# The a_fact fact comment +Facter.add("a_fact") do +end + EOF + ], + } end - before :each do - tmpdir = tmpfile('rdoc_parser_tmp') - Dir.mkdir(tmpdir) - @parsedfile = File.join(tmpdir, 'init.pp') + def write_file(file, content) + FileUtils.mkdir_p(File.dirname(file)) + File.open(file, 'w') do |f| + f.puts(content) + end + end - File.open(@parsedfile, 'w') do |f| - f.puts '# comment' - f.puts 'class ::test {}' + def prepare_manifests_and_modules + modules_and_manifests.each do |key,array| + write_file(*array) end + end + + def file_exists_and_matches_content(file, *content_patterns) + Puppet::FileSystem::File.exist?(file).should(be_true, "Cannot find #{file}") + content_patterns.each do |pattern| + content = File.read(file) + content.should match(pattern) + end + end - @top_level = stub_everything 'toplevel', :file_relative_name => @parsedfile - @module = stub_everything 'module' - @puppet_top_level = RDoc::PuppetTopLevel.new(@top_level) - RDoc::PuppetTopLevel.stubs(:new).returns(@puppet_top_level) - @puppet_top_level.expects(:add_module).returns(@module) - @parser = RDoc::Parser.new(@top_level, @parsedfile, nil, Options.instance, RDoc::Stats.new) + def some_file_exists_with_matching_content(glob, *content_patterns) + Dir.glob(glob).select do |f| + contents = File.read(f) + content_patterns.all? { |p| p.match(contents) } + end.should_not(be_empty, "Could not match #{content_patterns} in any of the files found in #{glob}") end - after(:each) do - File.unlink(@parsedfile) + before :each do + prepare_manifests_and_modules + Puppet.settings[:document_all] = document_all + Puppet.settings[:modulepath] = modules_dir + Puppet::Util::RDoc.rdoc(doc_dir, [modules_dir, manifests_dir]) end - def get_test_class(toplevel) - # toplevel -> main -> test - toplevel.classes[0].classes[0] + module RdocTesters + def has_module_rdoc(module_name, *other_test_patterns) + file_exists_and_matches_content(module_path(module_name), /Module:? +#{module_name}/i, *other_test_patterns) + end + + def has_node_rdoc(module_name, node_name, *other_test_patterns) + file_exists_and_matches_content(node_path(module_name, node_name), /#{node_name}/, /node comment/, *other_test_patterns) + end + + def has_defined_type(module_name, type_name) + file_exists_and_matches_content(module_path(module_name), /#{type_name}.*?\(\s*\)/m, "The .*?#{type_name}.*? type comment") + end + + def has_class_rdoc(module_name, class_name, *other_test_patterns) + file_exists_and_matches_content(class_path(module_name, class_name), /#{class_name}.*? class comment/, *other_test_patterns) + end + + def has_plugin_rdoc(module_name, type, name) + file_exists_and_matches_content(plugin_path(module_name, type, name), /The .*?#{name}.*?\s*#{type} comment/m, /Type.*?#{type}/m) + end end - it "should parse to RDoc data structure" do - @parser.expects(:document_class).with { |n,k,c| n == "::test" and k.is_a?(Puppet::Resource::Type) } - @parser.scan + shared_examples_for :an_rdoc_site do + it "documents the __site__ module" do + has_module_rdoc("__site__") + end + + it "documents the __site__::test class" do + has_class_rdoc("__site__", "test") + end + + it "documents the __site__::foo node" do + has_node_rdoc("__site__", "foo") + end + + it "documents the a_module module" do + has_module_rdoc("a_module", /The .*?a_module.*? .*?README.*?docs/m) + end + + it "documents the a_module::a_module class" do + has_class_rdoc("a_module", "a_module") + end + + it "documents the a_module::a_type defined type" do + has_defined_type("a_module", "a_type") + end + + it "documents the a_module::a_plugin type" do + has_plugin_rdoc("a_module", :type, 'a_plugin') + end + + it "documents the a_module::a_function function" do + has_plugin_rdoc("a_module", :function, 'a_function') + end + + it "documents the a_module::a_fact fact" do + has_plugin_rdoc("a_module", :fact, 'a_fact') + end + + it "documents included classes" do + has_class_rdoc("__site__", "includes_another", /Included.*?another/m) + end end - it "should get a PuppetClass for the main class" do - @parser.scan.classes[0].should be_a(RDoc::PuppetClass) + shared_examples_for :an_rdoc1_site do + it "documents required classes" do + has_class_rdoc("__site__", "requires_another", /Required Classes.*?another/m) + end + + it "documents realized resources" do + has_node_rdoc("__site__", "foo", /Realized Resources.*?Notify\[virtual\]/m) + end + + it "documents global variables" do + has_node_rdoc("__site__", "foo", /Global Variables.*?a_var.*?=.*?var_value/m) + end + + describe "when document_all is true" do + let(:document_all) { true } + + it "documents virtual resource declarations" do + has_class_rdoc("__site__", "test", /Resources.*?Notify\[virtual\]/m, /The virtual resource comment/) + end + + it "documents resources" do + has_class_rdoc("__site__", "test", /Resources.*?Notify\[a_notify_resource\]/m, /message => "a_notify_resource message"/, /The a_notify_resource comment/) + end + end end - it "should produce a PuppetClass whose name is test" do - get_test_class(@parser.scan).name.should == "test" + describe "rdoc1 support", :if => Puppet.features.rdoc1? do + def module_path(module_name); "#{doc_dir}/classes/#{module_name}.html" end + def node_path(module_name, node_name); "#{doc_dir}/nodes/**/*.html" end + def class_path(module_name, class_name); "#{doc_dir}/classes/#{module_name}/#{class_name}.html" end + def plugin_path(module_name, type, name); "#{doc_dir}/plugins/#{name}.html" end + + include RdocTesters + + def has_node_rdoc(module_name, node_name, *other_test_patterns) + some_file_exists_with_matching_content(node_path(module_name, node_name), /#{node_name}/, /node comment/, *other_test_patterns) + end + + it_behaves_like :an_rdoc_site + it_behaves_like :an_rdoc1_site + + it "references nodes and classes in the __site__ module" do + file_exists_and_matches_content("#{doc_dir}/classes/__site__.html", /Node.*__site__::foo/, /Class.*__site__::test/) + end + + it "references functions, facts, and type plugins in the a_module module" do + file_exists_and_matches_content("#{doc_dir}/classes/a_module.html", /a_function/, /a_fact/, /a_plugin/, /Class.*a_module::a_module/) + end end - it "should produce a PuppetClass whose comment is 'comment'" do - get_test_class(@parser.scan).comment.should == "comment\n" + describe "rdoc2 support", :if => !Puppet.features.rdoc1? do + def module_path(module_name); "#{doc_dir}/#{module_name}.html" end + def node_path(module_name, node_name); "#{doc_dir}/#{module_name}/__nodes__/#{node_name}.html" end + def class_path(module_name, class_name); "#{doc_dir}/#{module_name}/#{class_name}.html" end + def plugin_path(module_name, type, name); "#{doc_dir}/#{module_name}/__#{type}s__.html" end + + include RdocTesters + + it_behaves_like :an_rdoc_site end end diff --git a/spec/integration/util/settings_spec.rb b/spec/integration/util/settings_spec.rb index 360b78310..0da0938e9 100755 --- a/spec/integration/util/settings_spec.rb +++ b/spec/integration/util/settings_spec.rb @@ -41,7 +41,7 @@ describe Puppet::Settings do settings.use(:main) - expect(File.stat(settings[:maindir]).mode & 007777).to eq(Puppet.features.microsoft_windows? ? 0755 : 0750) + expect(Puppet::FileSystem::File.new(settings[:maindir]).stat.mode & 007777).to eq(Puppet.features.microsoft_windows? ? 0755 : 0750) end it "reparses configuration if configuration file is touched", :if => !Puppet.features.microsoft_windows? do diff --git a/spec/integration/util/windows/process_spec.rb b/spec/integration/util/windows/process_spec.rb new file mode 100644 index 000000000..6dc54d228 --- /dev/null +++ b/spec/integration/util/windows/process_spec.rb @@ -0,0 +1,22 @@ +#! /usr/bin/env ruby + +require 'spec_helper' +require 'facter' + +describe "Puppet::Util::Windows::Process", :if => Puppet.features.microsoft_windows? do + describe "as an admin" do + it "should have the SeCreateSymbolicLinkPrivilege necessary to create symlinks on Vista / 2008+", + :if => Facter.value(:kernelmajversion).to_f >= 6.0 && Puppet.features.microsoft_windows? do + # this is a bit of a lame duck test since it requires running user to be admin + # a better integration test would create a new user with the privilege and verify + Puppet::Util::Windows::User.should be_admin + Puppet::Util::Windows::Process.process_privilege_symlink?.should be_true + end + + it "should not have the SeCreateSymbolicLinkPrivilege necessary to create symlinks on 2003 and earlier", + :if => Facter.value(:kernelmajversion).to_f < 6.0 && Puppet.features.microsoft_windows? do + Puppet::Util::Windows::User.should be_admin + Puppet::Util::Windows::Process.process_privilege_symlink?.should be_false + end + end +end diff --git a/spec/integration/util/windows/security_spec.rb b/spec/integration/util/windows/security_spec.rb index c2bd538ea..99d866f7f 100755 --- a/spec/integration/util/windows/security_spec.rb +++ b/spec/integration/util/windows/security_spec.rb @@ -16,31 +16,64 @@ describe "Puppet::Util::Windows::Security", :if => Puppet.features.microsoft_win before :all do @sids = { :current_user => Puppet::Util::Windows::Security.name_to_sid(Sys::Admin.get_login), + :system => Win32::Security::SID::LocalSystem, :admin => Puppet::Util::Windows::Security.name_to_sid("Administrator"), + :administrators => Win32::Security::SID::BuiltinAdministrators, :guest => Puppet::Util::Windows::Security.name_to_sid("Guest"), :users => Win32::Security::SID::BuiltinUsers, :power_users => Win32::Security::SID::PowerUsers, + :none => Win32::Security::SID::Nobody, + :everyone => Win32::Security::SID::Everyone } end let (:sids) { @sids } let (:winsec) { WindowsSecurityTester.new } + def set_group_depending_on_current_user(path) + if sids[:current_user] == sids[:system] + # if the current user is SYSTEM, by setting the group to + # guest, SYSTEM is automagically given full control, so instead + # override that behavior with SYSTEM as group and a specific mode + winsec.set_group(sids[:system], path) + mode = winsec.get_mode(path) + winsec.set_mode(mode & ~WindowsSecurityTester::S_IRWXG, path) + else + winsec.set_group(sids[:guest], path) + end + end + shared_examples_for "only child owner" do it "should allow child owner" do - check_child_owner + winsec.set_owner(sids[:guest], parent) + winsec.set_group(sids[:current_user], parent) + winsec.set_mode(0700, parent) + + check_delete(path) end it "should deny parent owner" do - lambda { check_parent_owner }.should raise_error(Errno::EACCES) + winsec.set_owner(sids[:guest], path) + winsec.set_group(sids[:current_user], path) + winsec.set_mode(0700, path) + + lambda { check_delete(path) }.should raise_error(Errno::EACCES) end it "should deny group" do - lambda { check_group }.should raise_error(Errno::EACCES) + winsec.set_owner(sids[:guest], path) + winsec.set_group(sids[:current_user], path) + winsec.set_mode(0700, path) + + lambda { check_delete(path) }.should raise_error(Errno::EACCES) end it "should deny other" do - lambda { check_other }.should raise_error(Errno::EACCES) + winsec.set_owner(sids[:guest], path) + winsec.set_group(sids[:current_user], path) + winsec.set_mode(0700, path) + + lambda { check_delete(path) }.should raise_error(Errno::EACCES) end end @@ -63,7 +96,7 @@ describe "Puppet::Util::Windows::Security", :if => Puppet.features.microsoft_win after :each do winsec.set_mode(WindowsSecurityTester::S_IRWXU, parent) - winsec.set_mode(WindowsSecurityTester::S_IRWXU, path) if File.exists?(path) + winsec.set_mode(WindowsSecurityTester::S_IRWXU, path) if Puppet::FileSystem::File.exist?(path) end describe "#supports_acl?" do @@ -122,6 +155,26 @@ describe "Puppet::Util::Windows::Security", :if => Puppet.features.microsoft_win end end + it "should preserve inherited full control for SYSTEM when setting owner and group" do + # new file has SYSTEM + system_aces = winsec.get_aces_for_path_by_sid(path, sids[:system]) + system_aces.should_not be_empty + + # when running under SYSTEM account, multiple ACEs come back + # so we only care that we have at least one of these + system_aces.any? do |ace| + ace.mask == Windows::File::FILE_ALL_ACCESS + end.should be_true + + # changing the owner/group will no longer make the SD protected + winsec.set_group(sids[:power_users], path) + winsec.set_owner(sids[:administrators], path) + + system_aces.find do |ace| + ace.mask == Windows::File::FILE_ALL_ACCESS && ace.inherited? + end.should_not be_nil + end + describe "#mode=" do (0000..0700).step(0100) do |mode| it "should enforce mode #{mode.to_s(8)}" do @@ -151,6 +204,28 @@ describe "Puppet::Util::Windows::Security", :if => Puppet.features.microsoft_win end end + it "should preserve full control for SYSTEM when setting mode" do + # new file has SYSTEM + system_aces = winsec.get_aces_for_path_by_sid(path, sids[:system]) + system_aces.should_not be_empty + + # when running under SYSTEM account, multiple ACEs come back + # so we only care that we have at least one of these + system_aces.any? do |ace| + ace.mask == WindowsSecurityTester::FILE_ALL_ACCESS + end.should be_true + + # changing the mode will make the SD protected + winsec.set_group(sids[:none], path) + winsec.set_mode(0600, path) + + # and should have a non-inherited SYSTEM ACE(s) + system_aces = winsec.get_aces_for_path_by_sid(path, sids[:system]) + system_aces.each do |ace| + ace.mask.should == Windows::File::FILE_ALL_ACCESS && ! ace.inherited? + end + end + describe "for modes that require deny aces" do it "should map everyone to group and owner" do winsec.set_mode(0426, path) @@ -167,6 +242,8 @@ describe "Puppet::Util::Windows::Security", :if => Puppet.features.microsoft_win describe "for read-only objects" do before :each do + winsec.set_group(sids[:none], path) + winsec.set_mode(0600, path) winsec.add_attributes(path, WindowsSecurityTester::FILE_ATTRIBUTE_READONLY) (winsec.get_attributes(path) & WindowsSecurityTester::FILE_ATTRIBUTE_READONLY).should be_nonzero end @@ -176,9 +253,17 @@ describe "Puppet::Util::Windows::Security", :if => Puppet.features.microsoft_win (winsec.get_attributes(path) & WindowsSecurityTester::FILE_ATTRIBUTE_READONLY).should == 0 end - it "should leave them read-only if no sid has write permission" do + it "should leave them read-only if no sid has write permission and should allow full access for SYSTEM" do winsec.set_mode(WindowsSecurityTester::S_IRUSR | WindowsSecurityTester::S_IXGRP, path) (winsec.get_attributes(path) & WindowsSecurityTester::FILE_ATTRIBUTE_READONLY).should be_nonzero + + system_aces = winsec.get_aces_for_path_by_sid(path, sids[:system]) + + # when running under SYSTEM account, and set_group / set_owner hasn't been called + # SYSTEM full access will be restored + system_aces.any? do |ace| + ace.mask == Windows::File::FILE_ALL_ACCESS + end.should be_true end end @@ -189,31 +274,39 @@ describe "Puppet::Util::Windows::Security", :if => Puppet.features.microsoft_win describe "#mode" do it "should report when extra aces are encounted" do - winsec.set_acl(path, true) do |acl| - (544..547).each do |rid| - winsec.add_access_allowed_ace(acl, WindowsSecurityTester::STANDARD_RIGHTS_ALL, "S-1-5-32-#{rid}") - end + sd = winsec.get_security_descriptor(path) + (544..547).each do |rid| + sd.dacl.allow("S-1-5-32-#{rid}", WindowsSecurityTester::STANDARD_RIGHTS_ALL) end + winsec.set_security_descriptor(path, sd) + mode = winsec.get_mode(path) - (mode & WindowsSecurityTester::S_IEXTRA).should_not == 0 + (mode & WindowsSecurityTester::S_IEXTRA).should == WindowsSecurityTester::S_IEXTRA end - it "should warn if a deny ace is encountered" do - winsec.set_acl(path) do |acl| - winsec.add_access_denied_ace(acl, WindowsSecurityTester::FILE_GENERIC_WRITE, sids[:guest]) - winsec.add_access_allowed_ace(acl, WindowsSecurityTester::STANDARD_RIGHTS_ALL | WindowsSecurityTester::SPECIFIC_RIGHTS_ALL, sids[:current_user]) - end - - Puppet.expects(:warning).with("Unsupported access control entry type: 0x1") + it "should return deny aces" do + sd = winsec.get_security_descriptor(path) + sd.dacl.deny(sids[:guest], WindowsSecurityTester::FILE_GENERIC_WRITE) + winsec.set_security_descriptor(path, sd) - winsec.get_mode(path) + guest_aces = winsec.get_aces_for_path_by_sid(path, sids[:guest]) + guest_aces.find do |ace| + ace.type == WindowsSecurityTester::ACCESS_DENIED_ACE_TYPE + end.should_not be_nil end it "should skip inherit-only ace" do - winsec.set_acl(path) do |acl| - winsec.add_access_allowed_ace(acl, WindowsSecurityTester::STANDARD_RIGHTS_ALL | WindowsSecurityTester::SPECIFIC_RIGHTS_ALL, sids[:current_user]) - winsec.add_access_allowed_ace(acl, WindowsSecurityTester::FILE_GENERIC_READ, Win32::Security::SID::Everyone, WindowsSecurityTester::INHERIT_ONLY_ACE | WindowsSecurityTester::OBJECT_INHERIT_ACE) - end + sd = winsec.get_security_descriptor(path) + dacl = Puppet::Util::Windows::AccessControlList.new + dacl.allow( + sids[:current_user], WindowsSecurityTester::STANDARD_RIGHTS_ALL | WindowsSecurityTester::SPECIFIC_RIGHTS_ALL + ) + dacl.allow( + sids[:everyone], + WindowsSecurityTester::FILE_GENERIC_READ, + WindowsSecurityTester::INHERIT_ONLY_ACE | WindowsSecurityTester::OBJECT_INHERIT_ACE + ) + winsec.set_security_descriptor(path, sd) (winsec.get_mode(path) & WindowsSecurityTester::S_IRWXO).should == 0 end @@ -224,9 +317,14 @@ describe "Puppet::Util::Windows::Security", :if => Puppet.features.microsoft_win end describe "inherited access control entries" do - it "should be absent when the access control list is protected" do + it "should be absent when the access control list is protected, and should not remove SYSTEM" do winsec.set_mode(WindowsSecurityTester::S_IRWXU, path) - (winsec.get_mode(path) & WindowsSecurityTester::S_IEXTRA).should == 0 + + mode = winsec.get_mode(path) + [ WindowsSecurityTester::S_IEXTRA, + WindowsSecurityTester::S_ISYSTEM_MISSING ].each do |flag| + (mode & flag).should_not == flag + end end it "should be present when the access control list is unprotected" do @@ -234,13 +332,20 @@ describe "Puppet::Util::Windows::Security", :if => Puppet.features.microsoft_win allow = WindowsSecurityTester::STANDARD_RIGHTS_ALL | WindowsSecurityTester::SPECIFIC_RIGHTS_ALL inherit = WindowsSecurityTester::OBJECT_INHERIT_ACE | WindowsSecurityTester::CONTAINER_INHERIT_ACE - winsec.set_acl(parent, true) do |acl| - winsec.add_access_allowed_ace(acl, allow, "S-1-1-0", inherit) # everyone - - (544..547).each do |rid| - winsec.add_access_allowed_ace(acl, WindowsSecurityTester::STANDARD_RIGHTS_ALL, "S-1-5-32-#{rid}", inherit) - end + sd = winsec.get_security_descriptor(parent) + sd.dacl.allow( + "S-1-1-0", #everyone + allow, + inherit + ) + (544..547).each do |rid| + sd.dacl.allow( + "S-1-5-32-#{rid}", + WindowsSecurityTester::STANDARD_RIGHTS_ALL, + inherit + ) end + winsec.set_security_descriptor(parent, sd) # unprotect child, it should inherit from parent winsec.set_mode(WindowsSecurityTester::S_IRWXU, path, false) @@ -252,13 +357,13 @@ describe "Puppet::Util::Windows::Security", :if => Puppet.features.microsoft_win describe "for an administrator", :if => Puppet.features.root? do before :each do winsec.set_mode(WindowsSecurityTester::S_IRWXU | WindowsSecurityTester::S_IRWXG, path) - winsec.set_group(sids[:guest], path) + set_group_depending_on_current_user(path) winsec.set_owner(sids[:guest], path) lambda { File.open(path, 'r') }.should raise_error(Errno::EACCES) end after :each do - if File.exists?(path) + if Puppet::FileSystem::File.exist?(path) winsec.set_owner(sids[:current_user], path) winsec.set_mode(WindowsSecurityTester::S_IRWXU, path) end @@ -295,16 +400,18 @@ describe "Puppet::Util::Windows::Security", :if => Puppet.features.microsoft_win winsec.get_group(path).should == sids[:admin] end - it "should allow owner and group to be the same sid" do - winsec.set_mode(0610, path) + it "should combine owner and group rights when they are the same sid" do winsec.set_owner(sids[:power_users], path) winsec.set_group(sids[:power_users], path) + winsec.set_mode(0610, path) winsec.get_owner(path).should == sids[:power_users] winsec.get_group(path).should == sids[:power_users] # note group execute permission added to user ace, and then group rwx value # reflected to match - winsec.get_mode(path).to_s(8).should == "770" + + # Exclude missing system ace, since that's not relevant + (winsec.get_mode(path) & 0777).to_s(8).should == "770" end it "should raise an exception if an invalid sid is provided" do @@ -355,10 +462,14 @@ describe "Puppet::Util::Windows::Security", :if => Puppet.features.microsoft_win end describe "#mode" do - it "should deny all access when the DACL is empty" do - winsec.set_acl(path, true) { |acl| } + it "should deny all access when the DACL is empty, including SYSTEM" do + sd = winsec.get_security_descriptor(path) + # don't allow inherited aces to affect the test + protect = true + new_sd = Puppet::Util::Windows::SecurityDescriptor.new(sd.owner, sd.group, [], protect) + winsec.set_security_descriptor(path, new_sd) - winsec.get_mode(path).should == 0 + winsec.get_mode(path).should == WindowsSecurityTester::S_ISYSTEM_MISSING end # REMIND: ruby crashes when trying to set a NULL DACL @@ -378,103 +489,121 @@ describe "Puppet::Util::Windows::Security", :if => Puppet.features.microsoft_win winsec.set_mode(0777, path, false) end - def check_child_owner - winsec.set_group(sids[:guest], parent) - winsec.set_owner(sids[:guest], parent) + describe "is writable and executable" do + describe "and sticky bit is set" do + it "should allow child owner" do + winsec.set_owner(sids[:guest], parent) + winsec.set_group(sids[:current_user], parent) + winsec.set_mode(01700, parent) - check_delete(path) - end + check_delete(path) + end - def check_parent_owner - winsec.set_group(sids[:guest], path) - winsec.set_owner(sids[:guest], path) + it "should allow parent owner" do + winsec.set_owner(sids[:current_user], parent) + winsec.set_group(sids[:guest], parent) + winsec.set_mode(01700, parent) - check_delete(path) - end + winsec.set_owner(sids[:current_user], path) + winsec.set_group(sids[:guest], path) + winsec.set_mode(0700, path) - def check_group - winsec.set_group(sids[:current_user], path) - winsec.set_owner(sids[:guest], path) + check_delete(path) + end - winsec.set_owner(sids[:guest], parent) + it "should deny group" do + winsec.set_owner(sids[:guest], parent) + winsec.set_group(sids[:current_user], parent) + winsec.set_mode(01770, parent) - check_delete(path) - end + winsec.set_owner(sids[:guest], path) + winsec.set_group(sids[:current_user], path) + winsec.set_mode(0700, path) - def check_other - winsec.set_group(sids[:guest], path) - winsec.set_owner(sids[:guest], path) + lambda { check_delete(path) }.should raise_error(Errno::EACCES) + end - winsec.set_owner(sids[:guest], parent) + it "should deny other" do + winsec.set_owner(sids[:guest], parent) + winsec.set_group(sids[:current_user], parent) + winsec.set_mode(01777, parent) - check_delete(path) - end + winsec.set_owner(sids[:guest], path) + winsec.set_group(sids[:current_user], path) + winsec.set_mode(0700, path) - describe "is writable and executable" do - describe "and sticky bit is set" do - before :each do - winsec.set_mode(01777, parent) + lambda { check_delete(path) }.should raise_error(Errno::EACCES) + end end - it "should allow child owner" do - check_child_owner - end + describe "and sticky bit is not set" do + it "should allow child owner" do + winsec.set_owner(sids[:guest], parent) + winsec.set_group(sids[:current_user], parent) + winsec.set_mode(0700, parent) - it "should allow parent owner" do - check_parent_owner - end + check_delete(path) + end - it "should deny group" do - lambda { check_group }.should raise_error(Errno::EACCES) - end + it "should allow parent owner" do + winsec.set_owner(sids[:current_user], parent) + winsec.set_group(sids[:guest], parent) + winsec.set_mode(0700, parent) - it "should deny other" do - lambda { check_other }.should raise_error(Errno::EACCES) - end - end + winsec.set_owner(sids[:current_user], path) + winsec.set_group(sids[:guest], path) + winsec.set_mode(0700, path) - describe "and sticky bit is not set" do - before :each do - winsec.set_mode(0777, parent) - end + check_delete(path) + end - it "should allow child owner" do - check_child_owner - end + it "should allow group" do + winsec.set_owner(sids[:guest], parent) + winsec.set_group(sids[:current_user], parent) + winsec.set_mode(0770, parent) - it "should allow parent owner" do - check_parent_owner - end + winsec.set_owner(sids[:guest], path) + winsec.set_group(sids[:current_user], path) + winsec.set_mode(0700, path) - it "should allow group" do - check_group - end + check_delete(path) + end - it "should allow other" do - check_other + it "should allow other" do + winsec.set_owner(sids[:guest], parent) + winsec.set_group(sids[:current_user], parent) + winsec.set_mode(0777, parent) + + winsec.set_owner(sids[:guest], path) + winsec.set_group(sids[:current_user], path) + winsec.set_mode(0700, path) + + check_delete(path) + end end end - end - describe "is not writable" do - before :each do - winsec.set_mode(0555, parent) + describe "is not writable" do + before :each do + winsec.set_group(sids[:current_user], parent) + winsec.set_mode(0555, parent) + end + + it_behaves_like "only child owner" end - it_behaves_like "only child owner" - end + describe "is not executable" do + before :each do + winsec.set_group(sids[:current_user], parent) + winsec.set_mode(0666, parent) + end - describe "is not executable" do - before :each do - winsec.set_mode(0666, parent) + it_behaves_like "only child owner" end - - it_behaves_like "only child owner" end end end end - end describe "file" do let (:parent) do @@ -603,9 +732,90 @@ describe "Puppet::Util::Windows::Security", :if => Puppet.features.microsoft_win Dir.mkdir(newdir) [newfile, newdir].each do |p| - winsec.get_mode(p).to_s(8).should == mode640.to_s(8) + mode = winsec.get_mode(p) + (mode & 07777).to_s(8).should == mode640.to_s(8) end end end end + + context "security descriptor" do + let(:path) { tmpfile('sec_descriptor') } + let(:read_execute) { 0x201FF } + let(:synchronize) { 0x100000 } + + before :each do + FileUtils.touch(path) + end + + it "preserves aces for other users" do + dacl = Puppet::Util::Windows::AccessControlList.new + sids_in_dacl = [sids[:current_user], sids[:users]] + sids_in_dacl.each do |sid| + dacl.allow(sid, read_execute) + end + sd = Puppet::Util::Windows::SecurityDescriptor.new(sids[:guest], sids[:guest], dacl, true) + winsec.set_security_descriptor(path, sd) + + aces = winsec.get_security_descriptor(path).dacl.to_a + aces.map(&:sid).should == sids_in_dacl + aces.map(&:mask).all? { |mask| mask == read_execute }.should be_true + end + + it "changes the sid for all aces that were assigned to the old owner" do + sd = winsec.get_security_descriptor(path) + sd.owner.should_not == sids[:guest] + + sd.dacl.allow(sd.owner, read_execute) + sd.dacl.allow(sd.owner, synchronize) + + sd.owner = sids[:guest] + winsec.set_security_descriptor(path, sd) + + dacl = winsec.get_security_descriptor(path).dacl + aces = dacl.find_all { |ace| ace.sid == sids[:guest] } + # only non-inherited aces will be reassigned to guest, so + # make sure we find at least the two we added + aces.size.should >= 2 + end + + it "preserves INHERIT_ONLY_ACEs" do + # inherit only aces can only be set on directories + dir = tmpdir('inheritonlyace') + + inherit_flags = Puppet::Util::Windows::AccessControlEntry::INHERIT_ONLY_ACE | + Puppet::Util::Windows::AccessControlEntry::OBJECT_INHERIT_ACE | + Puppet::Util::Windows::AccessControlEntry::CONTAINER_INHERIT_ACE + + sd = winsec.get_security_descriptor(dir) + sd.dacl.allow(sd.owner, Windows::File::FILE_ALL_ACCESS, inherit_flags) + winsec.set_security_descriptor(dir, sd) + + sd = winsec.get_security_descriptor(dir) + + winsec.set_owner(sids[:guest], dir) + + sd = winsec.get_security_descriptor(dir) + sd.dacl.find do |ace| + ace.sid == sids[:guest] && ace.inherit_only? + end.should_not be_nil + end + + context "when managing mode" do + it "removes aces for sids that are neither the owner nor group" do + # add a guest ace, it's never owner or group + sd = winsec.get_security_descriptor(path) + sd.dacl.allow(sids[:guest], read_execute) + winsec.set_security_descriptor(path, sd) + + # setting the mode, it should remove extra aces + winsec.set_mode(0770, path) + + # make sure it's gone + dacl = winsec.get_security_descriptor(path).dacl + aces = dacl.find_all { |ace| ace.sid == sids[:guest] } + aces.should be_empty + end + end + end end diff --git a/spec/lib/matchers/containment_matchers.rb b/spec/lib/matchers/containment_matchers.rb new file mode 100644 index 000000000..bc412fc5e --- /dev/null +++ b/spec/lib/matchers/containment_matchers.rb @@ -0,0 +1,52 @@ +module ContainmentMatchers + class ContainClass + def initialize(containee) + @containee = containee + end + + def in(container) + @container = container + self + end + + def matches?(catalog) + @catalog = catalog + + raise ArgumentError, "You must set the container using #in" unless @container + + @container_resource = catalog.resource("Class", @container) + @containee_resource = catalog.resource("Class", @containee) + + if @containee_resource && @container_resource + catalog.edge?(@container_resource, @containee_resource) + else + false + end + end + + def failure_message_for_should + message = "Expected #{@catalog.to_dot} to contain Class #{@containee.inspect} inside of Class #{@container.inspect} but " + + missing = [] + if @container_resource.nil? + missing << @container + end + if @containee_resource.nil? + missing << @containee + end + + if ! missing.empty? + message << "the catalog does not contain #{missing.map(&:inspect).join(' or ')}" + else + message << "no containment relationship exists" + end + + message + end + end + + # expect(catalog).to contain_class(containee).in(container) + def contain_class(containee) + ContainClass.new(containee) + end +end diff --git a/spec/lib/puppet_spec/compiler.rb b/spec/lib/puppet_spec/compiler.rb index 0286fbc8e..cf4851807 100644 --- a/spec/lib/puppet_spec/compiler.rb +++ b/spec/lib/puppet_spec/compiler.rb @@ -27,4 +27,10 @@ module PuppetSpec::Compiler transaction end + + def order_resources_traversed_in(relationships) + order_seen = [] + relationships.traverse { |resource| order_seen << resource.ref } + order_seen + end end diff --git a/spec/lib/puppet_spec/files.rb b/spec/lib/puppet_spec/files.rb index 196d4b9c4..afe86fba2 100755 --- a/spec/lib/puppet_spec/files.rb +++ b/spec/lib/puppet_spec/files.rb @@ -1,26 +1,13 @@ require 'fileutils' require 'tempfile' +require 'tmpdir' require 'pathname' # A support module for testing files. module PuppetSpec::Files - # This code exists only to support tests that run as root, pretty much. - # Once they have finally been eliminated this can all go... --daniel 2011-04-08 - def self.in_tmp(path) - tempdir = Dir.tmpdir - - Pathname.new(path).ascend do |dir| - return true if File.identical?(tempdir, dir) - end - - false - end - def self.cleanup $global_tempfiles ||= [] while path = $global_tempfiles.pop do - fail "Not deleting tmpfile #{path} outside regular tmpdir" unless in_tmp(path) - begin FileUtils.rm_rf path, :secure => true rescue Errno::ENOENT @@ -43,18 +30,30 @@ module PuppetSpec::Files path = source.path source.close! - # ...record it for cleanup, - $global_tempfiles ||= [] - $global_tempfiles << File.expand_path(path) + record_tmp(File.expand_path(path)) - # ...and bam. path end + def file_containing(name, contents) PuppetSpec::Files.file_containing(name, contents) end + def self.file_containing(name, contents) + file = tmpfile(name) + File.open(file, 'wb') { |f| f.write(contents) } + file + end + def tmpdir(name) PuppetSpec::Files.tmpdir(name) end def self.tmpdir(name) - path = tmpfile(name) - FileUtils.mkdir_p(path) - path + dir = Dir.mktmpdir(name) + + record_tmp(dir) + + dir + end + + def self.record_tmp(tmp) + # ...record it for cleanup, + $global_tempfiles ||= [] + $global_tempfiles << tmp end end diff --git a/spec/shared_behaviours/documentation_on_faces.rb b/spec/shared_behaviours/documentation_on_faces.rb index 204b173a5..7c4d9e04b 100644 --- a/spec/shared_behaviours/documentation_on_faces.rb +++ b/spec/shared_behaviours/documentation_on_faces.rb @@ -62,7 +62,7 @@ shared_examples_for "documentation on faces" do end it "should not remove formatting whitespace, only global indent" do - text = "this\n is\n the\n ultimate\ntest\n" + text = "this\n is\n the\n ultimate\ntest" subject.send(setter, text.gsub(/^/, indent)) subject.send(getter).should == text end @@ -70,8 +70,8 @@ shared_examples_for "documentation on faces" do end it "should strip whitespace with a blank line" do - subject.send(setter, " this\n\n should outdent\n") - subject.send(getter).should == "this\n\nshould outdent\n" + subject.send(setter, " this\n\n should outdent") + subject.send(getter).should == "this\n\nshould outdent" end end end diff --git a/spec/shared_behaviours/file_server_terminus.rb b/spec/shared_behaviours/file_server_terminus.rb index 8266a75d9..ff122f8ad 100755 --- a/spec/shared_behaviours/file_server_terminus.rb +++ b/spec/shared_behaviours/file_server_terminus.rb @@ -4,8 +4,8 @@ shared_examples_for "Puppet::Indirector::FileServerTerminus" do # the 'before' block in the including context. before do Puppet::FileServing::Configuration.instance_variable_set(:@configuration, nil) - FileTest.stubs(:exists?).returns true - FileTest.stubs(:exists?).with(Puppet[:fileserverconfig]).returns(true) + Puppet::FileSystem::File.stubs(:exist?).returns true + Puppet::FileSystem::File.stubs(:exist?).with(Puppet[:fileserverconfig]).returns(true) @path = Tempfile.new("file_server_testing") path = @path.path diff --git a/spec/shared_contexts/platform.rb b/spec/shared_contexts/platform.rb index 762eddfbd..2e6d4d7a6 100644 --- a/spec/shared_contexts/platform.rb +++ b/spec/shared_contexts/platform.rb @@ -6,6 +6,7 @@ shared_context "windows", :as_platform => :windows do before :each do Facter.stubs(:value).with(:operatingsystem).returns 'Windows' + Facter.stubs(:value).with(:osfamily).returns 'windows' Puppet.features.stubs(:microsoft_windows?).returns(true) Puppet.features.stubs(:posix?).returns(false) end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 7b4bd7219..49fa482cf 100755 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -43,6 +43,18 @@ Pathname.glob("#{dir}/shared_behaviours/**/*.rb") do |behaviour| require behaviour.relative_path_from(Pathname.new(dir)) end +# various spec tests now use json schema validation +# the json-schema gem doesn't support windows +if not Puppet.features.microsoft_windows? + require 'json' + require 'json-schema' + + JSON_META_SCHEMA = JSON.parse(File.read(File.join(File.dirname(__FILE__), '../api/schemas/json-meta-schema.json'))) + + # FACTS_SCHEMA is shared across two spec files so promote constant to here + FACTS_SCHEMA = JSON.parse(File.read(File.join(File.dirname(__FILE__), '../api/schemas/facts.json'))) +end + RSpec.configure do |config| include PuppetSpec::Fixtures @@ -146,6 +158,6 @@ RSpec.configure do |config| # Clean up switch of TMPDIR, don't know if needed after this, so needs to reset it # to old before removing it ENV['TMPDIR'] = oldtmpdir - FileUtils.rm_rf(tmpdir) if File.exists?(tmpdir) && tmpdir.to_s.start_with?(oldtmpdir) + FileUtils.rm_rf(tmpdir) if Puppet::FileSystem::File.exist?(tmpdir) && tmpdir.to_s.start_with?(oldtmpdir) end end diff --git a/spec/unit/agent_spec.rb b/spec/unit/agent_spec.rb index 536fb47de..38e53176d 100755 --- a/spec/unit/agent_spec.rb +++ b/spec/unit/agent_spec.rb @@ -135,18 +135,6 @@ describe Puppet::Agent do @agent.run end - it "should use a mutex to restrict multi-threading" do - client = AgentTestClient.new - AgentTestClient.expects(:new).returns client - - mutex = mock 'mutex' - @agent.expects(:sync).returns mutex - - mutex.expects(:synchronize) - client.expects(:run).never # if it doesn't run, then we know our yield is what triggers it - @agent.run - end - it "should use a filesystem lock to restrict multiple processes running the agent" do client = AgentTestClient.new AgentTestClient.expects(:new).returns client diff --git a/spec/unit/application/agent_spec.rb b/spec/unit/application/agent_spec.rb index 2ec32dbfb..4280a953c 100755 --- a/spec/unit/application/agent_spec.rb +++ b/spec/unit/application/agent_spec.rb @@ -198,7 +198,7 @@ describe Puppet::Application::Agent do describe "during setup" do before :each do Puppet.stubs(:info) - FileTest.stubs(:exists?).returns(true) + Puppet::FileSystem::File.stubs(:exist?).returns(true) Puppet[:libdir] = "/dev/null/lib" Puppet::Transaction::Report.indirection.stubs(:terminus_class=) Puppet::Transaction::Report.indirection.stubs(:cache_class=) @@ -449,8 +449,8 @@ describe Puppet::Application::Agent do describe "when setting up listen" do before :each do - FileTest.stubs(:exists?).with('auth').returns(true) - File.stubs(:exist?).returns(true) + Puppet::FileSystem::File.stubs(:exist?).with('auth').returns(true) + Puppet::FileSystem::File.stubs(:exist?).returns(true) @puppetd.options[:serve] = [] @server = stub_everything 'server' Puppet::Network::Server.stubs(:new).returns(@server) @@ -460,7 +460,7 @@ describe Puppet::Application::Agent do it "should exit if no authorization file" do Puppet[:listen] = true Puppet.stubs(:err) - FileTest.stubs(:exists?).with(Puppet[:rest_authconfig]).returns(false) + Puppet::FileSystem::File.stubs(:exist?).with(Puppet[:rest_authconfig]).returns(false) expect do execute_agent diff --git a/spec/unit/application/apply_spec.rb b/spec/unit/application/apply_spec.rb index 97728c06e..d3cc54884 100755 --- a/spec/unit/application/apply_spec.rb +++ b/spec/unit/application/apply_spec.rb @@ -45,8 +45,8 @@ describe Puppet::Application::Apply do @apply.handle_logdest("console") end - it "should put the logset options to true" do - @apply.options.expects(:[]=).with(:logset,true) + it "should set the setdest options to true" do + @apply.options.expects(:[]=).with(:setdest,true) @apply.handle_logdest("console") end @@ -103,6 +103,22 @@ describe Puppet::Application::Apply do @apply.setup end + it "configures a profiler when profiling is enabled" do + Puppet[:profile] = true + + @apply.setup + + expect(Puppet::Util::Profiler.current).to be_a(Puppet::Util::Profiler::WallClock) + end + + it "does not have a profiler if profiling is disabled" do + Puppet[:profile] = false + + @apply.setup + + expect(Puppet::Util::Profiler.current).to eq(Puppet::Util::Profiler::NONE) + end + it "should set default_file_terminus to `file_server` to be local" do @apply.app_defaults[:default_file_terminus].should == :file_server end diff --git a/spec/unit/application/cert_spec.rb b/spec/unit/application/cert_spec.rb index 25d74c859..f24ef8677 100755 --- a/spec/unit/application/cert_spec.rb +++ b/spec/unit/application/cert_spec.rb @@ -122,10 +122,12 @@ describe Puppet::Application::Cert => true do @ca = stub_everything 'ca' @cert_app.ca = @ca @cert_app.command_line.stubs(:args).returns([]) + @iface = stub_everything 'iface' + Puppet::SSL::CertificateAuthority::Interface.stubs(:new).returns(@iface) end it "should delegate to the CertificateAuthority" do - @ca.expects(:apply) + @iface.expects(:apply) @cert_app.main end @@ -133,7 +135,7 @@ describe Puppet::Application::Cert => true do it "should delegate with :all if option --all was given" do @cert_app.handle_all(0) - @ca.expects(:apply).with { |cert_mode,to| to[:to] == :all } + Puppet::SSL::CertificateAuthority::Interface.expects(:new).returns(@iface).with { |cert_mode,to| to[:to] == :all } @cert_app.main end @@ -141,7 +143,7 @@ describe Puppet::Application::Cert => true do it "should delegate to ca.apply with the hosts given on command line" do @cert_app.command_line.stubs(:args).returns(["host"]) - @ca.expects(:apply).with { |cert_mode,to| to[:to] == ["host"]} + Puppet::SSL::CertificateAuthority::Interface.expects(:new).returns(@iface).with { |cert_mode,to| to[:to] == ["host"]} @cert_app.main end @@ -150,7 +152,7 @@ describe Puppet::Application::Cert => true do @cert_app.command_line.stubs(:args).returns(["host"]) @cert_app.handle_digest(:digest) - @ca.expects(:apply).with { |cert_mode,to| to[:digest] == :digest} + Puppet::SSL::CertificateAuthority::Interface.expects(:new).returns(@iface).with { |cert_mode,to| to[:digest] == :digest} @cert_app.main end @@ -159,8 +161,8 @@ describe Puppet::Application::Cert => true do @cert_app.subcommand = :destroy @cert_app.command_line.stubs(:args).returns(["host"]) - @ca.expects(:apply).with { |cert_mode,to| cert_mode == :revoke } - @ca.expects(:apply).with { |cert_mode,to| cert_mode == :destroy } + Puppet::SSL::CertificateAuthority::Interface.expects(:new).returns(@iface).with { |cert_mode,to| cert_mode == :revoke } + Puppet::SSL::CertificateAuthority::Interface.expects(:new).returns(@iface).with { |cert_mode,to| cert_mode == :destroy } @cert_app.main end diff --git a/spec/unit/application/device_spec.rb b/spec/unit/application/device_spec.rb index 43787b69f..bbbc011c1 100755 --- a/spec/unit/application/device_spec.rb +++ b/spec/unit/application/device_spec.rb @@ -127,7 +127,7 @@ describe Puppet::Application::Device do before :each do @device.options.stubs(:[]) Puppet.stubs(:info) - FileTest.stubs(:exists?).returns(true) + Puppet::FileSystem::File.stubs(:exist?).returns(true) Puppet[:libdir] = "/dev/null/lib" Puppet::SSL::Host.stubs(:ca_location=) Puppet::Transaction::Report.indirection.stubs(:terminus_class=) diff --git a/spec/unit/application/filebucket_spec.rb b/spec/unit/application/filebucket_spec.rb index e7702e45a..4301c7dcd 100755 --- a/spec/unit/application/filebucket_spec.rb +++ b/spec/unit/application/filebucket_spec.rb @@ -178,7 +178,7 @@ describe Puppet::Application::Filebucket do it "should call the client backup method for each given parameter" do @filebucket.stubs(:puts) - FileTest.stubs(:exists?).returns(true) + Puppet::FileSystem::File.stubs(:exist?).returns(true) FileTest.stubs(:readable?).returns(true) @filebucket.stubs(:args).returns(["file1", "file2"]) diff --git a/spec/unit/application/inspect_spec.rb b/spec/unit/application/inspect_spec.rb index c0a034511..da73ee51c 100755 --- a/spec/unit/application/inspect_spec.rb +++ b/spec/unit/application/inspect_spec.rb @@ -36,7 +36,7 @@ describe Puppet::Application::Inspect do describe "when executing" do before :each do Puppet[:report] = true - @inspect.options[:logset] = true + @inspect.options[:setdest] = true Puppet::Transaction::Report::Rest.any_instance.stubs(:save) @inspect.setup end diff --git a/spec/unit/application_spec.rb b/spec/unit/application_spec.rb index 912416d53..1c9dd49b8 100755 --- a/spec/unit/application_spec.rb +++ b/spec/unit/application_spec.rb @@ -618,4 +618,28 @@ describe Puppet::Application do end end + + describe "#handle_logdest_arg" do + + let(:test_arg) { "arg_test_logdest" } + + it "should log an exception that is raised" do + our_exception = Puppet::DevError.new("test exception") + Puppet::Util::Log.expects(:newdestination).with(test_arg).raises(our_exception) + Puppet.expects(:log_exception).with(our_exception) + @app.handle_logdest_arg(test_arg) + end + + it "should set the new log destination" do + Puppet::Util::Log.expects(:newdestination).with(test_arg) + @app.handle_logdest_arg(test_arg) + end + + it "should set the flag that a destination is set in the options hash" do + Puppet::Util::Log.stubs(:newdestination).with(test_arg) + @app.handle_logdest_arg(test_arg) + @app.options[:setdest].should be_true + end + end + end diff --git a/spec/unit/configurer/downloader_spec.rb b/spec/unit/configurer/downloader_spec.rb index 403e92c1a..a818370fc 100755 --- a/spec/unit/configurer/downloader_spec.rb +++ b/spec/unit/configurer/downloader_spec.rb @@ -51,11 +51,7 @@ describe Puppet::Configurer::Downloader do @dler.file end - describe "on POSIX" do - before :each do - Puppet.features.stubs(:microsoft_windows?).returns false - end - + describe "on POSIX", :as_platform => :posix do it "should always set the owner to the current UID" do Process.expects(:uid).returns 51 Puppet::Type.type(:file).expects(:new).with { |opts| opts[:owner] == 51 } @@ -69,7 +65,7 @@ describe Puppet::Configurer::Downloader do end end - describe "on Windows", :if => Puppet.features.microsoft_windows? do + describe "on Windows", :as_platform => :windows do it "should omit the owner" do Puppet::Type.type(:file).expects(:new).with { |opts| opts[:owner] == nil } @dler.file @@ -79,6 +75,11 @@ describe Puppet::Configurer::Downloader do Puppet::Type.type(:file).expects(:new).with { |opts| opts[:group] == nil } @dler.file end + + it "should set source_permissions to ignore" do + Puppet::Type.type(:file).expects(:new).with { |opts| opts[:source_permissions] == :ignore } + @dler.file + end end it "should always force the download" do @@ -135,7 +136,7 @@ describe Puppet::Configurer::Downloader do Puppet[:tags] = 'maytag' @dler.evaluate - File.exists?(@dl_name).should be_true + Puppet::FileSystem::File.exist?(@dl_name).should be_true end it "should log that it is downloading" do diff --git a/spec/unit/configurer/fact_handler_spec.rb b/spec/unit/configurer/fact_handler_spec.rb index a74a91dfa..a92ad7d4f 100755 --- a/spec/unit/configurer/fact_handler_spec.rb +++ b/spec/unit/configurer/fact_handler_spec.rb @@ -3,6 +3,16 @@ require 'spec_helper' require 'puppet/configurer' require 'puppet/configurer/fact_handler' +# the json-schema gem doesn't support windows +if not Puppet.features.microsoft_windows? + describe "catalog facts schema" do + it "should validate against the json meta-schema" do + JSON::Validator.validate!(JSON_META_SCHEMA, FACTS_SCHEMA) + end + end + + end + class FactHandlerTester include Puppet::Configurer::FactHandler @@ -74,4 +84,17 @@ describe Puppet::Configurer::FactHandler do @facthandler.facts_for_uploading.should == {:facts_format => :pson, :facts => text} end + + def validate_json_for_facts(catalog_facts) + JSON::Validator.validate!(FACTS_SCHEMA, catalog_facts) + end + + it "should generate valid facts data against the facts schema", :unless => Puppet.features.microsoft_windows? do + facts = Puppet::Node::Facts.new(Puppet[:node_name_value], 'my_name_fact' => 'other_node_name') + Puppet::Node::Facts.indirection.save(facts) + + validate_json_for_facts(CGI.unescape(@facthandler.facts_for_uploading[:facts])) + end + end + diff --git a/spec/unit/configurer/plugin_handler_spec.rb b/spec/unit/configurer/plugin_handler_spec.rb index e5fda57e2..20027333d 100755 --- a/spec/unit/configurer/plugin_handler_spec.rb +++ b/spec/unit/configurer/plugin_handler_spec.rb @@ -19,7 +19,7 @@ describe Puppet::Configurer::PluginHandler do it "should use an Agent Downloader, with the name, source, destination, ignore, and environment set correctly, to download plugins when downloading is enabled" do downloader = mock 'downloader' - + Puppet.features.stubs(:external_facts?).returns(:true) # This is needed in order to make sure we pass on windows plugindest = File.expand_path("/tmp/pdest") @@ -27,9 +27,14 @@ describe Puppet::Configurer::PluginHandler do Puppet[:plugindest] = plugindest Puppet[:pluginsignore] = "pignore" + Puppet[:pluginfactsource] = "psource" + Puppet[:pluginfactdest] = plugindest + + Puppet::Configurer::Downloader.expects(:new).with("pluginfacts", plugindest, "psource", "pignore", "myenv").returns downloader Puppet::Configurer::Downloader.expects(:new).with("plugin", plugindest, "psource", "pignore", "myenv").returns downloader - downloader.expects(:evaluate).returns [] + downloader.stubs(:evaluate).returns([]) + downloader.expects(:evaluate).twice @pluginhandler.environment = "myenv" @pluginhandler.download_plugins diff --git a/spec/unit/configurer_spec.rb b/spec/unit/configurer_spec.rb index cba1df584..7732bea08 100755 --- a/spec/unit/configurer_spec.rb +++ b/spec/unit/configurer_spec.rb @@ -110,6 +110,16 @@ describe Puppet::Configurer do @agent.run.should == 0 end + it "applies a cached catalog when it can't connect to the master" do + error = Errno::ECONNREFUSED.new('Connection refused - connect(2)') + + Puppet::Node.indirection.expects(:find).raises(error) + Puppet::Resource::Catalog.indirection.expects(:find).with(anything, has_entry(:ignore_cache => true)).raises(error) + Puppet::Resource::Catalog.indirection.expects(:find).with(anything, has_entry(:ignore_terminus => true)).returns(@catalog) + + @agent.run.should == 0 + end + it "should initialize a transaction report if one is not provided" do report = Puppet::Transaction::Report.new("apply") Puppet::Transaction::Report.expects(:new).returns report @@ -341,12 +351,12 @@ describe Puppet::Configurer do @agent.environment.should == "second_env" end - it "should clear the thread local caches" do - Thread.current[:env_module_directories] = false + it "should clear the global caches" do + $env_module_directories = false @agent.run - Thread.current[:env_module_directories].should == nil + $env_module_directories.should == nil end describe "when not using a REST terminus for catalogs" do @@ -454,7 +464,7 @@ describe Puppet::Configurer do it "should write the last run file" do @configurer.save_last_run_summary(@report) - FileTest.exists?(Puppet[:lastrunfile]).should be_true + Puppet::FileSystem::File.exist?(Puppet[:lastrunfile]).should be_true end it "should write the raw summary as yaml" do @@ -486,7 +496,7 @@ describe Puppet::Configurer do require 'puppet/util/windows/security' mode = Puppet::Util::Windows::Security.get_mode(Puppet[:lastrunfile]) else - mode = File.stat(Puppet[:lastrunfile]).mode + mode = Puppet::FileSystem::File.new(Puppet[:lastrunfile]).stat.mode end (mode & 0777).should == 0664 end diff --git a/spec/unit/provider/confine/exists_spec.rb b/spec/unit/confine/exists_spec.rb index 17fd3f562..87959fe34 100755 --- a/spec/unit/provider/confine/exists_spec.rb +++ b/spec/unit/confine/exists_spec.rb @@ -1,20 +1,20 @@ #! /usr/bin/env ruby require 'spec_helper' -require 'puppet/provider/confine/exists' +require 'puppet/confine/exists' -describe Puppet::Provider::Confine::Exists do +describe Puppet::Confine::Exists do before do - @confine = Puppet::Provider::Confine::Exists.new("/my/file") + @confine = Puppet::Confine::Exists.new("/my/file") @confine.label = "eh" end it "should be named :exists" do - Puppet::Provider::Confine::Exists.name.should == :exists + Puppet::Confine::Exists.name.should == :exists end it "should not pass if exists is nil" do - confine = Puppet::Provider::Confine::Exists.new(nil) + confine = Puppet::Confine::Exists.new(nil) confine.label = ":exists => nil" confine.expects(:pass?).with(nil) confine.should_not be_valid @@ -30,12 +30,12 @@ describe Puppet::Provider::Confine::Exists do end it "should return false if the value does not point to a file" do - FileTest.expects(:exist?).with("/my/file").returns false + Puppet::FileSystem::File.expects(:exist?).with("/my/file").returns false @confine.pass?("/my/file").should be_false end it "should return true if the value points to a file" do - FileTest.expects(:exist?).with("/my/file").returns true + Puppet::FileSystem::File.expects(:exist?).with("/my/file").returns true @confine.pass?("/my/file").should be_true end @@ -62,11 +62,11 @@ describe Puppet::Provider::Confine::Exists do end it "should produce a summary containing all missing files" do - FileTest.stubs(:exist?).returns true - FileTest.expects(:exist?).with("/two").returns false - FileTest.expects(:exist?).with("/four").returns false + Puppet::FileSystem::File.stubs(:exist?).returns true + Puppet::FileSystem::File.expects(:exist?).with("/two").returns false + Puppet::FileSystem::File.expects(:exist?).with("/four").returns false - confine = Puppet::Provider::Confine::Exists.new %w{/one /two /three /four} + confine = Puppet::Confine::Exists.new %w{/one /two /three /four} confine.summary.should == %w{/two /four} end @@ -75,6 +75,6 @@ describe Puppet::Provider::Confine::Exists do c2 = mock '2', :summary => %w{two} c3 = mock '3', :summary => %w{three} - Puppet::Provider::Confine::Exists.summarize([c1, c2, c3]).should == %w{one two three} + Puppet::Confine::Exists.summarize([c1, c2, c3]).should == %w{one two three} end end diff --git a/spec/unit/provider/confine/false_spec.rb b/spec/unit/confine/false_spec.rb index 91d738c52..44ecd40ed 100755 --- a/spec/unit/provider/confine/false_spec.rb +++ b/spec/unit/confine/false_spec.rb @@ -1,22 +1,22 @@ #! /usr/bin/env ruby require 'spec_helper' -require 'puppet/provider/confine/false' +require 'puppet/confine/false' -describe Puppet::Provider::Confine::False do +describe Puppet::Confine::False do it "should be named :false" do - Puppet::Provider::Confine::False.name.should == :false + Puppet::Confine::False.name.should == :false end it "should require a value" do - lambda { Puppet::Provider::Confine.new }.should raise_error(ArgumentError) + lambda { Puppet::Confine.new }.should raise_error(ArgumentError) end describe "when testing values" do - before { @confine = Puppet::Provider::Confine::False.new("foo") } + before { @confine = Puppet::Confine::False.new("foo") } it "should use the 'pass?' method to test validity" do - @confine = Puppet::Provider::Confine::False.new("foo") + @confine = Puppet::Confine::False.new("foo") @confine.label = "eh" @confine.expects(:pass?).with("foo") @confine.valid? @@ -31,13 +31,13 @@ describe Puppet::Provider::Confine::False do end it "should produce a message that a value is true" do - @confine = Puppet::Provider::Confine::False.new("foo") + @confine = Puppet::Confine::False.new("foo") @confine.message("eh").should be_include("true") end end it "should be able to produce a summary with the number of incorrectly true values" do - confine = Puppet::Provider::Confine::False.new %w{one two three four} + confine = Puppet::Confine::False.new %w{one two three four} confine.expects(:pass?).times(4).returns(true).returns(false).returns(true).returns(false) confine.summary.should == 2 end @@ -47,6 +47,6 @@ describe Puppet::Provider::Confine::False do c2 = mock '2', :summary => 2 c3 = mock '3', :summary => 3 - Puppet::Provider::Confine::False.summarize([c1, c2, c3]).should == 6 + Puppet::Confine::False.summarize([c1, c2, c3]).should == 6 end end diff --git a/spec/unit/provider/confine/feature_spec.rb b/spec/unit/confine/feature_spec.rb index b90662235..dad9d9b8b 100755 --- a/spec/unit/provider/confine/feature_spec.rb +++ b/spec/unit/confine/feature_spec.rb @@ -1,24 +1,24 @@ #! /usr/bin/env ruby require 'spec_helper' -require 'puppet/provider/confine/feature' +require 'puppet/confine/feature' -describe Puppet::Provider::Confine::Feature do +describe Puppet::Confine::Feature do it "should be named :feature" do - Puppet::Provider::Confine::Feature.name.should == :feature + Puppet::Confine::Feature.name.should == :feature end it "should require a value" do - lambda { Puppet::Provider::Confine::Feature.new }.should raise_error(ArgumentError) + lambda { Puppet::Confine::Feature.new }.should raise_error(ArgumentError) end it "should always convert values to an array" do - Puppet::Provider::Confine::Feature.new("/some/file").values.should be_instance_of(Array) + Puppet::Confine::Feature.new("/some/file").values.should be_instance_of(Array) end describe "when testing values" do before do - @confine = Puppet::Provider::Confine::Feature.new("myfeature") + @confine = Puppet::Confine::Feature.new("myfeature") @confine.label = "eh" end @@ -44,14 +44,14 @@ describe Puppet::Provider::Confine::Feature do it "should summarize multiple instances by returning a flattened array of all missing features" do confines = [] - confines << Puppet::Provider::Confine::Feature.new(%w{one two}) - confines << Puppet::Provider::Confine::Feature.new(%w{two}) - confines << Puppet::Provider::Confine::Feature.new(%w{three four}) + confines << Puppet::Confine::Feature.new(%w{one two}) + confines << Puppet::Confine::Feature.new(%w{two}) + confines << Puppet::Confine::Feature.new(%w{three four}) features = mock 'feature' features.stub_everything Puppet.stubs(:features).returns features - Puppet::Provider::Confine::Feature.summarize(confines).sort.should == %w{one two three four}.sort + Puppet::Confine::Feature.summarize(confines).sort.should == %w{one two three four}.sort end end diff --git a/spec/unit/provider/confine/true_spec.rb b/spec/unit/confine/true_spec.rb index e869817f0..d7484d59c 100755 --- a/spec/unit/provider/confine/true_spec.rb +++ b/spec/unit/confine/true_spec.rb @@ -1,20 +1,20 @@ #! /usr/bin/env ruby require 'spec_helper' -require 'puppet/provider/confine/true' +require 'puppet/confine/true' -describe Puppet::Provider::Confine::True do +describe Puppet::Confine::True do it "should be named :true" do - Puppet::Provider::Confine::True.name.should == :true + Puppet::Confine::True.name.should == :true end it "should require a value" do - lambda { Puppet::Provider::Confine::True.new }.should raise_error(ArgumentError) + lambda { Puppet::Confine::True.new }.should raise_error(ArgumentError) end describe "when testing values" do before do - @confine = Puppet::Provider::Confine::True.new("foo") + @confine = Puppet::Confine::True.new("foo") @confine.label = "eh" end @@ -37,7 +37,7 @@ describe Puppet::Provider::Confine::True do end it "should produce the number of false values when asked for a summary" do - @confine = Puppet::Provider::Confine::True.new %w{one two three four} + @confine = Puppet::Confine::True.new %w{one two three four} @confine.expects(:pass?).times(4).returns(true).returns(false).returns(true).returns(false) @confine.summary.should == 2 end @@ -47,6 +47,6 @@ describe Puppet::Provider::Confine::True do c2 = mock '2', :summary => 2 c3 = mock '3', :summary => 3 - Puppet::Provider::Confine::True.summarize([c1, c2, c3]).should == 6 + Puppet::Confine::True.summarize([c1, c2, c3]).should == 6 end end diff --git a/spec/unit/provider/confine/variable_spec.rb b/spec/unit/confine/variable_spec.rb index d06705c6f..be45e3bb1 100755 --- a/spec/unit/provider/confine/variable_spec.rb +++ b/spec/unit/confine/variable_spec.rb @@ -1,28 +1,28 @@ #! /usr/bin/env ruby require 'spec_helper' -require 'puppet/provider/confine/variable' +require 'puppet/confine/variable' -describe Puppet::Provider::Confine::Variable do +describe Puppet::Confine::Variable do it "should be named :variable" do - Puppet::Provider::Confine::Variable.name.should == :variable + Puppet::Confine::Variable.name.should == :variable end it "should require a value" do - lambda { Puppet::Provider::Confine::Variable.new }.should raise_error(ArgumentError) + lambda { Puppet::Confine::Variable.new }.should raise_error(ArgumentError) end it "should always convert values to an array" do - Puppet::Provider::Confine::Variable.new("/some/file").values.should be_instance_of(Array) + Puppet::Confine::Variable.new("/some/file").values.should be_instance_of(Array) end it "should have an accessor for its name" do - Puppet::Provider::Confine::Variable.new(:bar).should respond_to(:name) + Puppet::Confine::Variable.new(:bar).should respond_to(:name) end describe "when testing values" do before do - @confine = Puppet::Provider::Confine::Variable.new("foo") + @confine = Puppet::Confine::Variable.new("foo") @confine.name = :myvar end @@ -63,7 +63,7 @@ describe Puppet::Provider::Confine::Variable do end it "should produce a message that the fact value is not correct" do - @confine = Puppet::Provider::Confine::Variable.new(%w{bar bee}) + @confine = Puppet::Confine::Variable.new(%w{bar bee}) @confine.name = "eh" message = @confine.message("value") message.should be_include("facter") @@ -71,7 +71,7 @@ describe Puppet::Provider::Confine::Variable do end it "should be valid if the test value matches any of the provided values" do - @confine = Puppet::Provider::Confine::Variable.new(%w{bar bee}) + @confine = Puppet::Confine::Variable.new(%w{bar bee}) @confine.expects(:test_value).returns "bee" @confine.should be_valid end @@ -79,28 +79,28 @@ describe Puppet::Provider::Confine::Variable do describe "when summarizing multiple instances" do it "should return a hash of failing variables and their values" do - c1 = Puppet::Provider::Confine::Variable.new("one") + c1 = Puppet::Confine::Variable.new("one") c1.name = "uno" c1.expects(:valid?).returns false - c2 = Puppet::Provider::Confine::Variable.new("two") + c2 = Puppet::Confine::Variable.new("two") c2.name = "dos" c2.expects(:valid?).returns true - c3 = Puppet::Provider::Confine::Variable.new("three") + c3 = Puppet::Confine::Variable.new("three") c3.name = "tres" c3.expects(:valid?).returns false - Puppet::Provider::Confine::Variable.summarize([c1, c2, c3]).should == {"uno" => %w{one}, "tres" => %w{three}} + Puppet::Confine::Variable.summarize([c1, c2, c3]).should == {"uno" => %w{one}, "tres" => %w{three}} end it "should combine the values of multiple confines with the same fact" do - c1 = Puppet::Provider::Confine::Variable.new("one") + c1 = Puppet::Confine::Variable.new("one") c1.name = "uno" c1.expects(:valid?).returns false - c2 = Puppet::Provider::Confine::Variable.new("two") + c2 = Puppet::Confine::Variable.new("two") c2.name = "uno" c2.expects(:valid?).returns false - Puppet::Provider::Confine::Variable.summarize([c1, c2]).should == {"uno" => %w{one two}} + Puppet::Confine::Variable.summarize([c1, c2]).should == {"uno" => %w{one two}} end end end diff --git a/spec/unit/provider/confine_collection_spec.rb b/spec/unit/confine_collection_spec.rb index 31a778fd7..958f69197 100755 --- a/spec/unit/provider/confine_collection_spec.rb +++ b/spec/unit/confine_collection_spec.rb @@ -1,72 +1,72 @@ #! /usr/bin/env ruby require 'spec_helper' -require 'puppet/provider/confine_collection' +require 'puppet/confine_collection' -describe Puppet::Provider::ConfineCollection do +describe Puppet::ConfineCollection do it "should be able to add confines" do - Puppet::Provider::ConfineCollection.new("label").should respond_to(:confine) + Puppet::ConfineCollection.new("label").should respond_to(:confine) end it "should require a label at initialization" do - lambda { Puppet::Provider::ConfineCollection.new }.should raise_error(ArgumentError) + lambda { Puppet::ConfineCollection.new }.should raise_error(ArgumentError) end it "should make its label available" do - Puppet::Provider::ConfineCollection.new("mylabel").label.should == "mylabel" + Puppet::ConfineCollection.new("mylabel").label.should == "mylabel" end describe "when creating confine instances" do it "should create an instance of the named test with the provided values" do test_class = mock 'test_class' test_class.expects(:new).with(%w{my values}).returns(stub('confine', :label= => nil)) - Puppet::Provider::Confine.expects(:test).with(:foo).returns test_class + Puppet::Confine.expects(:test).with(:foo).returns test_class - Puppet::Provider::ConfineCollection.new("label").confine :foo => %w{my values} + Puppet::ConfineCollection.new("label").confine :foo => %w{my values} end it "should copy its label to the confine instance" do confine = mock 'confine' test_class = mock 'test_class' test_class.expects(:new).returns confine - Puppet::Provider::Confine.expects(:test).returns test_class + Puppet::Confine.expects(:test).returns test_class confine.expects(:label=).with("label") - Puppet::Provider::ConfineCollection.new("label").confine :foo => %w{my values} + Puppet::ConfineCollection.new("label").confine :foo => %w{my values} end describe "and the test cannot be found" do it "should create a Facter test with the provided values and set the name to the test name" do - confine = Puppet::Provider::Confine.test(:variable).new(%w{my values}) + confine = Puppet::Confine.test(:variable).new(%w{my values}) confine.expects(:name=).with(:foo) confine.class.expects(:new).with(%w{my values}).returns confine - Puppet::Provider::ConfineCollection.new("label").confine(:foo => %w{my values}) + Puppet::ConfineCollection.new("label").confine(:foo => %w{my values}) end end describe "and the 'for_binary' option was provided" do it "should mark the test as a binary confine" do - confine = Puppet::Provider::Confine.test(:exists).new(:bar) + confine = Puppet::Confine.test(:exists).new(:bar) confine.expects(:for_binary=).with true - Puppet::Provider::Confine.test(:exists).expects(:new).with(:bar).returns confine - Puppet::Provider::ConfineCollection.new("label").confine :exists => :bar, :for_binary => true + Puppet::Confine.test(:exists).expects(:new).with(:bar).returns confine + Puppet::ConfineCollection.new("label").confine :exists => :bar, :for_binary => true end end end it "should be valid if no confines are present" do - Puppet::Provider::ConfineCollection.new("label").should be_valid + Puppet::ConfineCollection.new("label").should be_valid end it "should be valid if all confines pass" do c1 = stub 'c1', :valid? => true, :label= => nil c2 = stub 'c2', :valid? => true, :label= => nil - Puppet::Provider::Confine.test(:true).expects(:new).returns(c1) - Puppet::Provider::Confine.test(:false).expects(:new).returns(c2) + Puppet::Confine.test(:true).expects(:new).returns(c1) + Puppet::Confine.test(:false).expects(:new).returns(c2) - confiner = Puppet::Provider::ConfineCollection.new("label") + confiner = Puppet::ConfineCollection.new("label") confiner.confine :true => :bar, :false => :bee confiner.should be_valid @@ -76,10 +76,10 @@ describe Puppet::Provider::ConfineCollection do c1 = stub 'c1', :valid? => true, :label= => nil c2 = stub 'c2', :valid? => false, :label= => nil - Puppet::Provider::Confine.test(:true).expects(:new).returns(c1) - Puppet::Provider::Confine.test(:false).expects(:new).returns(c2) + Puppet::Confine.test(:true).expects(:new).returns(c1) + Puppet::Confine.test(:false).expects(:new).returns(c2) - confiner = Puppet::Provider::ConfineCollection.new("label") + confiner = Puppet::ConfineCollection.new("label") confiner.confine :true => :bar, :false => :bee confiner.should_not be_valid @@ -87,7 +87,7 @@ describe Puppet::Provider::ConfineCollection do describe "when providing a summary" do before do - @confiner = Puppet::Provider::ConfineCollection.new("label") + @confiner = Puppet::ConfineCollection.new("label") end it "should return a hash" do @@ -100,32 +100,32 @@ describe Puppet::Provider::ConfineCollection do it "should add each test type's summary to the hash" do @confiner.confine :true => :bar, :false => :bee - Puppet::Provider::Confine.test(:true).expects(:summarize).returns :tsumm - Puppet::Provider::Confine.test(:false).expects(:summarize).returns :fsumm + Puppet::Confine.test(:true).expects(:summarize).returns :tsumm + Puppet::Confine.test(:false).expects(:summarize).returns :fsumm @confiner.summary.should == {:true => :tsumm, :false => :fsumm} end it "should not include tests that return 0" do @confiner.confine :true => :bar, :false => :bee - Puppet::Provider::Confine.test(:true).expects(:summarize).returns 0 - Puppet::Provider::Confine.test(:false).expects(:summarize).returns :fsumm + Puppet::Confine.test(:true).expects(:summarize).returns 0 + Puppet::Confine.test(:false).expects(:summarize).returns :fsumm @confiner.summary.should == {:false => :fsumm} end it "should not include tests that return empty arrays" do @confiner.confine :true => :bar, :false => :bee - Puppet::Provider::Confine.test(:true).expects(:summarize).returns [] - Puppet::Provider::Confine.test(:false).expects(:summarize).returns :fsumm + Puppet::Confine.test(:true).expects(:summarize).returns [] + Puppet::Confine.test(:false).expects(:summarize).returns :fsumm @confiner.summary.should == {:false => :fsumm} end it "should not include tests that return empty hashes" do @confiner.confine :true => :bar, :false => :bee - Puppet::Provider::Confine.test(:true).expects(:summarize).returns({}) - Puppet::Provider::Confine.test(:false).expects(:summarize).returns :fsumm + Puppet::Confine.test(:true).expects(:summarize).returns({}) + Puppet::Confine.test(:false).expects(:summarize).returns :fsumm @confiner.summary.should == {:false => :fsumm} end diff --git a/spec/unit/provider/confine_spec.rb b/spec/unit/confine_spec.rb index 38dfd8c3f..06c10abd7 100755 --- a/spec/unit/provider/confine_spec.rb +++ b/spec/unit/confine_spec.rb @@ -1,40 +1,40 @@ #! /usr/bin/env ruby require 'spec_helper' -require 'puppet/provider/confine' +require 'puppet/confine' -describe Puppet::Provider::Confine do +describe Puppet::Confine do it "should require a value" do - lambda { Puppet::Provider::Confine.new }.should raise_error(ArgumentError) + lambda { Puppet::Confine.new }.should raise_error(ArgumentError) end it "should always convert values to an array" do - Puppet::Provider::Confine.new("/some/file").values.should be_instance_of(Array) + Puppet::Confine.new("/some/file").values.should be_instance_of(Array) end it "should have a 'true' test" do - Puppet::Provider::Confine.test(:true).should be_instance_of(Class) + Puppet::Confine.test(:true).should be_instance_of(Class) end it "should have a 'false' test" do - Puppet::Provider::Confine.test(:false).should be_instance_of(Class) + Puppet::Confine.test(:false).should be_instance_of(Class) end it "should have a 'feature' test" do - Puppet::Provider::Confine.test(:feature).should be_instance_of(Class) + Puppet::Confine.test(:feature).should be_instance_of(Class) end it "should have an 'exists' test" do - Puppet::Provider::Confine.test(:exists).should be_instance_of(Class) + Puppet::Confine.test(:exists).should be_instance_of(Class) end it "should have a 'variable' test" do - Puppet::Provider::Confine.test(:variable).should be_instance_of(Class) + Puppet::Confine.test(:variable).should be_instance_of(Class) end describe "when testing all values" do before do - @confine = Puppet::Provider::Confine.new(%w{a b c}) + @confine = Puppet::Confine.new(%w{a b c}) @confine.label = "foo" end @@ -64,7 +64,7 @@ describe Puppet::Provider::Confine do end describe "when testing the result of the values" do - before { @confine = Puppet::Provider::Confine.new(%w{a b c d}) } + before { @confine = Puppet::Confine.new(%w{a b c d}) } it "should return an array with the result of the test for each value" do @confine.stubs(:pass?).returns true diff --git a/spec/unit/provider/confiner_spec.rb b/spec/unit/confiner_spec.rb index 2afdd71a3..2d71054fe 100755 --- a/spec/unit/provider/confiner_spec.rb +++ b/spec/unit/confiner_spec.rb @@ -1,12 +1,12 @@ #! /usr/bin/env ruby require 'spec_helper' -require 'puppet/provider/confiner' +require 'puppet/confiner' -describe Puppet::Provider::Confiner do +describe Puppet::Confiner do before do @object = Object.new - @object.extend(Puppet::Provider::Confiner) + @object.extend(Puppet::Confiner) end it "should have a method for defining confines" do @@ -29,7 +29,7 @@ describe Puppet::Provider::Confiner do end it "should create a new confine collection if one does not exist" do - Puppet::Provider::ConfineCollection.expects(:new).with("mylabel").returns "mycoll" + Puppet::ConfineCollection.expects(:new).with("mylabel").returns "mycoll" @object.expects(:to_s).returns "mylabel" @object.confine_collection.should == "mycoll" end diff --git a/spec/unit/face/parser_spec.rb b/spec/unit/face/parser_spec.rb new file mode 100644 index 000000000..a517ae641 --- /dev/null +++ b/spec/unit/face/parser_spec.rb @@ -0,0 +1,54 @@ +require 'spec_helper' +require 'puppet_spec/files' + +require 'puppet/face' + +describe Puppet::Face[:parser, :current] do + include PuppetSpec::Files + + let(:parser) { Puppet::Face[:parser, :current] } + + it "validates the configured site manifest when no files are given" do + Puppet[:manifest] = file_containing('site.pp', "{ invalid =>") + from_an_interactive_terminal + + expect { parser.validate() }.to exit_with(1) + end + + it "validates the given file" do + manifest = file_containing('site.pp', "{ invalid =>") + from_an_interactive_terminal + + expect { parser.validate(manifest) }.to exit_with(1) + end + + it "validates the contents of STDIN when no files given and STDIN is not a tty" do + from_a_piped_input_of("{ invalid =>") + + expect { parser.validate() }.to exit_with(1) + end + + it "runs error free when there are no validation errors" do + manifest = file_containing('site.pp', "notify { valid: }") + from_an_interactive_terminal + + parser.validate(manifest) + end + + it "reports missing files" do + from_an_interactive_terminal + + expect do + parser.validate("missing.pp") + end.to raise_error(Puppet::Error, /One or more file\(s\) specified did not exist.*missing\.pp/m) + end + + def from_an_interactive_terminal + STDIN.stubs(:tty?).returns(true) + end + + def from_a_piped_input_of(contents) + STDIN.stubs(:tty?).returns(false) + STDIN.stubs(:read).returns(contents) + end +end diff --git a/spec/unit/file_bucket/dipper_spec.rb b/spec/unit/file_bucket/dipper_spec.rb index 397cb9219..83e914851 100755 --- a/spec/unit/file_bucket/dipper_spec.rb +++ b/spec/unit/file_bucket/dipper_spec.rb @@ -45,7 +45,7 @@ describe Puppet::FileBucket::Dipper do Digest::MD5.hexdigest("my\r\ncontents").should == checksum @dipper.backup(file).should == checksum - File.exists?("#{file_bucket}/f/0/d/7/d/4/e/4/f0d7d4e480ad698ed56aeec8b6bd6dea/contents").should == true + Puppet::FileSystem::File.exist?("#{file_bucket}/f/0/d/7/d/4/e/4/f0d7d4e480ad698ed56aeec8b6bd6dea/contents").should == true end it "should not backup a file that is already in the bucket" do @@ -123,7 +123,7 @@ describe Puppet::FileBucket::Dipper do klass.any_instance.expects(:find).with { |r| request = r }.returns(Puppet::FileBucket::File.new(contents)) dipper.restore(dest, md5).should == md5 - Digest::MD5.hexdigest(IO.binread(dest)).should == md5 + Digest::MD5.hexdigest(Puppet::FileSystem::File.new(dest).binread).should == md5 request.key.should == "md5/#{md5}" request.server.should == server diff --git a/spec/unit/file_serving/base_spec.rb b/spec/unit/file_serving/base_spec.rb index a172830e8..65168d3a3 100755 --- a/spec/unit/file_serving/base_spec.rb +++ b/spec/unit/file_serving/base_spec.rb @@ -42,12 +42,12 @@ describe Puppet::FileServing::Base do end it "should allow specification of a path" do - FileTest.stubs(:exists?).returns(true) + Puppet::FileSystem::File.stubs(:exist?).returns(true) Puppet::FileServing::Base.new(path, :path => file).path.should == file end it "should allow specification of a relative path" do - FileTest.stubs(:exists?).returns(true) + Puppet::FileSystem::File.stubs(:exist?).returns(true) Puppet::FileServing::Base.new(path, :relative_path => "my/file").relative_path.should == "my/file" end @@ -56,19 +56,22 @@ describe Puppet::FileServing::Base do end it "should correctly indicate if the file is present" do - File.expects(:lstat).with(file).returns(mock("stat")) + mock_file = mock(file, :lstat => stub('stat')) + Puppet::FileSystem::File.expects(:new).with(file).returns mock_file Puppet::FileServing::Base.new(file).exist?.should be_true end it "should correctly indicate if the file is absent" do - File.expects(:lstat).with(file).raises RuntimeError + mock_file = mock(file) + Puppet::FileSystem::File.expects(:new).with(file).returns mock_file + mock_file.expects(:lstat).raises RuntimeError Puppet::FileServing::Base.new(file).exist?.should be_false end describe "when setting the relative path" do it "should require that the relative path be unqualified" do @file = Puppet::FileServing::Base.new(path) - FileTest.stubs(:exists?).returns(true) + Puppet::FileSystem::File.stubs(:exist?).returns(true) proc { @file.relative_path = File.expand_path("/qualified/file") }.should raise_error(ArgumentError) end end @@ -102,27 +105,47 @@ describe Puppet::FileServing::Base do end end + describe "when handling a UNC file path on Windows" do + let(:path) { '//server/share/filename' } + let(:file) { Puppet::FileServing::Base.new(path) } + + it "should preserve double slashes at the beginning of the path" do + Puppet.features.stubs(:microsoft_windows?).returns(true) + file.full_path.should == path + end + + it "should strip double slashes not at the beginning of the path" do + Puppet.features.stubs(:microsoft_windows?).returns(true) + file = Puppet::FileServing::Base.new('//server//share//filename') + file.full_path.should == path + end + end + + describe "when stat'ing files" do let(:path) { File.expand_path('/this/file') } let(:file) { Puppet::FileServing::Base.new(path) } + let(:stat) { stub('stat', :ftype => 'file' ) } + let(:stubbed_file) { stub(path, :stat => stat, :lstat => stat)} it "should stat the file's full path" do - File.expects(:lstat).with(path).returns stub("stat", :ftype => "file") + Puppet::FileSystem::File.expects(:new).with(path).returns stubbed_file file.stat end it "should fail if the file does not exist" do - File.expects(:lstat).with(path).raises(Errno::ENOENT) + Puppet::FileSystem::File.expects(:new).with(path).returns stubbed_file + stubbed_file.expects(:lstat).raises(Errno::ENOENT) proc { file.stat }.should raise_error(Errno::ENOENT) end it "should use :lstat if :links is set to :manage" do - File.expects(:lstat).with(path).returns stub("stat", :ftype => "file") + Puppet::FileSystem::File.expects(:new).with(path).returns stubbed_file file.stat end it "should use :stat if :links is set to :follow" do - File.expects(:stat).with(path).returns stub("stat", :ftype => "file") + Puppet::FileSystem::File.expects(:new).with(path).returns stubbed_file file.links = :follow file.stat end diff --git a/spec/unit/file_serving/configuration_spec.rb b/spec/unit/file_serving/configuration_spec.rb index 2552cd808..b6999e086 100755 --- a/spec/unit/file_serving/configuration_spec.rb +++ b/spec/unit/file_serving/configuration_spec.rb @@ -27,12 +27,12 @@ describe Puppet::FileServing::Configuration do describe "when initializing" do it "should work without a configuration file" do - FileTest.stubs(:exists?).with(@path).returns(false) + Puppet::FileSystem::File.stubs(:exist?).with(@path).returns(false) expect { Puppet::FileServing::Configuration.configuration }.to_not raise_error end it "should parse the configuration file if present" do - FileTest.stubs(:exists?).with(@path).returns(true) + Puppet::FileSystem::File.stubs(:exist?).with(@path).returns(true) @parser = mock 'parser' @parser.expects(:parse).returns({}) Puppet::FileServing::Configuration::Parser.stubs(:new).returns(@parser) @@ -47,7 +47,7 @@ describe Puppet::FileServing::Configuration do describe "when parsing the configuration file" do before do - FileTest.stubs(:exists?).with(@path).returns(true) + Puppet::FileSystem::File.stubs(:exist?).with(@path).returns(true) @parser = mock 'parser' Puppet::FileServing::Configuration::Parser.stubs(:new).returns(@parser) end @@ -84,14 +84,14 @@ describe Puppet::FileServing::Configuration do end it "should add modules and plugins mounts even if the file does not exist" do - FileTest.expects(:exists?).returns false # the file doesn't exist + Puppet::FileSystem::File.expects(:exist?).returns false # the file doesn't exist config = Puppet::FileServing::Configuration.configuration config.mounted?("modules").should be_true config.mounted?("plugins").should be_true end it "should allow all access to modules and plugins if no fileserver.conf exists" do - FileTest.expects(:exists?).returns false # the file doesn't exist + Puppet::FileSystem::File.expects(:exist?).returns false # the file doesn't exist modules = stub 'modules', :empty? => true Puppet::FileServing::Mount::Modules.stubs(:new).returns(modules) modules.expects(:allow).with('*') @@ -104,7 +104,7 @@ describe Puppet::FileServing::Configuration do end it "should not allow access from all to modules and plugins if the fileserver.conf provided some rules" do - FileTest.expects(:exists?).returns false # the file doesn't exist + Puppet::FileSystem::File.expects(:exist?).returns false # the file doesn't exist modules = stub 'modules', :empty? => false Puppet::FileServing::Mount::Modules.stubs(:new).returns(modules) @@ -119,7 +119,7 @@ describe Puppet::FileServing::Configuration do it "should add modules and plugins mounts even if they are not returned by the parser" do @parser.expects(:parse).returns("one" => mock("mount")) - FileTest.expects(:exists?).returns true # the file doesn't exist + Puppet::FileSystem::File.expects(:exist?).returns true # the file doesn't exist config = Puppet::FileServing::Configuration.configuration config.mounted?("modules").should be_true config.mounted?("plugins").should be_true diff --git a/spec/unit/file_serving/content_spec.rb b/spec/unit/file_serving/content_spec.rb index 2169c9b57..2cb159f0a 100755 --- a/spec/unit/file_serving/content_spec.rb +++ b/spec/unit/file_serving/content_spec.rb @@ -26,7 +26,8 @@ describe Puppet::FileServing::Content do content = Puppet::FileServing::Content.new(path) result = "foo" - File.stubs(:lstat).returns(stub("stat", :ftype => "file")) + stub_file = stub(path, :lstat => stub('stat', :ftype => "file")) + Puppet::FileSystem::File.expects(:new).with(path).returns stub_file File.expects(:read).with(path).never content.collect @@ -37,7 +38,8 @@ describe Puppet::FileServing::Content do content = Puppet::FileServing::Content.new(path) result = "foo" - File.stubs(:lstat).returns(stub("stat", :ftype => "directory")) + stub_file = stub(path, :lstat => stub('stat', :ftype => "directory")) + Puppet::FileSystem::File.expects(:new).with(path).returns stub_file File.expects(:read).with(path).never content.collect @@ -83,7 +85,8 @@ describe Puppet::FileServing::Content, "when returning the contents" do it "should fail if the file is a symlink and links are set to :manage" do content.links = :manage - File.expects(:lstat).with(path).returns stub("stat", :ftype => "symlink") + stub_file = stub(path, :lstat => stub("stat", :ftype => "symlink")) + Puppet::FileSystem::File.expects(:new).with(path).returns stub_file proc { content.content }.should raise_error(ArgumentError) end @@ -97,14 +100,16 @@ describe Puppet::FileServing::Content, "when returning the contents" do end it "should return the contents of the path if the file exists" do - File.expects(:stat).with(path).returns stub("stat", :ftype => "file") - IO.expects(:binread).with(path).returns(:mycontent) + mocked_file = mock(path, :stat => stub('stat', :ftype => 'file')) + Puppet::FileSystem::File.expects(:new).with(path).twice.returns(mocked_file) + mocked_file.expects(:binread).returns(:mycontent) content.content.should == :mycontent end it "should cache the returned contents" do - File.expects(:stat).with(path).returns stub("stat", :ftype => "file") - IO.expects(:binread).with(path).returns(:mycontent) + mocked_file = mock(path, :stat => stub('stat', :ftype => 'file')) + Puppet::FileSystem::File.expects(:new).with(path).twice.returns(mocked_file) + mocked_file.expects(:binread).returns(:mycontent) content.content # The second run would throw a failure if the content weren't being cached. content.content diff --git a/spec/unit/file_serving/fileset_spec.rb b/spec/unit/file_serving/fileset_spec.rb index f6a661843..0e61a2a59 100755 --- a/spec/unit/file_serving/fileset_spec.rb +++ b/spec/unit/file_serving/fileset_spec.rb @@ -18,49 +18,58 @@ describe Puppet::FileServing::Fileset do it "removes a trailing file path separator" do path_with_separator = "#{somefile}#{File::SEPARATOR}" - File.stubs(:lstat).with(somefile).returns stub('stat') + stub_file = stub(somefile, :lstat => stub('stat')) + Puppet::FileSystem::File.expects(:new).with(somefile).returns stub_file fileset = Puppet::FileServing::Fileset.new(path_with_separator) fileset.path.should == somefile end it "can be created from the root directory" do path = File.expand_path(File::SEPARATOR) - File.stubs(:lstat).with(path).returns stub('stat') + stub_file = stub(path, :lstat => stub('stat')) + Puppet::FileSystem::File.expects(:new).with(path).returns stub_file fileset = Puppet::FileServing::Fileset.new(path) fileset.path.should == path end it "fails if its path does not exist" do - File.expects(:lstat).with(somefile).raises(Errno::ENOENT) + mock_file = mock(somefile) + Puppet::FileSystem::File.expects(:new).with(somefile).returns mock_file + mock_file.expects(:lstat).raises(Errno::ENOENT) expect { Puppet::FileServing::Fileset.new(somefile) }.to raise_error(ArgumentError, "Fileset paths must exist") end it "accepts a 'recurse' option" do - File.expects(:lstat).with(somefile).returns stub("stat") + stub_file = stub(somefile, :lstat => stub('stat')) + Puppet::FileSystem::File.expects(:new).with(somefile).returns stub_file set = Puppet::FileServing::Fileset.new(somefile, :recurse => true) set.recurse.should be_true end it "accepts a 'recurselimit' option" do - File.expects(:lstat).with(somefile).returns stub("stat") + stub_file = stub(somefile, :lstat => stub('stat')) + Puppet::FileSystem::File.expects(:new).with(somefile).returns stub_file set = Puppet::FileServing::Fileset.new(somefile, :recurselimit => 3) set.recurselimit.should == 3 end it "accepts an 'ignore' option" do - File.expects(:lstat).with(somefile).returns stub("stat") + stub_file = stub(somefile, :lstat => stub('stat')) + Puppet::FileSystem::File.expects(:new).with(somefile).returns stub_file set = Puppet::FileServing::Fileset.new(somefile, :ignore => ".svn") set.ignore.should == [".svn"] end it "accepts a 'links' option" do - File.expects(:lstat).with(somefile).returns stub("stat") + stub_file = stub(somefile, :lstat => stub('stat')) + Puppet::FileSystem::File.expects(:new).with(somefile).returns stub_file set = Puppet::FileServing::Fileset.new(somefile, :links => :manage) set.links.should == :manage end it "accepts a 'checksum_type' option" do - File.expects(:lstat).with(somefile).returns stub("stat") + stub_file = stub(somefile, :lstat => stub('stat')) + Puppet::FileSystem::File.expects(:new).with(somefile).returns stub_file set = Puppet::FileServing::Fileset.new(somefile, :checksum_type => :test) set.checksum_type.should == :test end @@ -70,30 +79,35 @@ describe Puppet::FileServing::Fileset do end it "defaults to 'false' for recurse" do - File.expects(:lstat).with(somefile).returns stub("stat") + stub_file = stub(somefile, :lstat => stub('stat')) + Puppet::FileSystem::File.expects(:new).with(somefile).returns stub_file Puppet::FileServing::Fileset.new(somefile).recurse.should == false end it "defaults to :infinite for recurselimit" do - File.expects(:lstat).with(somefile).returns stub("stat") + stub_file = stub(somefile, :lstat => stub('stat')) + Puppet::FileSystem::File.expects(:new).with(somefile).returns stub_file Puppet::FileServing::Fileset.new(somefile).recurselimit.should == :infinite end it "defaults to an empty ignore list" do - File.expects(:lstat).with(somefile).returns stub("stat") + stub_file = stub(somefile, :lstat => stub('stat')) + Puppet::FileSystem::File.expects(:new).with(somefile).returns stub_file Puppet::FileServing::Fileset.new(somefile).ignore.should == [] end it "defaults to :manage for links" do - File.expects(:lstat).with(somefile).returns stub("stat") + stub_file = stub(somefile, :lstat => stub('stat')) + Puppet::FileSystem::File.expects(:new).with(somefile).returns stub_file Puppet::FileServing::Fileset.new(somefile).links.should == :manage end describe "using an indirector request" do let(:values) { { :links => :manage, :ignore => %w{a b}, :recurse => true, :recurselimit => 1234 } } + let(:stub_file) { stub(somefile, :lstat => stub('stat')) } before :each do - File.stubs(:lstat).returns stub("stat") + Puppet::FileSystem::File.expects(:new).with(somefile).returns stub_file end [:recurse, :recurselimit, :ignore, :links].each do |option| @@ -130,7 +144,8 @@ describe Puppet::FileServing::Fileset do context "when recursing" do before do @path = make_absolute("/my/path") - File.expects(:lstat).with(@path).returns stub("stat", :directory? => true) + @stub_file = stub(@path, :lstat => stub('stat', :directory? => true)) + Puppet::FileSystem::File.stubs(:new).with(@path).returns @stub_file @fileset = Puppet::FileServing::Fileset.new(@path) @dirstat = stub 'dirstat', :directory? => true @@ -138,7 +153,7 @@ describe Puppet::FileServing::Fileset do end def mock_dir_structure(path, stat_method = :lstat) - File.stubs(stat_method).with(path).returns(@dirstat) + @stub_file.stubs(stat_method).returns(@dirstat) Dir.stubs(:entries).with(path).returns(%w{one two .svn CVS}) # Keep track of the files we're stubbing. @@ -147,11 +162,14 @@ describe Puppet::FileServing::Fileset do %w{one two .svn CVS}.each do |subdir| @files << subdir # relative path subpath = File.join(path, subdir) - File.stubs(stat_method).with(subpath).returns(@dirstat) + stub_subpath = stub(subpath, stat_method => @dirstat) + Puppet::FileSystem::File.stubs(:new).with(subpath).returns stub_subpath Dir.stubs(:entries).with(subpath).returns(%w{.svn CVS file1 file2}) %w{file1 file2 .svn CVS}.each do |file| @files << File.join(subdir, file) # relative path - File.stubs(stat_method).with(File.join(subpath, file)).returns(@filestat) + subfile_path = File.join(subpath, file) + stub_subfile_path = stub(subfile_path, stat_method => @filestat) + Puppet::FileSystem::File.stubs(:new).with(subfile_path).returns stub_subfile_path end end end @@ -165,8 +183,10 @@ describe Puppet::FileServing::Fileset do MockDirectory = Struct.new(:name, :entries) do def mock(base_path) + extend Mocha::API path = File.join(base_path, name) - File.stubs(:lstat).with(path).returns(MockStat.new(path, true)) + stub_dir = stub(path, :lstat => MockStat.new(path, true)) + Puppet::FileSystem::File.stubs(:new).with(path).returns stub_dir Dir.stubs(:entries).with(path).returns(['.', '..'] + entries.map(&:name)) entries.each do |entry| entry.mock(path) @@ -176,8 +196,10 @@ describe Puppet::FileServing::Fileset do MockFile = Struct.new(:name) do def mock(base_path) + extend Mocha::API path = File.join(base_path, name) - File.stubs(:lstat).with(path).returns(MockStat.new(path, false)) + stub_file = stub(path, :lstat => MockStat.new(path, false)) + Puppet::FileSystem::File.stubs(:new).with(path).returns stub_file end end @@ -239,14 +261,14 @@ describe Puppet::FileServing::Fileset do @fileset.files.find { |file| file.include?(".svn") or file.include?("CVS") }.should be_nil end - it "uses File.stat if :links is set to :follow" do + it "uses Puppet::FileSystem::File#stat if :links is set to :follow" do mock_dir_structure(@path, :stat) @fileset.recurse = true @fileset.links = :follow @fileset.files.sort.should == @files.sort end - it "uses File.lstat if :links is set to :manage" do + it "uses Puppet::FileSystem::File#lstat if :links is set to :manage" do mock_dir_structure(@path, :lstat) @fileset.recurse = true @fileset.links = :manage @@ -255,7 +277,9 @@ describe Puppet::FileServing::Fileset do it "works when paths have regexp significant characters" do @path = make_absolute("/my/path/rV1x2DafFr0R6tGG+1bbk++++TM") - File.expects(:lstat).with(@path).returns stub("stat", :directory? => true) + stat = stub('dir_stat', :directory? => true) + stub_file = stub(@path, :stat => stat, :lstat => stat) + Puppet::FileSystem::File.expects(:new).with(@path).twice.returns stub_file @fileset = Puppet::FileServing::Fileset.new(@path) mock_dir_structure(@path) @fileset.recurse = true @@ -267,9 +291,13 @@ describe Puppet::FileServing::Fileset do path = make_absolute("/my/path") stat = stub 'stat', :directory? => true - File.expects(:lstat).with(path).returns(stat) - File.expects(:stat).with(path).returns(stat) - File.expects(:stat).with(File.join(path, "mylink")).raises(Errno::ENOENT) + mock_file = mock(path, :lstat => stat, :stat => stat) + Puppet::FileSystem::File.expects(:new).with(path).twice.returns mock_file + + link_path = File.join(path, "mylink") + mock_link = mock(link_path) + Puppet::FileSystem::File.expects(:new).with(link_path).returns mock_link + mock_link.expects(:stat).raises(Errno::ENOENT) Dir.stubs(:entries).with(path).returns(["mylink"]) @@ -284,10 +312,12 @@ describe Puppet::FileServing::Fileset do context "when merging other filesets" do before do @paths = [make_absolute("/first/path"), make_absolute("/second/path"), make_absolute("/third/path")] - File.stubs(:lstat).returns stub("stat", :directory? => false) + stub_file = stub(:lstat => stub('stat', :directory? => false)) + Puppet::FileSystem::File.stubs(:new).returns stub_file @filesets = @paths.collect do |path| - File.stubs(:lstat).with(path).returns stub("stat", :directory? => true) + stub_dir = stub(path, :lstat => stub('stat', :directory? => true)) + Puppet::FileSystem::File.stubs(:new).with(path).returns stub_dir Puppet::FileServing::Fileset.new(path, :recurse => true) end diff --git a/spec/unit/file_serving/metadata_spec.rb b/spec/unit/file_serving/metadata_spec.rb index 28d48e4ec..8b74d4b7e 100755 --- a/spec/unit/file_serving/metadata_spec.rb +++ b/spec/unit/file_serving/metadata_spec.rb @@ -3,6 +3,21 @@ require 'spec_helper' require 'puppet/file_serving/metadata' +# the json-schema gem doesn't support windows +if not Puppet.features.microsoft_windows? + FILE_METADATA_SCHEMA = JSON.parse(File.read(File.join(File.dirname(__FILE__), '../../../api/schemas/file_metadata.json'))) + + describe "catalog schema" do + it "should validate against the json meta-schema" do + JSON::Validator.validate!(JSON_META_SCHEMA, FILE_METADATA_SCHEMA) + end + end + + def validate_json_for_file_metadata(file_metadata) + JSON::Validator.validate!(FILE_METADATA_SCHEMA, file_metadata.to_pson) + end +end + describe Puppet::FileServing::Metadata do let(:foobar) { File.expand_path('/foo/bar') } @@ -84,7 +99,6 @@ describe Puppet::FileServing::Metadata do it "should pass the checksum in the hash verbatim as the checksum's value" do metadata.to_pson_data_hash['data']['checksum']['value'].should == metadata.checksum end - end end @@ -139,6 +153,10 @@ describe Puppet::FileServing::Metadata do metadata.checksum.should == "{mtime}#{@time}" end end + + it "should validate against the schema", :unless => Puppet.features.microsoft_windows? do + validate_json_for_file_metadata(metadata) + end end describe "when managing directories" do @@ -160,25 +178,45 @@ describe Puppet::FileServing::Metadata do metadata.collect metadata.checksum.should == "{ctime}#{time}" end + + it "should validate against the schema", :unless => Puppet.features.microsoft_windows? do + metadata.collect + validate_json_for_file_metadata(metadata) + end end + end + end - describe "when managing links", :unless => Puppet.features.microsoft_windows? do + shared_examples_for "metadata collector symlinks" do + + let(:metadata) do + data = described_class.new(path) + data.collect + data + end + + describe "when collecting attributes" do + describe "when managing links" do # 'path' is a link that points to 'target' let(:path) { tmpfile('file_serving_metadata_link') } let(:target) { tmpfile('file_serving_metadata_target') } let(:checksum) { Digest::MD5.hexdigest("some content\n") } - let(:fmode) { File.lstat(path).mode & 0777 } + let(:fmode) { Puppet::FileSystem::File.new(path).lstat.mode & 0777 } before :each do File.open(target, "wb") {|f| f.print("some content\n")} set_mode(0644, target) - FileUtils.symlink(target, path) + Puppet::FileSystem::File.new(target).symlink(path) end it "should read links instead of returning their checksums" do metadata.destination.should == target end + + it "should validate against the schema", :unless => Puppet.features.microsoft_windows? do + validate_json_for_file_metadata(metadata) + end end end @@ -209,6 +247,10 @@ describe Puppet::FileServing::Metadata do proc { metadata.collect}.should raise_error(Errno::ENOENT) end + + it "should validate against the schema", :unless => Puppet.features.microsoft_windows? do + validate_json_for_file_metadata(metadata) + end end end @@ -222,6 +264,7 @@ describe Puppet::FileServing::Metadata do end it_should_behave_like "metadata collector" + it_should_behave_like "metadata collector symlinks" def set_mode(mode, path) File.chmod(mode, path) @@ -239,6 +282,7 @@ describe Puppet::FileServing::Metadata do end it_should_behave_like "metadata collector" + it_should_behave_like "metadata collector symlinks" if Puppet.features.manages_symlinks? describe "if ACL metadata cannot be collected" do let(:path) { tmpdir('file_serving_metadata_acl') } @@ -274,16 +318,24 @@ describe Puppet::FileServing::Metadata do end -describe Puppet::FileServing::Metadata, " when pointing to a link", :unless => Puppet.features.microsoft_windows? do +describe Puppet::FileServing::Metadata, " when pointing to a link", :if => Puppet.features.manages_symlinks? do describe "when links are managed" do before do - @file = Puppet::FileServing::Metadata.new("/base/path/my/file", :links => :manage) - File.expects(:lstat).with("/base/path/my/file").returns stub("stat", :uid => 1, :gid => 2, :ftype => "link", :mode => 0755) - File.expects(:readlink).with("/base/path/my/file").returns "/some/other/path" - + path = "/base/path/my/file" + @file = Puppet::FileServing::Metadata.new(path, :links => :manage) + stat = stub("stat", :uid => 1, :gid => 2, :ftype => "link", :mode => 0755) + stub_file = stub(:readlink => "/some/other/path", :lstat => stat) + Puppet::FileSystem::File.expects(:new).with(path).at_least_once.returns stub_file @checksum = Digest::MD5.hexdigest("some content\n") # Remove these when :managed links are no longer checksumed. @file.stubs(:md5_file).returns(@checksum) # + + if Puppet.features.microsoft_windows? + win_stat = stub('win_stat', :owner => 'snarf', :group => 'thundercats', + :ftype => 'link', :mode => 0755) + Puppet::FileServing::Metadata::WindowsStat.stubs(:new).returns win_stat + end end + it "should store the destination of the link in :destination if links are :manage" do @file.collect @file.destination.should == "/some/other/path" @@ -301,9 +353,19 @@ describe Puppet::FileServing::Metadata, " when pointing to a link", :unless => P describe "when links are followed" do before do - @file = Puppet::FileServing::Metadata.new("/base/path/my/file", :links => :follow) - File.expects(:stat).with("/base/path/my/file").returns stub("stat", :uid => 1, :gid => 2, :ftype => "file", :mode => 0755) - File.expects(:readlink).with("/base/path/my/file").never + path = "/base/path/my/file" + @file = Puppet::FileServing::Metadata.new(path, :links => :follow) + stat = stub("stat", :uid => 1, :gid => 2, :ftype => "file", :mode => 0755) + mocked_file = mock(path, :stat => stat) + Puppet::FileSystem::File.expects(:new).with(path).at_least_once.returns mocked_file + mocked_file.expects(:readlink).never + + if Puppet.features.microsoft_windows? + win_stat = stub('win_stat', :owner => 'snarf', :group => 'thundercats', + :ftype => 'file', :mode => 0755) + Puppet::FileServing::Metadata::WindowsStat.stubs(:new).returns win_stat + end + @checksum = Digest::MD5.hexdigest("some content\n") @file.stubs(:md5_file).returns(@checksum) end diff --git a/spec/unit/file_serving/mount/file_spec.rb b/spec/unit/file_serving/mount/file_spec.rb index 59394be46..5821f3f98 100755 --- a/spec/unit/file_serving/mount/file_spec.rb +++ b/spec/unit/file_serving/mount/file_spec.rb @@ -85,7 +85,7 @@ describe Puppet::FileServing::Mount::File do include FileServingMountTesting before do - FileTest.stubs(:exist?).returns(true) + Puppet::FileSystem::File.stubs(:exist?).returns(true) FileTest.stubs(:directory?).returns(true) FileTest.stubs(:readable?).returns(true) @mount = Puppet::FileServing::Mount::File.new("test") @@ -95,12 +95,12 @@ describe Puppet::FileServing::Mount::File do end it "should return nil if the file is absent" do - FileTest.stubs(:exist?).returns(false) + Puppet::FileSystem::File.stubs(:exist?).returns(false) @mount.complete_path("/my/path", nil).should be_nil end it "should write a log message if the file is absent" do - FileTest.stubs(:exist?).returns(false) + Puppet::FileSystem::File.stubs(:exist?).returns(false) Puppet.expects(:info).with("File does not exist or is not accessible: /mount/my/path") @@ -108,12 +108,12 @@ describe Puppet::FileServing::Mount::File do end it "should return the file path if the file is present" do - FileTest.stubs(:exist?).with("/my/path").returns(true) + Puppet::FileSystem::File.stubs(:exist?).with("/my/path").returns(true) @mount.complete_path("/my/path", nil).should == "/mount/my/path" end it "should treat a nil file name as the path to the mount itself" do - FileTest.stubs(:exist?).returns(true) + Puppet::FileSystem::File.stubs(:exist?).returns(true) @mount.complete_path(nil, nil).should == "/mount" end @@ -141,7 +141,7 @@ describe Puppet::FileServing::Mount::File do include FileServingMountTesting before do - FileTest.stubs(:exist?).returns(true) + Puppet::FileSystem::File.stubs(:exist?).returns(true) FileTest.stubs(:directory?).returns(true) FileTest.stubs(:readable?).returns(true) @mount = Puppet::FileServing::Mount::File.new("test") @@ -153,7 +153,7 @@ describe Puppet::FileServing::Mount::File do end it "should return the results of the complete file path" do - FileTest.stubs(:exist?).returns(false) + Puppet::FileSystem::File.stubs(:exist?).returns(false) @mount.expects(:complete_path).with("/my/path", "foo").returns "eh" @mount.find("/my/path", @request).should == "eh" end @@ -163,7 +163,7 @@ describe Puppet::FileServing::Mount::File do include FileServingMountTesting before do - FileTest.stubs(:exist?).returns(true) + Puppet::FileSystem::File.stubs(:exist?).returns(true) FileTest.stubs(:directory?).returns(true) FileTest.stubs(:readable?).returns(true) @mount = Puppet::FileServing::Mount::File.new("test") @@ -175,13 +175,13 @@ describe Puppet::FileServing::Mount::File do end it "should return the results of the complete file path as an array" do - FileTest.stubs(:exist?).returns(false) + Puppet::FileSystem::File.stubs(:exist?).returns(false) @mount.expects(:complete_path).with("/my/path", "foo").returns "eh" @mount.search("/my/path", @request).should == ["eh"] end it "should return nil if the complete path is nil" do - FileTest.stubs(:exist?).returns(false) + Puppet::FileSystem::File.stubs(:exist?).returns(false) @mount.expects(:complete_path).with("/my/path", "foo").returns nil @mount.search("/my/path", @request).should be_nil end diff --git a/spec/unit/file_serving/mount/pluginfacts_spec.rb b/spec/unit/file_serving/mount/pluginfacts_spec.rb new file mode 100755 index 000000000..a54e45e4c --- /dev/null +++ b/spec/unit/file_serving/mount/pluginfacts_spec.rb @@ -0,0 +1,73 @@ +#! /usr/bin/env ruby +require 'spec_helper' +require 'puppet/file_serving/mount/pluginfacts' + +describe Puppet::FileServing::Mount::PluginFacts do + before do + @mount = Puppet::FileServing::Mount::PluginFacts.new("pluginfacts") + + @environment = stub 'environment', :module => nil + @options = { :recurse => true } + @request = stub 'request', :environment => @environment, :options => @options + end + + describe "when finding files" do + it "should use the provided environment to find the modules" do + @environment.expects(:modules).returns [] + + @mount.find("foo", @request) + end + + it "should return nil if no module can be found with a matching plugin" do + mod = mock 'module' + mod.stubs(:pluginfact).with("foo/bar").returns nil + + @environment.stubs(:modules).returns [mod] + @mount.find("foo/bar", @request).should be_nil + end + + it "should return the file path from the module" do + mod = mock 'module' + mod.stubs(:pluginfact).with("foo/bar").returns "eh" + + @environment.stubs(:modules).returns [mod] + @mount.find("foo/bar", @request).should == "eh" + end + end + + describe "when searching for files" do + it "should use the node's environment to find the modules" do + @environment.expects(:modules).at_least_once.returns [] + @environment.stubs(:modulepath).returns ["/tmp/modules"] + + @mount.search("foo", @request) + end + + it "should return modulepath if no modules can be found that have plugins" do + mod = mock 'module' + mod.stubs(:pluginfacts?).returns false + + @environment.stubs(:modules).returns [] + @environment.stubs(:modulepath).returns ["/"] + @options.expects(:[]=).with(:recurse, false) + @mount.search("foo/bar", @request).should == ["/"] + end + + it "should return nil if no modules can be found that have plugins and modulepath is invalid" do + mod = mock 'module' + mod.stubs(:pluginfacts?).returns false + + @environment.stubs(:modules).returns [] + @environment.stubs(:modulepath).returns [] + @mount.search("foo/bar", @request).should be_nil + end + + it "should return the plugin paths for each module that has plugins" do + one = stub 'module', :pluginfacts? => true, :plugin_fact_directory => "/one" + two = stub 'module', :pluginfacts? => true, :plugin_fact_directory => "/two" + + @environment.stubs(:modules).returns [one, two] + @mount.search("foo/bar", @request).should == %w{/one /two} + end + end +end diff --git a/spec/unit/file_system/file_spec.rb b/spec/unit/file_system/file_spec.rb new file mode 100644 index 000000000..564d4096c --- /dev/null +++ b/spec/unit/file_system/file_spec.rb @@ -0,0 +1,486 @@ +require 'spec_helper' +require 'puppet/file_system' +require 'puppet/util/platform' + +describe Puppet::FileSystem::File do + include PuppetSpec::Files + + context "#exclusive_open" do + it "opens ands allows updating of an existing file" do + file = Puppet::FileSystem::File.new(file_containing("file_to_update", "the contents")) + + file.exclusive_open(0660, 'r+') do |fh| + old = fh.read + fh.truncate(0) + fh.rewind + fh.write("updated #{old}") + end + + expect(file.read).to eq("updated the contents") + end + + it "opens, creates ands allows updating of a new file" do + file = Puppet::FileSystem::File.new(tmpfile("file_to_update")) + + file.exclusive_open(0660, 'w') do |fh| + fh.write("updated new file") + end + + expect(file.read).to eq("updated new file") + end + + it "excludes other processes from updating at the same time", :unless => Puppet::Util::Platform.windows? do + file = Puppet::FileSystem::File.new(file_containing("file_to_update", "0")) + + increment_counter_in_multiple_processes(file, 5, 'r+') + + expect(file.read).to eq("5") + end + + it "excludes other processes from updating at the same time even when creating the file", :unless => Puppet::Util::Platform.windows? do + file = Puppet::FileSystem::File.new(tmpfile("file_to_update")) + + increment_counter_in_multiple_processes(file, 5, 'a+') + + expect(file.read).to eq("5") + end + + it "times out if the lock cannot be aquired in a specified amount of time", :unless => Puppet::Util::Platform.windows? do + file = tmpfile("file_to_update") + + child = spawn_process_that_locks(file) + + expect do + Puppet::FileSystem::File.new(file).exclusive_open(0666, 'a', 0.1) do |f| + end + end.to raise_error(Timeout::Error) + + Process.kill(9, child) + end + + def spawn_process_that_locks(file) + read, write = IO.pipe + + child = Kernel.fork do + read.close + Puppet::FileSystem::File.new(file).exclusive_open(0666, 'a') do |fh| + write.write(true) + write.close + sleep 10 + end + end + + write.close + read.read + read.close + + child + end + + def increment_counter_in_multiple_processes(file, num_procs, options) + children = [] + 5.times do |number| + children << Kernel.fork do + file.exclusive_open(0660, options) do |fh| + fh.rewind + contents = (fh.read || 0).to_i + fh.truncate(0) + fh.rewind + fh.write((contents + 1).to_s) + end + exit(0) + end + end + + children.each { |pid| Process.wait(pid) } + end + end + + describe "symlink", + :if => ! Puppet.features.manages_symlinks? && + Puppet.features.microsoft_windows? do + + let (:file) { Puppet::FileSystem::File.new(tmpfile("somefile")) } + let (:missing_file) { Puppet::FileSystem::File.new(tmpfile("missingfile")) } + let (:expected_msg) { "This version of Windows does not support symlinks. Windows Vista / 2008 or higher is required." } + + before :each do + FileUtils.touch(file.path) + end + + it "should raise an error when trying to create a symlink" do + expect { file.symlink('foo') }.to raise_error(Puppet::Util::Windows::Error) + end + + it "should return false when trying to check if a path is a symlink" do + file.symlink?.should be_false + end + + it "should raise an error when trying to read a symlink" do + expect { file.readlink }.to raise_error(Puppet::Util::Windows::Error) + end + + it "should return a File::Stat instance when calling stat on an existing file" do + file.stat.should be_instance_of(File::Stat) + end + + it "should raise Errno::ENOENT when calling stat on a missing file" do + expect { missing_file.stat }.to raise_error(Errno::ENOENT) + end + + it "should fall back to stat when trying to lstat a file" do + Puppet::Util::Windows::File.expects(:stat).with(file.path) + + file.lstat + end + end + + describe "symlink", :if => Puppet.features.manages_symlinks? do + + let (:file) { Puppet::FileSystem::File.new(tmpfile("somefile")) } + let (:missing_file) { Puppet::FileSystem::File.new(tmpfile("missingfile")) } + let (:dir) { Puppet::FileSystem::File.new(tmpdir("somedir")) } + + before :each do + FileUtils.touch(file.path) + end + + it "should return true for exist? on a present file" do + file.exist?.should be_true + Puppet::FileSystem::File.exist?(file.path).should be_true + end + + it "should return false for exist? on a non-existant file" do + missing_file.exist?.should be_false + Puppet::FileSystem::File.exist?(missing_file.path).should be_false + end + + it "should return true for exist? on a present directory" do + dir.exist?.should be_true + Puppet::FileSystem::File.exist?(dir.path).should be_true + end + + it "should return false for exist? on a dangling symlink" do + symlink = Puppet::FileSystem::File.new(tmpfile("somefile_link")) + missing_file.symlink(symlink.path) + + missing_file.exist?.should be_false + symlink.exist?.should be_false + end + + it "should return true for exist? on valid symlinks" do + [file, dir].each do |target| + symlink = Puppet::FileSystem::File.new(tmpfile("#{target.path.basename.to_s}_link")) + target.symlink(symlink.path) + + target.exist?.should be_true + symlink.exist?.should be_true + end + end + + it "should not create a symlink when the :noop option is specified" do + [file, dir].each do |target| + symlink = Puppet::FileSystem::File.new(tmpfile("#{target.path.basename.to_s}_link")) + target.symlink(symlink.path, { :noop => true }) + + target.exist?.should be_true + symlink.exist?.should be_false + end + end + + it "should raise Errno::EEXIST if trying to create a file / directory symlink when the symlink path already exists as a file" do + existing_file = Puppet::FileSystem::File.new(tmpfile("#{file.path.basename.to_s}_link")) + FileUtils.touch(existing_file.path) + + [file, dir].each do |target| + expect { target.symlink(existing_file.path) }.to raise_error(Errno::EEXIST) + + existing_file.exist?.should be_true + existing_file.symlink?.should be_false + end + end + + it "should silently fail if trying to create a file / directory symlink when the symlink path already exists as a directory" do + existing_dir = Puppet::FileSystem::File.new(tmpdir("#{file.path.basename.to_s}_dir")) + + [file, dir].each do |target| + target.symlink(existing_dir.path).should == 0 + + existing_dir.exist?.should be_true + File.directory?(existing_dir.path).should be_true + existing_dir.symlink?.should be_false + end + end + + it "should silently fail to modify an existing directory symlink to reference a new file or directory" do + [file, dir].each do |target| + existing_dir = Puppet::FileSystem::File.new(tmpdir("#{target.path.basename.to_s}_dir")) + symlink = Puppet::FileSystem::File.new(tmpfile("#{existing_dir.path.basename.to_s}_link")) + existing_dir.symlink(symlink.path) + + symlink.readlink.should == existing_dir.path.to_s + + # now try to point it at the new target, no error raised, but file system unchanged + target.symlink(symlink.path).should == 0 + symlink.readlink.should == existing_dir.path.to_s + end + end + + it "should raise Errno::EEXIST if trying to modify a file symlink to reference a new file or directory" do + symlink = Puppet::FileSystem::File.new(tmpfile("#{file.path.basename.to_s}_link")) + file_2 = Puppet::FileSystem::File.new(tmpfile("#{file.path.basename.to_s}_2")) + FileUtils.touch(file_2.path) + # symlink -> file_2 + file_2.symlink(symlink.path) + + [file, dir].each do |target| + expect { target.symlink(symlink.path) }.to raise_error(Errno::EEXIST) + symlink.readlink.should == file_2.path.to_s + end + end + + it "should delete the existing file when creating a file / directory symlink with :force when the symlink path exists as a file" do + [file, dir].each do |target| + existing_file = Puppet::FileSystem::File.new(tmpfile("#{target.path.basename.to_s}_existing")) + FileUtils.touch(existing_file.path) + existing_file.symlink?.should be_false + + target.symlink(existing_file.path, { :force => true }) + + existing_file.symlink?.should be_true + existing_file.readlink.should == target.path.to_s + end + end + + it "should modify an existing file symlink when using :force to reference a new file or directory" do + [file, dir].each do |target| + existing_file = Puppet::FileSystem::File.new(tmpfile("#{target.path.basename.to_s}_existing")) + FileUtils.touch(existing_file.path) + existing_symlink = Puppet::FileSystem::File.new(tmpfile("#{existing_file.path.basename.to_s}_link")) + existing_file.symlink(existing_symlink.path) + + existing_symlink.readlink.should == existing_file.path.to_s + + target.symlink(existing_symlink.path, { :force => true }) + + existing_symlink.readlink.should == target.path.to_s + end + end + + it "should silently fail if trying to overwrite an existing directory with a new symlink when using :force to reference a file or directory" do + [file, dir].each do |target| + existing_dir = Puppet::FileSystem::File.new(tmpdir("#{target.path.basename.to_s}_existing")) + + target.symlink(existing_dir.path, { :force => true }).should == 0 + + existing_dir.symlink?.should be_false + end + end + + it "should silently fail if trying to modify an existing directory symlink when using :force to reference a new file or directory" do + [file, dir].each do |target| + existing_dir = Puppet::FileSystem::File.new(tmpdir("#{target.path.basename.to_s}_existing")) + existing_symlink = Puppet::FileSystem::File.new(tmpfile("#{existing_dir.path.basename.to_s}_link")) + existing_dir.symlink(existing_symlink.path) + + existing_symlink.readlink.should == existing_dir.path.to_s + + target.symlink(existing_symlink.path, { :force => true }).should == 0 + + existing_symlink.readlink.should == existing_dir.path.to_s + end + end + + it "should accept a string, Pathname or object with to_str (Puppet::Util::WatchedFile) for exist?" do + [ tmpfile('bogus1'), + Pathname.new(tmpfile('bogus2')), + Puppet::Util::WatchedFile.new(tmpfile('bogus3')) + ].each { |f| Puppet::FileSystem::File.exist?(f).should be_false } + end + + it "should return a File::Stat instance when calling stat on an existing file" do + file.stat.should be_instance_of(File::Stat) + end + + it "should raise Errno::ENOENT when calling stat on a missing file" do + expect { missing_file.stat }.to raise_error(Errno::ENOENT) + end + + it "should be able to create a symlink, and verify it with symlink?" do + symlink = Puppet::FileSystem::File.new(tmpfile("somefile_link")) + file.symlink(symlink.path) + + symlink.symlink?.should be_true + end + + it "should report symlink? as false on file, directory and missing files" do + [file, dir, missing_file].each do |f| + f.symlink?.should be_false + end + end + + it "should return a File::Stat with ftype 'link' when calling lstat on a symlink pointing to existing file" do + symlink = Puppet::FileSystem::File.new(tmpfile("somefile_link")) + file.symlink(symlink.path) + + stat = symlink.lstat + stat.should be_instance_of(File::Stat) + stat.ftype.should == 'link' + end + + it "should return a File::Stat of ftype 'link' when calling lstat on a symlink pointing to missing file" do + symlink = Puppet::FileSystem::File.new(tmpfile("somefile_link")) + missing_file.symlink(symlink.path) + + stat = symlink.lstat + stat.should be_instance_of(File::Stat) + stat.ftype.should == 'link' + end + + it "should return a File::Stat of ftype 'file' when calling stat on a symlink pointing to existing file" do + symlink = Puppet::FileSystem::File.new(tmpfile("somefile_link")) + file.symlink(symlink.path) + + stat = symlink.stat + stat.should be_instance_of(File::Stat) + stat.ftype.should == 'file' + end + + it "should return a File::Stat of ftype 'directory' when calling stat on a symlink pointing to existing directory" do + symlink = Puppet::FileSystem::File.new(tmpfile("somefile_link")) + dir.symlink(symlink.path) + + stat = symlink.stat + stat.should be_instance_of(File::Stat) + stat.ftype.should == 'directory' + end + + it "should return a File::Stat of ftype 'file' when calling stat on a symlink pointing to another symlink" do + # point symlink -> file + symlink = Puppet::FileSystem::File.new(tmpfile("somefile_link")) + file.symlink(symlink.path) + + # point symlink2 -> symlink + symlink2 = Puppet::FileSystem::File.new(tmpfile("somefile_link2")) + symlink.symlink(symlink2.path) + + symlink2.stat.ftype.should == 'file' + end + + + it "should raise Errno::ENOENT when calling stat on a dangling symlink" do + symlink = Puppet::FileSystem::File.new(tmpfile("somefile_link")) + missing_file.symlink(symlink.path) + + expect { symlink.stat }.to raise_error(Errno::ENOENT) + end + + it "should be able to readlink to resolve the physical path to a symlink" do + symlink = Puppet::FileSystem::File.new(tmpfile("somefile_link")) + file.symlink(symlink.path) + + file.exist?.should be_true + symlink.readlink.should == file.path.to_s + end + + it "should not resolve entire symlink chain with readlink on a symlink'd symlink" do + # point symlink -> file + symlink = Puppet::FileSystem::File.new(tmpfile("somefile_link")) + file.symlink(symlink.path) + + # point symlink2 -> symlink + symlink2 = Puppet::FileSystem::File.new(tmpfile("somefile_link2")) + symlink.symlink(symlink2.path) + + file.exist?.should be_true + symlink2.readlink.should == symlink.path.to_s + end + + it "should be able to readlink to resolve the physical path to a dangling symlink" do + symlink = Puppet::FileSystem::File.new(tmpfile("somefile_link")) + missing_file.symlink(symlink.path) + + missing_file.exist?.should be_false + symlink.readlink.should == missing_file.path.to_s + end + + it "should delete only the symlink and not the target when calling unlink instance method" do + [file, dir].each do |target| + symlink = Puppet::FileSystem::File.new(tmpfile("#{target.path.basename.to_s}_link")) + target.symlink(symlink.path) + + target.exist?.should be_true + symlink.readlink.should == target.path.to_s + + symlink.unlink.should == 1 # count of files + + target.exist?.should be_true + symlink.exist?.should be_false + end + end + + it "should delete only the symlink and not the target when calling unlink class method" do + [file, dir].each do |target| + symlink = Puppet::FileSystem::File.new(tmpfile("#{target.path.basename.to_s}_link")) + target.symlink(symlink.path) + + target.exist?.should be_true + symlink.readlink.should == target.path.to_s + + Puppet::FileSystem::File.unlink(symlink.path).should == 1 # count of files + + target.exist?.should be_true + symlink.exist?.should be_false + end + end + + describe "unlink" do + it "should delete files with unlink" do + file.exist?.should be_true + + file.unlink.should == 1 # count of files + + file.exist?.should be_false + end + + it "should delete files with unlink class method" do + file.exist?.should be_true + + Puppet::FileSystem::File.unlink(file.path).should == 1 # count of files + + file.exist?.should be_false + end + + it "should delete multiple files with unlink class method" do + paths = (1..3).collect do |i| + f = Puppet::FileSystem::File.new(tmpfile("somefile_#{i}")) + FileUtils.touch(f.path) + f.exist?.should be_true + f.path.to_s + end + + Puppet::FileSystem::File.unlink(*paths).should == 3 # count of files + + paths.each { |p| Puppet::FileSystem::File.exist?(p).should be_false } + end + + it "should raise Errno::EPERM or Errno::EISDIR when trying to delete a directory with the unlink class method" do + dir.exist?.should be_true + + ex = nil + begin + Puppet::FileSystem::File.unlink(dir.path) + rescue Exception => e + ex = e + end + + [ + Errno::EPERM, # Windows and OSX + Errno::EISDIR # Linux + ].should include ex.class + + dir.exist?.should be_true + end + end + end +end diff --git a/spec/unit/file_system/tempfile_spec.rb b/spec/unit/file_system/tempfile_spec.rb new file mode 100644 index 000000000..5ad0a8abc --- /dev/null +++ b/spec/unit/file_system/tempfile_spec.rb @@ -0,0 +1,48 @@ +require 'spec_helper' + +describe Puppet::FileSystem::Tempfile do + it "makes the name of the file available" do + Puppet::FileSystem::Tempfile.open('foo') do |file| + expect(file.path).to match(/foo/) + end + end + + it "provides a writeable file" do + Puppet::FileSystem::Tempfile.open('foo') do |file| + file.write("stuff") + file.flush + + expect(Puppet::FileSystem::File.new(file.path).read).to eq("stuff") + end + end + + it "returns the value of the block" do + the_value = Puppet::FileSystem::Tempfile.open('foo') do |file| + "my value" + end + + expect(the_value).to eq("my value") + end + + it "unlinks the temporary file" do + filename = Puppet::FileSystem::Tempfile.open('foo') do |file| + file.path + end + + expect(Puppet::FileSystem::File.new(filename).exist?).to be_false + end + + it "unlinks the temporary file even if the block raises an error" do + filename = nil + + begin + Puppet::FileSystem::Tempfile.open('foo') do |file| + filename = file.path + raise "error!" + end + rescue + end + + expect(Puppet::FileSystem::File.new(filename).exist?).to be_false + end +end diff --git a/spec/unit/graph/relationship_graph_spec.rb b/spec/unit/graph/relationship_graph_spec.rb index 7a698d769..8bd35de4d 100755 --- a/spec/unit/graph/relationship_graph_spec.rb +++ b/spec/unit/graph/relationship_graph_spec.rb @@ -165,12 +165,6 @@ describe Puppet::Graph::RelationshipGraph do expect(order_resources_traversed_in(relationships)).to( include_in_order("Notify[a]", "Notify[b]", "Notify[c]", "Notify[d]")) end - - def order_resources_traversed_in(relationships) - order_seen = [] - relationships.traverse { |resource| order_seen << resource.ref } - order_seen - end end describe "when interrupting traversal" do diff --git a/spec/unit/hiera_puppet_spec.rb b/spec/unit/hiera_puppet_spec.rb index 6c0f882a0..894b6e334 100644 --- a/spec/unit/hiera_puppet_spec.rb +++ b/spec/unit/hiera_puppet_spec.rb @@ -52,7 +52,7 @@ describe 'HieraPuppet' do pending("This example does not apply to Puppet #{Puppet.version} because it does not have this setting") end - File.stubs(:exist?).with(Puppet[:hiera_config]).returns(true) + Puppet::FileSystem::File.stubs(:exist?).with(Puppet[:hiera_config]).returns(true) HieraPuppet.send(:hiera_config_file).should == Puppet[:hiera_config] end @@ -64,7 +64,7 @@ describe 'HieraPuppet' do end Puppet.settings[:confdir] = "/dev/null/puppet" hiera_config = File.join(Puppet[:confdir], 'hiera.yaml') - File.stubs(:exist?).with(hiera_config).returns(true) + Puppet::FileSystem::File.stubs(:exist?).with(hiera_config).returns(true) HieraPuppet.send(:hiera_config_file).should == hiera_config end diff --git a/spec/unit/indirector/catalog/compiler_spec.rb b/spec/unit/indirector/catalog/compiler_spec.rb index ff32f7c7b..c968794e4 100755 --- a/spec/unit/indirector/catalog/compiler_spec.rb +++ b/spec/unit/indirector/catalog/compiler_spec.rb @@ -24,12 +24,12 @@ describe Puppet::Resource::Catalog::Compiler do end it "should cache the server metadata and reuse it" do + Puppet[:node_terminus] = :memory + Puppet::Node.indirection.save(Puppet::Node.new("node1")) + Puppet::Node.indirection.save(Puppet::Node.new("node2")) + compiler = Puppet::Resource::Catalog::Compiler.new - node1 = stub 'node1', :merge => nil - node2 = stub 'node2', :merge => nil compiler.stubs(:compile) - Puppet::Node.indirection.stubs(:find).with('node1', has_entry(:environment => anything)).returns(node1) - Puppet::Node.indirection.stubs(:find).with('node2', has_entry(:environment => anything)).returns(node2) compiler.find(Puppet::Indirector::Request.new(:catalog, :find, 'node1', nil, :node => 'node1')) compiler.find(Puppet::Indirector::Request.new(:catalog, :find, 'node2', nil, :node => 'node2')) @@ -175,19 +175,16 @@ describe Puppet::Resource::Catalog::Compiler do end describe "when finding nodes" do - before do + it "should look node information up via the Node class with the provided key" do Facter.stubs(:value).returns("whatever") - @compiler = Puppet::Resource::Catalog::Compiler.new - @name = "me" - @node = mock 'node' - @request = Puppet::Indirector::Request.new(:catalog, :find, @name, nil) - @compiler.stubs(:compile) - end + node = Puppet::Node.new('node') + compiler = Puppet::Resource::Catalog::Compiler.new + request = Puppet::Indirector::Request.new(:catalog, :find, "me", nil) + compiler.stubs(:compile) - it "should look node information up via the Node class with the provided key" do - @node.stubs :merge - Puppet::Node.indirection.expects(:find).with(@name, anything).returns(@node) - @compiler.find(@request) + Puppet::Node.indirection.expects(:find).with("me", anything).returns(node) + + compiler.find(request) end end @@ -197,11 +194,10 @@ describe Puppet::Resource::Catalog::Compiler do Facter.expects(:value).with('fqdn').returns("my.server.com") Facter.expects(:value).with('ipaddress').returns("my.ip.address") @compiler = Puppet::Resource::Catalog::Compiler.new - @name = "me" - @node = mock 'node' - @request = Puppet::Indirector::Request.new(:catalog, :find, @name, nil) + @node = Puppet::Node.new("me") + @request = Puppet::Indirector::Request.new(:catalog, :find, "me", nil) @compiler.stubs(:compile) - Puppet::Node.indirection.stubs(:find).with(@name, anything).returns(@node) + Puppet::Node.indirection.stubs(:find).with("me", anything).returns(@node) end it "should add the server's Puppet version to the node's parameters as 'serverversion'" do diff --git a/spec/unit/indirector/certificate_status/file_spec.rb b/spec/unit/indirector/certificate_status/file_spec.rb index c880bf786..83d4de210 100755 --- a/spec/unit/indirector/certificate_status/file_spec.rb +++ b/spec/unit/indirector/certificate_status/file_spec.rb @@ -87,16 +87,14 @@ describe "Puppet::Indirector::CertificateStatus::File" do retrieved_host.certificate_request.content.to_s.chomp.should == @host.certificate_request.content.to_s.chomp end - it "should return the Puppet::SSL::Host when a public key exist for the host" do - pending("porting to Windows", :if => Puppet.features.microsoft_windows?) do - generate_signed_cert(@host) - request = Puppet::Indirector::Request.new(:certificate_status, :find, "foo", @host) + it "should return the Puppet::SSL::Host when a public key exists for the host" do + generate_signed_cert(@host) + request = Puppet::Indirector::Request.new(:certificate_status, :find, "foo", @host) - retrieved_host = @terminus.find(request) + retrieved_host = @terminus.find(request) - retrieved_host.name.should == @host.name - retrieved_host.certificate.content.to_s.chomp.should == @host.certificate.content.to_s.chomp - end + retrieved_host.name.should == @host.name + retrieved_host.certificate.content.to_s.chomp.should == @host.certificate.content.to_s.chomp end it "should return nil when neither a CSR nor public key exist for the host" do @@ -122,12 +120,10 @@ describe "Puppet::Indirector::CertificateStatus::File" do end it "should sign the on-disk CSR when it is present" do - pending("porting to Windows", :if => Puppet.features.microsoft_windows?) do - signed_host = generate_signed_cert(@host) + signed_host = generate_signed_cert(@host) - signed_host.state.should == "signed" - Puppet::SSL::Certificate.indirection.find("foobar").should be_instance_of(Puppet::SSL::Certificate) - end + signed_host.state.should == "signed" + Puppet::SSL::Certificate.indirection.find("foobar").should be_instance_of(Puppet::SSL::Certificate) end end @@ -142,11 +138,9 @@ describe "Puppet::Indirector::CertificateStatus::File" do end it "should revoke the certificate when it is present" do - pending("porting to Windows", :if => Puppet.features.microsoft_windows?) do - generate_revoked_cert(@host) + generate_revoked_cert(@host) - @host.state.should == 'revoked' - end + @host.state.should == 'revoked' end end end @@ -163,39 +157,35 @@ describe "Puppet::Indirector::CertificateStatus::File" do end it "should clean certs, cert requests, keys" do - pending("porting to Windows", :if => Puppet.features.microsoft_windows?) do - signed_host = Puppet::SSL::Host.new("clean_signed_cert") - generate_signed_cert(signed_host) - signed_request = Puppet::Indirector::Request.new(:certificate_status, :delete, "clean_signed_cert", signed_host) - @terminus.destroy(signed_request).should == "Deleted for clean_signed_cert: Puppet::SSL::Certificate, Puppet::SSL::Key" - - requested_host = Puppet::SSL::Host.new("clean_csr") - generate_csr(requested_host) - csr_request = Puppet::Indirector::Request.new(:certificate_status, :delete, "clean_csr", requested_host) - @terminus.destroy(csr_request).should == "Deleted for clean_csr: Puppet::SSL::CertificateRequest, Puppet::SSL::Key" - end + signed_host = Puppet::SSL::Host.new("clean_signed_cert") + generate_signed_cert(signed_host) + signed_request = Puppet::Indirector::Request.new(:certificate_status, :delete, "clean_signed_cert", signed_host) + @terminus.destroy(signed_request).should == "Deleted for clean_signed_cert: Puppet::SSL::Certificate, Puppet::SSL::Key" + + requested_host = Puppet::SSL::Host.new("clean_csr") + generate_csr(requested_host) + csr_request = Puppet::Indirector::Request.new(:certificate_status, :delete, "clean_csr", requested_host) + @terminus.destroy(csr_request).should == "Deleted for clean_csr: Puppet::SSL::CertificateRequest, Puppet::SSL::Key" end end describe "when searching" do it "should return a list of all hosts with certificate requests, signed certs, or revoked certs" do - pending("porting to Windows", :if => Puppet.features.microsoft_windows?) do - Puppet.settings.use(:main) + Puppet.settings.use(:main) - signed_host = Puppet::SSL::Host.new("signed_host") - generate_signed_cert(signed_host) + signed_host = Puppet::SSL::Host.new("signed_host") + generate_signed_cert(signed_host) - requested_host = Puppet::SSL::Host.new("requested_host") - generate_csr(requested_host) + requested_host = Puppet::SSL::Host.new("requested_host") + generate_csr(requested_host) - revoked_host = Puppet::SSL::Host.new("revoked_host") - generate_revoked_cert(revoked_host) + revoked_host = Puppet::SSL::Host.new("revoked_host") + generate_revoked_cert(revoked_host) - retrieved_hosts = @terminus.search(Puppet::Indirector::Request.new(:certificate_status, :search, "all", signed_host)) + retrieved_hosts = @terminus.search(Puppet::Indirector::Request.new(:certificate_status, :search, "all", signed_host)) - results = retrieved_hosts.map {|h| [h.name, h.state]}.sort{ |h,i| h[0] <=> i[0] } - results.should == [["ca","signed"],["requested_host","requested"],["revoked_host","revoked"],["signed_host","signed"]] - end + results = retrieved_hosts.map {|h| [h.name, h.state]}.sort{ |h,i| h[0] <=> i[0] } + results.should == [["ca","signed"],["requested_host","requested"],["revoked_host","revoked"],["signed_host","signed"]] end end end diff --git a/spec/unit/indirector/data_binding/hiera_spec.rb b/spec/unit/indirector/data_binding/hiera_spec.rb index 98883bb9d..7ab3b27fc 100644 --- a/spec/unit/indirector/data_binding/hiera_spec.rb +++ b/spec/unit/indirector/data_binding/hiera_spec.rb @@ -2,8 +2,32 @@ require 'spec_helper' require 'puppet/indirector/data_binding/hiera' describe Puppet::DataBinding::Hiera do - it "should be a subclass of the Hiera terminus" do - Puppet::DataBinding::Hiera.superclass.should equal(Puppet::Indirector::Hiera) + include PuppetSpec::Files + + def write_hiera_config(config_file, datadir) + File.open(config_file, 'w') do |f| + f.write("--- + :yaml: + :datadir: #{datadir} + :hierarchy: ['global', 'invalid'] + :logger: 'noop' + :backends: ['yaml'] + ") + end + end + + def request(key) + Puppet::Indirector::Request.new(:hiera, :find, key, nil) + end + + before do + hiera_config_file = tmpfile("hiera.yaml") + Puppet.settings[:hiera_config] = hiera_config_file + write_hiera_config(hiera_config_file, my_fixture_dir) + end + + after do + Puppet::DataBinding::Hiera.instance_variable_set(:@hiera, nil) end it "should have documentation" do @@ -18,4 +42,73 @@ describe Puppet::DataBinding::Hiera do it "should have its name set to :hiera" do Puppet::DataBinding::Hiera.name.should == :hiera end + it "should be the default data_binding terminus" do + Puppet.settings[:data_binding_terminus].should == :hiera + end + + it "should raise an error if we don't have the hiera feature" do + Puppet.features.expects(:hiera?).returns(false) + lambda { Puppet::DataBinding::Hiera.new }.should raise_error RuntimeError, + "Hiera terminus not supported without hiera library" + end + + describe "the behavior of the hiera_config method", :if => Puppet.features.hiera? do + it "should override the logger and set it to puppet" do + Puppet::DataBinding::Hiera.hiera_config[:logger].should == "puppet" + end + + context "when the Hiera configuration file does not exist" do + let(:path) { File.expand_path('/doesnotexist') } + + before do + Puppet.settings[:hiera_config] = path + end + + it "should log a warning" do + Puppet.expects(:warning).with( + "Config file #{path} not found, using Hiera defaults") + Puppet::DataBinding::Hiera.hiera_config + end + + it "should only configure the logger and set it to puppet" do + Puppet.expects(:warning).with( + "Config file #{path} not found, using Hiera defaults") + Puppet::DataBinding::Hiera.hiera_config.should == { :logger => 'puppet' } + end + end + end + + describe "the behavior of the find method", :if => Puppet.features.hiera? do + + let(:data_binder) { Puppet::DataBinding::Hiera.new } + + it "should support looking up an integer" do + data_binder.find(request("integer")).should == 3000 + end + + it "should support looking up a string" do + data_binder.find(request("string")).should == 'apache' + end + + it "should support looking up an array" do + data_binder.find(request("array")).should == [ + '0.ntp.puppetlabs.com', + '1.ntp.puppetlabs.com', + ] + end + + it "should support looking up a hash" do + data_binder.find(request("hash")).should == { + 'user' => 'Hightower', + 'group' => 'admin', + 'mode' => '0644' + } + end + + it "raises a data binding error if hiera cannot parse the yaml data" do + expect do + data_binder.find(request('invalid')) + end.to raise_error(Puppet::DataBinding::LookupError) + end + end end diff --git a/spec/unit/indirector/direct_file_server_spec.rb b/spec/unit/indirector/direct_file_server_spec.rb index 234f25ef9..8b08195bb 100755 --- a/spec/unit/indirector/direct_file_server_spec.rb +++ b/spec/unit/indirector/direct_file_server_spec.rb @@ -26,12 +26,12 @@ describe Puppet::Indirector::DirectFileServer do describe Puppet::Indirector::DirectFileServer, "when finding a single file" do it "should return nil if the file does not exist" do - FileTest.expects(:exists?).with(@path).returns false + Puppet::FileSystem::File.expects(:exist?).with(@path).returns false @server.find(@request).should be_nil end it "should return a Content instance created with the full path to the file if the file exists" do - FileTest.expects(:exists?).with(@path).returns true + Puppet::FileSystem::File.expects(:exist?).with(@path).returns true @model.expects(:new).returns(:mycontent) @server.find(@request).should == :mycontent end @@ -42,7 +42,7 @@ describe Puppet::Indirector::DirectFileServer do before do @data = mock 'content' @data.stubs(:collect) - FileTest.expects(:exists?).with(@path).returns true + Puppet::FileSystem::File.expects(:exist?).with(@path).returns true end it "should pass the full path to the instance" do @@ -61,18 +61,18 @@ describe Puppet::Indirector::DirectFileServer do describe Puppet::Indirector::DirectFileServer, "when searching for multiple files" do it "should return nil if the file does not exist" do - FileTest.expects(:exists?).with(@path).returns false + Puppet::FileSystem::File.expects(:exist?).with(@path).returns false @server.find(@request).should be_nil end it "should use :path2instances from the terminus_helper to return instances if the file exists" do - FileTest.expects(:exists?).with(@path).returns true + Puppet::FileSystem::File.expects(:exist?).with(@path).returns true @server.expects(:path2instances) @server.search(@request) end it "should pass the original request to :path2instances" do - FileTest.expects(:exists?).with(@path).returns true + Puppet::FileSystem::File.expects(:exist?).with(@path).returns true @server.expects(:path2instances).with(@request, @path) @server.search(@request) end diff --git a/spec/unit/indirector/facts/facter_spec.rb b/spec/unit/indirector/facts/facter_spec.rb index 3e301a5a5..4c55ec6c3 100755 --- a/spec/unit/indirector/facts/facter_spec.rb +++ b/spec/unit/indirector/facts/facter_spec.rb @@ -48,6 +48,9 @@ describe Puppet::Node::Facts::Facter do Facter.stubs(:to_hash).returns({}) @name = "me" @request = stub 'request', :key => @name + @environment = stub 'environment' + @request.stubs(:environment).returns(@environment) + @request.environment.stubs(:modules).returns([]) end describe Puppet::Node::Facts::Facter, " when finding facts" do @@ -58,6 +61,15 @@ describe Puppet::Node::Facts::Facter do @facter.find(@request) end + it "should include external facts when feature is present" do + clear = sequence 'clear' + Puppet.features.stubs(:external_facts?).returns(:true) + Puppet::Node::Facts::Facter.expects(:setup_external_facts).in_sequence(clear) + Puppet::Node::Facts::Facter.expects(:reload_facter).in_sequence(clear) + Puppet::Node::Facts::Facter.expects(:load_fact_plugins).in_sequence(clear) + @facter.find(@request) + end + it "should return a Facts instance" do @facter.find(@request).should be_instance_of(Puppet::Node::Facts) end @@ -131,6 +143,20 @@ describe Puppet::Node::Facts::Facter do Puppet::Node::Facts::Facter.load_facts_in_dir("mydir") end + it "should include pluginfactdest when loading external facts", + :if => Puppet.features.external_facts?, :unless => Puppet.features.microsoft_windows? do + Puppet[:pluginfactdest] = "/plugin/dest" + @facter.find(@request) + Facter::Util::Config.external_facts_dirs.include?("/plugin/dest") + end + + it "should include pluginfactdest when loading external facts", + :if => Puppet.features.external_facts?, :if => Puppet.features.microsoft_windows? do + Puppet[:pluginfactdest] = "/plugin/dest" + @facter.find(@request) + Facter::Util::Config.external_facts_dirs.include?("C:/plugin/dest") + end + describe "when loading fact plugins from disk" do let(:one) { File.expand_path("one") } let(:two) { File.expand_path("two") } @@ -160,5 +186,12 @@ describe Puppet::Node::Facts::Facter do Puppet::Node::Facts::Facter.load_fact_plugins end + + it "should include module plugin facts when present", :if => Puppet.features.external_facts? do + mod = Puppet::Module.new("mymodule", "#{one}/mymodule", @request.environment) + @request.environment.stubs(:modules).returns([mod]) + @facter.find(@request) + Facter::Util::Config.external_facts_dirs.include?("#{one}/mymodule/facts.d") + end end end diff --git a/spec/unit/indirector/file_bucket_file/file_spec.rb b/spec/unit/indirector/file_bucket_file/file_spec.rb index 37529e5d5..2f9e140f7 100755 --- a/spec/unit/indirector/file_bucket_file/file_spec.rb +++ b/spec/unit/indirector/file_bucket_file/file_spec.rb @@ -2,18 +2,11 @@ require 'spec_helper' require 'puppet/indirector/file_bucket_file/file' +require 'puppet/util/platform' describe Puppet::FileBucketFile::File do include PuppetSpec::Files - it "should be a subclass of the Code terminus class" do - Puppet::FileBucketFile::File.superclass.should equal(Puppet::Indirector::Code) - end - - it "should have documentation" do - Puppet::FileBucketFile::File.doc.should be_instance_of(String) - end - describe "non-stubbing tests" do include PuppetSpec::Files @@ -23,7 +16,7 @@ describe Puppet::FileBucketFile::File do def save_bucket_file(contents, path = "/who_cares") bucket_file = Puppet::FileBucket::File.new(contents) - Puppet::FileBucket::File.indirection.save(bucket_file, "md5/#{Digest::MD5.hexdigest(contents)}#{path}") + Puppet::FileBucket::File.indirection.save(bucket_file, "#{bucket_file.name}#{path}") bucket_file.checksum_data end @@ -34,17 +27,58 @@ describe Puppet::FileBucketFile::File do result.contents.should be_empty end + it "deals with multiple processes saving at the same time", :unless => Puppet::Util::Platform.windows? do + bucket_file = Puppet::FileBucket::File.new("contents") + + children = [] + 5.times do |count| + children << Kernel.fork do + save_bucket_file("contents", "/testing") + exit(0) + end + end + children.each { |child| Process.wait(child) } + + paths = File.read("#{Puppet[:bucketdir]}/9/8/b/f/7/d/8/c/98bf7d8c15784f0a3d63204441e1e2aa/paths").lines.to_a + paths.length.should == 1 + Puppet::FileBucket::File.indirection.head("#{bucket_file.checksum_type}/#{bucket_file.checksum_data}/testing").should be_true + end + + it "fails if the contents collide with existing contents" do + # This is the shortest known MD5 collision. See http://eprint.iacr.org/2010/643.pdf + first_contents = [0x6165300e,0x87a79a55,0xf7c60bd0,0x34febd0b, + 0x6503cf04,0x854f709e,0xfb0fc034,0x874c9c65, + 0x2f94cc40,0x15a12deb,0x5c15f4a3,0x490786bb, + 0x6d658673,0xa4341f7d,0x8fd75920,0xefd18d5a].pack("I" * 16) + + collision_contents = [0x6165300e,0x87a79a55,0xf7c60bd0,0x34febd0b, + 0x6503cf04,0x854f749e,0xfb0fc034,0x874c9c65, + 0x2f94cc40,0x15a12deb,0xdc15f4a3,0x490786bb, + 0x6d658673,0xa4341f7d,0x8fd75920,0xefd18d5a].pack("I" * 16) + + save_bucket_file(first_contents, "/foo/bar") + + expect do + save_bucket_file(collision_contents, "/foo/bar") + end.to raise_error(Puppet::FileBucket::BucketError, /Got passed new contents/) + end + describe "when supplying a path" do it "should store the path if not already stored" do checksum = save_bucket_file("stuff\r\n", "/foo/bar") + dir_path = "#{Puppet[:bucketdir]}/f/c/7/7/7/c/0/b/fc777c0bc467e1ab98b4c6915af802ec" - IO.binread("#{dir_path}/contents").should == "stuff\r\n" - File.read("#{dir_path}/paths").should == "foo/bar\n" + contents_file = Puppet::FileSystem::File.new("#{dir_path}/contents") + paths_file = Puppet::FileSystem::File.new("#{dir_path}/paths") + contents_file.binread.should == "stuff\r\n" + paths_file.read.should == "foo/bar\n" end it "should leave the paths file alone if the path is already stored" do checksum = save_bucket_file("stuff", "/foo/bar") + checksum = save_bucket_file("stuff", "/foo/bar") + dir_path = "#{Puppet[:bucketdir]}/c/1/3/d/8/8/c/b/c13d88cb4cb02003daedb8a84e5d272a" File.read("#{dir_path}/contents").should == "stuff" File.read("#{dir_path}/paths").should == "foo/bar\n" @@ -52,7 +86,9 @@ describe Puppet::FileBucketFile::File do it "should store an additional path if the new path differs from those already stored" do checksum = save_bucket_file("stuff", "/foo/bar") + checksum = save_bucket_file("stuff", "/foo/baz") + dir_path = "#{Puppet[:bucketdir]}/c/1/3/d/8/8/c/b/c13d88cb4cb02003daedb8a84e5d272a" File.read("#{dir_path}/contents").should == "stuff" File.read("#{dir_path}/paths").should == "foo/bar\nfoo/baz\n" @@ -62,6 +98,7 @@ describe Puppet::FileBucketFile::File do describe "when not supplying a path" do it "should save the file and create an empty paths file" do checksum = save_bucket_file("stuff", "") + dir_path = "#{Puppet[:bucketdir]}/c/1/3/d/8/8/c/b/c13d88cb4cb02003daedb8a84e5d272a" File.read("#{dir_path}/contents").should == "stuff" File.read("#{dir_path}/paths").should == "" @@ -78,16 +115,18 @@ describe Puppet::FileBucketFile::File do it "should return false/nil if the file is bucketed but with a different path" do checksum = save_bucket_file("I'm the contents of a file", '/foo/bar') + Puppet::FileBucket::File.indirection.head("md5/#{checksum}/foo/baz").should == false Puppet::FileBucket::File.indirection.find("md5/#{checksum}/foo/baz").should == nil end it "should return true/file if the file is already bucketed with the given path" do contents = "I'm the contents of a file" + checksum = save_bucket_file(contents, '/foo/bar') + Puppet::FileBucket::File.indirection.head("md5/#{checksum}/foo/bar").should == true find_result = Puppet::FileBucket::File.indirection.find("md5/#{checksum}/foo/bar") - find_result.should be_a(Puppet::FileBucket::File) find_result.checksum.should == "{md5}#{checksum}" find_result.to_s.should == contents end @@ -105,10 +144,11 @@ describe Puppet::FileBucketFile::File do it "should return true/file if the file is already bucketed" do contents = "I'm the contents of a file" + checksum = save_bucket_file(contents, '/foo/bar') + Puppet::FileBucket::File.indirection.head("md5/#{checksum}#{trailing_string}").should == true find_result = Puppet::FileBucket::File.indirection.find("md5/#{checksum}#{trailing_string}") - find_result.should be_a(Puppet::FileBucket::File) find_result.checksum.should == "{md5}#{checksum}" find_result.to_s.should == contents end @@ -126,6 +166,7 @@ describe Puppet::FileBucketFile::File do it "should generate a proper diff if there is a diff" do checksum1 = save_bucket_file("foo\nbar\nbaz") checksum2 = save_bucket_file("foo\nbiz\nbaz") + diff = Puppet::FileBucket::File.indirection.find("md5/#{checksum1}", :diff_with => checksum2) diff.should == <<HERE 2c2 @@ -136,27 +177,23 @@ HERE end it "should raise an exception if the hash to diff against isn't found" do - checksum = save_bucket_file("whatever") bogus_checksum = "d1bf072d0e2c6e20e3fbd23f022089a1" - lambda { Puppet::FileBucket::File.indirection.find("md5/#{checksum}", :diff_with => bogus_checksum) }.should raise_error "could not find diff_with #{bogus_checksum}" + checksum = save_bucket_file("whatever") + + expect do + Puppet::FileBucket::File.indirection.find("md5/#{checksum}", :diff_with => bogus_checksum) + end.to raise_error "could not find diff_with #{bogus_checksum}" end it "should return nil if the hash to diff from isn't found" do - checksum = save_bucket_file("whatever") bogus_checksum = "d1bf072d0e2c6e20e3fbd23f022089a1" + checksum = save_bucket_file("whatever") + Puppet::FileBucket::File.indirection.find("md5/#{bogus_checksum}", :diff_with => checksum).should == nil end end end - describe "when initializing" do - it "should use the filebucket settings section" do - Puppet.settings.expects(:use).with(:filebucket) - Puppet::FileBucketFile::File.new - end - end - - [true, false].each do |override_bucket_path| describe "when bucket path #{if override_bucket_path then 'is' else 'is not' end} overridden" do [true, false].each do |supply_path| @@ -245,32 +282,4 @@ HERE end end end - - describe "when verifying identical files" do - let(:contents) { "file\r\n contents" } - let(:digest) { "8b3702ad1aed1ace7e32bde76ffffb2d" } - let(:checksum) { "{md5}#{@digest}" } - let(:bucketdir) { tmpdir('file_bucket_file') } - let(:destdir) { "#{bucketdir}/8/b/3/7/0/2/a/d/8b3702ad1aed1ace7e32bde76ffffb2d" } - let(:bucket) { Puppet::FileBucket::File.new(contents) } - - before :each do - Puppet[:bucketdir] = bucketdir - FileUtils.mkdir_p(destdir) - end - - it "should raise an error if the files don't match" do - File.open(File.join(destdir, 'contents'), 'wb') { |f| f.print "corrupt contents" } - - lambda{ - Puppet::FileBucketFile::File.new.send(:verify_identical_file!, bucket) - }.should raise_error(Puppet::FileBucket::BucketError) - end - - it "should do nothing if the files match" do - File.open(File.join(destdir, 'contents'), 'wb') { |f| f.print contents } - - Puppet::FileBucketFile::File.new.send(:verify_identical_file!, bucket) - end - end end diff --git a/spec/unit/indirector/file_metadata/file_spec.rb b/spec/unit/indirector/file_metadata/file_spec.rb index d51c65882..459c1dbe8 100755 --- a/spec/unit/indirector/file_metadata/file_spec.rb +++ b/spec/unit/indirector/file_metadata/file_spec.rb @@ -19,7 +19,7 @@ describe Puppet::Indirector::FileMetadata::File do @uri = Puppet::Util.path_to_uri(@path).to_s @data = mock 'metadata' @data.stubs(:collect) - FileTest.expects(:exists?).with(@path).returns true + Puppet::FileSystem::File.expects(:exist?).with(@path).returns true @request = Puppet::Indirector::Request.new(:file_metadata, :find, @uri, nil) end @@ -42,7 +42,7 @@ describe Puppet::Indirector::FileMetadata::File do end it "should collect the attributes of the instances returned" do - FileTest.expects(:exists?).with(@path).returns true + Puppet::FileSystem::File.expects(:exist?).with(@path).returns true @metadata.expects(:path2instances).returns( [mock("one", :collect => nil), mock("two", :collect => nil)] ) @metadata.search(@request) end diff --git a/spec/unit/indirector/file_server_spec.rb b/spec/unit/indirector/file_server_spec.rb index 00649875f..6f491b065 100755 --- a/spec/unit/indirector/file_server_spec.rb +++ b/spec/unit/indirector/file_server_spec.rb @@ -154,7 +154,7 @@ describe Puppet::Indirector::FileServer do @mount.expects(:search).with { |key, request| key == "rel/path" }.returns %w{/one /two} - FileTest.stubs(:exist?).returns true + Puppet::FileSystem::File.stubs(:exist?).returns true one = mock 'fileset_one' Puppet::FileServing::Fileset.expects(:new).with("/one", @request).returns(one) @@ -171,7 +171,7 @@ describe Puppet::Indirector::FileServer do @mount.expects(:search).with { |key, request| key == "rel/path" }.returns [] - FileTest.stubs(:exist?).returns true + Puppet::FileSystem::File.stubs(:exist?).returns true Puppet::FileServing::Fileset.expects(:merge).returns("one" => "/one", "two" => "/two") @@ -193,7 +193,7 @@ describe Puppet::Indirector::FileServer do @mount.expects(:search).with { |key, request| key == "rel/path" }.returns [] - FileTest.stubs(:exist?).returns true + Puppet::FileSystem::File.stubs(:exist?).returns true Puppet::FileServing::Fileset.expects(:merge).returns("one" => "/one") @@ -211,7 +211,7 @@ describe Puppet::Indirector::FileServer do @mount.expects(:search).with { |key, options| key == "rel/path" }.returns [] - FileTest.stubs(:exist?).returns true + Puppet::FileSystem::File.stubs(:exist?).returns true Puppet::FileServing::Fileset.expects(:merge).returns("one" => "/one") diff --git a/spec/unit/indirector/hiera_spec.rb b/spec/unit/indirector/hiera_spec.rb deleted file mode 100644 index 36b598c37..000000000 --- a/spec/unit/indirector/hiera_spec.rb +++ /dev/null @@ -1,154 +0,0 @@ -require 'spec_helper' -require 'puppet/indirector/hiera' -require 'hiera/backend' - -describe Puppet::Indirector::Hiera do - include PuppetSpec::Files - - def write_hiera_config(config_file, datadir) - File.open(config_file, 'w') do |f| - f.write("--- - :yaml: - :datadir: #{datadir} - :hierarchy: ['global'] - :logger: 'noop' - :backends: ['yaml'] - ") - end - end - - before do - Puppet.settings[:hiera_config] = hiera_config_file - write_hiera_config(hiera_config_file, datadir) - - Puppet::Indirector::Terminus.stubs(:register_terminus_class) - Puppet::Indirector::Indirection.stubs(:instance).returns(indirection) - - module Testing; end - @hiera_class = class Testing::Hiera < Puppet::Indirector::Hiera - self - end - end - - let(:model) { mock('model') } - let(:options) { {:host => 'foo' } } - - let(:request_integer) do - stub('request', :key => "integer", :options => options) - end - - let(:request_string) do - stub('request', :key => "string", :options => options) - end - - let(:request_array) do - stub('request', :key => "array", :options => options) - end - - let(:request_hash) do - stub('request', :key => "hash", :options => options) - end - - let(:indirection) do - stub('indirection', :name => :none, :register_terminus_type => nil, - :model => model) - end - - let(:facts) do - { 'fqdn' => 'agent.testing.com' } - end - let(:facter_obj) { stub(:values => facts) } - - let(:hiera_config_file) do - tmpfile("hiera.yaml") - end - - let(:datadir) { my_fixture_dir } - - it "should be the default data_binding terminus" do - Puppet.settings[:data_binding_terminus].should == :hiera - end - - it "should raise an error if we don't have the hiera feature" do - Puppet.features.expects(:hiera?).returns(false) - lambda { @hiera_class.new }.should raise_error RuntimeError, - "Hiera terminus not supported without hiera library" - end - - describe "the behavior of the hiera_config method", :if => Puppet.features.hiera? do - let(:default_hiera_config) do - { - :logger => "puppet", - :backends => ["yaml"], - :yaml => { :datadir => datadir }, - :hierarchy => ["global"] - } - end - - it "should load the hiera config file by delegating to Hiera" do - Hiera::Config.expects(:load).with(hiera_config_file).returns({}) - @hiera_class.hiera_config - end - - it "should override the logger and set it to puppet" do - @hiera_class.hiera_config[:logger].should == "puppet" - end - - it "should return a hiera configuration hash" do - results = @hiera_class.hiera_config - default_hiera_config.each do |key,value| - results[key].should == value - end - results.should be_a_kind_of Hash - end - - context "when the Hiera configuration file does not exist" do - let(:path) { File.expand_path('/doesnotexist') } - - before do - Puppet.settings[:hiera_config] = path - end - - it "should log a warning" do - Puppet.expects(:warning).with( - "Config file #{path} not found, using Hiera defaults") - @hiera_class.hiera_config - end - - it "should only configure the logger and set it to puppet" do - Puppet.expects(:warning).with( - "Config file #{path} not found, using Hiera defaults") - @hiera_class.hiera_config.should == { :logger => 'puppet' } - end - end - end - - describe "the behavior of the find method", :if => Puppet.features.hiera? do - - let(:data_binder) { @hiera_class.new } - - it "should support looking up an integer" do - data_binder.find(request_integer).should == 3000 - end - - it "should support looking up a string" do - data_binder.find(request_string).should == 'apache' - end - - it "should support looking up an array" do - data_binder.find(request_array).should == [ - '0.ntp.puppetlabs.com', - '1.ntp.puppetlabs.com', - ] - end - - it "should support looking up a hash" do - data_binder.find(request_hash).should == { - 'user' => 'Hightower', - 'group' => 'admin', - 'mode' => '0644' - } - end - end -end - diff --git a/spec/unit/indirector/json_spec.rb b/spec/unit/indirector/json_spec.rb index c80bcd33c..664a32749 100755 --- a/spec/unit/indirector/json_spec.rb +++ b/spec/unit/indirector/json_spec.rb @@ -131,20 +131,20 @@ describe Puppet::Indirector::JSON do with_content('hello') do subject.destroy(request) end - File.should_not be_exist file + Puppet::FileSystem::File.exist?(file).should be_false end it "silently succeeds when files don't exist" do - File.unlink(file) rescue nil + Puppet::FileSystem::File.unlink(file) rescue nil subject.destroy(request).should be_true end it "raises an informative error for other failures" do - File.stubs(:unlink).with(file).raises(Errno::EPERM, 'fake permission problem') + Puppet::FileSystem::File.stubs(:unlink).with(file).raises(Errno::EPERM, 'fake permission problem') with_content('hello') do expect { subject.destroy(request) }.to raise_error Puppet::Error end - File.unstub(:unlink) # thanks, mocha + Puppet::FileSystem::File.unstub(:unlink) # thanks, mocha end end end diff --git a/spec/unit/indirector/key/file_spec.rb b/spec/unit/indirector/key/file_spec.rb index 57be73691..95c212523 100755 --- a/spec/unit/indirector/key/file_spec.rb +++ b/spec/unit/indirector/key/file_spec.rb @@ -64,33 +64,32 @@ describe Puppet::SSL::Key::File do end it "should save the public key when saving the private key" do - Puppet.settings.stubs(:writesub) + fh = StringIO.new - fh = mock 'filehandle' - - Puppet.settings.expects(:writesub).with(:publickeydir, @public_key_path).yields fh + Puppet.settings.setting(:publickeydir).expects(:open_file).with(@public_key_path, 'w').yields fh + Puppet.settings.setting(:privatekeydir).stubs(:open_file) @public_key.expects(:to_pem).returns "my pem" - fh.expects(:print).with "my pem" - @searcher.save(@request) + + expect(fh.string).to eq("my pem") end it "should destroy the public key when destroying the private key" do - File.stubs(:unlink).with(@private_key_path) - FileTest.stubs(:exist?).with(@private_key_path).returns true - FileTest.expects(:exist?).with(@public_key_path).returns true - File.expects(:unlink).with(@public_key_path) + Puppet::FileSystem::File.stubs(:unlink).with(@private_key_path) + Puppet::FileSystem::File.stubs(:exist?).with(@private_key_path).returns true + Puppet::FileSystem::File.expects(:exist?).with(@public_key_path).returns true + Puppet::FileSystem::File.expects(:unlink).with(@public_key_path) @searcher.destroy(@request) end it "should not fail if the public key does not exist when deleting the private key" do - File.stubs(:unlink).with(@private_key_path) + Puppet::FileSystem::File.stubs(:unlink).with(@private_key_path) - FileTest.stubs(:exist?).with(@private_key_path).returns true - FileTest.expects(:exist?).with(@public_key_path).returns false - File.expects(:unlink).with(@public_key_path).never + Puppet::FileSystem::File.stubs(:exist?).with(@private_key_path).returns true + Puppet::FileSystem::File.expects(:exist?).with(@public_key_path).returns false + Puppet::FileSystem::File.expects(:unlink).with(@public_key_path).never @searcher.destroy(@request) end diff --git a/spec/unit/indirector/resource/ral_spec.rb b/spec/unit/indirector/resource/ral_spec.rb index 0e14a322b..8b42f6881 100755 --- a/spec/unit/indirector/resource/ral_spec.rb +++ b/spec/unit/indirector/resource/ral_spec.rb @@ -2,6 +2,13 @@ require 'spec_helper' describe "Puppet::Resource::Ral" do + + it "is deprecated on the network, but still allows requests" do + Puppet.expects(:deprecation_warning) + + expect(Puppet::Resource::Ral.new.allow_remote_requests?).to eq(true) + end + describe "find" do before do @request = stub 'request', :key => "user/root" diff --git a/spec/unit/indirector/resource/store_configs_spec.rb b/spec/unit/indirector/resource/store_configs_spec.rb index aab2a4475..0ddb94470 100755 --- a/spec/unit/indirector/resource/store_configs_spec.rb +++ b/spec/unit/indirector/resource/store_configs_spec.rb @@ -9,4 +9,15 @@ end describe Puppet::Resource::StoreConfigs do it_should_behave_like "a StoreConfigs terminus" + + before :each do + Puppet[:storeconfigs] = true + Puppet[:storeconfigs_backend] = "store_configs_testing" + end + + it "is deprecated on the network, but still allows requests" do + Puppet.expects(:deprecation_warning) + + expect(Puppet::Resource::StoreConfigs.new.allow_remote_requests?).to eq(true) + end end diff --git a/spec/unit/indirector/rest_spec.rb b/spec/unit/indirector/rest_spec.rb index 787c4cb5e..0bf0bcb48 100755 --- a/spec/unit/indirector/rest_spec.rb +++ b/spec/unit/indirector/rest_spec.rb @@ -109,6 +109,10 @@ describe Puppet::Indirector::REST do string.split(',').collect { |s| convert_from(format, s) } end + def to_data_hash + { 'name' => @name, 'data' => @data } + end + def ==(other) other.is_a? Puppet::TestModel and other.name == name and other.data == data end @@ -205,21 +209,21 @@ describe Puppet::Indirector::REST do @request = stub 'request', :key => "foo", :server => nil, :port => nil terminus.class.expects(:port).returns 321 terminus.class.expects(:server).returns "myserver" - Puppet::Network::HTTP::Connection.expects(:new).with("myserver", 321).returns "myconn" + Puppet::Network::HttpPool.expects(:http_instance).with("myserver", 321).returns "myconn" terminus.network(@request).should == "myconn" end it "should use the server from the indirection request if one is present" do @request = stub 'request', :key => "foo", :server => "myserver", :port => nil terminus.class.stubs(:port).returns 321 - Puppet::Network::HTTP::Connection.expects(:new).with("myserver", 321).returns "myconn" + Puppet::Network::HttpPool.expects(:http_instance).with("myserver", 321).returns "myconn" terminus.network(@request).should == "myconn" end it "should use the port from the indirection request if one is present" do @request = stub 'request', :key => "foo", :server => nil, :port => 321 terminus.class.stubs(:server).returns "myserver" - Puppet::Network::HTTP::Connection.expects(:new).with("myserver", 321).returns "myconn" + Puppet::Network::HttpPool.expects(:http_instance).with("myserver", 321).returns "myconn" terminus.network(@request).should == "myconn" end end diff --git a/spec/unit/indirector/ssl_file_spec.rb b/spec/unit/indirector/ssl_file_spec.rb index 4cc0e216f..8406cc9d7 100755 --- a/spec/unit/indirector/ssl_file_spec.rb +++ b/spec/unit/indirector/ssl_file_spec.rb @@ -121,9 +121,9 @@ describe Puppet::Indirector::SslFile do describe "when finding certificates on disk" do describe "and no certificate is present" do it "should return nil" do - FileTest.expects(:exist?).with(@path).returns(true) + Puppet::FileSystem::File.expects(:exist?).with(@path).returns(true) Dir.expects(:entries).with(@path).returns([]) - FileTest.expects(:exist?).with(@certpath).returns(false) + Puppet::FileSystem::File.expects(:exist?).with(@certpath).returns(false) @searcher.find(@request).should be_nil end @@ -139,7 +139,7 @@ describe Puppet::Indirector::SslFile do context "is readable" do it "should return an instance of the model, which it should use to read the certificate" do - FileTest.expects(:exist?).with(@certpath).returns true + Puppet::FileSystem::File.expects(:exist?).with(@certpath).returns true model.expects(:new).with("myname").returns cert cert.expects(:read).with(@certpath) @@ -150,7 +150,7 @@ describe Puppet::Indirector::SslFile do context "is unreadable" do it "should raise an exception" do - FileTest.expects(:exist?).with(@certpath).returns(true) + Puppet::FileSystem::File.expects(:exist?).with(@certpath).returns(true) model.expects(:new).with("myname").returns cert cert.expects(:read).with(@certpath).raises(Errno::EACCES) @@ -171,9 +171,9 @@ describe Puppet::Indirector::SslFile do # the support for upper-case certs can be removed around mid-2009. it "should rename the existing file to the lower-case path" do @path = @searcher.path("myhost") - FileTest.expects(:exist?).with(@path).returns(false) + Puppet::FileSystem::File.expects(:exist?).with(@path).returns(false) dir, file = File.split(@path) - FileTest.expects(:exist?).with(dir).returns true + Puppet::FileSystem::File.expects(:exist?).with(dir).returns true Dir.expects(:entries).with(dir).returns [".", "..", "something.pem", file.upcase] File.expects(:rename).with(File.join(dir, file.upcase), @path) @@ -221,7 +221,7 @@ describe Puppet::Indirector::SslFile do @searcher.class.store_in @setting fh = mock 'filehandle' fh.stubs :print - Puppet.settings.expects(:writesub).with(@setting, @certpath).yields fh + Puppet.settings.setting(@setting).expects(:open_file).with(@certpath, 'w').yields fh @searcher.save(@request) end @@ -233,7 +233,7 @@ describe Puppet::Indirector::SslFile do fh = mock 'filehandle' fh.stubs :print - Puppet.settings.expects(:write).with(@setting).yields fh + Puppet.settings.setting(@setting).expects(:open).with('w').yields fh @searcher.save(@request) end end @@ -246,7 +246,7 @@ describe Puppet::Indirector::SslFile do fh = mock 'filehandle' fh.stubs :print - Puppet.settings.expects(:write).with(:cakey).yields fh + Puppet.settings.setting(:cakey).expects(:open).with('w').yields fh @searcher.stubs(:ca?).returns true @searcher.save(@request) end @@ -256,7 +256,7 @@ describe Puppet::Indirector::SslFile do describe "when destroying certificates" do describe "that do not exist" do before do - FileTest.expects(:exist?).with(@certpath).returns false + Puppet::FileSystem::File.expects(:exist?).with(@certpath).returns false end it "should return false" do @@ -265,18 +265,15 @@ describe Puppet::Indirector::SslFile do end describe "that exist" do - before do - FileTest.expects(:exist?).with(@certpath).returns true - end - it "should unlink the certificate file" do - File.expects(:unlink).with(@certpath) + Puppet::FileSystem::File.expects(:exist?).with(@certpath).returns true + Puppet::FileSystem::File.expects(:unlink).with(@certpath) @searcher.destroy(@request) end it "should log that is removing the file" do - File.stubs(:exist?).returns true - File.stubs(:unlink) + Puppet::FileSystem::File.stubs(:exist?).returns true + Puppet::FileSystem::File.stubs(:unlink) Puppet.expects(:notice) @searcher.destroy(@request) end diff --git a/spec/unit/indirector/yaml_spec.rb b/spec/unit/indirector/yaml_spec.rb index 8a2753479..6830869b1 100755 --- a/spec/unit/indirector/yaml_spec.rb +++ b/spec/unit/indirector/yaml_spec.rb @@ -149,15 +149,15 @@ describe Puppet::Indirector::Yaml do end it "should unlink the right yaml file if it exists" do - File.expects(:exists?).with(path).returns true - File.expects(:unlink).with(path) + Puppet::FileSystem::File.expects(:exist?).with(path).returns true + Puppet::FileSystem::File.expects(:unlink).with(path) @store.destroy(@request) end it "should not unlink the yaml file if it does not exists" do - File.expects(:exists?).with(path).returns false - File.expects(:unlink).with(path).never + Puppet::FileSystem::File.expects(:exist?).with(path).returns false + Puppet::FileSystem::File.expects(:unlink).with(path).never @store.destroy(@request) end diff --git a/spec/unit/module_spec.rb b/spec/unit/module_spec.rb index c06f278eb..5c4065e14 100755 --- a/spec/unit/module_spec.rb +++ b/spec/unit/module_spec.rb @@ -15,7 +15,7 @@ describe Puppet::Module do before do # This is necessary because of the extra checks we have for the deprecated # 'plugins' directory - FileTest.stubs(:exist?).returns false + Puppet::FileSystem::File.stubs(:exist?).returns false end it "should have a class method that returns a named module from a given environment" do @@ -90,12 +90,14 @@ describe Puppet::Module do describe "when finding unmet dependencies" do before do - FileTest.unstub(:exist?) + Puppet::FileSystem::File.unstub(:exist?) @modpath = tmpdir('modpath') Puppet.settings[:modulepath] = @modpath end it "should list modules that are missing" do + metadata_file = "#{@modpath}/needy/metadata.json" + Puppet::FileSystem::File.expects(:exist?).twice.with(metadata_file).returns true mod = PuppetSpec::Modules.create( 'needy', @modpath, @@ -116,6 +118,8 @@ describe Puppet::Module do end it "should list modules that are missing and have invalid names" do + metadata_file = "#{@modpath}/needy/metadata.json" + Puppet::FileSystem::File.expects(:exist?).with(metadata_file).twice.returns true mod = PuppetSpec::Modules.create( 'needy', @modpath, @@ -136,6 +140,10 @@ describe Puppet::Module do end it "should list modules with unmet version requirement" do + ['foobar', 'foobaz'].each do |mod_name| + metadata_file = "#{@modpath}/#{mod_name}/metadata.json" + Puppet::FileSystem::File.stubs(:exist?).with(metadata_file).returns true + end mod = PuppetSpec::Modules.create( 'foobar', @modpath, @@ -204,6 +212,8 @@ describe Puppet::Module do end it "should consider a dependency without a semantic version to be unmet" do + metadata_file = "#{@modpath}/foobar/metadata.json" + Puppet::FileSystem::File.expects(:exist?).with(metadata_file).times(3).returns true mod = PuppetSpec::Modules.create( 'foobar', @modpath, @@ -244,6 +254,10 @@ describe Puppet::Module do end it "should only list unmet dependencies" do + [name, 'satisfied'].each do |mod_name| + metadata_file = "#{@modpath}/#{mod_name}/metadata.json" + Puppet::FileSystem::File.expects(:exist?).with(metadata_file).twice.returns true + end mod = PuppetSpec::Modules.create( name, @modpath, @@ -357,35 +371,42 @@ describe Puppet::Module do end end - [:plugins, :templates, :files, :manifests].each do |filetype| - dirname = filetype == :plugins ? "lib" : filetype.to_s + [:plugins, :pluginfacts, :templates, :files, :manifests].each do |filetype| + case filetype + when :plugins + dirname = "lib" + when :pluginfacts + dirname = "facts.d" + else + dirname = filetype.to_s + end it "should be able to return individual #{filetype}" do module_file = File.join(path, dirname, "my/file") - FileTest.expects(:exist?).with(module_file).returns true + Puppet::FileSystem::File.expects(:exist?).with(module_file).returns true mod.send(filetype.to_s.sub(/s$/, ''), "my/file").should == module_file end it "should consider #{filetype} to be present if their base directory exists" do module_file = File.join(path, dirname) - FileTest.expects(:exist?).with(module_file).returns true + Puppet::FileSystem::File.expects(:exist?).with(module_file).returns true mod.send(filetype.to_s + "?").should be_true end it "should consider #{filetype} to be absent if their base directory does not exist" do module_file = File.join(path, dirname) - FileTest.expects(:exist?).with(module_file).returns false + Puppet::FileSystem::File.expects(:exist?).with(module_file).returns false mod.send(filetype.to_s + "?").should be_false end it "should return nil if asked to return individual #{filetype} that don't exist" do module_file = File.join(path, dirname, "my/file") - FileTest.expects(:exist?).with(module_file).returns false + Puppet::FileSystem::File.expects(:exist?).with(module_file).returns false mod.send(filetype.to_s.sub(/s$/, ''), "my/file").should be_nil end it "should return the base directory if asked for a nil path" do base = File.join(path, dirname) - FileTest.expects(:exist?).with(base).returns true + Puppet::FileSystem::File.expects(:exist?).with(base).returns true mod.send(filetype.to_s.sub(/s$/, ''), nil).should == base end end @@ -418,8 +439,8 @@ describe Puppet::Module, "when finding matching manifests" do end it "should default to the 'init' file if no glob pattern is specified" do - FileTest.expects(:exist?).with("/a/manifests/init.pp").returns(true) - FileTest.expects(:exist?).with("/a/manifests/init.rb").returns(false) + Puppet::FileSystem::File.expects(:exist?).with("/a/manifests/init.pp").returns(true) + Puppet::FileSystem::File.expects(:exist?).with("/a/manifests/init.rb").returns(false) @mod.match_manifests(nil).should == %w{/a/manifests/init.pp} end @@ -471,21 +492,21 @@ describe Puppet::Module do end it "should have metadata if it has a metadata file and its data is not empty" do - FileTest.expects(:exist?).with(@module.metadata_file).returns true + Puppet::FileSystem::File.expects(:exist?).with(@module.metadata_file).returns true File.stubs(:read).with(@module.metadata_file).returns "{\"foo\" : \"bar\"}" @module.should be_has_metadata end it "should have metadata if it has a metadata file and its data is not empty" do - FileTest.expects(:exist?).with(@module.metadata_file).returns true + Puppet::FileSystem::File.expects(:exist?).with(@module.metadata_file).returns true File.stubs(:read).with(@module.metadata_file).returns "{\"foo\" : \"bar\"}" @module.should be_has_metadata end it "should not have metadata if has a metadata file and its data is empty" do - FileTest.expects(:exist?).with(@module.metadata_file).returns true + Puppet::FileSystem::File.expects(:exist?).with(@module.metadata_file).returns true File.stubs(:read).with(@module.metadata_file).returns "/* +-----------------------------------------------------------------------+ | | @@ -503,7 +524,7 @@ describe Puppet::Module do end it "should know if it is missing a metadata file" do - FileTest.expects(:exist?).with(@module.metadata_file).returns false + Puppet::FileSystem::File.expects(:exist?).with(@module.metadata_file).returns false @module.should_not be_has_metadata end @@ -519,6 +540,13 @@ describe Puppet::Module do Puppet::Module.new("yay", "/path", mock("env")) end + it "should tolerate failure to parse" do + Puppet::FileSystem::File.expects(:exist?).with(@module.metadata_file).returns true + File.stubs(:read).with(@module.metadata_file).returns(my_fixture('trailing-comma.json')) + + @module.has_metadata?.should be_false + end + def a_module_with_metadata(data) text = data.to_pson diff --git a/spec/unit/module_tool/tar/gnu_spec.rb b/spec/unit/module_tool/tar/gnu_spec.rb index b5fc78918..93245da1a 100644 --- a/spec/unit/module_tool/tar/gnu_spec.rb +++ b/spec/unit/module_tool/tar/gnu_spec.rb @@ -8,9 +8,9 @@ describe Puppet::ModuleTool::Tar::Gnu do let(:destfile) { '/the/dest/file.tar.gz' } it "unpacks a tar file" do - Puppet::Util::Execution.expects(:execute).with("tar xzf #{sourcefile} --no-same-permissions --no-same-owner -C #{destdir}") + Puppet::Util::Execution.expects(:execute).with("tar xzf #{sourcefile} --no-same-owner -C #{destdir}") Puppet::Util::Execution.expects(:execute).with("find #{destdir} -type d -exec chmod 755 {} +") - Puppet::Util::Execution.expects(:execute).with("find #{destdir} -type f -exec chmod 644 {} +") + Puppet::Util::Execution.expects(:execute).with("find #{destdir} -type f -exec chmod a-wst {} +") Puppet::Util::Execution.expects(:execute).with("chown -R <owner:group> #{destdir}") subject.unpack(sourcefile, destdir, '<owner:group>') end diff --git a/spec/unit/module_tool/tar/solaris_spec.rb b/spec/unit/module_tool/tar/solaris_spec.rb index 23bbd8d20..1ca7a6a09 100644 --- a/spec/unit/module_tool/tar/solaris_spec.rb +++ b/spec/unit/module_tool/tar/solaris_spec.rb @@ -8,9 +8,9 @@ describe Puppet::ModuleTool::Tar::Solaris do let(:destfile) { '/the/dest/file.tar.gz' } it "unpacks a tar file" do - Puppet::Util::Execution.expects(:execute).with("gtar xzf #{sourcefile} --no-same-permissions --no-same-owner -C #{destdir}") + Puppet::Util::Execution.expects(:execute).with("gtar xzf #{sourcefile} --no-same-owner -C #{destdir}") Puppet::Util::Execution.expects(:execute).with("find #{destdir} -type d -exec chmod 755 {} +") - Puppet::Util::Execution.expects(:execute).with("find #{destdir} -type f -exec chmod 644 {} +") + Puppet::Util::Execution.expects(:execute).with("find #{destdir} -type f -exec chmod a-wst {} +") Puppet::Util::Execution.expects(:execute).with("chown -R <owner:group> #{destdir}") subject.unpack(sourcefile, destdir, '<owner:group>') end diff --git a/spec/unit/module_tool/tar_spec.rb b/spec/unit/module_tool/tar_spec.rb new file mode 100644 index 000000000..3de0dda48 --- /dev/null +++ b/spec/unit/module_tool/tar_spec.rb @@ -0,0 +1,45 @@ +require 'spec_helper' +require 'puppet/module_tool/tar' + +describe Puppet::ModuleTool::Tar do + + it "uses gtar when present on Solaris" do + Facter.stubs(:value).with('osfamily').returns 'Solaris' + Puppet::Util.stubs(:which).with('gtar').returns '/usr/bin/gtar' + + described_class.instance(nil).should be_a_kind_of Puppet::ModuleTool::Tar::Solaris + end + + it "uses gtar when present on OpenBSD" do + Facter.stubs(:value).with('osfamily').returns 'OpenBSD' + Puppet::Util.stubs(:which).with('gtar').returns '/usr/bin/gtar' + + described_class.instance(nil).should be_a_kind_of Puppet::ModuleTool::Tar::Solaris + end + + it "uses tar when present and not on Windows" do + Facter.stubs(:value).with('osfamily').returns 'ObscureLinuxDistro' + Puppet::Util.stubs(:which).with('tar').returns '/usr/bin/tar' + Puppet::Util::Platform.stubs(:windows?).returns false + + described_class.instance(nil).should be_a_kind_of Puppet::ModuleTool::Tar::Gnu + end + + it "falls back to minitar when it and zlib are present" do + Facter.stubs(:value).with('osfamily').returns 'Windows' + Puppet::Util.stubs(:which).with('tar') + Puppet::Util::Platform.stubs(:windows?).returns true + Puppet.stubs(:features).returns(stub(:minitar? => true, :zlib? => true)) + + described_class.instance(nil).should be_a_kind_of Puppet::ModuleTool::Tar::Mini + end + + it "fails when there is no possible implementation" do + Facter.stubs(:value).with('osfamily').returns 'Windows' + Puppet::Util.stubs(:which).with('tar') + Puppet::Util::Platform.stubs(:windows?).returns true + Puppet.stubs(:features).returns(stub(:minitar? => false, :zlib? => false)) + + expect { described_class.instance(nil) }.to raise_error RuntimeError, /No suitable tar/ + end +end diff --git a/spec/unit/network/authconfig_spec.rb b/spec/unit/network/authconfig_spec.rb index 6814b2c1b..be45152c0 100755 --- a/spec/unit/network/authconfig_spec.rb +++ b/spec/unit/network/authconfig_spec.rb @@ -5,7 +5,8 @@ require 'puppet/network/authconfig' describe Puppet::Network::AuthConfig do before :each do - File.stubs(:stat).returns(stub('stat', :ctime => :now)) + stub_file = stub('file', :stat => stub('stat', :ctime => :now)) + Puppet::FileSystem::File.stubs(:new).returns stub_file Time.stubs(:now).returns Time.now Puppet::Network::AuthConfig.any_instance.stubs(:exists?).returns(true) diff --git a/spec/unit/network/authentication_spec.rb b/spec/unit/network/authentication_spec.rb index 5b54757b8..c18552ab8 100755 --- a/spec/unit/network/authentication_spec.rb +++ b/spec/unit/network/authentication_spec.rb @@ -20,7 +20,7 @@ describe Puppet::Network::Authentication do describe "when warning about upcoming expirations" do before do Puppet::SSL::CertificateAuthority.stubs(:ca?).returns(false) - FileTest.stubs(:exist?).returns(false) + Puppet::FileSystem::File.stubs(:exist?).returns(false) end it "should check the expiration of the CA certificate" do @@ -34,7 +34,7 @@ describe Puppet::Network::Authentication do it "should check the expiration of the localhost certificate" do Puppet::SSL::Host.stubs(:localhost).returns(host) cert.expects(:near_expiration?).returns(false) - FileTest.stubs(:exist?).with(Puppet[:hostcert]).returns(true) + Puppet::FileSystem::File.stubs(:exist?).with(Puppet[:hostcert]).returns(true) subject.warn_if_near_expiration end diff --git a/spec/unit/network/format_handler_spec.rb b/spec/unit/network/format_handler_spec.rb index b8ab39634..1716a7296 100755 --- a/spec/unit/network/format_handler_spec.rb +++ b/spec/unit/network/format_handler_spec.rb @@ -63,8 +63,8 @@ describe Puppet::Network::FormatHandler do Puppet::Network::FormatHandler.most_suitable_format_for(accepted, [:one, :two]) end - it "finds either format when anything is accepted" do - [format_one, format_two].should include(suitable_in_setup_formats(["*/*"])) + it "finds the most preferred format when anything is acceptable" do + Puppet::Network::FormatHandler.most_suitable_format_for(["*/*"], [:two, :one]).should == format_two end it "finds no format when none are acceptable" do diff --git a/spec/unit/network/formats_spec.rb b/spec/unit/network/formats_spec.rb index 8531a541a..57ff77795 100755 --- a/spec/unit/network/formats_spec.rb +++ b/spec/unit/network/formats_spec.rb @@ -26,6 +26,30 @@ class PsonTest end describe "Puppet Network Format" do + it "should include a msgpack format", :if => Puppet.features.msgpack? do + Puppet::Network::FormatHandler.format(:msgpack).should_not be_nil + end + + describe "msgpack", :if => Puppet.features.msgpack? do + before do + @msgpack = Puppet::Network::FormatHandler.format(:msgpack) + end + + it "should have its mime type set to application/x-msgpack" do + @msgpack.mime.should == "application/x-msgpack" + end + + it "should have a weight of 20" do + @msgpack.weight.should == 20 + end + + it "should fail when one element does not have a from_pson" do + expect do + @msgpack.intern_multiple(Hash, MessagePack.pack(["foo"])) + end.to raise_error(NoMethodError) + end + end + it "should include a yaml format" do Puppet::Network::FormatHandler.format(:yaml).should_not be_nil end diff --git a/spec/unit/network/http/connection_spec.rb b/spec/unit/network/http/connection_spec.rb index 0d6da0671..467705f01 100644 --- a/spec/unit/network/http/connection_spec.rb +++ b/spec/unit/network/http/connection_spec.rb @@ -7,19 +7,13 @@ describe Puppet::Network::HTTP::Connection do let (:host) { "me" } let (:port) { 54321 } - subject { Puppet::Network::HTTP::Connection.new(host, port) } + subject { Puppet::Network::HTTP::Connection.new(host, port, :verify => Puppet::SSL::Validator.no_validator) } context "when providing HTTP connections" do after do Puppet::Network::HTTP::Connection.instance_variable_set("@ssl_host", nil) end - it "should use the global SSL::Host instance to get its certificate information" do - host = mock 'host' - Puppet::SSL::Host.expects(:localhost).with.returns host - subject.send(:ssl_host).should equal(host) - end - context "when initializing http instances" do before :each do # All of the cert stuff is tested elsewhere @@ -39,51 +33,12 @@ describe Puppet::Network::HTTP::Connection do end it "can set ssl using an option" do - Puppet::Network::HTTP::Connection.new(host, port, :use_ssl => false).send(:connection).should_not be_use_ssl - Puppet::Network::HTTP::Connection.new(host, port, :use_ssl => true).send(:connection).should be_use_ssl - end - - describe "peer verification" do - def setup_standard_ssl_configuration - ca_cert_file = File.expand_path('/path/to/ssl/certs/ca_cert.pem') - FileTest.stubs(:exist?).with(ca_cert_file).returns(true) - - ssl_configuration = stub('ssl_configuration', :ca_auth_file => ca_cert_file) - Puppet::Network::HTTP::Connection.any_instance.stubs(:ssl_configuration).returns(ssl_configuration) - end - - def setup_standard_hostcert - host_cert_file = File.expand_path('/path/to/ssl/certs/host_cert.pem') - FileTest.stubs(:exist?).with(host_cert_file).returns(true) - - Puppet[:hostcert] = host_cert_file - end - - def setup_standard_ssl_host - cert = stub('cert', :content => 'real_cert') - key = stub('key', :content => 'real_key') - host = stub('host', :certificate => cert, :key => key, :ssl_store => stub('store')) - - Puppet::Network::HTTP::Connection.any_instance.stubs(:ssl_host).returns(host) - end - - before do - setup_standard_ssl_configuration - setup_standard_hostcert - setup_standard_ssl_host - end - - it "can enable peer verification" do - Puppet::Network::HTTP::Connection.new(host, port, :verify_peer => true).send(:connection).verify_mode.should == OpenSSL::SSL::VERIFY_PEER - end - - it "can disable peer verification" do - Puppet::Network::HTTP::Connection.new(host, port, :verify_peer => false).send(:connection).verify_mode.should == OpenSSL::SSL::VERIFY_NONE - end + Puppet::Network::HTTP::Connection.new(host, port, :use_ssl => false, :verify => Puppet::SSL::Validator.no_validator).send(:connection).should_not be_use_ssl + Puppet::Network::HTTP::Connection.new(host, port, :use_ssl => true, :verify => Puppet::SSL::Validator.no_validator).send(:connection).should be_use_ssl end context "proxy and timeout settings should propagate" do - subject { Puppet::Network::HTTP::Connection.new(host, port).send(:connection) } + subject { Puppet::Network::HTTP::Connection.new(host, port, :verify => Puppet::SSL::Validator.no_validator).send(:connection) } before :each do Puppet[:http_proxy_host] = "myhost" Puppet[:http_proxy_port] = 432 @@ -104,94 +59,13 @@ describe Puppet::Network::HTTP::Connection do it "should raise Puppet::Error when invalid options are specified" do expect { Puppet::Network::HTTP::Connection.new(host, port, :invalid_option => nil) }.to raise_error(Puppet::Error, 'Unrecognized option(s): :invalid_option') end - - end - - describe "when doing SSL setup for http instances" do - let :store do stub('store') end - - let :ca_auth_file do - '/path/to/ssl/certs/ssl_server_ca_auth.pem' - end - - let :ssl_configuration do - stub('ssl_configuration', :ca_auth_file => ca_auth_file) - end - - before :each do - Puppet[:hostcert] = '/host/cert' - Puppet::Network::HTTP::Connection.any_instance.stubs(:ssl_configuration).returns(ssl_configuration) - - cert = stub 'cert', :content => 'real_cert' - key = stub 'key', :content => 'real_key' - host = stub 'host', :certificate => cert, :key => key, :ssl_store => store - Puppet::Network::HTTP::Connection.any_instance.stubs(:ssl_host).returns(host) - end - - shared_examples "HTTPS setup without all certificates" do - subject { Puppet::Network::HTTP::Connection.new(host, port, :use_ssl => true).send(:connection) } - - it { should be_use_ssl } - its(:cert) { should be_nil } - its(:ca_file) { should be_nil } - its(:key) { should be_nil } - its(:verify_mode) { should == OpenSSL::SSL::VERIFY_NONE } - end - - context "with neither a host cert or a local CA cert" do - before :each do - FileTest.stubs(:exist?).with(Puppet[:hostcert]).returns(false) - FileTest.stubs(:exist?).with(ca_auth_file).returns(false) - end - - include_examples "HTTPS setup without all certificates" - end - - context "with there is no host certificate" do - before :each do - FileTest.stubs(:exist?).with(Puppet[:hostcert]).returns(false) - FileTest.stubs(:exist?).with(ca_auth_file).returns(true) - end - - include_examples "HTTPS setup without all certificates" - end - - context "with there is no local CA certificate" do - before :each do - FileTest.stubs(:exist?).with(Puppet[:hostcert]).returns(true) - FileTest.stubs(:exist?).with(ca_auth_file).returns(false) - end - - include_examples "HTTPS setup without all certificates" - end - - context "with both the host and CA cert" do - subject { Puppet::Network::HTTP::Connection.new(host, port, :use_ssl => true).send(:connection) } - - before :each do - FileTest.expects(:exist?).with(Puppet[:hostcert]).returns(true) - FileTest.expects(:exist?).with(ca_auth_file).returns(true) - end - - it { should be_use_ssl } - its(:cert_store) { should equal store } - its(:cert) { should == "real_cert" } - its(:key) { should == "real_key" } - its(:verify_mode) { should == OpenSSL::SSL::VERIFY_PEER } - its(:ca_file) { should == ca_auth_file } - end - - it "should set up certificate information when creating http instances" do - subject.expects(:cert_setup) - subject.send(:connection) - end end end context "when methods that accept a block are called with a block" do let (:host) { "my_server" } let (:port) { 8140 } - let (:subject) { Puppet::Network::HTTP::Connection.new(host, port, :use_ssl => false) } + let (:subject) { Puppet::Network::HTTP::Connection.new(host, port, :use_ssl => false, :verify => Puppet::SSL::Validator.no_validator) } let (:httpok) { Net::HTTPOK.new('1.1', 200, '') } before :each do @@ -228,95 +102,99 @@ describe Puppet::Network::HTTP::Connection do let (:host) { "my_server" } let (:port) { 8140 } let (:httpok) { Net::HTTPOK.new('1.1', 200, '') } - let (:subject) { Puppet::Network::HTTP::Connection.new(host, port) } - - def a_connection_that_verifies(args) - connection = Net::HTTP.new(host, port) - connection.stubs(:warn_if_near_expiration) - connection.stubs(:get).with do - connection.verify_callback.call(args[:has_passed_pre_checks], args[:in_context]) - true - end.raises(OpenSSL::SSL::SSLError.new(args[:fails_with])) - connection - end - - def a_store_context(args) - Puppet[:confdir] = tmpdir('conf') - ssl_context = mock('OpenSSL::X509::StoreContext') - if args[:verify_raises] - ssl_context.stubs(:current_cert).raises("oh noes") - else - cert = Puppet::SSL::CertificateAuthority.new.generate(args[:for_server], :dns_alt_names => args[:for_aliases]).content - ssl_context.stubs(:current_cert).returns(cert) - end - ssl_context.stubs(:chain).returns([]) - ssl_context.stubs(:error_string).returns(args[:with_error_string]) - ssl_context - end it "should provide a useful error message when one is available and certificate validation fails", :unless => Puppet.features.microsoft_windows? do - subject.stubs(:create_connection). - returns(a_connection_that_verifies(:has_passed_pre_checks => false, - :in_context => a_store_context(:for_server => 'not_my_server', - :with_error_string => 'shady looking signature'), - :fails_with => 'certificate verify failed')) - expect do - subject.request(:get, stub('request')) - end.to raise_error(Puppet::Error, "certificate verify failed: [shady looking signature for /CN=not_my_server]") - end + connection = Puppet::Network::HTTP::Connection.new( + host, port, + :verify => ConstantErrorValidator.new(:fails_with => 'certificate verify failed', + :error_string => 'shady looking signature')) - it "should provide a useful error message when verify_callback raises", :unless => Puppet.features.microsoft_windows? do - subject.stubs(:create_connection). - returns(a_connection_that_verifies(:has_passed_pre_checks => false, - :in_context => a_store_context(:verify_raises => true), - :fails_with => 'certificate verify failed')) expect do - subject.request(:get, stub('request')) - end.to raise_error(Puppet::Error, "certificate verify failed: [oh noes]") + connection.get('request') + end.to raise_error(Puppet::Error, "certificate verify failed: [shady looking signature]") end it "should provide a helpful error message when hostname was not match with server certificate", :unless => Puppet.features.microsoft_windows? do - subject.stubs(:create_connection). - returns(a_connection_that_verifies(:has_passed_pre_checks => true, - :in_context => a_store_context(:for_server => 'not_my_server', - :for_aliases => 'foo,bar,baz'), - :fails_with => 'hostname was not match with server certificate')) - - expect { subject.request(:get, stub('request')) }. - to raise_error(Puppet::Error) do |error| + Puppet[:confdir] = tmpdir('conf') + + connection = Puppet::Network::HTTP::Connection.new( + host, port, + :verify => ConstantErrorValidator.new( + :fails_with => 'hostname was not match with server certificate', + :peer_certs => [Puppet::SSL::CertificateAuthority.new.generate( + 'not_my_server', :dns_alt_names => 'foo,bar,baz')])) + + expect do + connection.get('request') + end.to raise_error(Puppet::Error) do |error| error.message =~ /Server hostname 'my_server' did not match server certificate; expected one of (.+)/ $1.split(', ').should =~ %w[DNS:foo DNS:bar DNS:baz DNS:not_my_server not_my_server] end end it "should pass along the error message otherwise" do - connection = Net::HTTP.new('my_server', 8140) - subject.stubs(:create_connection).returns(connection) - - connection.stubs(:get).raises(OpenSSL::SSL::SSLError.new('some other message')) + connection = Puppet::Network::HTTP::Connection.new( + host, port, + :verify => ConstantErrorValidator.new(:fails_with => 'some other message')) expect do - subject.request(:get, stub('request')) + connection.get('request') end.to raise_error(/some other message/) end it "should check all peer certificates for upcoming expiration", :unless => Puppet.features.microsoft_windows? do - connection = Net::HTTP.new('my_server', 8140) - subject.stubs(:create_connection).returns(connection) + Puppet[:confdir] = tmpdir('conf') + cert = Puppet::SSL::CertificateAuthority.new.generate( + 'server', :dns_alt_names => 'foo,bar,baz') + + connection = Puppet::Network::HTTP::Connection.new( + host, port, + :verify => NoProblemsValidator.new(cert)) - cert = stubs 'cert' - Puppet::SSL::Certificate.expects(:from_instance).twice.returns(cert) + Net::HTTP.any_instance.stubs(:get).returns(httpok) - connection.stubs(:get).with do - context = a_store_context(:for_server => 'a_server', :with_error_string => false) - connection.verify_callback.call(true, context) - connection.verify_callback.call(true, context) - true - end.returns(httpok) + connection.expects(:warn_if_near_expiration).with(cert) - subject.expects(:warn_if_near_expiration).with(cert, cert) + connection.get('request') + end + + class ConstantErrorValidator + def initialize(args) + @fails_with = args[:fails_with] + @error_string = args[:error_string] || "" + @peer_certs = args[:peer_certs] || [] + end - subject.request(:get, stubs('request')) + def setup_connection(connection) + connection.stubs(:get).with do + true + end.raises(OpenSSL::SSL::SSLError.new(@fails_with)) + end + + def peer_certs + @peer_certs + end + + def verify_errors + [@error_string] + end + end + + class NoProblemsValidator + def initialize(cert) + @cert = cert + end + + def setup_connection(connection) + end + + def peer_certs + [@cert] + end + + def verify_errors + [] + end end end @@ -324,7 +202,7 @@ describe Puppet::Network::HTTP::Connection do let (:other_host) { "redirected" } let (:other_port) { 9292 } let (:other_path) { "other-path" } - let (:subject) { Puppet::Network::HTTP::Connection.new("my_server", 8140, :use_ssl => false) } + let (:subject) { Puppet::Network::HTTP::Connection.new("my_server", 8140, :use_ssl => false, :verify => Puppet::SSL::Validator.no_validator) } let (:httpredirection) { Net::HTTPFound.new('1.1', 302, 'Moved Temporarily') } let (:httpok) { Net::HTTPOK.new('1.1', 200, '') } @@ -356,5 +234,4 @@ describe Puppet::Network::HTTP::Connection do }.to raise_error(Puppet::Network::HTTP::RedirectionLimitExceededException) end end - end diff --git a/spec/unit/network/http/handler_spec.rb b/spec/unit/network/http/handler_spec.rb index 0ff087b02..53da5be01 100755 --- a/spec/unit/network/http/handler_spec.rb +++ b/spec/unit/network/http/handler_spec.rb @@ -38,6 +38,9 @@ describe Puppet::Network::HTTP::Handler do end class Puppet::TestModel::Memory < Puppet::Indirector::Memory + def supports_remote_requests? + true + end end Puppet::TestModel.indirection.terminus_class = :memory @@ -150,9 +153,7 @@ describe Puppet::Network::HTTP::Handler do describe "when processing a request" do let(:response) do - obj = stub "http 200 ok" - obj.stubs(:[]=).with(Puppet::Network::HTTP::HEADER_PUPPET_VERSION, Puppet.version) - obj + { :status => 200 } end before do @@ -163,7 +164,6 @@ describe Puppet::Network::HTTP::Handler do it "should check the client certificate for upcoming expiration" do request = a_request cert = mock 'cert' - handler.stubs(:uri2indirection).returns(["facts", :mymethod, "key", {:node => "name"}]) handler.expects(:client_cert).returns(cert).with(request) handler.expects(:warn_if_near_expiration).with(cert) @@ -201,33 +201,31 @@ describe Puppet::Network::HTTP::Handler do handler.process(request, response) end - it "should call the 'do' method and delegate authorization to the authorization layer" do + it "should return 403 if the request is not authorized" do request = a_request handler.expects(:uri2indirection).returns(["facts", :mymethod, "key", {:node => "name"}]) - handler.expects(:do_mymethod).with("facts", "key", {:node => "name"}, request, response) + handler.expects(:do_mymethod).never + + handler.expects(:check_authorization).with("facts", :mymethod, "key", {:node => "name"}).raises(Puppet::Network::AuthorizationError.new("forbidden")) - handler.expects(:check_authorization).with("facts", :mymethod, "key", {:node => "name"}) + handler.expects(:set_response).with(anything, anything, 403) handler.process(request, response) end - it "should return 403 if the request is not authorized" do + it "should return an error code if the indirection does not support remote requests" do request = a_request - handler.expects(:uri2indirection).returns(["facts", :mymethod, "key", {:node => "name"}]) - handler.expects(:do_mymethod).never - - handler.expects(:check_authorization).with("facts", :mymethod, "key", {:node => "name"}).raises(Puppet::Network::AuthorizationError.new("forbidden")) - - handler.expects(:set_response).with(anything, anything, 403) + indirection.expects(:allow_remote_requests?).returns(false) handler.process(request, response) + + expect(response[:status]).to eq 404 end it "should serialize a controller exception when an exception is thrown while finding the model instance" do - request = a_request - handler.expects(:uri2indirection).returns(["facts", :find, "key", {:node => "name"}]) + request = a_request_that_finds(Puppet::TestModel.new("key")) handler.expects(:do_find).raises(ArgumentError, "The exception") handler.expects(:set_response).with(anything, "The exception", 400) @@ -302,7 +300,7 @@ describe Puppet::Network::HTTP::Handler do handler.expects(:set_response).with(response, data.render(:pson)) handler.expects(:set_content_type).with(response, Puppet::Network::FormatHandler.format(:pson)) - handler.do_find(indirection.name, "my data", {}, request, response) + handler.do_find(indirection, "my data", {}, request, response) end it "responds with a 406 error when no accept header is provided" do @@ -311,7 +309,7 @@ describe Puppet::Network::HTTP::Handler do request = a_request_that_finds(data, :accept_header => nil) expect do - handler.do_find(indirection.name, "my data", {}, request, response) + handler.do_find(indirection, "my data", {}, request, response) end.to raise_error(Puppet::Network::HTTP::Handler::HTTPNotAcceptableError) end @@ -321,7 +319,7 @@ describe Puppet::Network::HTTP::Handler do request = a_request_that_finds(data, :accept_header => "unknown, also/unknown") expect do - handler.do_find(indirection.name, "my data", {}, request, response) + handler.do_find(indirection, "my data", {}, request, response) end.to raise_error(Puppet::Network::HTTP::Handler::HTTPNotAcceptableError) end @@ -334,7 +332,7 @@ describe Puppet::Network::HTTP::Handler do handler.expects(:set_response).with(response, data_string) handler.expects(:set_content_type).with(response, Puppet::Network::FormatHandler.format(:pson)) - handler.do_find(indirection.name, "my data", {}, request, response) + handler.do_find(indirection, "my data", {}, request, response) end it "should return a 404 when no model instance can be found" do @@ -342,7 +340,7 @@ describe Puppet::Network::HTTP::Handler do request = a_request_that_finds(data, :accept_header => "unknown, pson, yaml") expect do - handler.do_find(indirection.name, "my data", {}, request, response) + handler.do_find(indirection, "my data", {}, request, response) end.to raise_error(Puppet::Network::HTTP::Handler::HTTPNotFoundError) end end @@ -377,7 +375,7 @@ describe Puppet::Network::HTTP::Handler do handler.expects(:set_response).with(response, Puppet::TestModel.render_multiple(:pson, [data])) handler.expects(:set_content_type).with(response, Puppet::Network::FormatHandler.format(:pson)) - handler.do_search(indirection.name, "my", {}, request, response) + handler.do_search(indirection, "my", {}, request, response) end it "should return [] when searching returns an empty array" do @@ -386,7 +384,7 @@ describe Puppet::Network::HTTP::Handler do handler.expects(:set_response).with(response, Puppet::TestModel.render_multiple(:pson, [])) handler.expects(:set_content_type).with(response, Puppet::Network::FormatHandler.format(:pson)) - handler.do_search(indirection.name, "nothing", {}, request, response) + handler.do_search(indirection, "nothing", {}, request, response) end it "should return a 404 when searching returns nil" do @@ -394,7 +392,7 @@ describe Puppet::Network::HTTP::Handler do indirection.expects(:search).returns(nil) expect do - handler.do_search(indirection.name, "nothing", {}, request, response) + handler.do_search(indirection, "nothing", {}, request, response) end.to raise_error(Puppet::Network::HTTP::Handler::HTTPNotFoundError) end end @@ -405,7 +403,7 @@ describe Puppet::Network::HTTP::Handler do indirection.save(data, "my data") request = a_request_that_destroys(data) - handler.do_destroy(indirection.name, "my data", {}, request, response) + handler.do_destroy(indirection, "my data", {}, request, response) Puppet::TestModel.indirection.find("my data").should be_nil end @@ -418,7 +416,7 @@ describe Puppet::Network::HTTP::Handler do handler.expects(:set_response).with(response, data.render(:yaml)) handler.expects(:set_content_type).with(response, Puppet::Network::FormatHandler.format(:yaml)) - handler.do_destroy(indirection.name, "my data", {}, request, response) + handler.do_destroy(indirection, "my data", {}, request, response) end it "uses the first supported format for the response" do @@ -429,7 +427,7 @@ describe Puppet::Network::HTTP::Handler do handler.expects(:set_response).with(response, data.render(:pson)) handler.expects(:set_content_type).with(response, Puppet::Network::FormatHandler.format(:pson)) - handler.do_destroy(indirection.name, "my data", {}, request, response) + handler.do_destroy(indirection, "my data", {}, request, response) end it "raises an error and does not destory when no accepted formats are known" do @@ -438,7 +436,7 @@ describe Puppet::Network::HTTP::Handler do request = a_request_that_submits(data, :accept_header => "unknown, also/unknown") expect do - handler.do_destroy(indirection.name, "my data", {}, request, response) + handler.do_destroy(indirection, "my data", {}, request, response) end.to raise_error(Puppet::Network::HTTP::Handler::HTTPNotAcceptableError) Puppet::TestModel.indirection.find("my data").should_not be_nil @@ -460,7 +458,7 @@ describe Puppet::Network::HTTP::Handler do request[:content_type_header] = "application/x-raw" request[:body] = '' - handler.do_save(indirection.name, "test", {}, request, response) + handler.do_save(indirection, "test", {}, request, response) Puppet::TestModel.indirection.find("test").data.should == '' end @@ -469,7 +467,7 @@ describe Puppet::Network::HTTP::Handler do data = Puppet::TestModel.new("my data", "some data") request = a_request_that_submits(data) - handler.do_save(indirection.name, "my data", {}, request, response) + handler.do_save(indirection, "my data", {}, request, response) Puppet::TestModel.indirection.find("my data").should == data end @@ -481,7 +479,7 @@ describe Puppet::Network::HTTP::Handler do handler.expects(:set_response).with(response, data.render(:yaml)) handler.expects(:set_content_type).with(response, Puppet::Network::FormatHandler.format(:yaml)) - handler.do_save(indirection.name, "my data", {}, request, response) + handler.do_save(indirection, "my data", {}, request, response) end it "uses the first supported format for the response" do @@ -491,7 +489,7 @@ describe Puppet::Network::HTTP::Handler do handler.expects(:set_response).with(response, data.render(:pson)) handler.expects(:set_content_type).with(response, Puppet::Network::FormatHandler.format(:pson)) - handler.do_save(indirection.name, "my data", {}, request, response) + handler.do_save(indirection, "my data", {}, request, response) end it "raises an error and does not save when no accepted formats are known" do @@ -499,7 +497,7 @@ describe Puppet::Network::HTTP::Handler do request = a_request_that_submits(data, :accept_header => "unknown, also/unknown") expect do - handler.do_save(indirection.name, "my data", {}, request, response) + handler.do_save(indirection, "my data", {}, request, response) end.to raise_error(Puppet::Network::HTTP::Handler::HTTPNotAcceptableError) Puppet::TestModel.indirection.find("my data").should be_nil @@ -543,7 +541,8 @@ describe Puppet::Network::HTTP::Handler do end def set_response(response, body, status = 200) - "my_result" + response[:body] = body + response[:status] = status end def http_method(request) diff --git a/spec/unit/network/http_pool_spec.rb b/spec/unit/network/http_pool_spec.rb index c671e88f1..4ee507568 100755 --- a/spec/unit/network/http_pool_spec.rb +++ b/spec/unit/network/http_pool_spec.rb @@ -3,6 +3,10 @@ require 'spec_helper' require 'puppet/network/http_pool' describe Puppet::Network::HttpPool do + before :each do + Puppet::SSL::Key.indirection.terminus_class = :memory + Puppet::SSL::CertificateRequest.indirection.terminus_class = :memory + end describe "when managing http instances" do @@ -26,15 +30,14 @@ describe Puppet::Network::HttpPool do describe 'peer verification' do def setup_standard_ssl_configuration ca_cert_file = File.expand_path('/path/to/ssl/certs/ca_cert.pem') - FileTest.stubs(:exist?).with(ca_cert_file).returns(true) - ssl_configuration = stub('ssl_configuration', :ca_auth_file => ca_cert_file) - Puppet::Network::HTTP::Connection.any_instance.stubs(:ssl_configuration).returns(ssl_configuration) + Puppet[:ssl_client_ca_auth] = ca_cert_file + Puppet::FileSystem::File.stubs(:exist?).with(ca_cert_file).returns(true) end def setup_standard_hostcert host_cert_file = File.expand_path('/path/to/ssl/certs/host_cert.pem') - FileTest.stubs(:exist?).with(host_cert_file).returns(true) + Puppet::FileSystem::File.stubs(:exist?).with(host_cert_file).returns(true) Puppet[:hostcert] = host_cert_file end @@ -44,7 +47,7 @@ describe Puppet::Network::HttpPool do key = stub('key', :content => 'real_key') host = stub('host', :certificate => cert, :key => key, :ssl_store => stub('store')) - Puppet::Network::HTTP::Connection.any_instance.stubs(:ssl_host).returns(host) + Puppet::SSL::Host.stubs(:localhost).returns(host) end before do diff --git a/spec/unit/node/environment_spec.rb b/spec/unit/node/environment_spec.rb index 7b2d73b8a..be8ae1a94 100755 --- a/spec/unit/node/environment_spec.rb +++ b/spec/unit/node/environment_spec.rb @@ -20,111 +20,97 @@ describe Puppet::Node::Environment do it "should use the filetimeout for the ttl for the modulepath" do Puppet::Node::Environment.attr_ttl(:modulepath).should == Integer(Puppet[:filetimeout]) end - + it "should use the filetimeout for the ttl for the module list" do Puppet::Node::Environment.attr_ttl(:modules).should == Integer(Puppet[:filetimeout]) end - + it "should use the default environment if no name is provided while initializing an environment" do Puppet[:environment] = "one" Puppet::Node::Environment.new.name.should == :one end - + it "should treat environment instances as singletons" do Puppet::Node::Environment.new("one").should equal(Puppet::Node::Environment.new("one")) end - + it "should treat an environment specified as names or strings as equivalent" do Puppet::Node::Environment.new(:one).should equal(Puppet::Node::Environment.new("one")) end - + it "should return its name when converted to a string" do Puppet::Node::Environment.new(:one).to_s.should == "one" end - + it "should just return any provided environment if an environment is provided as the name" do one = Puppet::Node::Environment.new(:one) Puppet::Node::Environment.new(one).should equal(one) end - + describe "when managing known resource types" do before do @collection = Puppet::Resource::TypeCollection.new(env) env.stubs(:perform_initial_import).returns(Puppet::Parser::AST::Hostclass.new('')) - Thread.current[:known_resource_types] = nil + $known_resource_types = nil end - + it "should create a resource type collection if none exists" do Puppet::Resource::TypeCollection.expects(:new).with(env).returns @collection env.known_resource_types.should equal(@collection) end - + it "should reuse any existing resource type collection" do env.known_resource_types.should equal(env.known_resource_types) end - + it "should perform the initial import when creating a new collection" do env.expects(:perform_initial_import).returns(Puppet::Parser::AST::Hostclass.new('')) env.known_resource_types end - + it "should return the same collection even if stale if it's the same thread" do Puppet::Resource::TypeCollection.stubs(:new).returns @collection env.known_resource_types.stubs(:stale?).returns true - + env.known_resource_types.should equal(@collection) end - + it "should return the current thread associated collection if there is one" do - Thread.current[:known_resource_types] = @collection - + $known_resource_types = @collection + env.known_resource_types.should equal(@collection) end - - it "should give to all threads using the same environment the same collection if the collection isn't stale" do - @original_thread_type_collection = Puppet::Resource::TypeCollection.new(env) - Puppet::Resource::TypeCollection.expects(:new).with(env).returns @original_thread_type_collection - env.known_resource_types.should equal(@original_thread_type_collection) - - @original_thread_type_collection.expects(:require_reparse?).returns(false) - Puppet::Resource::TypeCollection.stubs(:new).with(env).returns @collection - - t = Thread.new { - env.known_resource_types.should equal(@original_thread_type_collection) - } - t.join - end - + it "should generate a new TypeCollection if the current one requires reparsing" do old_type_collection = env.known_resource_types old_type_collection.stubs(:require_reparse?).returns true - Thread.current[:known_resource_types] = nil + $known_resource_types = nil new_type_collection = env.known_resource_types - + new_type_collection.should be_a Puppet::Resource::TypeCollection new_type_collection.should_not equal(old_type_collection) end end - + it "should validate the modulepath directories" do real_file = tmpdir('moduledir') path = %W[/one /two #{real_file}].join(File::PATH_SEPARATOR) - + Puppet[:modulepath] = path - + env.modulepath.should == [real_file] end - + it "should prefix the value of the 'PUPPETLIB' environment variable to the module path if present" do Puppet::Util.withenv("PUPPETLIB" => %w{/l1 /l2}.join(File::PATH_SEPARATOR)) do module_path = %w{/one /two}.join(File::PATH_SEPARATOR) env.expects(:validate_dirs).with(%w{/l1 /l2 /one /two}).returns %w{/l1 /l2 /one /two} env.expects(:[]).with(:modulepath).returns module_path - + env.modulepath.should == %w{/l1 /l2 /one /two} end end - + describe "when validating modulepath or manifestdir directories" do before :each do @path_one = tmpdir("path_one") @@ -132,69 +118,69 @@ describe Puppet::Node::Environment do sep = File::PATH_SEPARATOR Puppet[:modulepath] = "#{@path_one}#{sep}#{@path_two}" end - + it "should not return non-directories" do FileTest.expects(:directory?).with(@path_one).returns true FileTest.expects(:directory?).with(@path_two).returns false - + env.validate_dirs([@path_one, @path_two]).should == [@path_one] end - + it "should use the current working directory to fully-qualify unqualified paths" do FileTest.stubs(:directory?).returns true - two = File.expand_path("two") + env.validate_dirs([@path_one, 'two']).should == [@path_one, two] end end - + describe "when modeling a specific environment" do it "should have a method for returning the environment name" do Puppet::Node::Environment.new("testing").name.should == :testing end - + it "should provide an array-like accessor method for returning any environment-specific setting" do env.should respond_to(:[]) end - + it "should ask the Puppet settings instance for the setting qualified with the environment name" do Puppet.settings.set_value(:server, "myval", :testing) env[:server].should == "myval" end - + it "should be able to return an individual module that exists in its module path" do env.stubs(:modules).returns [Puppet::Module.new('one', "/one", mock("env"))] - + mod = env.module('one') mod.should be_a(Puppet::Module) mod.name.should == 'one' end - + it "should not return a module if the module doesn't exist" do env.stubs(:modules).returns [Puppet::Module.new('one', "/one", mock("env"))] - + env.module('two').should be_nil end - + it "should return nil if asked for a module that does not exist in its path" do modpath = tmpdir('modpath') env.modulepath = [modpath] - + env.module("one").should be_nil end - + describe "module data" do before do dir = tmpdir("deep_path") - + @first = File.join(dir, "first") @second = File.join(dir, "second") Puppet[:modulepath] = "#{@first}#{File::PATH_SEPARATOR}#{@second}" - + FileUtils.mkdir_p(@first) FileUtils.mkdir_p(@second) end - + describe "#modules_by_path" do it "should return an empty list if there are no modules" do env.modules_by_path.should == { @@ -202,19 +188,19 @@ describe Puppet::Node::Environment do @second => [] } end - + it "should include modules even if they exist in multiple dirs in the modulepath" do modpath1 = File.join(@first, "foo") FileUtils.mkdir_p(modpath1) modpath2 = File.join(@second, "foo") FileUtils.mkdir_p(modpath2) - + env.modules_by_path.should == { @first => [Puppet::Module.new('foo', modpath1, env)], @second => [Puppet::Module.new('foo', modpath2, env)] } end - + it "should ignore modules with invalid names" do FileUtils.mkdir_p(File.join(@first, 'foo')) FileUtils.mkdir_p(File.join(@first, 'foo2')) @@ -226,12 +212,12 @@ describe Puppet::Node::Environment do FileUtils.mkdir_p(File.join(@first, '-foo')) FileUtils.mkdir_p(File.join(@first, 'foo-')) FileUtils.mkdir_p(File.join(@first, 'foo--bar')) - + env.modules_by_path[@first].collect{|mod| mod.name}.sort.should == %w{foo foo-bar foo2 foo_bar} end - + end - + describe "#module_requirements" do it "should return a list of what modules depend on other modules" do PuppetSpec::Modules.create( @@ -266,7 +252,7 @@ describe Puppet::Node::Environment do :dependencies => [{ 'name' => 'puppetlabs/bar', "version_requirement" => "~3.0.0" }] } ) - + env.module_requirements.should == { 'puppetlabs/alpha' => [], 'puppetlabs/foo' => [ @@ -297,7 +283,7 @@ describe Puppet::Node::Environment do } end end - + describe ".module_by_forge_name" do it "should find modules by forge_name" do mod = PuppetSpec::Modules.create( @@ -308,7 +294,7 @@ describe Puppet::Node::Environment do ) env.module_by_forge_name('puppetlabs/baz').should == mod end - + it "should not find modules with same name by the wrong author" do mod = PuppetSpec::Modules.create( 'baz', @@ -318,17 +304,17 @@ describe Puppet::Node::Environment do ) env.module_by_forge_name('puppetlabs/baz').should == nil end - + it "should return nil when the module can't be found" do env.module_by_forge_name('ima/nothere').should be_nil end end - + describe ".modules" do it "should return an empty list if there are no modules" do env.modules.should == [] end - + it "should return a module named for every directory in each module path" do %w{foo bar}.each do |mod_name| FileUtils.mkdir_p(File.join(@first, mod_name)) @@ -338,14 +324,14 @@ describe Puppet::Node::Environment do end env.modules.collect{|mod| mod.name}.sort.should == %w{foo bar bee baz}.sort end - + it "should remove duplicates" do FileUtils.mkdir_p(File.join(@first, 'foo')) FileUtils.mkdir_p(File.join(@second, 'foo')) - + env.modules.collect{|mod| mod.name}.sort.should == %w{foo} end - + it "should ignore modules with invalid names" do FileUtils.mkdir_p(File.join(@first, 'foo')) FileUtils.mkdir_p(File.join(@first, 'foo2')) @@ -353,63 +339,63 @@ describe Puppet::Node::Environment do FileUtils.mkdir_p(File.join(@first, 'foo_bar')) FileUtils.mkdir_p(File.join(@first, 'foo=bar')) FileUtils.mkdir_p(File.join(@first, 'foo bar')) - + env.modules.collect{|mod| mod.name}.sort.should == %w{foo foo-bar foo2 foo_bar} end - + it "should create modules with the correct environment" do FileUtils.mkdir_p(File.join(@first, 'foo')) env.modules.each {|mod| mod.environment.should == env } end - + end end - + it "should cache the module list" do env.modulepath = %w{/a} Dir.expects(:entries).once.with("/a").returns %w{foo} - + env.modules env.modules end end - + describe Puppet::Node::Environment::Helper do before do @helper = Object.new @helper.extend(Puppet::Node::Environment::Helper) end - + it "should be able to set and retrieve the environment as a symbol" do @helper.environment = :foo @helper.environment.name.should == :foo end - + it "should accept an environment directly" do @helper.environment = Puppet::Node::Environment.new(:foo) @helper.environment.name.should == :foo end - + it "should accept an environment as a string" do @helper.environment = 'foo' @helper.environment.name.should == :foo end end - + describe "when performing initial import" do before do @parser = Puppet::Parser::ParserFactory.parser("test") # @parser = Puppet::Parser::EParserAdapter.new(Puppet::Parser::Parser.new("test")) # TODO: FIX PARSER FACTORY Puppet::Parser::ParserFactory.stubs(:parser).returns @parser end - + it "should set the parser's string to the 'code' setting and parse if code is available" do Puppet.settings[:code] = "my code" @parser.expects(:string=).with "my code" @parser.expects(:parse) env.instance_eval { perform_initial_import } end - + it "should set the parser's file to the 'manifest' setting and parse if no code is available and the manifest is available" do filename = tmpfile('myfile') File.open(filename, 'w'){|f| } @@ -418,7 +404,7 @@ describe Puppet::Node::Environment do @parser.expects(:parse) env.instance_eval { perform_initial_import } end - + it "should pass the manifest file to the parser even if it does not exist on disk" do filename = tmpfile('myfile') Puppet.settings[:code] = "" @@ -427,15 +413,15 @@ describe Puppet::Node::Environment do @parser.expects(:parse).once env.instance_eval { perform_initial_import } end - + it "should fail helpfully if there is an error importing" do - File.stubs(:exist?).returns true + Puppet::FileSystem::File.stubs(:exist?).returns true env.stubs(:known_resource_types).returns Puppet::Resource::TypeCollection.new(env) @parser.expects(:file=).once @parser.expects(:parse).raises ArgumentError lambda { env.instance_eval { perform_initial_import } }.should raise_error(Puppet::Error) end - + it "should not do anything if the ignore_import settings is set" do Puppet.settings[:ignoreimport] = true @parser.expects(:string=).never @@ -443,11 +429,11 @@ describe Puppet::Node::Environment do @parser.expects(:parse).never env.instance_eval { perform_initial_import } end - + it "should mark the type collection as needing a reparse when there is an error parsing" do @parser.expects(:parse).raises Puppet::ParseError.new("Syntax error at ...") env.stubs(:known_resource_types).returns Puppet::Resource::TypeCollection.new(env) - + lambda { env.instance_eval { perform_initial_import } }.should raise_error(Puppet::Error, /Syntax error at .../) env.known_resource_types.require_reparse?.should be_true end @@ -465,5 +451,5 @@ describe Puppet::Node::Environment do end it_behaves_like 'the environment' end - + end diff --git a/spec/unit/node/facts_spec.rb b/spec/unit/node/facts_spec.rb index 9ba6baa11..55e3dbbf6 100755 --- a/spec/unit/node/facts_spec.rb +++ b/spec/unit/node/facts_spec.rb @@ -1,8 +1,17 @@ #! /usr/bin/env ruby require 'spec_helper' -require 'matchers/json' require 'puppet/node/facts' +# the json-schema gem doesn't support windows +if not Puppet.features.microsoft_windows? + describe "catalog facts schema" do + it "should validate against the json meta-schema" do + JSON::Validator.validate!(JSON_META_SCHEMA, FACTS_SCHEMA) + end + end + +end + describe Puppet::Node::Facts, "when indirecting" do before do @facts = Puppet::Node::Facts.new("me") @@ -143,8 +152,16 @@ describe Puppet::Node::Facts, "when indirecting" do result = PSON.parse(facts.to_pson) result['name'].should == facts.name result['values'].should == facts.values.reject { |key, value| key.to_s =~ /_/ } - result['timestamp'].should == facts.timestamp.to_s - result['expiration'].should == facts.expiration.to_s + result['timestamp'].should == facts.timestamp.iso8601(9) + result['expiration'].should == facts.expiration.iso8601(9) + end + + it "should generate valid facts data against the facts schema", :unless => Puppet.features.microsoft_windows? do + Time.stubs(:now).returns(@timestamp) + facts = Puppet::Node::Facts.new("foo", {'a' => 1, 'b' => 2, 'c' => 3}) + facts.expiration = @expiration + + JSON::Validator.validate!(FACTS_SCHEMA, facts.to_pson) end it "should not include nil values" do diff --git a/spec/unit/node_spec.rb b/spec/unit/node_spec.rb index 5c36149ad..f8691ed9d 100755 --- a/spec/unit/node_spec.rb +++ b/spec/unit/node_spec.rb @@ -2,6 +2,17 @@ require 'spec_helper' require 'matchers/json' +# the json-schema gem doesn't support windows +if not Puppet.features.microsoft_windows? + NODE_SCHEMA = JSON.parse(File.read(File.join(File.dirname(__FILE__), '../../api/schemas/node.json'))) + + describe "node schema" do + it "should validate against the json meta-schema" do + JSON::Validator.validate!(JSON_META_SCHEMA, NODE_SCHEMA) + end + end +end + describe Puppet::Node do it "should register its document type as Node" do PSON.registered_document_types["Node"].should equal(Puppet::Node) @@ -55,6 +66,38 @@ describe Puppet::Node do new_node.name.should == node.name end + it "can round-trip through pson" do + facts = Puppet::Node::Facts.new("hello", "one" => "c", "two" => "b") + node = Puppet::Node.new("hello", + :environment => 'kjhgrg', + :classes => ['erth', 'aiu'], + :parameters => {"hostname"=>"food"} + ) + new_node = Puppet::Node.convert_from('pson', node.render('pson')) + new_node.environment.should == node.environment + new_node.parameters.should == node.parameters + new_node.classes.should == node.classes + new_node.name.should == node.name + end + + it "validates against the node json schema", :unless => Puppet.features.microsoft_windows? do + facts = Puppet::Node::Facts.new("hello", "one" => "c", "two" => "b") + node = Puppet::Node.new("hello", + :environment => 'kjhgrg', + :classes => ['erth', 'aiu'], + :parameters => {"hostname"=>"food"} + ) + JSON::Validator.validate!(NODE_SCHEMA, node.to_pson) + end + + it "when missing optional parameters validates against the node json schema", :unless => Puppet.features.microsoft_windows? do + facts = Puppet::Node::Facts.new("hello", "one" => "c", "two" => "b") + node = Puppet::Node.new("hello", + :environment => 'kjhgrg' + ) + JSON::Validator.validate!(NODE_SCHEMA, node.to_pson) + end + describe "when converting to json" do before do @node = Puppet::Node.new("mynode") diff --git a/spec/unit/parameter/boolean_spec.rb b/spec/unit/parameter/boolean_spec.rb index 7039c42fc..505bc561f 100644 --- a/spec/unit/parameter/boolean_spec.rb +++ b/spec/unit/parameter/boolean_spec.rb @@ -5,21 +5,31 @@ require 'puppet/parameter/boolean' describe Puppet::Parameter::Boolean do let (:resource) { mock('resource') } - subject { described_class.new(:resource => resource) } - - [ true, :true, 'true', :yes, 'yes', 'TrUe', 'yEs' ].each do |arg| - it "should munge #{arg.inspect} as true" do - subject.munge(arg).should == true + describe "after initvars" do + before { described_class.initvars } + it "should have the correct value_collection" do + described_class.value_collection.values.sort.should == + [:true, :false, :yes, :no].sort end end - [ false, :false, 'false', :no, 'no', 'FaLSE', 'nO' ].each do |arg| - it "should munge #{arg.inspect} as false" do - subject.munge(arg).should == false + + describe "instances" do + subject { described_class.new(:resource => resource) } + + [ true, :true, 'true', :yes, 'yes', 'TrUe', 'yEs' ].each do |arg| + it "should munge #{arg.inspect} as true" do + subject.munge(arg).should == true + end end - end - [ nil, :undef, 'undef', '0', 0, '1', 1, 9284 ].each do |arg| - it "should fail to munge #{arg.inspect}" do - expect { subject.munge(arg) }.to raise_error Puppet::Error + [ false, :false, 'false', :no, 'no', 'FaLSE', 'nO' ].each do |arg| + it "should munge #{arg.inspect} as false" do + subject.munge(arg).should == false + end + end + [ nil, :undef, 'undef', '0', 0, '1', 1, 9284 ].each do |arg| + it "should fail to munge #{arg.inspect}" do + expect { subject.munge(arg) }.to raise_error Puppet::Error + end end end end diff --git a/spec/unit/parser/ast/resourceparam_spec.rb b/spec/unit/parser/ast/resourceparam_spec.rb new file mode 100644 index 000000000..818f146d3 --- /dev/null +++ b/spec/unit/parser/ast/resourceparam_spec.rb @@ -0,0 +1,51 @@ +#! /usr/bin/env ruby +require 'spec_helper' + +describe Puppet::Parser::AST::ResourceParam do + + ast = Puppet::Parser::AST + + before :each do + @compiler = Puppet::Parser::Compiler.new(Puppet::Node.new("mynode")) + @scope = Puppet::Parser::Scope.new(@compiler) + @params = ast::ASTArray.new({}) + @compiler.stubs(:add_override) + end + + it "should evaluate the parameter value" do + object = mock 'object' + object.expects(:safeevaluate).with(@scope).returns('value') + ast::ResourceParam.new(:param => 'myparam', :value => object).evaluate(@scope) + end + + it "should return a Puppet::Parser::Resource::Param on evaluation" do + object = mock 'object' + object.expects(:safeevaluate).with(@scope).returns('value') + evaled = ast::ResourceParam.new(:param => 'myparam', :value => object).evaluate(@scope) + evaled.should be_a(Puppet::Parser::Resource::Param) + evaled.name.to_s.should == 'myparam' + evaled.value.to_s.should == 'value' + end + + it "should copy line numbers to Puppet::Parser::Resource::Param" do + object = mock 'object' + object.expects(:safeevaluate).with(@scope).returns('value') + evaled = ast::ResourceParam.new(:param => 'myparam', :value => object, :line => 42).evaluate(@scope) + evaled.line.should == 42 + end + + it "should copy source file to Puppet::Parser::Resource::Param" do + object = mock 'object' + object.expects(:safeevaluate).with(@scope).returns('value') + evaled = ast::ResourceParam.new(:param => 'myparam', :value => object, :file => 'foo.pp').evaluate(@scope) + evaled.file.should == 'foo.pp' + end + + it "should change nil parameter values to undef" do + object = mock 'object' + object.expects(:safeevaluate).with(@scope).returns(nil) + evaled = ast::ResourceParam.new(:param => 'myparam', :value => object).evaluate(@scope) + evaled.should be_a(Puppet::Parser::Resource::Param) + evaled.value.should == :undef + end +end diff --git a/spec/unit/parser/compiler_spec.rb b/spec/unit/parser/compiler_spec.rb index 8e71756e1..68210a4df 100755 --- a/spec/unit/parser/compiler_spec.rb +++ b/spec/unit/parser/compiler_spec.rb @@ -1,5 +1,6 @@ #! /usr/bin/env ruby require 'spec_helper' +require 'puppet_spec/compiler' class CompilerTestResource attr_accessor :builtin, :virtual, :evaluated, :type, :title @@ -106,22 +107,20 @@ describe Puppet::Parser::Compiler do @compiler.classlist.sort.should == %w{one two}.sort end - it "should clear the thread local caches before compile" do + it "should clear the global caches before compile" do compiler = stub 'compiler' Puppet::Parser::Compiler.expects(:new).with(@node).returns compiler catalog = stub 'catalog' compiler.expects(:compile).returns catalog catalog.expects(:to_resource) - [:known_resource_types, :env_module_directories].each do |var| - Thread.current[var] = "rspec" - end + $known_resource_types = "rspec" + $env_module_directories = "rspec" Puppet::Parser::Compiler.compile(@node) - [:known_resource_types, :env_module_directories].each do |var| - Thread.current[var].should == nil - end + $known_resource_types = nil + $env_module_directories = nil end describe "when initializing" do @@ -218,27 +217,6 @@ describe Puppet::Parser::Compiler do @compiler.catalog.server_version.should == "3" end - it "should evaluate any existing classes named in the node" do - classes = %w{one two three four} - main = stub 'main' - one = stub 'one', :name => "one" - three = stub 'three', :name => "three" - @node.stubs(:name).returns("whatever") - @node.stubs(:classes).returns(classes) - compile_stub(:evaluate_node_classes) - - @compiler.expects(:evaluate_classes).with(classes, @compiler.topscope) - @compiler.compile - end - - it "should evaluate any parameterized classes named in the node" do - classes = {'foo'=>{'p1'=>'one'}, 'bar'=>{'p2'=>'two'}} - @node.stubs(:classes).returns(classes) - @compiler.expects(:evaluate_classes).with(classes, @compiler.topscope) - @compiler.compile - end - - it "should evaluate the main class if it exists" do compile_stub(:evaluate_main) main_class = @known_resource_types.add Puppet::Resource::Type.new(:hostclass, "") @@ -262,12 +240,6 @@ describe Puppet::Parser::Compiler do @compiler.catalog.edge?(stage, klass).should be_true end - it "should evaluate any node classes" do - @node.stubs(:classes).returns(%w{one two three four}) - @compiler.expects(:evaluate_classes).with(%w{one two three four}, @compiler.topscope) - @compiler.send(:evaluate_node_classes) - end - it "should evaluate all added collections" do colls = [] # And when the collections fail to evaluate. @@ -655,7 +627,7 @@ describe Puppet::Parser::Compiler do catalog = @compiler.compile r2 = catalog.resources.detect {|r| r.title == 'Bar::Foo' } - r2.tags.should =~ ['bar::foo', 'class', 'bar', 'foo'] + r2.tags.should == Puppet::Util::TagSet.new(['bar::foo', 'class', 'bar', 'foo']) end end @@ -791,6 +763,102 @@ describe Puppet::Parser::Compiler do end end + describe "when evaluating node classes" do + include PuppetSpec::Compiler + + describe "when provided classes in array format" do + let(:node) { Puppet::Node.new('someone', :classes => ['something']) } + + describe "when the class exists" do + it "should succeed if the class is already included" do + manifest = <<-MANIFEST + class something {} + include something + MANIFEST + + catalog = compile_to_catalog(manifest, node) + + catalog.resource('Class', 'Something').should_not be_nil + end + + it "should evaluate the class without parameters if it's not already included" do + manifest = "class something {}" + + catalog = compile_to_catalog(manifest, node) + + catalog.resource('Class', 'Something').should_not be_nil + end + end + + it "should fail if the class doesn't exist" do + expect { compile_to_catalog('', node) }.to raise_error(Puppet::Error, /Could not find class something/) + end + end + + describe "when provided classes in hash format" do + describe "for classes without parameters" do + let(:node) { Puppet::Node.new('someone', :classes => {'something' => {}}) } + + describe "when the class exists" do + it "should succeed if the class is already included" do + manifest = <<-MANIFEST + class something {} + include something + MANIFEST + + catalog = compile_to_catalog(manifest, node) + + catalog.resource('Class', 'Something').should_not be_nil + end + + it "should evaluate the class if it's not already included" do + manifest = <<-MANIFEST + class something {} + MANIFEST + + catalog = compile_to_catalog(manifest, node) + + catalog.resource('Class', 'Something').should_not be_nil + end + end + + it "should fail if the class doesn't exist" do + expect { compile_to_catalog('', node) }.to raise_error(Puppet::Error, /Could not find class something/) + end + end + + describe "for classes with parameters" do + let(:node) { Puppet::Node.new('someone', :classes => {'something' => {'configuron' => 'defrabulated'}}) } + + describe "when the class exists" do + it "should fail if the class is already included" do + manifest = <<-MANIFEST + class something($configuron=frabulated) {} + include something + MANIFEST + + expect { compile_to_catalog(manifest, node) }.to raise_error(Puppet::Error, /Class\[Something\] is already declared/) + end + + it "should evaluate the class if it's not already included" do + manifest = <<-MANIFEST + class something($configuron=frabulated) {} + MANIFEST + + catalog = compile_to_catalog(manifest, node) + + resource = catalog.resource('Class', 'Something') + resource['configuron'].should == 'defrabulated' + end + end + + it "should fail if the class doesn't exist" do + expect { compile_to_catalog('', node) }.to raise_error(Puppet::Error, /Could not find class something/) + end + end + end + end + describe "when managing resource overrides" do before do diff --git a/spec/unit/parser/eparser_adapter_spec.rb b/spec/unit/parser/eparser_adapter_spec.rb index 051633434..173cfb783 100644 --- a/spec/unit/parser/eparser_adapter_spec.rb +++ b/spec/unit/parser/eparser_adapter_spec.rb @@ -365,42 +365,42 @@ describe Puppet::Parser do end context "when parsing method calls" do it "should parse method call with one param lambda" do - expect { @parser.parse("$a.foreach {|$a| debug $a }") }.to_not raise_error + expect { @parser.parse("$a.each |$a|{ debug $a }") }.to_not raise_error end it "should parse method call with two param lambda" do - expect { @parser.parse("$a.foreach {|$a,$b| debug $a }") }.to_not raise_error + expect { @parser.parse("$a.each |$a,$b|{ debug $a }") }.to_not raise_error end it "should parse method call with two param lambda and default value" do - expect { @parser.parse("$a.foreach {|$a,$b=1| debug $a }") }.to_not raise_error + expect { @parser.parse("$a.each |$a,$b=1|{ debug $a }") }.to_not raise_error end it "should parse method call without lambda (statement)" do - expect { @parser.parse("$a.foreach") }.to_not raise_error + expect { @parser.parse("$a.each") }.to_not raise_error end it "should parse method call without lambda (expression)" do - expect { @parser.parse("$x = $a.foreach + 1") }.to_not raise_error + expect { @parser.parse("$x = $a.each + 1") }.to_not raise_error end context "a receiver expression of type" do it "variable should be allowed" do - expect { @parser.parse("$a.foreach") }.to_not raise_error + expect { @parser.parse("$a.each") }.to_not raise_error end it "hasharrayaccess should be allowed" do - expect { @parser.parse("$a[0][1].foreach") }.to_not raise_error + expect { @parser.parse("$a[0][1].each") }.to_not raise_error end it "quoted text should be allowed" do - expect { @parser.parse("\"monkey\".foreach") }.to_not raise_error - expect { @parser.parse("'monkey'.foreach") }.to_not raise_error + expect { @parser.parse("\"monkey\".each") }.to_not raise_error + expect { @parser.parse("'monkey'.each") }.to_not raise_error end it "selector text should be allowed" do - expect { @parser.parse("$a ? { 'banana'=>[1,2,3]}.foreach") }.to_not raise_error + expect { @parser.parse("$a ? { 'banana'=>[1,2,3]}.each") }.to_not raise_error end it "function call should be allowed" do - expect { @parser.parse("duh(1,2,3).foreach") }.to_not raise_error + expect { @parser.parse("duh(1,2,3).each") }.to_not raise_error end it "method call should be allowed" do expect { @parser.parse("$a.foo.bar") }.to_not raise_error end it "chained method calls with lambda should be allowed" do - expect { @parser.parse("$a.foo{||}.bar{||}") }.to_not raise_error + expect { @parser.parse("$a.foo||{}.bar||{}") }.to_not raise_error end end end diff --git a/spec/unit/parser/files_spec.rb b/spec/unit/parser/files_spec.rb index 2e84216cb..ca7e45b13 100755 --- a/spec/unit/parser/files_spec.rb +++ b/spec/unit/parser/files_spec.rb @@ -28,7 +28,7 @@ describe Puppet::Parser::Files do Puppet[:templatedir] = "/my/templates" Puppet[:modulepath] = "/one:/two" File.stubs(:directory?).returns(true) - FileTest.stubs(:exist?).returns(true) + Puppet::FileSystem::File.stubs(:exist?).returns(true) Puppet::Parser::Files.find_template("mymod/mytemplate").should == File.join(Puppet[:templatedir], "mymod/mytemplate") end @@ -43,59 +43,59 @@ describe Puppet::Parser::Files do end it "should return unqualified templates if they exist in the template dir" do - FileTest.stubs(:exist?).returns true + Puppet::FileSystem::File.stubs(:exist?).returns true Puppet::Parser::Files.stubs(:templatepath).with(nil).returns(["/my/templates"]) Puppet::Parser::Files.find_template("mytemplate").should == "/my/templates/mytemplate" end it "should only return templates if they actually exist" do - FileTest.expects(:exist?).with("/my/templates/mytemplate").returns true + Puppet::FileSystem::File.expects(:exist?).with("/my/templates/mytemplate").returns true Puppet::Parser::Files.stubs(:templatepath).with(nil).returns(["/my/templates"]) Puppet::Parser::Files.find_template("mytemplate").should == "/my/templates/mytemplate" end it "should return nil when asked for a template that doesn't exist" do - FileTest.expects(:exist?).with("/my/templates/mytemplate").returns false + Puppet::FileSystem::File.expects(:exist?).with("/my/templates/mytemplate").returns false Puppet::Parser::Files.stubs(:templatepath).with(nil).returns(["/my/templates"]) Puppet::Parser::Files.find_template("mytemplate").should be_nil end it "should search in the template directories before modules" do - FileTest.stubs(:exist?).returns true + Puppet::FileSystem::File.stubs(:exist?).returns true Puppet::Parser::Files.stubs(:templatepath).with(nil).returns(["/my/templates"]) Puppet::Module.expects(:find).never Puppet::Parser::Files.find_template("mytemplate") end it "should accept relative templatedirs" do - FileTest.stubs(:exist?).returns true + Puppet::FileSystem::File.stubs(:exist?).returns true Puppet[:templatedir] = "my/templates" File.expects(:directory?).with(File.expand_path("my/templates")).returns(true) Puppet::Parser::Files.find_template("mytemplate").should == File.expand_path("my/templates/mytemplate") end it "should use the environment templatedir if no module is found and an environment is specified" do - FileTest.stubs(:exist?).returns true + Puppet::FileSystem::File.stubs(:exist?).returns true Puppet::Parser::Files.stubs(:templatepath).with("myenv").returns(["/myenv/templates"]) Puppet::Parser::Files.find_template("mymod/mytemplate", "myenv").should == "/myenv/templates/mymod/mytemplate" end it "should use first dir from environment templatedir if no module is found and an environment is specified" do - FileTest.stubs(:exist?).returns true + Puppet::FileSystem::File.stubs(:exist?).returns true Puppet::Parser::Files.stubs(:templatepath).with("myenv").returns(["/myenv/templates", "/two/templates"]) Puppet::Parser::Files.find_template("mymod/mytemplate", "myenv").should == "/myenv/templates/mymod/mytemplate" end it "should use a valid dir when templatedir is a path for unqualified templates and the first dir contains template" do Puppet::Parser::Files.stubs(:templatepath).returns(["/one/templates", "/two/templates"]) - FileTest.expects(:exist?).with("/one/templates/mytemplate").returns(true) + Puppet::FileSystem::File.expects(:exist?).with("/one/templates/mytemplate").returns(true) Puppet::Parser::Files.find_template("mytemplate").should == "/one/templates/mytemplate" end it "should use a valid dir when templatedir is a path for unqualified templates and only second dir contains template" do Puppet::Parser::Files.stubs(:templatepath).returns(["/one/templates", "/two/templates"]) - FileTest.expects(:exist?).with("/one/templates/mytemplate").returns(false) - FileTest.expects(:exist?).with("/two/templates/mytemplate").returns(true) + Puppet::FileSystem::File.expects(:exist?).with("/one/templates/mytemplate").returns(false) + Puppet::FileSystem::File.expects(:exist?).with("/two/templates/mytemplate").returns(true) Puppet::Parser::Files.find_template("mytemplate").should == "/two/templates/mytemplate" end diff --git a/spec/unit/parser/functions/contain_spec.rb b/spec/unit/parser/functions/contain_spec.rb new file mode 100644 index 000000000..3150e0c8e --- /dev/null +++ b/spec/unit/parser/functions/contain_spec.rb @@ -0,0 +1,185 @@ +#! /usr/bin/env ruby +require 'spec_helper' +require 'puppet_spec/compiler' +require 'puppet/parser/functions' +require 'matchers/containment_matchers' +require 'matchers/include_in_order' + +describe 'The "contain" function' do + include PuppetSpec::Compiler + include ContainmentMatchers + + it "includes the class" do + catalog = compile_to_catalog(<<-MANIFEST) + class contained { + notify { "contained": } + } + + class container { + contain contained + } + + include container + MANIFEST + + expect(catalog.classes).to include("contained") + end + + it "makes the class contained in the current class" do + catalog = compile_to_catalog(<<-MANIFEST) + class contained { + notify { "contained": } + } + + class container { + contain contained + } + + include container + MANIFEST + + expect(catalog).to contain_class("contained").in("container") + end + + it "can contain multiple classes" do + catalog = compile_to_catalog(<<-MANIFEST) + class a { + notify { "a": } + } + + class b { + notify { "b": } + } + + class container { + contain a, b + } + + include container + MANIFEST + + expect(catalog).to contain_class("a").in("container") + expect(catalog).to contain_class("b").in("container") + end + + context "when containing a class in multiple classes" do + it "creates a catalog with all containment edges" do + catalog = compile_to_catalog(<<-MANIFEST) + class contained { + notify { "contained": } + } + + class container { + contain contained + } + + class another { + contain contained + } + + include container + include another + MANIFEST + + expect(catalog).to contain_class("contained").in("container") + expect(catalog).to contain_class("contained").in("another") + end + + it "and there are no dependencies applies successfully" do + manifest = <<-MANIFEST + class contained { + notify { "contained": } + } + + class container { + contain contained + } + + class another { + contain contained + } + + include container + include another + MANIFEST + + expect { apply_compiled_manifest(manifest) }.not_to raise_error + end + + it "and there are explicit dependencies on the containing class causes a dependency cycle" do + manifest = <<-MANIFEST + class contained { + notify { "contained": } + } + + class container { + contain contained + } + + class another { + contain contained + } + + include container + include another + + Class["container"] -> Class["another"] + MANIFEST + + expect { apply_compiled_manifest(manifest) }.to raise_error( + Puppet::Error, + /Found 1 dependency cycle/ + ) + end + end + + it "does not create duplicate edges" do + catalog = compile_to_catalog(<<-MANIFEST) + class contained { + notify { "contained": } + } + + class container { + contain contained + contain contained + } + + include container + MANIFEST + + contained = catalog.resource("Class", "contained") + container = catalog.resource("Class", "container") + + expect(catalog.edges_between(container, contained)).to have(1).item + end + + context "when a containing class has a dependency order" do + it "the contained class is applied in that order" do + catalog = compile_to_relationship_graph(<<-MANIFEST) + class contained { + notify { "contained": } + } + + class container { + contain contained + } + + class first { + notify { "first": } + } + + class last { + notify { "last": } + } + + include container, first, last + + Class["first"] -> Class["container"] -> Class["last"] + MANIFEST + + expect(order_resources_traversed_in(catalog)).to include_in_order( + "Notify[first]", "Notify[contained]", "Notify[last]" + ) + end + end +end diff --git a/spec/unit/parser/functions/create_resources_spec.rb b/spec/unit/parser/functions/create_resources_spec.rb index a0c3253ab..79ed02f22 100755 --- a/spec/unit/parser/functions/create_resources_spec.rb +++ b/spec/unit/parser/functions/create_resources_spec.rb @@ -23,6 +23,14 @@ describe 'function for dynamically creating resources' do expect { @scope.function_create_resources(['foo', 'bar', 'blah', 'baz']) }.to raise_error(ArgumentError, 'create_resources(): wrong number of arguments (4; must be 2 or 3)') end + it 'should require second argument to be a hash' do + expect { @scope.function_create_resources(['foo','bar']) }.to raise_error(ArgumentError, 'create_resources(): second argument must be a hash') + end + + it 'should require optional third argument to be a hash' do + expect { @scope.function_create_resources(['foo',{},'foo']) }.to raise_error(ArgumentError, 'create_resources(): third argument, if provided, must be a hash') + end + describe 'when creating native types' do it 'empty hash should not cause resources to be added' do noop_catalog = compile_to_catalog("create_resources('file', {})") @@ -75,12 +83,12 @@ describe 'function for dynamically creating resources' do end describe 'when dynamically creating resource types' do - it 'should be able to create defined resoure types' do + it 'should be able to create defined resource types' do catalog = compile_to_catalog(<<-MANIFEST) define foocreateresource($one) { notify { $name: message => $one } } - + create_resources('foocreateresource', {'blah'=>{'one'=>'two'}}) MANIFEST catalog.resource(:notify, "blah")['message'].should == 'two' @@ -92,7 +100,7 @@ describe 'function for dynamically creating resources' do define foocreateresource($one) { notify { $name: message => $one } } - + create_resources('foocreateresource', {'blah'=>{}}) MANIFEST }.to raise_error(Puppet::Error, 'Must pass one to Foocreateresource[blah] on node foonode') @@ -103,7 +111,7 @@ describe 'function for dynamically creating resources' do define foocreateresource($one) { notify { $name: message => $one } } - + create_resources('foocreateresource', {'blah'=>{'one'=>'two'}, 'blaz'=>{'one'=>'three'}}) MANIFEST @@ -118,7 +126,7 @@ describe 'function for dynamically creating resources' do } notify { test: } - + create_resources('foocreateresource', {'blah'=>{'one'=>'two', 'require' => 'Notify[test]'}}) MANIFEST diff --git a/spec/unit/parser/functions/generate_spec.rb b/spec/unit/parser/functions/generate_spec.rb index 90afbc8ea..593703d63 100755 --- a/spec/unit/parser/functions/generate_spec.rb +++ b/spec/unit/parser/functions/generate_spec.rb @@ -45,7 +45,7 @@ describe "the generate function" do scope.function_generate([command]).should == 'yay' end - describe "on Windows", :as_platform => :windows do + describe "on Windows", :if => Puppet.features.microsoft_windows? do it "should accept the tilde in the path" do command = "C:/DOCUME~1/ADMINI~1/foo.bat" Dir.expects(:chdir).with(File.dirname(command)).returns("yay") diff --git a/spec/unit/parser/functions_spec.rb b/spec/unit/parser/functions_spec.rb index 81ff655a3..8ad33c874 100755 --- a/spec/unit/parser/functions_spec.rb +++ b/spec/unit/parser/functions_spec.rb @@ -128,7 +128,7 @@ describe Puppet::Parser::Functions do describe "::get_function" do it "can retrieve a function defined on the *root* environment" do - Thread.current[:environment] = nil + $environment = nil function = Puppet::Parser::Functions.newfunction("atest", :type => :rvalue) do nil end @@ -162,7 +162,7 @@ describe Puppet::Parser::Functions do describe "::merged_functions" do it "returns functions in both the current and root environment" do - Thread.current[:environment] = nil + $environment = nil func_a = Puppet::Parser::Functions.newfunction("test_a", :type => :rvalue) do nil end diff --git a/spec/unit/parser/lexer_spec.rb b/spec/unit/parser/lexer_spec.rb index fc8394cb1..972a8f1bf 100755 --- a/spec/unit/parser/lexer_spec.rb +++ b/spec/unit/parser/lexer_spec.rb @@ -861,7 +861,7 @@ describe "when trying to lex a non-existent file" do it "should return an empty list of tokens" do lexer = Puppet::Parser::Lexer.new lexer.file = nofile = tmpfile('lexer') - File.exists?(nofile).should == false + Puppet::FileSystem::File.exist?(nofile).should == false lexer.fullscan.should == [[false,false]] end diff --git a/spec/unit/parser/methods/collect_spec.rb b/spec/unit/parser/methods/collect_spec.rb deleted file mode 100644 index acc26652a..000000000 --- a/spec/unit/parser/methods/collect_spec.rb +++ /dev/null @@ -1,153 +0,0 @@ -require 'puppet' -require 'spec_helper' -require 'puppet_spec/compiler' - -require 'unit/parser/methods/shared' - -describe 'the collect method' do - include PuppetSpec::Compiler - - before :each do - Puppet[:parser] = "future" - end - - context "using future parser" do - context "in Ruby style should be callable as" do - it 'collect on an array (multiplying each value by 2)' do - catalog = compile_to_catalog(<<-MANIFEST) - $a = [1,2,3] - $a.collect {|$x| $x*2}.foreach {|$v| - file { "/file_$v": ensure => present } - } - MANIFEST - - catalog.resource(:file, "/file_2")['ensure'].should == 'present' - catalog.resource(:file, "/file_4")['ensure'].should == 'present' - catalog.resource(:file, "/file_6")['ensure'].should == 'present' - end - - it 'collect on a hash selecting keys' do - catalog = compile_to_catalog(<<-MANIFEST) - $a = {'a'=>1,'b'=>2,'c'=>3} - $a.collect {|$x| $x[0]}.foreach {|$k| - file { "/file_$k": ensure => present } - } - MANIFEST - - catalog.resource(:file, "/file_a")['ensure'].should == 'present' - catalog.resource(:file, "/file_b")['ensure'].should == 'present' - catalog.resource(:file, "/file_c")['ensure'].should == 'present' - end - - it 'foreach on a hash selecting value' do - catalog = compile_to_catalog(<<-MANIFEST) - $a = {'a'=>1,'b'=>2,'c'=>3} - $a.collect {|$x| $x[1]}.foreach {|$k| - file { "/file_$k": ensure => present } - } - MANIFEST - - catalog.resource(:file, "/file_1")['ensure'].should == 'present' - catalog.resource(:file, "/file_2")['ensure'].should == 'present' - catalog.resource(:file, "/file_3")['ensure'].should == 'present' - end - end - - context "handles data type corner cases" do - it "collect gets values that are false" do - catalog = compile_to_catalog(<<-MANIFEST) - $a = [false,false] - $a.collect |$x| { $x }.each |$i, $v| { - file { "/file_$i.$v": ensure => present } - } - MANIFEST - - catalog.resource(:file, "/file_0.false")['ensure'].should == 'present' - catalog.resource(:file, "/file_1.false")['ensure'].should == 'present' - end - - it "collect gets values that are nil" do - Puppet::Parser::Functions.newfunction(:nil_array, :type => :rvalue) do |args| - [nil] - end - catalog = compile_to_catalog(<<-MANIFEST) - $a = nil_array() - $a.collect |$x| { $x }.each |$i, $v| { - file { "/file_$i.$v": ensure => present } - } - MANIFEST - - catalog.resource(:file, "/file_0.")['ensure'].should == 'present' - end - - it "collect gets values that are undef" do - catalog = compile_to_catalog(<<-MANIFEST) - $a = [$does_not_exist] - $a.collect |$x = "something"| { $x }.each |$i, $v| { - file { "/file_$i.$v": ensure => present } - } - MANIFEST - - catalog.resource(:file, "/file_0.")['ensure'].should == 'present' - end - end - - context "in Java style should be callable as" do - shared_examples_for 'java style' do - it 'collect on an array (multiplying each value by 2)' do - catalog = compile_to_catalog(<<-MANIFEST) - $a = [1,2,3] - $a.collect |$x| #{farr}{ $x*2}.foreach |$v| #{farr}{ - file { "/file_$v": ensure => present } - } - MANIFEST - - catalog.resource(:file, "/file_2")['ensure'].should == 'present' - catalog.resource(:file, "/file_4")['ensure'].should == 'present' - catalog.resource(:file, "/file_6")['ensure'].should == 'present' - end - - it 'collect on a hash selecting keys' do - catalog = compile_to_catalog(<<-MANIFEST) - $a = {'a'=>1,'b'=>2,'c'=>3} - $a.collect |$x| #{farr}{ $x[0]}.foreach |$k| #{farr}{ - file { "/file_$k": ensure => present } - } - MANIFEST - - catalog.resource(:file, "/file_a")['ensure'].should == 'present' - catalog.resource(:file, "/file_b")['ensure'].should == 'present' - catalog.resource(:file, "/file_c")['ensure'].should == 'present' - end - - it 'foreach on a hash selecting value' do - catalog = compile_to_catalog(<<-MANIFEST) - $a = {'a'=>1,'b'=>2,'c'=>3} - $a.collect |$x| #{farr} {$x[1]}.foreach |$k|#{farr}{ - file { "/file_$k": ensure => present } - } - MANIFEST - - catalog.resource(:file, "/file_1")['ensure'].should == 'present' - catalog.resource(:file, "/file_2")['ensure'].should == 'present' - catalog.resource(:file, "/file_3")['ensure'].should == 'present' - end - end - - describe 'without fat arrow' do - it_should_behave_like 'java style' do - let(:farr) { '' } - end - end - - describe 'with fat arrow' do - it_should_behave_like 'java style' do - let(:farr) { '=>' } - end - end - end - end - - it_should_behave_like 'all iterative functions argument checks', 'collect' - it_should_behave_like 'all iterative functions hash handling', 'collect' -end diff --git a/spec/unit/parser/methods/each_spec.rb b/spec/unit/parser/methods/each_spec.rb index 4d276e95d..5e9ce4e0c 100644 --- a/spec/unit/parser/methods/each_spec.rb +++ b/spec/unit/parser/methods/each_spec.rb @@ -26,7 +26,7 @@ describe 'the each method' do it 'each on an array selecting each value - function call style' do catalog = compile_to_catalog(<<-MANIFEST) $a = [1,2,3] - each ($a) |$index, $v| => { + each ($a) |$index, $v| { file { "/file_$v": ensure => present } } MANIFEST diff --git a/spec/unit/parser/methods/select_spec.rb b/spec/unit/parser/methods/filter_spec.rb index e61ee3a31..89fb15bbb 100644 --- a/spec/unit/parser/methods/select_spec.rb +++ b/spec/unit/parser/methods/filter_spec.rb @@ -4,17 +4,17 @@ require 'puppet_spec/compiler' require 'unit/parser/methods/shared' -describe 'the select method' do +describe 'the filter method' do include PuppetSpec::Compiler before :each do Puppet[:parser] = 'future' end - it 'should select on an array (all berries)' do + it 'should filter on an array (all berries)' do catalog = compile_to_catalog(<<-MANIFEST) $a = ['strawberry','blueberry','orange'] - $a.select {|$x| $x =~ /berry$/}.foreach {|$v| + $a.filter |$x|{ $x =~ /berry$/}.each |$v|{ file { "/file_$v": ensure => present } } MANIFEST @@ -26,7 +26,7 @@ describe 'the select method' do it 'should produce an array when acting on an array' do catalog = compile_to_catalog(<<-MANIFEST) $a = ['strawberry','blueberry','orange'] - $b = $a.select {|$x| $x =~ /berry$/} + $b = $a.filter |$x|{ $x =~ /berry$/} file { "/file_${b[0]}": ensure => present } file { "/file_${b[1]}": ensure => present } MANIFEST @@ -35,10 +35,10 @@ describe 'the select method' do catalog.resource(:file, "/file_blueberry")['ensure'].should == 'present' end - it 'selects on a hash (all berries) by key' do + it 'filters on a hash (all berries) by key' do catalog = compile_to_catalog(<<-MANIFEST) $a = {'strawberry'=>'red','blueberry'=>'blue','orange'=>'orange'} - $a.select {|$x| $x[0] =~ /berry$/}.foreach {|$v| + $a.filter |$x|{ $x[0] =~ /berry$/}.each |$v|{ file { "/file_${v[0]}": ensure => present } } MANIFEST @@ -50,7 +50,7 @@ describe 'the select method' do it 'should produce a hash when acting on a hash' do catalog = compile_to_catalog(<<-MANIFEST) $a = {'strawberry'=>'red','blueberry'=>'blue','orange'=>'orange'} - $b = $a.select {|$x| $x[0] =~ /berry$/} + $b = $a.filter |$x|{ $x[0] =~ /berry$/} file { "/file_${b['strawberry']}": ensure => present } file { "/file_${b['blueberry']}": ensure => present } file { "/file_${b['orange']}": ensure => present } @@ -62,10 +62,10 @@ describe 'the select method' do catalog.resource(:file, "/file_")['ensure'].should == 'present' end - it 'selects on a hash (all berries) by value' do + it 'filters on a hash (all berries) by value' do catalog = compile_to_catalog(<<-MANIFEST) $a = {'strawb'=>'red berry','blueb'=>'blue berry','orange'=>'orange fruit'} - $a.select {|$x| $x[1] =~ /berry$/}.foreach {|$v| + $a.filter |$x|{ $x[1] =~ /berry$/}.each |$v|{ file { "/file_${v[0]}": ensure => present } } MANIFEST @@ -74,6 +74,6 @@ describe 'the select method' do catalog.resource(:file, "/file_blueb")['ensure'].should == 'present' end - it_should_behave_like 'all iterative functions argument checks', 'select' - it_should_behave_like 'all iterative functions hash handling', 'select' + it_should_behave_like 'all iterative functions argument checks', 'filter' + it_should_behave_like 'all iterative functions hash handling', 'filter' end diff --git a/spec/unit/parser/methods/foreach_spec.rb b/spec/unit/parser/methods/foreach_spec.rb deleted file mode 100755 index 72a9379a4..000000000 --- a/spec/unit/parser/methods/foreach_spec.rb +++ /dev/null @@ -1,91 +0,0 @@ -require 'puppet' -require 'spec_helper' -require 'puppet_spec/compiler' -require 'rubygems' - -describe 'the foreach method' do - include PuppetSpec::Compiler - - before :each do - Puppet[:parser] = 'future' - end - - context "should be callable as" do - it 'foreach on an array selecting each value' do - catalog = compile_to_catalog(<<-MANIFEST) - $a = [1,2,3] - $a.foreach {|$v| - file { "/file_$v": ensure => present } - } - MANIFEST - - catalog.resource(:file, "/file_1")['ensure'].should == 'present' - catalog.resource(:file, "/file_2")['ensure'].should == 'present' - catalog.resource(:file, "/file_3")['ensure'].should == 'present' - end - it 'foreach on an array selecting each value - function call style' do - catalog = compile_to_catalog(<<-MANIFEST) - $a = [1,2,3] - foreach ($a) {|$v| - file { "/file_$v": ensure => present } - } - MANIFEST - - catalog.resource(:file, "/file_1")['ensure'].should == 'present' - catalog.resource(:file, "/file_2")['ensure'].should == 'present' - catalog.resource(:file, "/file_3")['ensure'].should == 'present' - end - - it 'foreach on an array with index' do - catalog = compile_to_catalog(<<-MANIFEST) - $a = [present, absent, present] - $a.foreach {|$k,$v| - file { "/file_${$k+1}": ensure => $v } - } - MANIFEST - - catalog.resource(:file, "/file_1")['ensure'].should == 'present' - catalog.resource(:file, "/file_2")['ensure'].should == 'absent' - catalog.resource(:file, "/file_3")['ensure'].should == 'present' - end - - it 'foreach on a hash selecting entries' do - catalog = compile_to_catalog(<<-MANIFEST) - $a = {'a'=>'present','b'=>'absent','c'=>'present'} - $a.foreach {|$e| - file { "/file_${e[0]}": ensure => $e[1] } - } - MANIFEST - - catalog.resource(:file, "/file_a")['ensure'].should == 'present' - catalog.resource(:file, "/file_b")['ensure'].should == 'absent' - catalog.resource(:file, "/file_c")['ensure'].should == 'present' - end - it 'foreach on a hash selecting key and value' do - catalog = compile_to_catalog(<<-MANIFEST) - $a = {'a'=>present,'b'=>absent,'c'=>present} - $a.foreach {|$k, $v| - file { "/file_$k": ensure => $v } - } - MANIFEST - - catalog.resource(:file, "/file_a")['ensure'].should == 'present' - catalog.resource(:file, "/file_b")['ensure'].should == 'absent' - catalog.resource(:file, "/file_c")['ensure'].should == 'present' - end - end - context "should produce receiver" do - it 'each checking produced value using single expression' do - catalog = compile_to_catalog(<<-MANIFEST) - $a = [1, 3, 2] - $b = $a.each |$x| { $x } - file { "/file_${b[1]}": - ensure => present - } - MANIFEST - - catalog.resource(:file, "/file_3")['ensure'].should == 'present' - end - - end -end diff --git a/spec/unit/parser/methods/map_spec.rb b/spec/unit/parser/methods/map_spec.rb new file mode 100644 index 000000000..025501754 --- /dev/null +++ b/spec/unit/parser/methods/map_spec.rb @@ -0,0 +1,95 @@ +require 'puppet' +require 'spec_helper' +require 'puppet_spec/compiler' + +require 'unit/parser/methods/shared' + +describe 'the map method' do + include PuppetSpec::Compiler + + before :each do + Puppet[:parser] = "future" + end + + context "using future parser" do + it 'map on an array (multiplying each value by 2)' do + catalog = compile_to_catalog(<<-MANIFEST) + $a = [1,2,3] + $a.map |$x|{ $x*2}.each |$v|{ + file { "/file_$v": ensure => present } + } + MANIFEST + + catalog.resource(:file, "/file_2")['ensure'].should == 'present' + catalog.resource(:file, "/file_4")['ensure'].should == 'present' + catalog.resource(:file, "/file_6")['ensure'].should == 'present' + end + + it 'map on a hash selecting keys' do + catalog = compile_to_catalog(<<-MANIFEST) + $a = {'a'=>1,'b'=>2,'c'=>3} + $a.map |$x|{ $x[0]}.each |$k|{ + file { "/file_$k": ensure => present } + } + MANIFEST + + catalog.resource(:file, "/file_a")['ensure'].should == 'present' + catalog.resource(:file, "/file_b")['ensure'].should == 'present' + catalog.resource(:file, "/file_c")['ensure'].should == 'present' + end + + it 'each on a hash selecting value' do + catalog = compile_to_catalog(<<-MANIFEST) + $a = {'a'=>1,'b'=>2,'c'=>3} + $a.map |$x|{ $x[1]}.each |$k|{ + file { "/file_$k": ensure => present } + } + MANIFEST + + catalog.resource(:file, "/file_1")['ensure'].should == 'present' + catalog.resource(:file, "/file_2")['ensure'].should == 'present' + catalog.resource(:file, "/file_3")['ensure'].should == 'present' + end + + context "handles data type corner cases" do + it "map gets values that are false" do + catalog = compile_to_catalog(<<-MANIFEST) + $a = [false,false] + $a.map |$x| { $x }.each |$i, $v| { + file { "/file_$i.$v": ensure => present } + } + MANIFEST + + catalog.resource(:file, "/file_0.false")['ensure'].should == 'present' + catalog.resource(:file, "/file_1.false")['ensure'].should == 'present' + end + + it "map gets values that are nil" do + Puppet::Parser::Functions.newfunction(:nil_array, :type => :rvalue) do |args| + [nil] + end + catalog = compile_to_catalog(<<-MANIFEST) + $a = nil_array() + $a.map |$x| { $x }.each |$i, $v| { + file { "/file_$i.$v": ensure => present } + } + MANIFEST + + catalog.resource(:file, "/file_0.")['ensure'].should == 'present' + end + + it "map gets values that are undef" do + catalog = compile_to_catalog(<<-MANIFEST) + $a = [$does_not_exist] + $a.map |$x = "something"| { $x }.each |$i, $v| { + file { "/file_$i.$v": ensure => present } + } + MANIFEST + + catalog.resource(:file, "/file_0.")['ensure'].should == 'present' + end + end + it_should_behave_like 'all iterative functions argument checks', 'map' + it_should_behave_like 'all iterative functions hash handling', 'map' + end +end diff --git a/spec/unit/parser/methods/reduce_spec.rb b/spec/unit/parser/methods/reduce_spec.rb index 99ecfd18d..5d4549b54 100644 --- a/spec/unit/parser/methods/reduce_spec.rb +++ b/spec/unit/parser/methods/reduce_spec.rb @@ -28,40 +28,41 @@ describe 'the reduce method' do it 'reduce on an array' do catalog = compile_to_catalog(<<-MANIFEST) $a = [1,2,3] - $b = $a.reduce {|$memo, $x| $memo + $x } + $b = $a.reduce |$memo, $x| { $memo + $x } file { "/file_$b": ensure => present } MANIFEST catalog.resource(:file, "/file_6")['ensure'].should == 'present' - end + end + it 'reduce on an array with start value' do catalog = compile_to_catalog(<<-MANIFEST) $a = [1,2,3] - $b = $a.reduce(4) {|$memo, $x| $memo + $x } + $b = $a.reduce(4) |$memo, $x| { $memo + $x } file { "/file_$b": ensure => present } MANIFEST - + catalog.resource(:file, "/file_10")['ensure'].should == 'present' - end + end it 'reduce on a hash' do catalog = compile_to_catalog(<<-MANIFEST) $a = {a=>1, b=>2, c=>3} $start = [ignored, 4] - $b = $a.reduce {|$memo, $x| ['sum', $memo[1] + $x[1]] } + $b = $a.reduce |$memo, $x| {['sum', $memo[1] + $x[1]] } file { "/file_${$b[0]}_${$b[1]}": ensure => present } MANIFEST - + catalog.resource(:file, "/file_sum_6")['ensure'].should == 'present' - end + end it 'reduce on a hash with start value' do catalog = compile_to_catalog(<<-MANIFEST) $a = {a=>1, b=>2, c=>3} $start = ['ignored', 4] - $b = $a.reduce($start) {|$memo, $x| ['sum', $memo[1] + $x[1]] } + $b = $a.reduce($start) |$memo, $x| { ['sum', $memo[1] + $x[1]] } file { "/file_${$b[0]}_${$b[1]}": ensure => present } MANIFEST - + catalog.resource(:file, "/file_sum_10")['ensure'].should == 'present' - end + end end end diff --git a/spec/unit/parser/methods/reject_spec.rb b/spec/unit/parser/methods/reject_spec.rb deleted file mode 100644 index e37eaebc5..000000000 --- a/spec/unit/parser/methods/reject_spec.rb +++ /dev/null @@ -1,73 +0,0 @@ -require 'puppet' -require 'spec_helper' -require 'puppet_spec/compiler' - -require 'unit/parser/methods/shared' - -describe 'the reject method' do - include PuppetSpec::Compiler - - before :each do - Puppet[:parser] = 'future' - end - - it 'rejects on an array (no berries)' do - catalog = compile_to_catalog(<<-MANIFEST) - $a = ['strawberry','blueberry','orange'] - $a.reject {|$x| $x =~ /berry$/}.foreach {|$v| - file { "/file_$v": ensure => present } - } - MANIFEST - - catalog.resource(:file, "/file_orange")['ensure'].should == 'present' - catalog.resource(:file, "/file_strawberry").should == nil - end - - it 'produces an array when acting on an array' do - catalog = compile_to_catalog(<<-MANIFEST) - $a = ['strawberry','blueberry','orange'] - $b = $a.reject {|$x| $x =~ /berry$/} - file { "/file_${b[0]}": ensure => present } - - MANIFEST - - catalog.resource(:file, "/file_orange")['ensure'].should == 'present' - catalog.resource(:file, "/file_strawberry").should == nil - end - - it 'rejects on a hash (all berries) by key' do - catalog = compile_to_catalog(<<-MANIFEST) - $a = {'strawberry'=>'red','blueberry'=>'blue','orange'=>'orange'} - $a.reject {|$x| $x[0] =~ /berry$/}.foreach {|$v| - file { "/file_${v[0]}": ensure => present } - } - MANIFEST - - catalog.resource(:file, "/file_orange")['ensure'].should == 'present' - end - - it 'produces a hash when acting on a hash' do - catalog = compile_to_catalog(<<-MANIFEST) - $a = {'strawberry'=>'red','blueberry'=>'blue','grape'=>'purple'} - $b = $a.reject {|$x| $x[0] =~ /berry$/} - file { "/file_${b[grape]}": ensure => present } - - MANIFEST - - catalog.resource(:file, "/file_purple")['ensure'].should == 'present' - end - - it 'rejects on a hash (all berries) by value' do - catalog = compile_to_catalog(<<-MANIFEST) - $a = {'strawb'=>'red berry','blueb'=>'blue berry','orange'=>'orange fruit'} - $a.reject {|$x| $x[1] =~ /berry$/}.foreach {|$v| - file { "/file_${v[0]}": ensure => present } - } - MANIFEST - - catalog.resource(:file, "/file_orange")['ensure'].should == 'present' - end - - it_should_behave_like 'all iterative functions argument checks', 'reject' - it_should_behave_like 'all iterative functions hash handling', 'reject' -end diff --git a/spec/unit/parser/methods/shared.rb b/spec/unit/parser/methods/shared.rb index bf9c9f05f..704769d83 100644 --- a/spec/unit/parser/methods/shared.rb +++ b/spec/unit/parser/methods/shared.rb @@ -2,7 +2,7 @@ shared_examples_for 'all iterative functions hash handling' do |func| it 'passes a hash entry as an array of the key and value' do catalog = compile_to_catalog(<<-MANIFEST) - {a=>1}.#{func} { |$v| notify { "${v[0]} ${v[1]}": } } + {a=>1}.#{func} |$v| { notify { "${v[0]} ${v[1]}": } } MANIFEST catalog.resource(:notify, "a 1").should_not be_nil @@ -14,7 +14,7 @@ shared_examples_for 'all iterative functions argument checks' do |func| it 'raises an error when defined with more than 1 argument' do expect do compile_to_catalog(<<-MANIFEST) - [1].#{func} { |$x, $yikes| } + [1].#{func} |$x, $yikes|{ } MANIFEST end.to raise_error(Puppet::Error, /Too few arguments/) end @@ -22,7 +22,7 @@ shared_examples_for 'all iterative functions argument checks' do |func| it 'raises an error when defined with fewer than 1 argument' do expect do compile_to_catalog(<<-MANIFEST) - [1].#{func} { || } + [1].#{func} || { } MANIFEST end.to raise_error(Puppet::Error, /Too many arguments/) end @@ -30,7 +30,7 @@ shared_examples_for 'all iterative functions argument checks' do |func| it 'raises an error when used against an unsupported type' do expect do compile_to_catalog(<<-MANIFEST) - "not correct".#{func} { |$v| } + "not correct".#{func} |$v| { } MANIFEST end.to raise_error(Puppet::Error, /must be an Array or a Hash/) end @@ -38,7 +38,7 @@ shared_examples_for 'all iterative functions argument checks' do |func| it 'raises an error when called with any parameters besides a block' do expect do compile_to_catalog(<<-MANIFEST) - [1].#{func}(1) { |$v| } + [1].#{func}(1) |$v| { } MANIFEST end.to raise_error(Puppet::Error, /Wrong number of arguments/) end diff --git a/spec/unit/parser/methods/slice_spec.rb b/spec/unit/parser/methods/slice_spec.rb index b213415a1..1069bc75a 100644 --- a/spec/unit/parser/methods/slice_spec.rb +++ b/spec/unit/parser/methods/slice_spec.rb @@ -27,15 +27,15 @@ describe 'methods' do end context "should be callable on array as" do - + it 'slice with explicit parameters' do catalog = compile_to_catalog(<<-MANIFEST) $a = [1, present, 2, absent, 3, present] - $a.slice(2) |$k,$v| { + $a.slice(2) |$k,$v| { file { "/file_${$k}": ensure => $v } } MANIFEST - + catalog.resource(:file, "/file_1")['ensure'].should == 'present' catalog.resource(:file, "/file_2")['ensure'].should == 'absent' catalog.resource(:file, "/file_3")['ensure'].should == 'present' @@ -43,11 +43,11 @@ describe 'methods' do it 'slice with one parameter' do catalog = compile_to_catalog(<<-MANIFEST) $a = [1, present, 2, absent, 3, present] - $a.slice(2) |$k| { + $a.slice(2) |$k| { file { "/file_${$k[0]}": ensure => $k[1] } } MANIFEST - + catalog.resource(:file, "/file_1")['ensure'].should == 'present' catalog.resource(:file, "/file_2")['ensure'].should == 'absent' catalog.resource(:file, "/file_3")['ensure'].should == 'present' @@ -55,39 +55,39 @@ describe 'methods' do it 'slice with shorter last slice' do catalog = compile_to_catalog(<<-MANIFEST) $a = [1, present, 2, present, 3, absent] - $a.slice(4) |$a, $b, $c, $d| { + $a.slice(4) |$a, $b, $c, $d| { file { "/file_$a.$c": ensure => $b } } MANIFEST - + catalog.resource(:file, "/file_1.2")['ensure'].should == 'present' catalog.resource(:file, "/file_3.")['ensure'].should == 'absent' end end context "should be callable on hash as" do - + it 'slice with explicit parameters, missing are empty' do catalog = compile_to_catalog(<<-MANIFEST) $a = {1=>present, 2=>present, 3=>absent} - $a.slice(2) |$a,$b| { + $a.slice(2) |$a,$b| { file { "/file_${a[0]}.${b[0]}": ensure => $a[1] } } MANIFEST - + catalog.resource(:file, "/file_1.2")['ensure'].should == 'present' catalog.resource(:file, "/file_3.")['ensure'].should == 'absent' end - + end context "when called without a block" do it "should produce an array with the result" do catalog = compile_to_catalog(<<-MANIFEST) $a = [1, present, 2, absent, 3, present] - $a.slice(2).each |$k| { + $a.slice(2).each |$k| { file { "/file_${$k[0]}": ensure => $k[1] } } MANIFEST - + catalog.resource(:file, "/file_1")['ensure'].should == 'present' catalog.resource(:file, "/file_2")['ensure'].should == 'absent' catalog.resource(:file, "/file_3")['ensure'].should == 'present' diff --git a/spec/unit/parser/parser_spec.rb b/spec/unit/parser/parser_spec.rb index 38a5c7a85..2c4cf50df 100755 --- a/spec/unit/parser/parser_spec.rb +++ b/spec/unit/parser/parser_spec.rb @@ -53,7 +53,7 @@ describe Puppet::Parser do describe "when parsing files" do before do - FileTest.stubs(:exist?).returns true + Puppet::FileSystem::File.stubs(:exist?).returns true File.stubs(:read).returns "" @parser.stubs(:watch_file) end diff --git a/spec/unit/parser/resource/param_spec.rb b/spec/unit/parser/resource/param_spec.rb new file mode 100755 index 000000000..7989d060d --- /dev/null +++ b/spec/unit/parser/resource/param_spec.rb @@ -0,0 +1,44 @@ +#! /usr/bin/env ruby +require 'spec_helper' + +describe Puppet::Parser::Resource::Param do + it "can be instantiated" do + Puppet::Parser::Resource::Param.new(:name => 'myparam', :value => 'foo') + end + + it "stores the source file" do + param = Puppet::Parser::Resource::Param.new(:name => 'myparam', :value => 'foo', :file => 'foo.pp') + param.file.should == 'foo.pp' + end + + it "stores the line number" do + param = Puppet::Parser::Resource::Param.new(:name => 'myparam', :value => 'foo', :line => 42) + param.line.should == 42 + end + + context "parameter validation" do + it "throws an error when instantiated without a name" do + expect { + Puppet::Parser::Resource::Param.new(:value => 'foo') + }.to raise_error(Puppet::Error, /name is a required option/) + end + + it "throws an error when instantiated without a value" do + expect { + Puppet::Parser::Resource::Param.new(:name => 'myparam') + }.to raise_error(Puppet::Error, /value is a required option/) + end + + it "throws an error when instantiated with a nil value" do + expect { + Puppet::Parser::Resource::Param.new(:name => 'myparam', :value => nil) + }.to raise_error(Puppet::Error, /value is a required option/) + end + + it "includes file/line context in errors" do + expect { + Puppet::Parser::Resource::Param.new(:name => 'myparam', :value => nil, :file => 'foo.pp', :line => 42) + }.to raise_error(Puppet::Error, /foo.pp:42/) + end + end +end diff --git a/spec/unit/parser/resource_spec.rb b/spec/unit/parser/resource_spec.rb index 221efb83b..74a66d1c1 100755 --- a/spec/unit/parser/resource_spec.rb +++ b/spec/unit/parser/resource_spec.rb @@ -90,7 +90,7 @@ describe Puppet::Parser::Resource do it "should use a Puppet::Resource for converting to a ral resource" do trans = mock 'resource', :to_ral => "yay" @resource = mkresource - @resource.expects(:to_resource).returns trans + @resource.expects(:copy_as_resource).returns trans @resource.to_ral.should == "yay" end @@ -124,7 +124,8 @@ describe Puppet::Parser::Resource do tags = [ "tag1", "tag2" ] @arguments[:parameters] = [ param(:tag, tags , :source) ] res = Puppet::Parser::Resource.new("resource", "testing", @arguments) - (res.tags & tags).should == tags + res.should be_tagged("tag1") + res.should be_tagged("tag2") end end @@ -445,7 +446,7 @@ describe Puppet::Parser::Resource do it "should be able to be converted to a normal resource" do @source = stub 'scope', :name => "myscope" @resource = mkresource :source => @source - @resource.should respond_to(:to_resource) + @resource.should respond_to(:copy_as_resource) end describe "when being converted to a resource" do @@ -454,19 +455,19 @@ describe Puppet::Parser::Resource do end it "should create an instance of Puppet::Resource" do - @parser_resource.to_resource.should be_instance_of(Puppet::Resource) + @parser_resource.copy_as_resource.should be_instance_of(Puppet::Resource) end it "should set the type correctly on the Puppet::Resource" do - @parser_resource.to_resource.type.should == @parser_resource.type + @parser_resource.copy_as_resource.type.should == @parser_resource.type end it "should set the title correctly on the Puppet::Resource" do - @parser_resource.to_resource.title.should == @parser_resource.title + @parser_resource.copy_as_resource.title.should == @parser_resource.title end it "should copy over all of the parameters" do - result = @parser_resource.to_resource.to_hash + result = @parser_resource.copy_as_resource.to_hash # The name will be in here, also. result[:foo].should == "bar" @@ -477,40 +478,40 @@ describe Puppet::Parser::Resource do @parser_resource.tag "foo" @parser_resource.tag "bar" - @parser_resource.to_resource.tags.should == @parser_resource.tags + @parser_resource.copy_as_resource.tags.should == @parser_resource.tags end it "should copy over the line" do @parser_resource.line = 40 - @parser_resource.to_resource.line.should == 40 + @parser_resource.copy_as_resource.line.should == 40 end it "should copy over the file" do @parser_resource.file = "/my/file" - @parser_resource.to_resource.file.should == "/my/file" + @parser_resource.copy_as_resource.file.should == "/my/file" end it "should copy over the 'exported' value" do @parser_resource.exported = true - @parser_resource.to_resource.exported.should be_true + @parser_resource.copy_as_resource.exported.should be_true end it "should copy over the 'virtual' value" do @parser_resource.virtual = true - @parser_resource.to_resource.virtual.should be_true + @parser_resource.copy_as_resource.virtual.should be_true end it "should convert any parser resource references to Puppet::Resource instances" do ref = Puppet::Resource.new("file", "/my/file") @parser_resource = mkresource :source => @source, :parameters => {:foo => "bar", :fee => ref} - result = @parser_resource.to_resource + result = @parser_resource.copy_as_resource result[:fee].should == Puppet::Resource.new(:file, "/my/file") end it "should convert any parser resource references to Puppet::Resource instances even if they are in an array" do ref = Puppet::Resource.new("file", "/my/file") @parser_resource = mkresource :source => @source, :parameters => {:foo => "bar", :fee => ["a", ref]} - result = @parser_resource.to_resource + result = @parser_resource.copy_as_resource result[:fee].should == ["a", Puppet::Resource.new(:file, "/my/file")] end @@ -518,7 +519,7 @@ describe Puppet::Parser::Resource do ref1 = Puppet::Resource.new("file", "/my/file1") ref2 = Puppet::Resource.new("file", "/my/file2") @parser_resource = mkresource :source => @source, :parameters => {:foo => "bar", :fee => ["a", [ref1,ref2]]} - result = @parser_resource.to_resource + result = @parser_resource.copy_as_resource result[:fee].should == ["a", Puppet::Resource.new(:file, "/my/file1"), Puppet::Resource.new(:file, "/my/file2")] end diff --git a/spec/unit/pops/model/ast_transformer_spec.rb b/spec/unit/pops/model/ast_transformer_spec.rb index f2b1c9b56..969e944de 100644 --- a/spec/unit/pops/model/ast_transformer_spec.rb +++ b/spec/unit/pops/model/ast_transformer_spec.rb @@ -13,28 +13,42 @@ describe Puppet::Pops::Model::AstTransformer do it "converts a decimal number to a string Name" do ast = transform(QNAME_OR_NUMBER("10")) - ast.should be_kind_of Puppet::Parser::AST::Name + ast.should be_kind_of(Puppet::Parser::AST::Name) ast.value.should == "10" end + it "converts a 0 to a decimal 0" do + ast = transform(QNAME_OR_NUMBER("0")) + + ast.should be_kind_of(Puppet::Parser::AST::Name) + ast.value.should == "0" + end + + it "converts a 00 to an octal 00" do + ast = transform(QNAME_OR_NUMBER("0")) + + ast.should be_kind_of(Puppet::Parser::AST::Name) + ast.value.should == "0" + end + it "converts an octal number to a string Name" do ast = transform(QNAME_OR_NUMBER("020")) - ast.should be_kind_of Puppet::Parser::AST::Name + ast.should be_kind_of(Puppet::Parser::AST::Name) ast.value.should == "020" end it "converts a hex number to a string Name" do ast = transform(QNAME_OR_NUMBER("0x20")) - ast.should be_kind_of Puppet::Parser::AST::Name + ast.should be_kind_of(Puppet::Parser::AST::Name) ast.value.should == "0x20" end it "converts an unknown radix to an error string" do ast = transform(Puppet::Pops::Model::Factory.new(Puppet::Pops::Model::LiteralNumber, 3, 2)) - ast.should be_kind_of Puppet::Parser::AST::Name + ast.should be_kind_of(Puppet::Parser::AST::Name) ast.value.should == "bad radix:3" end end diff --git a/spec/unit/pops/parser/lexer_spec.rb b/spec/unit/pops/parser/lexer_spec.rb index 985807261..71010c033 100755 --- a/spec/unit/pops/parser/lexer_spec.rb +++ b/spec/unit/pops/parser/lexer_spec.rb @@ -154,7 +154,7 @@ describe Puppet::Pops::Parser::Lexer::TOKENS do :LBRACK => '[', :RBRACK => ']', # :LBRACE => '{', - :RBRACE => '}', +# :RBRACE => '}', :LPAREN => '(', :RPAREN => ')', :EQUALS => '=', @@ -232,7 +232,7 @@ describe Puppet::Pops::Parser::Lexer::TOKENS do # These tokens' strings don't matter, just that the tokens exist. [:STRING, :DQPRE, :DQMID, :DQPOST, :BOOLEAN, :NAME, :NUMBER, :COMMENT, :MLCOMMENT, - :LBRACE, :LAMBDA, + :LBRACE, :RBRACE, :RETURN, :SQUOTE, :DQUOTE, :VARIABLE].each do |name| it "should have a token named #{name.to_s}" do Puppet::Pops::Parser::Lexer::TOKENS[name].should_not be_nil @@ -583,7 +583,24 @@ describe Puppet::Pops::Parser::Lexer,"when lexing strings" do %q[""] => [[:STRING,""]], %q["123 456 789 0"] => [[:STRING,"123 456 789 0"]], %q["${123} 456 $0"] => [[:DQPRE,""],[:VARIABLE,"123"],[:DQMID," 456 "],[:VARIABLE,"0"],[:DQPOST,""]], - %q["$foo::::bar"] => [[:DQPRE,""],[:VARIABLE,"foo"],[:DQPOST,"::::bar"]] + %q["$foo::::bar"] => [[:DQPRE,""],[:VARIABLE,"foo"],[:DQPOST,"::::bar"]], + # Keyword variables + %q["$true"] => [[:DQPRE,""],[:VARIABLE, "true"],[:DQPOST,""]], + %q["$false"] => [[:DQPRE,""],[:VARIABLE, "false"],[:DQPOST,""]], + %q["$if"] => [[:DQPRE,""],[:VARIABLE, "if"],[:DQPOST,""]], + %q["$case"] => [[:DQPRE,""],[:VARIABLE, "case"],[:DQPOST,""]], + %q["$unless"] => [[:DQPRE,""],[:VARIABLE, "unless"],[:DQPOST,""]], + %q["$undef"] => [[:DQPRE,""],[:VARIABLE, "undef"],[:DQPOST,""]], + # Expressions + %q["${true}"] => [[:DQPRE,""],[:BOOLEAN, true],[:DQPOST,""]], + %q["${false}"] => [[:DQPRE,""],[:BOOLEAN, false],[:DQPOST,""]], + %q["${undef}"] => [[:DQPRE,""],:UNDEF,[:DQPOST,""]], + %q["${if true {false}}"] => [[:DQPRE,""],:IF,[:BOOLEAN, true], :LBRACE, [:BOOLEAN, false], :RBRACE, [:DQPOST,""]], + %q["${unless true {false}}"] => [[:DQPRE,""],:UNLESS,[:BOOLEAN, true], :LBRACE, [:BOOLEAN, false], :RBRACE, [:DQPOST,""]], + %q["${case true {true:{false}}}"] => [ + [:DQPRE,""],:CASE,[:BOOLEAN, true], :LBRACE, [:BOOLEAN, true], :COLON, :LBRACE, [:BOOLEAN, false], + :RBRACE, :RBRACE, [:DQPOST,""]], + %q[{ "${a}" => 1 }] => [ :LBRACE, [:DQPRE,""], [:VARIABLE,"a"], [:DQPOST,""], :FARROW, [:NAME,"1"], :RBRACE ], }.each { |src,expected_result| it "should handle #{src} correctly" do EgrammarLexerSpec.tokens_scanned_from(src).should be_like(*expected_result) @@ -742,7 +759,7 @@ describe "Puppet::Pops::Parser::Lexer in the old tests" do it "should end variables at `-`" do EgrammarLexerSpec.tokens_scanned_from('$hyphenated-variable'). - should be_like [:VARIABLE, "hyphenated"], [:MINUS, '-'], [:NAME, 'variable'] + should be_like([:VARIABLE, "hyphenated"], [:MINUS, '-'], [:NAME, 'variable']) end it "should not include whitespace in a variable" do @@ -769,7 +786,7 @@ describe "when trying to lex a non-existent file" do it "should return an empty list of tokens" do lexer = Puppet::Pops::Parser::Lexer.new lexer.file = nofile = tmpfile('lexer') - File.exists?(nofile).should == false + Puppet::FileSystem::File.exist?(nofile).should == false lexer.fullscan.should == [[false,false]] end diff --git a/spec/unit/pops/parser/parse_calls_spec.rb b/spec/unit/pops/parser/parse_calls_spec.rb index 647d0e16c..800a15bec 100644 --- a/spec/unit/pops/parser/parse_calls_spec.rb +++ b/spec/unit/pops/parser/parse_calls_spec.rb @@ -81,16 +81,16 @@ describe "egrammar parsing function calls" do dump(parse("$a.foo")).should == "(call-method (. $a foo))" end - it "$a.foo {|| }" do + it "$a.foo || { }" do dump(parse("$a.foo || { }")).should == "(call-method (. $a foo) (lambda ()))" end - it "$a.foo {|$x| }" do - dump(parse("$a.foo {|$x| }")).should == "(call-method (. $a foo) (lambda (parameters x) ()))" + it "$a.foo |$x| { }" do + dump(parse("$a.foo |$x|{ }")).should == "(call-method (. $a foo) (lambda (parameters x) ()))" end - it "$a.foo {|$x| }" do - dump(parse("$a.foo {|$x| $b = $x}")).should == + it "$a.foo |$x|{ }" do + dump(parse("$a.foo |$x|{ $b = $x}")).should == "(call-method (. $a foo) (lambda (parameters x) (block (= $b $x))))" end end diff --git a/spec/unit/pops/transformer/transform_calls_spec.rb b/spec/unit/pops/transformer/transform_calls_spec.rb index f2303db5b..969a2d7b8 100644 --- a/spec/unit/pops/transformer/transform_calls_spec.rb +++ b/spec/unit/pops/transformer/transform_calls_spec.rb @@ -60,20 +60,20 @@ describe "transformation to Puppet AST for function calls" do astdump(parse("$a.foo")).should == "(call-method (. $a foo))" end - it "$a.foo {|| }" do + it "$a.foo ||{ }" do astdump(parse("$a.foo || { }")).should == "(call-method (. $a foo) (lambda ()))" end - it "$a.foo {|| []} # check transformation to block with empty array" do - astdump(parse("$a.foo || { []}")).should == "(call-method (. $a foo) (lambda (block ([]))))" + it "$a.foo ||{[]} # check transformation to block with empty array" do + astdump(parse("$a.foo || {[]}")).should == "(call-method (. $a foo) (lambda (block ([]))))" end it "$a.foo {|$x| }" do - astdump(parse("$a.foo {|$x| }")).should == "(call-method (. $a foo) (lambda (parameters x) ()))" + astdump(parse("$a.foo |$x| { }")).should == "(call-method (. $a foo) (lambda (parameters x) ()))" end - it "$a.foo {|$x| $b = $x}" do - astdump(parse("$a.foo {|$x| $b = $x}")).should == + it "$a.foo |$x| { $b = $x}" do + astdump(parse("$a.foo |$x| { $b = $x}")).should == "(call-method (. $a foo) (lambda (parameters x) (block (= $b $x))))" end end diff --git a/spec/unit/pops/transformer/transform_containers_spec.rb b/spec/unit/pops/transformer/transform_containers_spec.rb index 682860fe1..8c65e2bcc 100644 --- a/spec/unit/pops/transformer/transform_containers_spec.rb +++ b/spec/unit/pops/transformer/transform_containers_spec.rb @@ -154,12 +154,12 @@ describe "transformation to Puppet AST for containers" do it "node foo inherits 'bar' {}" do # AST can not differentiate between bare word and string - astdump(parse("node foo inherits 'bar' {}")).should == "(node (matches 'foo') (parent 'bar') ())" + astdump(parse("node foo inherits 'bar' {}")).should == "(node (matches 'foo') (parent bar) ())" end it "node foo inherits default {}" do # AST can not differentiate between bare word and string - astdump(parse("node foo inherits default {}")).should == "(node (matches 'foo') (parent :default) ())" + astdump(parse("node foo inherits default {}")).should == "(node (matches 'foo') (parent default) ())" end it "node /web.*/ {}" do diff --git a/spec/unit/pops/validator/validator_spec.rb b/spec/unit/pops/validator/validator_spec.rb new file mode 100644 index 000000000..d54d2ca61 --- /dev/null +++ b/spec/unit/pops/validator/validator_spec.rb @@ -0,0 +1,31 @@ +#! /usr/bin/env ruby +require 'spec_helper' +require 'puppet/pops' +require 'puppet_spec/pops' + +# relative to this spec file (./) does not work as this file is loaded by rspec +require File.join(File.dirname(__FILE__), '../parser/parser_rspec_helper') + +describe "validating 3x" do + include ParserRspecHelper + include PuppetSpec::Pops + + let(:acceptor) { Puppet::Pops::Validation::Acceptor.new() } + let(:validator) { Puppet::Pops::Validation::ValidatorFactory_3_1.new().validator(acceptor) } + + def validate(model) + validator.validate(model) + acceptor + end + + it 'should raise error for illegal names' do + expect(validate(fqn('Aaa'))).to have_issue(Puppet::Pops::Issues::ILLEGAL_NAME) + expect(validate(fqn('AAA'))).to have_issue(Puppet::Pops::Issues::ILLEGAL_NAME) + end + + it 'should raise error for illegal variable names' do + expect(validate(fqn('Aaa').var())).to have_issue(Puppet::Pops::Issues::ILLEGAL_NAME) + expect(validate(fqn('AAA').var())).to have_issue(Puppet::Pops::Issues::ILLEGAL_NAME) + end + +end
\ No newline at end of file diff --git a/spec/unit/provider/augeas/augeas_spec.rb b/spec/unit/provider/augeas/augeas_spec.rb index e75a0d36e..9b6e88ce8 100755 --- a/spec/unit/provider/augeas/augeas_spec.rb +++ b/spec/unit/provider/augeas/augeas_spec.rb @@ -276,6 +276,61 @@ describe provider_class do @provider.need_to_run?.should == true end + describe "performing numeric comparisons (#22617)" do + it "should return true when a get string compare is true" do + @resource[:onlyif] = "get bpath > a" + @augeas.stubs("get").returns("b") + @provider.need_to_run?.should == true + end + + it "should return false when a get string compare is false" do + @resource[:onlyif] = "get a19path > a2" + @augeas.stubs("get").returns("a19") + @provider.need_to_run?.should == false + end + + it "should return true when a get int gt compare is true" do + @resource[:onlyif] = "get path19 > 2" + @augeas.stubs("get").returns("19") + @provider.need_to_run?.should == true + end + + it "should return true when a get int ge compare is true" do + @resource[:onlyif] = "get path19 >= 2" + @augeas.stubs("get").returns("19") + @provider.need_to_run?.should == true + end + + it "should return true when a get int lt compare is true" do + @resource[:onlyif] = "get path2 < 19" + @augeas.stubs("get").returns("2") + @provider.need_to_run?.should == true + end + + it "should return false when a get int le compare is false" do + @resource[:onlyif] = "get path39 <= 4" + @augeas.stubs("get").returns("39") + @provider.need_to_run?.should == false + end + end + describe "performing is_numeric checks (#22617)" do + it "should return false for nil" do + @provider.is_numeric?(nil).should == false + end + it "should return true for Fixnums" do + @provider.is_numeric?(9).should == true + end + it "should return true for numbers in Strings" do + @provider.is_numeric?('9').should == true + end + it "should return false for non-number Strings" do + @provider.is_numeric?('x9').should == false + end + it "should return false for other types" do + @provider.is_numeric?([true]).should == false + end + end + it "should return false when a get filter does not match" do @resource[:onlyif] = "get path == another value" @augeas.stubs("get").returns("value") @@ -619,7 +674,7 @@ describe provider_class do link = tmpfile('link') target = tmpfile('target') FileUtils.touch(target) - FileUtils.symlink(target, link) + Puppet::FileSystem::File.new(target).symlink(link) resource = Puppet::Type.type(:augeas).new( :name => 'test', @@ -634,7 +689,7 @@ describe provider_class do catalog.apply File.ftype(link).should == 'link' - File.readlink(link).should == target + Puppet::FileSystem::File.new(link).readlink().should == target File.read(target).should =~ /PermitRootLogin no/ end end diff --git a/spec/unit/provider/exec/posix_spec.rb b/spec/unit/provider/exec/posix_spec.rb index 39cc10b41..7c4982fcc 100755 --- a/spec/unit/provider/exec/posix_spec.rb +++ b/spec/unit/provider/exec/posix_spec.rb @@ -64,7 +64,7 @@ describe Puppet::Type.type(:exec).provider(:posix) do provider.resource[:path] = [File.dirname(command)] filename = File.basename(command) - Puppet::Util::Execution.expects(:execute).with { |cmdline, arguments| (cmdline == filename) && (arguments.is_a? Hash) } + Puppet::Util::Execution.expects(:execute).with { |cmdline, arguments| (cmdline == filename) && (arguments.is_a? Hash) }.returns(Puppet::Util::Execution::ProcessOutput.new('', 0)) provider.run(filename) end @@ -95,7 +95,7 @@ describe Puppet::Type.type(:exec).provider(:posix) do provider.resource[:path] = ['/bogus/bin'] command = make_exe - Puppet::Util::Execution.expects(:execute).with { |cmdline, arguments| (cmdline == "#{command} bar --sillyarg=true --blah") && (arguments.is_a? Hash) } + Puppet::Util::Execution.expects(:execute).with { |cmdline, arguments| (cmdline == "#{command} bar --sillyarg=true --blah") && (arguments.is_a? Hash) }.returns(Puppet::Util::Execution::ProcessOutput.new('', 0)) provider.run("#{command} bar --sillyarg=true --blah") end @@ -110,11 +110,16 @@ describe Puppet::Type.type(:exec).provider(:posix) do provider.resource[:environment] = ['WHATEVER=/something/else', 'WHATEVER=/foo'] command = make_exe - Puppet::Util::Execution.expects(:execute).with { |cmdline, arguments| (cmdline == command) && (arguments.is_a? Hash) } + Puppet::Util::Execution.expects(:execute).with { |cmdline, arguments| (cmdline == command) && (arguments.is_a? Hash) }.returns(Puppet::Util::Execution::ProcessOutput.new('', 0)) provider.run(command) @logs.map {|l| "#{l.level}: #{l.message}" }.should == ["warning: Overriding environment setting 'WHATEVER' with '/foo'"] end + it "should set umask before execution if umask parameter is in use" do + provider.resource[:umask] = '0027' + Puppet::Util.expects(:withumask).with(0027) + provider.run(provider.resource[:command]) + end describe "posix locale settings", :unless => Puppet.features.microsoft_windows? do # a sentinel value that we can use to emulate what locale environment variables might be set to on an international diff --git a/spec/unit/provider/file/posix_spec.rb b/spec/unit/provider/file/posix_spec.rb index e84d23a5a..48eaae30a 100755 --- a/spec/unit/provider/file/posix_spec.rb +++ b/spec/unit/provider/file/posix_spec.rb @@ -85,7 +85,7 @@ describe Puppet::Type.type(:file).provider(:posix), :if => Puppet.features.posix describe "#owner" do it "should return the uid of the file owner" do FileUtils.touch(path) - owner = File.stat(path).uid + owner = Puppet::FileSystem::File.new(path).stat.uid provider.owner.should == owner end @@ -178,7 +178,7 @@ describe Puppet::Type.type(:file).provider(:posix), :if => Puppet.features.posix describe "#group" do it "should return the gid of the file group" do FileUtils.touch(path) - group = File.stat(path).gid + group = Puppet::FileSystem::File.new(path).stat.gid provider.group.should == group end diff --git a/spec/unit/provider/group/windows_adsi_spec.rb b/spec/unit/provider/group/windows_adsi_spec.rb index 21b967509..d28601172 100644 --- a/spec/unit/provider/group/windows_adsi_spec.rb +++ b/spec/unit/provider/group/windows_adsi_spec.rb @@ -24,12 +24,64 @@ describe Puppet::Type.type(:group).provider(:windows_adsi) do names = ['group1', 'group2', 'group3'] stub_groups = names.map{|n| stub(:name => n)} - connection.stubs(:execquery).with("select name from win32_group").returns stub_groups + connection.stubs(:execquery).with('select name from win32_group where localaccount = "TRUE"').returns stub_groups described_class.instances.map(&:name).should =~ names end end + describe "group type :members property helpers", :if => Puppet.features.microsoft_windows? do + + let(:user1) { stub(:account => 'user1', :domain => '.', :to_s => 'user1sid') } + let(:user2) { stub(:account => 'user2', :domain => '.', :to_s => 'user2sid') } + + before :each do + Puppet::Util::Windows::Security.stubs(:name_to_sid_object).with('user1').returns(user1) + Puppet::Util::Windows::Security.stubs(:name_to_sid_object).with('user2').returns(user2) + end + + describe "#members_insync?" do + it "should return false when current is nil" do + provider.members_insync?(nil, ['user2']).should be_false + end + it "should return false when should is nil" do + provider.members_insync?(['user1'], nil).should be_false + end + it "should return false for differing lists of members" do + provider.members_insync?(['user1'], ['user2']).should be_false + provider.members_insync?(['user1'], []).should be_false + provider.members_insync?([], ['user2']).should be_false + end + it "should return true for same lists of members" do + provider.members_insync?(['user1', 'user2'], ['user1', 'user2']).should be_true + end + it "should return true for same lists of unordered members" do + provider.members_insync?(['user1', 'user2'], ['user2', 'user1']).should be_true + end + it "should return true for same lists of members irrespective of duplicates" do + provider.members_insync?(['user1', 'user2', 'user2'], ['user2', 'user1', 'user1']).should be_true + end + end + + describe "#members_to_s" do + it "should return an empty string on non-array input" do + [Object.new, {}, 1, :symbol, ''].each do |input| + provider.members_to_s(input).should be_empty + end + end + it "should return an empty string on empty or nil users" do + provider.members_to_s([]).should be_empty + provider.members_to_s(nil).should be_empty + end + it "should return a user string like DOMAIN\\USER" do + provider.members_to_s(['user1']).should == '.\user1' + end + it "should return a user string like DOMAIN\\USER,DOMAIN2\\USER2" do + provider.members_to_s(['user1', 'user2']).should == '.\user1,.\user2' + end + end + end + describe "when managing members" do it "should be able to provide a list of members" do provider.group.stubs(:members).returns ['user1', 'user2', 'user3'] @@ -37,11 +89,22 @@ describe Puppet::Type.type(:group).provider(:windows_adsi) do provider.members.should =~ ['user1', 'user2', 'user3'] end - it "should be able to set group members" do + it "should be able to set group members", :if => Puppet.features.microsoft_windows? do provider.group.stubs(:members).returns ['user1', 'user2'] - provider.group.expects(:remove_members).with('user1') - provider.group.expects(:add_members).with('user3') + member_sids = [ + stub(:account => 'user1', :domain => 'testcomputername'), + stub(:account => 'user2', :domain => 'testcomputername'), + stub(:account => 'user3', :domain => 'testcomputername'), + ] + + provider.group.stubs(:member_sids).returns(member_sids[0..1]) + + Puppet::Util::Windows::Security.expects(:name_to_sid_object).with('user2').returns(member_sids[1]) + Puppet::Util::Windows::Security.expects(:name_to_sid_object).with('user3').returns(member_sids[2]) + + provider.group.expects(:remove_member_sids).with(member_sids[0]) + provider.group.expects(:add_member_sids).with(member_sids[2]) provider.members = ['user2', 'user3'] end @@ -97,4 +160,8 @@ describe Puppet::Type.type(:group).provider(:windows_adsi) do provider.expects(:fail).with { |msg| msg =~ /gid is read-only/ } provider.send(:gid=, 500) end + + it "should prefer the domain component from the resolved SID", :if => Puppet.features.microsoft_windows? do + provider.members_to_s(['.\Administrators']).should == 'BUILTIN\Administrators' + end end diff --git a/spec/unit/provider/nameservice/directoryservice_spec.rb b/spec/unit/provider/nameservice/directoryservice_spec.rb index e7fdcbef5..fbb74d0f3 100755 --- a/spec/unit/provider/nameservice/directoryservice_spec.rb +++ b/spec/unit/provider/nameservice/directoryservice_spec.rb @@ -110,7 +110,7 @@ describe 'DirectoryService password behavior' do it 'should execute convert_binary_to_xml once when getting the password on >= 10.7' do subject.expects(:convert_binary_to_xml).returns({'SALTED-SHA512' => StringIO.new(pw_string)}) - File.expects(:exists?).with(plist_path).once.returns(true) + Puppet::FileSystem::File.expects(:exist?).with(plist_path).once.returns(true) Plist.expects(:parse_xml).returns(shadow_hash_data) # On Mac OS X 10.7 we first need to convert to xml when reading the password subject.expects(:plutil).with('-convert', 'xml1', '-o', '/dev/stdout', plist_path) @@ -126,7 +126,7 @@ describe 'DirectoryService password behavior' do it 'should convert xml-to-binary and binary-to-xml when setting the pw on >= 10.7' do subject.expects(:convert_binary_to_xml).returns({'SALTED-SHA512' => StringIO.new(pw_string)}) subject.expects(:convert_xml_to_binary).returns(binary_plist) - File.expects(:exists?).with(plist_path).once.returns(true) + Puppet::FileSystem::File.expects(:exist?).with(plist_path).once.returns(true) Plist.expects(:parse_xml).returns(shadow_hash_data) # On Mac OS X 10.7 we first need to convert to xml subject.expects(:plutil).with('-convert', 'xml1', '-o', '/dev/stdout', plist_path) @@ -138,7 +138,7 @@ describe 'DirectoryService password behavior' do it '[#13686] should handle an empty ShadowHashData field in the users plist' do subject.expects(:convert_xml_to_binary).returns(binary_plist) - File.expects(:exists?).with(plist_path).once.returns(true) + Puppet::FileSystem::File.expects(:exist?).with(plist_path).once.returns(true) Plist.expects(:parse_xml).returns({'ShadowHashData' => nil}) subject.expects(:plutil).with('-convert', 'xml1', '-o', '/dev/stdout', plist_path) subject.expects(:plutil).with('-convert', 'binary1', plist_path) diff --git a/spec/unit/provider/package/apt_spec.rb b/spec/unit/provider/package/apt_spec.rb index 5e75a569f..5d7b3e4e6 100755 --- a/spec/unit/provider/package/apt_spec.rb +++ b/spec/unit/provider/package/apt_spec.rb @@ -63,7 +63,7 @@ Version table: it "should preseed with the provided responsefile when preseeding is called for" do @resource.expects(:[]).with(:responsefile).returns "/my/file" - FileTest.expects(:exist?).with("/my/file").returns true + Puppet::FileSystem::File.expects(:exist?).with("/my/file").returns true @provider.expects(:info) @provider.expects(:preseed).with("/my/file") diff --git a/spec/unit/provider/package/aptrpm_spec.rb b/spec/unit/provider/package/aptrpm_spec.rb index 6c77bee3a..83d343242 100755 --- a/spec/unit/provider/package/aptrpm_spec.rb +++ b/spec/unit/provider/package/aptrpm_spec.rb @@ -19,7 +19,7 @@ describe Puppet::Type.type(:package).provider(:aptrpm) do def rpm pkg.provider.expects(:rpm). with('-q', 'faff', '--nosignature', '--nodigest', '--qf', - "'%{NAME} %|EPOCH?{%{EPOCH}}:{0}| %{VERSION} %{RELEASE} %{ARCH} :DESC: %{SUMMARY}\\n'") + "%{NAME} %|EPOCH?{%{EPOCH}}:{0}| %{VERSION} %{RELEASE} %{ARCH} :DESC: %{SUMMARY}\\n") end it "should report absent packages" do diff --git a/spec/unit/provider/package/msi_spec.rb b/spec/unit/provider/package/msi_spec.rb index 50ff40deb..192b0a749 100755 --- a/spec/unit/provider/package/msi_spec.rb +++ b/spec/unit/provider/package/msi_spec.rb @@ -24,9 +24,8 @@ describe Puppet::Type.type(:package).provider(:msi) do MsiPackage.stubs(:installer).returns(installer) end - before :each do - # make sure we never try to execute msiexec - provider.expects(:execute).never + def expect_execute(command, status) + provider.expects(:execute).with(command, execute_options).returns(Puppet::Util::Execution::ProcessOutput.new('',status)) end describe 'provider features' do @@ -38,6 +37,7 @@ describe Puppet::Type.type(:package).provider(:msi) do describe 'on Windows', :as_platform => :windows do it 'should not be the default provider' do + # provider.expects(:execute).never Puppet::Type.type(:package).defaultprovider.should_not == subject.class end end @@ -90,9 +90,7 @@ describe Puppet::Type.type(:package).provider(:msi) do end context '#install' do - before :each do - provider.stubs(:execute) - end + let (:command) { "msiexec.exe /qn /norestart /i #{source}" } it 'should require the source parameter' do resource = Puppet::Type.type(:package).new(:name => name, :provider => :msi) @@ -104,40 +102,27 @@ describe Puppet::Type.type(:package).provider(:msi) do it 'should install using the source and install_options' do resource[:install_options] = { 'INSTALLDIR' => 'C:\mysql-5.1' } - - provider.expects(:execute).with("msiexec.exe /qn /norestart /i #{source} INSTALLDIR=C:\\mysql-5.1", execute_options) - provider.expects(:exit_status).returns(0) - - provider.install - end - - it 'should warn if the package requests a reboot' do - provider.stubs(:exit_status).returns(194) - - provider.expects(:warning).with('The package requested a reboot to finish the operation.') + expect_execute("#{command} INSTALLDIR=C:\\mysql-5.1", 0) provider.install end it 'should warn if reboot initiated' do - provider.stubs(:exit_status).returns(1641) - + expect_execute(command, 1641) provider.expects(:warning).with('The package installed successfully and the system is rebooting now.') provider.install end it 'should warn if reboot required' do - provider.stubs(:exit_status).returns(3010) - + expect_execute(command, 3010) provider.expects(:warning).with('The package installed successfully, but the system must be rebooted.') provider.install end it 'should fail otherwise', :if => Puppet.features.microsoft_windows? do - provider.stubs(:execute) - provider.stubs(:exit_status).returns(5) + expect_execute(command, 5) expect do provider.install @@ -146,6 +131,9 @@ describe Puppet::Type.type(:package).provider(:msi) do end context '#uninstall' do + + let (:command) { "msiexec.exe /qn /norestart /x #{productcode}" } + before :each do resource[:ensure] = :absent provider.set(:productcode => productcode) @@ -159,42 +147,27 @@ describe Puppet::Type.type(:package).provider(:msi) do end it 'should uninstall using the productcode' do - provider.expects(:execute).with("msiexec.exe /qn /norestart /x #{productcode}", execute_options) - provider.expects(:exit_status).returns(0) - - provider.uninstall - end - - it 'should warn if the package requests a reboot' do - provider.stubs(:execute) - provider.stubs(:exit_status).returns(194) - - provider.expects(:warning).with('The package requested a reboot to finish the operation.') + expect_execute(command, 0) provider.uninstall end it 'should warn if reboot initiated' do - provider.stubs(:execute) - provider.stubs(:exit_status).returns(1641) - + expect_execute(command, 1641) provider.expects(:warning).with('The package uninstalled successfully and the system is rebooting now.') provider.uninstall end it 'should warn if reboot required' do - provider.stubs(:execute) - provider.stubs(:exit_status).returns(3010) - + expect_execute(command, 3010) provider.expects(:warning).with('The package uninstalled successfully, but the system must be rebooted.') provider.uninstall end it 'should fail otherwise', :if => Puppet.features.microsoft_windows? do - provider.stubs(:execute) - provider.stubs(:exit_status).returns(5) + expect_execute(command, 5) expect do provider.uninstall diff --git a/spec/unit/provider/package/openbsd_spec.rb b/spec/unit/provider/package/openbsd_spec.rb index 5a7ef7b46..48ec4bedb 100755 --- a/spec/unit/provider/package/openbsd_spec.rb +++ b/spec/unit/provider/package/openbsd_spec.rb @@ -10,7 +10,7 @@ describe provider_class do def expect_read_from_pkgconf(lines) pkgconf = stub(:readlines => lines) - File.expects(:exist?).with('/etc/pkg.conf').returns(true) + Puppet::FileSystem::File.expects(:exist?).with('/etc/pkg.conf').returns(true) File.expects(:open).with('/etc/pkg.conf', 'rb').returns(pkgconf) end @@ -76,7 +76,7 @@ describe provider_class do context "#install" do it "should fail if the resource doesn't have a source" do - File.expects(:exist?).with('/etc/pkg.conf').returns(false) + Puppet::FileSystem::File.expects(:exist?).with('/etc/pkg.conf').returns(false) expect { provider.install @@ -84,7 +84,7 @@ describe provider_class do end it "should fail if /etc/pkg.conf exists, but is not readable" do - File.expects(:exist?).with('/etc/pkg.conf').returns(true) + Puppet::FileSystem::File.expects(:exist?).with('/etc/pkg.conf').returns(true) File.expects(:open).with('/etc/pkg.conf', 'rb').raises(Errno::EACCES) expect { diff --git a/spec/unit/provider/package/rpm_spec.rb b/spec/unit/provider/package/rpm_spec.rb index e438b51a5..e81abafde 100755 --- a/spec/unit/provider/package/rpm_spec.rb +++ b/spec/unit/provider/package/rpm_spec.rb @@ -20,7 +20,8 @@ describe provider_class do let(:resource) do Puppet::Type.type(:package).new( :name => resource_name, - :ensure => :installed + :ensure => :installed, + :provider => 'rpm' ) end @@ -30,7 +31,7 @@ describe provider_class do provider end - let(:nevra_format) { %Q{'%{NAME} %|EPOCH?{%{EPOCH}}:{0}| %{VERSION} %{RELEASE} %{ARCH} :DESC: %{SUMMARY}\\n'} } + let(:nevra_format) { %Q{%{NAME} %|EPOCH?{%{EPOCH}}:{0}| %{VERSION} %{RELEASE} %{ARCH} :DESC: %{SUMMARY}\\n} } let(:execute_options) do {:failonfail => true, :combine => true, :custom_environment => {}} end @@ -47,7 +48,7 @@ describe provider_class do describe "self.instances" do describe "with a modern version of RPM" do it "should include all the modern flags" do - Puppet::Util::Execution.expects(:execpipe).with("/bin/rpm -qa --nosignature --nodigest --qf #{nevra_format}").yields(packages) + Puppet::Util::Execution.expects(:execpipe).with("/bin/rpm -qa --nosignature --nodigest --qf '#{nevra_format}'").yields(packages) installed_packages = subject.instances end @@ -56,7 +57,7 @@ describe provider_class do describe "with a version of RPM < 4.1" do let(:rpm_version) { "RPM version 4.0.2\n" } it "should exclude the --nosignature flag" do - Puppet::Util::Execution.expects(:execpipe).with("/bin/rpm -qa --nodigest --qf #{nevra_format}").yields(packages) + Puppet::Util::Execution.expects(:execpipe).with("/bin/rpm -qa --nodigest --qf '#{nevra_format}'").yields(packages) installed_packages = subject.instances end @@ -65,14 +66,14 @@ describe provider_class do describe "with a version of RPM < 4.0.2" do let(:rpm_version) { "RPM version 3.0.5\n" } it "should exclude the --nodigest flag" do - Puppet::Util::Execution.expects(:execpipe).with("/bin/rpm -qa --qf #{nevra_format}").yields(packages) + Puppet::Util::Execution.expects(:execpipe).with("/bin/rpm -qa --qf '#{nevra_format}'").yields(packages) installed_packages = subject.instances end end it "returns an array of packages" do - Puppet::Util::Execution.expects(:execpipe).with("/bin/rpm -qa --nosignature --nodigest --qf #{nevra_format}").yields(packages) + Puppet::Util::Execution.expects(:execpipe).with("/bin/rpm -qa --nosignature --nodigest --qf '#{nevra_format}'").yields(packages) installed_packages = subject.instances @@ -138,23 +139,40 @@ describe provider_class do describe "when not already installed" do it "should only include the '-i' flag" do - Puppet::Util::Execution.expects(:execute).with(["/bin/rpm", "-i", '/path/to/package'], execute_options) + Puppet::Util::Execution.expects(:execute).with(["/bin/rpm", ["-i"], '/path/to/package'], execute_options) provider.install end - end + end - describe "when an older version is installed" do - before(:each) do - # Force the provider to think a version of the package is already installed - # This is real hacky. I'm sorry. --jeffweiss 25 Jan 2013 - provider.instance_variable_get('@property_hash')[:ensure] = '1.2.3.3' - end + describe "when installed with options" do + let(:resource) do + Puppet::Type.type(:package).new( + :name => resource_name, + :ensure => :installed, + :provider => 'rpm', + :source => '/path/to/package', + :install_options => ['-D', {'--test' => 'value'}, '-Q'] + ) + end - it "should include the '-U --oldpackage' flags" do - Puppet::Util::Execution.expects(:execute).with(["/bin/rpm", ["-U", "--oldpackage"], '/path/to/package'], execute_options) + it "should include the options" do + Puppet::Util::Execution.expects(:execute).with(["/bin/rpm", ["-i", "-D", "--test=value", "-Q"], '/path/to/package'], execute_options) provider.install - end - end + end + end + + describe "when an older version is installed" do + before(:each) do + # Force the provider to think a version of the package is already installed + # This is real hacky. I'm sorry. --jeffweiss 25 Jan 2013 + provider.instance_variable_get('@property_hash')[:ensure] = '1.2.3.3' + end + + it "should include the '-U --oldpackage' flags" do + Puppet::Util::Execution.expects(:execute).with(["/bin/rpm", ["-U", "--oldpackage"], '/path/to/package'], execute_options) + provider.install + end + end end describe "#latest" do @@ -268,6 +286,31 @@ describe provider_class do end end + describe "#install_options" do + it "should return empty array by default" do + provider.install_options.should == [] + end + + it "should return install_options when set" do + provider.resource[:install_options] = ['-n'] + provider.install_options.should == ['-n'] + end + + it "should return multiple install_options when set" do + provider.resource[:install_options] = ['-L', '/opt/puppet'] + provider.install_options.should == ['-L', '/opt/puppet'] + end + + it 'should return install_options when set as hash' do + provider.resource[:install_options] = { '-Darch' => 'vax' } + provider.install_options.should == ['-Darch=vax'] + end + it 'should return install_options when an array with hashes' do + provider.resource[:install_options] = [ '-L', { '-Darch' => 'vax' }] + provider.install_options.should == ['-L', '-Darch=vax'] + end + end + describe ".nodigest" do { '4.0' => nil, '4.0.1' => nil, diff --git a/spec/unit/provider/package/windows_spec.rb b/spec/unit/provider/package/windows_spec.rb index d07d584de..4541f9ea7 100755 --- a/spec/unit/provider/package/windows_spec.rb +++ b/spec/unit/provider/package/windows_spec.rb @@ -14,8 +14,7 @@ describe Puppet::Type.type(:package).provider(:windows) do end def expect_execute(command, status) - provider.expects(:execute).with(command, execute_options) - provider.expects(:exit_status).returns(status) + provider.expects(:execute).with(command, execute_options).returns(Puppet::Util::Execution::ProcessOutput.new('',status)) end describe 'provider features' do @@ -23,6 +22,7 @@ describe Puppet::Type.type(:package).provider(:windows) do it { should be_uninstallable } it { should be_install_options } it { should be_uninstall_options } + it { should be_versionable } end describe 'on Windows', :if => Puppet.features.microsoft_windows? do @@ -36,17 +36,19 @@ describe Puppet::Type.type(:package).provider(:windows) do pkg1 = stub('pkg1') pkg2 = stub('pkg2') - prov1 = stub('prov1', :name => 'pkg1', :package => pkg1) - prov2 = stub('prov2', :name => 'pkg2', :package => pkg2) + prov1 = stub('prov1', :name => 'pkg1', :version => '1.0.0', :package => pkg1) + prov2 = stub('prov2', :name => 'pkg2', :version => nil, :package => pkg2) Puppet::Provider::Package::Windows::Package.expects(:map).multiple_yields([prov1], [prov2]).returns([prov1, prov2]) providers = provider.class.instances providers.count.should == 2 providers[0].name.should == 'pkg1' + providers[0].version.should == '1.0.0' providers[0].package.should == pkg1 providers[1].name.should == 'pkg2' + providers[1].version.should be_nil providers[1].package.should == pkg2 end @@ -59,13 +61,21 @@ describe Puppet::Type.type(:package).provider(:windows) do context '#query' do it 'should return the hash of the matched packaged' do - pkg = mock(:name => 'pkg1') + pkg = mock(:name => 'pkg1', :version => nil) pkg.expects(:match?).returns(true) Puppet::Provider::Package::Windows::Package.expects(:find).yields(pkg) provider.query.should == { :name => 'pkg1', :ensure => :installed, :provider => :windows } end + it 'should include the version string when present' do + pkg = mock(:name => 'pkg1', :version => '1.0.0') + pkg.expects(:match?).returns(true) + Puppet::Provider::Package::Windows::Package.expects(:find).yields(pkg) + + provider.query.should == { :name => 'pkg1', :ensure => '1.0.0', :provider => :windows } + end + it 'should return nil if no package was found' do Puppet::Provider::Package::Windows::Package.expects(:find) @@ -102,13 +112,6 @@ describe Puppet::Type.type(:package).provider(:windows) do provider.install end - it 'should warn if the package requests a reboot' do - expect_execute(command, 194) - provider.expects(:warning).with('The package requested a reboot to finish the operation.') - - provider.install - end - it 'should warn if reboot initiated' do expect_execute(command, 1641) provider.expects(:warning).with('The package installed successfully and the system is rebooting now.') @@ -161,13 +164,6 @@ describe Puppet::Type.type(:package).provider(:windows) do provider.uninstall end - it 'should warn if the package requests a reboot' do - expect_execute(command, 194) - provider.expects(:warning).with('The package requested a reboot to finish the operation.') - - provider.uninstall - end - it 'should warn if reboot initiated' do expect_execute(command, 1641) provider.expects(:warning).with('The package uninstalled successfully and the system is rebooting now.') diff --git a/spec/unit/provider/package/yum_spec.rb b/spec/unit/provider/package/yum_spec.rb index 7a019bea7..c5d89ae60 100755 --- a/spec/unit/provider/package/yum_spec.rb +++ b/spec/unit/provider/package/yum_spec.rb @@ -153,7 +153,7 @@ _pkg mysummaryless 0 1.2.3.4 5.el4 noarch end def expect_execpipe_to_provide_package_info_for_an_rpm_query - Puppet::Util::Execution.expects(:execpipe).with("/bin/rpm -qa --nosignature --nodigest --qf #{nevra_format}").yields(packages) + Puppet::Util::Execution.expects(:execpipe).with("/bin/rpm -qa --nosignature --nodigest --qf '#{nevra_format}'").yields(packages) end def expect_python_yumhelper_call_to_return_latest_info diff --git a/spec/unit/provider/service/base_spec.rb b/spec/unit/provider/service/base_spec.rb index b78bb0134..9d6a7fdcc 100755 --- a/spec/unit/provider/service/base_spec.rb +++ b/spec/unit/provider/service/base_spec.rb @@ -34,7 +34,7 @@ describe "base service provider" do end before :each do - File.unlink(flag) if File.exist?(flag) + Puppet::FileSystem::File.unlink(flag) if Puppet::FileSystem::File.exist?(flag) end it { should be } diff --git a/spec/unit/provider/service/daemontools_spec.rb b/spec/unit/provider/service/daemontools_spec.rb index c538a0ced..4c35e0586 100755 --- a/spec/unit/provider/service/daemontools_spec.rb +++ b/spec/unit/provider/service/daemontools_spec.rb @@ -99,9 +99,14 @@ describe provider_class do end describe "when enabling" do - it "should create a symlink between daemon dir and service dir" do - FileTest.stubs(:symlink?).returns(false) - File.expects(:symlink).with(File.join(@daemondir,"myservice"), File.join(@servicedir,"myservice")).returns(0) + it "should create a symlink between daemon dir and service dir", :if => Puppet.features.manages_symlinks? do + daemon_path = File.join(@daemondir, "myservice") + stub_daemon = stub(daemon_path, :symlink? => false) + Puppet::FileSystem::File.expects(:new).with(daemon_path).returns(stub_daemon) + service_path = File.join(@servicedir, "myservice") + mock_service = mock(service_path, :symlink? => false) + Puppet::FileSystem::File.expects(:new).with(service_path).returns(mock_service) + stub_daemon.expects(:symlink).returns(0) @provider.enable end end @@ -109,16 +114,19 @@ describe provider_class do describe "when disabling" do it "should remove the symlink between daemon dir and service dir" do FileTest.stubs(:directory?).returns(false) - FileTest.stubs(:symlink?).returns(true) - File.expects(:unlink).with(File.join(@servicedir,"myservice")) + path = File.join(@servicedir,"myservice") + mocked_file = mock(path, :symlink? => true) + Puppet::FileSystem::File.expects(:new).with(path).returns(mocked_file) + Puppet::FileSystem::File.expects(:unlink).with(path) @provider.stubs(:texecute).returns("") @provider.disable end it "should stop the service" do FileTest.stubs(:directory?).returns(false) - FileTest.stubs(:symlink?).returns(true) - File.stubs(:unlink) + mocked_file = mock('anything', :symlink? => true) + Puppet::FileSystem::File.expects(:new).returns(mocked_file) + Puppet::FileSystem::File.stubs(:unlink) @provider.expects(:stop) @provider.disable end @@ -134,7 +142,9 @@ describe provider_class do [true, false].each do |t| it "should return #{t} if the symlink exists" do @provider.stubs(:status).returns(:stopped) - FileTest.stubs(:symlink?).returns(t) + path = File.join(@servicedir,"myservice") + mocked_file = mock(path, :symlink? => t) + Puppet::FileSystem::File.expects(:new).with(path).returns(mocked_file) @provider.enabled?.should == "#{t}".to_sym end diff --git a/spec/unit/provider/service/freebsd_spec.rb b/spec/unit/provider/service/freebsd_spec.rb index 81e3fc1af..69b923f9a 100755 --- a/spec/unit/provider/service/freebsd_spec.rb +++ b/spec/unit/provider/service/freebsd_spec.rb @@ -62,13 +62,13 @@ OUTPUT end it "should enable only the selected service" do - File.stubs(:exists?).with('/etc/rc.conf').returns(true) + Puppet::FileSystem::File.stubs(:exist?).with('/etc/rc.conf').returns(true) File.stubs(:read).with('/etc/rc.conf').returns("openntpd_enable=\"NO\"\nntpd_enable=\"NO\"\n") fh = stub 'fh' File.stubs(:open).with('/etc/rc.conf', File::WRONLY).yields(fh) fh.expects(:<<).with("openntpd_enable=\"NO\"\nntpd_enable=\"YES\"\n") - File.stubs(:exists?).with('/etc/rc.conf.local').returns(false) - File.stubs(:exists?).with('/etc/rc.conf.d/ntpd').returns(false) + Puppet::FileSystem::File.stubs(:exist?).with('/etc/rc.conf.local').returns(false) + Puppet::FileSystem::File.stubs(:exist?).with('/etc/rc.conf.d/ntpd').returns(false) @provider.rc_replace('ntpd', 'ntpd', 'YES') end diff --git a/spec/unit/provider/service/gentoo_spec.rb b/spec/unit/provider/service/gentoo_spec.rb index dab315537..aa802430e 100755 --- a/spec/unit/provider/service/gentoo_spec.rb +++ b/spec/unit/provider/service/gentoo_spec.rb @@ -13,7 +13,9 @@ describe Puppet::Type.type(:service).provider(:gentoo) do # The initprovider (parent of the gentoo provider) does a stat call # before it even tries to execute an initscript. We use sshd in all the # tests so make sure it is considered present. - File.stubs(:stat).with('/etc/init.d/sshd') + sshd_path = '/etc/init.d/sshd' + stub_file = stub(sshd_path, :stat => stub('stat')) + Puppet::FileSystem::File.stubs(:new).with(sshd_path).returns stub_file end let :initscripts do @@ -48,7 +50,6 @@ describe Puppet::Type.type(:service).provider(:gentoo) do it "should get a list of services from /etc/init.d but exclude helper scripts" do FileTest.expects(:directory?).with('/etc/init.d').returns true - File.stubs(:symlink?).returns(false) Dir.expects(:entries).with('/etc/init.d').returns initscripts (initscripts - helperscripts).each do |script| FileTest.expects(:executable?).with("/etc/init.d/#{script}").returns true @@ -56,6 +57,8 @@ describe Puppet::Type.type(:service).provider(:gentoo) do helperscripts.each do |script| FileTest.expects(:executable?).with("/etc/init.d/#{script}").never end + + Puppet::FileSystem::File.stubs(:new).returns stub('file', :symlink? => false) described_class.instances.map(&:name).should == [ 'alsasound', 'bootmisc', diff --git a/spec/unit/provider/service/init_spec.rb b/spec/unit/provider/service/init_spec.rb index e88672b40..896f13ac1 100755 --- a/spec/unit/provider/service/init_spec.rb +++ b/spec/unit/provider/service/init_spec.rb @@ -62,12 +62,12 @@ describe Puppet::Type.type(:service).provider(:init) do described_class.instances.should be_all { |provider| provider.get(:hasstatus) == true } end - it "should discard upstart jobs" do + it "should discard upstart jobs", :if => Puppet.features.manages_symlinks? do not_init_service, *valid_services = @services - File.stubs(:symlink?).returns false - File.stubs(:symlink?).with("tmp/#{not_init_service}").returns(true) - File.stubs(:readlink).with("tmp/#{not_init_service}").returns("/lib/init/upstart-job") - + path = "tmp/#{not_init_service}" + mocked_file = mock(path, :symlink? => true, :readlink => "/lib/init/upstart-job") + Puppet::FileSystem::File.stubs(:new).returns stub('file', :symlink? => false) + Puppet::FileSystem::File.expects(:new).with(path).returns(mocked_file) described_class.instances.map(&:name).should == valid_services end @@ -82,7 +82,7 @@ describe Puppet::Type.type(:service).provider(:init) do describe "when checking valid paths" do it "should discard paths that do not exist" do File.expects(:directory?).with(paths[0]).returns false - File.expects(:exist?).with(paths[0]).returns false + Puppet::FileSystem::File.expects(:exist?).with(paths[0]).returns false File.expects(:directory?).with(paths[1]).returns true provider.paths.should == [paths[1]] @@ -90,7 +90,7 @@ describe Puppet::Type.type(:service).provider(:init) do it "should discard paths that are not directories" do paths.each do |path| - File.expects(:exist?).with(path).returns true + Puppet::FileSystem::File.expects(:exist?).with(path).returns true File.expects(:directory?).with(path).returns false end provider.paths.should be_empty @@ -103,28 +103,28 @@ describe Puppet::Type.type(:service).provider(:init) do end it "should be able to find the init script in the service path" do - File.expects(:exist?).with("#{paths[0]}/myservice").returns true - File.expects(:exist?).with("#{paths[1]}/myservice").never # first one wins + Puppet::FileSystem::File.expects(:exist?).with("#{paths[0]}/myservice").returns true + Puppet::FileSystem::File.expects(:exist?).with("#{paths[1]}/myservice").never # first one wins provider.initscript.should == "/service/path/myservice" end it "should be able to find the init script in an alternate service path" do - File.expects(:exist?).with("#{paths[0]}/myservice").returns false - File.expects(:exist?).with("#{paths[1]}/myservice").returns true + Puppet::FileSystem::File.expects(:exist?).with("#{paths[0]}/myservice").returns false + Puppet::FileSystem::File.expects(:exist?).with("#{paths[1]}/myservice").returns true provider.initscript.should == "/alt/service/path/myservice" end it "should be able to find the init script if it ends with .sh" do - File.expects(:exist?).with("#{paths[0]}/myservice").returns false - File.expects(:exist?).with("#{paths[1]}/myservice").returns false - File.expects(:exist?).with("#{paths[0]}/myservice.sh").returns true + Puppet::FileSystem::File.expects(:exist?).with("#{paths[0]}/myservice").returns false + Puppet::FileSystem::File.expects(:exist?).with("#{paths[1]}/myservice").returns false + Puppet::FileSystem::File.expects(:exist?).with("#{paths[0]}/myservice.sh").returns true provider.initscript.should == "/service/path/myservice.sh" end it "should fail if the service isn't there" do paths.each do |path| - File.expects(:exist?).with("#{path}/myservice").returns false - File.expects(:exist?).with("#{path}/myservice.sh").returns false + Puppet::FileSystem::File.expects(:exist?).with("#{path}/myservice").returns false + Puppet::FileSystem::File.expects(:exist?).with("#{path}/myservice.sh").returns false end expect { provider.initscript }.to raise_error(Puppet::Error, "Could not find init script for 'myservice'") end @@ -134,7 +134,7 @@ describe Puppet::Type.type(:service).provider(:init) do before :each do File.stubs(:directory?).with("/service/path").returns true File.stubs(:directory?).with("/alt/service/path").returns true - File.stubs(:exist?).with("/service/path/myservice").returns true + Puppet::FileSystem::File.stubs(:exist?).with("/service/path/myservice").returns true end [:start, :stop, :status, :restart].each do |method| diff --git a/spec/unit/provider/service/launchd_spec.rb b/spec/unit/provider/service/launchd_spec.rb index fc82fdd00..b126fb22d 100755 --- a/spec/unit/provider/service/launchd_spec.rb +++ b/spec/unit/provider/service/launchd_spec.rb @@ -209,29 +209,82 @@ describe Puppet::Type.type(:service).provider(:launchd) do end end - describe "when encountering malformed plists" do - let(:plist_without_label) do - { - 'LimitLoadToSessionType' => 'Aqua' - } - end - let(:busted_plist_path) { '/Library/LaunchAgents/org.busted.plist' } - - it "[17624] should warn that the plist in question is being skipped" do - provider.expects(:launchd_paths).returns(['/Library/LaunchAgents']) - provider.expects(:return_globbed_list_of_file_paths).with('/Library/LaunchAgents').returns([busted_plist_path]) - provider.expects(:read_plist).with(busted_plist_path).returns(plist_without_label) - Puppet.expects(:warning).with("The #{busted_plist_path} plist does not contain a 'label' key; Puppet is skipping it") - provider.jobsearch - end - - it "[15929] should skip plists that plutil cannot read" do - provider.expects(:plutil).with('-convert', 'xml1', '-o', '/dev/stdout', - busted_plist_path).raises(Puppet::ExecutionFailure, 'boom') - Puppet.expects(:warning).with("Cannot read file #{busted_plist_path}; " + - "Puppet is skipping it. \n" + - "Details: boom") - provider.read_plist(busted_plist_path) + describe "make_label_to_path_map" do + before do + # clear out this class variable between runs + if provider.instance_variable_defined? :@label_to_path_map + provider.send(:remove_instance_variable, :@label_to_path_map) + end + end + describe "when encountering malformed plists" do + let(:plist_without_label) do + { + 'LimitLoadToSessionType' => 'Aqua' + } + end + let(:busted_plist_path) { '/Library/LaunchAgents/org.busted.plist' } + + it "[17624] should warn that the plist in question is being skipped" do + provider.expects(:launchd_paths).returns(['/Library/LaunchAgents']) + provider.expects(:return_globbed_list_of_file_paths).with('/Library/LaunchAgents').returns([busted_plist_path]) + provider.expects(:read_plist).with(busted_plist_path).returns(plist_without_label) + Puppet.expects(:warning).with("The #{busted_plist_path} plist does not contain a 'label' key; Puppet is skipping it") + provider.make_label_to_path_map + end + + it "[15929] should skip plists that plutil cannot read" do + provider.expects(:plutil).with('-convert', 'xml1', '-o', '/dev/stdout', + busted_plist_path).raises(Puppet::ExecutionFailure, 'boom') + Puppet.expects(:warning).with("Cannot read file #{busted_plist_path}; " + + "Puppet is skipping it. \n" + + "Details: boom") + provider.read_plist(busted_plist_path) + end + end + it "should return the cached value when available" do + provider.instance_variable_set(:@label_to_path_map, {'xx'=>'yy'}) + provider.make_label_to_path_map.should eq({'xx'=>'yy'}) + end + describe "when successful" do + let(:launchd_dir) { '/Library/LaunchAgents' } + let(:plist) { launchd_dir + '/foo.bar.service.plist' } + let(:label) { 'foo.bar.service' } + before do + provider.instance_variable_set(:@label_to_path_map, nil) + provider.expects(:launchd_paths).returns([launchd_dir]) + provider.expects(:return_globbed_list_of_file_paths).with(launchd_dir).returns([plist]) + provider.expects(:read_plist).with(plist).returns({'Label'=>'foo.bar.service'}) + end + it "should read the plists and return their contents" do + provider.make_label_to_path_map.should eq({label=>plist}) + end + it "should re-read the plists and return their contents when refreshed" do + provider.instance_variable_set(:@label_to_path_map, {'xx'=>'yy'}) + provider.make_label_to_path_map(true).should eq({label=>plist}) + end + end + end + + describe "jobsearch" do + let(:map) { {"org.mozilla.puppet" => "/path/to/puppet.plist", + "org.mozilla.python" => "/path/to/python.plist"} } + it "returns the entire map with no args" do + provider.expects(:make_label_to_path_map).returns(map) + provider.jobsearch.should == map + end + it "returns a singleton hash when given a label" do + provider.expects(:make_label_to_path_map).returns(map) + provider.jobsearch("org.mozilla.puppet").should == { "org.mozilla.puppet" => "/path/to/puppet.plist" } + end + it "refreshes the label_to_path_map when label is not found" do + provider.expects(:make_label_to_path_map).with().returns({}) + provider.expects(:make_label_to_path_map).with(true).returns(map) + provider.jobsearch("org.mozilla.puppet").should == { "org.mozilla.puppet" => "/path/to/puppet.plist" } + end + it "raises Puppet::Error when the label is still not found" do + provider.expects(:make_label_to_path_map).with().returns(map) + provider.expects(:make_label_to_path_map).with(true).returns(map) + expect { provider.jobsearch("NOSUCH") }.to raise_error(Puppet::Error) end end end diff --git a/spec/unit/provider/service/openbsd_spec.rb b/spec/unit/provider/service/openbsd_spec.rb new file mode 100644 index 000000000..8c417986d --- /dev/null +++ b/spec/unit/provider/service/openbsd_spec.rb @@ -0,0 +1,125 @@ +#!/usr/bin/env ruby +# +# Unit testing for the OpenBSD service provider + +require 'spec_helper' + +provider_class = Puppet::Type.type(:service).provider(:openbsd) + +describe provider_class do + before :each do + Puppet::Type.type(:service).stubs(:defaultprovider).returns described_class + Facter.stubs(:value).with(:operatingsystem).returns :openbsd + end + + let :rcscripts do + [ + 'apmd', + 'aucat', + 'cron', + 'puppetd' + ] + end + + describe "#instances" do + it "should have an instances method" do + described_class.should respond_to :instances + end + + it "should list all available services" do + FileTest.expects(:directory?).with('/etc/rc.d').returns true + Dir.expects(:entries).with('/etc/rc.d').returns rcscripts + + rcscripts.each do |script| + FileTest.expects(:executable?).with("/etc/rc.d/#{script}").returns true + end + + described_class.instances.map(&:name).should == [ + 'apmd', + 'aucat', + 'cron', + 'puppetd' + ] + end + end + + describe "#start" do + it "should use the supplied start command if specified" do + provider = described_class.new(Puppet::Type.type(:service).new(:name => 'sshd', :start => '/bin/foo')) + provider.expects(:execute).with(['/bin/foo'], :failonfail => true, :override_locale => false, :squelch => true) + provider.start + end + + it "should start the service otherwise" do + provider = described_class.new(Puppet::Type.type(:service).new(:name => 'sshd')) + provider.expects(:execute).with(['/etc/rc.d/sshd', '-f', :start], :failonfail => true, :override_locale => false, :squelch => true) + provider.expects(:search).with('sshd').returns('/etc/rc.d/sshd') + provider.start + end + end + + describe "#stop" do + it "should use the supplied stop command if specified" do + provider = described_class.new(Puppet::Type.type(:service).new(:name => 'sshd', :stop => '/bin/foo')) + provider.expects(:execute).with(['/bin/foo'], :failonfail => true, :override_locale => false, :squelch => true) + provider.stop + end + + it "should stop the service otherwise" do + provider = described_class.new(Puppet::Type.type(:service).new(:name => 'sshd')) + provider.expects(:execute).with(['/etc/rc.d/sshd', :stop], :failonfail => true, :override_locale => false, :squelch => true) + provider.expects(:search).with('sshd').returns('/etc/rc.d/sshd') + provider.stop + end + end + + describe "#status" do + it "should use the status command from the resource" do + provider = described_class.new(Puppet::Type.type(:service).new(:name => 'sshd', :status => '/bin/foo')) + provider.expects(:execute).with(['/etc/rc.d/sshd', :status], :failonfail => false, :override_locale => false, :squelch => true).never + provider.expects(:execute).with(['/bin/foo'], :failonfail => false, :override_locale => false, :squelch => true) + provider.status + end + + it "should return :stopped when status command returns with a non-zero exitcode" do + provider = described_class.new(Puppet::Type.type(:service).new(:name => 'sshd', :status => '/bin/foo')) + provider.expects(:execute).with(['/etc/rc.d/sshd', :status], :failonfail => false, :override_locale => false, :squelch => true).never + provider.expects(:execute).with(['/bin/foo'], :failonfail => false, :override_locale => false, :squelch => true) + $CHILD_STATUS.stubs(:exitstatus).returns 3 + provider.status.should == :stopped + end + + it "should return :running when status command returns with a zero exitcode" do + provider = described_class.new(Puppet::Type.type(:service).new(:name => 'sshd', :status => '/bin/foo')) + provider.expects(:execute).with(['/etc/rc.d/sshd', :status], :failonfail => false, :override_locale => false, :squelch => true).never + provider.expects(:execute).with(['/bin/foo'], :failonfail => false, :override_locale => false, :squelch => true) + $CHILD_STATUS.stubs(:exitstatus).returns 0 + provider.status.should == :running + end + end + + describe "#restart" do + it "should use the supplied restart command if specified" do + provider = described_class.new(Puppet::Type.type(:service).new(:name => 'sshd', :restart => '/bin/foo')) + provider.expects(:execute).with(['/etc/rc.d/sshd', '-f', :restart], :failonfail => true, :override_locale => false, :squelch => true).never + provider.expects(:execute).with(['/bin/foo'], :failonfail => true, :override_locale => false, :squelch => true) + provider.restart + end + + it "should restart the service with rc-service restart if hasrestart is true" do + provider = described_class.new(Puppet::Type.type(:service).new(:name => 'sshd', :hasrestart => true)) + provider.expects(:execute).with(['/etc/rc.d/sshd', '-f', :restart], :failonfail => true, :override_locale => false, :squelch => true) + provider.expects(:search).with('sshd').returns('/etc/rc.d/sshd') + provider.restart + end + + it "should restart the service with rc-service stop/start if hasrestart is false" do + provider = described_class.new(Puppet::Type.type(:service).new(:name => 'sshd', :hasrestart => false)) + provider.expects(:execute).with(['/etc/rc.d/sshd', '-f', :restart], :failonfail => true, :override_locale => false, :squelch => true).never + provider.expects(:execute).with(['/etc/rc.d/sshd', :stop], :failonfail => true, :override_locale => false, :squelch => true) + provider.expects(:execute).with(['/etc/rc.d/sshd', '-f', :start], :failonfail => true, :override_locale => false, :squelch => true) + provider.expects(:search).with('sshd').returns('/etc/rc.d/sshd') + provider.restart + end + end +end diff --git a/spec/unit/provider/service/openwrt_spec.rb b/spec/unit/provider/service/openwrt_spec.rb index bc8a6323d..2113aceb4 100755 --- a/spec/unit/provider/service/openwrt_spec.rb +++ b/spec/unit/provider/service/openwrt_spec.rb @@ -33,7 +33,7 @@ describe Puppet::Type.type(:service).provider(:openwrt), :as_platform => :posix # All OpenWrt tests operate on the init script directly. It must exist. File.stubs(:directory?).with('/etc/init.d').returns true - File.stubs(:exist?).with('/etc/init.d/myservice').returns true + Puppet::FileSystem::File.stubs(:exist?).with('/etc/init.d/myservice').returns true FileTest.stubs(:file?).with('/etc/init.d/myservice').returns true FileTest.stubs(:executable?).with('/etc/init.d/myservice').returns true end diff --git a/spec/unit/provider/service/runit_spec.rb b/spec/unit/provider/service/runit_spec.rb index 5c5b37b37..cbdc19ba6 100755 --- a/spec/unit/provider/service/runit_spec.rb +++ b/spec/unit/provider/service/runit_spec.rb @@ -96,18 +96,25 @@ describe provider_class do end describe "when enabling" do - it "should create a symlink between daemon dir and service dir" do - FileTest.stubs(:symlink?).returns(false) - File.expects(:symlink).with(File.join(@daemondir,"myservice"), File.join(@servicedir,"myservice")).returns(0) + it "should create a symlink between daemon dir and service dir", :if => Puppet.features.manages_symlinks? do + daemon_path = File.join(@daemondir,"myservice") + mock_daemon = mock(daemon_path) + Puppet::FileSystem::File.expects(:new).with(daemon_path).returns(mock_daemon) + service_path = File.join(@servicedir,"myservice") + mock_service = mock(service_path, :symlink? => false) + Puppet::FileSystem::File.expects(:new).with(service_path).returns(mock_service) + mock_daemon.expects(:symlink).with(File.join(@servicedir,"myservice")).returns(0) @provider.enable end end describe "when disabling" do it "should remove the '/etc/service/myservice' symlink" do + path = File.join(@servicedir,"myservice") + mocked_file = mock(path, :symlink? => true) FileTest.stubs(:directory?).returns(false) - FileTest.stubs(:symlink?).returns(true) - File.expects(:unlink).with(File.join(@servicedir,"myservice")).returns(0) + Puppet::FileSystem::File.expects(:new).with(path).returns(mocked_file) + Puppet::FileSystem::File.expects(:unlink).with(path).returns(0) @provider.disable end end diff --git a/spec/unit/provider/service/upstart_spec.rb b/spec/unit/provider/service/upstart_spec.rb index e24857901..ed93386b3 100755 --- a/spec/unit/provider/service/upstart_spec.rb +++ b/spec/unit/provider/service/upstart_spec.rb @@ -51,8 +51,8 @@ describe Puppet::Type.type(:service).provider(:upstart) do describe "#search" do it "searches through paths to find a matching conf file" do File.stubs(:directory?).returns(true) - File.stubs(:exists?).returns(false) - File.expects(:exists?).with("/etc/init/foo-bar.conf").returns(true) + Puppet::FileSystem::File.stubs(:exist?).returns(false) + Puppet::FileSystem::File.expects(:exist?).with("/etc/init/foo-bar.conf").returns(true) resource = Puppet::Type.type(:service).new(:name => "foo-bar", :provider => :upstart) provider = provider_class.new(resource) @@ -61,8 +61,8 @@ describe Puppet::Type.type(:service).provider(:upstart) do it "searches for just the name of a compound named service" do File.stubs(:directory?).returns(true) - File.stubs(:exists?).returns(false) - File.expects(:exists?).with("/etc/init/network-interface.conf").returns(true) + Puppet::FileSystem::File.stubs(:exist?).returns(false) + Puppet::FileSystem::File.expects(:exist?).with("/etc/init/network-interface.conf").returns(true) resource = Puppet::Type.type(:service).new(:name => "network-interface INTERFACE=lo", :provider => :upstart) provider = provider_class.new(resource) diff --git a/spec/unit/provider/ssh_authorized_key/parsed_spec.rb b/spec/unit/provider/ssh_authorized_key/parsed_spec.rb index 5cfa8994b..4d17ffe51 100755 --- a/spec/unit/provider/ssh_authorized_key/parsed_spec.rb +++ b/spec/unit/provider/ssh_authorized_key/parsed_spec.rb @@ -162,7 +162,7 @@ describe provider_class, :unless => Puppet.features.microsoft_windows? do end it "should create the directory" do - File.stubs(:exist?).with("/tmp/.ssh_dir").returns false + Puppet::FileSystem::File.stubs(:exist?).with("/tmp/.ssh_dir").returns false Dir.expects(:mkdir).with("/tmp/.ssh_dir", 0700) @provider.flush end @@ -199,19 +199,19 @@ describe provider_class, :unless => Puppet.features.microsoft_windows? do end it "should create the directory if it doesn't exist" do - File.stubs(:exist?).with(@dir).returns false + Puppet::FileSystem::File.stubs(:exist?).with(@dir).returns false Dir.expects(:mkdir).with(@dir,0700) @provider.flush end it "should not create or chown the directory if it already exist" do - File.stubs(:exist?).with(@dir).returns false + Puppet::FileSystem::File.stubs(:exist?).with(@dir).returns false Dir.expects(:mkdir).never @provider.flush end it "should absolutely not chown the directory to the user if it creates it" do - File.stubs(:exist?).with(@dir).returns false + Puppet::FileSystem::File.stubs(:exist?).with(@dir).returns false Dir.stubs(:mkdir).with(@dir,0700) uid = Puppet::Util.uid("nobody") File.expects(:chown).never @@ -219,7 +219,7 @@ describe provider_class, :unless => Puppet.features.microsoft_windows? do end it "should not create or chown the directory if it already exist" do - File.stubs(:exist?).with(@dir).returns false + Puppet::FileSystem::File.stubs(:exist?).with(@dir).returns false Dir.expects(:mkdir).never File.expects(:chown).never @provider.flush diff --git a/spec/unit/provider/user/directoryservice_spec.rb b/spec/unit/provider/user/directoryservice_spec.rb index e464e2c6f..fe72bfc4f 100755 --- a/spec/unit/provider/user/directoryservice_spec.rb +++ b/spec/unit/provider/user/directoryservice_spec.rb @@ -704,7 +704,7 @@ describe Puppet::Type.type(:user).provider(:directoryservice) do let(:stub_password_file) { stub('connection') } it 'should return a sha1 hash read from disk' do - File.expects(:exists?).with(password_hash_file).returns(true) + Puppet::FileSystem::File.expects(:exist?).with(password_hash_file).returns(true) File.expects(:file?).with(password_hash_file).returns(true) File.expects(:readable?).with(password_hash_file).returns(true) File.expects(:new).with(password_hash_file).returns(stub_password_file) @@ -714,18 +714,18 @@ describe Puppet::Type.type(:user).provider(:directoryservice) do end it 'should return nil if the password_hash_file does not exist' do - File.expects(:exists?).with(password_hash_file).returns(false) + Puppet::FileSystem::File.expects(:exist?).with(password_hash_file).returns(false) provider.class.get_sha1('user_guid').should == nil end it 'should return nil if the password_hash_file is not a file' do - File.expects(:exists?).with(password_hash_file).returns(true) + Puppet::FileSystem::File.expects(:exist?).with(password_hash_file).returns(true) File.expects(:file?).with(password_hash_file).returns(false) provider.class.get_sha1('user_guid').should == nil end it 'should raise an error if the password_hash_file is not readable' do - File.expects(:exists?).with(password_hash_file).returns(true) + Puppet::FileSystem::File.expects(:exist?).with(password_hash_file).returns(true) File.expects(:file?).with(password_hash_file).returns(true) File.expects(:readable?).with(password_hash_file).returns(false) expect { provider.class.get_sha1('user_guid').should == nil }.to raise_error Puppet::Error, /Could not read password hash file at #{password_hash_file}/ diff --git a/spec/unit/provider/user/windows_adsi_spec.rb b/spec/unit/provider/user/windows_adsi_spec.rb index 4e3a6f463..c25ccaf95 100755 --- a/spec/unit/provider/user/windows_adsi_spec.rb +++ b/spec/unit/provider/user/windows_adsi_spec.rb @@ -24,7 +24,7 @@ describe Puppet::Type.type(:user).provider(:windows_adsi) do it "should enumerate all users" do names = ['user1', 'user2', 'user3'] stub_users = names.map{|n| stub(:name => n)} - connection.stubs(:execquery).with("select name from win32_useraccount").returns(stub_users) + connection.stubs(:execquery).with('select name from win32_useraccount where localaccount = "TRUE"').returns(stub_users) described_class.instances.map(&:name).should =~ names end diff --git a/spec/unit/provider/zone/solaris_spec.rb b/spec/unit/provider/zone/solaris_spec.rb index 6fd58a46a..c143cabd1 100755 --- a/spec/unit/provider/zone/solaris_spec.rb +++ b/spec/unit/provider/zone/solaris_spec.rb @@ -138,7 +138,7 @@ net: it "should not require path if sysidcfg is specified" do resource[:path] = '/mypath' resource[:sysidcfg] = 'dummy' - File.stubs(:exists?).with('/mypath/root/etc/sysidcfg').returns true + Puppet::FileSystem::File.stubs(:exist?).with('/mypath/root/etc/sysidcfg').returns true File.stubs(:directory?).with('/mypath/root/etc').returns true provider.expects(:zoneadm).with(:boot) provider.start diff --git a/spec/unit/provider_spec.rb b/spec/unit/provider_spec.rb index 208094b7e..31e69f799 100755 --- a/spec/unit/provider_spec.rb +++ b/spec/unit/provider_spec.rb @@ -349,10 +349,10 @@ describe Puppet::Provider do command = Puppet::Util.which('sh') || Puppet::Util.which('cmd.exe') parent.commands :sh => command - FileTest.should be_exists parent.command(:sh) + Puppet::FileSystem::File.exist?(parent.command(:sh)).should be_true parent.command(:sh).should =~ /#{Regexp.escape(command)}$/ - FileTest.should be_exists child.command(:sh) + Puppet::FileSystem::File.exist?(child.command(:sh)).should be_true child.command(:sh).should =~ /#{Regexp.escape(command)}$/ end diff --git a/spec/unit/reports/http_spec.rb b/spec/unit/reports/http_spec.rb index 13f202607..1a99761d8 100755 --- a/spec/unit/reports/http_spec.rb +++ b/spec/unit/reports/http_spec.rb @@ -15,65 +15,50 @@ describe processor do http.expects(:post).returns(httpok) end - it "should use the reporturl setting's host, port and ssl option" do - uri = URI.parse(Puppet[:reporturl]) - ssl = (uri.scheme == 'https') - Net::HTTP.expects(:new).with( - uri.host, uri.port, optionally(anything, anything) - ).returns http - http.expects(:use_ssl=).with(ssl) - subject.process - end + it "configures the connection for ssl when using https" do + Puppet[:reporturl] = 'https://testing:8080/the/path' - it "uses ssl if reporturl has the https protocol" do - Puppet[:reporturl] = "https://myhost.mydomain:1234/report/upload" - uri = URI.parse(Puppet[:reporturl]) - Net::HTTP.expects(:new).with( - uri.host, uri.port, optionally(anything, anything) + Puppet::Network::HttpPool.expects(:http_instance).with( + 'testing', 8080, true ).returns http - http.expects(:use_ssl=).with(true) + subject.process end - it "does not use ssl if reporturl has plain http protocol" do - Puppet[:reporturl] = "http://myhost.mydomain:1234/report/upload" - uri = URI.parse(Puppet[:reporturl]) - Net::HTTP.expects(:new).with( - uri.host, uri.port, optionally(anything, anything) + it "does not configure the connectino for ssl when using http" do + Puppet[:reporturl] = "http://testing:8080/the/path" + + Puppet::Network::HttpPool.expects(:http_instance).with( + 'testing', 8080, false ).returns http - http.expects(:use_ssl=).with(false) + subject.process end end describe "when making a request" do - let(:http) { stub_everything "http" } + let(:connection) { stub_everything "connection" } let(:httpok) { Net::HTTPOK.new('1.1', 200, '') } before :each do - Net::HTTP.expects(:new).returns(http) + Puppet::Network::HttpPool.expects(:http_instance).returns(connection) end it "should use the path specified by the 'reporturl' setting" do - http.expects(:post).with {|path, data, headers| - path.should == URI.parse(Puppet[:reporturl]).path - }.returns(httpok) + report_path = URI.parse(Puppet[:reporturl]).path + connection.expects(:post).with(report_path, anything, anything).returns(httpok) subject.process end it "should give the body as the report as YAML" do - http.expects(:post).with {|path, data, headers| - data.should == subject.to_yaml - }.returns(httpok) + connection.expects(:post).with(anything, subject.to_yaml, anything).returns(httpok) subject.process end it "should set content-type to 'application/x-yaml'" do - http.expects(:post).with {|path, data, headers| - headers["Content-Type"].should == "application/x-yaml" - }.returns(httpok) + connection.expects(:post).with(anything, anything, has_entry("Content-Type" => "application/x-yaml")).returns(httpok) subject.process end @@ -82,7 +67,7 @@ describe processor do if code.to_i >= 200 and code.to_i < 300 it "should succeed on http code #{code}" do response = klass.new('1.1', code, '') - http.expects(:post).returns(response) + connection.expects(:post).returns(response) Puppet.expects(:err).never subject.process @@ -92,7 +77,7 @@ describe processor do if code.to_i >= 300 && ![301, 302, 307].include?(code.to_i) it "should log error on http code #{code}" do response = klass.new('1.1', code, '') - http.expects(:post).returns(response) + connection.expects(:post).returns(response) Puppet.expects(:err) subject.process diff --git a/spec/unit/reports/store_spec.rb b/spec/unit/reports/store_spec.rb index 5320c6479..421f8404d 100755 --- a/spec/unit/reports/store_spec.rb +++ b/spec/unit/reports/store_spec.rb @@ -47,7 +47,7 @@ describe processor do it "rejects invalid hostnames" do @report.host = ".." - FileTest.expects(:exists?).never + Puppet::FileSystem::File.expects(:exist?).never Tempfile.expects(:new).never expect { @report.process }.to raise_error(ArgumentError, /Invalid node/) end @@ -55,7 +55,7 @@ describe processor do describe "::destroy" do it "rejects invalid hostnames" do - File.expects(:unlink).never + Puppet::FileSystem::File.expects(:unlink).never expect { processor.destroy("..") }.to raise_error(ArgumentError, /Invalid node/) end end diff --git a/spec/unit/resource/catalog_spec.rb b/spec/unit/resource/catalog_spec.rb index dab6e4591..dd88e8e20 100755 --- a/spec/unit/resource/catalog_spec.rb +++ b/spec/unit/resource/catalog_spec.rb @@ -1,5 +1,18 @@ #! /usr/bin/env ruby require 'spec_helper' +require 'puppet_spec/compiler' + +# the json-schema gem doesn't support windows +if not Puppet.features.microsoft_windows? + CATALOG_SCHEMA = JSON.parse(File.read(File.join(File.dirname(__FILE__), '../../../api/schemas/catalog.json'))) + + describe "catalog schema" do + it "should validate against the json meta-schema" do + JSON::Validator.validate!(JSON_META_SCHEMA, CATALOG_SCHEMA) + end + end + +end describe Puppet::Resource::Catalog, "when compiling" do include PuppetSpec::Files @@ -86,26 +99,28 @@ describe Puppet::Resource::Catalog, "when compiling" do it "should accept tags" do config = Puppet::Resource::Catalog.new("mynode") config.tag("one") - config.tags.should == %w{one} + config.should be_tagged("one") end it "should accept multiple tags at once" do config = Puppet::Resource::Catalog.new("mynode") config.tag("one", "two") - config.tags.should == %w{one two} + config.should be_tagged("one") + config.should be_tagged("two") end it "should convert all tags to strings" do config = Puppet::Resource::Catalog.new("mynode") config.tag("one", :two) - config.tags.should == %w{one two} + config.should be_tagged("one") + config.should be_tagged("two") end it "should tag with both the qualified name and the split name" do config = Puppet::Resource::Catalog.new("mynode") config.tag("one::two") - config.tags.include?("one").should be_true - config.tags.include?("one::two").should be_true + config.should be_tagged("one") + config.should be_tagged("one::two") end it "should accept classes" do @@ -119,7 +134,7 @@ describe Puppet::Resource::Catalog, "when compiling" do it "should tag itself with passed class names" do config = Puppet::Resource::Catalog.new("mynode") config.add_class("one") - config.tags.should == %w{one} + config.should be_tagged("one") end end @@ -204,12 +219,12 @@ describe Puppet::Resource::Catalog, "when compiling" do @r1 = stub_everything 'r1', :ref => "File[/a]" @r1.stubs(:respond_to?).with(:ref).returns(true) - @r1.stubs(:dup).returns(@r1) + @r1.stubs(:copy_as_resource).returns(@r1) @r1.stubs(:is_a?).with(Puppet::Resource).returns(true) @r2 = stub_everything 'r2', :ref => "File[/b]" @r2.stubs(:respond_to?).with(:ref).returns(true) - @r2.stubs(:dup).returns(@r2) + @r2.stubs(:copy_as_resource).returns(@r2) @r2.stubs(:is_a?).with(Puppet::Resource).returns(true) @resources = [@r1,@r2] @@ -720,6 +735,60 @@ describe Puppet::Resource::Catalog, "when compiling" do end end +describe Puppet::Resource::Catalog, "when converting a resource catalog to pson" do + include PuppetSpec::Compiler + + def validate_json_for_catalog(catalog) + JSON::Validator.validate!(CATALOG_SCHEMA, catalog.to_pson) + end + + it "should validate an empty catalog against the schema", :unless => Puppet.features.microsoft_windows? do + empty_catalog = compile_to_catalog("") + validate_json_for_catalog(empty_catalog) + end + + it "should validate a noop catalog against the schema", :unless => Puppet.features.microsoft_windows? do + noop_catalog = compile_to_catalog("create_resources('file', {})") + validate_json_for_catalog(noop_catalog) + end + + it "should validate a single resource catalog against the schema", :unless => Puppet.features.microsoft_windows? do + catalog = compile_to_catalog("create_resources('file', {'/etc/foo'=>{'ensure'=>'present'}})") + validate_json_for_catalog(catalog) + end + + it "should validate a virtual resource catalog against the schema", :unless => Puppet.features.microsoft_windows? do + catalog = compile_to_catalog("create_resources('@file', {'/etc/foo'=>{'ensure'=>'present'}})\nrealize(File['/etc/foo'])") + validate_json_for_catalog(catalog) + end + + it "should validate a single exported resource catalog against the schema", :unless => Puppet.features.microsoft_windows? do + catalog = compile_to_catalog("create_resources('@@file', {'/etc/foo'=>{'ensure'=>'present'}})") + validate_json_for_catalog(catalog) + end + + it "should validate a two resource catalog against the schema", :unless => Puppet.features.microsoft_windows? do + catalog = compile_to_catalog("create_resources('notify', {'foo'=>{'message'=>'one'}, 'bar'=>{'message'=>'two'}})") + validate_json_for_catalog(catalog) + end + + it "should validate a two parameter class catalog against the schema", :unless => Puppet.features.microsoft_windows? do + catalog = compile_to_catalog(<<-MANIFEST) + class multi_param_class ($one, $two) { + notify {'foo': + message => "One is $one, two is $two", + } + } + + class {'multi_param_class': + one => 'hello', + two => 'world', + } + MANIFEST + validate_json_for_catalog(catalog) + end +end + describe Puppet::Resource::Catalog, "when converting to pson" do before do @catalog = Puppet::Resource::Catalog.new("myhost") @@ -742,11 +811,11 @@ describe Puppet::Resource::Catalog, "when converting to pson" do PSON.parse @catalog.to_pson end - [:name, :version, :tags, :classes].each do |param| + [:name, :version, :classes].each do |param| it "should set its #{param} to the #{param} of the resource" do @catalog.send(param.to_s + "=", "testing") unless @catalog.send(param) - pson_output_should { |hash| hash['data'][param.to_s] == @catalog.send(param) } + pson_output_should { |hash| hash['data'][param.to_s].should == @catalog.send(param) } PSON.parse @catalog.to_pson end end @@ -815,7 +884,8 @@ describe Puppet::Resource::Catalog, "when converting from pson" do it "should set any provided tags on the catalog" do @data['tags'] = %w{one two} PSON.parse @pson.to_pson - @catalog.tags.should == @data['tags'] + @catalog.should be_tagged("one") + @catalog.should be_tagged("two") end it "should set any provided classes on the catalog" do diff --git a/spec/unit/resource/resource_type.json b/spec/unit/resource/resource_type.json deleted file mode 100644 index ffd15d639..000000000 --- a/spec/unit/resource/resource_type.json +++ /dev/null @@ -1,34 +0,0 @@ -{ - "type": "object", - "properties": { - "doc": { - "type": "string" - }, - "line": { - "type": "integer" - }, - "file": { - "type": "string" - }, - "parent": { - "type": "string" - }, - "name": { - "type": "string", - "required": "true" - }, - "kind": { - "type": "string", - "enum": [ - "class", - "node", - "defined_type" - ], - "required": "true" - }, - "parameters": { - "type": "object" - } - }, - "additionalProperties": false -} diff --git a/spec/unit/resource/status_spec.rb b/spec/unit/resource/status_spec.rb index f3cc5699c..d50abdce3 100755 --- a/spec/unit/resource/status_spec.rb +++ b/spec/unit/resource/status_spec.rb @@ -59,7 +59,9 @@ describe Puppet::Resource::Status do it "should copy the resource's tags" do @resource.expects(:tags).returns %w{foo bar} - Puppet::Resource::Status.new(@resource).tags.should == %w{foo bar} + status = Puppet::Resource::Status.new(@resource) + status.should be_tagged("foo") + status.should be_tagged("bar") end it "should always convert the resource to a string" do @@ -113,6 +115,14 @@ describe Puppet::Resource::Status do @status.events.should == [event] end + it "records an event for a failure caused by an error" do + @status.failed_because(StandardError.new("the message")) + + expect(@status.events[0].message).to eq("the message") + expect(@status.events[0].status).to eq("failure") + expect(@status.events[0].name).to eq(:resource_error) + end + it "should count the number of successful events and set changed" do 3.times{ @status << Puppet::Transaction::Event.new(:status => 'success') } @status.change_count.should == 3 diff --git a/spec/unit/resource/type_spec.rb b/spec/unit/resource/type_spec.rb index a4929208e..1b5a4727b 100755 --- a/spec/unit/resource/type_spec.rb +++ b/spec/unit/resource/type_spec.rb @@ -1,8 +1,19 @@ #! /usr/bin/env ruby require 'spec_helper' - require 'puppet/resource/type' +# the json-schema gem doesn't support windows +if not Puppet.features.microsoft_windows? + RESOURCE_TYPE_SCHEMA = JSON.parse(File.read(File.join(File.dirname(__FILE__), '../../../api/schemas/resource_type.json'))) + + describe "resource type schema" do + it "should validate against the json meta-schema" do + JSON::Validator.validate!(JSON_META_SCHEMA, RESOURCE_TYPE_SCHEMA) + end + end + +end + describe Puppet::Resource::Type do it "should have a 'name' attribute" do Puppet::Resource::Type.new(:hostclass, "foo").name.should == "foo" @@ -31,6 +42,10 @@ describe Puppet::Resource::Type do end describe "when converting to json" do + def validate_json_for_type(type) + JSON::Validator.validate!(RESOURCE_TYPE_SCHEMA, type.to_pson) + end + before do @type = Puppet::Resource::Type.new(:hostclass, "foo") end @@ -48,6 +63,20 @@ describe Puppet::Resource::Type do double_convert.type.should == @type.type end + it "should validate with only name and kind", :unless => Puppet.features.microsoft_windows? do + validate_json_for_type(@type) + end + + it "should validate with all fields set", :unless => Puppet.features.microsoft_windows? do + @type.set_arguments("one" => nil, "two" => "foo") + @type.line = 100 + @type.doc = "A weird type" + @type.file = "/etc/manifests/thing.pp" + @type.parent = "one::two" + + validate_json_for_type(@type) + end + it "should include any arguments" do @type.set_arguments("one" => nil, "two" => "foo") diff --git a/spec/unit/resource_spec.rb b/spec/unit/resource_spec.rb index 37d2001b8..a8f21d205 100755 --- a/spec/unit/resource_spec.rb +++ b/spec/unit/resource_spec.rb @@ -339,8 +339,11 @@ describe Puppet::Resource do end it "should query the injector using a namespaced key" do - compiler.injector.expects(:lookup).with(scope, 'apache::port') + compiler.injector.expects(:lookup).with(scope, 'apache::port').returns("8081") + resource.set_default_parameters(scope) + + resource[:port].should == "8081" end it "should use the value from the data_binding terminus" do @@ -376,8 +379,16 @@ describe Puppet::Resource do resource[:port].should == '80' end + it "should fail with error message about data binding on a hiera failure" do + Puppet::DataBinding.indirection.expects(:find).raises(Puppet::DataBinding::LookupError, 'Forgettabotit') + expect { + resource.set_default_parameters(scope) + }.to raise_error(Puppet::Error, /Error from DataBinding 'hiera' while looking up 'apache::port':.*Forgettabotit/) + end + it "should use the default value if the injector returns nil" do compiler.injector.expects(:lookup).returns(nil) + Puppet::DataBinding.indirection.expects(:find).returns(nil) resource.set_default_parameters(scope) @@ -607,7 +618,7 @@ describe Puppet::Resource do end end - describe "when serializing" do + describe "when serializing a native type" do before do @resource = Puppet::Resource.new("file", "/my/file") @resource["one"] = "test" @@ -622,6 +633,31 @@ describe Puppet::Resource do end end + describe "when serializing a defined type" do + before do + type = Puppet::Resource::Type.new(:definition, "foo::bar") + Puppet::Node::Environment.new.known_resource_types.add type + end + + before :each do + @resource = Puppet::Resource.new('foo::bar', 'xyzzy') + @resource['one'] = 'test' + @resource['two'] = 'other' + @resource.resource_type + end + + it "doesn't include transient instance variables (#4506)" do + expect(@resource.to_yaml_properties).to_not include :@rstype + end + + it "produces an equivalent yaml object" do + text = @resource.render('yaml') + + newresource = Puppet::Resource.convert_from('yaml', text) + newresource.should equal_attributes_of @resource + end + end + describe "when converting to a RAL resource" do it "should use the resource type's :new method to create the resource if the resource is of a builtin type" do resource = Puppet::Resource.new("file", basepath+"/my/file") @@ -810,9 +846,9 @@ describe Puppet::Resource do end end - describe "it should implement to_resource" do + describe "it should implement copy_as_resource" do resource = Puppet::Resource.new("file", "/my/file") - resource.to_resource.should == resource + resource.copy_as_resource.should == resource end describe "because it is an indirector model" do diff --git a/spec/unit/settings/autosign_setting_spec.rb b/spec/unit/settings/autosign_setting_spec.rb new file mode 100644 index 000000000..c08171374 --- /dev/null +++ b/spec/unit/settings/autosign_setting_spec.rb @@ -0,0 +1,46 @@ +require 'spec_helper' + +require 'puppet/settings' +require 'puppet/settings/autosign_setting' + +describe Puppet::Settings::AutosignSetting do + let(:setting) { described_class.new(:settings => mock('settings'), :desc => "test") } + + it "is of type :autosign" do + expect(setting.type).to eq :autosign + end + + describe "when munging the setting" do + it "passes boolean values through" do + expect(setting.munge(true)).to eq true + expect(setting.munge(false)).to eq false + end + + it "converts nil to false" do + expect(setting.munge(nil)).to eq false + end + + it "munges string 'true' to boolean true" do + expect(setting.munge('true')).to eq true + end + + it "munges string 'false' to boolean false" do + expect(setting.munge('false')).to eq false + end + + it "passes absolute paths through" do + path = File.expand_path('/path/to/autosign.conf') + expect(setting.munge(path)).to eq path + end + + it "fails if given anything else" do + cases = [1.0, 'sometimes', 'relative/autosign.conf'] + + cases.each do |invalid| + expect { + setting.munge(invalid) + }.to raise_error Puppet::Settings::ValidationError, /Invalid autosign value/ + end + end + end +end diff --git a/spec/unit/settings/file_setting_spec.rb b/spec/unit/settings/file_setting_spec.rb index a6e3f3f12..d9c6f5629 100755 --- a/spec/unit/settings/file_setting_spec.rb +++ b/spec/unit/settings/file_setting_spec.rb @@ -139,14 +139,14 @@ describe Puppet::Settings::FileSetting do it "should skip non-existent files if 'create_files' is not enabled" do @file.expects(:create_files?).returns false @file.expects(:type).returns :file - File.expects(:exist?).with(@basepath).returns false + Puppet::FileSystem::File.expects(:exist?).with(@basepath).returns false @file.to_resource.should be_nil end it "should manage existent files even if 'create_files' is not enabled" do @file.expects(:create_files?).returns false @file.expects(:type).returns :file - File.expects(:exist?).with(@basepath).returns true + Puppet::FileSystem::File.expects(:exist?).with(@basepath).returns true @file.to_resource.should be_instance_of(Puppet::Resource) end diff --git a/spec/unit/settings/path_setting_spec.rb b/spec/unit/settings/path_setting_spec.rb index d2ec46b5d..b7bc3198c 100755 --- a/spec/unit/settings/path_setting_spec.rb +++ b/spec/unit/settings/path_setting_spec.rb @@ -22,8 +22,8 @@ describe Puppet::Settings::PathSetting do end it "should work with UNC paths" do - subject.munge('//server/some/path').should == '//server/some/path' - subject.munge('\\\\server\some\path').should == '//server/some/path' + subject.munge('//localhost/some/path').should == '//localhost/some/path' + subject.munge('\\\\localhost\some\path').should == '//localhost/some/path' end end end diff --git a/spec/unit/settings/priority_setting_spec.rb b/spec/unit/settings/priority_setting_spec.rb new file mode 100755 index 000000000..d51e39dc4 --- /dev/null +++ b/spec/unit/settings/priority_setting_spec.rb @@ -0,0 +1,66 @@ +#!/usr/bin/env ruby +require 'spec_helper' + +require 'puppet/settings' +require 'puppet/settings/priority_setting' +require 'puppet/util/platform' + +describe Puppet::Settings::PrioritySetting do + let(:setting) { described_class.new(:settings => mock('settings'), :desc => "test") } + + it "is of type :priority" do + setting.type.should == :priority + end + + describe "when munging the setting" do + it "passes nil through" do + setting.munge(nil).should be_nil + end + + it "returns the same value if given an integer" do + setting.munge(5).should == 5 + end + + it "returns an integer if given a decimal string" do + setting.munge('12').should == 12 + end + + it "returns a negative integer if given a negative integer string" do + setting.munge('-5').should == -5 + end + + it "fails if given anything else" do + [ 'foo', 'realtime', true, 8.3, [] ].each do |value| + expect { + setting.munge(value) + }.to raise_error(Puppet::Settings::ValidationError) + end + end + + describe "on a Unix-like platform it", :unless => Puppet::Util::Platform.windows? do + it "parses high, normal, low, and idle priorities" do + { + 'high' => -10, + 'normal' => 0, + 'low' => 10, + 'idle' => 19 + }.each do |value, converted_value| + setting.munge(value).should == converted_value + end + end + end + + describe "on a Windows-like platform it", :if => Puppet::Util::Platform.windows? do + it "parses high, normal, low, and idle priorities" do + { + 'high' => Process::HIGH_PRIORITY_CLASS, + 'normal' => Process::NORMAL_PRIORITY_CLASS, + 'low' => Process::BELOW_NORMAL_PRIORITY_CLASS, + 'idle' => Process::IDLE_PRIORITY_CLASS + }.each do |value, converted_value| + setting.munge(value).should == converted_value + end + end + end + end +end diff --git a/spec/unit/settings_spec.rb b/spec/unit/settings_spec.rb index 12107bcb7..b3dc31f3d 100755 --- a/spec/unit/settings_spec.rb +++ b/spec/unit/settings_spec.rb @@ -485,7 +485,7 @@ describe Puppet::Settings do :three => { :default => "$one $two THREE", :desc => "c"}, :four => { :default => "$two $three FOUR", :desc => "d"}, :five => { :default => nil, :desc => "e" } - FileTest.stubs(:exist?).returns true + Puppet::FileSystem::File.stubs(:exist?).returns true end describe "call_on_define" do @@ -589,7 +589,7 @@ describe Puppet::Settings do :config => { :type => :file, :default => "/my/file", :desc => "a" }, :one => { :default => "ONE", :desc => "a" }, :two => { :default => "TWO", :desc => "b" } - FileTest.stubs(:exist?).returns true + Puppet::FileSystem::File.stubs(:exist?).returns true @settings.preferred_run_mode = :agent end @@ -666,8 +666,8 @@ describe Puppet::Settings do describe "when root" do it "should look for the main config file default location config settings haven't been overridden'" do Puppet.features.stubs(:root?).returns(true) - FileTest.expects(:exist?).with(main_config_file_default_location).returns(false) - FileTest.expects(:exist?).with(user_config_file_default_location).never + Puppet::FileSystem::File.expects(:exist?).with(main_config_file_default_location).returns(false) + Puppet::FileSystem::File.expects(:exist?).with(user_config_file_default_location).never @settings.send(:parse_config_files) end @@ -678,7 +678,7 @@ describe Puppet::Settings do Puppet.features.stubs(:root?).returns(false) seq = sequence "load config files" - FileTest.expects(:exist?).with(user_config_file_default_location).returns(false).in_sequence(seq) + Puppet::FileSystem::File.expects(:exist?).with(user_config_file_default_location).returns(false).in_sequence(seq) @settings.send(:parse_config_files) end @@ -699,8 +699,8 @@ describe Puppet::Settings do :two => { :default => "$one TWO", :desc => "b" }, :three => { :default => "$one $two THREE", :desc => "c" } @settings.stubs(:user_config_file).returns(@userconfig) - FileTest.stubs(:exist?).with(@file).returns true - FileTest.stubs(:exist?).with(@userconfig).returns false + Puppet::FileSystem::File.stubs(:exist?).with(@file).returns true + Puppet::FileSystem::File.stubs(:exist?).with(@userconfig).returns false end it "should not ignore the report setting" do @@ -712,7 +712,7 @@ describe Puppet::Settings do [puppetd] report=true CONF - FileTest.expects(:exist?).with(myfile).returns(true) + Puppet::FileSystem::File.expects(:exist?).with(myfile).returns(true) @settings.expects(:read_file).returns(text) @settings.send(:parse_config_files) @settings[:report].should be_true @@ -722,7 +722,7 @@ describe Puppet::Settings do myfile = make_absolute("/my/file") # do not stub expand_path here, as this leads to a stack overflow, when mocha tries to use it @settings[:config] = myfile - FileTest.expects(:exist?).with(myfile).returns(true) + Puppet::FileSystem::File.expects(:exist?).with(myfile).returns(true) File.expects(:read).with(myfile).returns "[main]" @@ -730,7 +730,7 @@ describe Puppet::Settings do end it "should not try to parse non-existent files" do - FileTest.expects(:exist?).with(@file).returns false + Puppet::FileSystem::File.expects(:exist?).with(@file).returns false File.expects(:read).with(@file).never @@ -881,6 +881,33 @@ describe Puppet::Settings do values.should == ["yay/setval"] end + it "should allow hooks invoked at parse time to be deferred" do + hook_invoked = false + @settings.define_settings :section, :deferred => {:desc => '', + :hook => proc { |v| hook_invoked = true }, + :call_hook => :on_initialize_and_write, } + + @settings.define_settings(:main, + :logdir => { :type => :directory, :default => nil, :desc => "logdir" }, + :confdir => { :type => :directory, :default => nil, :desc => "confdir" }, + :vardir => { :type => :directory, :default => nil, :desc => "vardir" }) + + text = <<-EOD + [main] + deferred=$confdir/goose + EOD + + @settings.stubs(:read_file).returns(text) + @settings.initialize_global_settings + + hook_invoked.should be_false + + @settings.initialize_app_defaults(:logdir => '/path/to/logdir', :confdir => '/path/to/confdir', :vardir => '/path/to/vardir') + + hook_invoked.should be_true + @settings[:deferred].should eq File.expand_path('/path/to/confdir/goose') + end + it "should allow empty values" do @settings.define_settings :section, :myarg => { :default => "myfile", :desc => "a" } @@ -925,7 +952,7 @@ describe Puppet::Settings do context "running non-root without explicit config file" do before :each do Puppet.features.stubs(:root?).returns(false) - FileTest.expects(:exist?). + Puppet::FileSystem::File.expects(:exist?). with(user_config_file_default_location). returns(true).in_sequence(seq) @settings.expects(:read_file). @@ -947,7 +974,7 @@ describe Puppet::Settings do context "running as root without explicit config file" do before :each do Puppet.features.stubs(:root?).returns(true) - FileTest.expects(:exist?). + Puppet::FileSystem::File.expects(:exist?). with(main_config_file_default_location). returns(true).in_sequence(seq) @settings.expects(:read_file). @@ -970,7 +997,7 @@ describe Puppet::Settings do before :each do Puppet.features.stubs(:root?).returns(false) @settings[:confdir] = File.dirname(main_config_file_default_location) - FileTest.expects(:exist?). + Puppet::FileSystem::File.expects(:exist?). with(main_config_file_default_location). returns(true).in_sequence(seq) @settings.expects(:read_file). @@ -1000,13 +1027,13 @@ describe Puppet::Settings do :one => { :default => "ONE", :desc => "a" }, :two => { :default => "$one TWO", :desc => "b" }, :three => { :default => "$one $two THREE", :desc => "c" } - FileTest.stubs(:exist?).with(@file).returns true - FileTest.stubs(:exist?).with(@userconfig).returns false + Puppet::FileSystem::File.stubs(:exist?).with(@file).returns true + Puppet::FileSystem::File.stubs(:exist?).with(@userconfig).returns false @settings.stubs(:user_config_file).returns(@userconfig) end it "does not create the WatchedFile instance and should not parse if the file does not exist" do - FileTest.expects(:exist?).with(@file).returns false + Puppet::FileSystem::File.expects(:exist?).with(@file).returns false Puppet::Util::WatchedFile.expects(:new).never @settings.expects(:parse_config_files).never @@ -1285,10 +1312,6 @@ describe Puppet::Settings do @settings.define_settings :files, :myfile => {:type => :file, :default => make_absolute("/myfile"), :desc => "a", :mode => 0755} end - it "should provide a method that writes files with the correct modes" do - @settings.should respond_to(:write) - end - it "should provide a method that creates directories with the correct modes" do Puppet::Util::SUIDManager.expects(:asuser).with("suser", "sgroup").yields Dir.expects(:mkdir).with(make_absolute("/otherdir"), 0755) @@ -1567,17 +1590,6 @@ describe Puppet::Settings do end end - describe "#writesub" do - it "should only pass valid arguments to File.open" do - settings = Puppet::Settings.new - settings.stubs(:get_config_file_default).with(:privatekeydir).returns(OpenStruct.new(:mode => "750")) - - File.expects(:open).with("/path/to/keydir", "w", 750).returns true - settings.writesub(:privatekeydir, "/path/to/keydir") - end - end - - describe "when dealing with command-line options" do let(:settings) { Puppet::Settings.new } diff --git a/spec/unit/ssl/certificate_authority/autosign_command_spec.rb b/spec/unit/ssl/certificate_authority/autosign_command_spec.rb new file mode 100644 index 000000000..5792bb4f5 --- /dev/null +++ b/spec/unit/ssl/certificate_authority/autosign_command_spec.rb @@ -0,0 +1,30 @@ +require 'spec_helper' + +require 'puppet/ssl/certificate_authority/autosign_command' + +describe Puppet::SSL::CertificateAuthority::AutosignCommand do + + let(:csr) { stub 'csr', :name => 'host', :to_s => 'CSR PEM goes here' } + let(:decider) { Puppet::SSL::CertificateAuthority::AutosignCommand.new('/autosign/command') } + + it "returns true if the command succeeded" do + executes_the_command_resulting_in(0) + + decider.allowed?(csr).should == true + end + + it "returns false if the command failed" do + executes_the_command_resulting_in(1) + + decider.allowed?(csr).should == false + end + + def executes_the_command_resulting_in(exitstatus) + Puppet::Util::Execution.expects(:execute). + with(['/autosign/command', 'host'], + has_entries(:stdinfile => anything, + :combine => true, + :failonfail => false)). + returns(Puppet::Util::Execution::ProcessOutput.new('', exitstatus)) + end +end diff --git a/spec/unit/ssl/certificate_authority_spec.rb b/spec/unit/ssl/certificate_authority_spec.rb index 70186a137..13c169a0a 100755 --- a/spec/unit/ssl/certificate_authority_spec.rb +++ b/spec/unit/ssl/certificate_authority_spec.rb @@ -92,12 +92,6 @@ describe Puppet::SSL::CertificateAuthority do Puppet::SSL::CertificateAuthority.new end - it "should create an inventory instance" do - Puppet::SSL::Inventory.expects(:new).returns "inventory" - - Puppet::SSL::CertificateAuthority.new.inventory.should == "inventory" - end - it "should make sure the CA is set up" do Puppet::SSL::CertificateAuthority.any_instance.expects(:setup) @@ -171,16 +165,16 @@ describe Puppet::SSL::CertificateAuthority do it "should create and store a password at :capass" do Puppet[:capass] = File.expand_path("/path/to/pass") - FileTest.expects(:exist?).with(Puppet[:capass]).returns false + Puppet::FileSystem::File.expects(:exist?).with(Puppet[:capass]).returns false - fh = mock 'filehandle' - Puppet.settings.expects(:write).with(:capass).yields fh - - fh.expects(:print).with { |s| s.length > 18 } + fh = StringIO.new + Puppet.settings.setting(:capass).expects(:open).with('w').yields fh @ca.stubs(:sign) @ca.generate_ca_certificate + + expect(fh.string.length).to be > 18 end it "should generate a key if one does not exist" do @@ -238,11 +232,10 @@ describe Puppet::SSL::CertificateAuthority do Puppet::SSL::Certificate.stubs(:new).returns @cert - @cert.stubs(:content=) Puppet::SSL::Certificate.indirection.stubs(:save) # Stub out the factory - Puppet::SSL::CertificateFactory.stubs(:build).returns "my real cert" + Puppet::SSL::CertificateFactory.stubs(:build).returns @cert.content @request_content = stub "request content stub", :subject => OpenSSL::X509::Name.new([['CN', @name]]), :public_key => stub('public_key') @request = stub 'request', :name => @name, :request_extensions => [], :subject_alt_names => [], :content => @request_content @@ -255,39 +248,6 @@ describe Puppet::SSL::CertificateAuthority do Puppet::SSL::CertificateRequest.indirection.stubs(:destroy) end - describe "and calculating the next certificate serial number" do - before do - @path = File.expand_path("/path/to/serial") - Puppet[:serial] = @path - - @filehandle = stub 'filehandle', :<< => @filehandle - Puppet.settings.stubs(:readwritelock).with(:serial).yields @filehandle - end - - it "should default to 0x1 for the first serial number" do - @ca.next_serial.should == 0x1 - end - - it "should return the current content of the serial file" do - FileTest.stubs(:exist?).with(@path).returns true - File.expects(:read).with(@path).returns "0002" - - @ca.next_serial.should == 2 - end - - it "should write the next serial number to the serial file as hex" do - @filehandle.expects(:<<).with("0002") - - @ca.next_serial - end - - it "should lock the serial file while writing" do - Puppet.settings.expects(:readwritelock).with(:serial) - - @ca.next_serial - end - end - describe "its own certificate" do before do @serial = 10 @@ -303,28 +263,28 @@ describe Puppet::SSL::CertificateAuthority do it "should use a certificate type of :ca" do Puppet::SSL::CertificateFactory.expects(:build).with do |*args| args[0].should == :ca - end.returns "my real cert" + end.returns @cert.content @ca.sign(@name, :ca, @request) end it "should pass the provided CSR as the CSR" do Puppet::SSL::CertificateFactory.expects(:build).with do |*args| args[1].should == @request - end.returns "my real cert" + end.returns @cert.content @ca.sign(@name, :ca, @request) end it "should use the provided CSR's content as the issuer" do Puppet::SSL::CertificateFactory.expects(:build).with do |*args| args[2].subject.to_s.should == "/CN=myhost" - end.returns "my real cert" + end.returns @cert.content @ca.sign(@name, :ca, @request) end it "should pass the next serial as the serial number" do Puppet::SSL::CertificateFactory.expects(:build).with do |*args| args[3].should == @serial - end.returns "my real cert" + end.returns @cert.content @ca.sign(@name, :ca, @request) end @@ -355,7 +315,7 @@ describe Puppet::SSL::CertificateAuthority do it "should use a certificate type of :server" do Puppet::SSL::CertificateFactory.expects(:build).with do |*args| args[0] == :server - end.returns "my real cert" + end.returns @cert.content @ca.sign(@name) end @@ -404,14 +364,14 @@ describe Puppet::SSL::CertificateAuthority do it "should use the CA certificate as the issuer" do Puppet::SSL::CertificateFactory.expects(:build).with do |*args| args[2] == @cacert.content - end.returns "my real cert" - @ca.sign(@name) + end.returns @cert.content + signed = @ca.sign(@name) end it "should pass the next serial as the serial number" do Puppet::SSL::CertificateFactory.expects(:build).with do |*args| args[3] == @serial - end.returns "my real cert" + end.returns @cert.content @ca.sign(@name) end @@ -518,6 +478,40 @@ describe Puppet::SSL::CertificateAuthority do end end + it "accepts numeric OIDs under the ppRegCertExt subtree" do + exts = [{ 'oid' => '1.3.6.1.4.1.34380.1.1.1', + 'value' => '657e4780-4cf5-11e3-8f96-0800200c9a66'}] + + @request.stubs(:request_extensions).returns exts + + expect { + @ca.check_internal_signing_policies(@name, @request, false) + }.to_not raise_error + end + + it "accepts short name OIDs under the ppRegCertExt subtree" do + exts = [{ 'oid' => 'pp_uuid', + 'value' => '657e4780-4cf5-11e3-8f96-0800200c9a66'}] + + @request.stubs(:request_extensions).returns exts + + expect { + @ca.check_internal_signing_policies(@name, @request, false) + }.to_not raise_error + end + + it "accepts OIDs under the ppPrivCertAttrs subtree" do + exts = [{ 'oid' => '1.3.6.1.4.1.34380.1.2.1', + 'value' => 'private extension'}] + + @request.stubs(:request_extensions).returns exts + + expect { + @ca.check_internal_signing_policies(@name, @request, false) + }.to_not raise_error + end + + it "should reject a critical extension that isn't on the whitelist" do @request.stubs(:request_extensions).returns [{ "oid" => "banana", "value" => "yumm", @@ -610,76 +604,104 @@ describe Puppet::SSL::CertificateAuthority do end describe "when autosigning certificates" do - let(:autosign) { File.expand_path("/auto/sign") } - it "should do nothing if autosign is disabled" do - Puppet[:autosign] = 'false' + let(:csr) { Puppet::SSL::CertificateRequest.new("host") } - Puppet::SSL::CertificateRequest.indirection.expects(:search).never - @ca.autosign - end + describe "using the autosign setting" do + let(:autosign) { File.expand_path("/auto/sign") } - it "should do nothing if no autosign.conf exists" do - Puppet[:autosign] = autosign - FileTest.expects(:exist?).with(autosign).returns false + it "should do nothing if autosign is disabled" do + Puppet[:autosign] = false - Puppet::SSL::CertificateRequest.indirection.expects(:search).never - @ca.autosign - end + @ca.expects(:sign).never + @ca.autosign(csr) + end - describe "and autosign is enabled and the autosign.conf file exists" do - before do + it "should do nothing if no autosign.conf exists" do Puppet[:autosign] = autosign - FileTest.stubs(:exist?).with(autosign).returns true - File.stubs(:readlines).with(autosign).returns ["one\n", "two\n"] + non_existent_file = Puppet::FileSystem::MemoryFile.a_missing_file(autosign) + Puppet::FileSystem::File.overlay(non_existent_file) do + @ca.expects(:sign).never + @ca.autosign(csr) + end + end - Puppet::SSL::CertificateRequest.indirection.stubs(:search).returns [] + describe "and autosign is enabled and the autosign.conf file exists" do + let(:store) { stub 'store', :allow => nil, :allowed? => false } - @store = stub 'store', :allow => nil - Puppet::Network::AuthStore.stubs(:new).returns @store - end + before do + Puppet[:autosign] = autosign + end - describe "when creating the AuthStore instance to verify autosigning" do - it "should create an AuthStore with each line in the configuration file allowed to be autosigned" do - Puppet::Network::AuthStore.expects(:new).returns @store + describe "when creating the AuthStore instance to verify autosigning" do + it "should create an AuthStore with each line in the configuration file allowed to be autosigned" do + Puppet::FileSystem::File.overlay(Puppet::FileSystem::MemoryFile.a_regular_file_containing(autosign, "one\ntwo\n")) do + Puppet::Network::AuthStore.stubs(:new).returns store - @store.expects(:allow).with("one") - @store.expects(:allow).with("two") + store.expects(:allow).with("one") + store.expects(:allow).with("two") - @ca.autosign - end + @ca.autosign(csr) + end + end - it "should reparse the autosign configuration on each call" do - Puppet::Network::AuthStore.expects(:new).times(2).returns @store + it "should reparse the autosign configuration on each call" do + Puppet::FileSystem::File.overlay(Puppet::FileSystem::MemoryFile.a_regular_file_containing(autosign, "one")) do + Puppet::Network::AuthStore.stubs(:new).times(2).returns store - @ca.autosign - @ca.autosign - end + @ca.autosign(csr) + @ca.autosign(csr) + end + end - it "should ignore comments" do - File.stubs(:readlines).with(autosign).returns ["one\n", "#two\n"] + it "should ignore comments" do + Puppet::FileSystem::File.overlay(Puppet::FileSystem::MemoryFile.a_regular_file_containing(autosign, "one\n#two\n")) do + Puppet::Network::AuthStore.stubs(:new).returns store - @store.expects(:allow).with("one") - @ca.autosign - end + store.expects(:allow).with("one") - it "should ignore blank lines" do - File.stubs(:readlines).with(autosign).returns ["one\n", "\n"] + @ca.autosign(csr) + end + end - @store.expects(:allow).with("one") - @ca.autosign + it "should ignore blank lines" do + Puppet::FileSystem::File.overlay(Puppet::FileSystem::MemoryFile.a_regular_file_containing(autosign, "one\n\n")) do + Puppet::Network::AuthStore.stubs(:new).returns store + + store.expects(:allow).with("one") + @ca.autosign(csr) + end + end end end + end + + describe "using the autosign command setting" do + let(:cmd) { File.expand_path('/autosign_cmd') } + let(:autosign_cmd) { mock 'autosign_command' } + let(:autosign_executable) { Puppet::FileSystem::MemoryFile.an_executable(cmd) } + + before do + Puppet[:autosign] = cmd + + Puppet::SSL::CertificateAuthority::AutosignCommand.stubs(:new).returns autosign_cmd + end - it "should sign all CSRs whose hostname matches the autosign configuration" do - csr1 = mock 'csr1' - csr2 = mock 'csr2' - Puppet::SSL::CertificateRequest.indirection.stubs(:search).returns [csr1, csr2] + it "autosigns the CSR if the autosign command returned true" do + Puppet::FileSystem::File.overlay(autosign_executable) do + autosign_cmd.expects(:allowed?).with(csr).returns true + + @ca.expects(:sign).with('host') + @ca.autosign(csr) + end end - it "should not sign CSRs whose hostname does not match the autosign configuration" do - csr1 = mock 'csr1' - csr2 = mock 'csr2' - Puppet::SSL::CertificateRequest.indirection.stubs(:search).returns [csr1, csr2] + it "doesn't autosign the CSR if the autosign_command returned false" do + Puppet::FileSystem::File.overlay(autosign_executable) do + autosign_cmd.expects(:allowed?).with(csr).returns false + + @ca.expects(:sign).never + @ca.autosign(csr) + end end end end @@ -701,28 +723,6 @@ describe Puppet::SSL::CertificateAuthority do @ca = Puppet::SSL::CertificateAuthority.new end - it "should have a method for acting on the SSL files" do - @ca.should respond_to(:apply) - end - - describe "when applying a method to a set of hosts" do - it "should fail if no subjects have been specified" do - expect { @ca.apply(:generate) }.to raise_error(ArgumentError) - end - - it "should create an Interface instance with the specified method and the options" do - Puppet::SSL::CertificateAuthority::Interface.expects(:new).with(:generate, :to => :host).returns(stub('applier', :apply => nil)) - @ca.apply(:generate, :to => :host) - end - - it "should apply the Interface with itself as the argument" do - applier = stub('applier') - applier.expects(:apply).with(@ca) - Puppet::SSL::CertificateAuthority::Interface.expects(:new).returns applier - @ca.apply(:generate, :to => :ca_testing) - end - end - it "should be able to list waiting certificate requests" do req1 = stub 'req1', :name => "one" req2 = stub 'req2', :name => "two" @@ -979,16 +979,15 @@ require 'puppet/indirector/memory' describe "CertificateAuthority.generate" do def expect_to_increment_serial_file - Puppet.settings.expects(:readwritelock).with(:serial) + Puppet.settings.setting(:serial).expects(:exclusive_open) end def expect_to_sign_a_cert expect_to_increment_serial_file - Puppet.settings.expects(:write).with(:cert_inventory, "a") end def expect_to_write_the_ca_password - Puppet.settings.expects(:write).with(:capass) + Puppet.settings.setting(:capass).expects(:open).with('w') end def expect_ca_initialization @@ -996,10 +995,6 @@ describe "CertificateAuthority.generate" do expect_to_sign_a_cert end - def avoid_rebuilding_inventory_for_every_cert - Puppet::SSL::Inventory.any_instance.stubs(:rebuild) - end - INDIRECTED_CLASSES = [ Puppet::SSL::Certificate, Puppet::SSL::CertificateRequest, @@ -1021,7 +1016,7 @@ describe "CertificateAuthority.generate" do end before do - avoid_rebuilding_inventory_for_every_cert + Puppet::SSL::Inventory.stubs(:new).returns(stub("Inventory", :add => nil)) INDIRECTED_CLASSES.each { |const| const.indirection.terminus_class = :memory } end @@ -1036,7 +1031,7 @@ describe "CertificateAuthority.generate" do let(:ca) { Puppet::SSL::CertificateAuthority.new } before do - expect_ca_initialization + expect_ca_initialization end it "should fail if a certificate already exists for the host" do diff --git a/spec/unit/ssl/certificate_factory_spec.rb b/spec/unit/ssl/certificate_factory_spec.rb index 85609593a..fa436edcf 100755 --- a/spec/unit/ssl/certificate_factory_spec.rb +++ b/spec/unit/ssl/certificate_factory_spec.rb @@ -115,6 +115,24 @@ describe Puppet::SSL::CertificateFactory do end end + it "can add custom extension requests" do + csr = Puppet::SSL::CertificateRequest.new(name) + csr.generate(key) + + csr.stubs(:request_extensions).returns([ + {'oid' => '1.3.6.1.4.1.34380.1.2.1', 'value' => 'some-value'}, + {'oid' => 'pp_uuid', 'value' => 'some-uuid'}, + ]) + + cert = subject.build(:client, csr, issuer, serial) + + priv_ext = cert.extensions.find {|ext| ext.oid == '1.3.6.1.4.1.34380.1.2.1'} + uuid_ext = cert.extensions.find {|ext| ext.oid == 'pp_uuid'} + + expect(priv_ext.value).to eq 'some-value' + expect(uuid_ext.value).to eq 'some-uuid' + end + # Can't check the CA here, since that requires way more infrastructure # that I want to build up at this time. We can verify the critical # values, though, which are non-CA certs. --daniel 2011-10-11 diff --git a/spec/unit/ssl/certificate_request_attributes_spec.rb b/spec/unit/ssl/certificate_request_attributes_spec.rb new file mode 100644 index 000000000..6165330aa --- /dev/null +++ b/spec/unit/ssl/certificate_request_attributes_spec.rb @@ -0,0 +1,61 @@ +require 'spec_helper' + +require 'puppet/ssl/certificate_request_attributes' + +describe Puppet::SSL::CertificateRequestAttributes do + + let(:expected) do + { + "custom_attributes" => { + "1.3.6.1.4.1.34380.2.2"=>[3232235521, 3232235777], # system IPs in hex + "1.3.6.1.4.1.34380.2.0"=>"hostname.domain.com", + } + } + end + let(:csr_attributes_hash) { expected.dup } + let(:csr_attributes_path) { '/some/where/csr_attributes.yaml' } + let(:csr_attributes) { Puppet::SSL::CertificateRequestAttributes.new(csr_attributes_path) } + + it "initializes with a path" do + expect(csr_attributes.path).to eq(csr_attributes_path) + end + + describe "loading" do + it "returns nil when loading from a non-existent file" do + expect(csr_attributes.load).to be_false + end + + context "with an available attributes file" do + before do + Puppet::FileSystem::File.expects(:exist?).with(csr_attributes_path).returns(true) + Puppet::Util::Yaml.expects(:load_file).with(csr_attributes_path, {}).returns(csr_attributes_hash) + end + + it "loads csr attributes from a file when the file is present" do + expect(csr_attributes.load).to be_true + end + + it "exposes custom_attributes" do + csr_attributes.load + expect(csr_attributes.custom_attributes).to eq(expected['custom_attributes']) + end + + it "returns an empty hash if custom_attributes points to nil" do + csr_attributes_hash["custom_attributes"] = nil + csr_attributes.load + expect(csr_attributes.custom_attributes).to eq({}) + end + + it "returns an empty hash if custom_attributes key is not present" do + csr_attributes_hash.delete("custom_attributes") + csr_attributes.load + expect(csr_attributes.custom_attributes).to eq({}) + end + + it "raise a Puppet::Error if an unexpected root key is defined" do + csr_attributes_hash['unintentional'] = 'data' + expect { csr_attributes.load }.to raise_error(Puppet::Error, /unexpected attributes.*unintentional/) + end + end + end +end diff --git a/spec/unit/ssl/certificate_request_spec.rb b/spec/unit/ssl/certificate_request_spec.rb index 1dc449d53..192c929b5 100755 --- a/spec/unit/ssl/certificate_request_spec.rb +++ b/spec/unit/ssl/certificate_request_spec.rb @@ -178,6 +178,109 @@ describe Puppet::SSL::CertificateRequest do end end + context "with custom CSR attributes" do + + it "adds attributes with single values" do + csr_attributes = { + '1.3.6.1.4.1.34380.1.2.1' => 'CSR specific info', + '1.3.6.1.4.1.34380.1.2.2' => 'more CSR specific info', + } + + request.generate(key, :csr_attributes => csr_attributes) + + attrs = request.custom_attributes + attrs.should include({'oid' => '1.3.6.1.4.1.34380.1.2.1', 'value' => 'CSR specific info'}) + attrs.should include({'oid' => '1.3.6.1.4.1.34380.1.2.2', 'value' => 'more CSR specific info'}) + end + + ['extReq', '1.2.840.113549.1.9.14'].each do |oid| + it "doesn't overwrite standard PKCS#9 CSR attribute '#{oid}'" do + expect do + request.generate(key, :csr_attributes => {oid => 'data'}) + end.to raise_error ArgumentError, /Cannot specify.*#{oid}/ + end + end + + ['msExtReq', '1.3.6.1.4.1.311.2.1.14'].each do |oid| + it "doesn't overwrite Microsoft extension request OID '#{oid}'" do + expect do + request.generate(key, :csr_attributes => {oid => 'data'}) + end.to raise_error ArgumentError, /Cannot specify.*#{oid}/ + end + end + + it "raises an error if an attribute cannot be created" do + csr_attributes = { "thats.no.moon" => "death star" } + + expect do + request.generate(key, :csr_attributes => csr_attributes) + end.to raise_error Puppet::Error, /Cannot create CSR with attribute thats\.no\.moon: first num too large/ + end + end + + context "with extension requests" do + let(:extension_data) do + { + '1.3.6.1.4.1.34380.1.1.31415' => 'pi', + '1.3.6.1.4.1.34380.1.1.2718' => 'e', + } + end + + it "adds an extreq attribute to the CSR" do + request.generate(key, :extension_requests => extension_data) + + exts = request.content.attributes.select { |attr| attr.oid = 'extReq' } + exts.length.should == 1 + end + + it "adds an extension for each entry in the extension request structure" do + request.generate(key, :extension_requests => extension_data) + + exts = request.request_extensions + + exts.should include('oid' => '1.3.6.1.4.1.34380.1.1.31415', 'value' => 'pi') + exts.should include('oid' => '1.3.6.1.4.1.34380.1.1.2718', 'value' => 'e') + end + + it "defines the extensions as non-critical" do + request.generate(key, :extension_requests => extension_data) + request.request_extensions.each do |ext| + ext['critical'].should be_false + end + end + + it "rejects the subjectAltNames extension" do + san_names = ['subjectAltName', '2.5.29.17'] + san_field = 'DNS:first.tld, DNS:second.tld' + + san_names.each do |name| + expect do + request.generate(key, :extension_requests => {name => san_field}) + end.to raise_error Puppet::Error, /conflicts with internally used extension/ + end + end + + it "merges the extReq attribute with the subjectAltNames extension" do + request.generate(key, + :dns_alt_names => 'first.tld, second.tld', + :extension_requests => extension_data) + exts = request.request_extensions + + exts.should include('oid' => '1.3.6.1.4.1.34380.1.1.31415', 'value' => 'pi') + exts.should include('oid' => '1.3.6.1.4.1.34380.1.1.2718', 'value' => 'e') + exts.should include('oid' => 'subjectAltName', 'value' => 'DNS:first.tld, DNS:myname, DNS:second.tld') + + request.subject_alt_names.should eq ['DNS:first.tld', 'DNS:myname', 'DNS:second.tld'] + end + + it "raises an error if the OID could not be created" do + exts = {"thats.no.moon" => "death star"} + expect do + request.generate(key, :extension_requests => exts) + end.to raise_error Puppet::Error, /Cannot create CSR with extension request thats\.no\.moon: first num too large/ + end + end + it "should sign the csr with the provided key" do request.generate(key) request.content.verify(key.content.public_key).should be_true diff --git a/spec/unit/ssl/certificate_spec.rb b/spec/unit/ssl/certificate_spec.rb index 70d35d4c7..c8f9930f3 100755 --- a/spec/unit/ssl/certificate_spec.rb +++ b/spec/unit/ssl/certificate_spec.rb @@ -75,6 +75,17 @@ describe Puppet::SSL::Certificate do end describe "when managing instances" do + + def build_cert(opts) + key = Puppet::SSL::Key.new('quux') + key.generate + csr = Puppet::SSL::CertificateRequest.new('quux') + csr.generate(key, opts) + + raw_cert = Puppet::SSL::CertificateFactory.build('client', csr, csr.content, 14) + @class.from_instance(raw_cert) + end + before do @certificate = @class.new("myname") end @@ -93,33 +104,35 @@ describe Puppet::SSL::Certificate do describe "#subject_alt_names" do it "should list all alternate names when the extension is present" do - key = Puppet::SSL::Key.new('quux') - key.generate - - csr = Puppet::SSL::CertificateRequest.new('quux') - csr.generate(key, :dns_alt_names => 'foo, bar,baz') - - raw_csr = csr.content - - cert = Puppet::SSL::CertificateFactory.build('server', csr, raw_csr, 14) - certificate = @class.from_s(cert.to_pem) + certificate = build_cert(:dns_alt_names => 'foo, bar,baz') certificate.subject_alt_names. should =~ ['DNS:foo', 'DNS:bar', 'DNS:baz', 'DNS:quux'] end it "should return an empty list of names if the extension is absent" do - key = Puppet::SSL::Key.new('quux') - key.generate + certificate = build_cert({}) + certificate.subject_alt_names.should be_empty + end + end - csr = Puppet::SSL::CertificateRequest.new('quux') - csr.generate(key) + describe "custom extensions" do + it "returns extensions under the ppRegCertExt" do + exts = {'pp_uuid' => 'abcdfd'} + cert = build_cert(:extension_requests => exts) + expect(cert.custom_extensions).to include('oid' => 'pp_uuid', 'value' => 'abcdfd') + end - raw_csr = csr.content + it "returns extensions under the ppPrivCertExt" do + exts = {'1.3.6.1.4.1.34380.1.2.1' => 'x509 :('} + cert = build_cert(:extension_requests => exts) + expect(cert.custom_extensions).to include('oid' => '1.3.6.1.4.1.34380.1.2.1', 'value' => 'x509 :(') + end - cert = Puppet::SSL::CertificateFactory.build('client', csr, raw_csr, 14) - certificate = @class.from_s(cert.to_pem) - certificate.subject_alt_names.should be_empty + it "doesn't return standard extensions" do + cert = build_cert(:dns_alt_names => 'foo') + expect(cert.custom_extensions).to be_empty end + end it "should return a nil expiration if there is no actual certificate" do diff --git a/spec/unit/ssl/host_spec.rb b/spec/unit/ssl/host_spec.rb index 8240991c7..3b341cc4e 100755 --- a/spec/unit/ssl/host_spec.rb +++ b/spec/unit/ssl/host_spec.rb @@ -9,9 +9,24 @@ def base_pson_comparison(result, pson_hash) result["state"].should == pson_hash["desired_state"] end +# the json-schema gem doesn't support windows +if not Puppet.features.microsoft_windows? + HOST_SCHEMA = JSON.parse(File.read(File.join(File.dirname(__FILE__), '../../../api/schemas/host.json'))) + + describe "host schema" do + it "should validate against the json meta-schema" do + JSON::Validator.validate!(JSON_META_SCHEMA, HOST_SCHEMA) + end + end +end + describe Puppet::SSL::Host do include PuppetSpec::Files + def validate_json_for_host(host) + JSON::Validator.validate!(HOST_SCHEMA, host.to_pson) + end + before do Puppet::SSL::Host.indirection.terminus_class = :file @@ -823,7 +838,7 @@ describe Puppet::SSL::Host do let(:host) do Puppet::SSL::Host.new("bazinga") end - + let(:pson_hash) do { "fingerprint" => host.certificate_request.fingerprint, @@ -831,15 +846,20 @@ describe Puppet::SSL::Host do "name" => host.name } end - + it "should be able to identify a host with an unsigned certificate request" do host.generate_certificate_request result = PSON.parse(Puppet::SSL::Host.new(host.name).to_pson) - + base_pson_comparison result, pson_hash end - + + it "should validate against the schema", :unless => Puppet.features.microsoft_windows? do + host.generate_certificate_request + validate_json_for_host(host) + end + describe "explicit fingerprints" do [:SHA1, :SHA256, :SHA512].each do |md| it "should include #{md}" do @@ -854,7 +874,7 @@ describe Puppet::SSL::Host do end end end - + describe "dns_alt_names" do describe "when not specified" do it "should include the dns_alt_names associated with the certificate" do @@ -867,22 +887,28 @@ describe Puppet::SSL::Host do end end - [ "", + [ "", "test, alt, names" ].each do |alt_names| describe "when #{alt_names}" do - it "should include the dns_alt_names associated with the certificate" do + before(:each) do host.generate_certificate_request :dns_alt_names => alt_names + end + + it "should include the dns_alt_names associated with the certificate" do pson_hash["desired_alt_names"] = host.certificate_request.subject_alt_names result = PSON.parse(Puppet::SSL::Host.new(host.name).to_pson) base_pson_comparison result, pson_hash result["dns_alt_names"].should == pson_hash["desired_alt_names"] end + + it "should validate against the schema", :unless => Puppet.features.microsoft_windows? do + validate_json_for_host(host) + end end end end - it "should be able to identify a host with a signed certificate" do host.generate_certificate_request diff --git a/spec/unit/ssl/inventory_spec.rb b/spec/unit/ssl/inventory_spec.rb index b145cd9ab..9bcbbcea5 100755 --- a/spec/unit/ssl/inventory_spec.rb +++ b/spec/unit/ssl/inventory_spec.rb @@ -20,7 +20,7 @@ describe Puppet::SSL::Inventory, :unless => Puppet.features.microsoft_windows? d before do Puppet[:cert_inventory] = cert_inventory - FileTest.stubs(:exist?).with(cert_inventory).returns true + Puppet::FileSystem::File.stubs(:exist?).with(cert_inventory).returns true @inventory = @class.new @@ -28,86 +28,51 @@ describe Puppet::SSL::Inventory, :unless => Puppet.features.microsoft_windows? d end describe "and creating the inventory file" do - before do - Puppet.settings.stubs(:write) - FileTest.stubs(:exist?).with(cert_inventory).returns false - - Puppet::SSL::Certificate.indirection.stubs(:search).returns [] - end - - it "should log that it is building a new inventory file" do - Puppet.expects(:notice) - - @inventory.rebuild - end - - it "should use the Settings to write to the file" do - Puppet.settings.expects(:write).with(:cert_inventory) - - @inventory.rebuild - end - - it "should add a header to the file" do - fh = mock 'filehandle' - Puppet.settings.stubs(:write).yields fh - fh.expects(:print).with { |str| str =~ /^#/ } - - @inventory.rebuild - end - - it "should add formatted information on all existing certificates" do - cert1 = mock 'cert1' - cert2 = mock 'cert2' - + it "re-adds all of the existing certificates" do + inventory_file = StringIO.new + Puppet.settings.setting(:cert_inventory).stubs(:open).yields(inventory_file) + + cert1 = Puppet::SSL::Certificate.new("cert1") + cert1.content = stub 'cert1', + :serial => 2, + :not_before => Time.now, + :not_after => Time.now, + :subject => "/CN=smocking" + cert2 = Puppet::SSL::Certificate.new("cert2") + cert2.content = stub 'cert2', + :serial => 3, + :not_before => Time.now, + :not_after => Time.now, + :subject => "/CN=mocking bird" Puppet::SSL::Certificate.indirection.expects(:search).with("*").returns [cert1, cert2] - @class.any_instance.expects(:add).with(cert1) - @class.any_instance.expects(:add).with(cert2) - @inventory.rebuild + + expect(inventory_file.string).to match(/\/CN=smocking/) + expect(inventory_file.string).to match(/\/CN=mocking bird/) end end describe "and adding a certificate" do - it "should build the inventory file if one does not exist" do - Puppet[:cert_inventory] = cert_inventory - Puppet.settings.stubs(:write) - - FileTest.expects(:exist?).with(cert_inventory).returns false - - @inventory.expects(:rebuild) - - @inventory.add(@cert) - end it "should use the Settings to write to the file" do - Puppet.settings.expects(:write).with(:cert_inventory, "a") + Puppet.settings.setting(:cert_inventory).expects(:open).with("a") @inventory.add(@cert) end - it "should use the actual certificate if it was passed a Puppet certificate" do + it "should add formatted certificate information to the end of the file" do cert = Puppet::SSL::Certificate.new("mycert") cert.content = @cert - fh = stub 'filehandle', :print => nil - Puppet.settings.stubs(:write).yields fh - - @inventory.expects(:format).with(@cert) - - @inventory.add(@cert) - end - - it "should add formatted certificate information to the end of the file" do - fh = mock 'filehandle' - - Puppet.settings.stubs(:write).yields fh + fh = StringIO.new + Puppet.settings.setting(:cert_inventory).expects(:open).with("a").yields(fh) @inventory.expects(:format).with(@cert).returns "myformat" - fh.expects(:print).with("myformat") - @inventory.add(@cert) + + expect(fh.string).to eq("myformat") end end @@ -152,7 +117,7 @@ describe Puppet::SSL::Inventory, :unless => Puppet.features.microsoft_windows? d describe "and finding a serial number" do it "should return nil if the inventory file is missing" do - FileTest.expects(:exist?).with(cert_inventory).returns false + Puppet::FileSystem::File.expects(:exist?).with(cert_inventory).returns false @inventory.serial(:whatever).should be_nil end diff --git a/spec/unit/ssl/key_spec.rb b/spec/unit/ssl/key_spec.rb index 112505087..4cea5491c 100755 --- a/spec/unit/ssl/key_spec.rb +++ b/spec/unit/ssl/key_spec.rb @@ -71,7 +71,7 @@ describe Puppet::SSL::Key do end it "should not try to use the provided password file if the file does not exist" do - FileTest.stubs(:exist?).returns false + Puppet::FileSystem::File.stubs(:exist?).returns false @key.password_file = "/path/to/password" path = "/my/path" @@ -84,7 +84,7 @@ describe Puppet::SSL::Key do end it "should read the key with the password retrieved from the password file if one is provided" do - FileTest.stubs(:exist?).returns true + Puppet::FileSystem::File.stubs(:exist?).returns true @key.password_file = "/path/to/password" path = "/my/path" @@ -154,7 +154,7 @@ describe Puppet::SSL::Key do describe "with a password file set" do it "should return a nil password if the password file does not exist" do - FileTest.expects(:exist?).with("/path/to/pass").returns false + Puppet::FileSystem::File.expects(:exist?).with("/path/to/pass").returns false File.expects(:read).with("/path/to/pass").never @instance.password_file = "/path/to/pass" @@ -163,7 +163,7 @@ describe Puppet::SSL::Key do end it "should return the contents of the password file as its password" do - FileTest.expects(:exist?).with("/path/to/pass").returns true + Puppet::FileSystem::File.expects(:exist?).with("/path/to/pass").returns true File.expects(:read).with("/path/to/pass").returns "my password" @instance.password_file = "/path/to/pass" diff --git a/spec/unit/ssl/oids_spec.rb b/spec/unit/ssl/oids_spec.rb new file mode 100644 index 000000000..ee95bb942 --- /dev/null +++ b/spec/unit/ssl/oids_spec.rb @@ -0,0 +1,48 @@ +require 'spec_helper' +require 'puppet/ssl/oids' + +describe Puppet::SSL::Oids do + describe "defining application OIDs" do + + { + 'puppetlabs' => '1.3.6.1.4.1.34380', + 'ppCertExt' => '1.3.6.1.4.1.34380.1', + 'ppRegCertExt' => '1.3.6.1.4.1.34380.1.1', + 'pp_uuid' => '1.3.6.1.4.1.34380.1.1.1', + 'pp_instance_id' => '1.3.6.1.4.1.34380.1.1.2', + 'pp_image_name' => '1.3.6.1.4.1.34380.1.1.3', + 'pp_preshared_key' => '1.3.6.1.4.1.34380.1.1.4', + 'ppPrivCertExt' => '1.3.6.1.4.1.34380.1.2', + }.each_pair do |sn, oid| + it "defines #{sn} as #{oid}" do + object_id = OpenSSL::ASN1::ObjectId.new(sn) + expect(object_id.oid).to eq oid + end + end + end + + describe "checking if an OID is a subtree of another OID" do + + it "can determine if an OID is contained in another OID" do + described_class.subtree_of?('1.3.6.1', '1.3.6.1.4.1').should be_true + described_class.subtree_of?('1.3.6.1.4.1', '1.3.6.1').should be_false + end + + it "returns true if an OID is compared against itself and exclusive is false" do + described_class.subtree_of?('1.3.6.1', '1.3.6.1', false).should be_true + end + + it "returns false if an OID is compared against itself and exclusive is true" do + described_class.subtree_of?('1.3.6.1', '1.3.6.1', true).should be_false + end + + it "can compare OIDs defined as short names" do + described_class.subtree_of?('IANA', '1.3.6.1.4.1').should be_true + described_class.subtree_of?('1.3.6.1', 'enterprises').should be_true + end + + it "returns false when an invalid OID shortname is passed" do + described_class.subtree_of?('IANA', 'bananas').should be_false + end + end +end diff --git a/spec/unit/ssl/validator_spec.rb b/spec/unit/ssl/validator_spec.rb index fe0904cb8..2b8cfb0f9 100644 --- a/spec/unit/ssl/validator_spec.rb +++ b/spec/unit/ssl/validator_spec.rb @@ -1,8 +1,8 @@ require 'spec_helper' -require 'puppet/ssl/validator' +require 'puppet/ssl' require 'puppet/ssl/configuration' -describe Puppet::SSL::Validator do +describe Puppet::SSL::Validator::DefaultValidator do let(:ssl_context) do mock('OpenSSL::X509::StoreContext') end @@ -14,8 +14,16 @@ describe Puppet::SSL::Validator do :ca_auth_file => Puppet[:ssl_client_ca_auth]) end + let(:ssl_host) do + stub('ssl_host', + :ssl_store => nil, + :certificate => stub('cert', :content => nil), + :key => stub('key', :content => nil)) + end + subject do - described_class.new(:ssl_configuration => ssl_configuration) + described_class.new(ssl_configuration, + ssl_host) end before :each do @@ -49,17 +57,20 @@ describe Puppet::SSL::Validator do before :each do ssl_context.stubs(:error_string).returns("Something went wrong.") end + it 'does not make the error available via #verify_errors' do subject.call(true, ssl_context) subject.verify_errors.should == [] end end + context 'and the chain is valid' do it 'is true for each CA certificate in the chain' do (cert_chain.length - 1).times do subject.call(true, ssl_context).should be_true end end + it 'is true for the SSL certificate ending the chain' do (cert_chain.length - 1).times do subject.call(true, ssl_context) @@ -67,17 +78,20 @@ describe Puppet::SSL::Validator do subject.call(true, ssl_context).should be_true end end + context 'and the chain is invalid' do before :each do ssl_configuration.stubs(:read_file). with(Puppet[:localcacert]). returns(agent_ca) end + it 'is true for each CA certificate in the chain' do (cert_chain.length - 1).times do subject.call(true, ssl_context).should be_true end end + it 'is false for the SSL certificate ending the chain' do (cert_chain.length - 1).times do subject.call(true, ssl_context) @@ -85,13 +99,16 @@ describe Puppet::SSL::Validator do subject.call(true, ssl_context).should be_false end end + context 'an error is raised inside of #call' do before :each do ssl_context.expects(:current_cert).raises(StandardError, "BOOM!") end + it 'is false' do subject.call(true, ssl_context).should be_false end + it 'makes the error available through #verify_errors' do subject.call(true, ssl_context) subject.verify_errors.should == ["BOOM!"] @@ -100,11 +117,28 @@ describe Puppet::SSL::Validator do end end - describe '#register_verify_callback' do - it 'registers itself using #verify_callback' do + describe '#setup_connection' do + it 'updates the connection for verification' do + subject.stubs(:ssl_certificates_are_present?).returns(true) connection = mock('Net::HTTP') + + connection.expects(:cert_store=).with(ssl_host.ssl_store) + connection.expects(:ca_file=).with(ssl_configuration.ca_auth_file) + connection.expects(:cert=).with(ssl_host.certificate.content) + connection.expects(:key=).with(ssl_host.key.content) connection.expects(:verify_callback=).with(subject) - subject.register_verify_callback(connection) + connection.expects(:verify_mode=).with(OpenSSL::SSL::VERIFY_PEER) + + subject.setup_connection(connection) + end + + it 'does not perform verification if certificate files are missing' do + subject.stubs(:ssl_certificates_are_present?).returns(false) + connection = mock('Net::HTTP') + + connection.expects(:verify_mode=).with(OpenSSL::SSL::VERIFY_NONE) + + subject.setup_connection(connection) end end @@ -120,17 +154,21 @@ describe Puppet::SSL::Validator do before :each do subject.stubs(:has_authz_peer_cert).returns(true) end + it 'is true' do subject.valid_peer?.should be_true end end + context 'when the peer presents an invalid chain' do before :each do subject.stubs(:has_authz_peer_cert).returns(false) end + it 'is false' do subject.valid_peer?.should be_false end + it 'makes a helpful error message available via #verify_errors' do subject.valid_peer? subject.verify_errors.should == [expected_authz_error_msg] @@ -143,22 +181,27 @@ describe Puppet::SSL::Validator do it 'returns true when the SSL cert is issued by the Master CA' do subject.has_authz_peer_cert(cert_chain, [root_ca_cert]).should be_true end + it 'returns true when the SSL cert is issued by the Agent CA' do subject.has_authz_peer_cert(cert_chain_agent_ca, [root_ca_cert]).should be_true end end + context 'when the Master CA is listed as authorized' do it 'returns false when the SSL cert is issued by the Master CA' do subject.has_authz_peer_cert(cert_chain, [master_ca_cert]).should be_true end + it 'returns true when the SSL cert is issued by the Agent CA' do subject.has_authz_peer_cert(cert_chain_agent_ca, [master_ca_cert]).should be_false end end + context 'when the Agent CA is listed as authorized' do it 'returns true when the SSL cert is issued by the Master CA' do subject.has_authz_peer_cert(cert_chain, [agent_ca_cert]).should be_false end + it 'returns true when the SSL cert is issued by the Agent CA' do subject.has_authz_peer_cert(cert_chain_agent_ca, [agent_ca_cert]).should be_true end diff --git a/spec/unit/status_spec.rb b/spec/unit/status_spec.rb index 3f6ae7d82..8f598f579 100755 --- a/spec/unit/status_spec.rb +++ b/spec/unit/status_spec.rb @@ -37,4 +37,13 @@ describe Puppet::Status do new_status = Puppet::Status.convert_from('yaml', status.render('yaml')) new_status.should equal_attributes_of(status) end + + it "serializes to PSON that conforms to the status schema", :unless => Puppet.features.microsoft_windows? do + schema = JSON.parse(File.read('api/schemas/status.json')) + status = Puppet::Status.new + status.version = Puppet.version + + JSON::Validator.validate!(JSON_META_SCHEMA, schema) + JSON::Validator.validate!(schema, status.render('pson')) + end end diff --git a/spec/unit/transaction/event_spec.rb b/spec/unit/transaction/event_spec.rb index a60e6e907..8e62e02f6 100755 --- a/spec/unit/transaction/event_spec.rb +++ b/spec/unit/transaction/event_spec.rb @@ -15,14 +15,6 @@ end describe Puppet::Transaction::Event do include PuppetSpec::Files - [:previous_value, :desired_value, :property, :name, :message, :file, :line, :tags, :audited].each do |attr| - it "should support #{attr}" do - event = Puppet::Transaction::Event.new - event.send(attr.to_s + "=", "foo") - event.send(attr).should == "foo" - end - end - it "should support resource" do event = Puppet::Transaction::Event.new event.resource = TestResource.new @@ -101,7 +93,7 @@ describe Puppet::Transaction::Event do end it "should set the tags to the event tags" do - Puppet::Util::Log.expects(:new).with { |args| args[:tags] == %w{one two} } + Puppet::Util::Log.expects(:new).with { |args| args[:tags].to_a.should =~ %w{one two} } Puppet::Transaction::Event.new(:tags => %w{one two}).send_log end diff --git a/spec/unit/transaction/report_spec.rb b/spec/unit/transaction/report_spec.rb index 7195b0623..661349dab 100755 --- a/spec/unit/transaction/report_spec.rb +++ b/spec/unit/transaction/report_spec.rb @@ -4,6 +4,19 @@ require 'spec_helper' require 'puppet' require 'puppet/transaction/report' +# the json-schema gem doesn't support windows +if not Puppet.features.microsoft_windows? + REPORT_SCHEMA_URI = File.join(File.dirname(__FILE__), '../../../api/schemas/report.json') + REPORT_SCHEMA = JSON.parse(File.read(REPORT_SCHEMA_URI)) + + describe "report schema" do + it "should validate against the json meta-schema" do + JSON::Validator.validate!(JSON_META_SCHEMA, REPORT_SCHEMA) + end + end + +end + describe Puppet::Transaction::Report do include PuppetSpec::Files before do @@ -392,6 +405,12 @@ describe Puppet::Transaction::Report do expect_equivalent_reports(tripped, report) end + it "generates pson which validates against the report schema", :unless => Puppet.features.microsoft_windows? do + Puppet[:report_serialization_format] = "pson" + report = generate_report + JSON::Validator.validate!(REPORT_SCHEMA, report.render) + end + it "can make a round trip through yaml" do Puppet[:report_serialization_format] = "yaml" report = generate_report @@ -458,7 +477,7 @@ describe Puppet::Transaction::Report do status = Puppet::Resource::Status.new(Puppet::Type.type(:notify).new(:title => "a resource")) status.changed = true - report = Puppet::Transaction::Report.new('testy', 1357986, 'test_environment', "df34516e-4050-402d-a166-05b03b940749") + report = Puppet::Transaction::Report.new('apply', 1357986, 'test_environment', "df34516e-4050-402d-a166-05b03b940749") report << Puppet::Util::Log.new(:level => :warning, :message => "log message") report.add_times("timing", 4) report.add_resource_status(status) diff --git a/spec/unit/transaction/resource_harness_spec.rb b/spec/unit/transaction/resource_harness_spec.rb index 0c250ae92..afe1ec912 100755 --- a/spec/unit/transaction/resource_harness_spec.rb +++ b/spec/unit/transaction/resource_harness_spec.rb @@ -127,6 +127,34 @@ describe Puppet::Transaction::ResourceHarness do false end end + + newproperty(:brillig) do + desc "A property that raises a StandardError exception when you test if it's insync?" + def sync + end + + def retrieve + :absent + end + + def insync?(reference_value) + raise ZeroDivisionError.new('brillig') + end + end + + newproperty(:slithy) do + desc "A property that raises an Exception when you test if it's insync?" + def sync + end + + def retrieve + :absent + end + + def insync?(reference_value) + raise Exception.new('slithy') + end + end end stubProvider end @@ -164,6 +192,35 @@ describe Puppet::Transaction::ResourceHarness do end end + describe "when a StandardError exception occurs during insync?" do + before :each do + stub_provider = make_stub_provider + @resource = stub_provider.new :name => 'name', :brillig => 1 + @resource.expects(:err).never + end + + it "should record a failure event" do + @status = @harness.evaluate(@resource) + @status.events[0].name.to_s.should == 'brillig_changed' + @status.events[0].property.should == 'brillig' + @status.events[0].status.should == 'failure' + end + end + + describe "when an Exception occurs during insync?" do + before :each do + stub_provider = make_stub_provider + @resource = stub_provider.new :name => 'name', :slithy => 1 + @resource.expects(:err).never + end + + it "should log and pass the exception through" do + lambda { @harness.evaluate(@resource) }.should raise_error(Exception, /slithy/) + @logs.first.message.should == "change from absent to 1 failed: slithy" + @logs.first.level.should == :err + end + end + describe "when auditing" do it "should not call insync? on parameters that are merely audited" do stub_provider = make_stub_provider @@ -180,7 +237,9 @@ describe Puppet::Transaction::ResourceHarness do File.open(test_file, 'w').close resource = Puppet::Type.type(:file).new :path => test_file, :audit => ['group'], :backup => false resource.expects(:err).never # make sure no exceptions get swallowed + status = @harness.evaluate(resource) + status.events.each do |event| event.status.should != 'failure' end @@ -188,222 +247,13 @@ describe Puppet::Transaction::ResourceHarness do end describe "when applying changes" do - [false, true].each do |noop_mode|; describe (noop_mode ? "in noop mode" : "in normal mode") do - [nil, @mode_750].each do |machine_state|; describe (machine_state ? "with a file initially present" : "with no file initially present") do - [nil, @mode_750, @mode_755].each do |yaml_mode| - [nil, :file, :absent].each do |yaml_ensure|; describe "with mode=#{yaml_mode.inspect} and ensure=#{yaml_ensure.inspect} stored in state.yml" do - [false, true].each do |auditing_ensure| - [false, true].each do |auditing_mode| - auditing = [] - auditing.push(:mode) if auditing_mode - auditing.push(:ensure) if auditing_ensure - [nil, :file, :absent].each do |ensure_property| # what we set "ensure" to in the manifest - [nil, @mode_750, @mode_755].each do |mode_property| # what we set "mode" to in the manifest - manifest_settings = {} - manifest_settings[:audit] = auditing if !auditing.empty? - manifest_settings[:ensure] = ensure_property if ensure_property - manifest_settings[:mode] = mode_property if mode_property - describe "with manifest settings #{manifest_settings.inspect}" do; it "should behave properly" do - # Set up preconditions - test_file = tmpfile('foo') - if machine_state - File.open(test_file, 'w', machine_state.to_i(8)).close - end - - Puppet[:noop] = noop_mode - params = { :path => test_file, :backup => false } - params.merge!(manifest_settings) - resource = Puppet::Type.type(:file).new params - - @harness.cache(resource, :mode, yaml_mode) if yaml_mode - @harness.cache(resource, :ensure, yaml_ensure) if yaml_ensure - - fake_time = Time.utc(2011, 'jan', 3, 12, 24, 0) - Time.stubs(:now).returns(fake_time) # So that Puppet::Resource::Status objects will compare properly - - resource.expects(:err).never # make sure no exceptions get swallowed - status = @harness.evaluate(resource) # do the thing - - # check that the state of the machine has been properly updated - expected_logs = [] - expected_status_events = [] - if auditing_mode - @harness.cached(resource, :mode).should == (machine_state || :absent) - else - @harness.cached(resource, :mode).should == yaml_mode - end - if auditing_ensure - @harness.cached(resource, :ensure).should == (machine_state ? :file : :absent) - else - @harness.cached(resource, :ensure).should == yaml_ensure - end - if ensure_property == :file - file_would_be_there_if_not_noop = true - elsif ensure_property == nil - file_would_be_there_if_not_noop = machine_state != nil - else # ensure_property == :absent - file_would_be_there_if_not_noop = false - end - file_should_be_there = noop_mode ? machine_state != nil : file_would_be_there_if_not_noop - File.exists?(test_file).should == file_should_be_there - if file_should_be_there - if noop_mode - expected_file_mode = machine_state - else - expected_file_mode = mode_property || machine_state - end - if !expected_file_mode - # we didn't specify a mode and the file was created, so mode comes from umode - else - file_mode = File.stat(test_file).mode & 0777 - file_mode.should == expected_file_mode.to_i(8) - end - end - - # Test log output for the "mode" parameter - previously_recorded_mode_already_logged = false - mode_status_msg = nil - if machine_state && file_would_be_there_if_not_noop && mode_property && machine_state != mode_property - if noop_mode - what_happened = "current_value #{machine_state}, should be #{mode_property} (noop)" - expected_status = 'noop' - else - what_happened = "mode changed '#{machine_state}' to '#{mode_property}'" - expected_status = 'success' - end - if auditing_mode && yaml_mode && yaml_mode != machine_state - previously_recorded_mode_already_logged = true - mode_status_msg = "#{what_happened} (previously recorded value was #{yaml_mode})" - else - mode_status_msg = what_happened - end - expected_logs << "notice: /#{resource}/mode: #{mode_status_msg}" - end - if @harness.cached(resource, :mode) && @harness.cached(resource, :mode) != yaml_mode - if yaml_mode - unless previously_recorded_mode_already_logged - mode_status_msg = "audit change: previously recorded value #{yaml_mode} has been changed to #{@harness.cached(resource, :mode)}" - expected_logs << "notice: /#{resource}/mode: #{mode_status_msg}" - expected_status = 'audit' - end - else - expected_logs << "notice: /#{resource}/mode: audit change: newly-recorded value #{@harness.cached(resource, :mode)}" - end - end - if mode_status_msg - expected_status_events << Puppet::Transaction::Event.new( - :source_description => "/#{resource}/mode", :resource => resource, :file => nil, - :line => nil, :tags => %w{file}, :desired_value => mode_property, - :historical_value => yaml_mode, :message => mode_status_msg, :name => :mode_changed, - :previous_value => machine_state || :absent, :property => :mode, :status => expected_status, - :audited => auditing_mode) - end - - # Test log output for the "ensure" parameter - previously_recorded_ensure_already_logged = false - ensure_status_msg = nil - if file_would_be_there_if_not_noop != (machine_state != nil) - if noop_mode - what_happened = "current_value #{machine_state ? 'file' : 'absent'}, should be #{file_would_be_there_if_not_noop ? 'file' : 'absent'} (noop)" - expected_status = 'noop' - else - what_happened = file_would_be_there_if_not_noop ? 'created' : 'removed' - expected_status = 'success' - end - if auditing_ensure && yaml_ensure && yaml_ensure != (machine_state ? :file : :absent) - previously_recorded_ensure_already_logged = true - ensure_status_msg = "#{what_happened} (previously recorded value was #{yaml_ensure})" - else - ensure_status_msg = "#{what_happened}" - end - expected_logs << "notice: /#{resource}/ensure: #{ensure_status_msg}" - end - if @harness.cached(resource, :ensure) && @harness.cached(resource, :ensure) != yaml_ensure - if yaml_ensure - unless previously_recorded_ensure_already_logged - ensure_status_msg = "audit change: previously recorded value #{yaml_ensure} has been changed to #{@harness.cached(resource, :ensure)}" - expected_logs << "notice: /#{resource}/ensure: #{ensure_status_msg}" - expected_status = 'audit' - end - else - expected_logs << "notice: /#{resource}/ensure: audit change: newly-recorded value #{@harness.cached(resource, :ensure)}" - end - end - if ensure_status_msg - if ensure_property == :file - ensure_event_name = :file_created - elsif ensure_property == nil - ensure_event_name = :file_changed - else # ensure_property == :absent - ensure_event_name = :file_removed - end - expected_status_events << Puppet::Transaction::Event.new( - :source_description => "/#{resource}/ensure", :resource => resource, :file => nil, - :line => nil, :tags => %w{file}, :desired_value => ensure_property, - :historical_value => yaml_ensure, :message => ensure_status_msg, :name => ensure_event_name, - :previous_value => machine_state ? :file : :absent, :property => :ensure, - :status => expected_status, :audited => auditing_ensure) - end - - # Actually check the logs. - @logs.map {|l| "#{l.level}: #{l.source}: #{l.message}"}.should =~ expected_logs - - # All the log messages should show up as events except the "newly-recorded" ones. - expected_event_logs = @logs.reject {|l| l.message =~ /newly-recorded/ } - status.events.map {|e| e.message}.should =~ expected_event_logs.map {|l| l.message } - events_to_hash(status.events).should =~ events_to_hash(expected_status_events) - - # Check change count - this is the number of changes that actually occurred. - expected_change_count = 0 - if (machine_state != nil) != file_should_be_there - expected_change_count = 1 - elsif machine_state != nil - if expected_file_mode != machine_state - expected_change_count = 1 - end - end - status.change_count.should == expected_change_count - - # Check out of sync count - this is the number - # of changes that would have occurred in - # non-noop mode. - expected_out_of_sync_count = 0 - if (machine_state != nil) != file_would_be_there_if_not_noop - expected_out_of_sync_count = 1 - elsif machine_state != nil - if mode_property != nil && mode_property != machine_state - expected_out_of_sync_count = 1 - end - end - if !noop_mode - expected_out_of_sync_count.should == expected_change_count - end - status.out_of_sync_count.should == expected_out_of_sync_count - - # Check legacy summary fields - status.changed.should == (expected_change_count != 0) - status.out_of_sync.should == (expected_out_of_sync_count != 0) - - # Check the :synced field on state.yml - synced_should_be_set = !noop_mode && status.changed - (@harness.cached(resource, :synced) != nil).should == synced_should_be_set - end; end - end - end - end - end - end; end - end - end; end - end; end - it "should not apply changes if allow_changes?() returns false" do test_file = tmpfile('foo') resource = Puppet::Type.type(:file).new :path => test_file, :backup => false, :ensure => :file resource.expects(:err).never # make sure no exceptions get swallowed @harness.expects(:allow_changes?).with(resource).returns false status = @harness.evaluate(resource) - File.exists?(test_file).should == false + Puppet::FileSystem::File.exist?(test_file).should == false end end diff --git a/spec/unit/transaction_spec.rb b/spec/unit/transaction_spec.rb index 840b34c15..04ce08c4f 100755 --- a/spec/unit/transaction_spec.rb +++ b/spec/unit/transaction_spec.rb @@ -308,7 +308,7 @@ describe Puppet::Transaction do transaction.evaluate generated.each do |res| - res.must be_tagged(generator.tags) + res.must be_tagged(*generator.tags) end end end @@ -404,7 +404,6 @@ describe Puppet::Transaction do it "should otherwise let the resource determine if it is missing tags" do tags = ['one', 'two'] @transaction.tags = tags - @resource.expects(:tagged?).with(*tags).returns(false) @transaction.should be_missing_tags(@resource) end end @@ -478,6 +477,48 @@ describe Puppet::Transaction do end end + describe "during teardown" do + before :each do + @catalog = Puppet::Resource::Catalog.new + @transaction = Puppet::Transaction.new(@catalog, nil, Puppet::Graph::RandomPrioritizer.new) + end + + it "should call ::post_resource_eval on provider classes that support it" do + @resource = Puppet::Type.type(:notify).new :title => "foo" + @catalog.add_resource @resource + + # 'expects' will cause 'respond_to?(:post_resource_eval)' to return true + @resource.provider.class.expects(:post_resource_eval) + @transaction.evaluate + end + + it "should call ::post_resource_eval even if other providers' ::post_resource_eval fails" do + @resource3 = Puppet::Type.type(:user).new :title => "bloo" + @resource3.provider.class.stubs(:post_resource_eval).raises + @resource4 = Puppet::Type.type(:notify).new :title => "blob" + @resource4.provider.class.stubs(:post_resource_eval).raises + @catalog.add_resource @resource3 + @catalog.add_resource @resource4 + + # ruby's Set does not guarantee ordering, so both resource3 and resource4 + # need to expect post_resource_eval, rather than just the 'first' one. + @resource3.provider.class.expects(:post_resource_eval) + @resource4.provider.class.expects(:post_resource_eval) + + @transaction.evaluate + end + + it "should call ::post_resource_eval even if one of the resources fails" do + @resource3 = Puppet::Type.type(:notify).new :title => "bloo" + @resource3.stubs(:retrieve_resource).raises + @catalog.add_resource @resource3 + + @resource3.provider.class.expects(:post_resource_eval) + + @transaction.evaluate + end + end + describe 'when checking application run state' do before do @catalog = Puppet::Resource::Catalog.new @@ -594,32 +635,37 @@ describe Puppet::Transaction, " when determining tags" do it "should default to the tags specified in the :tags setting" do Puppet[:tags] = "one" - @transaction.tags.should == %w{one} + @transaction.should be_tagged("one") end it "should split tags based on ','" do Puppet[:tags] = "one,two" - @transaction.tags.should == %w{one two} + @transaction.should be_tagged("one") + @transaction.should be_tagged("two") end it "should use any tags set after creation" do Puppet[:tags] = "" @transaction.tags = %w{one two} - @transaction.tags.should == %w{one two} + @transaction.should be_tagged("one") + @transaction.should be_tagged("two") end it "should always convert assigned tags to an array" do @transaction.tags = "one::two" - @transaction.tags.should == %w{one::two} + @transaction.should be_tagged("one::two") end it "should accept a comma-delimited string" do @transaction.tags = "one, two" - @transaction.tags.should == %w{one two} + @transaction.should be_tagged("one") + @transaction.should be_tagged("two") end it "should accept an empty string" do + @transaction.tags = "one, two" + @transaction.should be_tagged("one") @transaction.tags = "" - @transaction.tags.should == [] + @transaction.should_not be_tagged("one") end end diff --git a/spec/unit/type/component_spec.rb b/spec/unit/type/component_spec.rb index 82c822dea..f3326a161 100755 --- a/spec/unit/type/component_spec.rb +++ b/spec/unit/type/component_spec.rb @@ -42,8 +42,8 @@ describe component do component.new(:name => "Class[foo]").pathbuilder.must == ["Foo"] end - it "should produce an empty string if the component models the 'main' class" do - component.new(:name => "Class[main]").pathbuilder.must == [""] + it "should produce the class name even for the class named main" do + component.new(:name => "Class[main]").pathbuilder.must == ["Main"] end it "should produce a resource reference if the component does not model a class" do diff --git a/spec/unit/type/exec_spec.rb b/spec/unit/type/exec_spec.rb index 85a5809dc..6d780b3ff 100755 --- a/spec/unit/type/exec_spec.rb +++ b/spec/unit/type/exec_spec.rb @@ -8,6 +8,8 @@ describe Puppet::Type.type(:exec) do Puppet.features.stubs(:root?).returns(true) output = rest.delete(:output) || '' + + output = Puppet::Util::Execution::ProcessOutput.new(output, exitstatus) tries = rest[:tries] || 1 args = { @@ -21,14 +23,12 @@ describe Puppet::Type.type(:exec) do exec = Puppet::Type.type(:exec).new(args) status = stub "process", :exitstatus => exitstatus - Puppet::Util::SUIDManager.expects(:run_and_capture).times(tries). + Puppet::Util::Execution.expects(:execute).times(tries). with() { |*args| args[0] == command && - args[1] == nil && - args[2] == nil && - args[3][:override_locale] == false && - args[3].has_key?(:custom_environment) - } .returns([output, status]) + args[1][:override_locale] == false && + args[1].has_key?(:custom_environment) + }.returns(output) return exec end @@ -61,7 +61,7 @@ describe Puppet::Type.type(:exec) do end describe "when execing" do - it "should use the 'run_and_capture' method to exec" do + it "should use the 'execute' method to exec" do exec_tester("true").refresh.should == :executed_command end @@ -753,4 +753,11 @@ describe Puppet::Type.type(:exec) do type.new(:command => abs, :path => path).must be end end + describe "when providing a umask" do + it "should fail if an invalid umask is used" do + resource = Puppet::Type.type(:exec).new :command => @command + expect { resource[:umask] = '0028'}.to raise_error(Puppet::ResourceError, /umask specification is invalid/) + expect { resource[:umask] = '28' }.to raise_error(Puppet::ResourceError, /umask specification is invalid/) + end + end end diff --git a/spec/unit/type/file/content_spec.rb b/spec/unit/type/file/content_spec.rb index c1c97a452..5a73dceb1 100755 --- a/spec/unit/type/file/content_spec.rb +++ b/spec/unit/type/file/content_spec.rb @@ -84,6 +84,13 @@ describe content do @content.should.must == string end + + it "should convert the value to ASCII-8BIT", :if => "".respond_to?(:encode) do + @content = content.new(:resource => @resource) + @content.should= "Let's make a \u{2603}" + + @content.actual_content.should == "Let's make a \xE2\x98\x83".force_encoding(Encoding::ASCII_8BIT) + end end describe "when retrieving the current content" do @@ -329,8 +336,10 @@ describe content do end it "should copy content from the source to the file" do + dest_file = Puppet::FileSystem::File.new(@filename) @resource.write(@source) - IO.binread(@filename).should == @source_content + + dest_file.binread.should == @source_content end it "should return the checksum computed" do @@ -358,8 +367,10 @@ describe content do end it "should write the contents to the file" do + dest_file = Puppet::FileSystem::File.new(@filename) @resource.write(@source) - IO.binread(@filename).should == @source_content + + dest_file.binread.should == @source_content end it "should not write anything if source is not found" do diff --git a/spec/unit/type/file/ctime_spec.rb b/spec/unit/type/file/ctime_spec.rb index ba46da286..eea0f1f92 100755 --- a/spec/unit/type/file/ctime_spec.rb +++ b/spec/unit/type/file/ctime_spec.rb @@ -16,7 +16,7 @@ describe Puppet::Type.type(:file).attrclass(:ctime) do @resource[:audit] = [:ctime] # this .to_resource audit behavior is magical :-( - @resource.to_resource[:ctime].should == File.stat(@filename).ctime + @resource.to_resource[:ctime].should == Puppet::FileSystem::File.new(@filename).stat.ctime end it "should return absent if auditing an absent file" do diff --git a/spec/unit/type/file/mode_spec.rb b/spec/unit/type/file/mode_spec.rb index dc43ae498..8663fe57d 100755 --- a/spec/unit/type/file/mode_spec.rb +++ b/spec/unit/type/file/mode_spec.rb @@ -18,6 +18,10 @@ describe Puppet::Type.type(:file).attrclass(:mode) do expect { mode.value = '0755' }.not_to raise_error end + it "should accept valid symbolic strings" do + expect { mode.value = 'g+w,u-x' }.not_to raise_error + end + it "should not accept strings other than octal numbers" do expect do mode.value = 'readable please!' @@ -35,6 +39,10 @@ describe Puppet::Type.type(:file).attrclass(:mode) do mode.munge('0644').should == '644' end + it "should accept symbolic strings as arguments and return them intact" do + mode.munge('u=rw,go=r').should == 'u=rw,go=r' + end + it "should accept integers are arguments" do mode.munge(0644).should == '644' end @@ -72,11 +80,34 @@ describe Puppet::Type.type(:file).attrclass(:mode) do mode.must_not be_insync('755') end - it "should return true if the file is a link and we are managing links", :unless => Puppet.features.microsoft_windows? do - File.symlink('anything', path) + it "should return true if the file is a link and we are managing links", :if => Puppet.features.manages_symlinks? do + Puppet::FileSystem::File.new('anything').symlink(path) mode.must be_insync('644') end + + describe "with a symbolic mode" do + let(:resource_sym) { Puppet::Type.type(:file).new :path => path, :mode => 'u+w,g-w' } + let(:mode_sym) { resource_sym.property(:mode) } + + it "should return true if the mode matches, regardless of other bits" do + FileUtils.touch(path) + + mode_sym.must be_insync('644') + end + + it "should return false if the mode requires 0's where there are 1's" do + FileUtils.touch(path) + + mode_sym.must_not be_insync('624') + end + + it "should return false if the mode requires 1's where there are 0's" do + FileUtils.touch(path) + + mode_sym.must_not be_insync('044') + end + end end describe "#retrieve" do @@ -145,4 +176,19 @@ describe Puppet::Type.type(:file).attrclass(:mode) do end end end + + describe "#sync with a symbolic mode" do + let(:resource_sym) { Puppet::Type.type(:file).new :path => path, :mode => 'u+w,g-w' } + let(:mode_sym) { resource_sym.property(:mode) } + + before { FileUtils.touch(path) } + + it "changes only the requested bits" do + # lower nibble must be set to 4 for the sake of passing on Windows + FileUtils.chmod 0464, path + mode_sym.sync + file = Puppet::FileSystem::File.new(path) + (file.stat.mode & 0777).to_s(8).should == "644" + end + end end diff --git a/spec/unit/type/file/mtime_spec.rb b/spec/unit/type/file/mtime_spec.rb index 93abcb2fe..a20bdf196 100755 --- a/spec/unit/type/file/mtime_spec.rb +++ b/spec/unit/type/file/mtime_spec.rb @@ -16,7 +16,7 @@ describe Puppet::Type.type(:file).attrclass(:mtime) do @resource[:audit] = [:mtime] # this .to_resource audit behavior is magical :-( - @resource.to_resource[:mtime].should == File.stat(@filename).mtime + @resource.to_resource[:mtime].should == Puppet::FileSystem::File.new(@filename).stat.mtime end it "should return absent if auditing an absent file" do diff --git a/spec/unit/type/file/source_spec.rb b/spec/unit/type/file/source_spec.rb index 36cad8849..e645842c0 100755 --- a/spec/unit/type/file/source_spec.rb +++ b/spec/unit/type/file/source_spec.rb @@ -154,11 +154,10 @@ describe Puppet::Type.type(:file).attrclass(:source) do describe "when copying the source values" do before do - @resource = Puppet::Type.type(:file).new :path => @foobar @source = source.new(:resource => @resource) - @metadata = stub 'metadata', :owner => 100, :group => 200, :mode => 123, :checksum => "{md5}asdfasdf", :ftype => "file", :source => @foobar + @metadata = stub 'metadata', :owner => 100, :group => 200, :mode => "173", :checksum => "{md5}asdfasdf", :ftype => "file", :source => @foobar @source.stubs(:metadata).returns @metadata Puppet.features.stubs(:root?).returns true @@ -202,7 +201,7 @@ describe Puppet::Type.type(:file).attrclass(:source) do @resource[:content].must == @metadata.checksum end - it "should not copy the metadata's owner to the resource if it is already set" do + it "should not copy the metadata's owner, group, checksum and mode to the resource if they are already set" do @resource[:owner] = 1 @resource[:group] = 2 @resource[:mode] = 3 @@ -217,26 +216,153 @@ describe Puppet::Type.type(:file).attrclass(:source) do end describe "and puppet is not running as root" do - it "should not try to set the owner" do - Puppet.features.expects(:root?).returns false + before do + Puppet.features.stubs(:root?).returns false + end + it "should not try to set the owner" do @source.copy_source_values @resource[:owner].should be_nil end + + it "should not try to set the group" do + @source.copy_source_values + @resource[:group].should be_nil + end + end + + context "when source_permissions is `use_when_creating`" do + before :each do + @resource[:source_permissions] = "use_when_creating" + Puppet.features.expects(:root?).returns true + @source.stubs(:local?).returns(false) + end + + context "when managing a new file" do + it "should copy owner and group from local sources" do + @source.stubs(:local?).returns true + + @source.copy_source_values + + @resource[:owner].must == 100 + @resource[:group].must == 200 + @resource[:mode].must == "173" + end + + it "copies the remote owner" do + @source.copy_source_values + + @resource[:owner].must == 100 + end + + it "copies the remote group" do + @source.copy_source_values + + @resource[:group].must == 200 + end + + it "copies the remote mode" do + @source.copy_source_values + + @resource[:mode].must == "173" + end + end + + context "when managing an existing file" do + before :each do + Puppet::FileSystem::File.stubs(:exist?).with(@resource[:path]).returns(true) + end + + it "should not copy owner, group or mode from local sources" do + @source.stubs(:local?).returns true + + @source.copy_source_values + + @resource[:owner].must be_nil + @resource[:group].must be_nil + @resource[:mode].must be_nil + end + + it "preserves the local owner" do + @source.copy_source_values + + @resource[:owner].must be_nil + end + + it "preserves the local group" do + @source.copy_source_values + + @resource[:group].must be_nil + end + + it "preserves the local mode" do + @source.copy_source_values + + @resource[:mode].must be_nil + end + end + end + + context "when source_permissions is `ignore`" do + before :each do + @resource[:source_permissions] = "ignore" + @source.stubs(:local?).returns(false) + Puppet.features.expects(:root?).returns true + end + + it "should not copy owner, group or mode from local sources" do + @source.stubs(:local?).returns true + + @source.copy_source_values + + @resource[:owner].must be_nil + @resource[:group].must be_nil + @resource[:mode].must be_nil + end + + it "preserves the local owner" do + @source.copy_source_values + + @resource[:owner].must be_nil + end + + it "preserves the local group" do + @source.copy_source_values + + @resource[:group].must be_nil + end + + it "preserves the local mode" do + @source.copy_source_values + + @resource[:mode].must be_nil + end end describe "on Windows" do before :each do Puppet.features.stubs(:microsoft_windows?).returns true end + let(:deprecation_message) { "Copying owner/mode/group from the" << + " source file on Windows is deprecated;" << + " use source_permissions => ignore." } - it "should not copy owner and group from remote sources" do + it "should copy only mode from remote sources" do @source.stubs(:local?).returns false @source.copy_source_values @resource[:owner].must be_nil @resource[:group].must be_nil + @resource[:mode].must == "173" + end + + it "should copy mode from remote sources" do + @source.stubs(:local?).returns false + + @source.copy_source_values + + @resource[:mode].must == "173" end it "should copy owner and group from local sources" do @@ -246,6 +372,51 @@ describe Puppet::Type.type(:file).attrclass(:source) do @resource[:owner].must == 100 @resource[:group].must == 200 + @resource[:mode].must == "173" + end + + it "should issue deprecation warning when copying metadata from remote sources when group, owner, and mode are unspecified" do + @source.stubs(:local?).returns false + Puppet.expects(:deprecation_warning).with(deprecation_message).at_least_once + + @source.copy_source_values + end + + it "should issue deprecation warning when copying metadata from remote sources if only user is unspecified" do + @source.stubs(:local?).returns false + Puppet.expects(:deprecation_warning).with(deprecation_message).at_least_once + @resource[:group] = 2 + @resource[:mode] = 3 + + @source.copy_source_values + end + + it "should issue deprecation warning when copying metadata from remote sources if only group is unspecified" do + @source.stubs(:local?).returns false + Puppet.expects(:deprecation_warning).with(deprecation_message).at_least_once + @resource[:owner] = 1 + @resource[:mode] = 3 + + @source.copy_source_values + end + + it "should issue deprecation warning when copying metadata from remote sources if only mode is unspecified" do + @source.stubs(:local?).returns false + Puppet.expects(:deprecation_warning).with(deprecation_message).at_least_once + @resource[:owner] = 1 + @resource[:group] = 2 + + @source.copy_source_values + end + + it "should not issue deprecation warning when copying metadata from remote sources if group, owner, and mode are all specified" do + @source.stubs(:local?).returns false + Puppet.expects(:deprecation_warning).with(deprecation_message).never + @resource[:owner] = 1 + @resource[:group] = 2 + @resource[:mode] = 3 + + @source.copy_source_values end end end @@ -358,5 +529,4 @@ describe Puppet::Type.type(:file).attrclass(:source) do end end end - end diff --git a/spec/unit/type/file_spec.rb b/spec/unit/type/file_spec.rb index dd695eeda..75b58926a 100755 --- a/spec/unit/type/file_spec.rb +++ b/spec/unit/type/file_spec.rb @@ -10,7 +10,6 @@ describe Puppet::Type.type(:file) do let(:catalog) { Puppet::Resource::Catalog.new } before do - @real_posix = Puppet.features.posix? Puppet.features.stubs("posix?").returns(true) end @@ -78,33 +77,29 @@ describe Puppet::Type.type(:file) do end describe "when using UNC filenames", :if => Puppet.features.microsoft_windows? do - before :each do - pending("UNC file paths not yet supported") - end - it "should remove trailing slashes" do - file[:path] = "//server/foo/bar/baz/" - file[:path].should == "//server/foo/bar/baz" + file[:path] = "//localhost/foo/bar/baz/" + file[:path].should == "//localhost/foo/bar/baz" end it "should remove double slashes" do - file[:path] = "//server/foo/bar//baz" - file[:path].should == "//server/foo/bar/baz" + file[:path] = "//localhost/foo/bar//baz" + file[:path].should == "//localhost/foo/bar/baz" end it "should remove trailing double slashes" do - file[:path] = "//server/foo/bar/baz//" - file[:path].should == "//server/foo/bar/baz" + file[:path] = "//localhost/foo/bar/baz//" + file[:path].should == "//localhost/foo/bar/baz" end it "should remove a trailing slash from a sharename" do - file[:path] = "//server/foo/" - file[:path].should == "//server/foo" + file[:path] = "//localhost/foo/" + file[:path].should == "//localhost/foo" end it "should not modify a sharename" do - file[:path] = "//server/foo" - file[:path].should == "//server/foo" + file[:path] = "//localhost/foo" + file[:path].should == "//localhost/foo" end end end @@ -352,7 +347,7 @@ describe Puppet::Type.type(:file) do file[:ensure].should == :file end - it "should set a desired 'ensure' value if none is set and 'target' is set" do + it "should set a desired 'ensure' value if none is set and 'target' is set", :if => described_class.defaultprovider.feature?(:manages_symlinks) do file = described_class.new(:path => path, :target => File.expand_path(__FILE__)) file[:ensure].should == :link end @@ -397,7 +392,7 @@ describe Puppet::Type.type(:file) do :target => "some_target", :source => File.expand_path("some_source"), }.each do |param, value| - it "should omit the #{param} parameter" do + it "should omit the #{param} parameter", :if => described_class.defaultprovider.feature?(:manages_symlinks) do # Make a new file, because we have to set the param at initialization # or it wouldn't be copied regardless. file = described_class.new(:path => path, param => value) @@ -603,7 +598,7 @@ describe Puppet::Type.type(:file) do file.recurse_link("first" => @resource) end - it "should set the target to the full path of discovered file and set :ensure to :link if the file is not a directory" do + it "should set the target to the full path of discovered file and set :ensure to :link if the file is not a directory", :if => described_class.defaultprovider.feature?(:manages_symlinks) do file.stubs(:perform_recursion).returns [@first, @second] file.recurse_link("first" => @resource, "second" => file) @@ -925,20 +920,20 @@ describe Puppet::Type.type(:file) do file.remove_existing(:directory).should == true - File.exists?(file[:path]).should == false + Puppet::FileSystem::File.exist?(file[:path]).should == false end - it "should remove an existing link", :unless => Puppet.features.microsoft_windows? do + it "should remove an existing link", :if => described_class.defaultprovider.feature?(:manages_symlinks) do file.stubs(:perform_backup).returns true target = tmpfile('link_target') FileUtils.touch(target) - FileUtils.symlink(target, path) + Puppet::FileSystem::File.new(target).symlink(path) file[:target] = target file.remove_existing(:directory).should == true - File.exists?(file[:path]).should == false + Puppet::FileSystem::File.exist?(file[:path]).should == false end it "should fail if the file is not a file, link, or directory" do @@ -952,7 +947,7 @@ describe Puppet::Type.type(:file) do file.stat file.stubs(:stat).returns stub('stat', :ftype => 'file') - File.stubs(:unlink) + Puppet::FileSystem::File.stubs(:unlink) file.remove_existing(:directory).should == true file.instance_variable_get(:@stat).should == :needs_stat @@ -1010,11 +1005,11 @@ describe Puppet::Type.type(:file) do end end - describe "#stat", :unless => Puppet.features.microsoft_windows? do + describe "#stat", :if => described_class.defaultprovider.feature?(:manages_symlinks) do before do target = tmpfile('link_target') FileUtils.touch(target) - FileUtils.symlink(target, path) + Puppet::FileSystem::File.new(target).symlink(path) file[:target] = target file[:links] = :manage # so we always use :lstat @@ -1033,7 +1028,7 @@ describe Puppet::Type.type(:file) do end it "should return nil if the file does not exist" do - file[:path] = '/foo/bar/baz/non-existent' + file[:path] = make_absolute('/foo/bar/baz/non-existent') file.stat.should be_nil end @@ -1207,7 +1202,7 @@ describe Puppet::Type.type(:file) do describe "when autorequiring" do describe "target" do - it "should require file resource when specified with the target property" do + it "should require file resource when specified with the target property", :if => described_class.defaultprovider.feature?(:manages_symlinks) do file = described_class.new(:path => File.expand_path("/foo"), :ensure => :directory) link = described_class.new(:path => File.expand_path("/bar"), :ensure => :link, :target => File.expand_path("/foo")) catalog.add_resource file @@ -1229,7 +1224,7 @@ describe Puppet::Type.type(:file) do reqs[0].target.must == link end - it "should not require target if target is not managed" do + it "should not require target if target is not managed", :if => described_class.defaultprovider.feature?(:manages_symlinks) do link = described_class.new(:path => File.expand_path('/foo'), :ensure => :link, :target => '/bar') catalog.add_resource link link.autorequire.size.should == 0 @@ -1272,8 +1267,8 @@ describe Puppet::Type.type(:file) do describe "on Windows systems", :if => Puppet.features.microsoft_windows? do describe "when using UNC filenames" do it "should autorequire its parent directory" do - file[:path] = '//server/foo/bar/baz' - dir = described_class.new(:path => "//server/foo/bar") + file[:path] = '//localhost/foo/bar/baz' + dir = described_class.new(:path => "//localhost/foo/bar") catalog.add_resource file catalog.add_resource dir reqs = file.autorequire @@ -1282,9 +1277,9 @@ describe Puppet::Type.type(:file) do end it "should autorequire its nearest ancestor directory" do - file = described_class.new(:path => "//server/foo/bar/baz/qux") - dir = described_class.new(:path => "//server/foo/bar/baz") - grandparent = described_class.new(:path => "//server/foo/bar") + file = described_class.new(:path => "//localhost/foo/bar/baz/qux") + dir = described_class.new(:path => "//localhost/foo/bar/baz") + grandparent = described_class.new(:path => "//localhost/foo/bar") catalog.add_resource file catalog.add_resource dir catalog.add_resource grandparent @@ -1295,13 +1290,13 @@ describe Puppet::Type.type(:file) do end it "should not autorequire anything when there is no nearest ancestor directory" do - file = described_class.new(:path => "//server/foo/bar/baz/qux") + file = described_class.new(:path => "//localhost/foo/bar/baz/qux") catalog.add_resource file file.autorequire.should be_empty end it "should not autorequire its parent dir if its parent dir is itself" do - file = described_class.new(:path => "//server/foo") + file = described_class.new(:path => "//localhost/foo") catalog.add_resource file puts file.autorequire file.autorequire.should be_empty @@ -1311,49 +1306,46 @@ describe Puppet::Type.type(:file) do end end - describe "when managing links" do + describe "when managing links", :if => Puppet.features.manages_symlinks? do require 'tempfile' - if @real_posix - describe "on POSIX systems" do - before do - Dir.mkdir(path) - @target = File.join(path, "target") - @link = File.join(path, "link") - - File.open(@target, "w", 0644) { |f| f.puts "yayness" } - File.symlink(@target, @link) - - file[:path] = @link - file[:mode] = 0755 - - catalog.add_resource file - end + before :each do + Dir.mkdir(path) + @target = File.join(path, "target") + @link = File.join(path, "link") + + target = described_class.new( + :ensure => :file, :path => @target, + :catalog => catalog, :content => 'yayness', + :mode => 0644) + catalog.add_resource target + + @link_resource = described_class.new( + :ensure => :link, :path => @link, + :target => @target, :catalog => catalog, + :mode => 0755) + catalog.add_resource @link_resource - it "should default to managing the link" do - catalog.apply - # I convert them to strings so they display correctly if there's an error. - (File.stat(@target).mode & 007777).to_s(8).should == '644' - end + # to prevent the catalog from trying to write state.yaml + Puppet::Util::Storage.stubs(:store) + end - it "should be able to follow links" do - file[:links] = :follow - catalog.apply + it "should preserve the original file mode and ignore the one set by the link" do + @link_resource[:links] = :manage # default + catalog.apply - (File.stat(@target).mode & 007777).to_s(8).should == '755' - end - end - else # @real_posix - # should recode tests using expectations instead of using the filesystem + # I convert them to strings so they display correctly if there's an error. + (Puppet::FileSystem::File.new(@target).stat.mode & 007777).to_s(8).should == '644' end - describe "on Microsoft Windows systems" do - before do - Puppet.features.stubs(:posix?).returns(false) - Puppet.features.stubs(:microsoft_windows?).returns(true) - end + it "should manage the mode of the followed link" do + pending("Windows cannot presently manage the mode when following symlinks", + :if => Puppet.features.microsoft_windows?) do + @link_resource[:links] = :follow + catalog.apply - it "should refuse to work with links" + (Puppet::FileSystem::File.new(@target).stat.mode & 007777).to_s(8).should == '755' + end end end @@ -1436,7 +1428,7 @@ describe Puppet::Type.type(:file) do catalog.apply - File.should be_exist(path) + Puppet::FileSystem::File.exist?(path).should be_true @logs.should_not be_any {|l| l.level != :notice } end end diff --git a/spec/unit/type/group_spec.rb b/spec/unit/type/group_spec.rb index f8d24f0df..55f6e548b 100755 --- a/spec/unit/type/group_spec.rb +++ b/spec/unit/type/group_spec.rb @@ -61,4 +61,24 @@ describe Puppet::Type.type(:group) do type.exists?.should == true end + + describe "should delegate :members implementation to the provider:" do + + let (:provider) { @class.provide(:testing) { has_features :manages_members } } + let (:provider_instance) { provider.new } + let (:type) { @class.new(:name => "group", :provider => provider_instance, :members => ['user1']) } + + it "insync? calls members_insync?" do + provider_instance.expects(:members_insync?).with(['user1'], ['user1']).returns true + type.property(:members).insync?(['user1']).should be_true + end + + it "is_to_s and should_to_s call members_to_s" do + provider_instance.expects(:members_to_s).with(['user2', 'user1']).returns "user2 (), user1 ()" + provider_instance.expects(:members_to_s).with(['user1']).returns "user1 ()" + + type.property(:members).is_to_s('user1').should == 'user1 ()' + type.property(:members).should_to_s('user2,user1').should == 'user2 (), user1 ()' + end + end end diff --git a/spec/unit/type/k5login_spec.rb b/spec/unit/type/k5login_spec.rb index 2524d98c5..484ddf8e7 100755 --- a/spec/unit/type/k5login_spec.rb +++ b/spec/unit/type/k5login_spec.rb @@ -46,7 +46,7 @@ describe Puppet::Type.type(:k5login), :unless => Puppet.features.microsoft_windo it "should create the file when synced" do resource(:ensure => 'present').parameter(:ensure).sync - File.should be_exist path + Puppet::FileSystem::File.exist?(path).should be_true end end @@ -83,7 +83,7 @@ describe Puppet::Type.type(:k5login), :unless => Puppet.features.microsoft_windo it "should remove the file ensure is absent" do resource(:ensure => 'absent').property(:ensure).sync - File.should_not be_exist path + Puppet::FileSystem::File.exist?(path).should be_false end it "should write one principal to the file" do @@ -106,7 +106,7 @@ describe Puppet::Type.type(:k5login), :unless => Puppet.features.microsoft_windo it "should update the mode to #{mode}" do resource(:mode => mode).property(:mode).sync - (File.stat(path).mode & 07777).to_s(8).should == mode + (Puppet::FileSystem::File.new(path).stat.mode & 07777).to_s(8).should == mode end end end diff --git a/spec/unit/type/mount_spec.rb b/spec/unit/type/mount_spec.rb index 726577891..75b2f505d 100755 --- a/spec/unit/type/mount_spec.rb +++ b/spec/unit/type/mount_spec.rb @@ -536,4 +536,57 @@ describe Puppet::Type.type(:mount), :unless => Puppet.features.microsoft_windows run_in_catalog(resource) end end + + describe "establishing autorequires" do + + def create_resource(path) + described_class.new( + :name => path, + :provider => providerclass.new(path) + ) + end + + def create_catalog(*resources) + catalog = Puppet::Resource::Catalog.new + resources.each do |resource| + catalog.add_resource resource + end + + catalog + end + + let(:root_mount) { create_resource("/") } + let(:var_mount) { create_resource("/var") } + let(:log_mount) { create_resource("/var/log") } + + before do + create_catalog(root_mount, var_mount, log_mount) + end + + it "adds no autorequires for the root mount" do + expect(root_mount.autorequire).to be_empty + end + + it "adds the parent autorequire for a mount with one parent" do + parent_relationship = var_mount.autorequire[0] + + expect(var_mount.autorequire).to have_exactly(1).item + + expect(parent_relationship.source).to eq root_mount + expect(parent_relationship.target).to eq var_mount + end + + it "adds both parent autorequires for a mount with two parents" do + grandparent_relationship = log_mount.autorequire[0] + parent_relationship = log_mount.autorequire[1] + + expect(log_mount.autorequire).to have_exactly(2).items + + expect(grandparent_relationship.source).to eq root_mount + expect(grandparent_relationship.target).to eq log_mount + + expect(parent_relationship.source).to eq var_mount + expect(parent_relationship.target).to eq log_mount + end + end end diff --git a/spec/unit/type/nagios_spec.rb b/spec/unit/type/nagios_spec.rb index 8c86aae7f..84d4338de 100755 --- a/spec/unit/type/nagios_spec.rb +++ b/spec/unit/type/nagios_spec.rb @@ -3,6 +3,222 @@ require 'spec_helper' require 'puppet/external/nagios' +describe "Nagios parser" do + + NONESCAPED_SEMICOLON_COMMENT = <<-'EOL' +define host{ + use linux-server ; Name of host template to use + host_name localhost + alias localhost + address 127.0.0.1 + } + +define command{ + command_name notify-host-by-email + command_line /usr/bin/printf "%b" "***** Nagios *****\n\nNotification Type: $NOTIFICATIONTYPE$\nHost: $HOSTNAME$\nState: $HOSTSTATE$\nAddress: $HOSTADDRESS$\nInfo: $HOSTOUTPUT$\n\nDate/Time: $LONGDATETIME$\n" | /usr/bin/mail -s "** $NOTIFICATIONTYPE$ Host Alert: $HOSTNAME$ is $HOSTSTATE$ **" $CONTACTEMAIL$ + } +EOL + + LINE_COMMENT_SNIPPET = <<-'EOL' + +# This is a comment starting at the beginning of a line + +define command{ + +# This is a comment starting at the beginning of a line + + command_name command_name + +# This is a comment starting at the beginning of a line + ## --PUPPET_NAME-- (called '_naginator_name' in the manifest) command_name + + command_line command_line + +# This is a comment starting at the beginning of a line + + } + +# This is a comment starting at the beginning of a line + +EOL + + LINE_COMMENT_SNIPPET2 = <<-'EOL' + define host{ + use linux-server ; Name of host template to use + host_name localhost + alias localhost + address 127.0.0.1 + } +define command{ + command_name command_name2 + command_line command_line2 + } +EOL + + UNKNOWN_NAGIOS_OBJECT_DEFINITION = <<-'EOL' + define command2{ + command_name notify-host-by-email + command_line /usr/bin/printf "%b" "***** Nagios *****\n\nNotification Type: $NOTIFICATIONTYPE$\nHost: $HOSTNAME$\nState: $HOSTSTATE$\nAddress: $HOSTADDRESS$\nInfo: $HOSTOUTPUT$\n\nDate/Time: $LONGDATETIME$\n" | /usr/bin/mail -s "** $NOTIFICATIONTYPE$ Host Alert: $HOSTNAME$ is $HOSTSTATE$ **" $CONTACTEMAIL$ + } + EOL + + MISSING_CLOSING_CURLY_BRACKET = <<-'EOL' + define command{ + command_name notify-host-by-email + command_line /usr/bin/printf "%b" "***** Nagios *****\n\nNotification Type: $NOTIFICATIONTYPE$\nHost: $HOSTNAME$\nState: $HOSTSTATE$\nAddress: $HOSTADDRESS$\nInfo: $HOSTOUTPUT$\n\nDate/Time: $LONGDATETIME$\n" | /usr/bin/mail -s "** $NOTIFICATIONTYPE$ Host Alert: $HOSTNAME$ is $HOSTSTATE$ **" $CONTACTEMAIL$ + EOL + + ESCAPED_SEMICOLON = <<-'EOL' + define command { + command_name nagios_table_size + command_line $USER3$/check_mysql_health --hostname localhost --username nagioschecks --password nagiosCheckPWD --mode sql --name "SELECT ROUND(Data_length/1024) as Data_kBytes from INFORMATION_SCHEMA.TABLES where TABLE_NAME=\"$ARG1$\"\;" --name2 "table size" --units kBytes -w $ARG2$ -c $ARG3$ + } + EOL + + POUND_SIGN_HASH_SYMBOL_NOT_IN_FIRST_COLUMN = <<-'EOL' + define command { + command_name notify-by-irc + command_line /usr/local/bin/riseup-nagios-client.pl "$HOSTNAME$ ($SERVICEDESC$) $NOTIFICATIONTYPE$ #$SERVICEATTEMPT$ $SERVICESTATETYPE$ $SERVICEEXECUTIONTIME$s $SERVICELATENCY$s $SERVICEOUTPUT$ $SERVICEPERFDATA$" + } + EOL + + ANOTHER_ESCAPED_SEMICOLON = <<-EOL +define command { +\tcommand_line LC_ALL=en_US.UTF-8 /usr/lib/nagios/plugins/check_haproxy -u 'http://blah:blah@$HOSTADDRESS$:8080/haproxy?stats\\;csv' +\tcommand_name check_haproxy +} +EOL + + it "should parse without error" do + parser = Nagios::Parser.new + expect { + results = parser.parse(NONESCAPED_SEMICOLON_COMMENT) + }.to_not raise_error + end + + describe "when parsing a statement" do + parser = Nagios::Parser.new + results = parser.parse(NONESCAPED_SEMICOLON_COMMENT) + results.each do |obj| + it "should have the proper base type" do + obj.should be_a_kind_of(Nagios::Base) + end + end + end + + it "should raise an error when an incorrect object definition is present" do + parser = Nagios::Parser.new + expect { + results = parser.parse(UNKNOWN_NAGIOS_OBJECT_DEFINITION) + }.to raise_error Nagios::Base::UnknownNagiosType + end + + it "should raise an error when syntax is not correct" do + parser = Nagios::Parser.new + expect { + results = parser.parse(MISSING_CLOSING_CURLY_BRACKET) + }.to raise_error Nagios::Parser::SyntaxError + end + + describe "when encoutering ';'" do + it "should not throw an exception" do + parser = Nagios::Parser.new + expect { + results = parser.parse(ESCAPED_SEMICOLON) + }.to_not raise_error Nagios::Parser::SyntaxError + end + + it "should ignore it if it is a comment" do + parser = Nagios::Parser.new + results = parser.parse(NONESCAPED_SEMICOLON_COMMENT) + results[0].use.should eql("linux-server") + end + + it "should parse correctly if it is escaped" do + parser = Nagios::Parser.new + results = parser.parse(ESCAPED_SEMICOLON) + results[0].command_line.should eql("$USER3$/check_mysql_health --hostname localhost --username nagioschecks --password nagiosCheckPWD --mode sql --name \"SELECT ROUND(Data_length/1024) as Data_kBytes from INFORMATION_SCHEMA.TABLES where TABLE_NAME=\\\"$ARG1$\\\";\" --name2 \"table size\" --units kBytes -w $ARG2$ -c $ARG3$") + end + end + + describe "when encountering '#'" do + + it "should not throw an exception" do + parser = Nagios::Parser.new + expect { + results = parser.parse(POUND_SIGN_HASH_SYMBOL_NOT_IN_FIRST_COLUMN) + }.to_not raise_error Nagios::Parser::SyntaxError + end + + + it "should ignore it at the beginning of a line" do + parser = Nagios::Parser.new + results = parser.parse(LINE_COMMENT_SNIPPET) + results[0].command_line.should eql("command_line") + end + + it "should let it go anywhere else" do + parser = Nagios::Parser.new + results = parser.parse(POUND_SIGN_HASH_SYMBOL_NOT_IN_FIRST_COLUMN) + results[0].command_line.should eql("/usr/local/bin/riseup-nagios-client.pl \"$HOSTNAME$ ($SERVICEDESC$) $NOTIFICATIONTYPE$ \#$SERVICEATTEMPT$ $SERVICESTATETYPE$ $SERVICEEXECUTIONTIME$s $SERVICELATENCY$s $SERVICEOUTPUT$ $SERVICEPERFDATA$\"") + end + + end + + describe "when encountering ';' again" do + it "should not throw an exception" do + parser = Nagios::Parser.new + expect { + results = parser.parse(ANOTHER_ESCAPED_SEMICOLON) + }.to_not raise_error Nagios::Parser::SyntaxError + end + + it "should parse correctly" do + parser = Nagios::Parser.new + results = parser.parse(ANOTHER_ESCAPED_SEMICOLON) + results[0].command_line.should eql("LC_ALL=en_US.UTF-8 /usr/lib/nagios/plugins/check_haproxy -u 'http://blah:blah@$HOSTADDRESS$:8080/haproxy?stats;csv'") + end + end + + + it "should be idempotent" do + parser = Nagios::Parser.new + src = ANOTHER_ESCAPED_SEMICOLON.dup + results = parser.parse(src) + nagios_type = Nagios::Base.create(:command) + nagios_type.command_name = results[0].command_name + nagios_type.command_line = results[0].command_line + nagios_type.to_s.should eql(ANOTHER_ESCAPED_SEMICOLON) + end + +end + +describe "Nagios generator" do + + it "should escape ';'" do + param = '$USER3$/check_mysql_health --hostname localhost --username nagioschecks --password nagiosCheckPWD --mode sql --name "SELECT ROUND(Data_length/1024) as Data_kBytes from INFORMATION_SCHEMA.TABLES where TABLE_NAME=\"$ARG1$\";" --name2 "table size" --units kBytes -w $ARG2$ -c $ARG3$' + nagios_type = Nagios::Base.create(:command) + nagios_type.command_line = param + nagios_type.to_s.should eql("define command {\n\tcommand_line $USER3$/check_mysql_health --hostname localhost --username nagioschecks --password nagiosCheckPWD --mode sql --name \"SELECT ROUND(Data_length/1024) as Data_kBytes from INFORMATION_SCHEMA.TABLES where TABLE_NAME=\\\"$ARG1$\\\"\\;\" --name2 \"table size\" --units kBytes -w $ARG2$ -c $ARG3$\n}\n") + end + + it "should escape ';' if it is not already the case" do + param = "LC_ALL=en_US.UTF-8 /usr/lib/nagios/plugins/check_haproxy -u 'http://blah:blah@$HOSTADDRESS$:8080/haproxy?stats;csv'" + nagios_type = Nagios::Base.create(:command) + nagios_type.command_line = param + nagios_type.to_s.should eql("define command {\n\tcommand_line LC_ALL=en_US.UTF-8 /usr/lib/nagios/plugins/check_haproxy -u 'http://blah:blah@$HOSTADDRESS$:8080/haproxy?stats\\;csv'\n}\n") + end + + it "should be idempotent" do + param = '$USER3$/check_mysql_health --hostname localhost --username nagioschecks --password nagiosCheckPWD --mode sql --name "SELECT ROUND(Data_length/1024) as Data_kBytes from INFORMATION_SCHEMA.TABLES where TABLE_NAME=\"$ARG1$\";" --name2 "table size" --units kBytes -w $ARG2$ -c $ARG3$' + nagios_type = Nagios::Base.create(:command) + nagios_type.command_line = param + parser = Nagios::Parser.new + results = parser.parse(nagios_type.to_s) + results[0].command_line.should eql(param) + end +end + describe "Nagios resource types" do Nagios::Base.eachtype do |name, nagios_type| puppet_type = Puppet::Type.type("nagios_#{name}") diff --git a/spec/unit/type/package_spec.rb b/spec/unit/type/package_spec.rb index 5312b3f38..fefd15805 100755 --- a/spec/unit/type/package_spec.rb +++ b/spec/unit/type/package_spec.rb @@ -51,7 +51,7 @@ describe Puppet::Type.type(:package) do :clear => nil, :validate_source => nil ) - Puppet::Type.type(:package).defaultprovider.expects(:new).returns(@provider) + Puppet::Type.type(:package).defaultprovider.stubs(:new).returns(@provider) end it "should support :present as a value to :ensure" do @@ -100,6 +100,12 @@ describe Puppet::Type.type(:package) do it "should accept any string as an argument to :source" do expect { Puppet::Type.type(:package).new(:name => "yay", :source => "stuff") }.to_not raise_error end + + it "should not accept a non-string name" do + expect do + Puppet::Type.type(:package).new(:name => ["error"]) + end.to raise_error(Puppet::ResourceError, /Name must be a String/) + end end module PackageEvaluationTesting diff --git a/spec/unit/type/schedule_spec.rb b/spec/unit/type/schedule_spec.rb index 020f7d841..0610bd175 100755 --- a/spec/unit/type/schedule_spec.rb +++ b/spec/unit/type/schedule_spec.rb @@ -62,6 +62,12 @@ describe Puppet::Type.type(:schedule) do end end + it "should not produce default schedules when default_schedules is false" do + Puppet[:default_schedules] = false + schedules = Puppet::Type.type(:schedule).mkdefaultschedules + schedules.must have_exactly(0).items + end + it "should produce a schedule named puppet with a period of hourly and a repeat of 2" do schedules = Puppet::Type.type(:schedule).mkdefaultschedules schedules.find { |s| diff --git a/spec/unit/type/service_spec.rb b/spec/unit/type/service_spec.rb index 630b44b75..e36d11a56 100755 --- a/spec/unit/type/service_spec.rb +++ b/spec/unit/type/service_spec.rb @@ -120,14 +120,14 @@ describe Puppet::Type.type(:service), "when validating attribute values" do end it "should split paths on '#{File::PATH_SEPARATOR}'" do - FileTest.stubs(:exist?).returns(true) + Puppet::FileSystem::File.stubs(:exist?).returns(true) FileTest.stubs(:directory?).returns(true) svc = Puppet::Type.type(:service).new(:name => "yay", :path => "/one/two#{File::PATH_SEPARATOR}/three/four") svc[:path].should == %w{/one/two /three/four} end it "should accept arrays of paths joined by '#{File::PATH_SEPARATOR}'" do - FileTest.stubs(:exist?).returns(true) + Puppet::FileSystem::File.stubs(:exist?).returns(true) FileTest.stubs(:directory?).returns(true) svc = Puppet::Type.type(:service).new(:name => "yay", :path => ["/one#{File::PATH_SEPARATOR}/two", "/three#{File::PATH_SEPARATOR}/four"]) svc[:path].should == %w{/one /two /three /four} @@ -137,7 +137,7 @@ end describe Puppet::Type.type(:service), "when setting default attribute values" do it "should default to the provider's default path if one is available" do FileTest.stubs(:directory?).returns(true) - FileTest.stubs(:exist?).returns(true) + Puppet::FileSystem::File.stubs(:exist?).returns(true) Puppet::Type.type(:service).defaultprovider.stubs(:respond_to?).returns(true) Puppet::Type.type(:service).defaultprovider.stubs(:defpath).returns("testing") diff --git a/spec/unit/type/tidy_spec.rb b/spec/unit/type/tidy_spec.rb index cf3fc828b..abe5d26ff 100755 --- a/spec/unit/type/tidy_spec.rb +++ b/spec/unit/type/tidy_spec.rb @@ -10,17 +10,15 @@ describe tidy do before do @basepath = make_absolute("/what/ever") Puppet.settings.stubs(:use) - - # for an unknown reason some of these specs fails when run individually - # with a failed expectation on File.lstat in the autoloader. - File.stubs(:lstat) end it "should use :lstat when stating a file" do - resource = tidy.new :path => "/foo/bar", :age => "1d" + path = '/foo/bar' + resource = tidy.new :path => path, :age => "1d" stat = mock 'stat' - File.expects(:lstat).with("/foo/bar").returns stat - resource.stat("/foo/bar").should == stat + stub_file = stub(path, :lstat => stat) + Puppet::FileSystem::File.expects(:new).with(path).returns stub_file + resource.stat(path).should == stat end [:age, :size, :path, :matches, :type, :recurse, :rmdirs].each do |param| @@ -130,7 +128,8 @@ describe tidy do before do @tidy = Puppet::Type.type(:tidy).new :path => @basepath @stat = stub 'stat', :ftype => "directory" - File.stubs(:lstat).with(@basepath).returns @stat + @stub_file = stub(@basepath, :lstat => @stat) + Puppet::FileSystem::File.stubs(:new).with(@basepath).returns @stub_file end describe "and generating files" do @@ -160,7 +159,7 @@ describe tidy do end it "should do nothing if the targeted file does not exist" do - File.expects(:lstat).with(@basepath).raises Errno::ENOENT + @stub_file.expects(:lstat).raises Errno::ENOENT @tidy.generate.should == [] end @@ -311,32 +310,33 @@ describe tidy do before do @tidy = Puppet::Type.type(:tidy).new :path => @basepath @stat = stub 'stat', :ftype => "file" - File.stubs(:lstat).with(@basepath).returns @stat + @stub_file = stub(@basepath, :lstat => @stat) + Puppet::FileSystem::File.expects(:new).with(@basepath).returns @stub_file end it "should not try to recurse if the file does not exist" do @tidy[:recurse] = true - File.stubs(:lstat).with(@basepath).returns nil + @stub_file.stubs(:lstat).returns nil @tidy.generate.should == [] end it "should not be tidied if the file does not exist" do - File.expects(:lstat).with(@basepath).raises Errno::ENOENT + @stub_file.expects(:lstat).raises Errno::ENOENT @tidy.should_not be_tidy(@basepath) end it "should not be tidied if the user has no access to the file" do - File.expects(:lstat).with(@basepath).raises Errno::EACCES + @stub_file.expects(:lstat).raises Errno::EACCES @tidy.should_not be_tidy(@basepath) end it "should not be tidied if it is a directory and rmdirs is set to false" do stat = mock 'stat', :ftype => "directory" - File.expects(:lstat).with(@basepath).returns stat + @stub_file.expects(:lstat).returns stat @tidy.should_not be_tidy(@basepath) end diff --git a/spec/unit/type/user_spec.rb b/spec/unit/type/user_spec.rb index dd3dca6d9..2877d25c5 100755 --- a/spec/unit/type/user_spec.rb +++ b/spec/unit/type/user_spec.rb @@ -339,6 +339,15 @@ describe Puppet::Type.type(:user) do end end + describe "when managing comment on Ruby 1.9", :if => String.respond_to?(:encode) do + it "should force value encoding to ASCII-8BIT" do + value = 'abcd'.encode(Encoding::UTF_8) + comment = described_class.new(:name => 'foo', :comment => value) + comment[:comment].should == 'abcd' + comment[:comment].encoding.should == Encoding::ASCII_8BIT + end + end + describe "when manages_solaris_rbac is enabled" do it "should support a :role value for ensure" do expect { described_class.new(:name => 'foo', :ensure => :role) }.to_not raise_error diff --git a/spec/unit/type_spec.rb b/spec/unit/type_spec.rb index b160c743f..ca29d680e 100755 --- a/spec/unit/type_spec.rb +++ b/spec/unit/type_spec.rb @@ -1,9 +1,10 @@ #! /usr/bin/env ruby require 'spec_helper' - +require 'puppet_spec/compiler' describe Puppet::Type, :unless => Puppet.features.microsoft_windows? do include PuppetSpec::Files + include PuppetSpec::Compiler it "should be Comparable" do a = Puppet::Type.type(:notify).new(:name => "a") @@ -63,6 +64,28 @@ describe Puppet::Type, :unless => Puppet.features.microsoft_windows? do end end + it "can retrieve all set parameters" do + resource = Puppet::Type.type(:mount).new(:name => "foo", :fstype => "bar", :pass => 1, :ensure => :present, :tag => 'foo') + params = resource.parameters_with_value + [:name, :provider, :ensure, :fstype, :pass, :dump, :target, :loglevel, :tag].each do |name| + params.should be_include(resource.parameter(name)) + end + end + + it "can not return any `nil` values when retrieving all set parameters" do + resource = Puppet::Type.type(:mount).new(:name => "foo", :fstype => "bar", :pass => 1, :ensure => :present, :tag => 'foo') + params = resource.parameters_with_value + params.should_not be_include(nil) + end + + it "can return an iterator for all set parameters" do + resource = Puppet::Type.type(:notify).new(:name=>'foo',:message=>'bar',:tag=>'baz',:require=> "File['foo']") + params = [:name, :message, :withpath, :loglevel, :tag, :require] + resource.eachparameter { |param| + params.should be_include(param.to_s.to_sym) + } + end + it "should have a method for setting default values for resources" do Puppet::Type.type(:mount).new(:name => "foo").must respond_to(:set_default) end @@ -109,6 +132,36 @@ describe Puppet::Type, :unless => Puppet.features.microsoft_windows? do Puppet::Type.type(:mount).new(:name => "foo").version.should == 0 end + it "reports the correct path even after path is used during setup of the type" do + Puppet::Type.newtype(:testing) do + newparam(:name) do + isnamevar + validate do |value| + path # forces the computation of the path + end + end + end + + ral = compile_to_ral(<<-MANIFEST) + class something { + testing { something: } + } + include something + MANIFEST + + ral.resource("Testing[something]").path.should == "/Stage[main]/Something/Testing[something]" + end + + context "alias metaparam" do + it "creates a new name that can be used for resource references" do + ral = compile_to_ral(<<-MANIFEST) + notify { a: alias => c } + MANIFEST + + expect(ral.resource("Notify[a]")).to eq(ral.resource("Notify[c]")) + end + end + context "resource attributes" do let(:resource) { resource = Puppet::Type.type(:mount).new(:name => "foo") @@ -123,7 +176,8 @@ describe Puppet::Type, :unless => Puppet.features.microsoft_windows? do end it "should have tags" do - resource.tags.should == ["mount", "foo"] + expect(resource).to be_tagged("mount") + expect(resource).to be_tagged("foo") end it "should have a path" do @@ -165,13 +219,19 @@ describe Puppet::Type, :unless => Puppet.features.microsoft_windows? do @resource.event.default_log_level.should == :warning end - {:file => "/my/file", :line => 50, :tags => %{foo bar}}.each do |attr, value| + {:file => "/my/file", :line => 50}.each do |attr, value| it "should set the #{attr}" do @resource.stubs(attr).returns value @resource.event.send(attr).should == value end end + it "should set the tags" do + @resource.tag("abc", "def") + @resource.event.should be_tagged("abc") + @resource.event.should be_tagged("def") + end + it "should allow specification of event attributes" do @resource.event(:status => "noop").status.should == "noop" end @@ -470,6 +530,28 @@ describe Puppet::Type, :unless => Puppet.features.microsoft_windows? do end end + describe "when #finish is called on a type" do + let(:post_hook_type) do + Puppet::Type.newtype(:finish_test) do + newparam(:name) { isnamevar } + + newparam(:post) do + def post_compile + raise "post_compile hook ran" + end + end + end + end + + let(:post_hook_resource) do + post_hook_type.new(:name => 'foo',:post => 'fake_value') + end + + it "should call #post_compile on parameters that implement it" do + expect { post_hook_resource.finish }.to raise_error(RuntimeError, "post_compile hook ran") + end + end + it "should have a class method for converting a hash into a Puppet::Resource instance" do Puppet::Type.type(:mount).must respond_to(:hash2resource) end @@ -588,7 +670,7 @@ describe Puppet::Type, :unless => Puppet.features.microsoft_windows? do resource.should be_a Puppet::Resource resource[:fstype].should == 15 resource[:remounts].should == :true - resource.tags.should =~ %w{foo bar baz mount} + resource.tags.should == Puppet::Util::TagSet.new(%w{foo bar baz mount}) end end diff --git a/spec/unit/util/adsi_spec.rb b/spec/unit/util/adsi_spec.rb index 3b851f5f1..974aba79c 100755 --- a/spec/unit/util/adsi_spec.rb +++ b/spec/unit/util/adsi_spec.rb @@ -52,13 +52,48 @@ describe Puppet::Util::ADSI do end end + describe ".sid_uri", :if => Puppet.features.microsoft_windows? do + it "should raise an error when the input is not a SID object" do + [Object.new, {}, 1, :symbol, '', nil].each do |input| + expect { + Puppet::Util::ADSI.sid_uri(input) + }.to raise_error(Puppet::Error, /Must use a valid SID object/) + end + end + + it "should return a SID uri for a well-known SID (SYSTEM)" do + sid = Win32::Security::SID.new('SYSTEM') + Puppet::Util::ADSI.sid_uri(sid).should == 'WinNT://S-1-5-18' + end + end + describe Puppet::Util::ADSI::User do let(:username) { 'testuser' } + let(:domain) { 'DOMAIN' } + let(:domain_username) { "#{domain}\\#{username}"} it "should generate the correct URI" do Puppet::Util::ADSI::User.uri(username).should == "WinNT://./#{username},user" end + it "should generate the correct URI for a user with a domain" do + Puppet::Util::ADSI::User.uri(username, domain).should == "WinNT://#{domain}/#{username},user" + end + + it "should be able to parse a username without a domain" do + Puppet::Util::ADSI::User.parse_name(username).should == [username, '.'] + end + + it "should be able to parse a username with a domain" do + Puppet::Util::ADSI::User.parse_name(domain_username).should == [username, domain] + end + + it "should raise an error with a username that contains a /" do + expect { + Puppet::Util::ADSI::User.parse_name("#{domain}/#{username}") + }.to raise_error(Puppet::Error, /Value must be in DOMAIN\\user style syntax/) + end + it "should be able to create a user" do adsi_user = stub('adsi') @@ -76,6 +111,11 @@ describe Puppet::Util::ADSI do Puppet::Util::ADSI::User.exists?(username).should be_true end + it "should be able to check the existence of a domain user" do + Puppet::Util::ADSI.expects(:connect).with("WinNT://#{domain}/#{username},user").returns connection + Puppet::Util::ADSI::User.exists?(domain_username).should be_true + end + it "should be able to delete a user" do connection.expects(:Delete).with('user', username) @@ -85,7 +125,7 @@ describe Puppet::Util::ADSI do it "should return an enumeration of IADsUser wrapped objects" do name = 'Administrator' wmi_users = [stub('WMI', :name => name)] - Puppet::Util::ADSI.expects(:execquery).with("select name from win32_useraccount").returns(wmi_users) + Puppet::Util::ADSI.expects(:execquery).with('select name from win32_useraccount where localaccount = "TRUE"').returns(wmi_users) native_user = stub('IADsUser') homedir = "C:\\Users\\#{name}" @@ -99,7 +139,8 @@ describe Puppet::Util::ADSI do end describe "an instance" do - let(:adsi_user) { stub 'user' } + let(:adsi_user) { stub('user', :objectSID => []) } + let(:sid) { stub(:account => username, :domain => 'testcomputername') } let(:user) { Puppet::Util::ADSI::User.new(username, adsi_user) } it "should provide its groups as a list of names" do @@ -133,14 +174,16 @@ describe Puppet::Util::ADSI do user.password = 'pwd' end - it "should generate the correct URI" do - user.uri.should == "WinNT://./#{username},user" + it "should generate the correct URI",:if => Puppet.features.microsoft_windows? do + Puppet::Util::Windows::Security.stubs(:octet_string_to_sid_object).returns(sid) + user.uri.should == "WinNT://testcomputername/#{username},user" end - describe "when given a set of groups to which to add the user" do + describe "when given a set of groups to which to add the user", :if => Puppet.features.microsoft_windows? do let(:groups_to_set) { 'group1,group2' } before(:each) do + Puppet::Util::Windows::Security.stubs(:octet_string_to_sid_object).returns(sid) user.expects(:groups).returns ['group2', 'group3'] end @@ -152,6 +195,7 @@ describe Puppet::Util::ADSI do group3 = stub 'group1' group3.expects(:Remove).with("WinNT://testcomputername/#{username},user") + Puppet::Util::ADSI.expects(:sid_uri).with(sid).returns("WinNT://testcomputername/#{username},user").twice Puppet::Util::ADSI.expects(:connect).with('WinNT://./group1,group').returns group1 Puppet::Util::ADSI.expects(:connect).with('WinNT://./group3,group').returns group3 @@ -164,6 +208,7 @@ describe Puppet::Util::ADSI do group1 = stub 'group1' group1.expects(:Add).with("WinNT://testcomputername/#{username},user") + Puppet::Util::ADSI.expects(:sid_uri).with(sid).returns("WinNT://testcomputername/#{username},user") Puppet::Util::ADSI.expects(:connect).with('WinNT://./group1,group').returns group1 user.set_groups(groups_to_set, true) @@ -179,19 +224,58 @@ describe Puppet::Util::ADSI do describe "an instance" do let(:adsi_group) { stub 'group' } let(:group) { Puppet::Util::ADSI::Group.new(groupname, adsi_group) } + let(:someone_sid){ stub(:account => 'someone', :domain => 'testcomputername')} + + it "should be able to add a member (deprecated)", :if => Puppet.features.microsoft_windows? do + Puppet.expects(:deprecation_warning).with('Puppet::Util::ADSI::Group#add_members is deprecated; please use Puppet::Util::ADSI::Group#add_member_sids') + + Puppet::Util::Windows::Security.expects(:name_to_sid_object).with('someone').returns(someone_sid) + Puppet::Util::ADSI.expects(:sid_uri).with(someone_sid).returns("WinNT://testcomputername/someone,user") - it "should be able to add a member" do adsi_group.expects(:Add).with("WinNT://testcomputername/someone,user") group.add_member('someone') end - it "should be able to remove a member" do + it "should raise when adding a member that can't resolve to a SID (deprecated)", :if => Puppet.features.microsoft_windows? do + expect { + group.add_member('foobar') + }.to raise_error(Puppet::Error, /Could not resolve username: foobar/) + end + + it "should be able to remove a member (deprecated)", :if => Puppet.features.microsoft_windows? do + Puppet.expects(:deprecation_warning).with('Puppet::Util::ADSI::Group#remove_members is deprecated; please use Puppet::Util::ADSI::Group#remove_member_sids') + + Puppet::Util::Windows::Security.expects(:name_to_sid_object).with('someone').returns(someone_sid) + Puppet::Util::ADSI.expects(:sid_uri).with(someone_sid).returns("WinNT://testcomputername/someone,user") + adsi_group.expects(:Remove).with("WinNT://testcomputername/someone,user") group.remove_member('someone') end + it "should raise when removing a member that can't resolve to a SID (deprecated)", :if => Puppet.features.microsoft_windows? do + expect { + group.remove_member('foobar') + }.to raise_error(Puppet::Error, /Could not resolve username: foobar/) + end + + describe "should be able to use SID objects", :if => Puppet.features.microsoft_windows? do + let(:system) { Puppet::Util::Windows::Security.name_to_sid_object('SYSTEM') } + + it "to add a member" do + adsi_group.expects(:Add).with("WinNT://S-1-5-18") + + group.add_member_sids(system) + end + + it "to remove a member" do + adsi_group.expects(:Remove).with("WinNT://S-1-5-18") + + group.remove_member_sids(system) + end + end + it "should provide its groups as a list of names" do names = ['user1', 'user2'] @@ -202,14 +286,38 @@ describe Puppet::Util::ADSI do group.members.should =~ names end - it "should be able to add a list of users to a group" do - names = ['user1', 'user2'] - adsi_group.expects(:Members).returns names.map{|n| stub(:Name => n)} + it "should be able to add a list of users to a group", :if => Puppet.features.microsoft_windows? do + names = ['DOMAIN\user1', 'user2'] + sids = [ + stub(:account => 'user1', :domain => 'DOMAIN'), + stub(:account => 'user2', :domain => 'testcomputername'), + stub(:account => 'user3', :domain => 'DOMAIN2'), + ] + + # use stubbed objectSid on member to return stubbed SID + Puppet::Util::Windows::Security.expects(:octet_string_to_sid_object).with([0]).returns(sids[0]) + Puppet::Util::Windows::Security.expects(:octet_string_to_sid_object).with([1]).returns(sids[1]) - adsi_group.expects(:Remove).with('WinNT://testcomputername/user1,user') - adsi_group.expects(:Add).with('WinNT://testcomputername/user3,user') + Puppet::Util::Windows::Security.expects(:name_to_sid_object).with('user2').returns(sids[1]) + Puppet::Util::Windows::Security.expects(:name_to_sid_object).with('DOMAIN2\user3').returns(sids[2]) + + Puppet::Util::ADSI.expects(:sid_uri).with(sids[0]).returns("WinNT://DOMAIN/user1,user") + Puppet::Util::ADSI.expects(:sid_uri).with(sids[2]).returns("WinNT://DOMAIN2/user3,user") + + members = names.each_with_index.map{|n,i| stub(:Name => n, :objectSID => [i])} + adsi_group.expects(:Members).returns members + + adsi_group.expects(:Remove).with('WinNT://DOMAIN/user1,user') + adsi_group.expects(:Add).with('WinNT://DOMAIN2/user3,user') + + group.set_members(['user2', 'DOMAIN2\user3']) + end - group.set_members(['user2', 'user3']) + it "should raise an error when a username does not resolve to a SID", :if => Puppet.features.microsoft_windows? do + expect { + adsi_group.expects(:Members).returns [] + group.set_members(['foobar']) + }.to raise_error(Puppet::Error, /Could not resolve username: foobar/) end it "should generate the correct URI" do @@ -248,7 +356,7 @@ describe Puppet::Util::ADSI do it "should return an enumeration of IADsGroup wrapped objects" do name = 'Administrators' wmi_groups = [stub('WMI', :name => name)] - Puppet::Util::ADSI.expects(:execquery).with("select name from win32_group").returns(wmi_groups) + Puppet::Util::ADSI.expects(:execquery).with('select name from win32_group where localaccount = "TRUE"').returns(wmi_groups) native_group = stub('IADsGroup') native_group.expects(:Members).returns([stub(:Name => 'Administrator')]) diff --git a/spec/unit/util/autoload_spec.rb b/spec/unit/util/autoload_spec.rb index 933855914..4a0782ac7 100755 --- a/spec/unit/util/autoload_spec.rb +++ b/spec/unit/util/autoload_spec.rb @@ -71,7 +71,7 @@ describe Puppet::Util::Autoload do [RuntimeError, LoadError, SyntaxError].each do |error| it "should die with Puppet::Error if a #{error.to_s} exception is thrown" do - File.stubs(:exist?).returns true + Puppet::FileSystem::File.stubs(:exist?).returns true Kernel.expects(:load).raises error @@ -84,7 +84,7 @@ describe Puppet::Util::Autoload do end it "should register loaded files with the autoloader" do - File.stubs(:exist?).returns true + Puppet::FileSystem::File.stubs(:exist?).returns true Kernel.stubs(:load) @autoload.load("myfile") @@ -94,7 +94,7 @@ describe Puppet::Util::Autoload do end it "should be seen by loaded? on the instance using the short name" do - File.stubs(:exist?).returns true + Puppet::FileSystem::File.stubs(:exist?).returns true Kernel.stubs(:load) @autoload.load("myfile") @@ -104,7 +104,7 @@ describe Puppet::Util::Autoload do end it "should register loaded files with the main loaded file list so they are not reloaded by ruby" do - File.stubs(:exist?).returns true + Puppet::FileSystem::File.stubs(:exist?).returns true Kernel.stubs(:load) @autoload.load("myfile") @@ -117,7 +117,7 @@ describe Puppet::Util::Autoload do it "should load the first file in the searchpath" do @autoload.stubs(:search_directories).returns [make_absolute("/a"), make_absolute("/b")] FileTest.stubs(:directory?).returns true - File.stubs(:exist?).returns true + Puppet::FileSystem::File.stubs(:exist?).returns true Kernel.expects(:load).with(make_absolute("/a/tmp/myfile.rb"), optionally(anything)) @autoload.load("myfile") @@ -126,7 +126,7 @@ describe Puppet::Util::Autoload do end it "should treat equivalent paths to a loaded file as loaded" do - File.stubs(:exist?).returns true + Puppet::FileSystem::File.stubs(:exist?).returns true Kernel.stubs(:load) @autoload.load("myfile") @@ -144,7 +144,7 @@ describe Puppet::Util::Autoload do @autoload.class.stubs(:search_directories).returns [make_absolute("/a")] FileTest.stubs(:directory?).returns true Dir.stubs(:glob).returns [make_absolute("/a/foo/file.rb")] - File.stubs(:exist?).returns true + Puppet::FileSystem::File.stubs(:exist?).returns true @time_a = Time.utc(2010, 'jan', 1, 6, 30) File.stubs(:mtime).returns @time_a @@ -185,7 +185,7 @@ describe Puppet::Util::Autoload do it "changes should be seen by changed? on the instance using the short name" do File.stubs(:mtime).returns(@first_time) - File.stubs(:exist?).returns true + Puppet::FileSystem::File.stubs(:exist?).returns true Kernel.stubs(:load) @autoload.load("myfile") @autoload.loaded?("myfile").should be @@ -206,14 +206,14 @@ describe Puppet::Util::Autoload do it "should reload if mtime changes" do File.stubs(:mtime).with(@file_a).returns(@first_time + 60) - File.stubs(:exist?).with(@file_a).returns true + Puppet::FileSystem::File.stubs(:exist?).with(@file_a).returns true Kernel.expects(:load).with(@file_a, optionally(anything)) @autoload.class.reload_changed end it "should do nothing if the file is deleted" do File.stubs(:mtime).with(@file_a).raises(Errno::ENOENT) - File.stubs(:exist?).with(@file_a).returns false + Puppet::FileSystem::File.stubs(:exist?).with(@file_a).returns false Kernel.expects(:load).never @autoload.class.reload_changed end @@ -228,8 +228,8 @@ describe Puppet::Util::Autoload do File.expects(:mtime).with(@file_a).returns(@first_time) @autoload.class.mark_loaded("file", @file_a) File.stubs(:mtime).with(@file_a).raises(Errno::ENOENT) - File.stubs(:exist?).with(@file_a).returns false - File.stubs(:exist?).with(@file_b).returns true + Puppet::FileSystem::File.stubs(:exist?).with(@file_a).returns false + Puppet::FileSystem::File.stubs(:exist?).with(@file_b).returns true File.stubs(:mtime).with(@file_b).returns @first_time Kernel.expects(:load).with(@file_b, optionally(anything)) @autoload.class.reload_changed @@ -238,11 +238,11 @@ describe Puppet::Util::Autoload do it "should load a/file when b/file is loaded and a/file is created" do File.stubs(:mtime).with(@file_b).returns @first_time - File.stubs(:exist?).with(@file_b).returns true + Puppet::FileSystem::File.stubs(:exist?).with(@file_b).returns true @autoload.class.mark_loaded("file", @file_b) File.stubs(:mtime).with(@file_a).returns @first_time - File.stubs(:exist?).with(@file_a).returns true + Puppet::FileSystem::File.stubs(:exist?).with(@file_a).returns true Kernel.expects(:load).with(@file_a, optionally(anything)) @autoload.class.reload_changed @autoload.class.send(:loaded)["file"].should == [@file_a, @first_time] diff --git a/spec/unit/util/backups_spec.rb b/spec/unit/util/backups_spec.rb index 8d3ea1956..654ddb788 100755 --- a/spec/unit/util/backups_spec.rb +++ b/spec/unit/util/backups_spec.rb @@ -20,7 +20,7 @@ describe Puppet::Util::Backups do file = Puppet::Type.type(:file).new(:name => path) file.expects(:bucket).never - FileTest.expects(:exists?).with(path).returns false + Puppet::FileSystem::File.expects(:exist?).with(path).returns false file.perform_backup end @@ -29,23 +29,25 @@ describe Puppet::Util::Backups do file = Puppet::Type.type(:file).new(:name => path, :backup => false) file.expects(:bucket).never - FileTest.expects(:exists?).never + Puppet::FileSystem::File.expects(:exist?).never file.perform_backup end it "a bucket should be used when provided" do - File.stubs(:lstat).with(path).returns(mock('lstat', :ftype => 'file')) + stub_file = stub(path, :lstat => mock('lstat', :ftype => 'file')) + Puppet::FileSystem::File.expects(:new).with(path).returns stub_file bucket.expects(:backup).with(path).returns("mysum") - FileTest.expects(:exists?).with(path).returns(true) + Puppet::FileSystem::File.expects(:exist?).with(path).returns(true) file.perform_backup end it "should propagate any exceptions encountered when backing up to a filebucket" do - File.stubs(:lstat).with(path).returns(mock('lstat', :ftype => 'file')) + stub_file = stub(path, :lstat => mock('lstat', :ftype => 'file')) + Puppet::FileSystem::File.expects(:new).with(path).returns stub_file bucket.expects(:backup).raises ArgumentError - FileTest.expects(:exists?).with(path).returns(true) + Puppet::FileSystem::File.expects(:exist?).with(path).returns(true) lambda { file.perform_backup }.should raise_error(ArgumentError) end @@ -56,35 +58,39 @@ describe Puppet::Util::Backups do let(:file) { Puppet::Type.type(:file).new(:name => path, :backup => '.'+ext) } it "should remove any local backup if one exists" do - File.expects(:lstat).with(backup).returns stub("stat", :ftype => "file") - File.expects(:unlink).with(backup) + stub_file = stub(backup, :lstat => stub('stat', :ftype => 'file')) + Puppet::FileSystem::File.expects(:new).with(backup).returns stub_file + Puppet::FileSystem::File.expects(:unlink).with(backup) FileUtils.stubs(:cp_r) - FileTest.expects(:exists?).with(path).returns(true) + Puppet::FileSystem::File.expects(:exist?).with(path).returns(true) file.perform_backup end it "should fail when the old backup can't be removed" do - File.expects(:lstat).with(backup).returns stub("stat", :ftype => "file") - File.expects(:unlink).with(backup).raises ArgumentError + stub_file = stub(backup, :lstat => stub('stat', :ftype => 'file')) + Puppet::FileSystem::File.expects(:new).with(backup).returns stub_file + Puppet::FileSystem::File.expects(:unlink).with(backup).raises ArgumentError FileUtils.expects(:cp_r).never - FileTest.expects(:exists?).with(path).returns(true) + Puppet::FileSystem::File.expects(:exist?).with(path).returns(true) lambda { file.perform_backup }.should raise_error(Puppet::Error) end it "should not try to remove backups that don't exist" do - File.expects(:lstat).with(backup).raises(Errno::ENOENT) - File.expects(:unlink).with(backup).never + stub_file = stub(backup) + Puppet::FileSystem::File.expects(:new).with(backup).returns stub_file + stub_file.expects(:lstat).raises(Errno::ENOENT) + Puppet::FileSystem::File.expects(:unlink).with(backup).never FileUtils.stubs(:cp_r) - FileTest.expects(:exists?).with(path).returns(true) + Puppet::FileSystem::File.expects(:exist?).with(path).returns(true) file.perform_backup end it "a copy should be created in the local directory" do FileUtils.expects(:cp_r).with(path, backup, :preserve => true) - FileTest.stubs(:exists?).with(path).returns(true) + Puppet::FileSystem::File.stubs(:exist?).with(path).returns(true) file.perform_backup.should be_true end @@ -92,7 +98,7 @@ describe Puppet::Util::Backups do it "should propagate exceptions if no backup can be created" do FileUtils.expects(:cp_r).raises ArgumentError - FileTest.stubs(:exists?).with(path).returns(true) + Puppet::FileSystem::File.stubs(:exist?).with(path).returns(true) lambda { file.perform_backup }.should raise_error(Puppet::Error) end end @@ -108,10 +114,11 @@ describe Puppet::Util::Backups do bucket.expects(:backup).with(filename).returns true - File.stubs(:lstat).with(path).returns(stub('lstat', :ftype => 'directory')) + stub_file = stub(path, :lstat => stub('stat', :ftype => 'directory')) + Puppet::FileSystem::File.expects(:new).with(path).returns stub_file - FileTest.stubs(:exists?).with(path).returns(true) - FileTest.stubs(:exists?).with(filename).returns(true) + Puppet::FileSystem::File.stubs(:exist?).with(path).returns(true) + Puppet::FileSystem::File.stubs(:exist?).with(filename).returns(true) file.perform_backup end @@ -120,7 +127,8 @@ describe Puppet::Util::Backups do file = Puppet::Type.type(:file).new(:name => path, :backup => 'foo', :recurse => true) bucket.expects(:backup).never - File.stubs(:stat).with(path).returns(stub('stat', :ftype => 'directory')) + stub_file = stub('file', :stat => stub('stat', :ftype => 'directory')) + Puppet::FileSystem::File.stubs(:new).with(path).returns stub_file Find.expects(:find).never file.perform_backup diff --git a/spec/unit/util/checksums_spec.rb b/spec/unit/util/checksums_spec.rb index e928ca8ae..0adff7a9a 100755 --- a/spec/unit/util/checksums_spec.rb +++ b/spec/unit/util/checksums_spec.rb @@ -132,7 +132,8 @@ describe Puppet::Util::Checksums do file = "/my/file" stat = mock 'stat', sum => "mysum" - File.expects(:stat).with(file).returns(stat) + stub_file = stub(file, :stat => stat) + Puppet::FileSystem::File.expects(:new).with(file).returns stub_file @summer.send(sum.to_s + "_file", file).should == "mysum" end diff --git a/spec/unit/util/command_line_spec.rb b/spec/unit/util/command_line_spec.rb index 3d8f896d1..6ba8077c2 100755 --- a/spec/unit/util/command_line_spec.rb +++ b/spec/unit/util/command_line_spec.rb @@ -143,5 +143,46 @@ describe Puppet::Util::CommandLine do Puppet::Util::CommandLine.available_subcommands end end + + describe 'when setting process priority' do + let(:command_line) do + Puppet::Util::CommandLine.new("puppet", %w{ agent }) + end + + before :each do + Puppet::Util::CommandLine::ApplicationSubcommand.any_instance.stubs(:run) + end + + it 'should never set priority by default' do + Process.expects(:setpriority).never + + command_line.execute + end + + it 'should lower the process priority if one has been specified' do + Puppet[:priority] = 10 + + Process.expects(:setpriority).with(0, Process.pid, 10) + command_line.execute + end + + it 'should warn if trying to raise priority, but not privileged user' do + Puppet[:priority] = -10 + + Process.expects(:setpriority).raises(Errno::EACCES, 'Permission denied') + Puppet.expects(:warning).with("Failed to set process priority to '-10'") + + command_line.execute + end + + it "should warn if the platform doesn't support `Process.setpriority`" do + Puppet[:priority] = 15 + + Process.expects(:setpriority).raises(NotImplementedError, 'NotImplementedError: setpriority() function is unimplemented on this machine') + Puppet.expects(:warning).with("Failed to set process priority to '15'") + + command_line.execute + end + end end end diff --git a/spec/unit/util/docs_spec.rb b/spec/unit/util/docs_spec.rb new file mode 100644 index 000000000..eb736ff72 --- /dev/null +++ b/spec/unit/util/docs_spec.rb @@ -0,0 +1,91 @@ +require 'spec_helper' + +describe Puppet::Util::Docs do + + describe '.scrub' do + let(:my_cleaned_output) do + %q{This resource type uses the prescribed native tools for creating +groups and generally uses POSIX APIs for retrieving information +about them. It does not directly modify `/etc/passwd` or anything. + +* Just for fun, we'll add a list. +* list item two, + which has some add'l lines included in it. + +And here's a code block: + + this is the piece of code + it does something cool + +**Autorequires:** I would be listing autorequired resources here.} + end + + it "strips the least common indent from multi-line strings, without mangling indentation beyond the least common indent" do + input = <<EOT + This resource type uses the prescribed native tools for creating + groups and generally uses POSIX APIs for retrieving information + about them. It does not directly modify `/etc/passwd` or anything. + + * Just for fun, we'll add a list. + * list item two, + which has some add'l lines included in it. + + And here's a code block: + + this is the piece of code + it does something cool + + **Autorequires:** I would be listing autorequired resources here. +EOT + output = Puppet::Util::Docs.scrub(input) + expect(output).to eq my_cleaned_output + end + + it "ignores the first line when calculating least common indent" do + input = "This resource type uses the prescribed native tools for creating + groups and generally uses POSIX APIs for retrieving information + about them. It does not directly modify `/etc/passwd` or anything. + + * Just for fun, we'll add a list. + * list item two, + which has some add'l lines included in it. + + And here's a code block: + + this is the piece of code + it does something cool + + **Autorequires:** I would be listing autorequired resources here." + output = Puppet::Util::Docs.scrub(input) + expect(output).to eq my_cleaned_output + end + + it "strips trailing whitespace from each line, and strips trailing newlines at end" do + input = "This resource type uses the prescribed native tools for creating \n groups and generally uses POSIX APIs for retrieving information \n about them. It does not directly modify `/etc/passwd` or anything. \n\n * Just for fun, we'll add a list. \n * list item two,\n which has some add'l lines included in it. \n\n And here's a code block:\n\n this is the piece of code \n it does something cool \n\n **Autorequires:** I would be listing autorequired resources here. \n\n" + output = Puppet::Util::Docs.scrub(input) + expect(output).to eq my_cleaned_output + end + + it "has no side effects on original input string" do + input = "First line \n second line \n \n indented line \n \n last line\n\n" + clean_input = "First line \n second line \n \n indented line \n \n last line\n\n" + not_used = Puppet::Util::Docs.scrub(input) + expect(input).to eq clean_input + end + + it "does not include whitespace-only lines when calculating least common indent" do + input = "First line\n second line\n \n indented line\n\n last line" + expected_output = "First line\nsecond line\n\n indented line\n\nlast line" + #bogus_output = "First line\nsecond line\n\n indented line\n\nlast line" + output = Puppet::Util::Docs.scrub(input) + expect(output).to eq expected_output + end + + it "accepts a least common indent of zero, thus not adding errors when input string is already scrubbed" do + expect(Puppet::Util::Docs.scrub(my_cleaned_output)).to eq my_cleaned_output + end + + + end +end + diff --git a/spec/unit/util/execution_spec.rb b/spec/unit/util/execution_spec.rb index 71af8f01f..7bb15cd75 100755 --- a/spec/unit/util/execution_spec.rb +++ b/spec/unit/util/execution_spec.rb @@ -306,11 +306,21 @@ describe Puppet::Util::Execution do Puppet::Util::Execution.stubs(:execute_windows).returns(proc_info_stub) Puppet::Util::Windows::Process.expects(:wait_process).with(process_handle).raises('whatever') - Process.expects(:CloseHandle).with(thread_handle) - Process.expects(:CloseHandle).with(process_handle) + Puppet::Util::Windows::Process.expects(:CloseHandle).with(thread_handle) + Puppet::Util::Windows::Process.expects(:CloseHandle).with(process_handle) expect { Puppet::Util::Execution.execute('test command') }.to raise_error(RuntimeError) end + + it "should return the correct exit status even when exit status is greater than 256" do + real_exit_status = 3010 + + Puppet::Util::Execution.stubs(:execute_windows).returns(proc_info_stub) + stub_process_wait(real_exit_status) + $CHILD_STATUS.stubs(:exitstatus).returns(real_exit_status % 256) # The exitstatus is changed to be mod 256 so that ruby can fit it into 8 bits. + + Puppet::Util::Execution.execute('test command', :failonfail => false).exitstatus.should == real_exit_status + end end end @@ -509,7 +519,7 @@ describe Puppet::Util::Execution do Tempfile.stubs(:new).returns(stdout) stdout.write("My expected command output") - Puppet::Util::Execution.execute('test command', :squelch => true).should == nil + Puppet::Util::Execution.execute('test command', :squelch => true).should == '' end it "should delete the file used for output if squelch is false" do @@ -519,7 +529,7 @@ describe Puppet::Util::Execution do Puppet::Util::Execution.execute('test command') - File.should_not be_exist(path) + Puppet::FileSystem::File.exist?(path).should be_false end it "should not raise an error if the file is open" do @@ -591,6 +601,20 @@ describe Puppet::Util::Execution do Puppet::Util::Execution.execpipe('echo hello').should == 'hello' end + it "should print meaningful debug message for string argument" do + Puppet::Util::Execution.expects(:debug).with("Executing 'echo hello'") + Puppet::Util::Execution.expects(:open).with('| echo hello 2>&1').returns('hello') + $CHILD_STATUS.expects(:==).with(0).returns(true) + Puppet::Util::Execution.execpipe('echo hello') + end + + it "should print meaningful debug message for array argument" do + Puppet::Util::Execution.expects(:debug).with("Executing 'echo hello'") + Puppet::Util::Execution.expects(:open).with('| echo hello 2>&1').returns('hello') + $CHILD_STATUS.expects(:==).with(0).returns(true) + Puppet::Util::Execution.execpipe(['echo','hello']) + end + it "should execute an array by pasting together with spaces" do Puppet::Util::Execution.expects(:open).with('| echo hello 2>&1').returns('hello') $CHILD_STATUS.expects(:==).with(0).returns(true) diff --git a/spec/unit/util/filetype_spec.rb b/spec/unit/util/filetype_spec.rb index ecf7c3690..5d8f0b36d 100755 --- a/spec/unit/util/filetype_spec.rb +++ b/spec/unit/util/filetype_spec.rb @@ -16,15 +16,15 @@ describe Puppet::Util::FileType do describe "when the file already exists" do it "should return the file's contents when asked to read it" do - File.expects(:exist?).with(path).returns true + Puppet::FileSystem::File.expects(:exist?).with(path).returns true File.expects(:read).with(path).returns "my text" file.read.should == "my text" end it "should unlink the file when asked to remove it" do - File.expects(:exist?).with(path).returns true - File.expects(:unlink).with(path) + Puppet::FileSystem::File.expects(:exist?).with(path).returns true + Puppet::FileSystem::File.expects(:unlink).with(path) file.remove end @@ -32,7 +32,7 @@ describe Puppet::Util::FileType do describe "when the file does not exist" do it "should return an empty string when asked to read the file" do - File.expects(:exist?).with(path).returns false + Puppet::FileSystem::File.expects(:exist?).with(path).returns false file.read.should == "" end @@ -63,13 +63,13 @@ describe Puppet::Util::FileType do describe "when backing up a file" do it "should do nothing if the file does not exist" do - File.expects(:exists?).with(path).returns false + Puppet::FileSystem::File.expects(:exist?).with(path).returns false file.expects(:bucket).never file.backup end it "should use its filebucket to backup the file if it exists" do - File.expects(:exists?).with(path).returns true + Puppet::FileSystem::File.expects(:exist?).with(path).returns true bucket = mock 'bucket' bucket.expects(:backup).with(path) @@ -155,7 +155,7 @@ describe Puppet::Util::FileType do end after :each do - File.should_not be_exist @tmp_cron_path + Puppet::FileSystem::File.exist?(@tmp_cron_path).should be_false end it "should run crontab as the target user on a temporary file" do diff --git a/spec/unit/util/lockfile_spec.rb b/spec/unit/util/lockfile_spec.rb index 4f3463429..8d10d5106 100644 --- a/spec/unit/util/lockfile_spec.rb +++ b/spec/unit/util/lockfile_spec.rb @@ -25,7 +25,7 @@ describe Puppet::Util::Lockfile do it "should create a lock file" do @lock.lock - File.should be_exists(@lockfile) + Puppet::FileSystem::File.exist?(@lockfile).should be_true end it "should create a lock file containing a string" do @@ -49,7 +49,7 @@ describe Puppet::Util::Lockfile do it "should clear the lock file" do File.open(@lockfile, 'w') { |fd| fd.print("locked") } @lock.unlock - File.should_not be_exists(@lockfile) + Puppet::FileSystem::File.exist?(@lockfile).should be_false end end diff --git a/spec/unit/util/log/destinations_spec.rb b/spec/unit/util/log/destinations_spec.rb index 332e8ad7c..b34b973cf 100755 --- a/spec/unit/util/log/destinations_spec.rb +++ b/spec/unit/util/log/destinations_spec.rb @@ -108,6 +108,38 @@ describe Puppet::Util::Log.desttypes[:syslog] do end end +describe Puppet::Util::Log.desttypes[:logstash_event] do + + describe "when using structured log format with logstash_event schema" do + before :each do + @msg = Puppet::Util::Log.new(:level => :info, :message => "So long, and thanks for all the fish.", :source => "a dolphin") + end + + it "format should fix the hash to have the correct structure" do + dest = described_class.new + result = dest.format(@msg) + result["version"].should == 1 + result["level"].should == :info + result["message"].should == "So long, and thanks for all the fish." + result["source"].should == "a dolphin" + # timestamp should be within 10 seconds + Time.parse(result["@timestamp"]).should >= ( Time.now - 10 ) + end + + it "format returns a structure that can be converted to json" do + dest = described_class.new + hash = dest.format(@msg) + JSON.parse(hash.to_json) + end + + it "handle should send the output to stdout" do + $stdout.expects(:puts).once + dest = described_class.new + dest.handle(@msg) + end + end +end + describe Puppet::Util::Log.desttypes[:console] do let (:klass) { Puppet::Util::Log.desttypes[:console] } diff --git a/spec/unit/util/monkey_patches_spec.rb b/spec/unit/util/monkey_patches_spec.rb index cc487020c..13e10454c 100755 --- a/spec/unit/util/monkey_patches_spec.rb +++ b/spec/unit/util/monkey_patches_spec.rb @@ -280,6 +280,47 @@ describe OpenSSL::SSL::SSLContext do end end + +describe OpenSSL::X509::Store, :if => Puppet::Util::Platform.windows? do + let(:store) { described_class.new } + let(:cert) { OpenSSL::X509::Certificate.new(File.read(my_fixture('x509.pem'))) } + + def with_root_certs(certs) + Puppet::Util::Windows::RootCerts.expects(:instance).returns(certs) + end + + it "adds a root cert to the store" do + with_root_certs([cert]) + + store.set_default_paths + end + + it "ignores duplicate root certs" do + with_root_certs([cert, cert]) + + store.expects(:add_cert).with(cert).once + + store.set_default_paths + end + + it "warns when adding a certificate that already exists" do + with_root_certs([cert]) + store.add_cert(cert) + + store.expects(:warn).with('Failed to add /DC=com/DC=microsoft/CN=Microsoft Root Certificate Authority') + + store.set_default_paths + end + + it "raises when adding an invalid certificate" do + with_root_certs(['notacert']) + + expect { + store.set_default_paths + }.to raise_error(TypeError) + end +end + describe SecureRandom do it 'generates a properly formatted uuid' do SecureRandom.uuid.should =~ /[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/i diff --git a/spec/unit/util/pidlock_spec.rb b/spec/unit/util/pidlock_spec.rb index 3e70ce008..e9c28f2b0 100644 --- a/spec/unit/util/pidlock_spec.rb +++ b/spec/unit/util/pidlock_spec.rb @@ -47,7 +47,7 @@ describe Puppet::Util::Pidlock do it "should create a lock file" do @lock.lock - File.should be_exists(@lockfile) + Puppet::FileSystem::File.exist?(@lockfile).should be_true end it "should expose the lock file_path" do @@ -74,7 +74,7 @@ describe Puppet::Util::Pidlock do it "should get rid of the lock file" do @lock.lock @lock.unlock - File.should_not be_exists(@lockfile) + Puppet::FileSystem::File.exist?(@lockfile).should be_false end end @@ -106,12 +106,12 @@ describe Puppet::Util::Pidlock do describe "#lock" do it "should clear stale locks" do @lock.locked? - File.should_not be_exists(@lockfile) + Puppet::FileSystem::File.exist?(@lockfile).should be_false end it "should replace with new locks" do @lock.lock - File.should be_exists(@lockfile) + Puppet::FileSystem::File.exist?(@lockfile).should be_true @lock.lock_pid.should == 6789 @lock.should be_mine @lock.should be_locked @@ -125,7 +125,7 @@ describe Puppet::Util::Pidlock do it "should not remove the lock file" do @lock.unlock - File.should be_exists(@lockfile) + Puppet::FileSystem::File.exist?(@lockfile).should be_true end end end @@ -170,7 +170,7 @@ describe Puppet::Util::Pidlock do it "should not remove the lock file" do @lock.unlock - File.should be_exists(@lockfile) + Puppet::FileSystem::File.exist?(@lockfile).should be_true end it "should still not be our lock" do diff --git a/spec/unit/util/rdoc/parser_spec.rb b/spec/unit/util/rdoc/parser_spec.rb index 476d15214..1699020a7 100755 --- a/spec/unit/util/rdoc/parser_spec.rb +++ b/spec/unit/util/rdoc/parser_spec.rb @@ -13,7 +13,9 @@ describe "RDoc::Parser", :if => Puppet.features.rdoc1? do include PuppetSpec::Files before :each do - File.stubs(:stat).with("init.pp") + stub_file = stub('init.pp', :stat => stub()) + # Ruby 1.8.7 needs the following call to be stubs and not expects + Puppet::FileSystem::File.stubs(:new).with('init.pp').returns stub_file @top_level = stub_everything 'toplevel', :file_relative_name => "init.pp" @parser = RDoc::Parser.new(@top_level, "module/manifests/init.pp", nil, Options.instance, RDoc::Stats.new) end @@ -82,7 +84,7 @@ describe "RDoc::Parser", :if => Puppet.features.rdoc1? do File.stubs(:open).returns("readme") @parser.stubs(:parse_elements) - @module.expects(:comment=).with("readme") + @module.expects(:add_comment).with("readme", "module/manifests/init.pp") @parser.scan_top_level(@topcontainer) end @@ -93,7 +95,7 @@ describe "RDoc::Parser", :if => Puppet.features.rdoc1? do File.stubs(:open).returns("readme") @parser.stubs(:parse_elements) - @module.expects(:comment=).with("readme") + @module.expects(:add_comment).with("readme", "module/manifests/init.pp") @parser.scan_top_level(@topcontainer) end @@ -105,7 +107,7 @@ describe "RDoc::Parser", :if => Puppet.features.rdoc1? do File.stubs(:open).with("module/README.rdoc", "r").returns("readme.rdoc") @parser.stubs(:parse_elements) - @module.expects(:comment=).with("readme.rdoc") + @module.expects(:add_comment).with("readme.rdoc", "module/manifests/init.pp") @parser.scan_top_level(@topcontainer) end @@ -311,7 +313,7 @@ describe "RDoc::Parser", :if => Puppet.features.rdoc1? do end it "should associate the node documentation to the rdoc node" do - @rdoc_node.expects(:comment=).with("mydoc") + @rdoc_node.expects(:add_comment).with("mydoc", "file") @parser.document_node("mynode", @node, @class) end @@ -360,7 +362,7 @@ describe "RDoc::Parser", :if => Puppet.features.rdoc1? do end it "should associate the node documentation to the rdoc class" do - @rdoc_class.expects(:comment=).with("mydoc") + @rdoc_class.expects(:add_comment).with("mydoc", "file") @parser.document_class("mynode", @class, @module) end @@ -397,7 +399,7 @@ describe "RDoc::Parser", :if => Puppet.features.rdoc1? do before(:each) do @class = stub_everything 'class' @code = stub_everything 'code' - @code.stubs(:is_a?).with(Puppet::Parser::AST::ASTArray).returns(true) + @code.stubs(:is_a?).with(Puppet::Parser::AST::BlockExpression).returns(true) end it "should also scan mono-instruction code" do @@ -435,7 +437,7 @@ describe "RDoc::Parser", :if => Puppet.features.rdoc1? do before(:each) do @class = stub_everything 'class' @code = stub_everything 'code' - @code.stubs(:is_a?).with(Puppet::Parser::AST::ASTArray).returns(true) + @code.stubs(:is_a?).with(Puppet::Parser::AST::BlockExpression).returns(true) end it "should also scan mono-instruction code" do @@ -457,11 +459,11 @@ describe "RDoc::Parser", :if => Puppet.features.rdoc1? do @class = stub_everything 'class' @stmt = stub_everything 'stmt', :name => "myvar", :value => "myvalue", :doc => "mydoc" - @stmt.stubs(:is_a?).with(Puppet::Parser::AST::ASTArray).returns(false) + @stmt.stubs(:is_a?).with(Puppet::Parser::AST::BlockExpression).returns(false) @stmt.stubs(:is_a?).with(Puppet::Parser::AST::VarDef).returns(true) @code = stub_everything 'code' - @code.stubs(:is_a?).with(Puppet::Parser::AST::ASTArray).returns(true) + @code.stubs(:is_a?).with(Puppet::Parser::AST::BlockExpression).returns(true) end it "should recursively register variables to the current container" do @@ -483,17 +485,17 @@ describe "RDoc::Parser", :if => Puppet.features.rdoc1? do @class = stub_everything 'class' @stmt = Puppet::Parser::AST::Resource.new( :type => "File", - :instances => Puppet::Parser::AST::ASTArray.new(:children => [ + :instances => Puppet::Parser::AST::BlockExpression.new(:children => [ Puppet::Parser::AST::ResourceInstance.new( :title => Puppet::Parser::AST::Name.new(:value => "myfile"), - :parameters => Puppet::Parser::AST::ASTArray.new(:children => []) + :parameters => Puppet::Parser::AST::BlockExpression.new(:children => []) ) ]), :doc => 'mydoc' ) @code = stub_everything 'code' - @code.stubs(:is_a?).with(Puppet::Parser::AST::ASTArray).returns(true) + @code.stubs(:is_a?).with(Puppet::Parser::AST::BlockExpression).returns(true) end it "should register a PuppetResource to the current container" do diff --git a/spec/unit/util/rdoc_spec.rb b/spec/unit/util/rdoc_spec.rb index b13591374..f71bf7939 100755 --- a/spec/unit/util/rdoc_spec.rb +++ b/spec/unit/util/rdoc_spec.rb @@ -5,15 +5,7 @@ require 'puppet/util/rdoc' require 'rdoc/rdoc' describe Puppet::Util::RDoc do - it "should fail with a clear error without RDoc 1.*" do - Puppet.features.stubs(:rdoc1?).returns(false) - - expect { - Puppet::Util::RDoc.rdoc("output", []) - }.to raise_error(/the version of RDoc .* is not supported/) - end - - describe "when generating RDoc HTML documentation", :if => Puppet.features.rdoc1? do + describe "when generating RDoc HTML documentation" do before :each do @rdoc = stub_everything 'rdoc' RDoc::RDoc.stubs(:new).returns(@rdoc) @@ -24,12 +16,6 @@ describe Puppet::Util::RDoc do Puppet::Util::RDoc.rdoc("output", []) end - it "should install the Puppet HTML Generator into RDoc generators" do - Puppet::Util::RDoc.rdoc("output", []) - - RDoc::RDoc::GENERATORS["puppet"].file_name.should == "puppet/util/rdoc/generators/puppet_generator.rb" - end - it "should tell RDoc to generate documentation using the Puppet generator" do @rdoc.expects(:document).with { |args| args.include?("--fmt") and args.include?("puppet") } @@ -48,18 +34,26 @@ describe Puppet::Util::RDoc do Puppet::Util::RDoc.rdoc("output", [], "utf-8") end - it "should tell RDoc to force updates of indices when RDoc supports it" do - Options::OptionList.stubs(:options).returns([["--force-update", "-U", 0 ]]) - @rdoc.expects(:document).with { |args| args.include?("--force-update") } + describe "with rdoc1", :if => Puppet.features.rdoc1? do + it "should install the Puppet HTML Generator into RDoc generators" do + Puppet::Util::RDoc.rdoc("output", []) - Puppet::Util::RDoc.rdoc("output", []) - end + RDoc::RDoc::GENERATORS["puppet"].file_name.should == "puppet/util/rdoc/generators/puppet_generator.rb" + end - it "should not tell RDoc to force updates of indices when RDoc doesn't support it" do - Options::OptionList.stubs(:options).returns([]) - @rdoc.expects(:document).never.with { |args| args.include?("--force-update") } + it "should tell RDoc to force updates of indices when RDoc supports it" do + ::Options::OptionList.stubs(:options).returns([["--force-update", "-U", 0 ]]) + @rdoc.expects(:document).with { |args| args.include?("--force-update") } - Puppet::Util::RDoc.rdoc("output", []) + Puppet::Util::RDoc.rdoc("output", []) + end + + it "should not tell RDoc to force updates of indices when RDoc doesn't support it" do + ::Options::OptionList.stubs(:options).returns([]) + @rdoc.expects(:document).never.with { |args| args.include?("--force-update") } + + Puppet::Util::RDoc.rdoc("output", []) + end end it "should tell RDoc to use the given outputdir" do diff --git a/spec/unit/util/resource_template_spec.rb b/spec/unit/util/resource_template_spec.rb index ee4c1b866..0e6037119 100755 --- a/spec/unit/util/resource_template_spec.rb +++ b/spec/unit/util/resource_template_spec.rb @@ -6,20 +6,20 @@ require 'puppet/util/resource_template' describe Puppet::Util::ResourceTemplate do describe "when initializing" do it "should fail if the template does not exist" do - FileTest.expects(:exist?).with("/my/template").returns false + Puppet::FileSystem::File.expects(:exist?).with("/my/template").returns false lambda { Puppet::Util::ResourceTemplate.new("/my/template", mock('resource')) }.should raise_error(ArgumentError) end it "should not create the ERB template" do ERB.expects(:new).never - FileTest.expects(:exist?).with("/my/template").returns true + Puppet::FileSystem::File.expects(:exist?).with("/my/template").returns true Puppet::Util::ResourceTemplate.new("/my/template", mock('resource')) end end describe "when evaluating" do before do - FileTest.stubs(:exist?).returns true + Puppet::FileSystem::File.stubs(:exist?).returns true File.stubs(:read).returns "eh" @template = stub 'template', :result => nil diff --git a/spec/unit/util/selinux_spec.rb b/spec/unit/util/selinux_spec.rb index ac2bc2977..1357b981a 100755 --- a/spec/unit/util/selinux_spec.rb +++ b/spec/unit/util/selinux_spec.rb @@ -123,7 +123,8 @@ describe Puppet::Util::SELinux do it "should return a context if a default context exists" do self.expects(:selinux_support?).returns true fstat = stub 'File::Stat', :mode => 0 - File.expects(:lstat).with("/foo").returns fstat + stub_file = stub('/foo', :lstat => fstat) + Puppet::FileSystem::File.expects(:new).with('/foo').returns stub_file self.expects(:find_fs).with("/foo").returns "ext3" Selinux.expects(:matchpathcon).with("/foo", 0).returns [0, "user_u:role_r:type_t:s0"] get_selinux_default_context("/foo").should == "user_u:role_r:type_t:s0" @@ -150,7 +151,8 @@ describe Puppet::Util::SELinux do it "should return nil if matchpathcon returns failure" do self.expects(:selinux_support?).returns true fstat = stub 'File::Stat', :mode => 0 - File.expects(:lstat).with("/foo").returns fstat + stub_file = stub('/foo', :lstat => fstat) + Puppet::FileSystem::File.expects(:new).with('/foo').returns stub_file self.expects(:find_fs).with("/foo").returns "ext3" Selinux.expects(:matchpathcon).with("/foo", 0).returns -1 get_selinux_default_context("/foo").should be_nil diff --git a/spec/unit/util/storage_spec.rb b/spec/unit/util/storage_spec.rb index ba1675981..582bd2422 100755 --- a/spec/unit/util/storage_spec.rb +++ b/spec/unit/util/storage_spec.rb @@ -77,7 +77,7 @@ describe Puppet::Util::Storage do end it "should not fail to load" do - FileTest.exists?(@path).should be_false + Puppet::FileSystem::File.exist?(@path).should be_false Puppet[:statedir] = @path Puppet::Util::Storage.load Puppet[:statefile] = @path @@ -85,7 +85,7 @@ describe Puppet::Util::Storage do end it "should not lose its internal state when load() is called" do - FileTest.exists?(@path).should be_false + Puppet::FileSystem::File.exist?(@path).should be_false Puppet::Util::Storage.cache(:yayness) Puppet::Util::Storage.state.should == {:yayness=>{}} @@ -176,12 +176,12 @@ describe Puppet::Util::Storage do end it "should create the state file if it does not exist" do - FileTest.exists?(Puppet[:statefile]).should be_false + Puppet::FileSystem::File.exist?(Puppet[:statefile]).should be_false Puppet::Util::Storage.cache(:yayness) Puppet::Util::Storage.store - FileTest.exists?(Puppet[:statefile]).should be_true + Puppet::FileSystem::File.exist?(Puppet[:statefile]).should be_true end it "should raise an exception if the state file is not a regular file" do diff --git a/spec/unit/util/suidmanager_spec.rb b/spec/unit/util/suidmanager_spec.rb index 5962e4362..f8ba883f5 100755 --- a/spec/unit/util/suidmanager_spec.rb +++ b/spec/unit/util/suidmanager_spec.rb @@ -231,6 +231,13 @@ describe Puppet::Util::SUIDManager do output.first.should == 'output' output.last.should be_a(Process::Status) end + + it "should log a deprecation notice" do + Puppet::Util::Execution.stubs(:execute).returns("success") + Puppet.expects(:deprecation_warning).with('Puppet::Util::SUIDManager.run_and_capture is deprecated; please use Puppet::Util::Execution.execute instead.') + + output = Puppet::Util::SUIDManager.run_and_capture 'yay', user[:uid], user[:gid] + end end end diff --git a/spec/unit/util/tag_set_spec.rb b/spec/unit/util/tag_set_spec.rb new file mode 100644 index 000000000..c59674fcb --- /dev/null +++ b/spec/unit/util/tag_set_spec.rb @@ -0,0 +1,46 @@ +#! /usr/bin/env ruby +require 'spec_helper' + +require 'puppet/util/tag_set' + +RSpec::Matchers.define :be_one_of do |*expected| + match do |actual| + expected.include? actual + end + + failure_message_for_should do |actual| + "expected #{actual.inspect} to be one of #{expected.map(&:inspect).join(' or ')}" + end +end + +describe Puppet::Util::TagSet do + let(:set) { Puppet::Util::TagSet.new } + + it 'serializes to yaml as an array' do + array = ['a', :b, 1, 5.4] + set.merge(array) + + Set.new(YAML.load(set.to_yaml)).should == Set.new(array) + end + + it 'deserializes from a yaml array' do + array = ['a', :b, 1, 5.4] + + Puppet::Util::TagSet.from_yaml(array.to_yaml).should == Puppet::Util::TagSet.new(array) + end + + it 'round trips through pson' do + array = ['a', 'b', 1, 5.4] + set.merge(array) + + tes = Puppet::Util::TagSet.from_pson(PSON.parse(set.to_pson)) + tes.should == set + end + + it 'can join its elements with a string separator' do + array = ['a', 'b'] + set.merge(array) + + set.join(', ').should be_one_of('a, b', 'b, a') + end +end diff --git a/spec/unit/util/tagging_spec.rb b/spec/unit/util/tagging_spec.rb index 67b766fe0..248e915e9 100755 --- a/spec/unit/util/tagging_spec.rb +++ b/spec/unit/util/tagging_spec.rb @@ -3,92 +3,129 @@ require 'spec_helper' require 'puppet/util/tagging' -describe Puppet::Util::Tagging, "when adding tags" do - before do - @tagger = Object.new - @tagger.extend(Puppet::Util::Tagging) - end - - it "should have a method for adding tags" do - @tagger.should be_respond_to(:tag) - end - - it "should have a method for returning all tags" do - @tagger.should be_respond_to(:tags) - end +describe Puppet::Util::Tagging do + let(:tagger) { Object.new.extend(Puppet::Util::Tagging) } it "should add tags to the returned tag list" do - @tagger.tag("one") - @tagger.tags.should be_include("one") - end - - it "should not add duplicate tags to the returned tag list" do - @tagger.tag("one") - @tagger.tag("one") - @tagger.tags.should == ["one"] + tagger.tag("one") + expect(tagger.tags).to include("one") end it "should return a duplicate of the tag list, rather than the original" do - @tagger.tag("one") - tags = @tagger.tags + tagger.tag("one") + tags = tagger.tags tags << "two" - @tagger.tags.should_not be_include("two") + expect(tagger.tags).to_not include("two") end it "should add all provided tags to the tag list" do - @tagger.tag("one", "two") - @tagger.tags.should be_include("one") - @tagger.tags.should be_include("two") + tagger.tag("one", "two") + expect(tagger.tags).to include("one") + expect(tagger.tags).to include("two") end it "should fail on tags containing '*' characters" do - expect { @tagger.tag("bad*tag") }.to raise_error(Puppet::ParseError) + expect { tagger.tag("bad*tag") }.to raise_error(Puppet::ParseError) end it "should fail on tags starting with '-' characters" do - expect { @tagger.tag("-badtag") }.to raise_error(Puppet::ParseError) + expect { tagger.tag("-badtag") }.to raise_error(Puppet::ParseError) end it "should fail on tags containing ' ' characters" do - expect { @tagger.tag("bad tag") }.to raise_error(Puppet::ParseError) + expect { tagger.tag("bad tag") }.to raise_error(Puppet::ParseError) end it "should allow alpha tags" do - expect { @tagger.tag("good_tag") }.to_not raise_error + expect { tagger.tag("good_tag") }.not_to raise_error end it "should allow tags containing '.' characters" do - expect { @tagger.tag("good.tag") }.to_not raise_error + expect { tagger.tag("good.tag") }.to_not raise_error(Puppet::ParseError) end it "should add qualified classes as tags" do - @tagger.tag("one::two") - @tagger.tags.should be_include("one::two") + tagger.tag("one::two") + expect(tagger.tags).to include("one::two") end it "should add each part of qualified classes as tags" do - @tagger.tag("one::two::three") - @tagger.tags.should be_include("one") - @tagger.tags.should be_include("two") - @tagger.tags.should be_include("three") + tagger.tag("one::two::three") + expect(tagger.tags).to include('one') + expect(tagger.tags).to include("two") + expect(tagger.tags).to include("three") end it "should indicate when the object is tagged with a provided tag" do - @tagger.tag("one") - @tagger.should be_tagged("one") + tagger.tag("one") + expect(tagger).to be_tagged("one") end it "should indicate when the object is not tagged with a provided tag" do - @tagger.should_not be_tagged("one") + expect(tagger).to_not be_tagged("one") end it "should indicate when the object is tagged with any tag in an array" do - @tagger.tag("one") - @tagger.should be_tagged("one","two","three") + tagger.tag("one") + expect(tagger).to be_tagged("one","two","three") end it "should indicate when the object is not tagged with any tag in an array" do - @tagger.tag("one") - @tagger.should_not be_tagged("two","three") + tagger.tag("one") + expect(tagger).to_not be_tagged("two","three") + end + + context "when tagging" do + it "converts symbols to strings" do + tagger.tag(:hello) + expect(tagger.tags).to include('hello') + end + + it "downcases tags" do + tagger.tag(:HEllO) + tagger.tag("GooDByE") + expect(tagger).to be_tagged("hello") + expect(tagger).to be_tagged("goodbye") + end + + it "accepts hyphenated tags" do + tagger.tag("my-tag") + expect(tagger).to be_tagged("my-tag") + end + end + + context "when querying if tagged" do + it "responds true if queried on the entire set" do + tagger.tag("one", "two") + expect(tagger).to be_tagged("one", "two") + end + + it "responds true if queried on a subset" do + tagger.tag("one", "two", "three") + expect(tagger).to be_tagged("two", "one") + end + + it "responds true if queried on an overlapping but not fully contained set" do + tagger.tag("one", "two") + expect(tagger).to be_tagged("zero", "one") + end + + it "responds false if queried on a disjoint set" do + tagger.tag("one", "two", "three") + expect(tagger).to_not be_tagged("five") + end + + it "responds false if queried on the empty set" do + expect(tagger).to_not be_tagged + end + end + + context "when assigning tags" do + it "splits a string on ','" do + tagger.tags = "one, two, three" + expect(tagger).to be_tagged("one") + expect(tagger).to be_tagged("two") + expect(tagger).to be_tagged("three") + end end end diff --git a/spec/unit/util/watcher_spec.rb b/spec/unit/util/watcher_spec.rb index 64fab9681..33f75ab12 100644 --- a/spec/unit/util/watcher_spec.rb +++ b/spec/unit/util/watcher_spec.rb @@ -14,7 +14,10 @@ describe Puppet::Util::Watcher do let(:filename) { "fake" } def after_reading_the_sequence(initial, *results) - expectation = File.stubs(:stat).with(filename) + mock_file = mock(filename) + Puppet::FileSystem::File.expects(:new).with(filename).at_least(1).returns mock_file + + expectation = mock_file.stubs(:stat) ([initial] + results).each do |result| expectation = if result.is_a? Class expectation.raises(result) diff --git a/spec/unit/util/windows/access_control_entry_spec.rb b/spec/unit/util/windows/access_control_entry_spec.rb new file mode 100644 index 000000000..b139b0d42 --- /dev/null +++ b/spec/unit/util/windows/access_control_entry_spec.rb @@ -0,0 +1,67 @@ +#!/usr/bin/env ruby +require 'spec_helper' +require 'puppet/util/windows' + +describe "Puppet::Util::Windows::AccessControlEntry", :if => Puppet.features.microsoft_windows? do + let(:klass) { Puppet::Util::Windows::AccessControlEntry } + let(:sid) { 'S-1-5-18' } + let(:mask) { Windows::File::FILE_ALL_ACCESS } + + it "creates an access allowed ace" do + ace = klass.new(sid, mask) + + ace.type.should == klass::ACCESS_ALLOWED_ACE_TYPE + end + + it "creates an access denied ace" do + ace = klass.new(sid, mask, 0, klass::ACCESS_DENIED_ACE_TYPE) + + ace.type.should == klass::ACCESS_DENIED_ACE_TYPE + end + + it "creates a non-inherited ace by default" do + ace = klass.new(sid, mask) + + ace.should_not be_inherited + end + + it "creates an inherited ace" do + ace = klass.new(sid, mask, klass::INHERITED_ACE) + + ace.should be_inherited + end + + it "creates a non-inherit-only ace by default" do + ace = klass.new(sid, mask) + + ace.should_not be_inherit_only + end + + it "creates an inherit-only ace" do + ace = klass.new(sid, mask, klass::INHERIT_ONLY_ACE) + + ace.should be_inherit_only + end + + context "when comparing aces" do + let(:ace1) { klass.new(sid, mask, klass::INHERIT_ONLY_ACE, klass::ACCESS_DENIED_ACE_TYPE) } + let(:ace2) { klass.new(sid, mask, klass::INHERIT_ONLY_ACE, klass::ACCESS_DENIED_ACE_TYPE) } + + it "returns true if different objects have the same set of values" do + ace1.should == ace2 + end + + it "returns false if different objects have different sets of values" do + ace = klass.new(sid, mask) + ace.should_not == ace1 + end + + it "returns true when testing if two objects are eql?" do + ace1.eql?(ace2) + end + + it "returns false when comparing object identity" do + ace1.should_not be_equal(ace2) + end + end +end diff --git a/spec/unit/util/windows/access_control_list_spec.rb b/spec/unit/util/windows/access_control_list_spec.rb new file mode 100644 index 000000000..66c917b29 --- /dev/null +++ b/spec/unit/util/windows/access_control_list_spec.rb @@ -0,0 +1,133 @@ +#!/usr/bin/env ruby +require 'spec_helper' +require 'puppet/util/windows' + +describe "Puppet::Util::Windows::AccessControlList", :if => Puppet.features.microsoft_windows? do + let(:klass) { Puppet::Util::Windows::AccessControlList } + let(:system_sid) { 'S-1-5-18' } + let(:admins_sid) { 'S-1-5-544' } + let(:none_sid) { 'S-1-0-0' } + + let(:system_ace) do + Puppet::Util::Windows::AccessControlEntry.new(system_sid, 0x1) + end + let(:admins_ace) do + Puppet::Util::Windows::AccessControlEntry.new(admins_sid, 0x2) + end + let(:none_ace) do + Puppet::Util::Windows::AccessControlEntry.new(none_sid, 0x3) + end + + it "constructs an empty list" do + acl = klass.new + + acl.to_a.should be_empty + end + + it "supports copy constructor" do + aces = klass.new([system_ace]).to_a + + aces.to_a.should == [system_ace] + end + + context "appending" do + it "appends an allow ace" do + acl = klass.new + acl.allow(system_sid, 0x1, 0x2) + + acl.first.type.should == klass::ACCESS_ALLOWED_ACE_TYPE + end + + it "appends a deny ace" do + acl = klass.new + acl.deny(system_sid, 0x1, 0x2) + + acl.first.type.should == klass::ACCESS_DENIED_ACE_TYPE + end + + it "always appends, never overwrites an ACE" do + acl = klass.new([system_ace]) + acl.allow(admins_sid, admins_ace.mask, admins_ace.flags) + + aces = acl.to_a + aces.size.should == 2 + aces[0].should == system_ace + aces[1].sid.should == admins_sid + aces[1].mask.should == admins_ace.mask + aces[1].flags.should == admins_ace.flags + end + end + + context "reassigning" do + it "preserves the mask from the old sid when reassigning to the new sid" do + dacl = klass.new([system_ace]) + + dacl.reassign!(system_ace.sid, admins_ace.sid) + # we removed system, so ignore prepended ace + ace = dacl.to_a[1] + ace.sid.should == admins_sid + ace.mask.should == system_ace.mask + end + + it "matches multiple sids" do + dacl = klass.new([system_ace, system_ace]) + + dacl.reassign!(system_ace.sid, admins_ace.sid) + # we removed system, so ignore prepended ace + aces = dacl.to_a + aces.size.should == 3 + aces.to_a[1,2].each do |ace| + ace.sid.should == admins_ace.sid + end + end + + it "preserves aces for sids that don't match, in their original order" do + dacl = klass.new([system_ace, admins_ace]) + + dacl.reassign!(system_sid, none_sid) + aces = dacl.to_a + aces[1].sid == admins_ace.sid + end + + it "preserves inherited aces, even if the sids match" do + flags = Puppet::Util::Windows::AccessControlEntry::INHERITED_ACE + inherited_ace = Puppet::Util::Windows::AccessControlEntry.new(system_sid, 0x1, flags) + dacl = klass.new([inherited_ace, system_ace]) + dacl.reassign!(system_sid, none_sid) + aces = dacl.to_a + + aces[0].sid.should == system_sid + end + + it "prepends an explicit ace for the new sid with the same mask and basic inheritance as the inherited ace" do + expected_flags = + Puppet::Util::Windows::AccessControlEntry::OBJECT_INHERIT_ACE | + Puppet::Util::Windows::AccessControlEntry::CONTAINER_INHERIT_ACE | + Puppet::Util::Windows::AccessControlEntry::INHERIT_ONLY_ACE + + flags = Puppet::Util::Windows::AccessControlEntry::INHERITED_ACE | expected_flags + + inherited_ace = Puppet::Util::Windows::AccessControlEntry.new(system_sid, 0x1, flags) + dacl = klass.new([inherited_ace]) + dacl.reassign!(system_sid, none_sid) + aces = dacl.to_a + + aces.size.should == 2 + aces[0].sid.should == none_sid + aces[0].should_not be_inherited + aces[0].flags.should == expected_flags + + aces[1].sid.should == system_sid + aces[1].should be_inherited + end + + it "makes a copy of the ace prior to modifying it" do + arr = [system_ace] + + acl = klass.new(arr) + acl.reassign!(system_sid, none_sid) + + arr[0].sid.should == system_sid + end + end +end diff --git a/spec/unit/util/windows/root_certs_spec.rb b/spec/unit/util/windows/root_certs_spec.rb index 155707e4e..6b1d9891e 100755 --- a/spec/unit/util/windows/root_certs_spec.rb +++ b/spec/unit/util/windows/root_certs_spec.rb @@ -3,13 +3,15 @@ require 'spec_helper' require 'puppet/util/windows' describe "Puppet::Util::Windows::RootCerts", :if => Puppet::Util::Platform.windows? do - let(:klass) { Puppet::Util::Windows::RootCerts } - let(:x509) { 'mycert' } - - context '#each' do - it "should enumerate each root cert" do - klass.expects(:load_certs).returns([x509]) - klass.instance.to_a.should == [x509] - end + let(:x509_store) { Puppet::Util::Windows::RootCerts.instance.to_a } + + it "should return at least one X509 certificate" do + expect(x509_store.to_a).to have_at_least(1).items + end + + it "should return an X509 certificate with a subject" do + x509 = x509_store.first + + expect(x509.subject.to_s).to match(/CN=.*/) end end diff --git a/spec/unit/util/windows/security_descriptor_spec.rb b/spec/unit/util/windows/security_descriptor_spec.rb new file mode 100644 index 000000000..61db2f598 --- /dev/null +++ b/spec/unit/util/windows/security_descriptor_spec.rb @@ -0,0 +1,117 @@ +#!/usr/bin/env ruby + +require 'spec_helper' +require 'puppet/util/windows' + +describe "Puppet::Util::Windows::SecurityDescriptor", :if => Puppet.features.microsoft_windows? do + let(:system_sid) { Win32::Security::SID::LocalSystem } + let(:admins_sid) { Win32::Security::SID::BuiltinAdministrators } + let(:group_sid) { Win32::Security::SID::Nobody } + let(:new_sid) { 'S-1-5-32-500-1-2-3' } + + def empty_dacl + Puppet::Util::Windows::AccessControlList.new + end + + def system_ace_dacl + dacl = Puppet::Util::Windows::AccessControlList.new + dacl.allow(system_sid, 0x1) + dacl + end + + context "owner" do + it "changes the owner" do + sd = Puppet::Util::Windows::SecurityDescriptor.new(system_sid, group_sid, system_ace_dacl) + sd.owner = new_sid + + sd.owner.should == new_sid + end + + it "performs a noop if the new owner is the same as the old one" do + dacl = system_ace_dacl + sd = Puppet::Util::Windows::SecurityDescriptor.new(system_sid, group_sid, dacl) + sd.owner = sd.owner + + sd.dacl.object_id.should == dacl.object_id + end + + it "prepends SYSTEM when security descriptor owner is no longer SYSTEM" do + sd = Puppet::Util::Windows::SecurityDescriptor.new(system_sid, group_sid, system_ace_dacl) + sd.owner = new_sid + + aces = sd.dacl.to_a + aces.size.should == 2 + aces[0].sid.should == system_sid + aces[1].sid.should == new_sid + end + + it "does not prepend SYSTEM when DACL already contains inherited SYSTEM ace" do + sd = Puppet::Util::Windows::SecurityDescriptor.new(admins_sid, system_sid, empty_dacl) + sd.dacl.allow(admins_sid, 0x1) + sd.dacl.allow(system_sid, 0x1, Puppet::Util::Windows::AccessControlEntry::INHERITED_ACE) + sd.owner = new_sid + + aces = sd.dacl.to_a + aces.size.should == 2 + aces[0].sid.should == new_sid + end + + it "does not prepend SYSTEM when security descriptor owner wasn't SYSTEM" do + sd = Puppet::Util::Windows::SecurityDescriptor.new(group_sid, group_sid, empty_dacl) + sd.dacl.allow(group_sid, 0x1) + sd.owner = new_sid + + aces = sd.dacl.to_a + aces.size.should == 1 + aces[0].sid.should == new_sid + end + end + + context "group" do + it "changes the group" do + sd = Puppet::Util::Windows::SecurityDescriptor.new(system_sid, group_sid, system_ace_dacl) + sd.group = new_sid + + sd.group.should == new_sid + end + + it "performs a noop if the new group is the same as the old one" do + dacl = system_ace_dacl + sd = Puppet::Util::Windows::SecurityDescriptor.new(system_sid, group_sid, dacl) + sd.group = sd.group + + sd.dacl.object_id.should == dacl.object_id + end + + it "prepends SYSTEM when security descriptor group is no longer SYSTEM" do + sd = Puppet::Util::Windows::SecurityDescriptor.new(new_sid, system_sid, system_ace_dacl) + sd.group = new_sid + + aces = sd.dacl.to_a + aces.size.should == 2 + aces[0].sid.should == system_sid + aces[1].sid.should == new_sid + end + + it "does not prepend SYSTEM when DACL already contains inherited SYSTEM ace" do + sd = Puppet::Util::Windows::SecurityDescriptor.new(admins_sid, admins_sid, empty_dacl) + sd.dacl.allow(admins_sid, 0x1) + sd.dacl.allow(system_sid, 0x1, Puppet::Util::Windows::AccessControlEntry::INHERITED_ACE) + sd.group = new_sid + + aces = sd.dacl.to_a + aces.size.should == 2 + aces[0].sid.should == new_sid + end + + it "does not prepend SYSTEM when security descriptor group wasn't SYSTEM" do + sd = Puppet::Util::Windows::SecurityDescriptor.new(group_sid, group_sid, empty_dacl) + sd.dacl.allow(group_sid, 0x1) + sd.group = new_sid + + aces = sd.dacl.to_a + aces.size.should == 1 + aces[0].sid.should == new_sid + end + end +end diff --git a/spec/unit/util/windows/sid_spec.rb b/spec/unit/util/windows/sid_spec.rb index d53e93ae6..770512188 100755 --- a/spec/unit/util/windows/sid_spec.rb +++ b/spec/unit/util/windows/sid_spec.rb @@ -15,6 +15,45 @@ describe "Puppet::Util::Windows::SID", :if => Puppet.features.microsoft_windows? let(:unknown_sid) { 'S-0-0-0' } let(:unknown_name) { 'chewbacca' } + context "#octet_string_to_sid_object" do + it "should properly convert an array of bytes for the local Administrator SID" do + host = '.' + username = 'Administrator' + admin = WIN32OLE.connect("WinNT://#{host}/#{username},user") + converted = subject.octet_string_to_sid_object(admin.objectSID) + + converted.should == Win32::Security::SID.new(username, host) + converted.should be_an_instance_of Win32::Security::SID + end + + it "should properly convert an array of bytes for a well-known SID" do + bytes = [1, 1, 0, 0, 0, 0, 0, 5, 18, 0, 0, 0] + converted = subject.octet_string_to_sid_object(bytes) + + converted.should == Win32::Security::SID.new('SYSTEM') + converted.should be_an_instance_of Win32::Security::SID + end + + it "should raise an error for non-array input" do + expect { + subject.octet_string_to_sid_object(invalid_sid) + }.to raise_error(Puppet::Error, /Octet string must be an array of bytes/) + end + + it "should raise an error for an empty byte array" do + expect { + subject.octet_string_to_sid_object([]) + }.to raise_error(Puppet::Error, /Octet string must be an array of bytes/) + end + + it "should raise an error for a malformed byte array" do + expect { + invalid_octet = [1] + subject.octet_string_to_sid_object(invalid_octet) + }.to raise_error(Win32::Security::SID::Error, /No mapping between account names and security IDs was done./) + end + end + context "#name_to_sid" do it "should return nil if the account does not exist" do subject.name_to_sid(unknown_name).should be_nil @@ -28,6 +67,10 @@ describe "Puppet::Util::Windows::SID", :if => Puppet.features.microsoft_windows? subject.name_to_sid('SYSTEM').should == subject.name_to_sid('system') end + it "should be leading and trailing whitespace-insensitive" do + subject.name_to_sid('SYSTEM').should == subject.name_to_sid(' SYSTEM ') + end + it "should accept domain qualified account names" do subject.name_to_sid('NT AUTHORITY\SYSTEM').should == sid end @@ -37,6 +80,32 @@ describe "Puppet::Util::Windows::SID", :if => Puppet.features.microsoft_windows? end end + context "#name_to_sid_object" do + it "should return nil if the account does not exist" do + subject.name_to_sid_object(unknown_name).should be_nil + end + + it "should return a Win32::Security::SID instance for any valid sid" do + subject.name_to_sid_object(sid).should be_an_instance_of(Win32::Security::SID) + end + + it "should accept unqualified account name" do + subject.name_to_sid_object('SYSTEM').to_s.should == sid + end + + it "should be case-insensitive" do + subject.name_to_sid_object('SYSTEM').should == subject.name_to_sid_object('system') + end + + it "should be leading and trailing whitespace-insensitive" do + subject.name_to_sid_object('SYSTEM').should == subject.name_to_sid_object(' SYSTEM ') + end + + it "should accept domain qualified account names" do + subject.name_to_sid_object('NT AUTHORITY\SYSTEM').to_s.should == sid + end + end + context "#sid_to_name" do it "should return nil if given a sid for an account that doesn't exist" do subject.sid_to_name(unknown_sid).should be_nil diff --git a/spec/unit/util/yaml_spec.rb b/spec/unit/util/yaml_spec.rb index 960388ade..978afb0a2 100644 --- a/spec/unit/util/yaml_spec.rb +++ b/spec/unit/util/yaml_spec.rb @@ -33,6 +33,20 @@ describe Puppet::Util::Yaml do expect { Puppet::Util::Yaml.load_file("not\0allowed") }.to raise_error(Puppet::Util::Yaml::YamlLoadError, /null byte/) end + context "when the file is empty" do + it "returns false" do + Puppet::FileSystem::File.new(filename).touch + + expect(Puppet::Util::Yaml.load_file(filename)).to be_false + end + + it "allows return value to be overridden" do + Puppet::FileSystem::File.new(filename).touch + + expect(Puppet::Util::Yaml.load_file(filename, {})).to eq({}) + end + end + def write_file(name, contents) File.open(name, "w") do |fh| fh.write(contents) diff --git a/spec/unit/util_spec.rb b/spec/unit/util_spec.rb index 48b6e6fc5..58cc4bbd8 100755 --- a/spec/unit/util_spec.rb +++ b/spec/unit/util_spec.rb @@ -19,7 +19,7 @@ describe Puppet::Util do end def get_mode(file) - File.lstat(file).mode & 07777 + Puppet::FileSystem::File.new(file).lstat.mode & 07777 end end @@ -449,7 +449,7 @@ describe Puppet::Util do it "should copy the permissions of the source file before yielding on Unix", :if => !Puppet.features.microsoft_windows? do set_mode(0555, target.path) - inode = File.stat(target.path).ino + inode = Puppet::FileSystem::File.new(target.path).stat.ino yielded = false subject.replace_file(target.path, 0600) do |fh| @@ -458,19 +458,19 @@ describe Puppet::Util do end yielded.should be_true - File.stat(target.path).ino.should_not == inode + Puppet::FileSystem::File.new(target.path).stat.ino.should_not == inode get_mode(target.path).should == 0555 end it "should use the default permissions if the source file doesn't exist" do new_target = target.path + '.foo' - File.should_not be_exist(new_target) + Puppet::FileSystem::File.exist?(new_target).should be_false begin subject.replace_file(new_target, 0555) {|fh| fh.puts "foo" } get_mode(new_target).should == 0555 ensure - File.unlink(new_target) if File.exists?(new_target) + Puppet::FileSystem::File.unlink(new_target) if Puppet::FileSystem::File.exist?(new_target) end end @@ -502,14 +502,14 @@ describe Puppet::Util do {:string => '664', :number => 0664, :symbolic => "ug=rw-,o=r--" }.each do |label,mode| it "should support #{label} format permissions" do new_target = target.path + "#{mode}.foo" - File.should_not be_exist(new_target) + Puppet::FileSystem::File.exist?(new_target).should be_false begin subject.replace_file(new_target, mode) {|fh| fh.puts "this is an interesting content" } get_mode(new_target).should == 0664 ensure - File.unlink(new_target) if File.exists?(new_target) + Puppet::FileSystem::File.unlink(new_target) if Puppet::FileSystem::File.exist?(new_target) end end end diff --git a/tasks/ci.rake b/tasks/ci.rake new file mode 100644 index 000000000..ea4b95383 --- /dev/null +++ b/tasks/ci.rake @@ -0,0 +1,29 @@ +require 'yaml' +require 'time' + +namespace "ci" do + task :spec do + ENV["LOG_SPEC_ORDER"] = "true" + sh %{rspec -r yarjuf -f JUnit -o result.xml -fd spec} + end + + desc "Tar up the acceptance/ directory so that package test runs have tests to run against." + task :acceptance_artifacts => :tag_creator do + Dir.chdir("acceptance") do + rm_f "acceptance-artifacts.tar.gz" + sh "tar -czv --exclude .bundle -f acceptance-artifacts.tar.gz *" + end + end + + task :tag_creator do + Dir.chdir("acceptance") do + File.open('creator.txt', 'w') do |fh| + YAML.dump({ + 'creator_id' => ENV['CREATOR'] || ENV['BUILD_URL'] || 'unknown', + 'created_on' => Time.now.iso8601, + 'commit' => (`git log -1 --oneline` rescue "unknown: #{$!}") + }, fh) + end + end + end +end |
