Page MenuHomePhorge

No OneTemporary

This file is larger than 256 KB, so syntax highlighting was skipped.
diff --git a/.gemspec b/.gemspec
index ea92fccbd..fd7379ebc 100644
--- a/.gemspec
+++ b/.gemspec
@@ -1,55 +1,55 @@
# -*- encoding: utf-8 -*-
#
# PLEASE NOTE
# This gemspec is not intended to be used for building the Puppet gem. This
# gemspec is intended for use with bundler when Puppet is a dependency of
# another project. For example, the stdlib project is able to integrate with
# the master branch of Puppet by using a Gemfile path of
# git://github.com/puppetlabs/puppet.git
#
# Please see the [packaging
# repository](https://github.com/puppetlabs/packaging) for information on how
# to build the Puppet gem package.
begin
require 'puppet/version'
rescue LoadError
$LOAD_PATH.unshift(File.expand_path("../lib", __FILE__))
require 'puppet/version'
end
Gem::Specification.new do |s|
s.name = "puppet"
version = Puppet.version
mdata = version.match(/(\d+\.\d+\.\d+)/)
s.version = mdata ? mdata[1] : version
s.required_rubygems_version = Gem::Requirement.new("> 1.3.1") if s.respond_to? :required_rubygems_version=
s.authors = ["Puppet Labs"]
s.date = "2012-08-17"
s.description = "Puppet, an automated configuration management tool"
s.email = "puppet@puppetlabs.com"
s.executables = ["puppet"]
s.files = ["bin/puppet"]
s.homepage = "http://puppetlabs.com"
s.rdoc_options = ["--title", "Puppet - Configuration Management", "--main", "README", "--line-numbers"]
s.require_paths = ["lib"]
s.rubyforge_project = "puppet"
s.rubygems_version = "1.8.24"
s.summary = "Puppet, an automated configuration management tool"
if s.respond_to? :specification_version then
s.specification_version = 3
if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then
- s.add_runtime_dependency(%q<facter>, ["> 1.5", "< 3"])
+ s.add_runtime_dependency(%q<facter>, [">= 1.7", "< 3"])
s.add_runtime_dependency(%q<hiera>, ["~> 1.0"])
else
- s.add_dependency(%q<facter>, ["> 1.5", "< 3"])
+ s.add_dependency(%q<facter>, [">= 1.7", "< 3"])
s.add_dependency(%q<hiera>, ["~> 1.0"])
end
else
- s.add_dependency(%q<facter>, ["> 1.5", "< 3"])
+ s.add_dependency(%q<facter>, [">= 1.7", "< 3"])
s.add_dependency(%q<hiera>, ["~> 1.0"])
end
end
diff --git a/.gitignore b/.gitignore
index 7466a53aa..873839586 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,22 +1,24 @@
.rspec
results
tags
.*.sw[op]
ext/packaging
pkg
test.pp
# YARD generated documentation
.yardoc
/doc
# Now that there is a gemfile, RVM ignores rvmrc in parent directories, so a local one is required
# to work around that. Which we don't want committed, so we can ignore it here.
/.rvmrc
.bundle/
ext/packaging/
pkg/
Gemfile.lock
Gemfile.local
.bundle/
puppet-acceptance/
/.project
.idea/
+.ruby-version
+.ruby-gemset
diff --git a/.travis.yml b/.travis.yml
index 09f7db6e5..8193a1496 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -1,13 +1,14 @@
language: ruby
bundler_args: --without development
-script: "bundle exec rake spec"
+script: "bundle exec rake \"parallel:spec[1]\""
notifications:
email: false
rvm:
+ - 2.1.0
- 2.0.0
- 1.9.3
- 1.8.7
- ruby-head
matrix:
allow_failures:
- rvm: ruby-head
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 681db4beb..aa27d05b2 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -1,87 +1,87 @@
# How to contribute
Third-party patches are essential for keeping puppet great. We simply can't
access the huge number of platforms and myriad configurations for running
puppet. We want to keep it as easy as possible to contribute changes that
get things working in your environment. There are a few guidelines that we
need contributors to follow so that we can have a chance of keeping on
top of things.
## Getting Started
-* Make sure you have a [Redmine account](http://projects.puppetlabs.com)
+* Make sure you have a [Jira account](http://tickets.puppetlabs.com)
* Make sure you have a [GitHub account](https://github.com/signup/free)
* Submit a ticket for your issue, assuming one does not already exist.
* Clearly describe the issue including steps to reproduce when it is a bug.
* Make sure you fill in the earliest version that you know has the issue.
* Fork the repository on GitHub
## Making Changes
* Create a topic branch from where you want to base your work.
* This is usually the master branch.
* Only target release branches if you are certain your fix must be on that
branch.
* To quickly create a topic branch based on master; `git branch
fix/master/my_contribution master` then checkout the new branch with `git
checkout fix/master/my_contribution`. Please avoid working directly on the
`master` branch.
* Make commits of logical units.
* Check for unnecessary whitespace with `git diff --check` before committing.
* Make sure your commit messages are in the proper format.
````
- (#99999) Make the example in CONTRIBUTING imperative and concrete
+ (PUP-1234) Make the example in CONTRIBUTING imperative and concrete
Without this patch applied the example commit message in the CONTRIBUTING
document is not a concrete example. This is a problem because the
contributor is left to imagine what the commit message should look like
based on a description rather than an example. This patch fixes the
problem by making the example concrete and imperative.
The first line is a real life imperative statement with a ticket number
from our issue tracker. The body describes the behavior without the patch,
why this is a problem, and how the patch fixes the problem when applied.
````
* 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
+always necessary to create a new ticket in Jira. 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).
* Push your changes to a topic branch in your fork of the repository.
* Submit a pull request to the repository in the puppetlabs organization.
-* Update your Redmine ticket to mark that you have submitted code and are ready for it to be reviewed.
- * Include a link to the pull request in the ticket
+* Update your Jira ticket to mark that you have submitted code and are ready for it to be reviewed (Status: Ready for Merge).
+ * Include a link to the pull request in the ticket.
# Additional Resources
* [More information on contributing](http://links.puppetlabs.com/contribute-to-puppet)
-* [Bug tracker (Redmine)](http://projects.puppetlabs.com)
+* [Bug tracker (Jira)](http://tickets.puppetlabs.com)
* [Contributor License Agreement](http://links.puppetlabs.com/cla)
* [General GitHub documentation](http://help.github.com/)
* [GitHub pull request documentation](http://help.github.com/send-pull-requests/)
* #puppet-dev IRC channel on freenode.org
diff --git a/Gemfile b/Gemfile
index 4bda54611..7188ad9b7 100644
--- a/Gemfile
+++ b/Gemfile
@@ -1,82 +1,88 @@
source ENV['GEM_SOURCE'] || "https://rubygems.org"
def location_for(place, fake_version = nil)
if place =~ /^(git[:@][^#]*)#(.*)/
[fake_version, { :git => $1, :branch => $2, :require => false }].compact
elsif place =~ /^file:\/\/(.*)/
['>= 0', { :path => File.expand_path($1), :require => false }]
else
[place, { :require => false }]
end
end
# C Ruby (MRI) or Rubinius, but NOT Windows
platforms :ruby do
gem 'pry', :group => :development
gem 'yard', :group => :development
gem 'redcarpet', '~> 2.0', :group => :development
gem "racc", "1.4.9", :group => :development
end
gem "puppet", :path => File.dirname(__FILE__), :require => false
gem "facter", *location_for(ENV['FACTER_LOCATION'] || ['> 1.6', '< 3'])
gem "hiera", *location_for(ENV['HIERA_LOCATION'] || '~> 1.0')
gem "rake", :require => false
gem "rgen", "0.6.5", :require => false
group(:development, :test) do
# Jenkins workers may be using RSpec 2.9, so RSpec 2.11 syntax
# (like `expect(value).to eq matcher`) should be avoided.
gem "rspec", "~> 2.11.0", :require => false
# Mocha is not compatible across minor version changes; because of this only
# versions matching ~> 0.10.5 are supported. All other versions are unsupported
# and can be expected to fail.
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(:development) do
+ case RUBY_VERSION
+ when /^1.8/
+ gem 'ruby-prof', "~> 0.13.1", :require => false
+ else
+ gem 'ruby-prof', :require => false
+ end
end
group(:extra) do
gem "rack", "~> 1.4", :require => false
- gem "activerecord", '~> 3.0.7', :require => false
+ gem "activerecord", '~> 3.2', :require => false
gem "couchrest", '~> 1.0', :require => false
gem "net-ssh", '~> 2.1', :require => false
gem "puppetlabs_spec_helper", :require => false
gem "sqlite3", :require => false
gem "stomp", :require => false
gem "tzinfo", :require => false
gem "msgpack", :require => false
end
-platforms :mswin, :mingw do
- 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
+require 'yaml'
+data = YAML.load_file(File.join(File.dirname(__FILE__), 'ext', 'project_data.yaml'))
+bundle_platforms = data['bundle_platforms']
+data['gem_platform_dependencies'].each_pair do |gem_platform, info|
+ if bundle_deps = info['gem_runtime_dependencies']
+ bundle_platform = bundle_platforms[gem_platform] or raise "Missing bundle_platform"
+ platform(bundle_platform.intern) do
+ bundle_deps.each_pair do |name, version|
+ gem(name, version, :require => false)
+ end
+ end
+ end
end
if File.exists? "#{__FILE__}.local"
eval(File.read("#{__FILE__}.local"), binding)
end
# vim:filetype=ruby
diff --git a/LICENSE b/LICENSE
index 934582dfa..e2f64ae04 100644
--- a/LICENSE
+++ b/LICENSE
@@ -1,17 +1,17 @@
Puppet - Automating Configuration Management.
- Copyright (C) 2005-2012 Puppet Labs Inc
+ Copyright (C) 2005-2014 Puppet Labs Inc
Puppet Labs can be contacted at: info@puppetlabs.com
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
diff --git a/README.md b/README.md
index 648e92327..0fe5627b3 100644
--- a/README.md
+++ b/README.md
@@ -1,53 +1,75 @@
Puppet
======
[![Build Status](https://travis-ci.org/puppetlabs/puppet.png?branch=master)](https://travis-ci.org/puppetlabs/puppet)
Puppet, an automated administrative engine for your Linux, Unix, and Windows systems, performs
administrative tasks (such as adding users, installing packages, and updating server
configurations) based on a centralized specification.
Documentation
-------------
Documentation for Puppet and related projects can be found online at the
[Puppet Docs site](http://docs.puppetlabs.com).
+HTTP API
+--------
+[HTTP API Index](api/docs/http_api_index.md)
+
Installation
------------
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)
To install an open source release of Puppet,
[see the installation guide on the docs site.](http://docs.puppetlabs.com/guides/installation.html)
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
+Developing and Contributing
------
-Please see our [Contribution
-Documents](https://github.com/puppetlabs/puppet/blob/master/CONTRIBUTING.md)
-and our [Developer
-Documentation](https://github.com/puppetlabs/puppet/blob/master/README_DEVELOPER.md).
+We'd love to get contributions from you! For a quick guide to getting your
+system setup for developing take a look at our [Quickstart
+Guide](docs/quickstart.md). Once you are up and running, take a look at the
+[Contribution Documents](CONTRIBUTING.md) to see how to get your changes merged
+in.
+
+For more complete docs on developing with puppet you can take a look at the
+rest of the [developer documents](docs/index.md).
License
-------
-See LICENSE file.
+See [LICENSE](LICENSE) file.
Support
-------
-Please log tickets and issues at our [Projects
-site](http://projects.puppetlabs.com). A [mailing
+Please log tickets and issues at our [JIRA tracker](http://tickets.puppetlabs.com). A [mailing
list](https://groups.google.com/forum/?fromgroups#!forum/puppet-users) is
available for asking questions and getting help from others. In addition there
is an active #puppet channel on Freenode.
-HTTP API
---------
-{file:api/docs/http_api_index.md HTTP API Index}
+We use semantic version numbers for our releases, and recommend that users stay
+as up-to-date as possible by upgrading to patch releases and minor releases as
+they become available.
+
+Bugfixes and ongoing development will occur in minor releases for the current
+major version. Security fixes will be backported to a previous major version on
+a best-effort basis, until the previous major version is no longer maintained.
+
+
+For example: If a security vulnerability is discovered in Puppet 4.1.1, we
+would fix it in the 4 series, most likely as 4.1.2. Maintainers would then make
+a best effort to backport that fix onto the latest Puppet 3 release.
+
+Long-term support, including security patches and bug fixes, is available for
+commercial customers. Please see the following page for more details:
+
+[Puppet Enterprise Support Lifecycle](http://puppetlabs.com/misc/puppet-enterprise-lifecycle)
+
diff --git a/README_DEVELOPER.md b/README_DEVELOPER.md
deleted file mode 100644
index aac8b0c25..000000000
--- a/README_DEVELOPER.md
+++ /dev/null
@@ -1,809 +0,0 @@
-# Developer README #
-
-This file is intended to provide a place for developers and contributors to
-document what other developers need to know about changes made to Puppet.
-
-# Internal Structures
-
-## Two Types of Catalog
-
-When working on subsystems of Puppet that deal with the catalog it is important
-to be aware of the two different types of Catalog. Developers will often find
-this difference while working on the static compiler and types and providers.
-
-The two different types of catalog becomes relevant when writing spec tests
-because we frequently need to wire up a fake catalog so that we can exercise
-types, providers, or termini that filter the catalog.
-
-The two different types of catalogs are so-called "resource" catalogs and "RAL"
-(resource abstraction layer) catalogs. At a high level, the resource catalog
-is the in-memory object we serialize and transfer around the network. The
-compiler terminus is expected to produce a resource catalog. The agent takes a
-resource catalog and converts it into a RAL catalog. The RAL catalog is what
-is used to apply the configuration model to the system.
-
-Resource dependency information is most easily obtained from a RAL catalog by
-walking the graph instance produced by the `relationship_graph` method.
-
-### Resource Catalog
-
-If you're writing spec tests for something that deals with a catalog "server
-side," a new catalog terminus for example, then you'll be dealing with a
-resource catalog. You can produce a resource catalog suitable for spec tests
-using something like this:
-
- let(:catalog) do
- catalog = Puppet::Resource::Catalog.new("node-name-val") # NOT certname!
- rsrc = Puppet::Resource.new("file", "sshd_config",
- :parameters => {
- :ensure => 'file',
- :source => 'puppet:///modules/filetest/sshd_config',
- }
- )
- rsrc.file = 'site.pp'
- rsrc.line = 21
- catalog.add_resource(rsrc)
- end
-
-The resources in this catalog may be accessed using `catalog.resources`.
-Resource dependencies are not easily walked using a resource catalog however.
-To walk the dependency tree convert the catalog to a RAL catalog as described
-in
-
-### RAL Catalog
-
-The resource catalog may be converted to a RAL catalog using `catalog.to_ral`.
-The RAL catalog contains `Puppet::Type` instances instead of `Puppet::Resource`
-instances as is the case with the resource catalog.
-
-One very useful feature of the RAL catalog are the methods to work with
-resource relationships. For example:
-
- irb> catalog = catalog.to_ral
- irb> graph = catalog.relationship_graph
- irb> pp graph.edges
- [{ Notify[alpha] => File[/tmp/file_20.txt] },
- { Notify[alpha] => File[/tmp/file_21.txt] },
- { Notify[alpha] => File[/tmp/file_22.txt] },
- { Notify[alpha] => File[/tmp/file_23.txt] },
- { Notify[alpha] => File[/tmp/file_24.txt] },
- { Notify[alpha] => File[/tmp/file_25.txt] },
- { Notify[alpha] => File[/tmp/file_26.txt] },
- { Notify[alpha] => File[/tmp/file_27.txt] },
- { Notify[alpha] => File[/tmp/file_28.txt] },
- { Notify[alpha] => File[/tmp/file_29.txt] },
- { File[/tmp/file_20.txt] => Notify[omega] },
- { File[/tmp/file_21.txt] => Notify[omega] },
- { File[/tmp/file_22.txt] => Notify[omega] },
- { File[/tmp/file_23.txt] => Notify[omega] },
- { File[/tmp/file_24.txt] => Notify[omega] },
- { File[/tmp/file_25.txt] => Notify[omega] },
- { File[/tmp/file_26.txt] => Notify[omega] },
- { File[/tmp/file_27.txt] => Notify[omega] },
- { File[/tmp/file_28.txt] => Notify[omega] },
- { File[/tmp/file_29.txt] => Notify[omega] }]
-
-If the `relationship_graph` method is throwing exceptions at you, there's a
-good chance the catalog is not a RAL catalog.
-
-## Settings Catalog ##
-
-Be aware that Puppet creates a mini catalog and applies this catalog locally to
-manage file resource from the settings. This behavior made it difficult and
-time consuming to track down a race condition in
-[2888](http://projects.puppetlabs.com/issues/2888).
-
-Even more surprising, the `File[puppetdlockfile]` resource is only added to the
-settings catalog if the file exists on disk. This caused the race condition as
-it will exist when a separate process holds the lock while applying the
-catalog.
-
-It may be sufficient to simply be aware of the settings catalog and the
-potential for race conditions it presents. An effective way to be reasonably
-sure and track down the problem is to wrap the File.open method like so:
-
- # We're wrapping ourselves around the File.open method.
- # As described at: http://goo.gl/lDsv6
- class File
- WHITELIST = [ /pidlock.rb:39/ ]
-
- class << self
- alias xxx_orig_open open
- end
-
- def self.open(name, *rest, &block)
- # Check the whitelist for any "good" File.open calls against the #
- puppetdlock file
- white_listed = caller(0).find do |line|
- JJM_WHITELIST.find { |re| re.match(line) }
- end
-
- # If you drop into IRB here, take a look at your caller, it might be
- # the ghost in the machine you're looking for.
- binding.pry if name =~ /puppetdlock/ and not white_listed
- xxx_orig_open(name, *rest, &block)
- end
- end
-
-The settings catalog is populated by the `Puppet::Util::Settings#to\_catalog`
-method.
-
-# Ruby Dependencies #
-
-To install the dependencies run:
-
- $ bundle install --path .bundle/gems/
-
-Once this is done, you can interact with puppet through bundler using `bundle
-exec <command>` which will ensure that `<command>` is executed in the context
-of puppet's dependencies.
-
-For example to run the specs:
-
- $ bundle exec rake spec
-
-To run puppet itself (for a resource lookup say):
-
- $ bundle exec puppet resource host localhost
-
-which should return something like:
-
- host { 'localhost':
- ensure => 'present',
- ip => '127.0.0.1',
- target => '/etc/hosts',
- }
-
-# Running Tests #
-
-Puppet Labs projects use a common convention of using Rake to run unit tests.
-The tests can be run with the following rake task:
-
- bundle exec rake spec
-
-This allows the Rakefile to set up the environment beforehand if needed. This
-method is how the unit tests are run in [Jenkins](https://jenkins.puppetlabs.com).
-
-Under the hood Puppet's tests use `rspec`. To run all of them, you can directly
-use 'rspec':
-
- bundle exec rspec
-
-To run a single file's worth of tests (much faster!), give the filename, and use
-the nested format to see the descriptions:
-
- bundle exec rspec spec/unit/ssl/host_spec.rb --format nested
-
-## Testing dependency version requirements
-
-Puppet is only compatible with certain versions of RSpec and Mocha. If you are
-not using Bundler to install the required test libraries you must ensure that
-you are using the right library versions. Using unsupported versions of Mocha
-and RSpec will probably display many spurious failures. The supported versions
-of RSpec and Mocha can be found in the project Gemfile.
-
-# A brief introduction to testing in Puppet
-
-Puppet relies heavily on automated testing to ensure that Puppet behaves as
-expected and that new features don't interfere with existing behavior. There are
-three primary sets of tests that Puppet uses: _unit tests_, _integration tests_,
-and _acceptance tests_.
-
-- - -
-
-Unit tests are used to test the individual components of Puppet to ensure that
-they function as expected in isolation. Unit tests are designed to hide the
-actual system implementations and provide canned information so that only the
-intended behavior is tested, rather than the targeted code and everything else
-connected to it. Unit tests should never affect the state of the system that's
-running the test.
-
-- - -
-
-Integration tests serve to test different units of code together to ensure that
-they interact correctly. While individual methods might perform correctly, when
-used with the rest of the system they might fail, so integration tests are a
-higher level version of unit tests that serve to check the behavior of
-individual subsystems.
-
-All of the unit and integration tests for Puppet are kept in the spec/ directory.
-
-- - -
-
-Acceptance tests are used to test high level behaviors of Puppet that deal with
-a number of concerns and aren't easily tested with normal unit tests. Acceptance
-tests function by changing system state and checking the system after
-the fact to make sure that the intended behavior occurred. Because of this
-acceptance tests can be destructive, so the systems being tested should be
-throwaway systems.
-
-All of the acceptance tests for Puppet are kept in the acceptance/tests/
-directory.
-
-## Puppet Continuous integration
-
- * Travis-ci (unit tests only): https://travis-ci.org/puppetlabs/puppet/
- * Jenkins (unit and acceptance tests): https://jenkins.puppetlabs.com/view/Puppet%20FOSS/
-
-## RSpec
-
-Puppet uses RSpec to perform unit and integration tests. RSpec handles a number
-of concerns to make testing easier:
-
- * Executing examples and ensuring the actual behavior matches the expected behavior (examples)
- * Grouping tests (describe and contexts)
- * Setting up test environments and cleaning up afterwards (before and after blocks)
- * Isolating tests (mocks and stubs)
-
-#### Examples and expectations
-
-At the most basic level, RSpec provides a framework for executing tests (which
-are called examples) and ensuring that the actual behavior matches the expected
-behavior (which are done with expectations)
-
-```ruby
-# This is an example; it sets the test name and defines the test to run
-specify "one equals one" do
- # 'should' is an expectation; it adds a check to make sure that the left argument
- # matches the right argument
- 1.should == 1
-end
-
-# Examples can be declared with either 'it' or 'specify'
-it "one doesn't equal two" do
- 1.should_not == 2
-end
-```
-
-Good examples generally do as little setup as possible and only test one or two
-things; it makes tests easier to understand and easier to debug.
-
-More complete documentation on expectations is available at https://www.relishapp.com/rspec/rspec-expectations/docs
-
-### Example groups
-
-Example groups are fairly self explanatory; they group similar examples into a
-set.
-
-```ruby
-describe "the number one" do
-
- it "is larger than zero" do
- 1.should be > 0
- end
-
- it "is an odd number" do
- 1.odd?.should be true
- end
-
- it "is not nil" do
- 1.should_not be_nil
- end
-end
-```
-
-Example groups have a number of uses that we'll get into later, but one of the
-simplest demonstrations of what they do is how they help to format
-documentation:
-
-```
-rspec ex.rb --format documentation
-
-the number one
- is larger than zero
- is an odd number
- is not nil
-
-Finished in 0.00516 seconds
-3 examples, 0 failures
-```
-
-### Setting up and tearing down tests
-
-Examples may require some setup before they can run, and might need to clean up
-afterwards. `before` and `after` blocks can be used before this, and can be
-used inside of example groups to limit how many examples they affect.
-
-```ruby
-
-describe "something that could warn" do
- before :each do
- # Disable warnings for this test
- $VERBOSE = nil
- end
-
- after do
- # Enable warnings afterwards
- $VERBOSE = true
- end
-
- it "doesn't generate a warning" do
- MY_CONSTANT = 1
- # reassigning a normally prints out 'warning: already initialized constant FOO'
- MY_CONSTANT = 2
- end
-end
-```
-
-### Setting up helper data
-
-Some examples may require setting up data before hand and making it available to
-tests. RSpec provides helper methods with the `let` method call that can be used
-inside of tests.
-
-```ruby
-describe "a helper object" do
- # This creates an array with three elements that we can retrieve in tests. A
- # new copy will be made for each test.
- let(:my_helper) do
- ['foo', 'bar', 'baz']
- end
-
- it "should be an array" do
- my_helper.should be_a_kind_of Array
- end
-
- it "should have three elements" do
- my_helper.should have(3).items
- end
-end
-```
-
-Like `before` blocks, helper objects like this are used to avoid doing a lot of
-setup in individual examples and share setup between similar tests.
-
-### Isolating tests with stubs
-
-RSpec allows you to provide fake data during testing to make sure that
-individual tests are only running the code being tested. You can stub out entire
-objects, or just stub out individual methods on an object. When a method is
-stubbed the method itself will never be called.
-
-While RSpec comes with its own stubbing framework, Puppet uses the Mocha
-framework.
-
-A brief usage guide for Mocha is available at http://gofreerange.com/mocha/docs/#Usage,
-and an overview of Mocha expectations is available at http://gofreerange.com/mocha/docs/Mocha/Expectation.html
-
-```ruby
-describe "stubbing a method on an object" do
- let(:my_helper) do
- ['foo', 'bar', 'baz']
- end
-
- it 'should have three items before being stubbed' do
- my_helper.size.should == 3
- end
-
- describe 'when stubbing the size' do
- before do
- my_helper.stubs(:size).returns 10
- end
-
- it 'should have the stubbed value for size' do
- my_helper.size.should == 10
- end
- end
-end
-```
-
-Entire objects can be stubbed as well.
-
-```ruby
-describe "stubbing an object" do
- let(:my_helper) do
- stub(:not_an_array, :size => 10)
- end
-
- it 'should have the stubbed size'
- my_helper.size.should == 10
- end
-end
-```
-
-### Adding expectations with mocks
-
-It's possible to combine the concepts of stubbing and expectations so that a
-method has to be called for the test to pass (like an expectation), and can
-return a fixed value (like a stub).
-
-```ruby
-describe "mocking a method on an object" do
- let(:my_helper) do
- ['foo', 'bar', 'baz']
- end
-
- describe "when mocking the size" do
- before do
- my_helper.expects(:size).returns 10
- end
-
- it "adds an expectation that a method was called" do
- my_helper.size
- end
- end
-end
-```
-
-Like stubs, entire objects can be mocked.
-
-```ruby
-describe "mocking an object" do
- let(:my_helper) do
- mock(:not_an_array)
- end
-
- before do
- not_an_array.expects(:size).returns 10
- end
-
- it "adds an expectation that the method was called" do
- not_an_array.size
- end
-end
-```
-### Writing tests without side effects
-
-When properly written each test should be able to run in isolation, and tests
-should be able to be run in any order. This makes tests more reliable and allows
-a single test to be run if only that test is failing, instead of running all
-17000+ tests each time something is changed. However, there are a number of ways
-that can make tests fail when run in isolation or out of order.
-
-#### Using instance variables
-
-Puppet has a number of older tests that use `before` blocks and instance
-variables to set up fixture data, instead of `let` blocks. These can retain
-state between tests, which can lead to test failures when tests are run out of
-order.
-
-```ruby
-# test.rb
-RSpec.configure do |c|
- c.mock_framework = :mocha
-end
-
-describe "fixture data" do
- describe "using instance variables" do
-
- # BAD
- before :all do
- # This fixture will be created only once and will retain the `foo` stub
- # between tests.
- @fixture = stub 'test data'
- end
-
- it "can be stubbed" do
- @fixture.stubs(:foo).returns :bar
- @fixture.foo.should == :bar
- end
-
- it "should not keep state between tests" do
- # The foo stub was added in the previous test and shouldn't be present
- # in this test.
- expect { @fixture.foo }.to raise_error
- end
- end
-
- describe "using `let` blocks" do
-
- # GOOD
- # This will be recreated between tests so that state isn't retained.
- let(:fixture) { stub 'test data' }
-
- it "can be stubbed" do
- fixture.stubs(:foo).returns :bar
- fixture.foo.should == :bar
- end
-
- it "should not keep state between tests" do
- # since let blocks are regenerated between tests, the foo stub added in
- # the previous test will not be present here.
- expect { fixture.foo }.to raise_error
- end
- end
-end
-```
-
-```
-bundle exec rspec test.rb -fd
-
-fixture data
- using instance variables
- can be stubbed
- should not keep state between tests (FAILED - 1)
- using `let` blocks
- can be stubbed
- should not keep state between tests
-
-Failures:
-
- 1) fixture data using instance variables should not keep state between tests
- Failure/Error: expect { @fixture.foo }.to raise_error
- expected Exception but nothing was raised
- # ./test.rb:17:in `block (3 levels) in <top (required)>'
-
-Finished in 0.00248 seconds
-4 examples, 1 failure
-
-Failed examples:
-
-rspec ./test.rb:16 # fixture data using instance variables should not keep state between tests
-```
-
-
-### RSpec references
-
- * RSpec core docs: https://www.relishapp.com/rspec/rspec-core/docs
- * RSpec guidelines with Ruby: http://betterspecs.org/
-
-### Puppet-acceptance
-
-[puppet-acceptance]: https://github.com/puppetlabs/puppet-acceptance
-[test::unit]: http://test-unit.rubyforge.org/
-
-Puppet has a custom acceptance testing framework called
-[puppet-acceptance][puppet-acceptance] for running acceptance tests.
-Puppet-acceptance runs the tests by configuring one or more VMs, copying the
-test cases onto the VMs, performing the tests and collecting the results, and
-ensuring that the results match the intended behavior. It uses
-[test::unit][test::unit] to perform the actual assertions.
-
-# UTF-8 Handling #
-
-As Ruby 1.9 becomes more commonly used with Puppet, developers should be aware
-of major changes to the way Strings and Regexp objects are handled.
-Specifically, every instance of these two classes will have an encoding
-attribute determined in a number of ways.
-
- * If the source file has an encoding specified in the magic comment at the
- top, the instance will take on that encoding.
- * Otherwise, the encoding will be determined by the LC\_LANG or LANG
- environment variables.
- * Otherwise, the encoding will default to ASCII-8BIT
-
-## References ##
-
-Excellent information about the differences between encodings in Ruby 1.8 and
-Ruby 1.9 is published in this blog series:
-[Understanding M17n](http://links.puppetlabs.com/understanding_m17n)
-
-## Encodings of Regexp and String instances ##
-
-In general, please be aware that Ruby 1.9 regular expressions need to be
-compatible with the encoding of a string being used to match them. If they are
-not compatible you can expect to receive and error such as:
-
- Encoding::CompatibilityError: incompatible encoding regexp match (ASCII-8BIT
- regexp with UTF-8 string)
-
-In addition, some escape sequences were valid in Ruby 1.8 are no longer valid
-in 1.9 if the regular expression is not marked as an ASCII-8BIT object. You
-may expect errors like this in this situation:
-
- SyntaxError: (irb):7: invalid multibyte escape: /\xFF/
-
-This error is particularly common when serializing a string to other
-representations like JSON or YAML. To resolve the problem you can explicitly
-mark the regular expression as ASCII-8BIT using the /n flag:
-
- "a" =~ /\342\230\203/n
-
-Finally, any time you're thinking of a string as an array of bytes rather than
-an array of characters, common when escaping a string, you should work with
-everything in ASCII-8BIT. Changing the encoding will not change the data
-itself and allow the Regexp and the String to deal with bytes rather than
-characters.
-
-Puppet provides a monkey patch to String which returns an encoding suitable for
-byte manipulations:
-
- # Example of how to escape non ASCII printable characters for YAML.
- >> snowman = "☃"
- >> snowman.to_ascii8bit.gsub(/([\x80-\xFF])/n) { |x| "\\x#{x.unpack("C")[0].to_s(16)} }
- => "\\xe2\\x98\\x83"
-
-If the Regexp is not marked as ASCII-8BIT using /n, then you can expect the
-SyntaxError, invalid multibyte escape as mentioned above.
-
-# Windows #
-
-If you'd like to run Puppet from source on Windows platforms, the
-include `ext/envpuppet.bat` will help.
-
-To quickly run Puppet from source, assuming you already have Ruby installed
-from [rubyinstaller.org](http://rubyinstaller.org).
-
- C:\> cd C:\work\puppet
- C:\work\puppet> set PATH=%PATH%;C:\work\puppet\ext
- C:\work\puppet> envpuppet bundle install
- C:\work\puppet> envpuppet puppet --version
- 2.7.9
-
-When writing a test that cannot possibly run on Windows, e.g. there is
-no mount type on windows, do the following:
-
- describe Puppet::MyClass, :unless => Puppet.features.microsoft_windows? do
- ..
- end
-
-If the test doesn't currently pass on Windows, e.g. due to on going porting, then use an rspec conditional pending block:
-
- pending("porting to Windows", :if => Puppet.features.microsoft_windows?) do
- <example1>
- end
-
- pending("porting to Windows", :if => Puppet.features.microsoft_windows?) do
- <example2>
- end
-
-Then run the test as:
-
- C:\work\puppet> envpuppet bundle exec rspec spec
-
-## Common Issues ##
-
- * Don't assume file paths start with '/', as that is not a valid path on
- Windows. Use Puppet::Util.absolute\_path? to validate that a path is fully
- qualified.
-
- * Use File.expand\_path('/tmp') in tests to generate a fully qualified path
- that is valid on POSIX and Windows. In the latter case, the current working
- directory will be used to expand the path.
-
- * Always use binary mode when performing file I/O, unless you explicitly want
- Ruby to translate between unix and dos line endings. For example, opening an
- executable file in text mode will almost certainly corrupt the resulting
- stream, as will occur when using:
-
- IO.open(path, 'r') { |f| ... }
- IO.read(path)
-
- If in doubt, specify binary mode explicitly:
-
- IO.open(path, 'rb')
-
- * Don't assume file paths are separated by ':'. Use `File::PATH_SEPARATOR`
- instead, which is ':' on POSIX and ';' on Windows.
-
- * On Windows, `File::SEPARATOR` is '/', and `File::ALT_SEPARATOR` is '\'. On
- POSIX systems, `File::ALT_SEPARATOR` is nil. In general, use '/' as the
- separator as most Windows APIs, e.g. CreateFile, accept both types of
- separators.
-
- * Don't use waitpid/waitpid2 if you need the child process' exit code,
- as the child process may exit before it has a chance to open the
- child's HANDLE and retrieve its exit code. Use Puppet::Util.execute.
-
- * Don't assume 'C' drive. Use environment variables to look these up:
-
- "#{ENV['windir']}/system32/netsh.exe"
-
-# Configuration Directory #
-
-In Puppet 3.x we've simplified the behavior of selecting a configuration file
-to load. The intended behavior of reading `puppet.conf` is:
-
- 1. Use the explicit configuration provided by --confdir or --config if present
- 2. If running as root (`Puppet.features.root?`) then use the system
- `puppet.conf`
- 3. Otherwise, use `~/.puppet/puppet.conf`.
-
-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/config.ru` file for an up-to-date example.
-
-# Determining the Puppet Version
-
-If you need to programmatically work with the Puppet version, please use the
-following:
-
- require 'puppet/version'
- # Get the version baked into the sourcecode:
- version = Puppet.version
- # Set the version (e.g. in a Rakefile based on `git describe`)
- Puppet.version = '2.3.4'
-
-Please do not monkey patch the constant `Puppet::PUPPETVERSION` or obtain the
-version using the constant. The only supported way to set and get the Puppet
-version is through the accessor methods.
-
-# Static Compiler
-
-The static compiler was added to Puppet in the 2.7.0 release.
-[1](http://links.puppetlabs.com/static-compiler-announce)
-
-The static compiler is intended to provide a configuration catalog that
-requires a minimal amount of network communication in order to apply the
-catalog to the system. As implemented in Puppet 2.7.x and Puppet 3.0.x this
-intention takes the form of replacing all of the source parameters of File
-resources with a content parameter containing an address in the form of a
-checksum. The expected behavior is that the process applying the catalog to
-the node will retrieve the file content from the FileBucket instead of the
-FileServer.
-
-The high level approach can be described as follows. The `StaticCompiler` is a
-terminus that inserts itself between the "normal" compiler terminus and the
-request. The static compiler takes the resource catalog produced by the
-compiler and filters all File resources. Any file resource that contains a
-source parameter with a value starting with 'puppet://' is filtered in the
-following way in a "standard" single master / networked agents deployment
-scenario:
-
- 1. The content, owner, group, and mode values are retrieved from th
- FileServer by the master.
- 2. The file content is stored in the file bucket on the master.
- 3. The source parameter value is stripped from the File resource.
- 4. The content parameter value is set in the File resource using the form
- '{XXX}1234567890' which can be thought of as a content address indexed by
- checksum.
- 5. The owner, group and mode values are set in the File resource if they are
- not already set.
- 6. The filtered catalog is returned in the response.
-
-In addition to the catalog terminus, the process requesting the catalog needs
-to obtain the file content. The default behavior of `puppet agent` is to
-obtain file contents from the local client bucket. The method we expect users
-to employ to reconfigure the agent to use the server bucket is to declare the
-`Filebucket[puppet]` resource with the address of the master. For example:
-
- node default {
- filebucket { puppet:
- server => $server,
- path => false,
- }
- class { filetest: }
- }
-
-This special filebucket resource named "puppet" will cause the agent to fetch
-file contents specified by checksum from the remote filebucket instead of the
-default clientbucket.
-
-## Trying out the Static Compiler
-
-Create a module that recursively downloads something. The jeffmccune-filetest
-module will recursively copy the rubygems source tree.
-
- $ bundle exec puppet module install jeffmccune-filetest
-
-Start the master with the StaticCompiler turned on:
-
- $ bundle exec puppet master \
- --catalog_terminus=static_compiler \
- --verbose \
- --no-daemonize
-
-Add the special Filebucket[puppet] resource:
-
- # site.pp
- node default {
- filebucket { puppet: server => $server, path => false }
- class { filetest: }
- }
-
-Get the static catalog:
-
- $ bundle exec puppet agent --test
-
-You should expect all file metadata to be contained in the catalog, including a
-checksum representing the content. When managing an out of sync file resource,
-the real contents should be fetched from the server instead of the
-clientbucket.
-
-Package Maintainers
-=====
-
-Software Version API
------
-
-Please see the public API regarding the software version as described in
-`lib/puppet/version.rb`. Puppet provides the means to easily specify the exact
-version of the software packaged using the VERSION file, for example:
-
- $ git describe --match "3.0.*" > lib/puppet/VERSION
- $ ruby -r puppet/version -e 'puts Puppet.version'
- 3.0.1-260-g9ca4e54
-
-EOF
diff --git a/Rakefile b/Rakefile
index 97172a50d..3bb695abe 100644
--- a/Rakefile
+++ b/Rakefile
@@ -1,68 +1,68 @@
# Rakefile for Puppet -*- ruby -*-
RAKE_ROOT = File.dirname(__FILE__)
# We need access to the Puppet.version method
$LOAD_PATH.unshift(File.expand_path("lib"))
require 'puppet/version'
$LOAD_PATH << File.join(RAKE_ROOT, 'tasks')
begin
require 'rubygems'
require 'rubygems/package_task'
rescue LoadError
# Users of older versions of Rake (0.8.7 for example) will not necessarily
# have rubygems installed, or the newer rubygems package_task for that
# matter.
require 'rake/packagetask'
require 'rake/gempackagetask'
end
require 'rake'
Dir['tasks/**/*.rake'].each { |t| load t }
begin
load File.join(RAKE_ROOT, 'ext', 'packaging', 'packaging.rake')
rescue LoadError
end
build_defs_file = 'ext/build_defaults.yaml'
if File.exist?(build_defs_file)
begin
require 'yaml'
@build_defaults ||= YAML.load_file(build_defs_file)
rescue Exception => e
STDERR.puts "Unable to load yaml from #{build_defs_file}:"
STDERR.puts e
end
@packaging_url = @build_defaults['packaging_url']
@packaging_repo = @build_defaults['packaging_repo']
raise "Could not find packaging url in #{build_defs_file}" if @packaging_url.nil?
raise "Could not find packaging repo in #{build_defs_file}" if @packaging_repo.nil?
namespace :package do
desc "Bootstrap packaging automation, e.g. clone into packaging repo"
task :bootstrap do
if File.exist?("ext/#{@packaging_repo}")
puts "It looks like you already have ext/#{@packaging_repo}. If you don't like it, blow it away with package:implode."
else
cd 'ext' do
%x{git clone #{@packaging_url}}
end
end
end
desc "Remove all cloned packaging automation"
task :implode do
rm_rf "ext/#{@packaging_repo}"
end
end
end
task :default do
sh %{rake -T}
end
task :spec do
- sh %{rspec spec}
+ sh %{rspec #{ENV['TEST'] || ENV['TESTS'] || 'spec'}}
end
diff --git a/acceptance/config/el6/.gitignore b/acceptance/.gitignore
similarity index 100%
rename from acceptance/config/el6/.gitignore
rename to acceptance/.gitignore
diff --git a/acceptance/config/el6/Gemfile b/acceptance/Gemfile
similarity index 91%
rename from acceptance/config/el6/Gemfile
rename to acceptance/Gemfile
index f15b1caac..134559362 100644
--- a/acceptance/config/el6/Gemfile
+++ b/acceptance/Gemfile
@@ -1,13 +1,13 @@
source ENV['GEM_SOURCE'] || "https://rubygems.org"
-gem "beaker", "~> 1.3.1"
+gem "beaker", "~> 1.7.0"
gem "rake"
group(:test) do
gem "rspec", "~> 2.11.0", :require => false
gem "mocha", "~> 0.10.5", :require => false
end
if File.exists? "#{__FILE__}.local"
eval(File.read("#{__FILE__}.local"), binding)
end
diff --git a/acceptance/config/el6/Rakefile b/acceptance/Rakefile
similarity index 80%
rename from acceptance/config/el6/Rakefile
rename to acceptance/Rakefile
index 6b6fa89ab..b0b287cff 100644
--- a/acceptance/config/el6/Rakefile
+++ b/acceptance/Rakefile
@@ -1,318 +1,309 @@
require 'rake/clean'
require 'pp'
require 'yaml'
ONE_DAY_IN_SECS = 24 * 60 * 60
REPO_CONFIGS_DIR = "repo-configs"
CLEAN.include('*.tar', REPO_CONFIGS_DIR, 'merged_options.rb')
module HarnessOptions
DEFAULTS = {
:type => 'git',
- :helper => ['../../lib/helper.rb'],
- :tests => ['../../tests'],
- :debug => true,
+ :helper => ['lib/helper.rb'],
+ :tests => ['tests'],
+ :log_level => 'debug',
:color => false,
:root_keys => true,
:ssh => {
:keys => ["id_rsa-acceptance"],
},
:xml => true,
:timesync => true,
:repo_proxy => true,
:add_el_extras => true,
+ :preserve_hosts => 'onfail'
}
class Aggregator
attr_reader :mode
def initialize(mode)
@mode = mode
end
def get_options(file_path)
puts file_path
if File.exists? file_path
options = eval(File.read(file_path), binding)
else
puts "No options file found at #{File.expand_path(file_path)}"
end
options || {}
end
def get_mode_options
get_options("./config/#{mode}/options.rb")
end
def get_local_options
get_options("./local_options.rb")
end
def final_options(intermediary_options = {})
mode_options = get_mode_options
local_overrides = get_local_options
final_options = DEFAULTS.merge(mode_options)
final_options.merge!(intermediary_options)
final_options.merge!(local_overrides)
return final_options
end
end
def self.options(mode, options)
final_options = Aggregator.new(mode).final_options(options)
final_options
end
end
def beaker_test(mode = :packages, options = {})
- final_options = HarnessOptions.options(mode, options)
+ delete_options = options[:__delete_options__] || []
+ final_options = HarnessOptions.options(mode,
+ options.reject { |k,v| k == :__delete_options__ })
if mode == :git
puppet_fork = ENV['FORK'] || 'puppetlabs'
git_server = ENV['GIT_SERVER'] || 'github.com'
final_options[:install] << "git://#{git_server}/#{puppet_fork}/puppet.git##{sha}"
end
+ delete_options.each do |delete_me|
+ final_options.delete(delete_me)
+ end
+
options_file = 'merged_options.rb'
File.open(options_file, 'w') do |merged|
merged.puts <<-EOS
# Copy this file to local_options.rb and adjust as needed if you wish to run
# with some local overrides.
EOS
merged.puts(final_options.pretty_inspect)
end
- tests = ENV['TESTS'] || ENV['TEST']
+ tests = ENV['TESTS'] || ENV['TEST']
tests_opt = "--tests=#{tests}" if tests
config_opt = "--hosts=#{config}" if config
overriding_options = ENV['OPTIONS']
args = ["--options-file", options_file, config_opt, tests_opt, overriding_options].compact
+ preserve_hosts = final_options[:preserve_hosts]
+ if md = /--preserve-hosts=?\s*['"]?(\w+)/.match(overriding_options)
+ preserve_hosts = md[1]
+ end
+
begin
- sh("beaker", *args)
+ failed = false
+ sh("beaker", *args) { |ok,res| failed = true if !ok }
ensure
if (hosts_file = config || final_options[:hosts_file]) && hosts_file !~ /preserved_config/
cp(hosts_file, "log/latest/config.yml")
- generate_config_for_latest_hosts if final_options[:preserve_hosts] || overriding_options =~ /--preserve-hosts/
+ generate_config_for_latest_hosts if preserve_hosts = 'always' || (failed && preserve_hosts = 'onfail')
end
mv(options_file, "log/latest")
end
end
def generate_config_for_latest_hosts
preserved_config_hash = { 'HOSTS' => {} }
config_hash = YAML.load_file('log/latest/config.yml').to_hash
nodes = config_hash['HOSTS'].map do |node_label,hash|
{ :node_label => node_label, :platform => hash['platform'] }
end
pre_suite_log = File.read('log/latest/pre_suite-run.log')
nodes.each do |node_info|
hostname = /^(\w+) \(#{node_info[:node_label]}\)/.match(pre_suite_log)[1]
fqdn = "#{hostname}.delivery.puppetlabs.net"
preserved_config_hash['HOSTS'][fqdn] = {
'roles' => [ 'agent'],
'platform' => node_info[:platform],
}
preserved_config_hash['HOSTS'][fqdn]['roles'].unshift('master') if node_info[:node_label] =~ /master/
end
pp preserved_config_hash
File.open('log/latest/preserved_config.yaml', 'w') do |config_file|
YAML.dump(preserved_config_hash, config_file)
end
rescue Errno::ENOENT => e
puts "Couldn't generate log #{e}"
end
def list_preserved_configurations(secs_ago = ONE_DAY_IN_SECS)
preserved = {}
Dir.glob('log/*_*').each do |dir|
preserved_config_path = "#{dir}/preserved_config.yaml"
yesterday = Time.now - secs_ago.to_i
if preserved_config = File.exists?(preserved_config_path)
directory = File.new(dir)
if directory.ctime > yesterday
hosts = []
preserved_config = YAML.load_file(preserved_config_path).to_hash
preserved_config['HOSTS'].each do |hostname,values|
hosts << "#{hostname}: #{values['platform']}, #{values['roles']}"
end
preserved[hosts] = directory.to_path
end
end
end
preserved.map { |k,v| [v,k] }.sort { |a,b| a[0] <=> b[0] }.reverse
end
def list_preserved_hosts(secs_ago = ONE_DAY_IN_SECS)
hosts = Set.new
Dir.glob('log/**/pre*suite*run.log').each do |log|
yesterday = Time.now - secs_ago.to_i
File.open(log, 'r') do |file|
if file.ctime > yesterday
file.each_line do |line|
- matchdata = /^(\w+) \(.*?\) \$/.match(line.encode!('UTF-8', 'UTF-8', :invalid => :replace))
+ matchdata = /^(\w+) \(.*?\) \d\d:\d\d:\d\d\$/.match(line.encode!('UTF-8', 'UTF-8', :invalid => :replace))
hosts.add(matchdata[1]) if matchdata
end
end
end
end
hosts
end
-# Plagiarized from Beaker::Vcloud#cleanup
-def destroy_preserved_hosts(hosts = nil, secs_ago = ONE_DAY_IN_SECS)
+def release_hosts(hosts = nil, secs_ago = ONE_DAY_IN_SECS)
secs_ago ||= ONE_DAY_IN_SECS
hosts ||= list_preserved_hosts(secs_ago)
- require 'beaker/hypervisor/vsphere_helper'
- vsphere_credentials = VsphereHelper.load_config("#{ENV['HOME']}/.fog")
-
- puts "Connecting to vSphere at #{vsphere_credentials[:server]}" +
- " with credentials for #{vsphere_credentials[:user]}"
-
- vsphere_helper = VsphereHelper.new( vsphere_credentials )
-
- vm_names = hosts.to_a
- pp vm_names
- vms = vsphere_helper.find_vms vm_names
- vm_names.each do |name|
- unless vm = vms[name]
- puts "Couldn't find VM #{name} in vSphere!"
- next
- end
-
- if vm.runtime.powerState == 'poweredOn'
- puts "Shutting down #{vm.name}"
- start = Time.now
- vm.PowerOffVM_Task.wait_for_completion
- puts "Spent %.2f seconds halting #{vm.name}" % (Time.now - start)
- end
-
- start = Time.now
- vm.Destroy_Task
- puts "Spent %.2f seconds destroying #{vm.name}" % (Time.now - start)
- end
-
- vsphere_helper.close
+ require 'beaker'
+ vcloud_pooled = Beaker::VcloudPooled.new(hosts.map { |h| { 'vmhostname' => h } },
+ :logger => Beaker::Logger.new,
+ :dot_fog => "#{ENV['HOME']}/.fog",
+ 'pooling_api' => 'http://vcloud.delivery.puppetlabs.net' ,
+ 'datastore' => 'not-used',
+ 'resourcepool' => 'not-used',
+ 'folder' => 'not-used')
+ vcloud_pooled.cleanup
end
def print_preserved(preserved)
preserved.each_with_index do |entry,i|
puts "##{i}: #{entry[0]}"
entry[1].each { |h| puts " #{h}" }
end
end
def beaker_run_type
type = ENV['TYPE'] || :packages
type = type.to_sym
end
def sha
ENV['SHA']
end
def config
ENV['CONFIG']
end
namespace :ci do
task :check_env do
raise(USAGE) unless sha
end
namespace :test do
USAGE = <<-EOS
Requires commit SHA to be put under test as environment variable: SHA='<sha>'.
Also must set CONFIG=config/nodes/foo.yaml or include it in an options.rb for Beaker.
You may set TESTS=path/to/test,and/more/tests.
You may set additional Beaker OPTIONS='--more --options'
If testing from git checkouts, you may optionally set the github fork to checkout from using FORK='other-puppet-fork'.
You may also optionally set the git server to checkout from using GIT_SERVER='my.host.with.git.daemon', if you have set up a `git daemon` to pull local commits from. (In this case, the FORK should be set to the path to the repository, and you will need to allow the git daemon to serve the repo (see `git help daemon`)).
If there is a Beaker options hash in a ./local_options.rb, it will be included. Commandline options set through the above environment variables will override settings in this file.
EOS
desc <<-EOS
Run the acceptance tests through Beaker and install packages on the configuration targets.
#{USAGE}
EOS
task :packages => 'ci:check_env' do
beaker_test
end
desc <<-EOS
Run the acceptance tests through Beaker and install from git on the configuration targets.
#{USAGE}
EOS
task :git => 'ci:check_env' do
beaker_test(:git)
end
end
desc "Capture the master and agent hostname from the latest log and construct a preserved_config.yaml for re-running against preserved hosts without provisioning."
task :extract_preserved_config do
generate_config_for_latest_hosts
end
desc <<-EOS
Run an acceptance test for a given node configuration and preserve the hosts.
Defaults to a packages run, but you can set it to 'git' with TYPE='git'.
#{USAGE}
EOS
task :test_and_preserve_hosts => 'ci:check_env' do
- beaker_test(beaker_run_type, :preserve_hosts => true)
+ beaker_test(beaker_run_type, :preserve_hosts => 'always')
end
desc "List acceptance runs from the past day which had hosts preserved."
task :list_preserved do
preserved = list_preserved_configurations
print_preserved(preserved)
end
desc <<-EOS
Shutdown and destroy any hosts that we have preserved for testing. These should be reaped daily by scripts, but this will free up resources immediately.
Specify a list of comma separated HOST_NAMES if you have a set of dynamic vcloud host names you want to purge outside of what can be grepped from the logs.
You can go back through the last SECS_AGO logs. Default is one day ago in secs.
EOS
- task :destroy_preserved_hosts do
+ task :release_hosts do
host_names = ENV['HOST_NAMES'].split(',') if ENV['HOST_NAMES']
secs_ago = ENV['SECS_AGO']
- destroy_preserved_hosts(host_names, secs_ago)
+ release_hosts(host_names, secs_ago)
+ end
+
+ task :destroy_preserved_hosts => 'ci:release_hosts' do
+ puts "Note: we are now releasing hosts back to the vcloud pooling api rather than destroying them directly. The rake task for this is ci:release_hosts"
end
desc <<-EOS
Rerun an acceptance test using the last captured preserved_config.yaml to skip provisioning.
Or specify a CONFIG_NUMBER from `rake ci:list_preserved`.
-Uses the setup/rsync/pre-suite to rsync the local puppet source onto master and agent.
-You may specify an RSYNC_FILTER_FILE as well.
-You may skip purgeing and reinstalling puppet packages by including SKIP_PACKAGE_REINSTALL.
-You may skip rsyncing local puppet files over to the tests hosts by including SKIP_RSYNC.
Defaults to a packages run, but you can set it to 'git' with TYPE='git'.
EOS
task :test_against_preserved_hosts do
config_number = (ENV['CONFIG_NUMBER'] || 0).to_i
preserved = list_preserved_configurations
print_preserved(preserved)
config_path = preserved[config_number][0]
puts "Using ##{config_number}: #{config_path}"
beaker_test(beaker_run_type,
:hosts_file => "#{config_path}/preserved_config.yaml",
:no_provision => true,
- :preserve_hosts => true,
- :pre_suite => ['setup/rsync/pre-suite']
+ :preserve_hosts => 'always',
+ :__delete_options__ => [:pre_suite]
)
end
end
task :default do
sh('rake -T')
end
diff --git a/acceptance/bin/ci-bootstrap-from-artifacts.sh b/acceptance/bin/ci-bootstrap-from-artifacts.sh
index 00f844991..36fdfbd49 100755
--- a/acceptance/bin/ci-bootstrap-from-artifacts.sh
+++ b/acceptance/bin/ci-bootstrap-from-artifacts.sh
@@ -1,44 +1,49 @@
#! /usr/bin/env bash
###############################################################################
# Initial preparation for a ci acceptance job in Jenkins. Crucially, it
# handles the untarring of the build artifact and bundle install, getting us to
# a state where we can then bundle exec rake the particular ci:test we want to
# run.
#
# Having this checked in in a script makes it much easier to have multiple
# acceptance jobs. It must be kept agnostic between Linux/Solaris/Windows
# builds, however.
set -x
# Use our internal rubygems mirror for the bundle install
if [ -z $GEM_SOURCE ]; then
export GEM_SOURCE='http://rubygems.delivery.puppetlabs.net'
fi
echo "SHA: ${SHA}"
+echo "FORK: ${FORK}"
echo "BUILD_SELECTOR: ${BUILD_SELECTOR}"
echo "PACKAGE_BUILD_STATUS: ${PACKAGE_BUILD_STATUS}"
rm -rf acceptance
mkdir acceptance
cd acceptance
-tar -xzvf ../acceptance-artifacts.tar.gz
+tar -xzf ../acceptance-artifacts.tar.gz
echo "===== This artifact is from ====="
cat creator.txt
-cd config/el6
bundle install --without=development --path=.bundle/gems
+if [[ "${platform}" =~ 'solaris' ]]; then
+ repo_proxy=" :repo_proxy => false,"
+fi
+
cat > local_options.rb <<-EOF
{
:hosts_file => 'config/nodes/${platform}.yaml',
:ssh => {
:keys => ["${HOME}/.ssh/id_rsa-old.private"],
},
+${repo_proxy}
}
EOF
[[ (-z "${PACKAGE_BUILD_STATUS}") || ("${PACKAGE_BUILD_STATUS}" = "success") ]] || exit 1
diff --git a/acceptance/bin/ci-package.sh b/acceptance/bin/ci-package.sh
new file mode 100755
index 000000000..44a2c24a5
--- /dev/null
+++ b/acceptance/bin/ci-package.sh
@@ -0,0 +1,16 @@
+#! /usr/bin/env bash
+
+set -e
+set -x
+
+JOB_NAME=$1
+[[ (-z "$JOB_NAME") ]] && echo "No job name passed in" && exit 1
+
+rake --trace package:implode
+rake --trace package:bootstrap
+
+# This obtains either the sha or tag if the commit is tagged
+REF=`rake pl:print_build_params |grep "^ref: " |cut -d ":" -f 2 | tr -d ' '`
+rake --trace pl:jenkins:uber_build DOWNSTREAM_JOB="http://jenkins-foss.delivery.puppetlabs.net/job/$JOB_NAME/buildWithParameters?token=iheartjenkins&SHA=$REF&BUILD_SELECTOR=$BUILD_NUMBER&FORK=$GIT_FORK"
+
+rake ci:acceptance_artifacts SHA=$REF
diff --git a/acceptance/config/el6/README.md b/acceptance/config/el6/README.md
deleted file mode 100644
index 92790a9aa..000000000
--- a/acceptance/config/el6/README.md
+++ /dev/null
@@ -1,76 +0,0 @@
-Local Use
-=========
-
-CI
----
-
-There are two ways to run the ci tests locally: against packages, or against git clones.
-
-`rake -T` will give short descriptions, and a `rake -D` will give full descriptions with information on ENV options required and optional for the various tasks.
-
-### Authentication
-
-Normally the ci tasks are called from a prepared Jenkins job.
-
-If you are running this on your laptop, you will need this ssh private key in order for beaker to be able to log into the vms created from the hosts file:
-
-https://github.com/puppetlabs/puppetlabs-modules/blob/qa/secure/jenkins/id_rsa-acceptance
-https://github.com/puppetlabs/puppetlabs-modules/blob/qa/secure/jenkins/id_rsa-acceptance.pub
-
-TODO fetch these files directly from github, but am running into rate limits and then would also have to cross the issue of authentication.
-
-You will also need QA credentials to vsphere in a ~/.fog file. These credentials can be found on any of the Jenkins coordinator hosts.
-
-### Packages
-
-In order to run the tests on hosts provisioned from packages produced by Delivery, you will need to reference a Puppet commit sha that has been packaged using Delivery's pl:jenkins:uber_build task. This is the snippet used by 'Puppet Packaging' Jenkins jobs:
-
- rake --trace package:implode
- rake --trace package:bootstrap
- rake --trace pl:jenkins:uber_build
-
-The above Rake tasks were run from the root of a Puppet checkout. They are quoted just for reference. Typically if you are investigating a failure, you will have a SHA from a failed jenkins run which should correspond to a successful pipeline run, and you should not need to run the pipeline manually.
-
-A finished pipeline will have repository information available at http://builds.puppetlabs.lan/puppet/ So you can also browse this list and select a recent sha which has repo_configs/ available.
-
-When executing the ci:test:packages task, you must set the SHA, and also set CONFIG to point to a valid Beaker hosts_file. Configurations used in the Jenkins jobs are available under config/nodes
-
- bundle exec rake ci:test:packages SHA=abcdef CONFIG=config/nodes/rhel.yaml
-
-Optionally you may set the TEST (TEST=a/test.rb,and/another/test.rb), and may pass additional OPTIONS to beaker (OPTIONS='--opt foo').
-
-You may also edit a ./local_options.rb hash which will override config/ options, and in turn be oferriden by commandline options set in the environment variables CONFIG, TEST and OPTIONS. This file is a ruby file containing a Ruby hash with configuration expected by Beaker. See Beaker source, and examples in config/.
-
-### Git
-
-Alternatively you may provision via git clone by calling the ci:test:git task. Currently we don't have packages for Windows or Solaris from the Delivery pipeline, and must use ci:test:git to privision and test these platforms.
-
-### Preserving Hosts
-
-If you have local changes to puppet code (outside of acceptance/) that you don't want to repackage for time reasons, or you just want to ssh into the hosts after a test run, you can use the following sequence:
-
- bundle exec rake ci:test_and_preserve_hosts CONFIG=some/config.yaml SHA=12345 TEST=a/foo_test.rb
-
-to get the initial templates provisioned, and a local log/latest/preserve_config.yaml created for them.
-
-Then you can log into the hosts, or rerun tests against them by:
-
- bundle exec rake ci:test_against_preserved_hosts TEST=a/foo_test.rb
-
-This will use the existing hosts, uninstall and reinstall the puppet packages and rsync in any changes from your local source lib dir. To skip reinstalling the packages set SKIP_PACKAGE_REINSTALL=1. To skip rsyncing, set SKIP_RSYNC=1. To use rsync filters, create a file with your rsync filter settings and set RSYNC_FILTER_FILE to the name of that file. For example:
-
- include puppet
- include puppet/defaults.rb
- exclude *
-
-will ensure that only puppet/defaults.rb is copied.
-
-NOTE: By default these tasks provision with packages. Set TYPE=git to use source checkouts.
-
-### Cleaning Up Preserved Hosts
-
-If you run a number of jobs with --preserve_hosts or vi ci:test_and_preserve_hosts, you may eventually generate a large number of stale vms. They should be reaped automatically by qa infrastructure within a day or so, but you may also run:
-
- bundle exec ci:destroy_preserved_hosts
-
-to clean them up sooner and free resources.
diff --git a/acceptance/config/el6/config/git/options.rb b/acceptance/config/el6/config/git/options.rb
deleted file mode 100644
index 2268fa0c9..000000000
--- a/acceptance/config/el6/config/git/options.rb
+++ /dev/null
@@ -1,7 +0,0 @@
-{
- :install => [
- 'git://github.com/puppetlabs/facter.git#stable',
- 'git://github.com/puppetlabs/hiera.git#stable',
- ],
- :pre_suite => ['setup/git/pre-suite'],
-}
diff --git a/acceptance/config/el6/config/packages/options.rb b/acceptance/config/el6/config/packages/options.rb
deleted file mode 100644
index bc2b8189f..000000000
--- a/acceptance/config/el6/config/packages/options.rb
+++ /dev/null
@@ -1,3 +0,0 @@
-{
- :pre_suite => ['setup/packages/pre-suite'],
-}
diff --git a/acceptance/config/el6/setup/packages/pre-suite/02_StopFirewall.rb b/acceptance/config/el6/setup/packages/pre-suite/02_StopFirewall.rb
deleted file mode 100644
index 0d651ba01..000000000
--- a/acceptance/config/el6/setup/packages/pre-suite/02_StopFirewall.rb
+++ /dev/null
@@ -1,9 +0,0 @@
-require 'puppet/acceptance/install_utils'
-
-extend Puppet::Acceptance::InstallUtils
-
-test_name "Stop firewall" do
- hosts.each do |host|
- stop_firewall_on(host)
- end
-end
diff --git a/acceptance/config/el6/setup/packages/pre-suite/04_ValidateSignCert.rb b/acceptance/config/el6/setup/packages/pre-suite/04_ValidateSignCert.rb
deleted file mode 100755
index 549432427..000000000
--- a/acceptance/config/el6/setup/packages/pre-suite/04_ValidateSignCert.rb
+++ /dev/null
@@ -1,6 +0,0 @@
-test_name "Validate Sign Cert"
-
-require 'puppet/acceptance/common_utils'
-extend Puppet::Acceptance::CAUtils
-
-initialize_ssl
diff --git a/acceptance/config/el6/setup/rsync/pre-suite/00_PurgeAndReinstall.rb b/acceptance/config/el6/setup/rsync/pre-suite/00_PurgeAndReinstall.rb
deleted file mode 100644
index aa2bdf412..000000000
--- a/acceptance/config/el6/setup/rsync/pre-suite/00_PurgeAndReinstall.rb
+++ /dev/null
@@ -1,10 +0,0 @@
-test_name "Purge and Reinstall Packages" do
- if !ENV['SKIP_PACKAGE_REINSTALL']
- hosts.each do |host|
- host.uninstall_package('puppet')
- host.uninstall_package('puppet-common')
- additional_switches = '--allow-unauthenticated' if host['platform'] =~ /debian|ubuntu/
- host.install_package('puppet', additional_switches)
- end
- end
-end
diff --git a/acceptance/config/el6/setup/rsync/pre-suite/01_RsyncSource.rb b/acceptance/config/el6/setup/rsync/pre-suite/01_RsyncSource.rb
deleted file mode 100644
index c903fd745..000000000
--- a/acceptance/config/el6/setup/rsync/pre-suite/01_RsyncSource.rb
+++ /dev/null
@@ -1,25 +0,0 @@
-test_name "Rsync Source" do
- if !ENV['SKIP_RSYNC']
- hosts.each do |host|
- step "rsyncing local puppet source to #{host}" do
- host.install_package('rsync') if !host.check_for_package('rsync')
- filter_opt = "--filter='merge #{ENV['RSYNC_FILTER_FILE']}'" if ENV['RSYNC_FILTER_FILE']
- destination_dir = case host['platform']
- when /debian|ubuntu/
- then '/usr/lib/ruby/vendor_ruby'
- when /el|centos/
- then '/usr/lib/ruby/site_ruby/1.8'
- when /fedora/
- then '/usr/share/ruby/vendor_ruby'
- else
- raise "We should actually do some #{host['platform']} platform specific rsyncing here..."
- end
- cmd = "rsync -r --exclude '.*.swp' #{filter_opt} --size-only -i -e'ssh -i id_rsa-acceptance' ../../../lib/* root@#{host}:#{destination_dir}"
- puts "RSYNC: #{cmd}"
- result = `#{cmd}`
- raise("Failed rsync execution:\n#{result}") if $? != 0
- puts result
- end
- end
- end
-end
diff --git a/acceptance/config/git/options.rb b/acceptance/config/git/options.rb
new file mode 100644
index 000000000..c20149d3f
--- /dev/null
+++ b/acceptance/config/git/options.rb
@@ -0,0 +1,18 @@
+{
+ :install => [
+ 'git://github.com/puppetlabs/facter.git#stable',
+ 'git://github.com/puppetlabs/hiera.git#stable',
+ ],
+ :pre_suite => [
+ 'setup/git/pre-suite/000_EnvSetup.rb',
+ 'setup/git/pre-suite/010_TestSetup.rb',
+ 'setup/git/pre-suite/020_PuppetUserAndGroup.rb',
+ 'setup/common/pre-suite/025_StopFirewall.rb',
+ 'setup/git/pre-suite/030_PuppetMasterSanity.rb',
+ 'setup/common/pre-suite/040_ValidateSignCert.rb',
+ 'setup/git/pre-suite/050_HieraSetup.rb',
+ 'setup/git/pre-suite/060_InstallModules.rb',
+ 'setup/git/pre-suite/070_InstalCACerts.rb',
+ 'setup/common/pre-suite/100_SetParser.rb',
+ ],
+}
diff --git a/acceptance/config/el6/config/nodes/all.yaml b/acceptance/config/nodes/all.yaml
similarity index 100%
rename from acceptance/config/el6/config/nodes/all.yaml
rename to acceptance/config/nodes/all.yaml
diff --git a/acceptance/config/el6/config/nodes/all_but_windows.yaml b/acceptance/config/nodes/all_but_windows.yaml
similarity index 100%
rename from acceptance/config/el6/config/nodes/all_but_windows.yaml
rename to acceptance/config/nodes/all_but_windows.yaml
diff --git a/acceptance/config/el6/config/nodes/all_linux.yaml b/acceptance/config/nodes/all_linux.yaml
similarity index 100%
rename from acceptance/config/el6/config/nodes/all_linux.yaml
rename to acceptance/config/nodes/all_linux.yaml
diff --git a/acceptance/config/el6/config/nodes/centos5.yaml b/acceptance/config/nodes/centos5.yaml
similarity index 100%
rename from acceptance/config/el6/config/nodes/centos5.yaml
rename to acceptance/config/nodes/centos5.yaml
diff --git a/acceptance/config/el6/config/nodes/centos6.yaml b/acceptance/config/nodes/centos6.yaml
similarity index 100%
rename from acceptance/config/el6/config/nodes/centos6.yaml
rename to acceptance/config/nodes/centos6.yaml
diff --git a/acceptance/config/el6/config/nodes/fedora18.yaml b/acceptance/config/nodes/fedora18.yaml
similarity index 100%
copy from acceptance/config/el6/config/nodes/fedora18.yaml
copy to acceptance/config/nodes/fedora18.yaml
diff --git a/acceptance/config/el6/config/nodes/fedora19.yaml b/acceptance/config/nodes/fedora19.yaml
similarity index 100%
rename from acceptance/config/el6/config/nodes/fedora19.yaml
rename to acceptance/config/nodes/fedora19.yaml
diff --git a/acceptance/config/el6/config/nodes/fedora18.yaml b/acceptance/config/nodes/fedora20.yaml
similarity index 85%
rename from acceptance/config/el6/config/nodes/fedora18.yaml
rename to acceptance/config/nodes/fedora20.yaml
index 899fc04b5..9a7bb3a80 100644
--- a/acceptance/config/el6/config/nodes/fedora18.yaml
+++ b/acceptance/config/nodes/fedora20.yaml
@@ -1,20 +1,20 @@
HOSTS:
master:
roles:
- master
- agent
- platform: fedora-18-x86_64
+ platform: fedora-20-x86_64
hypervisor: vcloud
- template: Delivery/Quality Assurance/Templates/vCloud/fedora-18-x86_64
+ template: Delivery/Quality Assurance/Templates/vCloud/fedora-20-x86_64
agent:
roles:
- agent
- platform: fedora-18-i386
+ platform: fedora-20-i386
hypervisor: vcloud
- template: Delivery/Quality Assurance/Templates/vCloud/fedora-18-i386
+ template: Delivery/Quality Assurance/Templates/vCloud/fedora-20-i386
CONFIG:
filecount: 12
datastore: instance0
resourcepool: delivery/Quality Assurance/FOSS/Dynamic
folder: Delivery/Quality Assurance/FOSS/Dynamic
pooling_api: http://vcloud.delivery.puppetlabs.net/
diff --git a/acceptance/config/el6/config/nodes/foss-win-2003r2-ruby193.yaml b/acceptance/config/nodes/foss-win-2003r2-ruby193.yaml
similarity index 100%
rename from acceptance/config/el6/config/nodes/foss-win-2003r2-ruby193.yaml
rename to acceptance/config/nodes/foss-win-2003r2-ruby193.yaml
diff --git a/acceptance/config/el6/config/nodes/lucid.yaml b/acceptance/config/nodes/lucid.yaml
similarity index 100%
rename from acceptance/config/el6/config/nodes/lucid.yaml
rename to acceptance/config/nodes/lucid.yaml
diff --git a/acceptance/config/el6/config/nodes/pe/centos-5-32ma-32da-32da b/acceptance/config/nodes/pe/centos-5-32ma-32da-32da
similarity index 100%
rename from acceptance/config/el6/config/nodes/pe/centos-5-32ma-32da-32da
rename to acceptance/config/nodes/pe/centos-5-32ma-32da-32da
diff --git a/acceptance/config/el6/config/nodes/pe/centos-5-32mda b/acceptance/config/nodes/pe/centos-5-32mda
similarity index 100%
rename from acceptance/config/el6/config/nodes/pe/centos-5-32mda
rename to acceptance/config/nodes/pe/centos-5-32mda
diff --git a/acceptance/config/el6/config/nodes/pe/centos-5-64ma-64da-64da b/acceptance/config/nodes/pe/centos-5-64ma-64da-64da
similarity index 100%
rename from acceptance/config/el6/config/nodes/pe/centos-5-64ma-64da-64da
rename to acceptance/config/nodes/pe/centos-5-64ma-64da-64da
diff --git a/acceptance/config/el6/config/nodes/pe/centos-5-64mda b/acceptance/config/nodes/pe/centos-5-64mda
similarity index 100%
rename from acceptance/config/el6/config/nodes/pe/centos-5-64mda
rename to acceptance/config/nodes/pe/centos-5-64mda
diff --git a/acceptance/config/el6/config/nodes/pe/centos-6-32ma-32da-32da b/acceptance/config/nodes/pe/centos-6-32ma-32da-32da
similarity index 100%
rename from acceptance/config/el6/config/nodes/pe/centos-6-32ma-32da-32da
rename to acceptance/config/nodes/pe/centos-6-32ma-32da-32da
diff --git a/acceptance/config/el6/config/nodes/pe/centos-6-32mda b/acceptance/config/nodes/pe/centos-6-32mda
similarity index 100%
rename from acceptance/config/el6/config/nodes/pe/centos-6-32mda
rename to acceptance/config/nodes/pe/centos-6-32mda
diff --git a/acceptance/config/el6/config/nodes/pe/centos-6-64ma-64da-64da b/acceptance/config/nodes/pe/centos-6-64ma-64da-64da
similarity index 100%
rename from acceptance/config/el6/config/nodes/pe/centos-6-64ma-64da-64da
rename to acceptance/config/nodes/pe/centos-6-64ma-64da-64da
diff --git a/acceptance/config/el6/config/nodes/pe/centos-6-64mda b/acceptance/config/nodes/pe/centos-6-64mda
similarity index 100%
rename from acceptance/config/el6/config/nodes/pe/centos-6-64mda
rename to acceptance/config/nodes/pe/centos-6-64mda
diff --git a/acceptance/config/el6/config/nodes/pe/centos-6-64mda-sol-10-64a b/acceptance/config/nodes/pe/centos-6-64mda-sol-10-64a
similarity index 100%
rename from acceptance/config/el6/config/nodes/pe/centos-6-64mda-sol-10-64a
rename to acceptance/config/nodes/pe/centos-6-64mda-sol-10-64a
diff --git a/acceptance/config/el6/config/nodes/pe/debian-6-32ma-32da-32da b/acceptance/config/nodes/pe/debian-6-32ma-32da-32da
similarity index 100%
rename from acceptance/config/el6/config/nodes/pe/debian-6-32ma-32da-32da
rename to acceptance/config/nodes/pe/debian-6-32ma-32da-32da
diff --git a/acceptance/config/el6/config/nodes/pe/debian-6-32mda b/acceptance/config/nodes/pe/debian-6-32mda
similarity index 100%
rename from acceptance/config/el6/config/nodes/pe/debian-6-32mda
rename to acceptance/config/nodes/pe/debian-6-32mda
diff --git a/acceptance/config/el6/config/nodes/pe/debian-6-64ma-64da-64da b/acceptance/config/nodes/pe/debian-6-64ma-64da-64da
similarity index 100%
rename from acceptance/config/el6/config/nodes/pe/debian-6-64ma-64da-64da
rename to acceptance/config/nodes/pe/debian-6-64ma-64da-64da
diff --git a/acceptance/config/el6/config/nodes/pe/debian-6-64mda b/acceptance/config/nodes/pe/debian-6-64mda
similarity index 100%
rename from acceptance/config/el6/config/nodes/pe/debian-6-64mda
rename to acceptance/config/nodes/pe/debian-6-64mda
diff --git a/acceptance/config/el6/config/nodes/pe/debian-7-32ma-32da-32da b/acceptance/config/nodes/pe/debian-7-32ma-32da-32da
similarity index 100%
rename from acceptance/config/el6/config/nodes/pe/debian-7-32ma-32da-32da
rename to acceptance/config/nodes/pe/debian-7-32ma-32da-32da
diff --git a/acceptance/config/el6/config/nodes/pe/debian-7-32mda b/acceptance/config/nodes/pe/debian-7-32mda
similarity index 100%
rename from acceptance/config/el6/config/nodes/pe/debian-7-32mda
rename to acceptance/config/nodes/pe/debian-7-32mda
diff --git a/acceptance/config/el6/config/nodes/pe/debian-7-64ma-64da-64da b/acceptance/config/nodes/pe/debian-7-64ma-64da-64da
similarity index 100%
rename from acceptance/config/el6/config/nodes/pe/debian-7-64ma-64da-64da
rename to acceptance/config/nodes/pe/debian-7-64ma-64da-64da
diff --git a/acceptance/config/el6/config/nodes/pe/debian-7-64mda b/acceptance/config/nodes/pe/debian-7-64mda
similarity index 100%
rename from acceptance/config/el6/config/nodes/pe/debian-7-64mda
rename to acceptance/config/nodes/pe/debian-7-64mda
diff --git a/acceptance/config/el6/config/nodes/pe/debian-7-i386 b/acceptance/config/nodes/pe/debian-7-i386
similarity index 100%
rename from acceptance/config/el6/config/nodes/pe/debian-7-i386
rename to acceptance/config/nodes/pe/debian-7-i386
diff --git a/acceptance/config/el6/config/nodes/pe/oracle-5-32ma-32da-32da b/acceptance/config/nodes/pe/oracle-5-32ma-32da-32da
similarity index 100%
rename from acceptance/config/el6/config/nodes/pe/oracle-5-32ma-32da-32da
rename to acceptance/config/nodes/pe/oracle-5-32ma-32da-32da
diff --git a/acceptance/config/el6/config/nodes/pe/oracle-5-32mda b/acceptance/config/nodes/pe/oracle-5-32mda
similarity index 100%
rename from acceptance/config/el6/config/nodes/pe/oracle-5-32mda
rename to acceptance/config/nodes/pe/oracle-5-32mda
diff --git a/acceptance/config/el6/config/nodes/pe/oracle-5-64ma-64da-64da b/acceptance/config/nodes/pe/oracle-5-64ma-64da-64da
similarity index 100%
rename from acceptance/config/el6/config/nodes/pe/oracle-5-64ma-64da-64da
rename to acceptance/config/nodes/pe/oracle-5-64ma-64da-64da
diff --git a/acceptance/config/el6/config/nodes/pe/oracle-5-64mda b/acceptance/config/nodes/pe/oracle-5-64mda
similarity index 100%
rename from acceptance/config/el6/config/nodes/pe/oracle-5-64mda
rename to acceptance/config/nodes/pe/oracle-5-64mda
diff --git a/acceptance/config/el6/config/nodes/pe/oracle-6-32ma-32da-32da b/acceptance/config/nodes/pe/oracle-6-32ma-32da-32da
similarity index 100%
rename from acceptance/config/el6/config/nodes/pe/oracle-6-32ma-32da-32da
rename to acceptance/config/nodes/pe/oracle-6-32ma-32da-32da
diff --git a/acceptance/config/el6/config/nodes/pe/oracle-6-32mda b/acceptance/config/nodes/pe/oracle-6-32mda
similarity index 100%
rename from acceptance/config/el6/config/nodes/pe/oracle-6-32mda
rename to acceptance/config/nodes/pe/oracle-6-32mda
diff --git a/acceptance/config/el6/config/nodes/pe/oracle-6-64ma-64da-64da b/acceptance/config/nodes/pe/oracle-6-64ma-64da-64da
similarity index 100%
rename from acceptance/config/el6/config/nodes/pe/oracle-6-64ma-64da-64da
rename to acceptance/config/nodes/pe/oracle-6-64ma-64da-64da
diff --git a/acceptance/config/el6/config/nodes/pe/oracle-6-64mda b/acceptance/config/nodes/pe/oracle-6-64mda
similarity index 100%
rename from acceptance/config/el6/config/nodes/pe/oracle-6-64mda
rename to acceptance/config/nodes/pe/oracle-6-64mda
diff --git a/acceptance/config/el6/config/nodes/pe/redhat-5-32ma-32da-32da b/acceptance/config/nodes/pe/redhat-5-32ma-32da-32da
similarity index 100%
rename from acceptance/config/el6/config/nodes/pe/redhat-5-32ma-32da-32da
rename to acceptance/config/nodes/pe/redhat-5-32ma-32da-32da
diff --git a/acceptance/config/el6/config/nodes/pe/redhat-5-32mda b/acceptance/config/nodes/pe/redhat-5-32mda
similarity index 100%
rename from acceptance/config/el6/config/nodes/pe/redhat-5-32mda
rename to acceptance/config/nodes/pe/redhat-5-32mda
diff --git a/acceptance/config/el6/config/nodes/pe/redhat-5-64ma-64da-64da b/acceptance/config/nodes/pe/redhat-5-64ma-64da-64da
similarity index 100%
rename from acceptance/config/el6/config/nodes/pe/redhat-5-64ma-64da-64da
rename to acceptance/config/nodes/pe/redhat-5-64ma-64da-64da
diff --git a/acceptance/config/el6/config/nodes/pe/redhat-5-64mda b/acceptance/config/nodes/pe/redhat-5-64mda
similarity index 100%
rename from acceptance/config/el6/config/nodes/pe/redhat-5-64mda
rename to acceptance/config/nodes/pe/redhat-5-64mda
diff --git a/acceptance/config/el6/config/nodes/pe/redhat-6-32ma-32da-32da b/acceptance/config/nodes/pe/redhat-6-32ma-32da-32da
similarity index 100%
rename from acceptance/config/el6/config/nodes/pe/redhat-6-32ma-32da-32da
rename to acceptance/config/nodes/pe/redhat-6-32ma-32da-32da
diff --git a/acceptance/config/el6/config/nodes/pe/redhat-6-32mda b/acceptance/config/nodes/pe/redhat-6-32mda
similarity index 100%
rename from acceptance/config/el6/config/nodes/pe/redhat-6-32mda
rename to acceptance/config/nodes/pe/redhat-6-32mda
diff --git a/acceptance/config/el6/config/nodes/pe/redhat-6-64ma-64da-64da b/acceptance/config/nodes/pe/redhat-6-64ma-64da-64da
similarity index 100%
rename from acceptance/config/el6/config/nodes/pe/redhat-6-64ma-64da-64da
rename to acceptance/config/nodes/pe/redhat-6-64ma-64da-64da
diff --git a/acceptance/config/el6/config/nodes/pe/redhat-6-64mda b/acceptance/config/nodes/pe/redhat-6-64mda
similarity index 100%
rename from acceptance/config/el6/config/nodes/pe/redhat-6-64mda
rename to acceptance/config/nodes/pe/redhat-6-64mda
diff --git a/acceptance/config/el6/config/nodes/pe/scientific-5-32ma-32da-32da b/acceptance/config/nodes/pe/scientific-5-32ma-32da-32da
similarity index 100%
rename from acceptance/config/el6/config/nodes/pe/scientific-5-32ma-32da-32da
rename to acceptance/config/nodes/pe/scientific-5-32ma-32da-32da
diff --git a/acceptance/config/el6/config/nodes/pe/scientific-5-32mda b/acceptance/config/nodes/pe/scientific-5-32mda
similarity index 100%
rename from acceptance/config/el6/config/nodes/pe/scientific-5-32mda
rename to acceptance/config/nodes/pe/scientific-5-32mda
diff --git a/acceptance/config/el6/config/nodes/pe/scientific-5-64ma-64da-64da b/acceptance/config/nodes/pe/scientific-5-64ma-64da-64da
similarity index 100%
rename from acceptance/config/el6/config/nodes/pe/scientific-5-64ma-64da-64da
rename to acceptance/config/nodes/pe/scientific-5-64ma-64da-64da
diff --git a/acceptance/config/el6/config/nodes/pe/scientific-5-64mda b/acceptance/config/nodes/pe/scientific-5-64mda
similarity index 100%
rename from acceptance/config/el6/config/nodes/pe/scientific-5-64mda
rename to acceptance/config/nodes/pe/scientific-5-64mda
diff --git a/acceptance/config/el6/config/nodes/pe/scientific-6-32ma-32da-32da b/acceptance/config/nodes/pe/scientific-6-32ma-32da-32da
similarity index 100%
rename from acceptance/config/el6/config/nodes/pe/scientific-6-32ma-32da-32da
rename to acceptance/config/nodes/pe/scientific-6-32ma-32da-32da
diff --git a/acceptance/config/el6/config/nodes/pe/scientific-6-32mda b/acceptance/config/nodes/pe/scientific-6-32mda
similarity index 100%
rename from acceptance/config/el6/config/nodes/pe/scientific-6-32mda
rename to acceptance/config/nodes/pe/scientific-6-32mda
diff --git a/acceptance/config/el6/config/nodes/pe/scientific-6-64ma-64da-64da b/acceptance/config/nodes/pe/scientific-6-64ma-64da-64da
similarity index 100%
rename from acceptance/config/el6/config/nodes/pe/scientific-6-64ma-64da-64da
rename to acceptance/config/nodes/pe/scientific-6-64ma-64da-64da
diff --git a/acceptance/config/el6/config/nodes/pe/scientific-6-64mda b/acceptance/config/nodes/pe/scientific-6-64mda
similarity index 100%
rename from acceptance/config/el6/config/nodes/pe/scientific-6-64mda
rename to acceptance/config/nodes/pe/scientific-6-64mda
diff --git a/acceptance/config/el6/config/nodes/pe/sles-11-32ma-32da-32da b/acceptance/config/nodes/pe/sles-11-32ma-32da-32da
similarity index 100%
rename from acceptance/config/el6/config/nodes/pe/sles-11-32ma-32da-32da
rename to acceptance/config/nodes/pe/sles-11-32ma-32da-32da
diff --git a/acceptance/config/el6/config/nodes/pe/sles-11-32mda b/acceptance/config/nodes/pe/sles-11-32mda
similarity index 100%
rename from acceptance/config/el6/config/nodes/pe/sles-11-32mda
rename to acceptance/config/nodes/pe/sles-11-32mda
diff --git a/acceptance/config/el6/config/nodes/pe/sles-11-64ma-64da-64da b/acceptance/config/nodes/pe/sles-11-64ma-64da-64da
similarity index 100%
rename from acceptance/config/el6/config/nodes/pe/sles-11-64ma-64da-64da
rename to acceptance/config/nodes/pe/sles-11-64ma-64da-64da
diff --git a/acceptance/config/el6/config/nodes/pe/sles-11-64mda b/acceptance/config/nodes/pe/sles-11-64mda
similarity index 100%
rename from acceptance/config/el6/config/nodes/pe/sles-11-64mda
rename to acceptance/config/nodes/pe/sles-11-64mda
diff --git a/acceptance/config/el6/config/nodes/pe/ubuntu-1004-32ma-32da-32da b/acceptance/config/nodes/pe/ubuntu-1004-32ma-32da-32da
similarity index 100%
rename from acceptance/config/el6/config/nodes/pe/ubuntu-1004-32ma-32da-32da
rename to acceptance/config/nodes/pe/ubuntu-1004-32ma-32da-32da
diff --git a/acceptance/config/el6/config/nodes/pe/ubuntu-1004-32mda b/acceptance/config/nodes/pe/ubuntu-1004-32mda
similarity index 100%
rename from acceptance/config/el6/config/nodes/pe/ubuntu-1004-32mda
rename to acceptance/config/nodes/pe/ubuntu-1004-32mda
diff --git a/acceptance/config/el6/config/nodes/pe/ubuntu-1004-64ma-64da-64da b/acceptance/config/nodes/pe/ubuntu-1004-64ma-64da-64da
similarity index 100%
rename from acceptance/config/el6/config/nodes/pe/ubuntu-1004-64ma-64da-64da
rename to acceptance/config/nodes/pe/ubuntu-1004-64ma-64da-64da
diff --git a/acceptance/config/el6/config/nodes/pe/ubuntu-1004-64mda b/acceptance/config/nodes/pe/ubuntu-1004-64mda
similarity index 100%
rename from acceptance/config/el6/config/nodes/pe/ubuntu-1004-64mda
rename to acceptance/config/nodes/pe/ubuntu-1004-64mda
diff --git a/acceptance/config/el6/config/nodes/pe/ubuntu-1204-32ma-32da-32da b/acceptance/config/nodes/pe/ubuntu-1204-32ma-32da-32da
similarity index 100%
rename from acceptance/config/el6/config/nodes/pe/ubuntu-1204-32ma-32da-32da
rename to acceptance/config/nodes/pe/ubuntu-1204-32ma-32da-32da
diff --git a/acceptance/config/el6/config/nodes/pe/ubuntu-1204-32mda b/acceptance/config/nodes/pe/ubuntu-1204-32mda
similarity index 100%
rename from acceptance/config/el6/config/nodes/pe/ubuntu-1204-32mda
rename to acceptance/config/nodes/pe/ubuntu-1204-32mda
diff --git a/acceptance/config/el6/config/nodes/pe/ubuntu-1204-64ma-64da-64da b/acceptance/config/nodes/pe/ubuntu-1204-64ma-64da-64da
similarity index 100%
rename from acceptance/config/el6/config/nodes/pe/ubuntu-1204-64ma-64da-64da
rename to acceptance/config/nodes/pe/ubuntu-1204-64ma-64da-64da
diff --git a/acceptance/config/el6/config/nodes/pe/ubuntu-1204-64mda b/acceptance/config/nodes/pe/ubuntu-1204-64mda
similarity index 100%
rename from acceptance/config/el6/config/nodes/pe/ubuntu-1204-64mda
rename to acceptance/config/nodes/pe/ubuntu-1204-64mda
diff --git a/acceptance/config/el6/config/nodes/precise.yaml b/acceptance/config/nodes/precise.yaml
similarity index 100%
rename from acceptance/config/el6/config/nodes/precise.yaml
rename to acceptance/config/nodes/precise.yaml
diff --git a/acceptance/config/el6/config/nodes/precise_and_lucid.yaml b/acceptance/config/nodes/precise_and_lucid.yaml
similarity index 100%
rename from acceptance/config/el6/config/nodes/precise_and_lucid.yaml
rename to acceptance/config/nodes/precise_and_lucid.yaml
diff --git a/acceptance/config/el6/config/nodes/rhel5.yaml b/acceptance/config/nodes/rhel5.yaml
similarity index 100%
rename from acceptance/config/el6/config/nodes/rhel5.yaml
rename to acceptance/config/nodes/rhel5.yaml
diff --git a/acceptance/config/el6/config/nodes/rhel6.yaml b/acceptance/config/nodes/rhel6.yaml
similarity index 100%
copy from acceptance/config/el6/config/nodes/rhel6.yaml
copy to acceptance/config/nodes/rhel6.yaml
diff --git a/acceptance/config/el6/config/nodes/rhel6_and_rhel5.yaml b/acceptance/config/nodes/rhel6_and_rhel5.yaml
similarity index 100%
rename from acceptance/config/el6/config/nodes/rhel6_and_rhel5.yaml
rename to acceptance/config/nodes/rhel6_and_rhel5.yaml
diff --git a/acceptance/config/el6/config/nodes/rhel6.yaml b/acceptance/config/nodes/rhel7.yaml
similarity index 86%
rename from acceptance/config/el6/config/nodes/rhel6.yaml
rename to acceptance/config/nodes/rhel7.yaml
index 6a1d1c1f7..b5706dd47 100644
--- a/acceptance/config/el6/config/nodes/rhel6.yaml
+++ b/acceptance/config/nodes/rhel7.yaml
@@ -1,20 +1,20 @@
HOSTS:
master:
roles:
- master
- agent
- platform: el-6-x86_64
+ platform: el-7-x86_64
hypervisor: vcloud
- template: Delivery/Quality Assurance/Templates/vCloud/redhat-6-x86_64
+ template: Delivery/Quality Assurance/Templates/vCloud/redhat-7-x86_64
agent:
roles:
- agent
- platform: el-6-i386
+ platform: el-7-x86_64
hypervisor: vcloud
- template: Delivery/Quality Assurance/Templates/vCloud/redhat-6-i386
+ template: Delivery/Quality Assurance/Templates/vCloud/redhat-7-x86_64
CONFIG:
filecount: 12
datastore: instance0
resourcepool: delivery/Quality Assurance/FOSS/Dynamic
folder: Delivery/Quality Assurance/FOSS/Dynamic
pooling_api: http://vcloud.delivery.puppetlabs.net/
diff --git a/acceptance/config/el6/config/nodes/solaris10.yaml b/acceptance/config/nodes/solaris10.yaml
similarity index 100%
rename from acceptance/config/el6/config/nodes/solaris10.yaml
rename to acceptance/config/nodes/solaris10.yaml
diff --git a/acceptance/config/el6/config/nodes/solaris11.yaml b/acceptance/config/nodes/solaris11.yaml
similarity index 100%
rename from acceptance/config/el6/config/nodes/solaris11.yaml
rename to acceptance/config/nodes/solaris11.yaml
diff --git a/acceptance/config/el6/config/nodes/squeeze.yaml b/acceptance/config/nodes/squeeze.yaml
similarity index 100%
rename from acceptance/config/el6/config/nodes/squeeze.yaml
rename to acceptance/config/nodes/squeeze.yaml
diff --git a/acceptance/config/el6/config/nodes/standalone/rhel6-and-precise.yaml b/acceptance/config/nodes/standalone/rhel6-and-precise.yaml
similarity index 100%
rename from acceptance/config/el6/config/nodes/standalone/rhel6-and-precise.yaml
rename to acceptance/config/nodes/standalone/rhel6-and-precise.yaml
diff --git a/acceptance/config/el6/config/nodes/wheezy.yaml b/acceptance/config/nodes/wheezy.yaml
similarity index 100%
rename from acceptance/config/el6/config/nodes/wheezy.yaml
rename to acceptance/config/nodes/wheezy.yaml
diff --git a/acceptance/config/el6/config/nodes/win2003-all.yaml b/acceptance/config/nodes/win2003-all.yaml
similarity index 100%
rename from acceptance/config/el6/config/nodes/win2003-all.yaml
rename to acceptance/config/nodes/win2003-all.yaml
diff --git a/acceptance/config/el6/config/nodes/win2003.yaml b/acceptance/config/nodes/win2003.yaml
similarity index 100%
rename from acceptance/config/el6/config/nodes/win2003.yaml
rename to acceptance/config/nodes/win2003.yaml
diff --git a/acceptance/config/el6/config/nodes/win2003r2.yaml b/acceptance/config/nodes/win2003r2.yaml
similarity index 100%
rename from acceptance/config/el6/config/nodes/win2003r2.yaml
rename to acceptance/config/nodes/win2003r2.yaml
diff --git a/acceptance/config/el6/config/nodes/win2008-all.yaml b/acceptance/config/nodes/win2008-all.yaml
similarity index 100%
rename from acceptance/config/el6/config/nodes/win2008-all.yaml
rename to acceptance/config/nodes/win2008-all.yaml
diff --git a/acceptance/config/el6/config/nodes/win2008r2.yaml b/acceptance/config/nodes/win2008r2.yaml
similarity index 100%
rename from acceptance/config/el6/config/nodes/win2008r2.yaml
rename to acceptance/config/nodes/win2008r2.yaml
diff --git a/acceptance/config/el6/config/nodes/win2012-all.yaml b/acceptance/config/nodes/win2012-all.yaml
similarity index 100%
rename from acceptance/config/el6/config/nodes/win2012-all.yaml
rename to acceptance/config/nodes/win2012-all.yaml
diff --git a/acceptance/config/el6/config/nodes/windows-all.yaml b/acceptance/config/nodes/windows-all.yaml
similarity index 100%
rename from acceptance/config/el6/config/nodes/windows-all.yaml
rename to acceptance/config/nodes/windows-all.yaml
diff --git a/acceptance/config/packages/options.rb b/acceptance/config/packages/options.rb
new file mode 100644
index 000000000..4adaab5de
--- /dev/null
+++ b/acceptance/config/packages/options.rb
@@ -0,0 +1,8 @@
+{
+ :pre_suite => [
+ 'setup/packages/pre-suite/010_Install.rb',
+ 'setup/common/pre-suite/025_StopFirewall.rb',
+ 'setup/common/pre-suite/040_ValidateSignCert.rb',
+ 'setup/common/pre-suite/100_SetParser.rb',
+ ],
+}
diff --git a/acceptance/lib/puppet/acceptance/install_utils.rb b/acceptance/lib/puppet/acceptance/install_utils.rb
index 904c74c6d..5df423574 100644
--- a/acceptance/lib/puppet/acceptance/install_utils.rb
+++ b/acceptance/lib/puppet/acceptance/install_utils.rb
@@ -1,163 +1,155 @@
require 'open-uri'
module Puppet
module Acceptance
module InstallUtils
PLATFORM_PATTERNS = {
:redhat => /fedora|el|centos/,
:debian => /debian|ubuntu/,
:solaris => /solaris/,
:windows => /windows/,
}.freeze
# Installs packages on the hosts.
#
# @param hosts [Array<Host>] Array of hosts to install packages to.
# @param package_hash [Hash{Symbol=>Array<String,Array<String,String>>}]
# Keys should be a symbol for a platform in PLATFORM_PATTERNS. Values
# should be an array of package names to install, or of two element
# arrays where a[0] is the command we expect to find on the platform
# and a[1] is the package name (when they are different).
# @param options [Hash{Symbol=>Boolean}]
# @option options [Boolean] :check_if_exists First check to see if
# command is present before installing package. (Default false)
# @return true
def install_packages_on(hosts, package_hash, options = {})
check_if_exists = options[:check_if_exists]
hosts = [hosts] unless hosts.kind_of?(Array)
hosts.each do |host|
package_hash.each do |platform_key,package_list|
if pattern = PLATFORM_PATTERNS[platform_key]
if pattern.match(host['platform'])
package_list.each do |cmd_pkg|
if cmd_pkg.kind_of?(Array)
command, package = cmd_pkg
else
command = package = cmd_pkg
end
if !check_if_exists || !host.check_for_package(command)
host.logger.notify("Installing #{package}")
additional_switches = '--allow-unauthenticated' if platform_key == :debian
host.install_package(package, additional_switches)
end
end
end
else
raise("Unknown platform '#{platform_key}' in package_hash")
end
end
end
return true
end
def fetch(base_url, file_name, dst_dir)
FileUtils.makedirs(dst_dir)
src = "#{base_url}/#{file_name}"
dst = File.join(dst_dir, file_name)
if File.exists?(dst)
logger.notify "Already fetched #{dst}"
else
logger.notify "Fetching: #{src}"
logger.notify " and saving to #{dst}"
open(src) do |remote|
File.open(dst, "w") do |file|
FileUtils.copy_stream(remote, file)
end
end
end
return dst
end
def stop_firewall_on(host)
case host['platform']
when /debian/
on host, 'iptables -F'
- when /fedora/
+ when /fedora|el-7/
on host, puppet('resource', 'service', 'firewalld', 'ensure=stopped')
when /el|centos/
on host, puppet('resource', 'service', 'iptables', 'ensure=stopped')
when /ubuntu/
on host, puppet('resource', 'service', 'ufw', 'ensure=stopped')
else
logger.notify("Not sure how to clear firewall on #{host['platform']}")
end
end
def install_repos_on(host, sha, repo_configs_dir)
platform = host['platform']
platform_configs_dir = File.join(repo_configs_dir,platform)
case platform
when /^(fedora|el|centos)-(\d+)-(.+)$/
variant = (($1 == 'centos') ? 'el' : $1)
fedora_prefix = ((variant == 'fedora') ? 'f' : '')
version = $2
arch = $3
- package_version = version == '19' ? '19-2' : "#{version}-7"
-
rpm = fetch(
- "http://yum.puppetlabs.com/%s/%s%s/products/i386/" % [
- variant,
- fedora_prefix,
- version,
- ],
- "puppetlabs-release-%s.noarch.rpm" % package_version,
+ "http://yum.puppetlabs.com",
+ "puppetlabs-release-%s-%s.noarch.rpm" % [variant, version],
platform_configs_dir
)
pattern = "pl-puppet-%s-%s-%s%s-%s.repo"
repo_filename = pattern % [
sha,
variant,
fedora_prefix,
version,
arch
]
- begin
- repo = fetch(
- "http://builds.puppetlabs.lan/puppet/%s/repo_configs/rpm/" % sha,
- repo_filename,
- platform_configs_dir
- )
- end
+ repo = fetch(
+ "http://builds.puppetlabs.lan/puppet/%s/repo_configs/rpm/" % sha,
+ repo_filename,
+ platform_configs_dir
+ )
on host, "rm -rf /root/*.repo; rm -rf /root/*.rpm"
scp_to host, rpm, '/root'
scp_to host, repo, '/root'
on host, "mv /root/*.repo /etc/yum.repos.d"
on host, "rpm -Uvh --force /root/*.rpm"
when /^(debian|ubuntu)-([^-]+)-(.+)$/
variant = $1
version = $2
arch = $3
deb = fetch(
"http://apt.puppetlabs.com/",
"puppetlabs-release-%s.deb" % version,
platform_configs_dir
)
list = fetch(
"http://builds.puppetlabs.lan/puppet/%s/repo_configs/deb/" % sha,
"pl-puppet-%s-%s.list" % [sha, version],
platform_configs_dir
)
on host, "rm -rf /root/*.list; rm -rf /root/*.deb"
scp_to host, deb, '/root'
scp_to host, list, '/root'
on host, "mv /root/*.list /etc/apt/sources.list.d"
on host, "dpkg -i --force-all /root/*.deb"
else
host.logger.notify("No repository installation step for #{platform} yet...")
end
end
end
end
end
diff --git a/acceptance/lib/puppet/acceptance/module_utils.rb b/acceptance/lib/puppet/acceptance/module_utils.rb
index 38d573198..ac67998a8 100644
--- a/acceptance/lib/puppet/acceptance/module_utils.rb
+++ b/acceptance/lib/puppet/acceptance/module_utils.rb
@@ -1,183 +1,228 @@
module Puppet
module Acceptance
module ModuleUtils
# Return an array of paths to installed modules for a given host.
#
# Example return value:
#
# [
# "/opt/puppet/share/puppet/modules/apt",
# "/opt/puppet/share/puppet/modules/auth_conf",
# "/opt/puppet/share/puppet/modules/concat",
# ]
#
# @param host [String] hostname
# @return [Array] paths for found modules
def get_installed_modules_for_host (host)
on host, puppet("module list --render-as pson")
str = stdout.lines.to_a.last
pat = /\(([^()]+)\)/
mods = str.scan(pat).flatten
return mods
end
# Return a hash of array of paths to installed modules for a hosts.
# The individual hostnames are the keys of the hash. The only value
# for a given key is an array of paths for the found modules.
#
# Example return value:
#
# {
# "my_master" =>
# [
# "/opt/puppet/share/puppet/modules/apt",
# "/opt/puppet/share/puppet/modules/auth_conf",
# "/opt/puppet/share/puppet/modules/concat",
# ],
# "my_agent01" =>
# [
# "/opt/puppet/share/puppet/modules/apt",
# "/opt/puppet/share/puppet/modules/auth_conf",
# "/opt/puppet/share/puppet/modules/concat",
# ],
# }
#
# @param hosts [Array] hostnames
# @return [Hash] paths for found modules indexed by hostname
def get_installed_modules_for_hosts (hosts)
mods = {}
hosts.each do |host|
mods[host] = get_installed_modules_for_host host
end
return mods
end
# Compare the module paths in given hashes and remove paths that
# are were not present in the first hash. The use case for this
# method is to remove any modules that were installed during the
# course of a test run.
#
# Installed module hashes would be gathered using the
# `get_+installed_module_for_hosts` command in the setup stage
# and teardown stages of a test. These hashes would be passed into
# this method in order to find modules installed during the test
# and delete them in order to return the SUT environments to their
# initial state.
#
# TODO: Enhance to take versions into account, so that upgrade/
# downgrade events during a test does not persist in the SUT
# environment.
#
# @param beginning_hash [Hash] paths for found modules indexed
# by hostname. Taken in the setup stage of a test.
# @param ending_hash [Hash] paths for found modules indexed
# by hostname. Taken in the teardown stage of a test.
def rm_installed_modules_from_hosts (beginning_hash, ending_hash)
ending_hash.each do |host, mod_array|
mod_array.each do |mod|
if ! beginning_hash[host].include? mod
on host, "rm -rf #{mod}"
end
end
end
end
# Convert a semantic version number string to an integer.
#
# Example return value given an input of '1.2.42':
#
# 10242
#
# @param semver [String] semantic version number
def semver_to_i ( semver )
# semver assumed to be in format <major>.<minor>.<patch>
# calculation assumes that each segment is < 100
tmp = semver.split('.')
tmp[0].to_i * 10000 + tmp[1].to_i * 100 + tmp[2].to_i
end
# Compare two given semantic version numbers.
#
# Returns an integer indicating the relationship between the two:
# 0 indicates that both are equal
# a value greater than 0 indicates that the semver1 is greater than semver2
# a value less than 0 indicates that the semver1 is less than semver2
#
def semver_cmp ( semver1, semver2 )
semver_to_i(semver1) - semver_to_i(semver2)
end
# Assert that a module was installed according to the UI..
#
# This is a wrapper to centralize the validation about how
# the UI responded that a module was installed.
# It is called after a call # to `on ( host )` and inspects
# STDOUT for specific content.
#
# @param stdout [String]
# @param module_author [String] the author portion of a module name
# @param module_name [String] the name portion of a module name
# @param module_verion [String] the version of the module to compare to
# installed version
# @param compare_op [String] the operator for comparing the verions of
# the installed module
def assert_module_installed_ui ( stdout, module_author, module_name, module_version = nil, compare_op = nil )
valid_compare_ops = {'==' => 'equal to', '>' => 'greater than', '<' => 'less than'}
assert_match(/#{module_author}-#{module_name}/, stdout,
"Notice that module '#{module_author}-#{module_name}' was installed was not displayed")
if version
/#{module_author}-#{module_name} \(.*v(\d+\.\d+\.\d+)/ =~ stdout
installed_version = Regexp.last_match[1]
if valid_compare_ops.include? compare_op
assert_equal( true, semver_cmp(installed_version, module_version).send(compare_op, 0),
"Installed version '#{installed_version}' of '#{module_name}' was not #{valid_compare_ops[compare_op]} '#{module_version}'")
end
end
end
# Assert that a module is installed on disk.
#
# @param host [HOST] the host object to make the remote call on
# @param moduledir [String] the path where the module should be
# @param module_name [String] the name portion of a module name
def assert_module_installed_on_disk ( host, moduledir, module_name )
# module directory should exist
on host, %Q{[ -d "#{moduledir}/#{module_name}" ]}
owner = ''
group = ''
on host, %Q{ls -ld "#{moduledir}"} do
listing = stdout.split(' ')
owner = listing[2]
group = listing[3]
end
# A module's files should have:
# * a mode of 444 (755, if they're a directory)
# * owner == owner of moduledir
# * group == group of moduledir
on host, %Q{ls -alR "#{moduledir}/#{module_name}"} do
listings = stdout.split("\n")
listings = listings.grep(/^[bcdlsp-]/)
listings = listings.reject { |l| l =~ /\.\.$/ }
listings.each do |line|
assert_match /(drwxr-xr-x|[^d]r--r--r--)[^\d]+\d+\s+#{owner}\s+#{group}/, line,
"bad permissions for '#{line[/\S+$/]}' - expected 444/755, #{owner}, #{group}"
end
end
end
# Assert that a module is not installed on disk.
#
# @param host [HOST] the host object to make the remote call on
# @param moduledir [String] the path where the module should be
# @param module_name [String] the name portion of a module name
def assert_module_not_installed_on_disk ( host, moduledir, module_name )
on host, %Q{[ ! -d "#{moduledir}/#{module_name}" ]}
end
+ # Create a simple legacy and directory environment at :path_to_environments.
+ #
+ # @note Also registers a teardown block to remove generated files.
+ #
+ # @param path_to_environments [String] directory to contain all the
+ # generated environment files
+ # @return [String] path to the new puppet configuration file defining the
+ # environments
+ def generate_base_legacy_and_directory_environments(path_to_environments)
+ puppet_conf = "#{path_to_environments}/puppet2.conf"
+ legacy_env = "#{path_to_environments}/legacyenv"
+ dir_envs = "#{path_to_environments}/environments"
+
+ step "ensure we don't have left over bad state from another, possibly failed run"
+ on master, "rm -rf #{legacy_env} #{dir_envs} #{puppet_conf}"
+
+ # and register to clean up afterwords
+ teardown do
+ on master, "rm -rf #{legacy_env} #{dir_envs} #{puppet_conf}"
+ end
+
+ step 'Configure a non-default legacy and directory environment'
+ apply_manifest_on master, %Q{
+ file {
+ [
+ '#{legacy_env}',
+ '#{legacy_env}/modules',
+ '#{dir_envs}',
+ '#{dir_envs}/direnv',
+ ]:
+ ensure => directory,
+ }
+ file {
+ '#{puppet_conf}':
+ source => $settings::config,
+ }
+ }
+
+ on master, puppet("config", "set",
+ "modulepath", "#{legacy_env}/modules",
+ "--section", "legacyenv",
+ "--config", puppet_conf)
+
+ return puppet_conf
+ end
end
end
end
diff --git a/acceptance/config/el6/setup/git/pre-suite/035_StopFirewall.rb b/acceptance/setup/common/pre-suite/025_StopFirewall.rb
similarity index 100%
rename from acceptance/config/el6/setup/git/pre-suite/035_StopFirewall.rb
rename to acceptance/setup/common/pre-suite/025_StopFirewall.rb
diff --git a/acceptance/config/el6/setup/git/pre-suite/04_ValidateSignCert.rb b/acceptance/setup/common/pre-suite/040_ValidateSignCert.rb
old mode 100755
new mode 100644
similarity index 100%
rename from acceptance/config/el6/setup/git/pre-suite/04_ValidateSignCert.rb
rename to acceptance/setup/common/pre-suite/040_ValidateSignCert.rb
diff --git a/acceptance/setup/common/pre-suite/100_SetParser.rb b/acceptance/setup/common/pre-suite/100_SetParser.rb
new file mode 100644
index 000000000..e8c76e69f
--- /dev/null
+++ b/acceptance/setup/common/pre-suite/100_SetParser.rb
@@ -0,0 +1,23 @@
+test_name "add parser=#{ENV['PARSER']} to all puppet.conf (only if $PARSER is set)" do
+
+ parser = ENV['PARSER']
+ next if parser.nil?
+
+ hosts.each do |host|
+ step "adjust #{host} puppet.conf" do
+ temp = host.tmpdir('parser-set')
+ opts = {
+ 'main' => {
+ 'parser' => parser
+ }
+ }
+ lay_down_new_puppet_conf(host, opts, temp)
+
+ if !options[:install].empty? and parser == 'future'
+ # We are installing from source rather than packages and need the following:
+ win_cmd_prefix = 'cmd /c ' if host['platform'] =~ /windows/
+ on(host, "#{win_cmd_prefix}gem install rgen")
+ end
+ end
+ end
+end
diff --git a/acceptance/config/el6/setup/git/pre-suite/00_EnvSetup.rb b/acceptance/setup/git/pre-suite/000_EnvSetup.rb
similarity index 74%
rename from acceptance/config/el6/setup/git/pre-suite/00_EnvSetup.rb
rename to acceptance/setup/git/pre-suite/000_EnvSetup.rb
index 889d6cc4b..79fb45e8f 100644
--- a/acceptance/config/el6/setup/git/pre-suite/00_EnvSetup.rb
+++ b/acceptance/setup/git/pre-suite/000_EnvSetup.rb
@@ -1,40 +1,48 @@
test_name "Setup environment"
step "Ensure Git and Ruby"
require 'puppet/acceptance/install_utils'
extend Puppet::Acceptance::InstallUtils
require 'beaker/dsl/install_utils'
extend Beaker::DSL::InstallUtils
PACKAGES = {
:redhat => [
'git',
'ruby',
+ 'rubygem-json',
],
:debian => [
['git', 'git-core'],
'ruby',
+ 'libjson-ruby',
],
:solaris => [
['git', 'developer/versioning/git'],
['ruby', 'runtime/ruby-18'],
+ # there isn't a package for json, so it is installed later via gems
],
:windows => [
'git',
+ # there isn't a need for json on windows because it is bundled in ruby 1.9
],
}
install_packages_on(hosts, PACKAGES, :check_if_exists => true)
hosts.each do |host|
- if host['platform'] =~ /windows/
+ case host['platform']
+ when /windows/
step "#{host} Install ruby from git"
install_from_git(host, "/opt/puppet-git-repos", :name => 'puppet-win32-ruby', :path => 'git://github.com/puppetlabs/puppet-win32-ruby')
on host, 'cd /opt/puppet-git-repos/puppet-win32-ruby; cp -r ruby/* /'
on host, 'cd /lib; icacls ruby /grant "Everyone:(OI)(CI)(RX)"'
on host, 'cd /lib; icacls ruby /reset /T'
on host, 'ruby --version'
on host, 'cmd /c gem list'
+ when /solaris/
+ step "#{host} Install json from rubygems"
+ on host, 'gem install json'
end
end
diff --git a/acceptance/config/el6/setup/git/pre-suite/01_TestSetup.rb b/acceptance/setup/git/pre-suite/010_TestSetup.rb
old mode 100755
new mode 100644
similarity index 100%
rename from acceptance/config/el6/setup/git/pre-suite/01_TestSetup.rb
rename to acceptance/setup/git/pre-suite/010_TestSetup.rb
diff --git a/acceptance/config/el6/setup/git/pre-suite/02_PuppetUserAndGroup.rb b/acceptance/setup/git/pre-suite/020_PuppetUserAndGroup.rb
similarity index 100%
rename from acceptance/config/el6/setup/git/pre-suite/02_PuppetUserAndGroup.rb
rename to acceptance/setup/git/pre-suite/020_PuppetUserAndGroup.rb
diff --git a/acceptance/config/el6/setup/git/pre-suite/03_PuppetMasterSanity.rb b/acceptance/setup/git/pre-suite/030_PuppetMasterSanity.rb
old mode 100755
new mode 100644
similarity index 100%
rename from acceptance/config/el6/setup/git/pre-suite/03_PuppetMasterSanity.rb
rename to acceptance/setup/git/pre-suite/030_PuppetMasterSanity.rb
diff --git a/acceptance/config/el6/setup/git/pre-suite/05_HieraSetup.rb b/acceptance/setup/git/pre-suite/050_HieraSetup.rb
similarity index 100%
rename from acceptance/config/el6/setup/git/pre-suite/05_HieraSetup.rb
rename to acceptance/setup/git/pre-suite/050_HieraSetup.rb
diff --git a/acceptance/config/el6/setup/git/pre-suite/06_InstallModules.rb b/acceptance/setup/git/pre-suite/060_InstallModules.rb
similarity index 100%
rename from acceptance/config/el6/setup/git/pre-suite/06_InstallModules.rb
rename to acceptance/setup/git/pre-suite/060_InstallModules.rb
diff --git a/acceptance/config/el6/setup/git/pre-suite/07_InstallCACerts.rb b/acceptance/setup/git/pre-suite/070_InstalCACerts.rb
similarity index 100%
rename from acceptance/config/el6/setup/git/pre-suite/07_InstallCACerts.rb
rename to acceptance/setup/git/pre-suite/070_InstalCACerts.rb
diff --git a/acceptance/config/el6/setup/packages/pre-suite/01_Install.rb b/acceptance/setup/packages/pre-suite/010_Install.rb
similarity index 100%
rename from acceptance/config/el6/setup/packages/pre-suite/01_Install.rb
rename to acceptance/setup/packages/pre-suite/010_Install.rb
diff --git a/acceptance/config/el6/setup/pe/pre-suite/00_install.rb b/acceptance/setup/pe/pre-suite/000_Install.rb
similarity index 100%
rename from acceptance/config/el6/setup/pe/pre-suite/00_install.rb
rename to acceptance/setup/pe/pre-suite/000_Install.rb
diff --git a/acceptance/tests/allow_symlinks_as_config_directories.rb b/acceptance/tests/allow_symlinks_as_config_directories.rb
index c8c8bbbf3..2d45d4fef 100644
--- a/acceptance/tests/allow_symlinks_as_config_directories.rb
+++ b/acceptance/tests/allow_symlinks_as_config_directories.rb
@@ -1,30 +1,27 @@
test_name "Should allow symlinks to directories as configuration directories"
confine :except, :platform => 'windows'
agents.each do |agent|
step "Create the test confdir with a link to it"
confdir = agent.tmpdir('puppet_conf-directory')
conflink = agent.tmpfile('puppet_conf-symlink')
on agent, "rm -rf #{conflink} #{confdir}"
on agent, "mkdir #{confdir}"
on agent, "ln -s #{confdir} #{conflink}"
- create_remote_file agent, "#{confdir}/puppet.conf", <<CONFFILE
-[main]
-certname = "awesome_certname"
-CONFFILE
+ on(agent, puppet('config', 'set', 'certname', 'awesome_certname', '--confdir', confdir))
-manifest = 'notify{"My certname is $clientcert": }'
+ manifest = 'notify{"My certname is $clientcert": }'
step "Run Puppet and ensure it used the conf file in the confdir"
on agent, puppet_apply("--confdir #{conflink}"), :stdin => manifest do
- assert_match("My certname is awesome_certname", stdout)
+ assert_match(/My certname is awesome_certname[^\w]/, stdout)
end
step "Check that the symlink and confdir are unchanged"
on agent, "[ -L #{conflink} ]"
on agent, "[ -d #{confdir} ]"
on agent, "[ $(readlink #{conflink}) = #{confdir} ]"
end
diff --git a/acceptance/tests/apply/hashes/should_not_reassign.rb b/acceptance/tests/apply/hashes/should_not_reassign.rb
index 2b0f9cc13..115e54b2b 100755
--- a/acceptance/tests/apply/hashes/should_not_reassign.rb
+++ b/acceptance/tests/apply/hashes/should_not_reassign.rb
@@ -1,10 +1,19 @@
test_name "hash reassignment should fail"
manifest = %q{
$my_hash = {'one' => '1', 'two' => '2' }
$my_hash['one']='1.5'
}
-apply_manifest_on(agents, manifest, :acceptable_exit_codes => [1]) do
- fail_test "didn't find the failure" unless
- stderr.include? "Assigning to the hash 'my_hash' with an existing key 'one'"
+agents.each do |host|
+ parser = on(host, puppet('agent --configprint parser')).stdout.chomp
+ apply_manifest_on(host, manifest, :acceptable_exit_codes => [1]) do
+ expected_error_message =
+ case parser
+ when 'future'
+ "Illegal attempt to assign via [index/key]. Not an assignable reference"
+ else
+ "Assigning to the hash 'my_hash' with an existing key 'one'"
+ end
+ fail_test("didn't find the failure") unless stderr.include?(expected_error_message)
+ end
end
diff --git a/acceptance/tests/config/apply_file_metadata_specified_in_config.rb b/acceptance/tests/config/apply_file_metadata_specified_in_config.rb
index 89e191466..932ec74f3 100644
--- a/acceptance/tests/config/apply_file_metadata_specified_in_config.rb
+++ b/acceptance/tests/config/apply_file_metadata_specified_in_config.rb
@@ -1,29 +1,26 @@
test_name "#17371 file metadata specified in puppet.conf needs to be applied"
# when owner/group works on windows for settings, this confine should be removed.
confine :except, :platform => 'windows'
require 'puppet/acceptance/temp_file_utils'
extend Puppet::Acceptance::TempFileUtils
initialize_temp_dirs()
agents.each do |agent|
logdir = get_test_file_path(agent, 'log')
create_test_file(agent, 'site.pp', <<-SITE)
node default {
notify { puppet_run: }
}
SITE
- create_test_file(agent, 'puppet.conf', <<-CONF)
- [user]
- logdir = #{logdir} { owner = root, group = root, mode = 0700 }
- CONF
+ on(agent, puppet('config', 'set', 'logdir', "'#{logdir} { owner = root, group = root, mode = 0700 }'", '--confdir', get_test_file_path(agent, '')))
on(agent, puppet('apply', get_test_file_path(agent, 'site.pp'), '--confdir', get_test_file_path(agent, '')))
on(agent, "stat --format '%U:%G %a' #{logdir}") do
assert_match(/root:root 700/, stdout)
end
end
diff --git a/acceptance/tests/environment/can_enumerate_environments.rb b/acceptance/tests/environment/can_enumerate_environments.rb
new file mode 100644
index 000000000..e09718f52
--- /dev/null
+++ b/acceptance/tests/environment/can_enumerate_environments.rb
@@ -0,0 +1,62 @@
+test_name "Can enumerate environments via an HTTP endpoint"
+
+def master_port(agent)
+ setting_on(agent, "agent", "masterport")
+end
+
+def setting_on(host, section, name)
+ on(host, puppet("config", "print", name, "--section", section)).stdout.chomp
+end
+
+def full_path(host, path)
+ if host['platform'] =~ /win/
+ on(host, "cygpath '#{path}'").stdout.chomp
+ else
+ path
+ end
+end
+
+def curl_master_from(agent, path, headers = '', &block)
+ url = "https://#{master}:#{master_port(agent)}#{path}"
+ cert_path = full_path(agent, setting_on(agent, "agent", "hostcert"))
+ key_path = full_path(agent, setting_on(agent, "agent", "hostprivkey"))
+ curl_base = "curl -sg --cert \"#{cert_path}\" --key \"#{key_path}\" -k -H '#{headers}'"
+
+ on agent, "#{curl_base} '#{url}'", &block
+end
+
+environments_dir = master.tmpdir("environments")
+apply_manifest_on(master, <<-MANIFEST)
+File {
+ ensure => directory,
+ owner => puppet,
+ mode => 0700,
+}
+
+file {
+ "#{environments_dir}":;
+ "#{environments_dir}/env1":;
+ "#{environments_dir}/env2":;
+}
+MANIFEST
+
+with_puppet_running_on(master, {
+ :master => {
+ :environmentpath => environments_dir
+ }
+}) do
+ agents.each do |agent|
+ step "Ensure that an unauthenticated client cannot access the environments list" do
+ on agent, "curl -ksv https://#{master}:#{master_port(agent)}/v2.0/environments", :acceptable_exit_codes => [0,7] do
+ assert_match(/< HTTP\/1\.\d 403/, stderr)
+ end
+ end
+
+ step "Ensure that an authenticated client can retrieve the list of environments" do
+ curl_master_from(agent, '/v2.0/environments') do
+ data = JSON.parse(stdout)
+ assert_equal(["env1", "env2"], data["environments"].keys.sort)
+ end
+ end
+ end
+end
diff --git a/acceptance/tests/environment/cmdline_overrides_environment.rb b/acceptance/tests/environment/cmdline_overrides_environment.rb
new file mode 100644
index 000000000..1518431b8
--- /dev/null
+++ b/acceptance/tests/environment/cmdline_overrides_environment.rb
@@ -0,0 +1,197 @@
+test_name "Commandline modulepath and manifest settings override environment"
+
+testdir = master.tmpdir('cmdline_and_environment')
+environmentpath = "#{testdir}/environments"
+modulepath = "#{testdir}/modules"
+manifests = "#{testdir}/manifests"
+sitepp = "#{manifests}/site.pp"
+other_manifestdir = "#{testdir}/other_manifests"
+other_sitepp = "#{other_manifestdir}/site.pp"
+other_modulepath = "#{testdir}/some_other_modulepath"
+cmdline_manifest = "#{testdir}/cmdline.pp"
+
+step "Prepare manifests and modules"
+apply_manifest_on(master, <<-MANIFEST, :catch_failures => true)
+File {
+ ensure => directory,
+ owner => puppet,
+ mode => 0700,
+}
+
+##############################################
+# The default production directory environment
+file {
+ "#{testdir}":;
+ "#{environmentpath}":;
+ "#{environmentpath}/production":;
+ "#{environmentpath}/production/manifests":;
+ "#{environmentpath}/production/modules":;
+ "#{environmentpath}/production/modules/amod":;
+ "#{environmentpath}/production/modules/amod/manifests":;
+}
+
+file { "#{environmentpath}/production/modules/amod/manifests/init.pp":
+ ensure => file,
+ content => 'class amod {
+ notify { "amod from production environment": }
+ }'
+}
+
+file { "#{environmentpath}/production/manifests/production.pp":
+ ensure => file,
+ content => '
+ notify { "in production.pp": }
+ include amod
+ '
+}
+
+##############################################################
+# To be set as default manifests and modulepath in puppet.conf
+file {
+ "#{modulepath}":;
+ "#{modulepath}/amod/":;
+ "#{modulepath}/amod/manifests":;
+}
+
+file { "#{modulepath}/amod/manifests/init.pp":
+ ensure => file,
+ content => 'class amod {
+ notify { "amod from modulepath": }
+ }'
+}
+
+file { "#{manifests}": }
+file { "#{sitepp}":
+ ensure => file,
+ content => '
+ notify { "in site.pp": }
+ include amod
+ '
+}
+
+file { "#{other_manifestdir}": }
+file { "#{other_sitepp}":
+ ensure => file,
+ content => '
+ notify { "in other manifestdir site.pp": }
+ include amod
+ '
+}
+
+################################
+# To be specified on commandline
+file {
+ "#{other_modulepath}":;
+ "#{other_modulepath}/amod/":;
+ "#{other_modulepath}/amod/manifests":;
+}
+
+file { "#{other_modulepath}/amod/manifests/init.pp":
+ ensure => file,
+ content => 'class amod {
+ notify { "amod from commandline modulepath": }
+ }'
+}
+
+file { "#{cmdline_manifest}":
+ ensure => file,
+ content => '
+ notify { "in cmdline.pp": }
+ include amod
+ '
+}
+MANIFEST
+
+master_opts = {
+ 'master' => {
+ 'environmentpath' => environmentpath,
+ 'manifest' => sitepp,
+ 'modulepath' => modulepath,
+ }
+}
+
+# Note: this is the semantics seen with legacy environments if commandline
+# manifest/modulepath are set.
+step "puppet master with --manifest and --modulepath overrides existing default production directory environment" do
+ master_opts = master_opts.merge(:__commandline_args__ => "--manifest=#{cmdline_manifest} --modulepath=#{other_modulepath}")
+ with_puppet_running_on master, master_opts, testdir do
+ agents.each do |agent|
+ on(agent, puppet("agent -t --server #{master}"), :acceptable_exit_codes => [2] ) do
+ assert_match(/in cmdline\.pp/, stdout)
+ assert_match(/amod from commandline modulepath/, stdout)
+ assert_no_match(/production/, stdout)
+ end
+
+ step "even if environment is specified"
+ on(agent, puppet("agent -t --server #{master} --environment production"), :acceptable_exit_codes => [2]) do
+ assert_match(/in cmdline\.pp/, stdout)
+ assert_match(/amod from commandline modulepath/, stdout)
+ assert_no_match(/production/, stdout)
+ end
+ end
+ end
+
+ step "or if you set --manifestdir" do
+ master_opts = master_opts.merge(:__commandline_args__ => "--manifestdir=#{other_manifestdir} --modulepath=#{other_modulepath}")
+ step "it is ignored if manifest is set in puppet.conf to something not using $manifestdir"
+ with_puppet_running_on master, master_opts, testdir do
+ agents.each do |agent|
+ on(agent, puppet("agent -t --server #{master}"), :acceptable_exit_codes => [2]) do
+ assert_match(/in production\.pp/, stdout)
+ assert_match(/amod from commandline modulepath/, stdout)
+ end
+ end
+ end
+
+ step "but does pull in the default manifest via manifestdir if manifest is not set"
+ master_opts = master_opts.merge(:__commandline_args__ => "--manifestdir=#{other_manifestdir} --modulepath=#{other_modulepath}")
+ master_opts['master'].delete('manifest')
+ with_puppet_running_on master, master_opts, testdir do
+ agents.each do |agent|
+ on(agent, puppet("agent -t --server #{master}"), :acceptable_exit_codes => [2]) do
+ assert_match(/in other manifestdir site\.pp/, stdout)
+ assert_match(/amod from commandline modulepath/, stdout)
+ assert_no_match(/production/, stdout)
+ end
+ end
+ end
+ end
+end
+
+step "puppet master with manifest and modulepath set in puppet.conf is overriden by an existing default production directory" do
+ with_puppet_running_on master, master_opts, testdir do
+ agents.each do |agent|
+ step "this case is unfortunate, but will be irrelevant when we remove legacyenv in 4.0"
+ on(agent, puppet("agent -t --server #{master}"), :acceptable_exit_codes => [2] ) do
+ assert_match(/in production\.pp/, stdout)
+ assert_match(/amod from production environment/, stdout)
+ end
+
+ step "if environment is specified"
+ on(agent, puppet("agent -t --server #{master} --environment production"), :acceptable_exit_codes => [2]) do
+ assert_match(/in production\.pp/, stdout)
+ assert_match(/amod from production environment/, stdout)
+ end
+ end
+ end
+end
+
+step "puppet master with default manifest, modulepath, environment, environmentpath and an existing default production directory environment directory" do
+ ssldir = on(master, puppet("master --configprint ssldir")).stdout.chomp
+ master_opts = {
+ :__commandline_args__ => "--confdir=#{testdir} --ssldir=#{ssldir}"
+ }
+ with_puppet_running_on master, master_opts, testdir do
+ agents.each do |agent|
+ step "default production directory environment takes precedence"
+ on(agent, puppet("agent -t --server #{master}"), :acceptable_exit_codes => [2] ) do
+ assert_match(/in production\.pp/, stdout)
+ assert_match(/amod from production environment/, stdout)
+ end
+ on(agent, puppet("agent -t --server #{master} --environment production"), :acceptable_exit_codes => [2]) do
+ assert_match(/in production\.pp/, stdout)
+ assert_match(/amod from production environment/, stdout)
+ end
+ end
+ end
+end
diff --git a/acceptance/tests/environment/use_environment_from_environmentpath.rb b/acceptance/tests/environment/use_environment_from_environmentpath.rb
new file mode 100644
index 000000000..b4edce410
--- /dev/null
+++ b/acceptance/tests/environment/use_environment_from_environmentpath.rb
@@ -0,0 +1,164 @@
+test_name "Use environments from the environmentpath"
+
+testdir = master.tmpdir('use_environmentpath')
+
+def generate_environment(path_to_env, environment)
+ env_content = <<-EOS
+ "#{path_to_env}/#{environment}":;
+ "#{path_to_env}/#{environment}/manifests":;
+ "#{path_to_env}/#{environment}/modules":;
+ EOS
+end
+
+def generate_module_content(module_name, options = {})
+ base_path = options[:base_path]
+ environment = options[:environment]
+ env_path = options[:env_path]
+
+ path_to_module = [base_path, env_path, environment, "modules"].compact.join("/")
+ module_info = "module-#{module_name}"
+ module_info << "-from-#{environment}" if environment
+
+ module_content = <<-EOS
+ "#{path_to_module}/#{module_name}":;
+ "#{path_to_module}/#{module_name}/manifests":;
+ "#{path_to_module}/#{module_name}/files":;
+ "#{path_to_module}/#{module_name}/templates":;
+ "#{path_to_module}/#{module_name}/lib":;
+ "#{path_to_module}/#{module_name}/lib/facter":;
+
+ "#{path_to_module}/#{module_name}/manifests/init.pp":
+ ensure => file,
+ content => 'class #{module_name} {
+ notify { "template-#{module_name}": message => template("#{module_name}/our_template.erb") }
+ file { "$agent_file_location/file-#{module_info}": source => "puppet:///modules/#{module_name}/data" }
+ }'
+ ;
+ "#{path_to_module}/#{module_name}/lib/facter/environment_fact_#{module_name}.rb":
+ ensure => file,
+ content => "Facter.add(:environment_fact_#{module_name}) { setcode { 'environment fact from #{module_info}' } }"
+ ;
+ "#{path_to_module}/#{module_name}/files/data":
+ ensure => file,
+ content => "data file from #{module_info}"
+ ;
+ "#{path_to_module}/#{module_name}/templates/our_template.erb":
+ ensure => file,
+ content => "<%= @environment_fact_#{module_name} %>"
+ ;
+ EOS
+end
+
+def generate_site_manifest(path_to_manifest, *modules_to_include)
+ manifest_content = <<-EOS
+ "#{path_to_manifest}/site.pp":
+ ensure => file,
+ content => "#{modules_to_include.map { |m| "include #{m}" }.join("\n")}"
+ ;
+ EOS
+end
+
+apply_manifest_on(master, <<-MANIFEST, :catch_failures => true)
+File {
+ ensure => directory,
+ owner => puppet,
+ mode => 0700,
+}
+
+file {
+ "#{testdir}":;
+ "#{testdir}/base":;
+ "#{testdir}/additional":;
+ "#{testdir}/modules":;
+#{generate_environment("#{testdir}/base", "shadowed")}
+#{generate_environment("#{testdir}/base", "onlybase")}
+#{generate_environment("#{testdir}/additional", "shadowed")}
+
+#{generate_module_content("atmp",
+ :base_path => testdir,
+ :env_path => 'base',
+ :environment => 'shadowed')}
+#{generate_site_manifest("#{testdir}/base/shadowed/manifests", "atmp", "globalmod")}
+
+#{generate_module_content("atmp",
+ :base_path => testdir,
+ :env_path => 'base',
+ :environment => 'onlybase')}
+#{generate_site_manifest("#{testdir}/base/onlybase/manifests", "atmp", "globalmod")}
+
+#{generate_module_content("atmp",
+ :base_path => testdir,
+ :env_path => 'additional',
+ :environment => 'shadowed')}
+#{generate_site_manifest("#{testdir}/additional/shadowed/manifests", "atmp", "globalmod")}
+
+# And one global module (--modulepath setting)
+#{generate_module_content("globalmod", :base_path => testdir)}
+}
+MANIFEST
+
+def run_with_environment(agent, environment, options = {})
+ expected_exit_code = options[:expected_exit_code] || 2
+ expected_strings = options[:expected_strings]
+
+ step "running an agent in environment '#{environment}'"
+ atmp = agent.tmpdir("use_environmentpath_#{environment}")
+
+ agent_config = [
+ "-t",
+ "--server", master,
+ ]
+ agent_config << '--environment' << environment if environment
+ agent_config << {
+ 'ENV' => { "FACTER_agent_file_location" => atmp },
+ }
+
+ on(agent,
+ puppet("agent", *agent_config),
+ :acceptable_exit_codes => [expected_exit_code]) do |result|
+
+ yield atmp, result
+ end
+
+ on agent, "rm -rf #{atmp}"
+end
+
+master_opts = {
+ 'master' => {
+ 'environmentpath' => "#{testdir}/additional:#{testdir}/base",
+ 'basemodulepath' => "#{testdir}/modules",
+ }
+}
+
+with_puppet_running_on master, master_opts, testdir do
+ agents.each do |agent|
+ run_with_environment(agent, "shadowed") do |tmpdir,catalog_result|
+ ["module-atmp-from-shadowed", "module-globalmod"].each do |expected|
+ assert_match(/environment fact from #{expected}/, catalog_result.stdout)
+ end
+
+ ["module-atmp-from-shadowed", "module-globalmod"].each do |expected|
+ on agent, "cat #{tmpdir}/file-#{expected}" do |file_result|
+ assert_match(/data file from #{expected}/, file_result.stdout)
+ end
+ end
+ end
+
+ run_with_environment(agent, "onlybase") do |tmpdir,catalog_result|
+ ["module-atmp-from-onlybase", "module-globalmod"].each do |expected|
+ assert_match(/environment fact from #{expected}/, catalog_result.stdout)
+ end
+
+ ["module-atmp-from-onlybase", "module-globalmod"].each do |expected|
+ on agent, "cat #{tmpdir}/file-#{expected}" do |file_result|
+ assert_match(/data file from #{expected}/, file_result.stdout)
+ end
+ end
+ end
+
+ run_with_environment(agent, nil, :expected_exit_code => 0) do |tmpdir, result|
+ assert_no_match(/module-atmp/, result.stdout, "module-atmp was included despite no environment being loaded")
+ assert_match(/Loading facts.*globalmod/, result.stdout)
+ end
+ end
+end
diff --git a/acceptance/tests/external_ca_support/apache_external_root_ca.rb b/acceptance/tests/external_ca_support/apache_external_root_ca.rb
index 7d8879f19..4019b56d5 100644
--- a/acceptance/tests/external_ca_support/apache_external_root_ca.rb
+++ b/acceptance/tests/external_ca_support/apache_external_root_ca.rb
@@ -1,188 +1,190 @@
begin
require 'puppet_x/acceptance/external_cert_fixtures'
rescue LoadError
$LOAD_PATH.unshift(File.expand_path('../../../lib', __FILE__))
require 'puppet_x/acceptance/external_cert_fixtures'
end
# This test only runs on EL-6 master roles.
confine :to, :platform => 'el-6'
confine :except, :type => 'pe'
# Verify that a trivial manifest can be run to completion.
# Supported Setup: Single, Root CA
# - Agent and Master SSL cert issued by the Root CA
# - Revocation disabled on the agent `certificate_revocation = false`
# - CA disabled on the master `ca = false`
#
# SUPPORT NOTES
#
# * If the x509 alt names extension is used when issuing SSL server certificates
# for the Puppet master, then the client SSL certificate issued by an external
# CA must posses the DNS common name in the alternate name field. This is
# due to a bug in Ruby. If the CN is not duplicated in the Alt Names, then
# the following error will appear on the agent with MRI 1.8.7:
#
# Warning: Server hostname 'master1.example.org' did not match server
# certificate; expected one of master1.example.org, DNS:puppet,
# DNS:master-ca.example.org
#
# See: https://bugs.ruby-lang.org/issues/6493
test_name "Puppet agent works with Apache, both configured with externally issued certificates from independent intermediate CA's"
step "Copy certificates and configuration files to the master..."
fixture_dir = File.expand_path('../fixtures', __FILE__)
testdir = master.tmpdir('apache_external_root_ca')
fixtures = PuppetX::Acceptance::ExternalCertFixtures.new(fixture_dir, testdir)
+# We need this variable in scope.
+disable_and_reenable_selinux = nil
+
+# Register our cleanup steps early in a teardown so that they will happen even
+# if execution aborts part way.
+teardown do
+ step "Cleanup Apache (httpd) and /etc/hosts"
+ # Restore /etc/hosts
+ on master, "cp -p '#{testdir}/hosts' /etc/hosts"
+ # stop the service before moving files around
+ on master, "/etc/init.d/httpd stop"
+ on master, "mv --force /etc/httpd/conf/httpd.conf{,.external_ca_test}"
+ on master, "mv --force /etc/httpd/conf/httpd.conf{.orig,}"
+
+ if disable_and_reenable_selinux
+ step "Restore the original state of SELinux"
+ on master, "setenforce 1"
+ end
+end
+
# Read all of the CA certificates.
# Copy all of the x.509 fixture data over to the master.
create_remote_file master, "#{testdir}/ca_root.crt", fixtures.root_ca_cert
create_remote_file master, "#{testdir}/ca_agent.crt", fixtures.agent_ca_cert
create_remote_file master, "#{testdir}/ca_master.crt", fixtures.master_ca_cert
create_remote_file master, "#{testdir}/ca_master.crl", fixtures.master_ca_crl
create_remote_file master, "#{testdir}/ca_master_bundle.crt", "#{fixtures.master_ca_cert}\n#{fixtures.root_ca_cert}\n"
create_remote_file master, "#{testdir}/ca_agent_bundle.crt", "#{fixtures.agent_ca_cert}\n#{fixtures.root_ca_cert}\n"
create_remote_file master, "#{testdir}/agent.crt", fixtures.agent_cert
create_remote_file master, "#{testdir}/agent.key", fixtures.agent_key
create_remote_file master, "#{testdir}/agent_email.crt", fixtures.agent_email_cert
create_remote_file master, "#{testdir}/agent_email.key", fixtures.agent_email_key
create_remote_file master, "#{testdir}/master.crt", fixtures.master_cert
create_remote_file master, "#{testdir}/master.key", fixtures.master_key
create_remote_file master, "#{testdir}/master_rogue.crt", fixtures.master_cert_rogue
create_remote_file master, "#{testdir}/master_rogue.key", fixtures.master_key_rogue
##
# Now create the master and agent puppet.conf
#
# We need to create the public directory for Passenger and the modules
# directory to avoid `Error: Could not evaluate: Could not retrieve information
# from environment production source(s) puppet://master1.example.org/plugins`
on master, "mkdir -p #{testdir}/etc/{master/{public,modules/empty/lib},agent}"
# Backup /etc/hosts
on master, "cp -p /etc/hosts '#{testdir}/hosts'"
# Make master1.example.org resolve if it doesn't already.
on master, "grep -q -x '#{fixtures.host_entry}' /etc/hosts || echo '#{fixtures.host_entry}' >> /etc/hosts"
create_remote_file master, "#{testdir}/etc/agent/puppet.conf", fixtures.agent_conf
create_remote_file master, "#{testdir}/etc/agent/puppet.conf.crl", fixtures.agent_conf_crl
create_remote_file master, "#{testdir}/etc/agent/puppet.conf.email", fixtures.agent_conf_email
create_remote_file master, "#{testdir}/etc/master/puppet.conf", fixtures.master_conf
# auth.conf to allow *.example.com access to the rest API
create_remote_file master, "#{testdir}/etc/master/auth.conf", fixtures.auth_conf
create_remote_file master, "#{testdir}/etc/master/config.ru", fixtures.config_ru
step "Set filesystem permissions and ownership for the master"
# These permissions are required for Passenger to start Puppet as puppet
-on master, "chown puppet:puppet #{testdir}/etc/master/config.ru"
on master, "chown -R puppet:puppet #{testdir}/etc/master"
# These permissions are just for testing, end users should protect their
# private keys.
on master, "chmod -R a+rX #{testdir}"
agent_cmd_prefix = "--confdir #{testdir}/etc/agent --vardir #{testdir}/etc/agent/var"
-master_cmd_prefix = "--confdir #{testdir}/etc/master --vardir #{testdir}/etc/master/var"
step "Configure EPEL"
epel_release_path = "http://mirror.us.leaseweb.net/epel/6/i386/epel-release-6-8.noarch.rpm"
on master, "rpm -q epel-release || (yum -y install #{epel_release_path} && yum -y upgrade epel-release)"
step "Configure Apache and Passenger"
packages = [ 'httpd', 'mod_ssl', 'mod_passenger', 'rubygem-passenger', 'policycoreutils-python' ]
packages.each do |pkg|
on master, "rpm -q #{pkg} || (yum -y install #{pkg})"
end
create_remote_file master, "#{testdir}/etc/httpd.conf", fixtures.httpd_conf
on master, 'test -f /etc/httpd/conf/httpd.conf.orig || cp -p /etc/httpd/conf/httpd.conf{,.orig}'
on master, "cat #{testdir}/etc/httpd.conf > /etc/httpd/conf/httpd.conf"
step "Make SELinux and Apache play nicely together..."
-# We need this variable in scope.
-disable_and_reenable_selinux = 'UNKNOWN'
on master, "sestatus" do
if stdout.match(/Current mode:.*enforcing/)
disable_and_reenable_selinux = true
else
disable_and_reenable_selinux = false
end
end
if disable_and_reenable_selinux
on master, "setenforce 0"
end
step "Start the Apache httpd service..."
on master, 'service httpd restart'
# Move the agent SSL cert and key into place.
# The filename must match the configured certname, otherwise Puppet will try
# and generate a new certificate and key
step "Configure the agent with the externally issued certificates"
on master, "mkdir -p #{testdir}/etc/agent/ssl/{public_keys,certs,certificate_requests,private_keys,private}"
create_remote_file master, "#{testdir}/etc/agent/ssl/certs/#{fixtures.agent_name}.pem", fixtures.agent_cert
create_remote_file master, "#{testdir}/etc/agent/ssl/private_keys/#{fixtures.agent_name}.pem", fixtures.agent_key
# Now, try and run the agent on the master against itself.
step "Successfully run the puppet agent on the master"
on master, puppet_agent("#{agent_cmd_prefix} --test"), :acceptable_exit_codes => (0..255) do
assert_no_match /Creating a new SSL key/, stdout
assert_no_match /\Wfailed\W/i, stderr
assert_no_match /\Wfailed\W/i, stdout
assert_no_match /\Werror\W/i, stderr
assert_no_match /\Werror\W/i, stdout
# Assert the exit code so we get a "Failed test" instead of an "Errored test"
assert exit_code == 0
end
step "Agent refuses to connect to a rogue master"
on master, puppet_agent("#{agent_cmd_prefix} --ssl_client_ca_auth=#{testdir}/ca_master.crt --masterport=8141 --test"), :acceptable_exit_codes => (0..255) do
assert_no_match /Creating a new SSL key/, stdout
assert_match /certificate verify failed/i, stderr
assert_match /The server presented a SSL certificate chain which does not include a CA listed in the ssl_client_ca_auth file/i, stderr
assert exit_code == 1
end
step "Master accepts client cert with email address in subject"
on master, "cp #{testdir}/etc/agent/puppet.conf{,.no_email}"
on master, "cp #{testdir}/etc/agent/puppet.conf{.email,}"
on master, puppet_agent("#{agent_cmd_prefix} --test"), :acceptable_exit_codes => (0..255) do
assert_no_match /\Wfailed\W/i, stdout
assert_no_match /\Wfailed\W/i, stderr
assert_no_match /\Werror\W/i, stdout
assert_no_match /\Werror\W/i, stderr
# Assert the exit code so we get a "Failed test" instead of an "Errored test"
assert exit_code == 0
end
-
step "Agent refuses to connect to revoked master"
on master, "cp #{testdir}/etc/agent/puppet.conf{,.no_crl}"
on master, "cp #{testdir}/etc/agent/puppet.conf{.crl,}"
revoke_opts = "--hostcrl #{testdir}/ca_master.crl"
on master, puppet_agent("#{agent_cmd_prefix} #{revoke_opts} --test"), :acceptable_exit_codes => (0..255) do
assert_match /certificate revoked.*?example.org/, stderr
assert exit_code == 1
end
-step "Cleanup Apache (httpd) and /etc/hosts"
-# Restore /etc/hosts
-on master, "cp -p '#{testdir}/hosts' /etc/hosts"
-# stop the service before moving files around
-on master, "/etc/init.d/httpd stop"
-on master, "mv --force /etc/httpd/conf/httpd.conf{,.external_ca_test}"
-on master, "mv --force /etc/httpd/conf/httpd.conf{.orig,}"
-
-if disable_and_reenable_selinux
- step "Restore the original state of SELinux"
- on master, "setenforce 1"
-end
-
step "Finished testing External Certificates"
diff --git a/acceptance/tests/face/loadable_from_modules.rb b/acceptance/tests/face/loadable_from_modules.rb
index 9acb3946e..272d84c26 100644
--- a/acceptance/tests/face/loadable_from_modules.rb
+++ b/acceptance/tests/face/loadable_from_modules.rb
@@ -1,92 +1,87 @@
test_name "Exercise loading a face from a module"
# Because the module tool does not work on windows, we can't run this test there
confine :except, :platform => 'windows'
require 'puppet/acceptance/temp_file_utils'
extend Puppet::Acceptance::TempFileUtils
initialize_temp_dirs
agents.each do |agent|
dev_modulepath = get_test_file_path(agent, 'dev/modules')
user_modulepath = get_test_file_path(agent, 'user/modules')
# make sure that we use the modulepath from the dev environment
- create_test_file(agent, 'puppet.conf', <<"END")
-[user]
-environment=dev
-modulepath=#{user_modulepath}
-
-[dev]
-modulepath=#{dev_modulepath}
-END
puppetconf = get_test_file_path(agent, 'puppet.conf')
+ on agent, puppet("config", "set", "environment", "dev", "--section", "user", "--config", puppetconf)
+ on agent, puppet("config", "set", "modulepath", user_modulepath, "--section", "user", "--config", puppetconf)
+ on agent, puppet("config", "set", "modulepath", dev_modulepath, "--section", "user", "--config", puppetconf)
on agent, 'rm -rf puppetlabs-helloworld'
on agent, puppet("module", "generate", "puppetlabs-helloworld")
mkdirs agent, 'puppetlabs-helloworld/lib/puppet/application'
mkdirs agent, 'puppetlabs-helloworld/lib/puppet/face'
# copy application, face, and utility module
create_remote_file(agent, "puppetlabs-helloworld/lib/puppet/application/helloworld.rb", <<'EOM')
require 'puppet/face'
require 'puppet/application/face_base'
class Puppet::Application::Helloworld < Puppet::Application::FaceBase
end
EOM
create_remote_file(agent, "puppetlabs-helloworld/lib/puppet/face/helloworld.rb", <<'EOM')
Puppet::Face.define(:helloworld, '0.0.1') do
summary "Hello world face"
description "This is the hello world face"
action 'actionprint' do
summary "Prints hello world from an action"
when_invoked do |options|
puts "Hello world from an action"
end
end
action 'moduleprint' do
summary "Prints hello world from a required module"
when_invoked do |options|
require 'puppet/helloworld.rb'
Puppet::Helloworld.print
end
end
end
EOM
create_remote_file(agent, "puppetlabs-helloworld/lib/puppet/helloworld.rb", <<'EOM')
module Puppet::Helloworld
def print
puts "Hello world from a required module"
end
module_function :print
end
EOM
on agent, puppet('module', 'build', 'puppetlabs-helloworld')
# Why from 3.1.1 -> 3.2.0 did the version of this module change from 0.0.1
# to 0.1.0 but the api within the face didn't?
on agent, puppet('module', 'install', '--ignore-dependencies', '--target-dir', dev_modulepath, 'puppetlabs-helloworld/pkg/puppetlabs-helloworld-0.1.0.tar.gz')
on(agent, puppet('help', '--config', puppetconf)) do
assert_match(/helloworld\s*Hello world face/, stdout, "Face missing from list of available subcommands")
end
on(agent, puppet('help', 'helloworld', '--config', puppetconf)) do
assert_match(/This is the hello world face/, stdout, "Descripion help missing")
assert_match(/moduleprint\s*Prints hello world from a required module/, stdout, "help for moduleprint action missing")
assert_match(/actionprint\s*Prints hello world from an action/, stdout, "help for actionprint action missing")
end
on(agent, puppet('helloworld', 'actionprint', '--config', puppetconf)) do
assert_match(/^Hello world from an action$/, stdout, "face did not print hello world")
end
on(agent, puppet('helloworld', 'moduleprint', '--config', puppetconf)) do
assert_match(/^Hello world from a required module$/, stdout, "face did not load module to print hello world")
end
end
diff --git a/acceptance/tests/jeff_append_to_array.rb b/acceptance/tests/jeff_append_to_array.rb
deleted file mode 100644
index 1fab8ad3c..000000000
--- a/acceptance/tests/jeff_append_to_array.rb
+++ /dev/null
@@ -1,20 +0,0 @@
-# Ported from the acceptance test suite.
-test_name "Jeff: Append to Array"
-
-manifest = %q{
- class parent {
- $arr1 = [ "parent array element" ]
- }
- class parent::child inherits parent {
- $arr1 += [ "child array element" ]
- notify { $arr1: }
- }
- include parent::child
-}
-
-agents.each do |host|
- apply_manifest_on(host, manifest) do
- assert_match(/parent array element/, stdout, "#{host}: parent missing")
- assert_match(/child array element/, stdout, "#{host}: child missing")
- end
-end
diff --git a/acceptance/tests/key_compare_puppet_conf_configprint.rb b/acceptance/tests/key_compare_puppet_conf_configprint.rb
deleted file mode 100644
index 38b639463..000000000
--- a/acceptance/tests/key_compare_puppet_conf_configprint.rb
+++ /dev/null
@@ -1,65 +0,0 @@
-# Check for the existance of keys found in puppet.conf in
-# --configprint all output
-#
-# Checking against key=>val pairs will cause erroneous errors:
-#
-# classfile
-# Puppet.conf --configprint
-# $vardir/classes.txt /var/opt/lib/pe-puppet/classes.txt
-
-test_name "Validate keys found in puppet.conf vs.--configprint all"
-
-puppet_conf_h = Hash.new
-config_print_h = Hash.new
-
-# Run tests against Master first
-step "Master: get puppet.conf file contents"
-on master, "cat #{master['puppetpath']}/puppet.conf | tr -d \" \"" do
- stdout.split("\n").select{ |v| v =~ /=/ }.each do |line|
- k,v = line.split("=")
- puppet_conf_h[k]=v
- end
-end
-
-step "Master: get --configprint all output"
-on master, puppet_master("--configprint all | tr -d \" \"") do
- stdout.split("\n").select{ |v| v =~ /=/ }.each do |line|
- k,v = line.split("=")
- config_print_h[k]=v
- end
-end
-
-step "Master: compare puppet.conf to --configprint output"
-puppet_conf_h.each do |k,v|
- puts "#{k}: #{puppet_conf_h[k]} #{config_print_h[k]}"
- fail_test "puppet.conf contains a key not found in configprint" unless config_print_h.include?(k)
- # fail_test "puppet.conf: #{puppet_conf_h[k]} differs from --configprintall: #{config_print_h[k]}" if ( puppet_conf_h[k] != config_print_h[k] )
-end
-
-# Run test on Agents
-agents.each { |agent|
- puppet_conf_h.clear
- config_print_h.clear
- step "Agent #{agent}: get puppet.conf file contents"
- on agent, "cat #{master['puppetpath']}/puppet.conf | tr -d \" \"" do
- stdout.split("\n").select{ |v| v =~ /=/ }.each do |line|
- k,v = line.split("=")
- puppet_conf_h[k]=v
- end
- end
-
- step "Agent #{agent}: get --configprint all output"
- on agent, puppet_agent("--configprint all | tr -d \" \"") do
- stdout.split("\n").select{ |v| v =~ /=/ }.each do |line|
- k,v = line.split("=")
- config_print_h[k]=v
- end
- end
-
- step "Agent #{agent}: compare puppet.conf to --configprint output"
- puppet_conf_h.each do |k,v|
- puts "#{k}: #{puppet_conf_h[k]} #{config_print_h[k]}"
- fail_test "puppet.conf contains a key not found in configprint" unless config_print_h.include?(k)
- # fail_test "puppet.conf: #{puppet_conf_h[k]} differs from --configprintall: #{config_print_h[k]}" if ( puppet_conf_h[k] != config_print_h[k] )
- end
-}
diff --git a/acceptance/tests/modules/backwards_compatibility/13682_do_not_monkey_patch_old_pmt.rb b/acceptance/tests/modules/backwards_compatibility/13682_do_not_monkey_patch_old_pmt.rb
index 3402c45ff..8cfb0be3f 100644
--- a/acceptance/tests/modules/backwards_compatibility/13682_do_not_monkey_patch_old_pmt.rb
+++ b/acceptance/tests/modules/backwards_compatibility/13682_do_not_monkey_patch_old_pmt.rb
@@ -1,23 +1,24 @@
begin
test_name "puppet module should not monkey patch puppet-module"
step "Simulate the behavior of puppet-module"
puppet_module = <<-PUPPET_MODULE
#{master['puppetbindir']}/ruby -e '
module Puppet
class Module
module Tool
REPOSITORY_URL=1
end
end
end
+require "rubygems"
require "puppet"
puts Puppet.version
'
PUPPET_MODULE
on(master, puppet_module) do
# If we monkey patch the existing puppet-module Gem then Ruby will issue a
# warning about redefined constants. This is not a comprehensive test but
# it should catch the majority of regressions.
assert_no_match(/warning/, stderr)
end
end
diff --git a/acceptance/tests/modules/install/with_environment.rb b/acceptance/tests/modules/install/with_environment.rb
index 31d2c43ab..d9200d138 100644
--- a/acceptance/tests/modules/install/with_environment.rb
+++ b/acceptance/tests/modules/install/with_environment.rb
@@ -1,43 +1,63 @@
test_name 'puppet module install (with environment)'
require 'puppet/acceptance/module_utils'
extend Puppet::Acceptance::ModuleUtils
module_author = "pmtacceptance"
module_name = "nginx"
-module_dependencies = []
orig_installed_modules = get_installed_modules_for_hosts hosts
teardown do
rm_installed_modules_from_hosts orig_installed_modules, (get_installed_modules_for_hosts hosts)
- # TODO make helper take environments into account
- on master, "rm -rf #{master['puppetpath']}/testenv #{master['puppetpath']}/puppet2.conf"
end
step 'Setup'
stub_forge_on(master)
-# Configure a non-default environment
-on master, "rm -rf #{master['puppetpath']}/testenv"
-apply_manifest_on master, %Q{
+puppet_conf = generate_base_legacy_and_directory_environments(master['puppetpath'])
+
+check_module_install_in = lambda do |environment_path, module_install_args|
+ on master, "puppet module install #{module_author}-#{module_name} --config=#{puppet_conf} #{module_install_args}" do
+ assert_module_installed_ui(stdout, module_author, module_name)
+ assert_match(/#{environment_path}/, stdout,
+ "Notice of non default install path was not displayed")
+ end
+ assert_module_installed_on_disk(master, "#{environment_path}", module_name)
+end
+
+step 'Install a module into a non default legacy environment' do
+ check_module_install_in.call("#{master['puppetpath']}/legacyenv/modules",
+ "--environment=legacyenv")
+end
+
+step 'Install a module into a non default directory environment' do
+ check_module_install_in.call("#{master['puppetpath']}/environments/direnv/modules",
+ "--environment=direnv")
+end
+
+step 'Prepare a separate modulepath'
+modulepath_dir = master.tmpdir("modulepath")
+apply_manifest_on(master, <<-MANIFEST , :catch_failures => true)
file {
[
- '#{master['puppetpath']}/testenv',
- '#{master['puppetpath']}/testenv/modules',
+ '#{master['puppetpath']}/environments/production',
+ '#{modulepath_dir}',
]:
- ensure => directory,
- }
- file {
- '#{master['puppetpath']}/puppet2.conf':
- source => $settings::config,
+
+ ensure => directory,
+ owner => puppet,
}
-}
-on master, "{ echo '[testenv]'; echo 'modulepath=#{master['puppetpath']}/testenv/modules'; } >> #{master['puppetpath']}/puppet2.conf"
-
-step 'Install a module into a non default environment'
-on master, "puppet module install #{module_author}-#{module_name} --config=#{master['puppetpath']}/puppet2.conf --environment=testenv" do
- assert_module_installed_ui(stdout, module_author, module_name)
- assert_match(/#{master['puppetpath']}\/testenv\/modules/, stdout,
- "Notice of non default install path was not displayed")
+MANIFEST
+
+step "Install a module into --modulepath #{modulepath_dir} despite the implicit production directory env existing" do
+ check_module_install_in.call(modulepath_dir, "--modulepath=#{modulepath_dir}")
+end
+
+step "Uninstall so we can try a different scenario" do
+ on master, "puppet module uninstall #{module_author}-#{module_name} --config=#{puppet_conf} --modulepath=#{modulepath_dir}"
+end
+
+step "Install a module into --modulepath #{modulepath_dir} with a directory env specified" do
+ check_module_install_in.call(modulepath_dir,
+ "--modulepath=#{modulepath_dir} --environment=direnv")
end
-assert_module_installed_on_disk(master, "#{master['puppetpath']}/testenv/modules", module_name)
diff --git a/acceptance/tests/modules/list/with_environment.rb b/acceptance/tests/modules/list/with_environment.rb
index 168678473..841265e7a 100644
--- a/acceptance/tests/modules/list/with_environment.rb
+++ b/acceptance/tests/modules/list/with_environment.rb
@@ -1,33 +1,36 @@
test_name 'puppet module list (with environment)'
+require 'puppet/acceptance/module_utils'
+extend Puppet::Acceptance::ModuleUtils
step 'Setup'
stub_forge_on(master)
-# Configure a non-default environment
-on master, "rm -rf #{master['puppetpath']}/testenv"
-apply_manifest_on master, %Q{
- file {
- [
- '#{master['puppetpath']}/testenv',
- '#{master['puppetpath']}/testenv/modules',
- ]:
- ensure => directory,
- }
- file {
- '#{master['puppetpath']}/puppet2.conf':
- source => $settings::config,
- }
-}
-on master, "{ echo '[testenv]'; echo 'modulepath=#{master['puppetpath']}/testenv/modules'; } >> #{master['puppetpath']}/puppet2.conf"
-on master, "puppet module install pmtacceptance-nginx --config=#{master['puppetpath']}/puppet2.conf --environment=testenv"
-
-teardown do
- on master, "rm -rf #{master['puppetpath']}/testenv #{master['puppetpath']}/puppet2.conf"
+puppet_conf = generate_base_legacy_and_directory_environments(master['puppetpath'])
+
+install_test_module_in = lambda do |environment|
+ on master, puppet("module", "install",
+ "pmtacceptance-nginx",
+ "--config", puppet_conf,
+ "--environment", environment)
+end
+
+check_module_list_in = lambda do |environment, environment_path|
+ on master, puppet("module", "list",
+ "--config", puppet_conf,
+ "--environment", environment) do
+
+ assert_match(/#{environment_path}/, stdout)
+ assert_match(/pmtacceptance-nginx/, stdout)
+ end
+end
+
+step 'List modules in a non default legacy environment' do
+ install_test_module_in.call('legacyenv')
+ check_module_list_in.call('legacyenv', "#{master['puppetpath']}/legacyenv/modules")
end
-step 'List modules in a non default environment'
-on master, puppet("module list --config=#{master['puppetpath']}/puppet2.conf --environment=testenv") do
- assert_match(/testenv\/modules/, stdout)
- assert_match(/pmtacceptance-nginx/, stdout)
+step 'List modules in a non default directory environment' do
+ install_test_module_in.call('direnv')
+ check_module_list_in.call('direnv', "#{master['puppetpath']}/environments/direnv/modules")
end
diff --git a/acceptance/tests/modules/uninstall/with_environment.rb b/acceptance/tests/modules/uninstall/with_environment.rb
index ac2aff7f2..ac35dadcd 100644
--- a/acceptance/tests/modules/uninstall/with_environment.rb
+++ b/acceptance/tests/modules/uninstall/with_environment.rb
@@ -1,47 +1,58 @@
test_name 'puppet module uninstall (with environment)'
-
-teardown do
-on master, "rm -rf #{master['puppetpath']}/testenv #{master['puppetpath']}/puppet2.conf"
-end
+require 'puppet/acceptance/module_utils'
+extend Puppet::Acceptance::ModuleUtils
step 'Setup'
stub_forge_on(master)
+puppet_conf = generate_base_legacy_and_directory_environments(master['puppetpath'])
+
+crakorn_metadata = <<-EOS
+{
+ "name": "jimmy/crakorn",
+ "version": "0.4.0",
+ "source": "",
+ "author": "jimmy",
+ "license": "MIT",
+ "dependencies": []
+}
+EOS
+
# Configure a non-default environment
-on master, "rm -rf #{master['puppetpath']}/testenv"
apply_manifest_on master, %Q{
file {
[
- '#{master['puppetpath']}/testenv',
- '#{master['puppetpath']}/testenv/modules',
- '#{master['puppetpath']}/testenv/modules/crakorn',
+ '#{master['puppetpath']}/legacyenv/modules/crakorn',
+ '#{master['puppetpath']}/environments/direnv/modules',
+ '#{master['puppetpath']}/environments/direnv/modules/crakorn',
]:
ensure => directory,
}
file {
- '#{master['puppetpath']}/testenv/modules/crakorn/metadata.json':
- content => '{
- "name": "jimmy/crakorn",
- "version": "0.4.0",
- "source": "",
- "author": "jimmy",
- "license": "MIT",
- "dependencies": []
- }',
+ '#{master['puppetpath']}/legacyenv/modules/crakorn/metadata.json':
+ content => '#{crakorn_metadata}',
}
file {
- '#{master['puppetpath']}/puppet2.conf':
- source => $settings::config,
+ '#{master['puppetpath']}/environments/direnv/modules/crakorn/metadata.json':
+ content => '#{crakorn_metadata}',
}
}
-on master, %Q{{ echo "[testenv]"; echo "modulepath=#{master['puppetpath']}/testenv/modules"; } >> #{master['puppetpath']}/puppet2.conf}
-
-step 'Uninstall a module from a non default environment'
-on master, "puppet module uninstall jimmy-crakorn --config=#{master['puppetpath']}/puppet2.conf --environment=testenv" do
- assert_output <<-OUTPUT
- \e[mNotice: Preparing to uninstall 'jimmy-crakorn' ...\e[0m
- Removed 'jimmy-crakorn' (\e[0;36mv0.4.0\e[0m) from #{master['puppetpath']}/testenv/modules
- OUTPUT
+
+check_module_uninstall_in = lambda do |environment, environment_path|
+ on master, "puppet module uninstall jimmy-crakorn --config=#{puppet_conf} --environment=#{environment}" do
+ assert_output <<-OUTPUT
+ \e[mNotice: Preparing to uninstall 'jimmy-crakorn' ...\e[0m
+ Removed 'jimmy-crakorn' (\e[0;36mv0.4.0\e[0m) from #{environment_path}
+ OUTPUT
+ end
+ on master, "[ ! -d #{environment_path}/crakorn ]"
+end
+
+step 'Uninstall a module from a non default legacy environment' do
+ check_module_uninstall_in.call('legacyenv', "#{master['puppetpath']}/legacyenv/modules")
+end
+
+step 'Uninstall a module from a non default directory environment' do
+ check_module_uninstall_in.call('direnv', "#{master['puppetpath']}/environments/direnv/modules")
end
-on master, "[ ! -d #{master['puppetpath']}/testenv/modules/crakorn ]"
diff --git a/acceptance/tests/modules/upgrade/with_environment.rb b/acceptance/tests/modules/upgrade/with_environment.rb
index 28824b19b..4dc5dba4c 100644
--- a/acceptance/tests/modules/upgrade/with_environment.rb
+++ b/acceptance/tests/modules/upgrade/with_environment.rb
@@ -1,45 +1,42 @@
test_name "puppet module upgrade (with environment)"
require 'puppet/acceptance/module_utils'
extend Puppet::Acceptance::ModuleUtils
module_author = "pmtacceptance"
module_name = "java"
module_dependencies = ["stdlib"]
orig_installed_modules = get_installed_modules_for_hosts hosts
teardown do
rm_installed_modules_from_hosts orig_installed_modules, (get_installed_modules_for_hosts hosts)
- # TODO make helper take environments into account
- on master, "rm -rf #{master['puppetpath']}/testenv #{master['puppetpath']}/puppet2.conf"
end
step 'Setup'
stub_forge_on(master)
-# Configure a non-default environment
-apply_manifest_on master, %Q{
- file {
- [
- '#{master['puppetpath']}/testenv',
- '#{master['puppetpath']}/testenv/modules',
- ]:
- ensure => directory,
- }
- file {
- '#{master['puppetpath']}/puppet2.conf':
- source => $settings::config,
- }
-}
-on master, "{ echo '[testenv]'; echo 'modulepath=#{master['puppetpath']}/testenv/modules'; } >> #{master['puppetpath']}/puppet2.conf"
-
-on master, puppet("module install #{module_author}-#{module_name} --config=#{master['puppetpath']}/puppet2.conf --version 1.6.0 --environment=testenv") do
- assert_module_installed_ui(stdout, module_author, module_name)
+puppet_conf = generate_base_legacy_and_directory_environments(master['puppetpath'])
+
+install_test_module_in = lambda do |environment|
+ on master, puppet("module install #{module_author}-#{module_name} --config=#{puppet_conf} --version 1.6.0 --environment=#{environment}") do
+ assert_module_installed_ui(stdout, module_author, module_name)
+ end
+end
+
+check_module_upgrade_in = lambda do |environment, environment_path|
+ on master, puppet("module upgrade #{module_author}-#{module_name} --config=#{puppet_conf} --environment=#{environment}") do
+ assert_module_installed_ui(stdout, module_author, module_name)
+ on master, "[ -f #{environment_path}/#{module_name}/Modulefile ]"
+ on master, "grep 1.7.1 #{environment_path}/#{module_name}/Modulefile"
+ end
+end
+
+step "Upgrade a module that has a more recent version published in a legacy environment" do
+ install_test_module_in.call('legacyenv')
+ check_module_upgrade_in.call('legacyenv', "#{master['puppetpath']}/legacyenv/modules")
end
-step "Upgrade a module that has a more recent version published"
-on master, puppet("module upgrade #{module_author}-#{module_name} --config=#{master['puppetpath']}/puppet2.conf --environment=testenv") do
- assert_module_installed_ui(stdout, module_author, module_name)
- on master, "[ -f #{master['puppetpath']}/testenv/modules/#{module_name}/Modulefile ]"
- on master, "grep 1.7.1 #{master['puppetpath']}/testenv/modules/#{module_name}/Modulefile"
+step "Upgrade a module that has a more recent version published in a directory environment" do
+ install_test_module_in.call('direnv')
+ check_module_upgrade_in.call('direnv', "#{master['puppetpath']}/environments/direnv/modules")
end
diff --git a/acceptance/tests/resource/service/ticket_4123_should_list_all_running_redhat.rb b/acceptance/tests/resource/service/ticket_4123_should_list_all_running_redhat.rb
index 064424766..bf556d6c6 100755
--- a/acceptance/tests/resource/service/ticket_4123_should_list_all_running_redhat.rb
+++ b/acceptance/tests/resource/service/ticket_4123_should_list_all_running_redhat.rb
@@ -1,12 +1,12 @@
test_name "#4123: should list all running services on Redhat/CentOS"
step "Validate services running agreement ralsh vs. OS service count"
# This will remotely exec:
# ticket_4123_should_list_all_running_redhat.sh
hosts.each do |host|
- if host['platform'].include?('el-')
+ if host['platform'] =~ /el-5/
run_script_on(host, File.join(File.dirname(__FILE__), 'ticket_4123_should_list_all_running_redhat.sh'))
else
skip_test "Test not supported on this plaform"
end
end
diff --git a/acceptance/tests/resource/service/ticket_4124_should_list_all_disabled.rb b/acceptance/tests/resource/service/ticket_4124_should_list_all_disabled.rb
index 7ba3c1a28..750f142be 100755
--- a/acceptance/tests/resource/service/ticket_4124_should_list_all_disabled.rb
+++ b/acceptance/tests/resource/service/ticket_4124_should_list_all_disabled.rb
@@ -1,12 +1,12 @@
test_name "#4124: should list all disabled services on Redhat/CentOS"
step "Validate disabled services agreement ralsh vs. OS service count"
# This will remotely exec:
# ticket_4124_should_list_all_disabled.sh
hosts.each do |host|
- unless host['platform'].include?('el-')
- skip_test "Test not supported on this plaform"
- else
+ if host['platform'] =~ /el-5/
run_script_on(host, File.join(File.dirname(__FILE__), 'ticket_4124_should_list_all_disabled.sh'))
+ else
+ skip_test "Test not supported on this plaform"
end
end
diff --git a/acceptance/tests/security/cve-2013-1653_puppet_kick.rb b/acceptance/tests/security/cve-2013-1653_puppet_kick.rb
index a3decd0cf..818516aed 100644
--- a/acceptance/tests/security/cve-2013-1653_puppet_kick.rb
+++ b/acceptance/tests/security/cve-2013-1653_puppet_kick.rb
@@ -1,112 +1,113 @@
test_name "CVE 2013-1653: Puppet Kick Remote Code Exploit" do
step "Determine suitability of the test" do
confine :except, :platform => 'windows'
versions = on( hosts, puppet( '--version' ))
skip_test( "This test will not run on Puppet 2.6" ) if
versions.any? {|r| r.stdout =~ /\A2\.6\./ }
end
def exploit_code( exploiter, exploitee, endpoint, port, file_to_create )
certfile = on( exploiter, puppet_agent( '--configprint hostcert' )).stdout.chomp
keyfile = on( exploiter, puppet_agent( '--configprint hostprivkey' )).stdout.chomp
exploit = %Q[#!#{exploiter['puppetbindir']}/ruby
+ require 'rubygems'
require 'puppet'
require 'openssl'
require 'net/https'
yaml = <<EOM
--- !ruby/object:ERB
safe_level:
src: |-
#coding:US-ASCII
_erbout = ''; _erbout.concat(( File.open( '#{file_to_create}', 'w') ).to_s)
filename:
EOM
headers = {'Content-Type' => 'text/yaml', 'Accept' => 'yaml'}
conn = Net::HTTP.new('#{exploitee}', #{port})
conn.use_ssl = true
conn.cert = OpenSSL::X509::Certificate.new(File.read('#{certfile}'))
conn.key = OpenSSL::PKey::RSA.new(File.read('#{keyfile}'))
conn.verify_mode = OpenSSL::SSL::VERIFY_NONE
conn.request_put("/production/#{endpoint}/#{exploiter}", yaml, headers) do |response|
response.read_body do |chunk|
puts chunk
end
end ]
return exploit
end
exploited = '/tmp/cve-2013-1653-has-worked'
restauth_conf = %q[
path /run
auth yes
allow *
]
teardown do
agents.each do |agent|
pidfile = on( agent, puppet_agent("--configprint pidfile") ).stdout.chomp
on agent, "[ -f #{pidfile} ] && kill `cat #{pidfile}` || true"
on agent, "rm -rf #{exploited}"
end
end
agents.each do |agent|
# We have to skip this case because of bug PP-436. When that gets fixed, we
# can test on all nodes again.
if agent == master
Log.warn("This test does not support nodes that are both master and agents")
next
end
atestdir = agent.tmpdir('puppet-kick-auth')
mtestdir = master.tmpdir('puppet-kick-auth')
step "Daemonize the agent" do
# Lay down a tempory auth.conf that will allow the agent to be kicked
create_remote_file(agent, "#{atestdir}/auth.conf", restauth_conf)
# Start the agent
on(agent, puppet_agent("--debug --daemonize --server #{master} --listen --no-client --rest_authconfig #{atestdir}/auth.conf"))
step "Wait for agent to start listening" do
timeout = 15
begin
Timeout.timeout(timeout) do
loop do
# 7 is "Could not connect to host", which will happen before it's running
result = on(agent, "curl -k https://#{agent}:8139", :acceptable_exit_codes => [0,7])
break if result.exit_code == 0
sleep 1
end
end
rescue Timeout::Error
fail_test "Puppet agent #{agent} failed to start after #{timeout} seconds"
end
end
end
step "Attempt to exploit #{agent}" do
# Ensure there's no stale data
on agent, "rm -rf #{exploited}"
on master, "rm -rf #{mtestdir}/exploit.rb"
# Copy over our exploit and execute
create_remote_file( master, "#{mtestdir}/exploit.rb", exploit_code( master, agent, 'run', 8139, exploited ))
on master, "chmod +x #{mtestdir}/exploit.rb"
on master, "#{mtestdir}/exploit.rb"
# Did it work?
fail_test( "Found exploit file #{exploited}" ) if
on( agent, "[ ! -f #{exploited} ]",
:acceptable_exit_codes => [0,1] ).exit_code == 1
end
end
end
diff --git a/acceptance/tests/security/cve-2013-3567_yaml_deserialization_again.rb b/acceptance/tests/security/cve-2013-3567_yaml_deserialization_again.rb
new file mode 100644
index 000000000..bb216b93b
--- /dev/null
+++ b/acceptance/tests/security/cve-2013-3567_yaml_deserialization_again.rb
@@ -0,0 +1,31 @@
+test_name "CVE-2013-3567 Arbitrary YAML Deserialization"
+
+reportdir = master.tmpdir('yaml_deserialization')
+
+dangerous_yaml = "--- !ruby/object:Puppet::Transaction::Report { metrics: { resources: !ruby/object:ERB { src: 'exit 0' } }, logs: [], resource_statuses: [], host: '$(puppet master --configprint certname)' }"
+
+submit_bad_yaml = [
+ "curl -k -X PUT",
+ "--cacert $(puppet master --configprint cacert)",
+ "--cert $(puppet master --configprint hostcert)",
+ "--key $(puppet master --configprint hostprivkey)",
+ "-H 'Content-Type: text/yaml'",
+ "-d \"#{dangerous_yaml}\"",
+ "\"https://#{master}:8140/production/report/$(puppet master --configprint certname)\""
+].join(' ')
+
+master_opts = {
+ 'master' => {
+ 'reportdir' => reportdir,
+ 'reports' => 'store',
+ }
+}
+
+with_puppet_running_on(master, master_opts) do
+ on master, submit_bad_yaml
+ on master, "cat #{reportdir}/$(puppet master --configprint certname)/*" do
+ assert_no_match(/ERB/, stdout, "Improperly propagated ERB object from input into puppet code")
+ end
+end
+
+on master, "rm -rf #{reportdir}"
diff --git a/acceptance/tests/security/cve-2013-3567_yaml_parameter_deserialization.rb b/acceptance/tests/security/cve-2013-3567_yaml_parameter_deserialization.rb
new file mode 100644
index 000000000..d4c059018
--- /dev/null
+++ b/acceptance/tests/security/cve-2013-3567_yaml_parameter_deserialization.rb
@@ -0,0 +1,36 @@
+test_name "CVE-2013-3567 Arbitrary YAML Query Parameter Deserialization"
+
+CURL_UNABLE_TO_FETCH_PAGE = 22
+
+require 'uri'
+
+dangerous_yaml = "--- !ruby/object:Puppet::Node::Environment { name: 'manage' }"
+
+submit_bad_yaml_as_parameter = [
+ "curl -f -s -S -k -X GET",
+ "--cacert $(puppet master --configprint cacert)",
+ "--cert $(puppet master --configprint hostcert)",
+ "--key $(puppet master --configprint hostprivkey)",
+ "-H 'Accept: yaml'",
+ "\"https://#{master}:8140/production/file_metadata/modules/testing/tested?links=#{URI.encode(dangerous_yaml)}\""
+].join(' ')
+
+
+modules = master.tmpdir('modules')
+apply_manifest_on master, <<MANIFEST
+ file { "#{modules}": ensure => directory, owner => puppet }
+-> file { "#{modules}/testing": ensure => directory, owner => puppet }
+-> file { "#{modules}/testing/files": ensure => directory, owner => puppet }
+-> file { "#{modules}/testing/files/tested": ensure => file, content => "test", owner => puppet }
+MANIFEST
+
+master_opts = {
+ 'master' => {
+ 'modulepath' => modules,
+ }
+}
+
+with_puppet_running_on(master, master_opts) do
+ step "Expect the master to reject the request"
+ on master, submit_bad_yaml_as_parameter, :acceptable_exit_codes => [CURL_UNABLE_TO_FETCH_PAGE]
+end
diff --git a/acceptance/tests/ssl/autosign_command.rb b/acceptance/tests/ssl/autosign_command.rb
index a06f351b9..54c6a969e 100644
--- a/acceptance/tests/ssl/autosign_command.rb
+++ b/acceptance/tests/ssl/autosign_command.rb
@@ -1,131 +1,129 @@
require 'puppet/acceptance/common_utils'
extend Puppet::Acceptance::CAUtils
test_name "autosign command and csr attributes behavior (#7243,#7244)" do
def assert_key_generated(name)
assert_match(/Creating a new SSL key for #{name}/, stdout, "Expected agent to create a new SSL key for autosigning")
end
testdirs = {}
step "generate tmp dirs on all hosts" do
hosts.each { |host| testdirs[host] = host.tmpdir('autosign_command') }
end
teardown do
step "Remove autosign configuration"
testdirs.each do |host,testdir|
on(host, host_command("rm -rf '#{testdir}'") )
end
reset_agent_ssl
end
hostname = master.execute('facter hostname')
fqdn = master.execute('facter fqdn')
reset_agent_ssl(false)
step "Step 1: ensure autosign command can approve CSRs" do
master_opts = {
'master' => {
'autosign' => '/bin/true',
'dns_alt_names' => "puppet,#{hostname},#{fqdn}",
}
}
with_puppet_running_on(master, master_opts) do
agents.each do |agent|
next if agent == master
on(agent, puppet("agent --test --server #{master} --waitforcert 0 --certname #{agent}-autosign"))
assert_key_generated(agent)
assert_match(/Caching certificate for #{agent}/, stdout, "Expected certificate to be autosigned")
end
end
end
reset_agent_ssl(false)
step "Step 2: ensure autosign command can reject CSRs" do
master_opts = {
'master' => {
'autosign' => '/bin/false',
'dns_alt_names' => "puppet,#{hostname},#{fqdn}",
}
}
with_puppet_running_on(master, master_opts) do
agents.each do |agent|
next if agent == master
on(agent, puppet("agent --test --server #{master} --waitforcert 0 --certname #{agent}-reject"), :acceptable_exit_codes => [1])
assert_key_generated(agent)
assert_match(/no certificate found/, stdout, "Expected certificate to not be autosigned")
end
end
end
autosign_inspect_csr_path = "#{testdirs[master]}/autosign_inspect_csr.rb"
step "Step 3: setup an autosign command that inspects CSR attributes" do
autosign_inspect_csr = <<-END
#!/usr/bin/env ruby
require 'openssl'
def unwrap_attr(attr)
set = attr.value
str = set.value.first
str.value
end
csr_text = STDIN.read
csr = OpenSSL::X509::Request.new(csr_text)
passphrase = csr.attributes.find { |a| a.oid == '1.3.6.1.4.1.34380.2.1' }
# And here we jump hoops to unwrap ASN1's Attr Set Str
if unwrap_attr(passphrase) == 'my passphrase'
exit 0
end
exit 1
END
create_remote_file(master, autosign_inspect_csr_path, autosign_inspect_csr)
on master, "chmod 777 #{testdirs[master]}"
on master, "chmod 777 #{autosign_inspect_csr_path}"
end
agent_csr_attributes = {}
step "Step 4: create attributes for inclusion on csr on agents" do
csr_attributes = <<-END
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
END
agents.each do |agent|
-# next if agent == master
-
agent_csr_attributes[agent] = "#{testdirs[agent]}/csr_attributes.yaml"
create_remote_file(agent, agent_csr_attributes[agent], csr_attributes)
end
end
reset_agent_ssl(false)
step "Step 5: successfully obtain a cert" do
master_opts = {
'master' => {
'autosign' => autosign_inspect_csr_path,
'dns_alt_names' => "puppet,#{hostname},#{fqdn}",
},
:__commandline_args__ => '--debug --trace',
}
with_puppet_running_on(master, master_opts) do
agents.each do |agent|
next if agent == master
step "attempting to obtain cert for #{agent}"
on(agent, puppet("agent --test --server #{master} --waitforcert 0 --csr_attributes '#{agent_csr_attributes[agent]}' --certname #{agent}-attrs"), :acceptable_exit_codes => [0])
assert_key_generated(agent)
end
end
end
end
diff --git a/acceptance/tests/ssl/certificate_extensions.rb b/acceptance/tests/ssl/certificate_extensions.rb
new file mode 100644
index 000000000..bb96d134f
--- /dev/null
+++ b/acceptance/tests/ssl/certificate_extensions.rb
@@ -0,0 +1,74 @@
+require 'puppet/acceptance/common_utils'
+require 'puppet/acceptance/temp_file_utils'
+extend Puppet::Acceptance::CAUtils
+extend Puppet::Acceptance::TempFileUtils
+
+initialize_temp_dirs
+
+test_name "certificate extensions available as trusted data" do
+ teardown do
+ remove_temp_dirs
+ reset_agent_ssl
+ end
+
+ hostname = master.execute('facter hostname')
+ fqdn = master.execute('facter fqdn')
+ site_pp = get_test_file_path(master, "site.pp")
+ master_config = {
+ 'master' => {
+ 'autosign' => '/bin/true',
+ 'dns_alt_names' => "puppet,#{hostname},#{fqdn}",
+ 'manifest' => site_pp,
+ 'trusted_node_data' => true,
+ }
+ }
+
+ csr_attributes = YAML.dump({
+ 'extension_requests' => {
+ # registered puppet extensions
+ 'pp_uuid' => 'b5e63090-5167-11e3-8f96-0800200c9a66',
+ 'pp_instance_id' => 'i-3fkva',
+ # private (arbitrary) extensions
+ '1.3.6.1.4.1.34380.1.2.1' => 'db-server', # node role
+ '1.3.6.1.4.1.34380.1.2.2' => 'webops' # node group
+ }
+ })
+
+ create_test_file(master, "site.pp", <<-SITE)
+ file { "$test_dir/trusted.yaml":
+ ensure => file,
+ content => inline_template("<%= YAML.dump(@trusted) %>")
+ }
+ SITE
+
+ reset_agent_ssl(false)
+ with_puppet_running_on(master, master_config) do
+ agents.each do |agent|
+ next if agent == master
+
+ agent_csr_attributes = get_test_file_path(agent, "csr_attributes.yaml")
+ create_remote_file(agent, agent_csr_attributes, csr_attributes)
+
+ on(agent, puppet("agent", "--test",
+ "--server", master,
+ "--waitforcert", 0,
+ "--csr_attributes", agent_csr_attributes,
+ 'ENV' => { "FACTER_test_dir" => get_test_file_path(agent, "") }),
+ :acceptable_exit_codes => [0, 2])
+
+ trusted_data = YAML.load(on(agent, "cat #{get_test_file_path(agent, 'trusted.yaml')}").stdout)
+
+ assert_equal({
+ 'authenticated' => 'remote',
+ 'certname' => fact_on(agent, 'fqdn'),
+ 'extensions' => {
+ 'pp_uuid' => 'b5e63090-5167-11e3-8f96-0800200c9a66',
+ 'pp_instance_id' => 'i-3fkva',
+ '1.3.6.1.4.1.34380.1.2.1' => 'db-server',
+ '1.3.6.1.4.1.34380.1.2.2' => 'webops'
+ }
+ },
+ trusted_data)
+ end
+ end
+end
diff --git a/acceptance/tests/ssl/should_connect_github.rb b/acceptance/tests/ssl/should_connect_github.rb
deleted file mode 100644
index ef8c154ae..000000000
--- a/acceptance/tests/ssl/should_connect_github.rb
+++ /dev/null
@@ -1,8 +0,0 @@
-test_name "puppet should be able to authenticate the forge"
-
-# See #11276
-confine :except, :platform => 'windows'
-
-agents.each do |agent|
- on(agent, puppet("module search stdlib"))
-end
diff --git a/acceptance/tests/store_configs/enc_provides_node_when_storeconfigs_enabled.rb b/acceptance/tests/store_configs/enc_provides_node_when_storeconfigs_enabled.rb
index 7bd992e98..849fee867 100644
--- a/acceptance/tests/store_configs/enc_provides_node_when_storeconfigs_enabled.rb
+++ b/acceptance/tests/store_configs/enc_provides_node_when_storeconfigs_enabled.rb
@@ -1,94 +1,113 @@
test_name "ENC node information is used when store configs enabled (#16698)"
confine :except, :platform => 'solaris'
confine :except, :platform => 'windows'
confine :except, :platform => 'el-6'
+confine :except, :platform => 'el-7'
confine :except, :platform => 'lucid'
confine :except, :platform => 'sles-11'
testdir = master.tmpdir('use_enc')
create_remote_file master, "#{testdir}/enc.rb", <<END
#!#{master['puppetbindir']}/ruby
require 'yaml'
puts({
'classes' => [],
'parameters' => {
'data' => 'data from enc'
},
}.to_yaml)
END
on master, "chmod 755 #{testdir}/enc.rb"
create_remote_file(master, "#{testdir}/site.pp", 'notify { $data: }')
on master, "chown -R #{master['user']}:#{master['group']} #{testdir}"
on master, "chmod -R g+rwX #{testdir}"
create_remote_file master, "#{testdir}/setup.pp", <<END
$active_record_version = $osfamily ? {
RedHat => $lsbmajdistrelease ? {
5 => '2.2.3',
- default => '3.0.20',
+ default => '3.2.16',
},
- default => '3.0.20',
+ default => '3.2.16',
}
package {
rubygems:
ensure => present;
activerecord:
ensure => $active_record_version,
provider => 'gem',
- require => Package[rubygems]
+ require => Package[rubygems];
}
if $osfamily == "Debian" {
package {
+ # This is the deb sqlite3 package
sqlite3:
ensure => present;
- libsqlite3-ruby:
+ libsqlite3-dev:
ensure => present,
- require => Package[sqlite3]
+ require => Package[sqlite3];
+
}
} elsif $osfamily == "RedHat" {
$sqlite_gem_pkg_name = $operatingsystem ? {
- Fedora => "rubygem-sqlite3",
+ "Fedora" => "rubygem-sqlite3",
default => "rubygem-sqlite3-ruby"
}
package {
sqlite:
ensure => present;
$sqlite_gem_pkg_name:
ensure => present,
require => Package[sqlite]
}
} else {
fail "Unknown OS $osfamily"
}
END
+# This is a brute force hack around PUP-1073 because the deb for the core
+# sqlite3 package and the rubygem for the sqlite3 driver are both named
+# 'sqlite3'. So we just run a second puppet apply.
+create_remote_file master, "#{testdir}/setup_sqlite_gem.pp", <<END
+if $osfamily == "Debian" {
+ package {
+ # This is the rubygem sqlite3 driver
+ sqlite3-gem:
+ name => 'sqlite3',
+ ensure => present,
+ provider => 'gem',
+ }
+}
+END
+
on master, puppet_apply("#{testdir}/setup.pp")
+on master, puppet_apply("#{testdir}/setup_sqlite_gem.pp")
master_opts = {
'master' => {
'node_terminus' => 'exec',
'external_nodes' => "#{testdir}/enc.rb",
'storeconfigs' => true,
'dbadapter' => 'sqlite3',
'dblocation' => "#{testdir}/store_configs.sqlite3",
'manifest' => "#{testdir}/site.pp"
}
}
with_puppet_running_on master, master_opts, testdir do
agents.each do |agent|
run_agent_on(agent, "--no-daemonize --onetime --server #{master} --verbose")
assert_match(/data from enc/, stdout)
end
end
diff --git a/acceptance/tests/ticket_6857_password-disclosure-when-changing-a-users-password.rb b/acceptance/tests/ticket_6857_password-disclosure-when-changing-a-users-password.rb
index 97577c9d0..d93348d70 100644
--- a/acceptance/tests/ticket_6857_password-disclosure-when-changing-a-users-password.rb
+++ b/acceptance/tests/ticket_6857_password-disclosure-when-changing-a-users-password.rb
@@ -1,33 +1,35 @@
test_name "#6857: redact password hashes when applying in noop mode"
hosts_to_test = agents.reject do |agent|
- if agent['platform'].match /(?:ubuntu|centos|debian|el-|fc-)/
- result = on(agent, %Q{#{agent['puppetbindir']}/ruby -e 'require "shadow" or raise'}, :silent => true)
+ if agent['platform'].match /(?:ubuntu|centos|debian|el-|fedora)/
+ result = on(agent, %Q{#{agent['puppetbindir']}/ruby -e 'require "shadow" or raise'}, :acceptable_exit_codes => [0,1])
result.exit_code != 0
else
+ # Non-linux platforms do not rely on ruby-libshadow for password management
+ # and so we don't reject them from testing
false
end
end
skip_test "No suitable hosts found" if hosts_to_test.empty?
adduser_manifest = <<MANIFEST
user { 'passwordtestuser':
ensure => 'present',
password => 'apassword',
}
MANIFEST
changepass_manifest = <<MANIFEST
user { 'passwordtestuser':
ensure => 'present',
password => 'newpassword',
noop => true,
}
MANIFEST
apply_manifest_on(hosts_to_test, adduser_manifest )
results = apply_manifest_on(hosts_to_test, changepass_manifest )
results.each do |result|
assert_match( /current_value \[old password hash redacted\], should be \[new password hash redacted\]/ , "#{result.host}: #{result.stdout}" )
end
diff --git a/api/docs/http_api_index.md b/api/docs/http_api_index.md
index f5fcc36f3..0683ee40f 100644
--- a/api/docs/http_api_index.md
+++ b/api/docs/http_api_index.md
@@ -1,47 +1,108 @@
-Services
---------
+V1 API Services
+---------------
Puppet Agents use various network services which the Puppet Master provides in
order to manage systems. Other systems can access these services in order to
put the information that the Puppet Master has to use.
+The V1 API is all based off of dispatching to puppet's internal "indirector"
+framework. Every HTTP endpoint in V1 follows the form
+`/:environment/:indirection/:key`, where
+ * `:environment` is the name of the environment that should be in effect for
+ the request. Not all endpoints need an environment, but the path component
+ must always be specified.
+ * `:indirection` is the indirection to dispatch the request to.
+ * `:key` is the "key" portion of the indirection call.
+
+Using this API requires a significant amount of understanding of how puppet's
+internal services are structured. The following documents provide some
+specification for what is available and the ways in which they can be
+interacted with.
+
### Configuration Management Services
These services are all related to how the Puppet Agent is able to manage the
configuration of a node.
* {file:api/docs/http_catalog.md Catalog}
* {file:api/docs/http_file_bucket_file.md File Bucket File}
* {file:api/docs/http_file_content.md File Content}
* {file:api/docs/http_file_metadata.md File Metadata}
* {file:api/docs/http_report.md Report}
### Informational Services
These services all provide extra information that can be used to understand how
the Puppet Master will be providing configuration management information to
Puppet Agents.
* {file:api/docs/http_facts.md Facts}
* {file:api/docs/http_node.md Node}
* {file:api/docs/http_resource_type.md Resource Type}
* {file:api/docs/http_status.md Status}
### SSL Certificate Related Services
These services are all in support of Puppet's PKI system.
* {file:api/docs/http_certificate.md Certificate}
* {file:api/docs/http_certificate_request.md Certificate Signing Requests}
* {file:api/docs/http_certificate_status.md Certificate Status}
* {file:api/docs/http_certificate_revocation_list.md Certificate Revocation List}
+V2 HTTP API
+-----------
+
+The V2 HTTP API is accessed by prefixing requests with `/v2.0`. Authorization for
+these endpoints is still controlled with the `auth.conf` authorization system
+in puppet. When specifying the authorization of the V2 endpoints in `auth.conf`
+the `/v2.0` prefix on V2 API paths must be retained; the full request path is used.
+
+The V2 API will only accept payloads formatted as JSON and respond with JSON
+(MIME application/json).
+
+### Endpoints
+
+* {file:api/docs/http_environments.md Environments}
+
+### Error Responses
+
+All V2 API endpoints will respond to error conditions in a uniform manner and
+use standard HTTP response code to signify those errors.
+
+* When the client submits a malformed request, the API will return a 400 Bad
+ Request response.
+* When the client is not authorized, the API will return a 403 Not Authorized
+ response.
+* When the client attempts to use an HTTP method that is not permissible for
+ the endpoint, the API will return a 405 Method Not Allowed response.
+* When the client asks for a response in a format other than JSON, the API will
+ return a 406 Unacceptable response.
+* When the server encounters an unexpected error during the handling of a
+ request, it will return a 500 Server Error response.
+* When the server is unable to find an endpoint handler for the request that
+ starts with `/v2.0`, it will return a 404 Not Found response
+
+The V2 API paths are prefixed with `/v2.0` instead of `/v2` so that it is able
+to respond with 404, but not interfere with any environments in the V1 API.
+`v2` is a valid environment name, but `v2.0` is not.
+
+All error responses will contain a body, except when it is a HEAD request. The
+error responses will uniformly be a JSON object with the following properties:
+
+ * `message`: [String] A human readable message explaining the error.
+ * `issue_kind`: [String] A unique label to identify the error class.
+ * `stacktrace` (only for 5xx errors): [Array<String>] A stacktrace to where the error occurred.
+
+A {file:api/schemas/error.json JSON schema for the error objects} is also available.
+
Serialization Formats
---------------------
Puppet sends messages using several different serialization formats. Not all
REST services support all of the formats.
* {file:api/docs/pson.md PSON}
* {http://www.yaml.org/spec/1.2/spec.html YAML}
+
diff --git a/api/docs/http_certificate.md b/api/docs/http_certificate.md
index b57949155..9b60eb034 100644
--- a/api/docs/http_certificate.md
+++ b/api/docs/http_certificate.md
@@ -1,108 +1,108 @@
Certificate
=============
The `certificate` endpoint returns the certificate for the specified name,
which might be either a standard certname or `ca`.
Find
----
Get a certificate.
GET /:environment/certificate/:nodename
### Supported HTTP Methods
GET
### Supported Response Formats
s (denotes a string of text)
The returned certificate is always in the `.pem` format.
### Parameters
None
### Notes
The environment field is ignored.
### Responses
#### Certificate found
GET /env/certificate/elmo.mydomain.com
HTTP 200 OK
Content-Type: text/plain
-----BEGIN CERTIFICATE-----
MIIFujCCA6KgAwIBAgIBATANBgkqhkiG9w0BAQsFADBiMWAwXgYDVQQDDFdQdXBw
ZXQgQ0EgZ2VuZXJhdGVkIG9uIGRoY3A1MC5reWxvLmJhY2tsaW5lLnB1cHBldGxh
YnMubmV0IGF0IDIwMTMtMDYtMjQgMTY6MzA6MTcgLTA3MDAwHhcNMTMwNjIzMjMz
MDE5WhcNMTgwNjIzMjMzMDE5WjBiMWAwXgYDVQQDDFdQdXBwZXQgQ0EgZ2VuZXJh
dGVkIG9uIGRoY3A1MC5reWxvLmJhY2tsaW5lLnB1cHBldGxhYnMubmV0IGF0IDIw
MTMtMDYtMjQgMTY6MzA6MTcgLTA3MDAwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAw
ggIKAoICAQDABq1lmzccjuRmnCdXvTmdeXJGb9S8r8+I+G6fkHTa1WKDSob9PZpS
eXJtanbl0zNws9yBt1Dko2zhKDKctBRWf5CT42nDxBZPY7SaD7KaCzb07g9wfWgU
BOb/6smyl/iySEmQzzFLRgZbo5A9WLiy/UdyQim1faakevRme2Xi/l/i0TKbpu27
DhCS+E8aC8Bvaj0ph0T+TzYphTR76pP5Kps6G7Jyk/HFYrVXnY44X2PEt2mgkEXp
xHCbU+qCFMtTLMG+ZArA/noM3I/O6W5LhLSzApjut/M7UdMlpZ45PGDrsvf2R306
NcOh+zbbkhxuIaGqaxeaenYzbOlA3gXhZvYaV6EKjXNtm7BslpsvhLi0U+CWyb3C
qRkpex0MgxJgxoqViJ4TDVA+EmztOnK86+G4HGeJqTPQloYO/Td1wMT1Txh9T5Ue
Wctw/g+4o22EyJQRo+vxxzHNRIfe7EHAerMUtLT5u9MJeQb9N1iUR2ATNAN+QiB2
KEqyc9eMapK6QUZFV23Xvbdup1WCrgsWXBqyRWKV7x0sc9Wv8RMRKEFYaBeHEVXU
m0hGgF34Z8Rzphq2H1FjkLD+xbtGOjrA1Mb2De81Hfvrf18497X5UMPtsuzOt/XU
PHbbSCy+05J7VNZ/gaiGqgpHfcG5yiqCdj1LIzhFuuvm+fADPxK38wIDAQABo3sw
eTA3BglghkgBhvhCAQ0EKhYoUHVwcGV0IFJ1YnkvT3BlblNTTCBJbnRlcm5hbCBD
ZXJ0aWZpY2F0ZTAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAdBgNV
HQ4EFgQUEhn/MqSDtuxg12klWosCGenxf1cwDQYJKoZIhvcNAQELBQADggIBAH1G
L3FG/keKlGqs70PxxvR1wCo4VM3K/C+5uxnzm1MHEAd96nhtwE6YSkUe+XgDiXfC
+NXS2C4TeTQAEo6grREapWDjhJvrhrgqTZmb4lTKzb91II3/VGYzG5UXxID262zy
QLoX/IBN/xDJ5ds0wF2adUbnHUssEGGljgngewH/7kjeW/L5iL+USXZnKHPSggjM
RAEjlucE/rDqDNoxhOS4K2PjseFm7krW4cZ0gNmxdrhc7OhmJ56dH92F4M9jn7Qy
EqxWB304U/aMcO3NJxTQc7AreL/pUtjtI6hxM4miHbjSh6RfNBqhzRyJvxA6gc6g
m3kumdw04KZFSs/6fPFFbI60i5K+vioB4CnUWpj+3Z+OnDEvhQJEACR1JC8A67Ih
x+GDlbHLU1BWonwZzSMJz+ABXV3dwIrOSFHI0UmDXg+cIdZ+SaL93qMjUVU4v9nu
gR9yJGMqNuzLjgfbD/KGCEEAITKBwPvCVd//OMlWVrXr7vvt+yo6STIlTJxABJDp
CSLyHUtT++CsPXsPADxgRctpIbh1eMFEivkK9Oy+W/CZYIZnARVysUpMWg7TkXqx
mSCXy9ZXLWqU/ssVhbLS9vFVa5pvxcyfiRpsFg0XZsx8mnZP6OaWcL8FjF+/NwNP
tg1+DuYTn+d54OHi/GZEnvutgrDZyrJDrrb/Czm9
-----END CERTIFICATE-----
#### Certificate not found
GET /env/certificate/certificate_does_not_exist
- HTTP 404 Not Found: Could not find certificate certificate_does_not_exist
+ HTTP 404 Not Found
Content-Type: text/plain
Not Found: Could not find certificate certificate_does_not_exist
#### No Certificate name given
GET /env/certificate/
- HTTP/1.1 400 No request key specified in /env/certificate/
+ HTTP/1.1 400 Bad Request
Content-Type: text/plain
No request key specified in /env/certificate/
#### Master is not a CA
GET /env/certificate/valid_certificate
- HTTP/1.1 400 this master is not a CA
+ HTTP/1.1 400 Bad Request
Content-Type: text/plain
this master is not a CA
Schema
------
A `certificate` response body is not structured data according to any standard scheme such as
json/pson/yaml, so no schema is applicable.
diff --git a/api/docs/http_certificate_request.md b/api/docs/http_certificate_request.md
index 3b3b500fb..f84883169 100644
--- a/api/docs/http_certificate_request.md
+++ b/api/docs/http_certificate_request.md
@@ -1,165 +1,165 @@
Certificate Request
=============
The `certificate_request` endpoint submits a Certificate Signing Request (CSR)
to the master. The master must be configured to be a CA. The returned
CSR is always in the `.pem` format.
In all requests the `:environment` must be given, but it has no bearing on the request. CSRs are not managed within environments, all CSRs are global.
Find
----
Get a submitted CSR
GET /:environment/certificate_request/:nodename
Accept: s
Save
----
Submit a CSR
PUT /:environment/certificate_request/:nodename
Content-Type: text/plain
Note: The `:nodename` must match the Common Name on the submitted CSR.
Note: Although the `Content-Type` is sent as `text/plain` the content is
specifically a CSR in PEM format.
Search
----
List submitted CSRs
GET /:environment/certificate_requests/:ignored_pattern
Accept: s
The `:ignored_pattern` parameter is not used, but must still be provided.
Destroy
----
Delete a submitted CSR
DELETE /:environment/certificate_request/:nodename
Accept: s
### Supported HTTP Methods
The default configuration only allows requests that result in a Find and a
Save. You need to modify auth.conf in order to allow clients to use Search and
Destroy actions. It is not recommended that you change the default settings.
GET, PUT, DELETE
### Supported Response Formats
s (denotes a string of text)
The returned CSR is always in the `.pem` format.
### Parameters
None
### Examples
#### CSR found
GET /env/certificate_request/agency
HTTP/1.1 200 OK
Content-Type: text/plain
-----BEGIN CERTIFICATE REQUEST-----
MIIBnzCCAQwCAQAwYzELMAkGA1UEBhMCVUsxDzANBgNVBAgTBkxvbmRvbjEPMA0G
A1UEBxMGTG9uZG9uMSEwHwYDVQQKExhJbnRlcm5ldCBXaWRnaXRzIFB0eSBMdGQx
DzANBgNVBAMTBmFnZW5jeTCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEAxSCr
FKUKjVGFPuQ0iGM9mZKw94sOIgGohqrHH743kPvjsId3d38Qk+H+1DbVf42bQY0W
kAVcwNDqmBnx0lOtQ0oeGnbbwlJFjhqXr8jFEljPrc9S2/IIILDf/FeYWw9lRiOV
LoU6ZfCIBfq6v4D4KX3utRbOoELNyBeT6VA1ufMCAwEAAaAAMAkGBSsOAwIPBQAD
gYEAno7O1jkR56TNMe1Cw/eyQUIaniG22+0kmoftjlcMYZ/IKCOz+HRgnDtBPf8j
O5nt0PQN8YClW7Xx2U8ZTvBXn/UEKMtCBkbF+SULiayxPgfyKy/axinfutEChnHS
ZtUMUBLlh+gGFqOuH69979SJ2QmQC6FNomTkYI7FOHD/TG0=
-----END CERTIFICATE REQUEST-----
#### CSR not found
GET /env/certificate_request/does_not_exist
- HTTP/1.1 404 Not Found: Could not find certificate_request does_not_exist
+ HTTP/1.1 404 Not Found
Content-Type: text/plain
Not Found: Could not find certificate_request does_not_exist
#### No node name given
GET /env/certificate_request/
- HTTP/1.1 400 No request key specified in /env/certificate_request/
+ HTTP/1.1 400 Bad Request
Content-Type: text/plain
No request key specified in /env/certificate_request/
#### Delete a CSR that exists
DELETE /production/certificate_request/agency
Accept: s
HTTP/1.1 200 OK
Content-Type: text/plain
1
#### Delete a CSR that does not exists
DELETE /production/certificate_request/missing
Accept: s
HTTP/1.1 200 OK
Content-Type: text/plain
false
#### Retrieve all CSRs
GET /production/certificate_requests/ignored
Accept: s
HTTP/1.1 200 OK
Content-Type: text/plain
-----BEGIN CERTIFICATE REQUEST-----
MIIBnzCCAQwCAQAwYzELMAkGA1UEBhMCVUsxDzANBgNVBAgTBkxvbmRvbjEPMA0G
A1UEBxMGTG9uZG9uMSEwHwYDVQQKExhJbnRlcm5ldCBXaWRnaXRzIFB0eSBMdGQx
DzANBgNVBAMTBmFnZW5jeTCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEAxSCr
FKUKjVGFPuQ0iGM9mZKw94sOIgGohqrHH743kPvjsId3d38Qk+H+1DbVf42bQY0W
kAVcwNDqmBnx0lOtQ0oeGnbbwlJFjhqXr8jFEljPrc9S2/IIILDf/FeYWw9lRiOV
LoU6ZfCIBfq6v4D4KX3utRbOoELNyBeT6VA1ufMCAwEAAaAAMAkGBSsOAwIPBQAD
gYEAno7O1jkR56TNMe1Cw/eyQUIaniG22+0kmoftjlcMYZ/IKCOz+HRgnDtBPf8j
O5nt0PQN8YClW7Xx2U8ZTvBXn/UEKMtCBkbF+SULiayxPgfyKy/axinfutEChnHS
ZtUMUBLlh+gGFqOuH69979SJ2QmQC6FNomTkYI7FOHD/TG0=
-----END CERTIFICATE REQUEST-----
---
-----BEGIN CERTIFICATE REQUEST-----
MIIBnjCCAQsCAQAwYjELMAkGA1UEBhMCVUsxDzANBgNVBAgTBkxvbmRvbjEPMA0G
A1UEBxMGTG9uZG9uMSEwHwYDVQQKExhJbnRlcm5ldCBXaWRnaXRzIFB0eSBMdGQx
DjAMBgNVBAMTBWFnZW50MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQC1tucK
enT1CkDPgsCU/0e2cbzRsiKF8yHH7+ntF6Q3d9ZCaZWJ00mj0+YmiYrnum+KAikE
45Iaf9vaUV3CPsDVrUPOI8kYehiv868ZhP3nxblE6iuNBK+Fdv9GN/vKQrmL5iRE
bIrOM3/lxpS7SpidGdA6EIVlS3604bwLY4xHNQIDAQABoAAwCQYFKw4DAg8FAAOB
gQAXH0YFuidPqB6P2MyPEEGZ3rzozINBx/oXvGptXI60Zy5mgH6iAkrZfi57pEzP
jFoO2JRaFxTJC1FVpc4zR1K6sq4h3fIMwqppJRX+5wJNKyhU61eY2gR2O/rAJzw4
wcUKf9JhoE7/p1cUulIIIq7t/ibCvf0LYSFwGqTwGqN2TQ==
-----END CERTIFICATE REQUEST-----
The CSR PEMs are separated by "\n---\n"
Schema
------
A `certificate_request` response body is not structured data according to any
standard scheme such as json/pson/yaml, so no schema is applicable.
diff --git a/api/docs/http_certificate_revocation_list.md b/api/docs/http_certificate_revocation_list.md
index 0fcf481fb..fa7468be4 100644
--- a/api/docs/http_certificate_revocation_list.md
+++ b/api/docs/http_certificate_revocation_list.md
@@ -1,187 +1,187 @@
Certificate Revocation List
===========================
The `certificate_revocation_list` endpoint retrieves a Certificate Revocation List (CRL)
from the master. The master must be configured to be a CA. The returned
CRL is always in the `.pem` format.
In all requests the `:environment` and `:nodename` must be given, but neither has any bearing on the request.
Find
----
Get the submitted CRL
GET /:environment/certificate_revocation_list/:nodename
Accept: s
### Supported HTTP Methods
GET
### Supported Response Formats
s (denotes a string of text)
The returned CRL is always in the `.pem` format.
### Parameters
None
### Examples
Since the returned CRL always looks similar to the human eye, the successful examples are each followed by an openssl
decoding of the CRL PEM file.
#### Empty revocation list
GET /env/certificate_revocation_list/ca
HTTP/1.1 200 OK
Content-Type: text/plain
-----BEGIN X509 CRL-----
MIICdzBhAgEBMA0GCSqGSIb3DQEBBQUAMB8xHTAbBgNVBAMMFFB1cHBldCBDQTog
bG9jYWxob3N0Fw0xMzA3MTYyMDQ4NDJaFw0xODA3MTUyMDQ4NDNaoA4wDDAKBgNV
HRQEAwIBADANBgkqhkiG9w0BAQUFAAOCAgEAqyBJOy3dtCOcrb0Fu7ZOOiDQnarg
IzXUV/ug1dauPEVyURLNNr+CJrr89QZnU/71lqgpWTN/J47mO/lffMSPjmINE+ng
XzOffm0qCG2+gNyaOBOdEmQTLdHPIXvcm7T+wEqc7XFW2tjEdpEubZgweruU/+DB
RX6/PhFbalQ0bKcMeFLzLAD4mmtBaQCJISmUUFWx1pyCS6pgBtQ1bNy3PJPN2PNW
YpDf3DNZ16vrAJ4a4SzXLXCoONw0MGxZcS6/hctJ75Vz+dTMrArKwckytWgQS/5e
c/1/wlMZn4xlho+EcIPMPfCB5hW1qzGU2WjUakTVxzF4goamnfFuKbHKEoXVOo9C
3dEQ9un4Uyd1xHxj8WvQck79In5/S2l9hdqp4eud4BaYB6tNRKxlUntSCvCNriR2
wrDNsMuQ5+KJReG51vM0OzzKmlScgIHaqbVeNFZI9X6TpsO2bLEZX2xyqKw4xrre
OIEZRoJrmX3VQ/4u9hj14Qbt72/khYo6z/Fckc5zVD+dW4fjP2ztVTSPzBqIK3+H
zAgewYW6cJ6Aan8GSl3IfRqj6WlOubWj8Gr1U0dOE7SkBX6w/X61uqsHrOyg/E/Z
0Wcz/V+W5iZxa4Spm0x4sfpNzf/bNmjTe4M2MXyn/hXx5MdHf/HZdhOs/lzwKUGL
kEwcy38d6hYtUjs=
-----END X509 CRL-----
> openssl crl -inform PEM -in empty.crl -text -noout
Certificate Revocation List (CRL):
Version 2 (0x1)
Signature Algorithm: sha1WithRSAEncryption
Issuer: /CN=Puppet CA: localhost
Last Update: Jul 16 20:48:42 2013 GMT
Next Update: Jul 15 20:48:43 2018 GMT
CRL extensions:
X509v3 CRL Number:
0
No Revoked Certificates.
Signature Algorithm: sha1WithRSAEncryption
ab:20:49:3b:2d:dd:b4:23:9c:ad:bd:05:bb:b6:4e:3a:20:d0:
9d:aa:e0:23:35:d4:57:fb:a0:d5:d6:ae:3c:45:72:51:12:cd:
36:bf:82:26:ba:fc:f5:06:67:53:fe:f5:96:a8:29:59:33:7f:
27:8e:e6:3b:f9:5f:7c:c4:8f:8e:62:0d:13:e9:e0:5f:33:9f:
7e:6d:2a:08:6d:be:80:dc:9a:38:13:9d:12:64:13:2d:d1:cf:
21:7b:dc:9b:b4:fe:c0:4a:9c:ed:71:56:da:d8:c4:76:91:2e:
6d:98:30:7a:bb:94:ff:e0:c1:45:7e:bf:3e:11:5b:6a:54:34:
6c:a7:0c:78:52:f3:2c:00:f8:9a:6b:41:69:00:89:21:29:94:
50:55:b1:d6:9c:82:4b:aa:60:06:d4:35:6c:dc:b7:3c:93:cd:
d8:f3:56:62:90:df:dc:33:59:d7:ab:eb:00:9e:1a:e1:2c:d7:
2d:70:a8:38:dc:34:30:6c:59:71:2e:bf:85:cb:49:ef:95:73:
f9:d4:cc:ac:0a:ca:c1:c9:32:b5:68:10:4b:fe:5e:73:fd:7f:
c2:53:19:9f:8c:65:86:8f:84:70:83:cc:3d:f0:81:e6:15:b5:
ab:31:94:d9:68:d4:6a:44:d5:c7:31:78:82:86:a6:9d:f1:6e:
29:b1:ca:12:85:d5:3a:8f:42:dd:d1:10:f6:e9:f8:53:27:75:
c4:7c:63:f1:6b:d0:72:4e:fd:22:7e:7f:4b:69:7d:85:da:a9:
e1:eb:9d:e0:16:98:07:ab:4d:44:ac:65:52:7b:52:0a:f0:8d:
ae:24:76:c2:b0:cd:b0:cb:90:e7:e2:89:45:e1:b9:d6:f3:34:
3b:3c:ca:9a:54:9c:80:81:da:a9:b5:5e:34:56:48:f5:7e:93:
a6:c3:b6:6c:b1:19:5f:6c:72:a8:ac:38:c6:ba:de:38:81:19:
46:82:6b:99:7d:d5:43:fe:2e:f6:18:f5:e1:06:ed:ef:6f:e4:
85:8a:3a:cf:f1:5c:91:ce:73:54:3f:9d:5b:87:e3:3f:6c:ed:
55:34:8f:cc:1a:88:2b:7f:87:cc:08:1e:c1:85:ba:70:9e:80:
6a:7f:06:4a:5d:c8:7d:1a:a3:e9:69:4e:b9:b5:a3:f0:6a:f5:
53:47:4e:13:b4:a4:05:7e:b0:fd:7e:b5:ba:ab:07:ac:ec:a0:
fc:4f:d9:d1:67:33:fd:5f:96:e6:26:71:6b:84:a9:9b:4c:78:
b1:fa:4d:cd:ff:db:36:68:d3:7b:83:36:31:7c:a7:fe:15:f1:
e4:c7:47:7f:f1:d9:76:13:ac:fe:5c:f0:29:41:8b:90:4c:1c:
cb:7f:1d:ea:16:2d:52:3b
#### One-item revocation list
GET /env/certificate_revocation_list/ca
HTTP/1.1 200 OK
Content-Type: text/plain
-----BEGIN X509 CRL-----
MIICnDCBhQIBATANBgkqhkiG9w0BAQUFADAfMR0wGwYDVQQDDBRQdXBwZXQgQ0E6
IGxvY2FsaG9zdBcNMTMxMDA3MTk0ODQwWhcNMTgxMDA2MTk0ODQxWjAiMCACAQUX
DTEzMTAwNzE5NDg0MVowDDAKBgNVHRUEAwoBAaAOMAwwCgYDVR0UBAMCAQEwDQYJ
KoZIhvcNAQEFBQADggIBALrh49WNdmrJOPCRntD1nxCObmqZgl8ZwTv7TO9VkmCG
Ksvo8zR2aTIOH9VUKqWrE0squhtFJXl8dxL4PR1RiLbmhO7dp+NHdu8ejTQpoOTp
h69xbQFT3oHcIdn2cBGrLJQcZgXsiswT0KJ8nuw6eDO93yXDrguSUdou99M99wTw
2nn1kUQKW9b0vUI7t2ADF5U8/DES+1IrvBq2IEHmg4+ekZRCxeJMuqd1R13gymcJ
osSPbRgIjCli6zD3aK4Nq5OMMpVLV/VVPwyQb4GwW4Wj5iyNAp8d/EAqtZ21ZHUi
nvuXmRtUWHJwfi40D5T2GQXxuUjB4pnh8cFq7f89iUvqoCwFo7nRIacrrweNFMYD
GxVJVMfz4PkP66ckIPQ5Uuey92dg5p2w4b2cp8NstxMdgcc3KAF483ItKA8uIDuU
1dbzw1v2k5qUjoImueHwKolbLmPyYmvFp7hbnV+WpFbvGjyIfW3BMankDEv4ig0L
MCw6n2GKv1hSWM6Mrk8Ja1yYOFLsjI0RoVCZsf1iNiRT28haldXVTPyNtct9mGAv
6az5W/nyixIPrrHubTx28zhmuHZx6y3hQMCLmuYOT+e7F/eFsYXVEjuJjxjr33uA
O/ii4EkTls1gzvonOtoBoGElzQAogrZI3HXCwFYvU2whLKr9cwv5bpRkUfPCMQ4n
-----END X509 CRL-----
> openssl crl -inform PEM -in 1revoked.crl -text -noout
Certificate Revocation List (CRL):
Version 2 (0x1)
Signature Algorithm: sha1WithRSAEncryption
Issuer: /CN=Puppet CA: localhost
Last Update: Oct 7 19:48:40 2013 GMT
Next Update: Oct 6 19:48:41 2018 GMT
CRL extensions:
X509v3 CRL Number:
1
Revoked Certificates:
Serial Number: 05
Revocation Date: Oct 7 19:48:41 2013 GMT
CRL entry extensions:
X509v3 CRL Reason Code:
Key Compromise
Signature Algorithm: sha1WithRSAEncryption
ba:e1:e3:d5:8d:76:6a:c9:38:f0:91:9e:d0:f5:9f:10:8e:6e:
6a:99:82:5f:19:c1:3b:fb:4c:ef:55:92:60:86:2a:cb:e8:f3:
34:76:69:32:0e:1f:d5:54:2a:a5:ab:13:4b:2a:ba:1b:45:25:
79:7c:77:12:f8:3d:1d:51:88:b6:e6:84:ee:dd:a7:e3:47:76:
ef:1e:8d:34:29:a0:e4:e9:87:af:71:6d:01:53:de:81:dc:21:
d9:f6:70:11:ab:2c:94:1c:66:05:ec:8a:cc:13:d0:a2:7c:9e:
ec:3a:78:33:bd:df:25:c3:ae:0b:92:51:da:2e:f7:d3:3d:f7:
04:f0:da:79:f5:91:44:0a:5b:d6:f4:bd:42:3b:b7:60:03:17:
95:3c:fc:31:12:fb:52:2b:bc:1a:b6:20:41:e6:83:8f:9e:91:
94:42:c5:e2:4c:ba:a7:75:47:5d:e0:ca:67:09:a2:c4:8f:6d:
18:08:8c:29:62:eb:30:f7:68:ae:0d:ab:93:8c:32:95:4b:57:
f5:55:3f:0c:90:6f:81:b0:5b:85:a3:e6:2c:8d:02:9f:1d:fc:
40:2a:b5:9d:b5:64:75:22:9e:fb:97:99:1b:54:58:72:70:7e:
2e:34:0f:94:f6:19:05:f1:b9:48:c1:e2:99:e1:f1:c1:6a:ed:
ff:3d:89:4b:ea:a0:2c:05:a3:b9:d1:21:a7:2b:af:07:8d:14:
c6:03:1b:15:49:54:c7:f3:e0:f9:0f:eb:a7:24:20:f4:39:52:
e7:b2:f7:67:60:e6:9d:b0:e1:bd:9c:a7:c3:6c:b7:13:1d:81:
c7:37:28:01:78:f3:72:2d:28:0f:2e:20:3b:94:d5:d6:f3:c3:
5b:f6:93:9a:94:8e:82:26:b9:e1:f0:2a:89:5b:2e:63:f2:62:
6b:c5:a7:b8:5b:9d:5f:96:a4:56:ef:1a:3c:88:7d:6d:c1:31:
a9:e4:0c:4b:f8:8a:0d:0b:30:2c:3a:9f:61:8a:bf:58:52:58:
ce:8c:ae:4f:09:6b:5c:98:38:52:ec:8c:8d:11:a1:50:99:b1:
fd:62:36:24:53:db:c8:5a:95:d5:d5:4c:fc:8d:b5:cb:7d:98:
60:2f:e9:ac:f9:5b:f9:f2:8b:12:0f:ae:b1:ee:6d:3c:76:f3:
38:66:b8:76:71:eb:2d:e1:40:c0:8b:9a:e6:0e:4f:e7:bb:17:
f7:85:b1:85:d5:12:3b:89:8f:18:eb:df:7b:80:3b:f8:a2:e0:
49:13:96:cd:60:ce:fa:27:3a:da:01:a0:61:25:cd:00:28:82:
b6:48:dc:75:c2:c0:56:2f:53:6c:21:2c:aa:fd:73:0b:f9:6e:
94:64:51:f3:c2:31:0e:27
#### No node name given
GET /env/certificate_revocation_list
- HTTP/1.1 400 No request key specified in /env/certificate_revocation_list
+ HTTP/1.1 400 Bad Request
Content-Type: text/plain
No request key specified in /env/certificate_revocation_list
Schema
------
A `certificate_revocation_list` response body is not structured data according to any
standard scheme such as json/pson/yaml, so no schema is applicable.
diff --git a/api/docs/http_environments.md b/api/docs/http_environments.md
new file mode 100644
index 000000000..bf7dce576
--- /dev/null
+++ b/api/docs/http_environments.md
@@ -0,0 +1,41 @@
+Environments
+============
+
+The `environments` endpoint allows for enumeration of the environments known to the master, along with the modules available in each.
+This endpoint is by default accessible to any client with a valid certificate, though this may be changed by `auth.conf`.
+
+Get
+---
+
+Get the list of known environments.
+
+ GET /v2.0/environments
+
+### Parameters
+
+None
+
+### Example Request & Response
+
+ GET /v2.0/environments
+
+ HTTP 200 OK
+ Content-Type: application/json
+
+ {
+ "search_paths": ["/etc/puppet/environments"]
+ "environments": {
+ "production": {
+ "settings": {
+ "modulepath": ["/first/module/directory", "/second/module/directory"],
+ "manifest": ["/location/of/manifests"]
+ }
+ }
+ }
+ }
+
+Schema
+------
+
+A environments response body adheres to the {file:api/schemas/environments.json
+api/schemas/environments.json} schema.
diff --git a/api/docs/http_file_bucket_file.md b/api/docs/http_file_bucket_file.md
index 7b5ade372..3adf17b43 100644
--- a/api/docs/http_file_bucket_file.md
+++ b/api/docs/http_file_bucket_file.md
@@ -1,99 +1,101 @@
File Bucket File
=============
The `file_bucket_file` endpoint manages the contents of files in the
file bucket. All access to files is managed with the md5 checksum of the
file contents, represented as `:md5`. Where used, `:filename` means the
full absolute path of the file on the client system. This is usually
optional and used as an error check to make sure correct file is
retrieved. The environment is required in all requests but ignored, as
the file bucket does not distinguish between environments.
Find
----
Retrieve the contents of a file.
GET /:environment/file_bucket_file/:md5
GET /:environment/file_bucket_file/:md5/:original_path
This will return the contents of the file if it's present. If
`:original_path` is provided then the contents will only be sent if the
file was uploaded with the same path at some point.
Head
----
Check if a file is present in the filebucket
HEAD /:environment/file_bucket_file/:md5
HEAD /:environment/file_bucket_file/:md5/:original_path
This behaves identically to find, only returning headers.
Save
----
Save a file to the filebucket
PUT /:environment/file_bucket_file/:md5
PUT /:environment/file_bucket_file/:md5/:original_path
The body should contain the file contents. This saves the file using the
md5 sum of the file contents. If `:original_path` is provided, it adds
the path to a list for the given file. If the md5 sum in the request is
incorrect, the file will be instead saved under the correct checksum.
### Supported HTTP Methods
GET, HEAD, PUT
### Supported Response Formats
s or text/plain (a string of the raw file contents)
Puppet also understands `pson` and `text/pson`, but their use is
deprecated and support will be removed in a future version.
### Parameters
None
### Examples
#### Saving a file
> PUT /production/file_bucket_file/md5/eb61eead90e3b899c6bcbe27ac581660//home/user/myfile.txt HTTP/1.1
> Content-Type: text/plain
> Content-Length: 24
> This is the file content
< HTTP/1.1 200 OK
#### Retrieving a file
> GET /production/file_bucket_file/md5/4949e56d376cc80ce5387e8e89a75396//home/user/myfile.txt HTTP/1.1
> Accept: s
< HTTP/1.1 200 OK
< Content-Length: 24
< This is the file content
#### Wrong file name
> GET /production/file_bucket_file/md5/4949e56d376cc80ce5387e8e89a75396//home/user/wrong_name HTTP/1.1
> Accept: s
- < HTTP/1.1 404 Not Found: Could not find file_bucket_file md5/4949e56d376cc80ce5387e8e89a75396/home/user/wrong_name
+ < HTTP/1.1 404 Not Found
+ <
+ < Not Found: Could not find file_bucket_file md5/4949e56d376cc80ce5387e8e89a75396/home/user/wrong_name
Schema
------
A `file_bucket_file` response body is not structured data according to any standard scheme such as
json/pson/yaml, so no schema is applicable.
diff --git a/api/docs/http_file_content.md b/api/docs/http_file_content.md
index 8720a4cec..8b13417c7 100644
--- a/api/docs/http_file_content.md
+++ b/api/docs/http_file_content.md
@@ -1,66 +1,70 @@
File Content
=============
The `file_content` endpoint returns the contents of the specified file.
Find
----
Get a file.
GET /:environment/file_content/:mount_point/:name
`:mount_point` is one of mounts configured in the `fileserver.conf`.
See [the puppet file server guide](http://docs.puppetlabs.com/guides/file_serving.html)
for more information about how mount points work.
`:name` is the path to the file within the `:mount_point` that is requested.
### Supported HTTP Methods
GET
### Supported Response Formats
raw (the raw binary content)
### Parameters
None
### Notes
### Responses
#### File found
GET /env/file_content/modules/example/my_file
Accept: raw
HTTP/1.1 200 OK
Content-Type: application/x-raw
Content-Length: 16
this is my file
#### File not found
GET /env/file_content/modules/example/not_found
Accept: raw
- HTTP/1.1 404 Not Found: Could not find file_content modules/example/not_found
+ HTTP/1.1 404 Not Found
Content-Type: text/plain
+ Not Found: Could not find file_content modules/example/not_found
+
#### No file name given
GET /env/file_content/
- HTTP/1.1 400 No request key specified in /env/file_content/
+ HTTP/1.1 400 Bad Request
Content-Type: text/plain
+ No request key specified in /env/file_content/
+
Schema
------
A `file_content` response body is not structured data according to any standard scheme such as
json/pson/yaml, so no schema is applicable.
diff --git a/api/docs/http_file_metadata.md b/api/docs/http_file_metadata.md
index 41b8077ce..e84c1fe14 100644
--- a/api/docs/http_file_metadata.md
+++ b/api/docs/http_file_metadata.md
@@ -1,434 +1,436 @@
File Metadata
=============
The `file_metadata` endpoint returns select metadata for a single file or many files. There are find and search variants
of the endpoint; the search variant has a trailing 's' so is actually `file_metadatas`.
Although the term 'file' is used generically in the endpoint name and documentation, each returned item can be one of
the following three types:
* file
* directory
* symbolic link
Note that an `:environment` must be specified in the endpoint, but is actually ignored since the puppet file server
is not environment-specific. (In fact, the specified `:environment` does even need to be valid.)
The endpoint path includes a `:mount` which can be one of three types:
* custom file serving mounts as specified in fileserver.conf -- see [the puppet file serving guide](http://docs.puppetlabs.com/guides/file_serving.html#serving-files-from-custom-mount-points)
* `modules/<module>` -- a semi-magical mount point which allows access to the `files` subdirectory of `module` -- see [the puppet file serving guide](http://docs.puppetlabs.com/guides/file_serving.html#serving-module-files)
* `plugins` -- a highly magical mount point which merges many directories together: used for plugin sync, sub-paths can not be specified, not intended for general consumption
Note: pson responses in the examples below are pretty-printed for readability.
Find
----
Get file metadata for a single file
GET /:environment/file_metadata/:mount/path/to/file
### Supported HTTP Methods
GET
### Supported Response Formats
PSON
### Parameters
None
### Example Response
#### File metadata found for a file
GET /env/file_metadata/modules/example/just_a_file.txt
HTTP/1.1 200 OK
Content-Type: text/pson
{
"data": {
"checksum": {
"type": "md5",
"value": "{md5}d0a10f45491acc8743bc5a82b228f89e"
},
"destination": null,
"group": 20,
"links": "manage",
"mode": 420,
"owner": 501,
"path": "/etc/puppet/conf/modules/example/files/just_a_file.txt",
"relative_path": null,
"type": "file"
},
"document_type": "FileMetadata",
"metadata": {
"api_version": 1
}
}
#### File metadata found for a directory
GET /env/file_metadata/modules/example/subdirectory
HTTP/1.1 200 OK
Content-Type: text/pson
{
"data": {
"checksum": {
"type": "ctime",
"value": "{ctime}2013-10-01 13:16:10 -0700"
},
"destination": null,
"group": 20,
"links": "manage",
"mode": 493,
"owner": 501,
"path": "/etc/puppet/conf/modules/example/files/subdirectory",
"relative_path": null,
"type": "directory"
},
"document_type": "FileMetadata",
"metadata": {
"api_version": 1
}
}
#### File metadata found for a link
GET /env/file_metadata/modules/example/link_to_file.txt
HTTP/1.1 200 OK
Content-Type: text/pson
{
"data": {
"checksum": {
"type": "md5",
"value": "{md5}d0a10f45491acc8743bc5a82b228f89e"
},
"destination": "/etc/puppet/conf/modules/example/files/just_a_file.txt",
"group": 20,
"links": "manage",
"mode": 493,
"owner": 501,
"path": "/etc/puppet/conf/modules/example/files/link_to_file.txt",
"relative_path": null,
"type": "link"
},
"document_type": "FileMetadata",
"metadata": {
"api_version": 1
}
}
#### File not found
GET /env/file_metadata/modules/example/does_not_exist
- HTTP/1.1 404 Not Found: Could not find file_metadata modules/example/does_not_exist
+ HTTP/1.1 404 Not Found
+
+ Not Found: Could not find file_metadata modules/example/does_not_exist
Search
------
Get a list of metadata for multiple files
GET /env/file_metadatas/foo.txt
### Supported HTTP Methods
GET
### Supported Format
Accept: pson, text/pson
### Parameters
* `recurse` -- should always be set to `yes`; unfortunately the default is `no` which causes renders this a Find operation
* `ignore` -- file or directory regex to ignore; can be repeated
* `links` -- either `manage` (default) or `follow`. See examples below.
### Example Response
#### Basic search
GET /env/file_metadatas/modules/example?recurse=yes
HTTP 200 OK
Content-Type: text/pson
[
{
"data": {
"checksum": {
"type": "ctime",
"value": "{ctime}2013-10-01 13:15:59 -0700"
},
"destination": null,
"group": 20,
"links": "manage",
"mode": 493,
"owner": 501,
"path": "/etc/puppet/conf/modules/example/files",
"relative_path": ".",
"type": "directory"
},
"document_type": "FileMetadata",
"metadata": {
"api_version": 1
}
},
{
"data": {
"checksum": {
"type": "md5",
"value": "{md5}d0a10f45491acc8743bc5a82b228f89e"
},
"destination": null,
"group": 20,
"links": "manage",
"mode": 420,
"owner": 501,
"path": "/etc/puppet/conf/modules/example/files",
"relative_path": "just_a_file.txt",
"type": "file"
},
"document_type": "FileMetadata",
"metadata": {
"api_version": 1
}
},
{
"data": {
"checksum": {
"type": "md5",
"value": "{md5}d0a10f45491acc8743bc5a82b228f89e"
},
"destination": "/etc/puppet/conf/modules/example/files/just_a_file.txt",
"group": 20,
"links": "manage",
"mode": 493,
"owner": 501,
"path": "/etc/puppet/conf/modules/example/files",
"relative_path": "link_to_file.txt",
"type": "link"
},
"document_type": "FileMetadata",
"metadata": {
"api_version": 1
}
},
{
"data": {
"checksum": {
"type": "ctime",
"value": "{ctime}2013-10-01 13:15:59 -0700"
},
"destination": null,
"group": 20,
"links": "manage",
"mode": 493,
"owner": 501,
"path": "/etc/puppet/conf/modules/example/files",
"relative_path": "subdirectory",
"type": "directory"
},
"document_type": "FileMetadata",
"metadata": {
"api_version": 1
}
},
{
"data": {
"checksum": {
"type": "md5",
"value": "{md5}d41d8cd98f00b204e9800998ecf8427e"
},
"destination": null,
"group": 20,
"links": "manage",
"mode": 420,
"owner": 501,
"path": "/etc/puppet/conf/modules/example/files",
"relative_path": "subdirectory/another_file.txt",
"type": "file"
},
"document_type": "FileMetadata",
"metadata": {
"api_version": 1
}
}
]
#### Search ignoring 'sub*' and links = manage
GET /env/file_metadatas/modules/example?recurse=true&ignore=sub*&links=manage
HTTP 200 OK
Content-Type: text/pson
[
{
"data": {
"checksum": {
"type": "ctime",
"value": "{ctime}2013-10-01 13:15:59 -0700"
},
"destination": null,
"group": 20,
"links": "manage",
"mode": 493,
"owner": 501,
"path": "/etc/puppet/conf/modules/example/files",
"relative_path": ".",
"type": "directory"
},
"document_type": "FileMetadata",
"metadata": {
"api_version": 1
}
},
{
"data": {
"checksum": {
"type": "md5",
"value": "{md5}d0a10f45491acc8743bc5a82b228f89e"
},
"destination": null,
"group": 20,
"links": "manage",
"mode": 420,
"owner": 501,
"path": "/etc/puppet/conf/modules/example/files",
"relative_path": "just_a_file.txt",
"type": "file"
},
"document_type": "FileMetadata",
"metadata": {
"api_version": 1
}
},
{
"data": {
"checksum": {
"type": "md5",
"value": "{md5}d0a10f45491acc8743bc5a82b228f89e"
},
"destination": "/etc/puppet/conf/modules/example/files/just_a_file.txt",
"group": 20,
"links": "manage",
"mode": 493,
"owner": 501,
"path": "/etc/puppet/conf/modules/example/files",
"relative_path": "link_to_file.txt",
"type": "link"
},
"document_type": "FileMetadata",
"metadata": {
"api_version": 1
}
}
]
#### Search ignoring 'sub*' and links = follow
This example is identical to the above example, except for the different links parameter. The result pson, then,
is identical to the above example, except for:
* the 'links' field is set to "follow" rather than "manage" in all metadata objects
* in the 'link_to_file.txt' metadata:
* for 'manage' the 'destination' field is the link destination; for 'follow', it's null
* for 'manage' the 'type' field is 'link'; for 'follow' it's 'file'
* for 'manage' the 'mode', 'owner' and 'group' fields are the link's values; for 'follow' the destination's values
` `
GET /env/file_metadatas/modules/example?recurse=true&ignore=sub*&links=follow
HTTP 200 OK
Content-Type: text/pson
[
{
"data": {
"checksum": {
"type": "ctime",
"value": "{ctime}2013-10-01 13:15:59 -0700"
},
"destination": null,
"group": 20,
"links": "follow",
"mode": 493,
"owner": 501,
"path": "/etc/puppet/conf/modules/example/files",
"relative_path": ".",
"type": "directory"
},
"document_type": "FileMetadata",
"metadata": {
"api_version": 1
}
},
{
"data": {
"checksum": {
"type": "md5",
"value": "{md5}d0a10f45491acc8743bc5a82b228f89e"
},
"destination": null,
"group": 20,
"links": "follow",
"mode": 420,
"owner": 501,
"path": "/etc/puppet/conf/modules/example/files",
"relative_path": "just_a_file.txt",
"type": "file"
},
"document_type": "FileMetadata",
"metadata": {
"api_version": 1
}
},
{
"data": {
"checksum": {
"type": "md5",
"value": "{md5}d0a10f45491acc8743bc5a82b228f89e"
},
"destination": null,
"group": 20,
"links": "follow",
"mode": 420,
"owner": 501,
"path": "/etc/puppet/conf/modules/example/files",
"relative_path": "link_to_file.txt",
"type": "file"
},
"document_type": "FileMetadata",
"metadata": {
"api_version": 1
}
}
]
Schema
------
The representation of file metadata conforms to the schema at {file:api/schemas/file_metadata.json api/schemas/file_metadata.json}.
Sample Module
-------------
The examples above use this (faux) module:
/etc/puppet/conf/modules/example/
files/
just_a_file.txt
link_to_file.txt -> /etc/puppet/conf/modules/example/files/just_a_file.txt
subdirectory/
another_file.txt
diff --git a/api/docs/http_resource_type.md b/api/docs/http_resource_type.md
index 6c1f6eee1..413b60cb8 100644
--- a/api/docs/http_resource_type.md
+++ b/api/docs/http_resource_type.md
@@ -1,222 +1,222 @@
Resource Type
=============
The `resource_type` and `resource_types` endpoints return information about the
following kinds of objects available to the puppet master:
* Classes (`class myclass { ... }`)
* Defined types (`define mytype ($parameter) { ... }`)
* Node definitions (`node 'web01.example.com' { ... }`)
For an object to be available to the puppet master, it must be present in the
site manifest (configured by the `manifest` setting) or in a module located in
the modulepath (configured by the `modulepath` setting; classes and defined
types only).
Note that this endpoint does **not** return information about native resource
types written in Ruby.
See the end of this page for the source manifest used to generate all example
responses.
Find
----
Get info about a specific class, defined type, or node, by name. Returns a
single resource_type response object (see "Schema" below).
GET /:environment/resource_type/:nodename
> **Note:** Although no two classes or defined types may have the same name,
> it's possible for a node definition to have the same name as a class or
> defined type. If this happens, the class or defined type will be returned
> instead of the node definition. The order in which kinds of objects are
> searched is classes, then defined types, then node definitions.
### Supported HTTP Methods
GET
### Supported Formats
PSON
### Parameters
None
### Responses
#### Resource Type Found
GET /env/resource_type/athing
HTTP 200 OK
Content-Type: text/pson
{
"line": 7,
"file": "/etc/puppet/manifests/site.pp",
"name":"athing",
"kind":"class"
}
#### Resource Type Not Found
GET /env/resource_type/resource_type_does_not_exist
- HTTP 404 Not Found: Could not find resource_type resource_type_does_not_exist
+ HTTP 404 Not Found
Content-Type: text/plain
Not Found: Could not find resource_type resource_type_does_not_exist
#### No Resource Type Name Given
GET /env/resource_type/
- HTTP/1.1 400 No request key specified in /env/resource_type/
+ HTTP/1.1 400 Bad Request
Content-Type: text/plain
No request key specified in /env/resource_type/
Search
------
List all resource types matching a regular expression. Returns an array of
resource_type response objects (see "Schema" below).
GET /:environment/resource_types/:search_string
The `search_string` is required. It must be either a Ruby regular expression or
the string `*` (which will match all resource types). Surrounding slashes are
stripped. Note that if you want to use the `?` character in a regular
expression, it must be escaped as `%3F`.
### Supported HTTP Methods
GET
### Supported Formats
Accept: pson, text/pson
### Parameters
* `kind`: Optional. Filter the returned resource types by the `kind` field.
Valid values are `class`, `node`, and `defined_type`.
### Responses
#### Search With Results
GET /env/resource_types/*
HTTP 200 OK
Content-Type: text/pson
[
{
"file": "/etc/puppet/manifests/site.pp",
"kind": "class",
"line": 7,
"name": "athing"
},
{
"doc": "An example class\n",
"file": "/etc/puppet/manifests/site.pp",
"kind": "class",
"line": 11,
"name": "bthing",
"parent": "athing"
},
{
"file": "/etc/puppet/manifests/site.pp",
"kind": "defined_type",
"line": 1,
"name": "hello",
"parameters": {
"a": "{key2 => \"val2\", key => \"val\"}",
"message": "$title"
}
},
{
"file": "/etc/puppet/manifests/site.pp",
"kind": "node",
"line": 14,
"name": "web01.example.com"
},
{
"file": "/etc/puppet/manifests/site.pp",
"kind": "node",
"line": 17,
"name": "default"
}
]
#### Search Not Found
GET /env/resource_types/pattern.that.finds.no.resources
HTTP/1.1 404 Not Found: Could not find instances in resource_type with 'pattern.that.finds.no.resources'
Content-Type: text/plain
Not Found: Could not find instances in resource_type with 'pattern.that.finds.no.resources'
#### No Search Term Given
GET /env/resource_types/
- HTTP/1.1 400 No request key specified in /env/resource_types/
+ HTTP/1.1 400 Bad Request
Content-Type: text/plain
No request key specified in /env/resource_types/
#### Search Term Is an Invalid Regular Expression
Searching on `[-` for instance.
GET /env/resource_types/%5b-
- HTTP/1.1 400 Invalid regex '[-': premature end of char-class: /[-/
+ HTTP/1.1 400 Bad Request
Content-Type: text/plain
Invalid regex '[-': premature end of char-class: /[-/
### Examples
List all classes:
GET /:environment/resource_types/*?kind=class
List matching a regular expression:
GET /:environment/resource_types/foo.*bar
Schema
------
A `resource_type` response body conforms to the schema at {file:api/schemas/resource_type.json api/schemas/resource_type.json}.
Source
------
Example site.pp used to generate all the responses in this file:
define hello ($message = $title, $a = { key => 'val', key2 => 'val2' }) {
notify {$message: }
}
hello { "there": }
class athing {
}
# An example class
class bthing inherits athing {
}
node 'web01.example.com' {}
node default {}
diff --git a/api/docs/pson.md b/api/docs/pson.md
index 5695b9b31..23108d1d3 100644
--- a/api/docs/pson.md
+++ b/api/docs/pson.md
@@ -1,60 +1,62 @@
PSON
=============
PSON is a variant of {http://json.org JSON} that puppet uses for serializing
data to transmit across the network or store on disk. Whereas JSON requires
that the serialized form is valid unicode (usually UTF-8), PSON is 8-bit ASCII,
which allows it to represent arbitrary byte sequences in strings.
Puppet uses the MIME types "pson" and "text/pson" to refer to PSON.
Differences from JSON
---------------------
-PSON does *not differ* from JSON in its representation of objects, arrays, numbers, booleans, and null values. PSON *does* serialize strings differently from JSON.
+PSON does *not differ* from JSON in its representation of objects, arrays,
+numbers, booleans, and null values. PSON *does* serialize strings differently
+from JSON.
A PSON string is a sequence of 8-bit ASCII encoded data. It must start and end
with " (ASCII 0x22) characters. Between these characters it may contain any
byte sequence. Some individual characters are represented by a sequence of
characters:
| Byte | ASCII Character | Encoded Sequence | Encoded ASCII Sequence |
| ---- | --------------- | ---------------- | ---------------------- |
| 0x22 | " | 0x5C, 0x22 | \" |
| 0x5c | \ | 0x5C, 0x5C | \\ |
| 0x08 | Backspace | 0x5C, 0x62 | \b |
| 0x09 | Horizontal Tab | 0x5C, 0x74 | \t |
| 0x0A | Line Feed | 0x5C, 0x6E | \n |
| 0x0C | Form Feed | 0x5C, 0x66 | \f |
| 0x0D | Carriage Return | 0x5C, 0x72 | \r |
In addition, any character between 0x00 and 0x1F, (except the ones listed
above) must be encoded as a six byte sequence of \u followed by four ASCII
digits of the hex number of the desired character. For example the ASCII
Record Separator character (0x1E) is represented as \u001E (0x5C, 0x75, 0x30,
0x30, 0x31, 0x45).
Decoding PSON Using JSON Parsers
--------------------------------
Many languages have JSON parsers already, which can often be used to parse PSON
data. Although JSON requires that it is encoded as unicode most parsers will
produce usable output from PSON if they are instructed to interpret the input
as Latin-1 encoding.
In all these examples there is a file available called `data.pson` that
contains the ruby structure `{ "data" => "\x07\x08\xC3\xC3" }` encoded as
PSON (the value is an invalid unicode sequence). In bytes the data is:
0x7b 0x22 0x64 0x61 0x74 0x61 0x22 0x3a 0x22 0x5c 0x75 0x30 0x30 0x30 0x37 0x5c 0x62 0xc3 0xc3 0x22 0x7d
Python Example:
>>> import json
>>> json.load(open("data.pson"), "latin_1")
{u'data': u'\x07\x08\xc3\xc3'}
Clojure Example:
user> (parse-string (slurp "data.pson" :encoding "ISO-8859-1"))
{"data" "^G\bÃÃ"}
diff --git a/api/schemas/environments.json b/api/schemas/environments.json
new file mode 100644
index 000000000..3b99869d1
--- /dev/null
+++ b/api/schemas/environments.json
@@ -0,0 +1,39 @@
+{
+ "$schema": "http://json-schema.org/draft-04/schema#",
+ "title": "Environment Enumeration",
+ "description": "An enumeration of environments and their settings",
+ "type": "object",
+ "properties": {
+ "search_paths": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ },
+ "minItems": 1,
+ "description": "An array of the paths where the master looked for environments."
+ },
+ "environments": {
+ "type": "object",
+ "patternProperties": {
+ "^[a-z0-9_]+$": {
+ "type": "object",
+ "properties": {
+ "settings" : {
+ "type": "object",
+ "properties": {
+ "manifest": { "type": "string" },
+ "modulepath": {
+ "type": "array",
+ "items": { "type": "string" }
+ }
+ },
+ "required": ["modulepath", "manifest"]
+ }
+ },
+ "required": ["settings"]
+ }
+ }
+ }
+ },
+ "required": ["search_paths", "environments"]
+}
diff --git a/api/schemas/error.json b/api/schemas/error.json
new file mode 100644
index 000000000..7cc570446
--- /dev/null
+++ b/api/schemas/error.json
@@ -0,0 +1,23 @@
+{
+ "$schema": "http://json-schema.org/draft-04/schema#",
+ "title": "HTTP Error Response Object",
+ "description": "A description of the error encountered when attempting to service an HTTP request.",
+ "type": "object",
+ "properties": {
+ "message": {
+ "description": "A human-readable message explaining the error",
+ "type": "string"
+ },
+ "issue_kind": {
+ "description": "A unique label to identify the error class",
+ "type": "string"
+ },
+ "stacktrace": {
+ "description": "For 5xx responses only, a ruby stacktrace to where an error occurred.",
+ "type": "array",
+ "items": { "type": "string" }
+ }
+ },
+ "required": ["message", "issue_kind"],
+ "additionalProperties": false
+}
diff --git a/api/schemas/report.json b/api/schemas/report.json
index 26f1988d4..cf76bfbbc 100644
--- a/api/schemas/report.json
+++ b/api/schemas/report.json
@@ -1,586 +1,592 @@
{
"$schema": "http://json-schema.org/draft-04/schema#",
"title": "Report",
"description": "A Puppet Report",
"type": "object",
"additionalProperties": false,
"required": [
"host",
"time",
"resource_statuses",
"configuration_version",
"report_format",
"puppet_version",
"kind",
"status",
"environment"
],
"properties": {
"host": {
"description": "The host that generated the report",
"type": "string"
},
"time": {
"description": "When the run began. In ISO 8601 format with 9 characters second-fragment",
"type": "string"
},
"logs": {
"description": "Zero or more occurrences of Log objects",
"type": "array",
"items": {
"type": "object",
"$ref": "#/definitions/log"
}
},
"metrics": {
"description": "Hash of metric category to data for that category",
"type": "object",
"properties": {
"time": {
"type": "object",
"$ref": "#/definitions/time_metrics"
},
"resources": {
"type": "object",
"$ref": "#/definitions/resources_metrics"
},
"events": {
"type": "object",
"$ref": "#/definitions/events_metrics"
},
"changes": {
"type": "object",
"$ref": "#/definitions/changes_metrics"
}
},
"additionalProperties": false,
"minProperties": 1
},
"resource_statuses": {
"description": "Object with one property per resource-name having type as described by #report/status.json schema type",
"type": "object",
"patternProperties": {
".*": {
"type": "object",
"$ref": "#/definitions/status"
}
},
"additionalProperties": false
},
"configuration_version": {
"description": "The configuration version of the Puppet run. This is a String if the user has specified their own versioning scheme, otherwise an Integer representing seconds since the epoch.",
"anyOf": [
{ "type": "integer", "description": "seconds since the epoch" },
{ "type": "string", "description": "custom versioning scheme" }
]
},
"transaction_uuid": {
"description": "A UUID covering the transaction. The query parameters for the catalog retrieval will have included the same UUID.",
"type": "string"
},
"report_format": {
"description": "The report format version documented by this schema",
"type": "integer",
"enum": ["4", 4]
},
"puppet_version": {
"description": "The version of the Puppet Agent the report is for.",
"type": "string"
},
"kind": {
"description": "Report kind, enumerator with one of the values:\n\n* `inspect`, if this report came from a puppet inspect run.\n* `apply`, if this report came from a puppet apply or puppet agent run",
"enum": [
"inspect",
"apply"
]
},
"status": {
"description": "Report status, enumerator with one of the values:\n\n* `failed`, if run failed\n* `changed`, if something changed\n* `unchanged`, if nothing changed from the previous run\n",
"enum": [
"failed",
"changed",
"unchanged"
]
},
"environment": {
"description": "The name of the environment that was used for the puppet run (e.g. \"production\").",
"type": "string"
}
},
"definitions" : {
"log" : {
"properties": {
"file": {
"description": "The pathname of the manifest file which triggered the log message.",
"oneOf": [
{"type": "string"},
{"type": "null"}
]
},
"line": {
"description": "The line number in the manifest file which triggered the log message.",
"oneOf": [
{"type": "string"},
{"type": "null"}
]
},
"level": {
"description": "The severity of the message.",
"enum": [
"debug",
"info",
"notice",
"warning",
"err",
"alert",
"emerg",
"crit"
]
},
"message": {
"description": "The message itself.",
"type": "string"
},
"source": {
"description": "The origin of the log message. This could be a resource, a property of a resource, or the string 'Puppet'.",
"type": "string"
},
"tags": {
"description": "The strings with which the source is tagged",
"type": "array",
"items": { "type": "string" }
},
"time": {
"description": "When the message was sent. In ISO 8601 format with 9 characters second-fragment",
"type": "string"
}
},
"required": [
"level",
"message",
"source",
"tags",
"time"
],
"additionalProperties": false
},
"time_metrics": {
"description": "A Metric in the `time` category",
"type": "object",
"properties": {
"name": {
"description": "The name of the metric category ('time')",
"enum": ["time"]
},
"label": {
"description": "The name in title form",
"enum": ["Time"]
},
"values": {
"description": "The metric values in the 'time' category contains one entry per resource type in the catalog for\nwhich there is at least one resource and the values named:\n\n* `config_retrieval`\n* `total`\n* `inspect` (only present if the report is of `inspect`-kind)\n",
"type": "array",
"items": {
"description": "Each entry in `values` is an array with 3 slots for `name`, `label` and `value`",
"type": "array",
"items": [
{
"description": "The name of the value (the name of a resource type, `config_retrieval`, or `total`",
"type": "string"
},
{
"description": "The name in title form",
"type": "string"
},
{
"description": "The value",
"type": "number"
}
]
}
}
},
"required": [ "name", "label", "values"],
"additionalProperties": false
},
"resources_metrics": {
"description": "A Metric in the `resources` category",
"type": "object",
"properties": {
"name": {
"description": "The name of the metric category ('resources')",
"enum": ["resources"]
},
"label": {
"description": "The name in title form",
"enum": ["Resources"]
},
"values": {
"description": "The metric values in the 'resources' category",
"type": "array",
"items": {
"description": "Each entry in `values` is an array with 3 slots for `name`, `label` and `value`",
"type": "array",
"items": [
{
"description": "The name of the value",
"enum": [
"failed",
"out_of_sync",
"changed",
"total",
"skipped",
- "failed_to_restart",
+ "failed_to_restart",
"restarted",
"scheduled"
]
},
{
"description": "The name in title form",
"enum": [
"Failed",
"Out of sync",
"Changed",
"Total",
"Skipped",
- "Failed to restart",
+ "Failed to restart",
"Restarted",
"Scheduled"
]
},
{
"description": "The value",
"type": "integer"
}
]
}
}
},
"required": [ "name", "label", "values"],
"additionalProperties": false
},
"events_metrics": {
"description": "A Metric in the `events` category",
"type": "object",
"properties": {
"name": {
"description": "The name of the metric category ('events')",
"enum": ["events"]
},
"label": {
"description": "The name in title form",
"enum": ["Events"]
},
"values": {
"description": "The metric values in the 'events' category. The entry named `total` is always present, the others are present only if their value is non zero.",
"type": "array",
"items": {
"description": "Each entry in `values` is an array with 3 slots for `name`, `label` and `value`.",
"type": "array",
"items": [
{
"description": "The name of the value",
"enum": [
"success",
"failure",
"audit",
"noop",
"total"
]
},
{
"description": "The name in title form",
"enum": [
"Success",
"Failure",
"Audit",
"Noop",
"Total"
]
},
{
"description": "The value",
"type": "integer"
}
]
}
}
},
"required": [ "name", "label", "values"],
"additionalProperties": false
},
"changes_metrics": {
"description": "A Metric in the `changes` category",
"type": "object",
"properties": {
"name": {
"description": "The name of the metric category ('changes')",
"enum": ["changes"]
},
"label": {
"description": "The name in title form",
"enum": ["Changes"]
},
"values": {
"description": "The metric value in the 'changes' category is called `total` - there is only one total.",
"type": "array",
"items": {
"description": "Each entry in `values` is an array with 3 slots for `name`, `label` and `value`.",
"type": "array",
"items": [
{
"description": "The name of the value",
"enum": ["total" ]
},
{
"description": "The name in title form",
"enum": ["Total"]
},
{
"description": "The value",
"type": "integer"
}
]
}
}
},
"required": [ "name", "label", "values"],
"additionalProperties": false
},
"status": {
"description": "A Status entry for a resource in a report",
"type": "object",
"properties": {
"resource_type": {
"description": "The type name of the resource, capitalized",
"type": "string",
"pattern": "^[A-Z].*$"
},
"title": {
"description": "The title of the resource.",
"type": "string"
},
"resource": {
"description": "The resource name, in the form Type[title]. **Deprecated**. This is always the same string as the name of the property where this Status object is the value.",
"type": "string"
},
"file": {
"description": "The pathname of the manifest file which declared the resource.",
"oneOf": [
{"type": "string"},
{"type": "null"}
]
},
"line": {
"description": "The line number in the manifest file which declared the resource.",
"oneOf": [
{"type": "string"},
{"type": "null"}
]
},
"evaluation_time": {
"description": "The amount of time, in seconds, taken to evaluate the resource. Not present in reports of `inspect` kind.",
"oneOf": [
{"type": "number"},
{"type": "null"}
]
},
"change_count": {
"description": "The number of properties which changed. Always 0 in reports of `inspect` kind.",
"type": "integer"
},
"out_of_sync_count": {
"description": "The number of properties which were out of sync. Always 0 in reports of `inspect` kind.",
"type": "integer"
},
"tags": {
"description": "The strings with which the resource is tagged",
"type": "array",
"items": { "type": "string" }
},
"time": {
"description": "The time the resource was evaluated. In ISO 8601 format with 9 characters second-fragment",
"type": "string"
},
"events": {
"description": "the Puppet::Transaction::Event objects for the resource",
"type": "array",
"items": { "$ref": "#/definitions/event" }
},
"out_of_sync": {
"description": "True if out_of_sync_count > 0, otherwise false. **Deprecated**",
"type": "boolean"
},
"changed": {
"description": "True if change_count > 0, otherwise false. **Deprecated**",
"type": "boolean"
},
"skipped": {
"description": "True if the resource was skipped, otherwise false.",
"type": "boolean"
},
"failed": {
"description": "True if Puppet experienced an error while evaluating this resource, otherwise false. **Deprecated**",
"type": "boolean"
},
"containment_path": {
"description": "An array of strings; each element represents a container (type or class) that, together, make up the path of the resource in the catalog.",
"type": "array",
"items": { "type": "string" }
}
},
"required": [
"resource_type",
"title",
- "change_count",
+ "change_count",
"out_of_sync_count",
"tags",
"events",
"skipped",
"containment_path"
],
"additionalProperties": false
},
"event": {
"description": "An Event in a Report",
"type": "object",
"properties": {
"audited": {
"description": "True if this property is being audited, otherwise false. True in report of `inspect` kind.",
"type": "boolean"
},
"property": {
"description": "The property for which the event occurred.",
- "type": "string"
+ "oneOf": [
+ { "type": "string" },
+ { "type": "null" }
+ ]
},
"previous_value": {
"description": "The value of the property before the change (if any) was applied.",
"oneOf": [
{ "type": "string" },
{ "type": "array" },
- { "type": "object" }
+ { "type": "object" },
+ { "type": "null" }
]
},
"desired_value": {
"description": "the value specified in the manifest. Absent in reports of `inspect` kind.",
"oneOf": [
{ "type": "string" },
{ "type": "array" },
- { "type": "object" }
+ { "type": "object" },
+ { "type": "null" }
]
},
"historical_value": {
"description": "The audited value from a previous run of Puppet, if known. Absent in reports of `inspect` kind.",
"oneOf": [
{ "type": "string" },
{ "type": "array" },
- { "type": "object" }
+ { "type": "object" },
+ { "type": "null" }
]
},
"message": {
"description": "The log message generated by this event.",
"type": "string"
},
"name": {
"description": "The name of the event. Absent in reports of `inspect` kind.",
"type": "string"
},
"status": {
"description": "One of the following strings:\n\n* `success` - property was out of sync, and was successfully changed to be in sync.\n* `failure`- property was out of sync, and couldn't be changed to be in sync due to an error.\n* `noop` - property was out of sync, and wasn't changed due to noop mode.\n* `audit` - property was in sync, and was being audited.\n\ndepending on the type of the event. Always `audit` in reports of `inspect` kind.\n",
"enum": [
"success",
"failure",
"noop",
"audit"
]
},
"time": {
"description": "The time at which the property was evaluated. In ISO 8601 format with 9 characters second-fragment",
"type": "string"
}
},
"required": [
"audited",
"property",
"message",
"name",
"status",
"time"
],
"additionalProperties": false
}
}
}
diff --git a/benchmarks/defined_types/benchmarker.rb b/benchmarks/defined_types/benchmarker.rb
index f2352bc05..77d46e71e 100644
--- a/benchmarks/defined_types/benchmarker.rb
+++ b/benchmarks/defined_types/benchmarker.rb
@@ -1,74 +1,73 @@
require 'erb'
require 'ostruct'
require 'fileutils'
require 'json'
class Benchmarker
include FileUtils
def initialize(target, size)
@target = target
@size = size
end
def setup
require 'puppet'
config = File.join(@target, 'puppet.conf')
Puppet.initialize_settings(['--config', config])
end
def run
- env = Puppet::Node::Environment.new(:benchmarking)
-# env = Puppet.lookup(:environments).get('benchmarking')
+ env = Puppet.lookup(:environments).get('benchmarking')
node = Puppet::Node.new("testing", :environment => env)
Puppet::Resource::Catalog.indirection.find("testing", :use_node => node)
end
def generate
environment = File.join(@target, 'environments', 'benchmarking')
templates = File.join('benchmarks', 'defined_types')
mkdir_p(File.join(environment, 'modules'))
mkdir_p(File.join(environment, 'manifests'))
render(File.join(templates, 'site.pp.erb'),
File.join(environment, 'manifests', 'site.pp'),
:size => @size)
@size.times do |i|
module_name = "module#{i}"
module_base = File.join(environment, 'modules', module_name)
manifests = File.join(module_base, 'manifests')
mkdir_p(manifests)
File.open(File.join(module_base, 'metadata.json'), 'w') do |f|
JSON.dump({
"types" => [],
"source" => "",
"author" => "Defined Types Benchmark",
"license" => "Apache 2.0",
"version" => "1.0.0",
"description" => "Defined Types benchmark module #{i}",
"summary" => "Just this benchmark module, you know?",
"dependencies" => [],
}, f)
end
render(File.join(templates, 'module', 'testing.pp.erb'),
File.join(manifests, 'testing.pp'),
:name => module_name)
end
render(File.join(templates, 'puppet.conf.erb'),
File.join(@target, 'puppet.conf'),
:location => @target)
end
def render(erb_file, output_file, bindings)
site = ERB.new(File.read(erb_file))
File.open(output_file, 'w') do |fh|
fh.write(site.result(OpenStruct.new(bindings).instance_eval { binding }))
end
end
end
diff --git a/benchmarks/defined_types/puppet.conf.erb b/benchmarks/defined_types/puppet.conf.erb
index 2f6745059..e0c5d8588 100644
--- a/benchmarks/defined_types/puppet.conf.erb
+++ b/benchmarks/defined_types/puppet.conf.erb
@@ -1,5 +1,3 @@
confdir = <%= location %>
vardir = <%= location %>
environmentpath = <%= File.join(location, 'environments') %>
-manifest = <%= File.join(location, 'environments', 'benchmarking', 'manifests', 'site.pp') %>
-modulepath = <%= File.join(location, 'environments', 'benchmarking', 'modules') %>
diff --git a/benchmarks/many_modules/benchmarker.rb b/benchmarks/many_modules/benchmarker.rb
index ec1b6114b..8540f85b1 100644
--- a/benchmarks/many_modules/benchmarker.rb
+++ b/benchmarks/many_modules/benchmarker.rb
@@ -1,78 +1,77 @@
require 'erb'
require 'ostruct'
require 'fileutils'
require 'json'
class Benchmarker
include FileUtils
def initialize(target, size)
@target = target
@size = size
end
def setup
require 'puppet'
config = File.join(@target, 'puppet.conf')
Puppet.initialize_settings(['--config', config])
end
def run
- env = Puppet::Node::Environment.new(:benchmarking)
-# env = Puppet.lookup(:environments).get('benchmarking')
+ env = Puppet.lookup(:environments).get('benchmarking')
node = Puppet::Node.new("testing", :environment => env)
Puppet::Resource::Catalog.indirection.find("testing", :use_node => node)
end
def generate
environment = File.join(@target, 'environments', 'benchmarking')
templates = File.join('benchmarks', 'many_modules')
mkdir_p(File.join(environment, 'modules'))
mkdir_p(File.join(environment, 'manifests'))
render(File.join(templates, 'site.pp.erb'),
File.join(environment, 'manifests', 'site.pp'),
:size => @size)
@size.times do |i|
module_name = "module#{i}"
module_base = File.join(environment, 'modules', module_name)
manifests = File.join(module_base, 'manifests')
mkdir_p(manifests)
File.open(File.join(module_base, 'metadata.json'), 'w') do |f|
JSON.dump({
"types" => [],
"source" => "",
"author" => "ManyModules Benchmark",
"license" => "Apache 2.0",
"version" => "1.0.0",
"description" => "Many Modules benchmark module #{i}",
"summary" => "Just this benchmark module, you know?",
"dependencies" => [],
}, f)
end
render(File.join(templates, 'module', 'init.pp.erb'),
File.join(manifests, 'init.pp'),
:name => module_name)
render(File.join(templates, 'module', 'internal.pp.erb'),
File.join(manifests, 'internal.pp'),
:name => module_name)
end
render(File.join(templates, 'puppet.conf.erb'),
File.join(@target, 'puppet.conf'),
:location => @target)
end
def render(erb_file, output_file, bindings)
site = ERB.new(File.read(erb_file))
File.open(output_file, 'w') do |fh|
fh.write(site.result(OpenStruct.new(bindings).instance_eval { binding }))
end
end
end
diff --git a/benchmarks/many_modules/puppet.conf.erb b/benchmarks/many_modules/puppet.conf.erb
index 2f6745059..e0c5d8588 100644
--- a/benchmarks/many_modules/puppet.conf.erb
+++ b/benchmarks/many_modules/puppet.conf.erb
@@ -1,5 +1,3 @@
confdir = <%= location %>
vardir = <%= location %>
environmentpath = <%= File.join(location, 'environments') %>
-manifest = <%= File.join(location, 'environments', 'benchmarking', 'manifests', 'site.pp') %>
-modulepath = <%= File.join(location, 'environments', 'benchmarking', 'modules') %>
diff --git a/conf/auth.conf b/conf/auth.conf
index b31906bae..96f078c48 100644
--- a/conf/auth.conf
+++ b/conf/auth.conf
@@ -1,116 +1,120 @@
# This is the default auth.conf file, which implements the default rules
# used by the puppet master. (That is, the rules below will still apply
# even if this file is deleted.)
#
# The ACLs are evaluated in top-down order. More specific stanzas should
# be towards the top of the file and more general ones at the bottom;
# otherwise, the general rules may "steal" requests that should be
# governed by the specific rules.
#
# See http://docs.puppetlabs.com/guides/rest_auth_conf.html for a more complete
# description of auth.conf's behavior.
#
# Supported syntax:
# Each stanza in auth.conf starts with a path to match, followed
# by optional modifiers, and finally, a series of allow or deny
# directives.
#
# Example Stanza
# ---------------------------------
# path /path/to/resource # simple prefix match
# # path ~ regex # alternately, regex match
# [environment envlist]
# [method methodlist]
# [auth[enthicated] {yes|no|on|off|any}]
# allow [host|backreference|*|regex]
# deny [host|backreference|*|regex]
# allow_ip [ip|cidr|ip_wildcard|*]
# deny_ip [ip|cidr|ip_wildcard|*]
#
# The path match can either be a simple prefix match or a regular
# expression. `path /file` would match both `/file_metadata` and
# `/file_content`. Regex matches allow the use of backreferences
# in the allow/deny directives.
#
# The regex syntax is the same as for Ruby regex, and captures backreferences
# for use in the `allow` and `deny` lines of that stanza
#
# Examples:
#
# path ~ ^/path/to/resource # Equivalent to `path /path/to/resource`.
# allow * # Allow all authenticated nodes (since auth
# # defaults to `yes`).
#
# path ~ ^/catalog/([^/]+)$ # Permit nodes to access their own catalog (by
# allow $1 # certname), but not any other node's catalog.
#
# path ~ ^/file_(metadata|content)/extra_files/ # Only allow certain nodes to
# auth yes # access the "extra_files"
# allow /^(.+)\.example\.com$/ # mount point; note this must
# allow_ip 192.168.100.0/24 # go ABOVE the "/file" rule,
# # since it is more specific.
#
# environment:: restrict an ACL to a comma-separated list of environments
# method:: restrict an ACL to a comma-separated list of HTTP methods
# auth:: restrict an ACL to an authenticated or unauthenticated request
# the default when unspecified is to restrict the ACL to authenticated requests
# (ie exactly as if auth yes was present).
#
### Authenticated ACLs - these rules apply only when the client
### has a valid certificate and is thus authenticated
# allow nodes to retrieve their own catalog
path ~ ^/catalog/([^/]+)$
method find
allow $1
# allow nodes to retrieve their own node definition
path ~ ^/node/([^/]+)$
method find
allow $1
# allow all nodes to access the certificates services
path /certificate_revocation_list/ca
method find
allow *
# allow all nodes to store their own reports
path ~ ^/report/([^/]+)$
method save
allow $1
# Allow all nodes to access all file services; this is necessary for
# pluginsync, file serving from modules, and file serving from custom
# mount points (see fileserver.conf). Note that the `/file` prefix matches
# requests to both the file_metadata and file_content paths. See "Examples"
# above if you need more granular access control for custom mount points.
path /file
allow *
### Unauthenticated ACLs, for clients without valid certificates; authenticated
### clients can also access these paths, though they rarely need to.
# allow access to the CA certificate; unauthenticated nodes need this
# in order to validate the puppet master's certificate
path /certificate/ca
auth any
method find
allow *
# allow nodes to retrieve the certificate they requested earlier
path /certificate/
auth any
method find
allow *
# allow nodes to request a new certificate
path /certificate_request
auth any
method find, save
allow *
+path /v2.0/environments
+method find
+allow *
+
# deny everything else; this ACL is not strictly necessary, but
# illustrates the default policy.
path /
auth any
diff --git a/docs/acceptance_tests.md b/docs/acceptance_tests.md
new file mode 100644
index 000000000..4eea425cc
--- /dev/null
+++ b/docs/acceptance_tests.md
@@ -0,0 +1,210 @@
+Running Acceptance Tests Yourself
+=================================
+
+Table of Contents
+-----------------
+
+* [General Notes](#general-notes)
+* [Running Tests on the vcloud](#running-tests-on-the-vcloud)
+* [Running Tests on Vagrant Boxen](#running-tests-on-vagrant-boxen)
+
+General Notes
+-------------
+
+The rake tasks for running the tests are defined by the Rakefile in the acceptance test directory.
+These tasks come with some documentation: `rake -T` will give short descriptions, and a `rake -D` will give full descriptions with information on ENV options required and optional for the various tasks.
+
+If you are setting up a new repository for acceptance, you will need to bundle install first. This step assumes you have ruby and the bundler gem installed.
+
+```sh
+cd /path/to/repo/acceptance
+bundle install --path=.bundle/gems
+```
+
+Running Tests on the vcloud
+---------------------------
+
+In order to use the Puppet Labs vcloud, you'll need to be a Puppet Labs employee.
+Community members should see the [guide to running the tests on vagrant boxen](#running-tests-on-local-vagrant-boxen).
+
+### Authentication
+
+Normally the ci tasks are called from a prepared Jenkins job.
+
+If you are running this on your laptop, you will need this ssh private key in order for beaker to be able to log into the vms created from the hosts file:
+
+https://github.com/puppetlabs/puppetlabs-modules/blob/qa/secure/jenkins/id_rsa-acceptance
+https://github.com/puppetlabs/puppetlabs-modules/blob/qa/secure/jenkins/id_rsa-acceptance.pub
+
+TODO fetch these files directly from github, but am running into rate limits and then would also have to cross the issue of authentication.
+
+You will also need QA credentials to vsphere in a ~/.fog file. These credentials can be found on any of the Jenkins coordinator hosts.
+
+### Packages
+
+In order to run the tests on hosts provisioned from packages produced by Delivery, you will need to reference a Puppet commit sha that has been packaged using Delivery's pl:jenkins:uber_build task. This is the snippet used by 'Puppet Packaging' Jenkins jobs:
+
+```sh
+rake --trace package:implode
+rake --trace package:bootstrap
+rake --trace pl:jenkins:uber_build
+```
+
+The above Rake tasks were run from the root of a Puppet checkout. They are quoted just for reference. Typically if you are investigating a failure, you will have a SHA from a failed jenkins run which should correspond to a successful pipeline run, and you should not need to run the pipeline manually.
+
+A finished pipeline will have repository information available at http://builds.puppetlabs.lan/puppet/ So you can also browse this list and select a recent sha which has repo_configs/ available.
+
+When executing the ci:test:packages task, you must set the SHA, and also set CONFIG to point to a valid Beaker hosts_file. Configurations used in the Jenkins jobs are available under config/nodes
+
+```sh
+bundle exec rake ci:test:packages SHA=abcdef CONFIG=config/nodes/rhel.yaml
+```
+
+Optionally you may set the TEST (TEST=a/test.rb,and/another/test.rb), and may pass additional OPTIONS to beaker (OPTIONS='--opt foo').
+
+You may also edit a ./local_options.rb hash which will override config/ options, and in turn be overriden by commandline options set in the environment variables CONFIG, TEST and OPTIONS. This file is a ruby file containing a Ruby hash with configuration expected by Beaker. See Beaker source, and examples in config/.
+
+### Git
+
+Alternatively you may provision via git clone by calling the ci:test:git task. Currently we don't have packages for Windows or Solaris from the Delivery pipeline, and must use ci:test:git to provision and test these platforms.
+
+#### Source Checkout for Different Fork
+
+If you have a branch pushed to your fork which you wish to test prior to merging into puppetlabs/puppet, you can do so be setting the FORK environment variable. So, if I have a branch 'issue/master/wonder-if-this-explodes' pushed to my jpartlow puppet fork that I want to test on Windows, I could invoke the following:
+
+```sh
+bundle exec rake ci:test:git CONFIG=config/nodes/win2008r2.yaml SHA=issue/master/wonder-if-this-explodes FORK=jpartlow
+```
+
+#### Source Checkout for Local Branch
+
+See notes on running acceptance with Vagrant for more details on using a local git daemon.
+
+TODO Fix up the Rakefile's handling of git urls so that there is a simple way to specify both a branch on a github fork, and a branch on some other git server daemon, so that you have fewer steps when serving from a local git daemon.
+
+### Preserving Hosts
+
+If you need to ssh into the hosts after a test run, you can use the following sequence:
+
+ bundle exec rake ci:test_and_preserve_hosts CONFIG=some/config.yaml SHA=12345 TEST=a/foo_test.rb
+
+to get the initial templates provisioned, and a local log/latest/preserve_config.yaml created for them.
+
+Then you can log into the hosts, or rerun tests against them by:
+
+ bundle exec rake ci:test_against_preserved_hosts TEST=a/foo_test.rb
+
+This will use the existing hosts.
+
+### Cleaning Up Preserved Hosts
+
+If you run a number of jobs with --preserve_hosts or vi ci:test_and_preserve_hosts, you may eventually generate a large number of stale vms. They should be reaped automatically by qa infrastructure within a day or so, but you may also run:
+
+ bundle exec rake ci:release_hosts
+
+to clean them up sooner and free resources.
+
+There also may be scenarios where you want to specify the host(s) to destroy. E.g. you may want to destroy a subset of the hosts you've created. Or, if a test run terminates early, ci:destroy_preserved_hosts may not be able to derive the name of the vm to delete. In such cases you can specify host(s) to be deleted using the HOST_NAMES environment variable. E.g.
+
+ HOST_NAMES=lvwwr9tdplg351u bundle exec rake ci:destroy_preserved_hosts
+ HOST_NAMES=lvwwr9tdplg351u,ylrqjh5l6xvym4t bundle exec rake ci:destroy_preserved_hosts
+
+
+Running Tests on Vagrant Boxen
+------------------------------
+
+This guide assumes that you have an acceptable Ruby (i.e. 1.9+) installed along with the bundler gem, that you have the puppet repo checked out locally somewhere, and that the name of the checkout folder is `puppet`.
+I used Ruby 1.9.3-p484
+
+Change to the `acceptance` directory in the root of the puppet repo:
+```sh
+cd /path/to/repo/puppet/acceptance
+```
+Install the necessary gems with bundler:
+```sh
+bundle install
+```
+
+Now you can get a list of test-related tasks you can run via rake:
+```sh
+bundle exec rake -T
+```
+and view detailed information on the tasks with
+```sh
+bundle exec rake -D
+```
+
+As an example, let's try running the acceptance tests using git as the code deployment mechanism.
+First, we'll have to create a beaker configuration file for a local vagrant box on which to run the tests.
+Here's what such a file could look like:
+```yaml
+HOSTS:
+ all-in-one:
+ roles:
+ - master
+ - agent
+ platform: centos-64-x64
+ hypervisor: vagrant
+ ip: 192.168.80.100
+ box: centos-64-x64-vbox4210-nocm
+ box_url: http://puppet-vagrant-boxes.puppetlabs.com/centos-64-x64-vbox4210-nocm.box
+
+CONFIG:
+```
+This defines a 64-bit CentOS 6.4 vagrant box that serves as both a puppet master and a puppet agent for the test roles.
+(For more information on beaker config files, see [beaker's README](https://github.com/puppetlabs/beaker/blob/master/README.md).)
+Save this file as `config/nodes/centos6-local.yaml`; we'll be needing it later.
+
+Since we have only provided a CentOS box, we don't have anywhere to run windows tests, therefore we'll have to skip those tests.
+That means we want to pass beaker a --tests argument that contains every directory and file in the `tests` directory besides the one called `windows`.
+We could pass this option on the command line, but it will be gigantic, so instead let's create a `local_options.rb` file that beaker will automatically read in.
+This file should contain a ruby hash of beaker's command-line flags to the corresponding flag arguments.
+Our hash will only contain the `tests` key, and its value will be a comma-seperated list of the other files and directories in `tests`.
+Here's an easy way to generate this file:
+```sh
+echo "{tests: \"$(echo tests/* | sed -e 's| *tests/windows *||' -e 's/ /,/g')\"}" > local_options.rb"
+```
+
+The last thing that needs to be done before we can run the tests is to set up a way for the test box to check out our local changes for testing.
+We'll do this by starting a git daemon on our host.
+In another session, navigate to the folder that contains your checkout of the puppet repo, and then create the following symlink:
+```sh
+ln -s . puppet.git
+```
+This works around the inflexible checkout path used by the test prep code.
+
+Now start the git daemon with
+```sh
+git daemon --verbose --informative-errors --reuseaddr --export-all --base-path=.
+```
+after which you should see a message like `[32963] Ready to rumble` echoed to the console.
+
+Now we can finally run the tests!
+The rake task that we'll use is `ci:test:git`.
+Run
+```
+bundle exec rake -D ci:test:git
+```
+to read the full description of this task.
+From the description, we can see that we'll need to set a few environment variables:
+ + CONFIG should be set to point to the CentOS beaker config file we created above.
+ + SHA should be the SHA of the commit we want to test.
+ + GIT_SERVER should be the IP address of the host (i.e. your machine) in the vagrant private network created for the test box.
+ This is derived from the test box's ip by replacing the last octet with 1.
+ For our example above, the host IP is 192.168.80.1
+ + FORK should be the path to a 'puppet.git' directory that points to the repo.
+ In our case, this is the path to the symlink we created before, which is inside your puppet repo checkout, so FORK should just be the name of your checkout.
+ We'll assume that the name is `puppet`.
+
+Putting it all together, we construct the following command-line invocation to run the tests:
+```sh
+CONFIG=config/nodes/centos6-local.yaml SHA=#{test-commit-sha} GIT_SERVER='192.168.80.1' FORK='puppet' bundle exec rake --trace ci:test:git
+```
+Go ahead and run that sucker!
+
+Testing will take some time.
+After the testing finishes, you'll either see this line
+```
+systest completed successfully, thanks.
+```
+near the end of the output, indicating that all tests completed succesfully, or you'll see the end of a stack trace, indicating failed tests further up.
diff --git a/docs/catalogs.md b/docs/catalogs.md
new file mode 100644
index 000000000..6702a9b27
--- /dev/null
+++ b/docs/catalogs.md
@@ -0,0 +1,122 @@
+# Two Types of Catalog
+
+When working on subsystems of Puppet that deal with the catalog it is important
+to be aware of the two different types of Catalog. Developers will often find
+this difference while working on the static compiler and types and providers.
+
+The two different types of catalog becomes relevant when writing spec tests
+because we frequently need to wire up a fake catalog so that we can exercise
+types, providers, or termini that filter the catalog.
+
+The two different types of catalogs are so-called "resource" catalogs and "RAL"
+(resource abstraction layer) catalogs. At a high level, the resource catalog
+is the in-memory object we serialize and transfer around the network. The
+compiler terminus is expected to produce a resource catalog. The agent takes a
+resource catalog and converts it into a RAL catalog. The RAL catalog is what
+is used to apply the configuration model to the system.
+
+Resource dependency information is most easily obtained from a RAL catalog by
+walking the graph instance produced by the `relationship_graph` method.
+
+### Resource Catalog
+
+If you're writing spec tests for something that deals with a catalog "server
+side," a new catalog terminus for example, then you'll be dealing with a
+resource catalog. You can produce a resource catalog suitable for spec tests
+using something like this:
+
+ let(:catalog) do
+ catalog = Puppet::Resource::Catalog.new("node-name-val") # NOT certname!
+ rsrc = Puppet::Resource.new("file", "sshd_config",
+ :parameters => {
+ :ensure => 'file',
+ :source => 'puppet:///modules/filetest/sshd_config',
+ }
+ )
+ rsrc.file = 'site.pp'
+ rsrc.line = 21
+ catalog.add_resource(rsrc)
+ end
+
+The resources in this catalog may be accessed using `catalog.resources`.
+Resource dependencies are not easily walked using a resource catalog however.
+To walk the dependency tree convert the catalog to a RAL catalog as described
+in
+
+### RAL Catalog
+
+The resource catalog may be converted to a RAL catalog using `catalog.to_ral`.
+The RAL catalog contains `Puppet::Type` instances instead of `Puppet::Resource`
+instances as is the case with the resource catalog.
+
+One very useful feature of the RAL catalog are the methods to work with
+resource relationships. For example:
+
+ irb> catalog = catalog.to_ral
+ irb> graph = catalog.relationship_graph
+ irb> pp graph.edges
+ [{ Notify[alpha] => File[/tmp/file_20.txt] },
+ { Notify[alpha] => File[/tmp/file_21.txt] },
+ { Notify[alpha] => File[/tmp/file_22.txt] },
+ { Notify[alpha] => File[/tmp/file_23.txt] },
+ { Notify[alpha] => File[/tmp/file_24.txt] },
+ { Notify[alpha] => File[/tmp/file_25.txt] },
+ { Notify[alpha] => File[/tmp/file_26.txt] },
+ { Notify[alpha] => File[/tmp/file_27.txt] },
+ { Notify[alpha] => File[/tmp/file_28.txt] },
+ { Notify[alpha] => File[/tmp/file_29.txt] },
+ { File[/tmp/file_20.txt] => Notify[omega] },
+ { File[/tmp/file_21.txt] => Notify[omega] },
+ { File[/tmp/file_22.txt] => Notify[omega] },
+ { File[/tmp/file_23.txt] => Notify[omega] },
+ { File[/tmp/file_24.txt] => Notify[omega] },
+ { File[/tmp/file_25.txt] => Notify[omega] },
+ { File[/tmp/file_26.txt] => Notify[omega] },
+ { File[/tmp/file_27.txt] => Notify[omega] },
+ { File[/tmp/file_28.txt] => Notify[omega] },
+ { File[/tmp/file_29.txt] => Notify[omega] }]
+
+If the `relationship_graph` method is throwing exceptions at you, there's a
+good chance the catalog is not a RAL catalog.
+
+## Settings Catalog ##
+
+Be aware that Puppet creates a mini catalog and applies this catalog locally to
+manage file resource from the settings. This behavior made it difficult and
+time consuming to track down a race condition in
+[2888](http://projects.puppetlabs.com/issues/2888).
+
+Even more surprising, the `File[puppetdlockfile]` resource is only added to the
+settings catalog if the file exists on disk. This caused the race condition as
+it will exist when a separate process holds the lock while applying the
+catalog.
+
+It may be sufficient to simply be aware of the settings catalog and the
+potential for race conditions it presents. An effective way to be reasonably
+sure and track down the problem is to wrap the File.open method like so:
+
+ # We're wrapping ourselves around the File.open method.
+ # As described at: http://goo.gl/lDsv6
+ class File
+ WHITELIST = [ /pidlock.rb:39/ ]
+
+ class << self
+ alias xxx_orig_open open
+ end
+
+ def self.open(name, *rest, &block)
+ # Check the whitelist for any "good" File.open calls against the #
+ puppetdlock file
+ white_listed = caller(0).find do |line|
+ JJM_WHITELIST.find { |re| re.match(line) }
+ end
+
+ # If you drop into IRB here, take a look at your caller, it might be
+ # the ghost in the machine you're looking for.
+ binding.pry if name =~ /puppetdlock/ and not white_listed
+ xxx_orig_open(name, *rest, &block)
+ end
+ end
+
+The settings catalog is populated by the `Puppet::Util::Settings#to\_catalog`
+method.
diff --git a/docs/index.md b/docs/index.md
new file mode 100644
index 000000000..240d30bab
--- /dev/null
+++ b/docs/index.md
@@ -0,0 +1,18 @@
+# Puppet Developer Documentation
+
+Setting up and running tests
+
+* [Quickstart Guide](quickstart.md)
+* [RSpec Tutorial](rspec_tutorial.md)
+* [Running acceptance tests](acceptance_tests.md)
+
+Developer References
+
+* [Profiling and Benchmarking](profiling.md)
+* [Various Catalog Forms](catalogs.md)
+* [Windows](windows.md)
+* [Unicode and you](unicode.md)
+
+Misc
+
+* [Static Compiler](static_compiler.md)
diff --git a/docs/profiling.md b/docs/profiling.md
new file mode 100644
index 000000000..83d4533bd
--- /dev/null
+++ b/docs/profiling.md
@@ -0,0 +1,49 @@
+# Profiling Puppet
+
+Puppet is a beast. Puppet is at times a very *slow* beast. Maybe we can find
+what is making it slow and fix it.
+
+## Coarse Grained Profiling
+
+There is a built-in system of profiling that can be used to identify some slow
+spots. This can only work with code that is explicitly instrumented, which, at
+the time of this writing, is primarily the compiler. To enable profiling there
+are several options:
+
+* To profile every request on the master add `--profile` to your master's
+ startup.
+* To profile a single run for an agent add `--profile` to your agent's options
+ for that run.
+* To profile a masterless run add `--profile` to your `puppet apply` options.
+
+The timing information will be output to the logs and tagged with the word
+"PROFILE".
+
+For the agent there is actually a second system: evaltrace. You can enable this
+on the agent by passing it `--evaltrace`. Timing information for each resource
+will be output to the logs.
+
+## Using a Ruby Profiler
+
+For much finer grained profiling, you'll want to use
+[ruby-prof](http://rubygems.org/gems/ruby-prof). Once you have the gem
+installed you can either modify the code to profile a certain section (using
+RubyProf.profile) or run the master with ruby-prof by adding `use
+Rack::RubyProf, :path => '/temp/profile'` to the config.ru for your master.
+
+## Running the Benchmarks
+
+Puppet has a number of benchmark scenarios to pinpoint problems in specific,
+known, use cases. The benchmark scenarios live in the `benchmarks` directory.
+
+To run a scenario you do:
+
+ bundle exec rake benchmark:<scenario_name>
+
+If you have ruby-prof installed you can get a calltrace of the benchmark
+scenario by running:
+
+ bundle exec rake benchmark:<scenario_name>:profile
+
+The calltrace file is viewable with
+[kcachegrind](http://kcachegrind.sourceforge.net/html/Home.html).
diff --git a/docs/quickstart.md b/docs/quickstart.md
new file mode 100644
index 000000000..e8a88c3ec
--- /dev/null
+++ b/docs/quickstart.md
@@ -0,0 +1,61 @@
+# Quick Start to Developing on Puppet
+
+Before diving into the code, you should first take the time to make sure you
+have an environment where you can run puppet as a developer. In a nutshell you
+need: the puppet codebase, ruby versions, and dependencies. Once you've got all
+of that in place you can make sure that you have a working development system
+by running the puppet spec tests.
+
+## The Puppet Codebase
+
+In order to contribute to puppet you'll need to have a github account. Once you
+have your account, fork the puppetlabs/puppet repo, and clone it onto your
+local machine. The [github docs have a good
+explanation](https://help.github.com/articles/fork-a-repo) of how to do all of
+this.
+
+## Ruby versions
+
+Puppet needs to work across a variety of ruby versions. At a minimum you need
+to try any changes you make on both ruby 1.8.7 and ruby 1.9.3. Ruby 2.0.0 and
+2.1.0 are also supported, but they have small enough differences from 1.9.3
+that they are not as important to always check while developing.
+
+Popular ways of making sure you have access to the various versions of ruby are
+to use either [rbenv](https://github.com/sstephenson/rbenv) or
+[rvm](http://rvm.io/). You can read up on the linked sites for how to get them
+installed on your system.
+
+## Dependencies
+
+Make sure you have [bundler](http://bundler.io/) installed. This should be as
+simple as:
+
+ $ gem install bundler
+
+Now you can get all of the dependencies using:
+
+ $ bundle install --path .bundle/gems/
+
+Once this is done, you can interact with puppet through bundler using `bundle
+exec <command>` which will ensure that `<command>` is executed in the context
+of puppet's dependencies.
+
+For example to run the specs:
+
+ $ bundle exec rake spec
+
+To run puppet itself (for a resource lookup say):
+
+ $ bundle exec puppet resource host localhost
+
+## Running Spec Tests
+
+Puppet Labs projects use a common convention of using Rake to run unit tests.
+The tests can be run with the following rake task:
+
+ bundle exec rake spec
+
+To run a single file's worth of tests (much faster!), give the filename:
+
+ bundle exec rake spec TEST=spec/unit/ssl/host_spec.rb
diff --git a/docs/rspec_tutorial.md b/docs/rspec_tutorial.md
new file mode 100644
index 000000000..c22e5c6ad
--- /dev/null
+++ b/docs/rspec_tutorial.md
@@ -0,0 +1,364 @@
+# A brief introduction to testing in Puppet
+
+Puppet relies heavily on automated testing to ensure that Puppet behaves as
+expected and that new features don't interfere with existing behavior. There are
+three primary sets of tests that Puppet uses: _unit tests_, _integration tests_,
+and _acceptance tests_.
+
+- - -
+
+Unit tests are used to test the individual components of Puppet to ensure that
+they function as expected in isolation. Unit tests are designed to hide the
+actual system implementations and provide canned information so that only the
+intended behavior is tested, rather than the targeted code and everything else
+connected to it. Unit tests should never affect the state of the system that's
+running the test.
+
+- - -
+
+Integration tests serve to test different units of code together to ensure that
+they interact correctly. While individual methods might perform correctly, when
+used with the rest of the system they might fail, so integration tests are a
+higher level version of unit tests that serve to check the behavior of
+individual subsystems.
+
+All of the unit and integration tests for Puppet are kept in the spec/ directory.
+
+- - -
+
+Acceptance tests are used to test high level behaviors of Puppet that deal with
+a number of concerns and aren't easily tested with normal unit tests. Acceptance
+tests function by changing system state and checking the system after
+the fact to make sure that the intended behavior occurred. Because of this
+acceptance tests can be destructive, so the systems being tested should be
+throwaway systems.
+
+All of the acceptance tests for Puppet are kept in the acceptance/tests/
+directory. Running the acceptance tests is much more involved than running the
+spec tests. Information about how to run them can be found in the [acceptance
+testing documentation](acceptance_tests.md)
+
+## Testing dependency version requirements
+
+Puppet is only compatible with certain versions of RSpec and Mocha. If you are
+not using Bundler to install the required test libraries you must ensure that
+you are using the right library versions. Using unsupported versions of Mocha
+and RSpec will probably display many spurious failures. The supported versions
+of RSpec and Mocha can be found in the project Gemfile.
+
+## Puppet Continuous integration
+
+ * Travis-ci (spec tests only): https://travis-ci.org/puppetlabs/puppet/
+ * Jenkins (spec and acceptance tests): https://jenkins.puppetlabs.com/view/Puppet%20FOSS/
+
+## RSpec
+
+Puppet uses RSpec to perform unit and integration tests. RSpec handles a number
+of concerns to make testing easier:
+
+ * Executing examples and ensuring the actual behavior matches the expected behavior (examples)
+ * Grouping tests (describe and contexts)
+ * Setting up test environments and cleaning up afterwards (before and after blocks)
+ * Isolating tests (mocks and stubs)
+
+#### Examples and expectations
+
+At the most basic level, RSpec provides a framework for executing tests (which
+are called examples) and ensuring that the actual behavior matches the expected
+behavior (which are done with expectations)
+
+```ruby
+# This is an example; it sets the test name and defines the test to run
+specify "one equals one" do
+ # 'should' is an expectation; it adds a check to make sure that the left argument
+ # matches the right argument
+ 1.should == 1
+end
+
+# Examples can be declared with either 'it' or 'specify'
+it "one doesn't equal two" do
+ 1.should_not == 2
+end
+```
+
+Good examples generally do as little setup as possible and only test one or two
+things; it makes tests easier to understand and easier to debug.
+
+More complete documentation on expectations is available at https://www.relishapp.com/rspec/rspec-expectations/docs
+
+### Example groups
+
+Example groups are fairly self explanatory; they group similar examples into a
+set.
+
+```ruby
+describe "the number one" do
+
+ it "is larger than zero" do
+ 1.should be > 0
+ end
+
+ it "is an odd number" do
+ 1.odd?.should be true
+ end
+
+ it "is not nil" do
+ 1.should_not be_nil
+ end
+end
+```
+
+Example groups have a number of uses that we'll get into later, but one of the
+simplest demonstrations of what they do is how they help to format
+documentation:
+
+```
+rspec ex.rb --format documentation
+
+the number one
+ is larger than zero
+ is an odd number
+ is not nil
+
+Finished in 0.00516 seconds
+3 examples, 0 failures
+```
+
+### Setting up and tearing down tests
+
+Examples may require some setup before they can run, and might need to clean up
+afterwards. `before` and `after` blocks can be used before this, and can be
+used inside of example groups to limit how many examples they affect.
+
+```ruby
+
+describe "something that could warn" do
+ before :each do
+ # Disable warnings for this test
+ $VERBOSE = nil
+ end
+
+ after :each do
+ # Enable warnings afterwards
+ $VERBOSE = true
+ end
+
+ it "doesn't generate a warning" do
+ MY_CONSTANT = 1
+ # reassigning a constant normally prints out 'warning: already initialized constant FOO'
+ MY_CONSTANT = 2
+ end
+end
+```
+
+### Setting up helper data
+
+Some examples may require setting up data before hand and making it available to
+tests. RSpec provides helper methods with the `let` method call that can be used
+inside of tests.
+
+```ruby
+describe "a helper object" do
+ # This creates an array with three elements that we can retrieve in tests. A
+ # new copy will be made for each test.
+ let(:my_helper) do
+ ['foo', 'bar', 'baz']
+ end
+
+ it "is an array" do
+ my_helper.should be_a_kind_of Array
+ end
+
+ it "has three elements" do
+ my_helper.should have(3).items
+ end
+end
+```
+
+Like `before` blocks, helper objects like this are used to avoid doing a lot of
+setup in individual examples and share setup between similar tests.
+
+### Isolating tests with stubs
+
+RSpec allows you to provide fake data during testing to make sure that
+individual tests are only running the code being tested. You can stub out entire
+objects, or just stub out individual methods on an object. When a method is
+stubbed the method itself will never be called.
+
+While RSpec comes with its own stubbing framework, Puppet uses the Mocha
+framework.
+
+A brief usage guide for Mocha is available at http://gofreerange.com/mocha/docs/#Usage,
+and an overview of Mocha expectations is available at http://gofreerange.com/mocha/docs/Mocha/Expectation.html
+
+```ruby
+describe "stubbing a method on an object" do
+ let(:my_helper) do
+ ['foo', 'bar', 'baz']
+ end
+
+ it 'has three items before being stubbed' do
+ my_helper.size.should == 3
+ end
+
+ describe 'when stubbing the size' do
+ before :each do
+ my_helper.stubs(:size).returns 10
+ end
+
+ it 'has the stubbed value for size' do
+ my_helper.size.should == 10
+ end
+ end
+end
+```
+
+Entire objects can be stubbed as well.
+
+```ruby
+describe "stubbing an object" do
+ let(:my_helper) do
+ stub(:not_an_array, :size => 10)
+ end
+
+ it 'has the stubbed size'
+ my_helper.size.should == 10
+ end
+end
+```
+
+### Adding expectations with mocks
+
+It's possible to combine the concepts of stubbing and expectations so that a
+method has to be called for the test to pass (like an expectation), and can
+return a fixed value (like a stub).
+
+```ruby
+describe "mocking a method on an object" do
+ let(:my_helper) do
+ ['foo', 'bar', 'baz']
+ end
+
+ describe "when mocking the size" do
+ before :each do
+ my_helper.expects(:size).returns 10
+ end
+
+ it "adds an expectation that a method was called" do
+ my_helper.size
+ end
+ end
+end
+```
+
+Like stubs, entire objects can be mocked.
+
+```ruby
+describe "mocking an object" do
+ let(:my_helper) do
+ mock(:not_an_array)
+ end
+
+ before :each do
+ not_an_array.expects(:size).returns 10
+ end
+
+ it "adds an expectation that the method was called" do
+ not_an_array.size
+ end
+end
+```
+### Writing tests without side effects
+
+When properly written each test should be able to run in isolation, and tests
+should be able to be run in any order. This makes tests more reliable and allows
+a single test to be run if only that test is failing, instead of running all
+17000+ tests each time something is changed. However, there are a number of ways
+that can make tests fail when run in isolation or out of order.
+
+#### Using instance variables
+
+Puppet has a number of older tests that use `before` blocks and instance
+variables to set up fixture data, instead of `let` blocks. These can retain
+state between tests, which can lead to test failures when tests are run out of
+order.
+
+```ruby
+# test.rb
+RSpec.configure do |c|
+ c.mock_framework = :mocha
+end
+
+describe "fixture data" do
+ describe "using instance variables" do
+
+ # BAD
+ before :all do
+ # This fixture will be created only once and will retain the `foo` stub
+ # between tests.
+ @fixture = stub 'test data'
+ end
+
+ it "can be stubbed" do
+ @fixture.stubs(:foo).returns :bar
+ @fixture.foo.should == :bar
+ end
+
+ it "does not keep state between tests" do
+ # The foo stub was added in the previous test and shouldn't be present
+ # in this test.
+ expect { @fixture.foo }.to raise_error
+ end
+ end
+
+ describe "using `let` blocks" do
+
+ # GOOD
+ # This will be recreated between tests so that state isn't retained.
+ let(:fixture) { stub 'test data' }
+
+ it "can be stubbed" do
+ fixture.stubs(:foo).returns :bar
+ fixture.foo.should == :bar
+ end
+
+ it "does not keep state between tests" do
+ # since let blocks are regenerated between tests, the foo stub added in
+ # the previous test will not be present here.
+ expect { fixture.foo }.to raise_error
+ end
+ end
+end
+```
+
+```
+bundle exec rspec test.rb -fd
+
+fixture data
+ using instance variables
+ can be stubbed
+ should not keep state between tests (FAILED - 1)
+ using `let` blocks
+ can be stubbed
+ should not keep state between tests
+
+Failures:
+
+ 1) fixture data using instance variables should not keep state between tests
+ Failure/Error: expect { @fixture.foo }.to raise_error
+ expected Exception but nothing was raised
+ # ./test.rb:17:in `block (3 levels) in <top (required)>'
+
+Finished in 0.00248 seconds
+4 examples, 1 failure
+
+Failed examples:
+
+rspec ./test.rb:16 # fixture data using instance variables should not keep state between tests
+```
+
+### RSpec references
+
+ * RSpec core docs: https://www.relishapp.com/rspec/rspec-core/docs
+ * RSpec guidelines with Ruby: http://betterspecs.org/
+
diff --git a/docs/static_compiler.md b/docs/static_compiler.md
new file mode 100644
index 000000000..8a3270eab
--- /dev/null
+++ b/docs/static_compiler.md
@@ -0,0 +1,83 @@
+# Static Compiler
+
+The static compiler was added to Puppet in the 2.7.0 release.
+[1](http://links.puppetlabs.com/static-compiler-announce)
+
+The static compiler is intended to provide a configuration catalog that
+requires a minimal amount of network communication in order to apply the
+catalog to the system. As implemented in Puppet 2.7.x and Puppet 3.0.x this
+intention takes the form of replacing all of the source parameters of File
+resources with a content parameter containing an address in the form of a
+checksum. The expected behavior is that the process applying the catalog to
+the node will retrieve the file content from the FileBucket instead of the
+FileServer.
+
+The high level approach can be described as follows. The `StaticCompiler` is a
+terminus that inserts itself between the "normal" compiler terminus and the
+request. The static compiler takes the resource catalog produced by the
+compiler and filters all File resources. Any file resource that contains a
+source parameter with a value starting with 'puppet://' is filtered in the
+following way in a "standard" single master / networked agents deployment
+scenario:
+
+ 1. The content, owner, group, and mode values are retrieved from th
+ FileServer by the master.
+ 2. The file content is stored in the file bucket on the master.
+ 3. The source parameter value is stripped from the File resource.
+ 4. The content parameter value is set in the File resource using the form
+ '{XXX}1234567890' which can be thought of as a content address indexed by
+ checksum.
+ 5. The owner, group and mode values are set in the File resource if they are
+ not already set.
+ 6. The filtered catalog is returned in the response.
+
+In addition to the catalog terminus, the process requesting the catalog needs
+to obtain the file content. The default behavior of `puppet agent` is to
+obtain file contents from the local client bucket. The method we expect users
+to employ to reconfigure the agent to use the server bucket is to declare the
+`Filebucket[puppet]` resource with the address of the master. For example:
+
+ node default {
+ filebucket { puppet:
+ server => $server,
+ path => false,
+ }
+ class { filetest: }
+ }
+
+This special filebucket resource named "puppet" will cause the agent to fetch
+file contents specified by checksum from the remote filebucket instead of the
+default clientbucket.
+
+## Trying out the Static Compiler
+
+Create a module that recursively downloads something. The jeffmccune-filetest
+module will recursively copy the rubygems source tree.
+
+ $ bundle exec puppet module install jeffmccune-filetest
+
+Start the master with the StaticCompiler turned on:
+
+ $ bundle exec puppet master \
+ --catalog_terminus=static_compiler \
+ --verbose \
+ --no-daemonize
+
+Add the special Filebucket[puppet] resource:
+
+ # site.pp
+ node default {
+ filebucket { puppet: server => $server, path => false }
+ class { filetest: }
+ }
+
+Get the static catalog:
+
+ $ bundle exec puppet agent --test
+
+You should expect all file metadata to be contained in the catalog, including a
+checksum representing the content. When managing an out of sync file resource,
+the real contents should be fetched from the server instead of the
+clientbucket.
+
+
diff --git a/docs/unicode.md b/docs/unicode.md
new file mode 100644
index 000000000..a6c81df82
--- /dev/null
+++ b/docs/unicode.md
@@ -0,0 +1,56 @@
+# UTF-8 Handling #
+
+As Ruby 1.9 becomes more commonly used with Puppet, developers should be aware
+of major changes to the way Strings and Regexp objects are handled.
+Specifically, every instance of these two classes will have an encoding
+attribute determined in a number of ways.
+
+ * If the source file has an encoding specified in the magic comment at the
+ top, the instance will take on that encoding.
+ * Otherwise, the encoding will be determined by the LC\_LANG or LANG
+ environment variables.
+ * Otherwise, the encoding will default to ASCII-8BIT
+
+## References ##
+
+Excellent information about the differences between encodings in Ruby 1.8 and
+Ruby 1.9 is published in this blog series:
+[Understanding M17n](http://links.puppetlabs.com/understanding_m17n)
+
+## Encodings of Regexp and String instances ##
+
+In general, please be aware that Ruby 1.9 regular expressions need to be
+compatible with the encoding of a string being used to match them. If they are
+not compatible you can expect to receive and error such as:
+
+ Encoding::CompatibilityError: incompatible encoding regexp match (ASCII-8BIT
+ regexp with UTF-8 string)
+
+In addition, some escape sequences were valid in Ruby 1.8 are no longer valid
+in 1.9 if the regular expression is not marked as an ASCII-8BIT object. You
+may expect errors like this in this situation:
+
+ SyntaxError: (irb):7: invalid multibyte escape: /\xFF/
+
+This error is particularly common when serializing a string to other
+representations like JSON or YAML. To resolve the problem you can explicitly
+mark the regular expression as ASCII-8BIT using the /n flag:
+
+ "a" =~ /\342\230\203/n
+
+Finally, any time you're thinking of a string as an array of bytes rather than
+an array of characters, common when escaping a string, you should work with
+everything in ASCII-8BIT. Changing the encoding will not change the data
+itself and allow the Regexp and the String to deal with bytes rather than
+characters.
+
+Puppet provides a monkey patch to String which returns an encoding suitable for
+byte manipulations:
+
+ # Example of how to escape non ASCII printable characters for YAML.
+ >> snowman = "☃"
+ >> snowman.to_ascii8bit.gsub(/([\x80-\xFF])/n) { |x| "\\x#{x.unpack("C")[0].to_s(16)} }
+ => "\\xe2\\x98\\x83"
+
+If the Regexp is not marked as ASCII-8BIT using /n, then you can expect the
+SyntaxError, invalid multibyte escape as mentioned above.
diff --git a/docs/windows.md b/docs/windows.md
new file mode 100644
index 000000000..1a61f8b45
--- /dev/null
+++ b/docs/windows.md
@@ -0,0 +1,73 @@
+# Windows #
+
+If you'd like to run Puppet from source on Windows platforms, the
+include `ext/envpuppet.bat` will help.
+
+To quickly run Puppet from source, assuming you already have Ruby installed
+from [rubyinstaller.org](http://rubyinstaller.org).
+
+ C:\> cd C:\work\puppet
+ C:\work\puppet> set PATH=%PATH%;C:\work\puppet\ext
+ C:\work\puppet> envpuppet bundle install
+ C:\work\puppet> envpuppet puppet --version
+ 2.7.9
+
+When writing a test that cannot possibly run on Windows, e.g. there is
+no mount type on windows, do the following:
+
+ describe Puppet::MyClass, :unless => Puppet.features.microsoft_windows? do
+ ..
+ end
+
+If the test doesn't currently pass on Windows, e.g. due to on going porting, then use an rspec conditional pending block:
+
+ pending("porting to Windows", :if => Puppet.features.microsoft_windows?) do
+ <example1>
+ end
+
+ pending("porting to Windows", :if => Puppet.features.microsoft_windows?) do
+ <example2>
+ end
+
+Then run the test as:
+
+ C:\work\puppet> envpuppet bundle exec rspec spec
+
+## Common Issues ##
+
+ * Don't assume file paths start with '/', as that is not a valid path on
+ Windows. Use Puppet::Util.absolute\_path? to validate that a path is fully
+ qualified.
+
+ * Use File.expand\_path('/tmp') in tests to generate a fully qualified path
+ that is valid on POSIX and Windows. In the latter case, the current working
+ directory will be used to expand the path.
+
+ * Always use binary mode when performing file I/O, unless you explicitly want
+ Ruby to translate between unix and dos line endings. For example, opening an
+ executable file in text mode will almost certainly corrupt the resulting
+ stream, as will occur when using:
+
+ IO.open(path, 'r') { |f| ... }
+ IO.read(path)
+
+ If in doubt, specify binary mode explicitly:
+
+ IO.open(path, 'rb')
+
+ * Don't assume file paths are separated by ':'. Use `File::PATH_SEPARATOR`
+ instead, which is ':' on POSIX and ';' on Windows.
+
+ * On Windows, `File::SEPARATOR` is '/', and `File::ALT_SEPARATOR` is '\'. On
+ POSIX systems, `File::ALT_SEPARATOR` is nil. In general, use '/' as the
+ separator as most Windows APIs, e.g. CreateFile, accept both types of
+ separators.
+
+ * Don't use waitpid/waitpid2 if you need the child process' exit code,
+ as the child process may exit before it has a chance to open the
+ child's HANDLE and retrieve its exit code. Use Puppet::Util.execute.
+
+ * Don't assume 'C' drive. Use environment variables to look these up:
+
+ "#{ENV['windir']}/system32/netsh.exe"
+
diff --git a/ext/build_defaults.yaml b/ext/build_defaults.yaml
index a4f4aed0f..04bc97ca4 100644
--- a/ext/build_defaults.yaml
+++ b/ext/build_defaults.yaml
@@ -1,23 +1,23 @@
---
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-squeeze-i386.cow base-stable-i386.cow base-testing-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 pl-fedora-20-i386'
+final_mocks: 'pl-el-5-i386 pl-el-6-i386 pl-el-7-x86_64 pl-fedora-19-i386 pl-fedora-20-i386'
yum_host: 'yum.puppetlabs.com'
yum_repo_path: '/opt/repository/yum/'
build_gem: TRUE
build_dmg: TRUE
apt_host: 'apt.puppetlabs.com'
apt_repo_url: 'http://apt.puppetlabs.com'
apt_repo_path: '/opt/repository/incoming'
ips_repo: '/var/pkgrepo'
ips_store: '/opt/repository'
ips_host: 'solaris-11-ips-repo.acctest.dc1.puppetlabs.net'
tar_host: 'downloads.puppetlabs.com'
diff --git a/ext/debian/control b/ext/debian/control
index 7ee8ee831..d294e3b69 100644
--- a/ext/debian/control
+++ b/ext/debian/control
@@ -1,143 +1,143 @@
Source: puppet
Section: admin
Priority: optional
Maintainer: Puppet Labs <info@puppetlabs.com>
Uploaders: Micah Anderson <micah@debian.org>, Andrew Pollock <apollock@debian.org>, Nigel Kersten <nigel@explanatorygap.net>, Stig Sandbeck Mathisen <ssm@debian.org>
-Build-Depends-Indep: ruby | ruby-interpreter, libopenssl-ruby, facter (>= 1.6.12)
+Build-Depends-Indep: ruby | ruby-interpreter, libopenssl-ruby, facter (>= 1.7.0)
Build-Depends: debhelper (>= 7.0.0), openssl
Standards-Version: 3.9.1
Vcs-Git: git://github.com/puppetlabs/puppet
Homepage: http://projects.puppetlabs.com/projects/puppet
Package: puppet-common
Architecture: all
-Depends: ${misc:Depends}, ruby | ruby-interpreter, libxmlrpc-ruby, libopenssl-ruby, ruby-shadow | libshadow-ruby1.8, libaugeas-ruby | libaugeas-ruby1.9.1 | libaugeas-ruby1.8, adduser, lsb-base, sysv-rc (>= 2.86) | file-rc, hiera (>= 1.0.0), facter (>= 1.6.12), ruby-rgen (>= 0.6.5)
+Depends: ${misc:Depends}, ruby | ruby-interpreter, libxmlrpc-ruby, libopenssl-ruby, ruby-shadow | libshadow-ruby1.8, libaugeas-ruby | libaugeas-ruby1.9.1 | libaugeas-ruby1.8, adduser, lsb-base, sysv-rc (>= 2.86) | file-rc, hiera (>= 1.0.0), facter (>= 1.7.0), ruby-rgen (>= 0.6.5), libjson-ruby | ruby-json
Recommends: lsb-release, debconf-utils
Suggests: ruby-selinux | libselinux-ruby1.8, librrd-ruby1.9.1 | librrd-ruby1.8
Breaks: puppet (<< 2.6.0~rc2-1), puppetmaster (<< 0.25.4-1)
Provides: hiera-puppet
Conflicts: hiera-puppet
Replaces: hiera-puppet
Description: Centralized configuration management
Puppet lets you centrally manage every important aspect of your system
using a cross-platform specification language that manages all the
separate elements normally aggregated in different files, like users,
cron jobs, and hosts, along with obviously discrete elements like
packages, services, and files.
.
Puppet's simple declarative specification language provides powerful
classing abilities for drawing out the similarities between hosts while
allowing them to be as specific as necessary, and it handles dependency
and prerequisite relationships between objects clearly and explicitly.
.
This package contains the puppet software and documentation. For the startup
scripts needed to run the puppet agent and master, see the "puppet" and
"puppetmaster" packages, respectively.
Package: puppet
Architecture: all
Depends: ${misc:Depends}, puppet-common (= ${binary:Version}), ruby | ruby-interpreter
Recommends: rdoc
Suggests: puppet-el, vim-puppet
Description: Centralized configuration management - agent startup and compatibility scripts
This package contains the startup script and compatbility scripts for the
puppet agent, which is the process responsible for configuring the local node.
.
Puppet lets you centrally manage every important aspect of your system
using a cross-platform specification language that manages all the
separate elements normally aggregated in different files, like users,
cron jobs, and hosts, along with obviously discrete elements like
packages, services, and files.
.
Puppet's simple declarative specification language provides powerful
classing abilities for drawing out the similarities between hosts while
allowing them to be as specific as necessary, and it handles dependency
and prerequisite relationships between objects clearly and explicitly.
Package: puppetmaster-common
Architecture: all
-Depends: ${misc:Depends}, ruby | ruby-interpreter, puppet-common (= ${binary:Version}), facter (>= 1.6.12), lsb-base
+Depends: ${misc:Depends}, ruby | ruby-interpreter, puppet-common (= ${binary:Version}), facter (>= 1.7.0), lsb-base
Breaks: puppet (<< 0.24.7-1), puppetmaster (<< 2.6.1~rc2-1)
Replaces: puppetmaster (<< 2.6.1~rc2-1)
Suggests: apache2 | nginx, puppet-el, vim-puppet, stompserver, ruby-stomp | libstomp-ruby1.8,
rdoc, ruby-ldap | libldap-ruby1.8, puppetdb-terminus
Description: Puppet master common scripts
This package contains common scripts for the puppet master,
which is the server hosting manifests and files for the puppet nodes.
.
Puppet lets you centrally manage every important aspect of your system
using a cross-platform specification language that manages all the
separate elements normally aggregated in different files, like users,
cron jobs, and hosts, along with obviously discrete elements like
packages, services, and files.
.
Puppet's simple declarative specification language provides powerful
classing abilities for drawing out the similarities between hosts while
allowing them to be as specific as necessary, and it handles dependency
and prerequisite relationships between objects clearly and explicitly.
Package: puppetmaster
Architecture: all
-Depends: ${misc:Depends}, ruby | ruby-interpreter, puppetmaster-common (= ${source:Version}), facter (>= 1.6.12), lsb-base
+Depends: ${misc:Depends}, ruby | ruby-interpreter, puppetmaster-common (= ${source:Version}), facter (>= 1.7.0), lsb-base
Breaks: puppet (<< 0.24.7-1)
Suggests: apache2 | nginx, puppet-el, vim-puppet, stompserver, ruby-stomp | libstomp-ruby1.8,
rdoc, ruby-ldap | libldap-ruby1.8, puppetdb-terminus
Description: Centralized configuration management - master startup and compatibility scripts
This package contains the startup and compatibility scripts for the puppet
master, which is the server hosting manifests and files for the puppet nodes.
.
Puppet lets you centrally manage every important aspect of your system
using a cross-platform specification language that manages all the
separate elements normally aggregated in different files, like users,
cron jobs, and hosts, along with obviously discrete elements like
packages, services, and files.
.
Puppet's simple declarative specification language provides powerful
classing abilities for drawing out the similarities between hosts while
allowing them to be as specific as necessary, and it handles dependency
and prerequisite relationships between objects clearly and explicitly.
Package: puppetmaster-passenger
Architecture: all
-Depends: ${misc:Depends}, ruby | ruby-interpreter, puppetmaster-common (= ${source:Version}), facter (>= 1.6.12), lsb-base, libapache2-mod-passenger
+Depends: ${misc:Depends}, ruby | ruby-interpreter, puppetmaster-common (= ${source:Version}), facter (>= 1.7.0), lsb-base, libapache2-mod-passenger
Conflicts: puppetmaster (<< 2.6.1~rc2-1)
Replaces: puppetmaster (<< 2.6.1~rc2-1)
Description: Centralised configuration management - master setup to run under mod passenger
This package provides a puppetmaster running under mod passenger.
This configuration offers better performance and scalability.
.
Puppet lets you centrally manage every important aspect of your system
using a cross-platform specification language that manages all the
separate elements normally aggregated in different files, like users,
cron jobs, and hosts, along with obviously discrete elements like
packages, services, and files.
.
Puppet's simple declarative specification language provides powerful
classing abilities for drawing out the similarities between hosts while
allowing them to be as specific as necessary, and it handles dependency
and prerequisite relationships between objects clearly and explicitly.
.
Package: vim-puppet
Architecture: all
Depends: ${misc:Depends}
Recommends: vim-addon-manager
Conflicts: puppet (<< ${source:Version})
Description: syntax highlighting for puppet manifests in vim
The vim-puppet package provides filetype detection and syntax highlighting for
puppet manifests (files ending with ".pp").
Package: puppet-el
Architecture: all
Depends: ${misc:Depends}, emacsen-common
Conflicts: puppet (<< ${source:Version})
Description: syntax highlighting for puppet manifests in emacs
The puppet-el package provides syntax highlighting for puppet manifests
Package: puppet-testsuite
Architecture: all
-Depends: ${misc:Depends}, ruby | ruby-interpreter, puppet-common (= ${source:Version}), facter (>= 1.6.12), lsb-base, rails (>= 1.2.3-2), rdoc, ruby-ldap | libldap-ruby1.8, ruby-rspec | librspec-ruby, git-core, ruby-mocha | libmocha-ruby1.8
+Depends: ${misc:Depends}, ruby | ruby-interpreter, puppet-common (= ${source:Version}), facter (>= 1.7.0), lsb-base, rails (>= 1.2.3-2), rdoc, ruby-ldap | libldap-ruby1.8, ruby-rspec | librspec-ruby, git-core, ruby-mocha | libmocha-ruby1.8
Recommends: cron
Description: Centralized configuration management - test suite
This package provides all the tests from the upstream puppet source code.
The tests are used for improving the QA of the puppet package.
diff --git a/ext/ips/transforms b/ext/ips/transforms
index cd49b2d2e..be91f74eb 100644
--- a/ext/ips/transforms
+++ b/ext/ips/transforms
@@ -1,34 +1,34 @@
<transform file dir link hardlink path=usr/share/man/.+(/.+)? -> default facet.doc.man true>
<transform file path=usr/share/man/.+(/.+)? -> add restart_fmri svc:/application/man-index:default>
# drop opt and user
<transform dir path=(lib|etc|usr|var)$->drop>
<transform dir path=usr/(share|ruby)$->drop>
<transform dir path=usr/share/man$->drop>
<transform dir path=usr/ruby/1.8$->drop>
<transform dir path=usr/ruby/1.8/lib$->drop>
<transform dir path=usr/ruby/1.8/ruby$->drop>
<transform dir path=usr/ruby/1.8/ruby/1.8$->drop>
<transform dir path=(var|lib)/svc$->drop>
<transform dir path=lib/svc/method$->drop>
<transform dir path=var/svc/manifest$->drop>
<transform dir path=var/svc/manifest/network$->drop>
# drop var/lib var/log
<transform dir path=var/(lib|log)$->drop>
# saner dependencies
<transform depend -> edit fmri "@[^ \t\n\r\f\v]*" "">
# make sure /var/log/puppet and /var/lib/puppet are owned by puppet
<transform dir path=var/(log|lib)/puppet$ -> edit group bin puppet>
<transform dir path=var/(log|lib)/puppet$ -> edit owner root puppet>
<transform file path=var/svc/manifest/.*\.xml -> add restart_fmri svc:/system/manifest-import:default>
# we depend on facter
-<transform pkg -> emit depend type=require fmri=application/facter@1.6.12>
+<transform pkg -> emit depend type=require fmri=application/facter@1.7.0>
# preserve the old conf file on upgrade.
<transform file path=etc/puppet/(puppet|auth).conf -> add overlay true>
<transform file path=etc/puppet/(puppet|auth).conf -> add preserve renamenew>
diff --git a/ext/nagios/naggen b/ext/nagios/naggen
index 6d5be7b0f..0d0e5824f 100755
--- a/ext/nagios/naggen
+++ b/ext/nagios/naggen
@@ -1,309 +1,309 @@
#!/usr/bin/env ruby
#
# = Synopsis
#
# Generate Nagios configurations from Puppet Resources in an ActiveRecord database
#
# = Usage
#
# naggen [-h|--help] [-d|--debug] [-v|--verbose] [--compare]
#
# = Description
#
# This executable is a means of short-circuiting the process of generating nagios
# configurations on your server using Puppet Exported Resources. It skips any possible
# naming conflicts resulting from Puppet's resource uniqueness requirements, and it
# also skips the inefficiencies involved in converting and transporting large numbers
# of Puppet resources.
#
# At the least, the machine that runs this will need ActiveRecord (2.0.2) installed,
# along with any database libraries you use.
#
# = Options
#
-# Note that any configuration parameter that's valid in the configuration file
+# Note that any setting that's valid in the configuration file
# is also a valid long argument. For example, 'ssldir' is a valid configuration
# parameter, so you can specify '--ssldir <directory>' as an argument.
#
# You can add naggen-specific settings to your puppet.conf in a '[naggen]' section,
# just like any other executable.
#
# See the configuration file documentation at
# http://reductivelabs.com/projects/puppet/reference/configref.html for
# the full list of acceptable parameters. A commented list of all
# configuration options can also be generated by running puppet with
# '--genconfig'.
#
# compare::
# Compare new and old files and only backup and write if the files are different.
# Potentially expensive computationally, but idempotent. Will exit with 0 if
# no changes were made and 1 if there were.
#
# debug::
# Enable full debugging.
#
# detailed-exitcodes::
# Provide transaction information via exit codes. If this is enabled, an exit
# code of '2' means there were changes, and an exit code of '4' means that there
# were failures during the transaction.
#
# help::
# Print this help message
#
# verbose::
# Print extra information.
#
# = Example
#
# naggen --storeconfigs --confdir /foo --compare
#
#
# = License
# Copyright 2011 Luke Kanies
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
require 'puppet'
require 'puppet/rails'
require 'puppet/rails/resource'
require 'puppet/rails/param_value'
require 'puppet/network/client'
require 'puppet/parser/collector'
require 'puppet/provider/naginator'
require 'getoptlong'
# Monkey-patch the rails resources so we can
# easily convert them to nagios instances.
class Puppet::Rails::Resource
def to_param_hash
values = @params_hash || Puppet::Rails::ParamValue.find_all_params_from_resource(self)
if values.size == 0
return {}
end
values.inject({}) do |hash, value|
hash[value['name']] ||= []
hash[value['name']] << value["value"]
hash
end
end
def to_nagios
unless nagios_type = Nagios::Base.type(restype.sub("Nagios_", '').to_sym)
raise Puppet::DevError, "Could not find nagios type '%s'" % restype
end
result = nagios_type.new
to_param_hash.each do |param, value|
next unless nagios_type.parameter?(param)
result[param] = value
end
result[:name] = self.title
result
end
end
class NagiosWriter
class FakeScope
def debug(string)
Puppet.debug string
end
def host
"this host doesn't exist"
end
end
attr_accessor :nagios_type, :bucket
def backup(target)
return unless FileTest.exist?(target) and File.stat(target).size > 0
Puppet.info "Backing up %s" % target
bucket.backup(target)
end
def collector
collector = Puppet::Parser::Collector.new(FakeScope.new, "nagios_" + @nagios_type.to_s, nil, nil, :exported)
# We don't have a scope, so we're stubbing everything out that would interact
# with the scope.
class << collector
def collect_virtual(*args)
[]
end
def exported_resource(res)
res
end
end
collector
end
def default_target
"/etc/nagios/nagios_#{nagios_type.to_s}.cfg"
end
def evaluate
return unless resources = rails_resources()
resources_by_target = resources.inject({}) do |hash, resource|
target = resource["target"] || default_target
hash[target] ||= []
hash[target] << resource
hash
end
changed = false
resources_by_target.each do |target, resources|
begin
result = write(target, resources)
rescue => detail
$stderr.puts detail.backtrace
Puppet.err "Could not write to %s: %s" % [target, detail]
end
changed = true if result
end
changed
end
def initialize(nagios_type)
@nagios_type = nagios_type
@bucket = Puppet::FileBucket::Dipper.new(:Path => Puppet[:clientbucketdir])
end
def rails_resources
collector.send(:collect_exported)
end
def write(target, resources)
# Skip the nagios type when we have no resources and no existing
# file.
return if resources.empty? and ! FileTest.exist?(target)
dir = File.dirname(target)
unless FileTest.exist?(dir)
FileUtils.mkdir_p(dir)
end
count = 0
tempfile = target + ".tmp"
File.open(tempfile, "w") do |file|
resources.each do |resource|
count += 1
file.puts resource.to_nagios.to_s.gsub("_naginator_name", Puppet::Provider::Naginator::NAME_STRING)
end
end
if $options[:compare]
if FileTest.exist?(target) and File.read(tempfile) == File.read(target)
return false
end
end
backup(target)
# Atomic rename
File.rename(tempfile, target)
Puppet.notice "Wrote %s resources to %s" % [count, target]
return true
ensure
File.unlink(tempfile) if tempfile and FileTest.exist?(tempfile)
end
end
arguments = [
[ "--compare", "-c", GetoptLong::NO_ARGUMENT ],
[ "--debug", "-d", GetoptLong::NO_ARGUMENT ],
[ "--verbose", "-v", GetoptLong::NO_ARGUMENT ],
[ "--help", "-h", GetoptLong::NO_ARGUMENT ]
]
Puppet.settings.addargs(arguments)
result = GetoptLong.new(*arguments)
$options = {}
result.each { |opt,arg|
case opt
when "--help"
begin
require 'rdoc/usage'
RDoc::usage && exit
rescue LoadError
docs = []
File.readlines(__FILE__).each do |line|
next if line =~ /^#\!/
unless line =~ /^#/
next if docs.length == 0 # skip the first line or so
break # else, we've passed the docs, so just break
end
docs << line.sub(/^# ?/, '')
end
print docs
exit
end
when "--compare"
$options[:compare] = true
when "--verbose"
$options[:verbose] = true
when "--debug"
$options[:debug] = true
when "--debug"
$options[:debug] = true
else
Puppet.settings.handlearg(opt, arg)
end
}
# Read in Puppet settings, so we know how Puppet's configured.
Puppet.initialize_settings
Puppet::Util::Log.newdestination(:console)
if $options[:debug]
Puppet::Util::Log.level = :debug
elsif $options[:verbose]
Puppet::Util::Log.level = :info
end
# See if Naginator is installed directly, else load Puppet's version.
begin
require 'nagios'
rescue LoadError
require 'puppet/external/nagios'
end
changed = false
Nagios::Base.eachtype do |name, type|
writer = NagiosWriter.new(name)
changed = true if writer.evaluate
end
if $options[:compare] and changed
exit(1)
else
exit(0)
end
diff --git a/ext/project_data.yaml b/ext/project_data.yaml
index 3df65fea3..ee3fc71f5 100644
--- a/ext/project_data.yaml
+++ b/ext/project_data.yaml
@@ -1,26 +1,46 @@
---
project: 'puppet'
author: 'Puppet Labs'
email: 'info@puppetlabs.com'
homepage: 'https://github.com/puppetlabs/puppet'
summary: 'Puppet, an automated configuration management tool'
description: 'Puppet, an automated configuration management tool'
version_file: 'lib/puppet/version.rb'
# files and gem_files are space separated lists
files: '[A-Z]* install.rb bin lib conf man examples ext tasks spec'
# The gem specification bits only work on Puppet >= 3.0rc, NOT 2.7.x and earlier
gem_files: '[A-Z]* install.rb bin lib conf man examples ext tasks spec'
gem_test_files: 'spec/**/*'
gem_executables: 'puppet'
gem_default_executables: 'puppet'
gem_forge_project: 'puppet'
gem_runtime_dependencies:
facter: ['> 1.6', '< 3']
hiera: '~> 1.0'
rgen: '~> 0.6.5'
+ json_pure:
gem_rdoc_options:
- --title
- "Puppet - Configuration Management"
- --main
- README.md
- --line-numbers
+gem_platform_dependencies:
+ x86-mingw32:
+ gem_runtime_dependencies:
+ # Pinning versions that require native extensions
+ ffi: '1.9.0'
+ sys-admin: '1.5.6'
+ win32-api: '1.4.8'
+ win32-dir: '~> 0.4.3'
+ win32-eventlog: '~> 0.5.3'
+ win32-process: '~> 0.6.5'
+ win32-security: '~> 0.1.4'
+ win32-service: '0.7.2'
+ win32-taskscheduler: '~> 0.2.2'
+ win32console: '1.3.2'
+ windows-api: '~> 0.4.2'
+ windows-pr: '~> 1.2.2'
+ minitar: '~> 0.5.4'
+bundle_platforms:
+ x86-mingw32: mingw
diff --git a/ext/puppet-test b/ext/puppet-test
index c2264da8c..47c464f86 100755
--- a/ext/puppet-test
+++ b/ext/puppet-test
@@ -1,566 +1,566 @@
#!/usr/bin/env ruby
# == Synopsis
#
# Test individual client performance. Can compile configurations, describe
# files, or retrieve files.
#
# = Usage
#
# puppet-test [-c|--compile] [-D|--describe <file>] [-d|--debug]
# [--fork <num>] [-h|--help] [-H|--hostname <host name>] [-l|--list] [-r|--repeat <number=1>]
# [-R|--retrieve <file>] [-t|--test <test>] [-V|--version] [-v|--verbose]
#
# = Description
#
# This is a simple script meant for doing performance tests with Puppet. By
# default it pulls down a compiled configuration, but it supports multiple
# other tests.
#
# = Options
#
-# Note that any configuration parameter that's valid in the configuration file
+# Note that any setting that's valid in the configuration file
# is also a valid long argument. For example, 'server' is a valid configuration
# parameter, so you can specify '--server <servername>' as an argument.
#
# See the configuration file documentation at
# http://reductivelabs.com/projects/puppet/reference/configref.html for
# the full list of acceptable parameters. A commented list of all
# configuration $options can also be generated by running puppetd with
# '--genconfig'.
#
# compile::
# Compile the client's configuration. The default.
#
# debug::
# Enable full debugging.
#
# describe::
# Describe the file being tested. This is a query to get information about
# the file from the server, to determine if it should be copied, and is the
# first half of every file transfer.
#
# fork::
# Fork the specified number of times, thus acting as multiple clients.
#
# fqdn::
# Set the fully-qualified domain name of the client. This is only used for
# certificate purposes, but can be used to override the discovered hostname.
# If you need to use this flag, it is generally an indication of a setup problem.
#
# help::
# Print this help message
#
# list::
# List all available tests.
#
# node::
# Specify the node to use. This is useful for looking up cached yaml data
# in your :clientyaml directory, and forcing a specific host's configuration to
# get compiled.
#
# pause::
# Pause before starting test (useful for testing with dtrace).
#
# repeat::
# How many times to perform the test.
#
# retrieve::
# Test file retrieval performance. Retrieves the specified file from the
# remote system. Note that the server should be specified via --server,
# so the argument to this option is just the remote module name and path,
# e.g., "/dist/apps/myapp/myfile", where "dist" is the module and
# "apps/myapp/myfile" is the path to the file relative to the module.
#
# test::
# Specify the test to run. You can see the list of tests by running this command with --list.
#
# verbose::
# Turn on verbose reporting.
#
# version::
# Print the puppet version number and exit.
#
# = Example
#
# puppet-test --retrieve /module/path/to/file
#
# = License
# Copyright 2011 Luke Kanies
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# Do an initial trap, so that cancels don't get a stack trace.
trap(:INT) do
$stderr.puts "Cancelling startup"
exit(1)
end
require 'puppet'
require 'puppet/network/client'
require 'getoptlong'
class Suite
attr_reader :name, :doc
@@suites = {}
@@tests = {}
def self.[](name)
@@suites[name]
end
# Run a test by first finding the suite then running the appropriate test.
def self.run(test)
unless suite_name = @@tests[test]
raise "Unknown test %s" % test
end
unless suite = @@suites[suite_name]
raise "Unknown test suite %s from test %s" % [suite_name, test]
end
suite.run(test)
end
# What suites are available?
def self.suites
@@suites.keys
end
def forked?
defined? @forking
end
# Create a new test suite.
def initialize(name, doc, &block)
@name = name
@doc = doc
@tests = {}
@@suites[name] = self
raise "You must pass a block to the Test" unless block_given?
instance_eval(&block)
end
# Define a new type of test on this suite.
def newtest(name, doc, &block)
@tests[name] = doc
if @@tests[name]
raise "Test names must be unique; cannot redefine %s" % name
end
@@tests[name] = @name
meta_def(name, &block)
end
# Run the actual test.
def run(test)
unless doc = @tests[test]
raise "Suite %s only supports tests %s; not %s" % [@name, @tests.keys.collect { |k| k.to_s }.join(","), test]
end
puts "Running %s %s test" % [@name, test]
prepare() if respond_to?(:prepare)
if $options[:pause]
puts "Hit any key to continue"
$stdin.readline
puts "Continuing with test"
end
if $options[:fork] > 0
@forking = true
$options[:fork].times {
if pid = fork
$pids << pid
else
break
end
}
end
$options[:repeat].times do |i|
@count = i
if forked?
msg = doc + " in PID %s" % Process.pid
else
msg = doc
end
Puppet::Util.benchmark(:notice, msg) do
begin
send(test)
rescue => detail
puts detail.backtrace if Puppet[:trace]
Puppet.err "%s failed: %s" % [@name, detail.to_s]
end
end
end
end
# What tests are available on this suite?
def tests
@tests.keys
end
end
Suite.new :parser, "Manifest parsing" do
newtest :parse, "Parsed files" do
@parser = Puppet::Parser::Parser.new(:environment => Puppet[:environment])
@parser.file = Puppet[:manifest]
@parser.parse
end
end
Suite.new :local_catalog, "Local catalog handling" do
def prepare
@node = Puppet::Node.find($options[:nodes][0])
end
newtest :compile, "Compiled catalog" do
Puppet::Resource::Catalog.find(@node)
end
end
Suite.new :resource_type, "Managing resource types" do
newtest :find, "Find a type" do
Puppet::Resource::Type.terminus_class = :parser
ARGV.each do |name|
json = Puppet::Resource::Type.find(name).to_pson
data = PSON.parse(json)
- p Puppet::Resource::Type.from_pson(data)
+ p Puppet::Resource::Type.from_data_hash(data)
end
end
newtest :search_types, "Find all types" do
Puppet::Resource::Type.terminus_class = :rest
result = Puppet::Resource::Type.search("*")
result.each { |r| p r }
end
newtest :restful_type, "Find a type and return it via REST" do
Puppet::Resource::Type.terminus_class = :rest
ARGV.each do |name|
p Puppet::Resource::Type.find(name)
end
end
end
Suite.new :remote_catalog, "Remote catalog handling" do
def prepare
$args[:cache] = false
# Create a config client and pull the config down
@client = Puppet::Network::Client.master.new($args)
unless @client.read_cert
fail "Could not read client certificate"
end
if tmp = Puppet::Node::Facts.find($options[:nodes][0])
@facts = tmp.values
else
raise "Could not find facts for %s" % $optins[:nodes][0]
end
if host = $options[:fqdn]
@facts["fqdn"] = host
@facts["hostname"] = host.sub(/\..+/, '')
@facts["domain"] = host.sub(/^[^.]+\./, '')
end
@facts = YAML.dump(@facts)
end
newtest :getconfig, "Compiled catalog" do
@client.driver.getconfig(@facts, "yaml")
end
# This test will always force a false answer.
newtest :fresh, "Checked freshness" do
@client.driver.freshness
end
end
Suite.new :file, "File interactions" do
def prepare
unless $options[:file]
fail "You must specify a file (using --file <file>) to interact with on the server"
end
@client = Puppet::Network::Client.file.new($args)
unless @client.read_cert
fail "Could not read client certificate"
end
end
newtest :describe, "Described file" do
@client.describe($options[:file], :ignore)
end
newtest :retrieve, "Retrieved file" do
@client.retrieve($options[:file], :ignore)
end
end
Suite.new :filebucket, "Filebucket interactions" do
def prepare
require 'tempfile'
@client = Puppet::FileBucket::Dipper.new($args)
end
newtest :backup, "Backed up file" do
Tempfile.open("bucket_testing") do |f|
f.print rand(1024)
f.close
@client.backup(f.path)
end
end
end
# Note that this uses an env variable to determine how many resources per
# host to create (with a default of 10). 'repeat' determines how
# many hosts to create. You probably will get mad failures if you
# use forking and sqlite.
# Here's an example run of this test, using sqlite:
# RESOURCE_COUNT=50 ext/puppet-test --suite rails --test storage --confdir /tmp/storagetesting --vardir /tmp/storagetesting --repeat 10
Suite.new :rails, "Rails Interactions" do
def prepare
Puppet::Rails.init
@facts = Facter.to_hash
if num = ENV["RESOURCECOUNT"]
@resources = Integer(num)
else
@resources = 10
end
end
Resource = Puppet::Parser::Resource
def execute(test, msg)
begin
send(test)
rescue => detail
puts detail.backtrace if Puppet[:trace]
Puppet.err "%s failed: %s" % [@name, detail.to_s]
end
end
def mkresource(type, title, parameters)
source = "fakesource"
res = Resource.new(:type => type, :title => title, :source => source, :scope => "fakescope")
parameters.each do |param, value|
res.set(param, value, source)
end
res
end
def ref(type, title)
Resource::Reference.new(:type => type, :title => title)
end
newtest :storage, "Stored resources" do
hostname = "host%s" % @count
@facts["hostname"] = hostname
args = {:facts => @facts, :name => hostname}
# Make all of the resources we want. Make them at least slightly complex,
# so we model real resources as close as we can.
resources = []
args[:resources] = resources
@resources.times do |resnum|
exec = mkresource("exec", "exec%s" % resnum, :command => "/bin/echo do something %s" % resnum)
exec.tags = %w{exec one} << "exec%s" % resnum
user = mkresource("user", "user%s" % resnum, :uid => resnum.to_s, :require => ref("exec", "exec%s" % resnum))
user.tags = %w{user one two} << "user%s" % resnum
file = mkresource("file", "/tmp/file%s" % resnum, :owner => resnum.to_s, :require => [ref("exec", "exec%s" % resnum), ref("user", "user%s" % resnum)])
file.tags = %w{file one three} << "file%s" % resnum
file.exported = true
resources << exec << user << file
end
Puppet::Rails::Host.store(args)
end
end
Suite.new :report, "Reports interactions" do
def prepare
Puppet::Transaction::Report.terminus_class = :rest
end
newtest :empty, "send empty report" do
report = Puppet::Transaction::Report.new
report.time = Time.now
report.save
end
newtest :fake, "send fake report" do
report = Puppet::Transaction::Report.new
resourcemetrics = {
:total => 12,
:out_of_sync => 20,
:applied => 45,
:skipped => 1,
:restarted => 23,
:failed_restarts => 1,
:scheduled => 10
}
report.newmetric(:resources, resourcemetrics)
timemetrics = {
:resource1 => 10,
:resource2 => 50,
:resource3 => 40,
:resource4 => 20,
}
report.newmetric(:times, timemetrics)
report.newmetric(:changes,
:total => 20
)
report.time = Time.now
report.save
end
end
$cmdargs = [
[ "--compile", "-c", GetoptLong::NO_ARGUMENT ],
[ "--describe", GetoptLong::REQUIRED_ARGUMENT ],
[ "--retrieve", "-R", GetoptLong::REQUIRED_ARGUMENT ],
[ "--fork", GetoptLong::REQUIRED_ARGUMENT ],
[ "--fqdn", "-F", GetoptLong::REQUIRED_ARGUMENT ],
[ "--suite", "-s", GetoptLong::REQUIRED_ARGUMENT ],
[ "--test", "-t", GetoptLong::REQUIRED_ARGUMENT ],
[ "--pause", "-p", GetoptLong::NO_ARGUMENT ],
[ "--repeat", "-r", GetoptLong::REQUIRED_ARGUMENT ],
[ "--node", "-n", GetoptLong::REQUIRED_ARGUMENT ],
[ "--debug", "-d", GetoptLong::NO_ARGUMENT ],
[ "--help", "-h", GetoptLong::NO_ARGUMENT ],
[ "--list", "-l", GetoptLong::NO_ARGUMENT ],
[ "--verbose", "-v", GetoptLong::NO_ARGUMENT ],
[ "--version", "-V", GetoptLong::NO_ARGUMENT ],
]
# Add all of the config parameters as valid $options.
Puppet.settings.addargs($cmdargs)
Puppet::Util::Log.newdestination(:console)
Puppet::Node.terminus_class = :plain
Puppet::Node.cache_class = :yaml
Puppet::Node::Facts.terminus_class = :facter
Puppet::Node::Facts.cache_class = :yaml
result = GetoptLong.new(*$cmdargs)
$args = {}
$options = {:repeat => 1, :fork => 0, :pause => false, :nodes => []}
begin
explicit_waitforcert = false
result.each { |opt,arg|
case opt
- # First check to see if the argument is a valid configuration parameter;
+ # First check to see if the argument is a valid setting;
# if so, set it.
when "--compile"
$options[:suite] = :configuration
$options[:test] = :compile
when "--retrieve"
$options[:suite] = :file
$options[:test] = :retrieve
$options[:file] = arg
when "--fork"
begin
$options[:fork] = Integer(arg)
rescue => detail
$stderr.puts "The argument to 'fork' must be an integer"
exit(14)
end
when "--describe"
$options[:suite] = :file
$options[:test] = :describe
$options[:file] = arg
when "--fqdn"
$options[:fqdn] = arg
when "--repeat"
$options[:repeat] = Integer(arg)
when "--help"
if Puppet.features.usage?
RDoc::usage && exit
else
puts "No help available unless you have RDoc::usage installed"
exit
end
when "--version"
puts "%s" % Puppet.version
exit
when "--verbose"
Puppet::Util::Log.level = :info
Puppet::Util::Log.newdestination(:console)
when "--debug"
Puppet::Util::Log.level = :debug
Puppet::Util::Log.newdestination(:console)
when "--suite"
$options[:suite] = arg.intern
when "--test"
$options[:test] = arg.intern
when "--file"
$options[:file] = arg
when "--pause"
$options[:pause] = true
when "--node"
$options[:nodes] << arg
when "--list"
Suite.suites.sort { |a,b| a.to_s <=> b.to_s }.each do |suite_name|
suite = Suite[suite_name]
tests = suite.tests.sort { |a,b| a.to_s <=> b.to_s }.join(", ")
puts "%20s: %s" % [suite_name, tests]
end
exit(0)
else
Puppet.settings.handlearg(opt, arg)
end
}
rescue GetoptLong::InvalidOption => detail
$stderr.puts detail
$stderr.puts "Try '#{$0} --help'"
exit(1)
end
# Now parse the config
Puppet.initialize_settings
$options[:nodes] << Puppet.settings[:certname] if $options[:nodes].empty?
$args[:Server] = Puppet[:server]
unless $options[:test]
$options[:suite] = :remote_catalog
$options[:test] = :getconfig
end
unless $options[:test]
raise "A suite was specified without a test"
end
$pids = []
Suite.run($options[:test])
if $options[:fork] > 0
Process.waitall
end
diff --git a/ext/redhat/puppet.spec.erb b/ext/redhat/puppet.spec.erb
index 095e6ff5b..865613eae 100644
--- a/ext/redhat/puppet.spec.erb
+++ b/ext/redhat/puppet.spec.erb
@@ -1,822 +1,848 @@
# Augeas and SELinux requirements may be disabled at build time by passing
# --without augeas and/or --without selinux to rpmbuild or mock
# Fedora 17 ships with ruby 1.9, RHEL 7 with ruby 2.0, which use vendorlibdir instead
# of sitelibdir. Adjust our target if installing on f17 or rhel7.
%if 0%{?fedora} >= 17 || 0%{?rhel} >= 7
%global puppet_libdir %(ruby -rrbconfig -e 'puts RbConfig::CONFIG["vendorlibdir"]')
%else
%global puppet_libdir %(ruby -rrbconfig -e 'puts RbConfig::CONFIG["sitelibdir"]')
%endif
%if 0%{?fedora} >= 17 || 0%{?rhel} >= 7
%global _with_systemd 1
%else
%global _with_systemd 0
%endif
# VERSION is subbed out during rake srpm process
%global realversion <%= @version %>
%global rpmversion <%= @rpmversion %>
%global confdir ext/redhat
+%global pending_upgrade_path %{_localstatedir}/lib/rpm-state/puppet
+%global pending_upgrade_file %{pending_upgrade_path}/upgrade_pending
Name: puppet
Version: %{rpmversion}
Release: <%= @rpmrelease -%>%{?dist}
Vendor: %{?_host_vendor}
Summary: A network tool for managing many disparate systems
License: ASL 2.0
URL: http://puppetlabs.com
Source0: http://puppetlabs.com/downloads/%{name}/%{name}-%{realversion}.tar.gz
Group: System Environment/Base
BuildRoot: %{_tmppath}/%{name}-%{version}-%{release}-root-%(%{__id_u} -n)
-BuildRequires: facter < 1:2.0
+BuildRequires: facter >= 1:1.7.0
# Puppet 3.x drops ruby 1.8.5 support and adds ruby 1.9 support
BuildRequires: ruby >= 1.8.7
BuildArch: noarch
Requires: ruby >= 1.8
Requires: ruby-shadow
+Requires: rubygem-json
# Pull in ruby selinux bindings where available
%if 0%{?fedora} || 0%{?rhel} >= 6
%{!?_without_selinux:Requires: ruby(selinux), libselinux-utils}
%else
%if 0%{?rhel} && 0%{?rhel} == 5
%{!?_without_selinux:Requires: libselinux-ruby, libselinux-utils}
%endif
%endif
-Requires: facter >= 1.6.11
+Requires: facter >= 1:1.7.0
# Puppet 3.x drops ruby 1.8.5 support and adds ruby 1.9 support
# Ruby 1.8.7 available for el5 at: yum.puppetlabs.com/el/5/devel/$ARCH
Requires: ruby >= 1.8.7
Requires: hiera >= 1.0.0
Requires: ruby-rgen >= 0.6.5
Obsoletes: hiera-puppet < 1.0.0
Provides: hiera-puppet >= 1.0.0
%{!?_without_augeas:Requires: ruby-augeas}
# Required for %%pre
Requires: shadow-utils
%if 0%{?_with_systemd}
# Required for %%post, %%preun, %%postun
Requires: systemd
%if 0%{?fedora} >= 18 || 0%{?rhel} >= 7
BuildRequires: systemd
%else
BuildRequires: systemd-units
%endif
%else
# Required for %%post and %%preun
Requires: chkconfig
# Required for %%preun and %%postun
Requires: initscripts
%endif
%description
Puppet lets you centrally manage every important aspect of your system using a
cross-platform specification language that manages all the separate elements
normally aggregated in different files, like users, cron jobs, and hosts,
along with obviously discrete elements like packages, services, and files.
%package server
Group: System Environment/Base
Summary: Server for the puppet system management tool
Requires: puppet = %{version}-%{release}
# chkconfig (%%post, %%preun) and initscripts (%%preun %%postun) are required for non systemd
# and systemd (%%post, %%preun, and %%postun) are required for systems with systemd as default
# They come along transitively with puppet-%{version}-%{release}.
%description server
Provides the central puppet server daemon which provides manifests to clients.
The server can also function as a certificate authority and file server.
%prep
%setup -q -n %{name}-%{realversion}
%build
for f in external/nagios.rb relationship.rb; do
sed -i -e '1d' lib/puppet/$f
done
find examples/ -type f | xargs --no-run-if-empty chmod a-x
%install
rm -rf %{buildroot}
ruby install.rb --destdir=%{buildroot} --quick --no-rdoc --sitelibdir=%{puppet_libdir}
install -d -m0755 %{buildroot}%{_sysconfdir}/puppet/manifests
install -d -m0755 %{buildroot}%{_datadir}/%{name}/modules
install -d -m0755 %{buildroot}%{_localstatedir}/lib/puppet
install -d -m0755 %{buildroot}%{_localstatedir}/run/puppet
# As per redhat bz #495096
install -d -m0750 %{buildroot}%{_localstatedir}/log/puppet
%if 0%{?_with_systemd}
# Systemd for fedora >= 17 or el 7
%{__install} -d -m0755 %{buildroot}%{_unitdir}
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
install -Dp -m0644 %{confdir}/client.sysconfig %{buildroot}%{_sysconfdir}/sysconfig/puppet
install -Dp -m0755 %{confdir}/client.init %{buildroot}%{_initrddir}/puppet
install -Dp -m0644 %{confdir}/server.sysconfig %{buildroot}%{_sysconfdir}/sysconfig/puppetmaster
install -Dp -m0755 %{confdir}/server.init %{buildroot}%{_initrddir}/puppetmaster
install -Dp -m0755 %{confdir}/queue.init %{buildroot}%{_initrddir}/puppetqueue
%endif
install -Dp -m0644 %{confdir}/fileserver.conf %{buildroot}%{_sysconfdir}/puppet/fileserver.conf
install -Dp -m0644 %{confdir}/puppet.conf %{buildroot}%{_sysconfdir}/puppet/puppet.conf
install -Dp -m0644 %{confdir}/logrotate %{buildroot}%{_sysconfdir}/logrotate.d/puppet
# Install the ext/ directory to %%{_datadir}/%%{name}
install -d %{buildroot}%{_datadir}/%{name}
cp -a ext/ %{buildroot}%{_datadir}/%{name}
# emacs and vim bits are installed elsewhere
rm -rf %{buildroot}%{_datadir}/%{name}/ext/{emacs,vim}
# remove misc packaging artifacts not applicable to rpms
rm -rf %{buildroot}%{_datadir}/%{name}/ext/{gentoo,freebsd,solaris,suse,windows,osx,ips,debian}
rm -f %{buildroot}%{_datadir}/%{name}/ext/redhat/*.init
rm -f %{buildroot}%{_datadir}/%{name}/ext/{build_defaults.yaml,project_data.yaml}
# Rpmlint fixup
chmod 755 %{buildroot}%{_datadir}/%{name}/ext/regexp_nodes/regexp_nodes.rb
chmod 755 %{buildroot}%{_datadir}/%{name}/ext/puppet-load.rb
# Install emacs mode files
emacsdir=%{buildroot}%{_datadir}/emacs/site-lisp
install -Dp -m0644 ext/emacs/puppet-mode.el $emacsdir/puppet-mode.el
install -Dp -m0644 ext/emacs/puppet-mode-init.el \
$emacsdir/site-start.d/puppet-mode-init.el
# Install vim syntax files
vimdir=%{buildroot}%{_datadir}/vim/vimfiles
install -Dp -m0644 ext/vim/ftdetect/puppet.vim $vimdir/ftdetect/puppet.vim
install -Dp -m0644 ext/vim/syntax/puppet.vim $vimdir/syntax/puppet.vim
%if 0%{?fedora} >= 15 || 0%{?rhel} >= 7
# Setup tmpfiles.d config
mkdir -p %{buildroot}%{_sysconfdir}/tmpfiles.d
echo "D /var/run/%{name} 0755 %{name} %{name} -" > \
%{buildroot}%{_sysconfdir}/tmpfiles.d/%{name}.conf
%endif
# Create puppet modules directory for puppet module tool
mkdir -p %{buildroot}%{_sysconfdir}/%{name}/modules
# Install a NetworkManager dispatcher script to pickup changes to
# # /etc/resolv.conf and such (https://bugzilla.redhat.com/532085).
mkdir -p %{buildroot}%{_sysconfdir}/NetworkManager/dispatcher.d
cp -pr ext/puppet-nm-dispatcher \
%{buildroot}%{_sysconfdir}/NetworkManager/dispatcher.d/98-%{name}
%files
%defattr(-, root, root, 0755)
%doc LICENSE README.md examples
%{_bindir}/puppet
%{_bindir}/extlookup2hiera
%{puppet_libdir}/*
%dir %{_sysconfdir}/NetworkManager
%dir %{_sysconfdir}/NetworkManager/dispatcher.d
%{_sysconfdir}/NetworkManager/dispatcher.d/98-puppet
%if 0%{?_with_systemd}
%{_unitdir}/puppet.service
%{_unitdir}/puppetagent.service
%else
%{_initrddir}/puppet
%config(noreplace) %{_sysconfdir}/sysconfig/puppet
%endif
%dir %{_sysconfdir}/puppet
%dir %{_sysconfdir}/%{name}/modules
%if 0%{?fedora} >= 15 || 0%{?rhel} >= 7
%config(noreplace) %{_sysconfdir}/tmpfiles.d/%{name}.conf
%endif
%config(noreplace) %{_sysconfdir}/puppet/puppet.conf
%config(noreplace) %{_sysconfdir}/puppet/auth.conf
%config(noreplace) %{_sysconfdir}/logrotate.d/puppet
# We don't want to require emacs or vim, so we need to own these dirs
%{_datadir}/emacs
%{_datadir}/vim
%{_datadir}/%{name}
# man pages
%{_mandir}/man5/puppet.conf.5.gz
%{_mandir}/man8/puppet.8.gz
%{_mandir}/man8/puppet-agent.8.gz
%{_mandir}/man8/puppet-apply.8.gz
%{_mandir}/man8/puppet-catalog.8.gz
%{_mandir}/man8/puppet-describe.8.gz
%{_mandir}/man8/puppet-ca.8.gz
%{_mandir}/man8/puppet-cert.8.gz
%{_mandir}/man8/puppet-certificate.8.gz
%{_mandir}/man8/puppet-certificate_request.8.gz
%{_mandir}/man8/puppet-certificate_revocation_list.8.gz
%{_mandir}/man8/puppet-config.8.gz
%{_mandir}/man8/puppet-device.8.gz
%{_mandir}/man8/puppet-doc.8.gz
%{_mandir}/man8/puppet-facts.8.gz
%{_mandir}/man8/puppet-file.8.gz
%{_mandir}/man8/puppet-filebucket.8.gz
%{_mandir}/man8/puppet-help.8.gz
%{_mandir}/man8/puppet-inspect.8.gz
%{_mandir}/man8/puppet-instrumentation_data.8.gz
%{_mandir}/man8/puppet-instrumentation_listener.8.gz
%{_mandir}/man8/puppet-instrumentation_probe.8.gz
%{_mandir}/man8/puppet-key.8.gz
%{_mandir}/man8/puppet-kick.8.gz
%{_mandir}/man8/puppet-man.8.gz
%{_mandir}/man8/puppet-module.8.gz
%{_mandir}/man8/puppet-node.8.gz
%{_mandir}/man8/puppet-parser.8.gz
%{_mandir}/man8/puppet-plugin.8.gz
%{_mandir}/man8/puppet-queue.8.gz
%{_mandir}/man8/puppet-report.8.gz
%{_mandir}/man8/puppet-resource.8.gz
%{_mandir}/man8/puppet-resource_type.8.gz
%{_mandir}/man8/puppet-secret_agent.8.gz
%{_mandir}/man8/puppet-status.8.gz
%{_mandir}/man8/extlookup2hiera.8.gz
# These need to be owned by puppet so the server can
# write to them. The separate %defattr's are required
# to work around RH Bugzilla 681540
%defattr(-, puppet, puppet, 0755)
%{_localstatedir}/run/puppet
%defattr(-, puppet, puppet, 0750)
%{_localstatedir}/log/puppet
%{_localstatedir}/lib/puppet
# Return the default attributes to 0755 to
# prevent incorrect permission assignment on EL6
%defattr(-, root, root, 0755)
%files server
%defattr(-, root, root, 0755)
%if 0%{?_with_systemd}
%{_unitdir}/puppetmaster.service
%else
%{_initrddir}/puppetmaster
%{_initrddir}/puppetqueue
%config(noreplace) %{_sysconfdir}/sysconfig/puppetmaster
%endif
%config(noreplace) %{_sysconfdir}/puppet/fileserver.conf
%dir %{_sysconfdir}/puppet/manifests
%{_mandir}/man8/puppet-ca.8.gz
%{_mandir}/man8/puppet-master.8.gz
# Fixed uid/gid were assigned in bz 472073 (Fedora), 471918 (RHEL-5),
# and 471919 (RHEL-4)
%pre
getent group puppet &>/dev/null || groupadd -r puppet -g 52 &>/dev/null
getent passwd puppet &>/dev/null || \
useradd -r -u 52 -g puppet -d %{_localstatedir}/lib/puppet -s /sbin/nologin \
-c "Puppet" puppet &>/dev/null
# ensure that old setups have the right puppet home dir
if [ $1 -gt 1 ] ; then
usermod -d %{_localstatedir}/lib/puppet puppet &>/dev/null
fi
exit 0
%post
%if 0%{?_with_systemd}
/bin/systemctl daemon-reload >/dev/null 2>&1 || :
if [ "$1" -ge 1 ]; then
# The pidfile changed from 0.25.x to 2.6.x, handle upgrades without leaving
# the old process running.
oldpid="%{_localstatedir}/run/puppet/puppetd.pid"
newpid="%{_localstatedir}/run/puppet/agent.pid"
if [ -s "$oldpid" -a ! -s "$newpid" ]; then
(kill $(< "$oldpid") && rm -f "$oldpid" && \
/bin/systemctl start puppet.service) >/dev/null 2>&1 || :
fi
fi
%else
/sbin/chkconfig --add puppet || :
if [ "$1" -ge 1 ]; then
# The pidfile changed from 0.25.x to 2.6.x, handle upgrades without leaving
# the old process running.
oldpid="%{_localstatedir}/run/puppet/puppetd.pid"
newpid="%{_localstatedir}/run/puppet/agent.pid"
if [ -s "$oldpid" -a ! -s "$newpid" ]; 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
%post server
%if 0%{?_with_systemd}
/bin/systemctl daemon-reload >/dev/null 2>&1 || :
if [ "$1" -ge 1 ]; then
# The pidfile changed from 0.25.x to 2.6.x, handle upgrades without leaving
# the old process running.
oldpid="%{_localstatedir}/run/puppet/puppetmasterd.pid"
newpid="%{_localstatedir}/run/puppet/master.pid"
if [ -s "$oldpid" -a ! -s "$newpid" ]; then
(kill $(< "$oldpid") && rm -f "$oldpid" && \
/bin/systemctl start puppetmaster.service) > /dev/null 2>&1 || :
fi
fi
%else
/sbin/chkconfig --add puppetmaster || :
if [ "$1" -ge 1 ]; then
# The pidfile changed from 0.25.x to 2.6.x, handle upgrades without leaving
# the old process running.
oldpid="%{_localstatedir}/run/puppet/puppetmasterd.pid"
newpid="%{_localstatedir}/run/puppet/master.pid"
if [ -s "$oldpid" -a ! -s "$newpid" ]; then
(kill $(< "$oldpid") && rm -f "$oldpid" && \
/sbin/service puppetmaster start) >/dev/null 2>&1 || :
fi
fi
%endif
%preun
%if 0%{?_with_systemd}
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
+
+if [ "$1" == "1" ]; then
+ /bin/systemctl is-enabled puppetagent.service > /dev/null 2>&1
+ if [ "$?" == "0" ]; then
+ /bin/systemctl --no-reload disable puppetagent.service > /dev/null 2>&1 ||:
+ /bin/systemctl stop puppetagent.service > /dev/null 2>&1 ||:
+ /bin/systemctl daemon-reload >/dev/null 2>&1 ||:
+ if [ ! -d %{pending_upgrade_path} ]; then
+ mkdir -p %{pending_upgrade_path}
+ fi
+
+ if [ ! -e %{pending_upgrade_file} ]; then
+ touch %{pending_upgrade_file}
+ fi
+ fi
+fi
+
%else
if [ "$1" = 0 ] ; then
/sbin/service puppet stop > /dev/null 2>&1
/sbin/chkconfig --del puppet || :
fi
%endif
%preun server
%if 0%{?_with_systemd}
if [ $1 -eq 0 ] ; then
# Package removal, not upgrade
/bin/systemctl --no-reload disable puppetmaster.service > /dev/null 2>&1 || :
/bin/systemctl stop puppetmaster.service > /dev/null 2>&1 || :
/bin/systemctl daemon-reload >/dev/null 2>&1 || :
fi
%else
if [ "$1" = 0 ] ; then
/sbin/service puppetmaster stop > /dev/null 2>&1
/sbin/chkconfig --del puppetmaster || :
fi
%endif
%postun
%if 0%{?_with_systemd}
if [ $1 -ge 1 ] ; then
+ if [ -e %{pending_upgrade_file} ]; then
+ /bin/systemctl --no-reload enable puppet.service > /dev/null 2>&1 ||:
+ /bin/systemctl start puppet.service > /dev/null 2>&1 ||:
+ /bin/systemctl daemon-reload >/dev/null 2>&1 ||:
+ rm %{pending_upgrade_file}
+ fi
# Package upgrade, not uninstall
/bin/systemctl try-restart puppetagent.service >/dev/null 2>&1 || :
fi
%else
if [ "$1" -ge 1 ]; then
/sbin/service puppet condrestart >/dev/null 2>&1 || :
fi
%endif
%postun server
%if 0%{?_with_systemd}
if [ $1 -ge 1 ] ; then
# Package upgrade, not uninstall
/bin/systemctl try-restart puppetmaster.service >/dev/null 2>&1 || :
fi
%else
if [ "$1" -ge 1 ]; then
/sbin/service puppetmaster condrestart >/dev/null 2>&1 || :
fi
%endif
%clean
rm -rf %{buildroot}
%changelog
* <%= Time.now.strftime("%a %b %d %Y") %> Puppet Labs Release <info@puppetlabs.com> - <%= @rpmversion %>-<%= @rpmrelease %>
- Build for <%= @version %>
* 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
* Fri Apr 12 2013 Matthaus Owens <matthaus@puppetlabs.com> - 3.2.0-0.1rc0
- Add requires on ruby-rgen for new parser in Puppet 3.2
* Fri Jan 25 2013 Matthaus Owens <matthaus@puppetlabs.com> - 3.1.0-0.1rc1
- Add extlookup2hiera.8.gz to the files list
* Wed Jan 9 2013 Ryan Uber <ru@ryanuber.com> - 3.1.0-0.1rc1
- Work-around for RH Bugzilla 681540
* Fri Dec 28 2012 Michael Stahnke <stahnma@puppetlabs.com> - 3.0.2-2
- Added a script for Network Manager for bug https://bugzilla.redhat.com/532085
* Tue Dec 18 2012 Matthaus Owens <matthaus@puppetlabs.com>
- Remove for loop on examples/ code which no longer exists. Add --no-run-if-empty to xargs invocations.
* Sat Dec 1 2012 Ryan Uber <ryuber@cisco.com>
- Fix for logdir perms regression (#17866)
* Wed Aug 29 2012 Moses Mendoza <moses@puppetlabs.com> - 3.0.0-0.1rc5
- Update for 3.0.0 rc5
* Fri Aug 24 2012 Eric Sorenson <eric0@puppetlabs.com> - 3.0.0-0.1rc4
- Facter requirement is 1.6.11, not 2.0
- Update for 3.0.0 rc4
* Tue Aug 21 2012 Moses Mendoza <moses@puppetlabs.com> - 2.7.19-1
- Update for 2.7.19
* Tue Aug 14 2012 Moses Mendoza <moses@puppetlabs.com> - 2.7.19-0.1rc3
- Update for 2.7.19rc3
* Tue Aug 7 2012 Moses Mendoza <moses@puppetlabs.com> - 2.7.19-0.1rc2
- Update for 2.7.19rc2
* Wed Aug 1 2012 Moses Mendoza <moses@puppetlabs.com> - 2.7.19-0.1rc1
- Update for 2.7.19rc1
* Wed Jul 11 2012 William Hopper <whopper@puppetlabs.com> - 2.7.18-2
- (#15221) Create /etc/puppet/modules for puppet module tool
* Mon Jul 9 2012 Moses Mendoza <moses@puppetlabs.com> - 2.7.18-1
- Update for 2.7.18
* Tue Jun 19 2012 Matthaus Litteken <matthaus@puppetlabs.com> - 2.7.17-1
- Update for 2.7.17
* Wed Jun 13 2012 Matthaus Litteken <matthaus@puppetlabs.com> - 2.7.16-1
- Update for 2.7.16
* Fri Jun 08 2012 Moses Mendoza <moses@puppetlabs.com> - 2.7.16-0.1rc1.2
- Updated facter 2.0 dep to include epoch 1
* Wed Jun 06 2012 Matthaus Litteken <matthaus@puppetlabs.com> - 2.7.16-0.1rc1
- Update for 2.7.16rc1, added generated manpages
* Fri Jun 01 2012 Matthaus Litteken <matthaus@puppetlabs.com> - 3.0.0-0.1rc3
- Puppet 3.0.0rc3 Release
* Fri Jun 01 2012 Matthaus Litteken <matthaus@puppetlabs.com> - 2.7.15-0.1rc4
- Update for 2.7.15rc4
* Tue May 29 2012 Moses Mendoza <moses@puppetlabs.com> - 2.7.15-0.1rc3
- Update for 2.7.15rc3
* Tue May 22 2012 Matthaus Litteken <matthaus@puppetlabs.com> - 3.0.0-0.1rc2
- Puppet 3.0.0rc2 Release
* Thu May 17 2012 Matthaus Litteken <matthaus@puppetlabs.com> - 3.0.0-0.1rc1
- Puppet 3.0.0rc1 Release
* Wed May 16 2012 Moses Mendoza <moses@puppetlabs.com> - 2.7.15-0.1rc2
- Update for 2.7.15rc2
* Tue May 15 2012 Moses Mendoza <moses@puppetlabs.com> - 2.7.15-0.1rc1
- Update for 2.7.15rc1
* Wed May 02 2012 Moses Mendoza <moses@puppetlabs.com> - 2.7.14-1
- Update for 2.7.14
* Tue Apr 10 2012 Matthaus Litteken <matthaus@puppetlabs.com> - 2.7.13-1
- Update for 2.7.13
* Mon Mar 12 2012 Michael Stahnke <stahnma@puppetlabs.com> - 2.7.12-1
- Update for 2.7.12
* Fri Feb 24 2012 Matthaus Litteken <matthaus@puppetlabs.com> - 2.7.11-2
- Update 2.7.11 from proper tag, including #12572
* Wed Feb 22 2012 Michael Stahnke <stahnma@puppetlabs.com> - 2.7.11-1
- Update for 2.7.11
* Wed Jan 25 2012 Michael Stahnke <stahnma@puppetlabs.com> - 2.7.10-1
- Update for 2.7.10
* Fri Dec 9 2011 Matthaus Litteken <matthaus@puppetlabs.com> - 2.7.9-1
- Update for 2.7.9
* Thu Dec 8 2011 Matthaus Litteken <matthaus@puppetlabs.com> - 2.7.8-1
- Update for 2.7.8
* Wed Nov 30 2011 Michael Stahnke <stahnma@puppetlabs.com> - 2.7.8-0.1rc1
- Update for 2.7.8rc1
* Mon Nov 21 2011 Michael Stahnke <stahnma@puppetlabs.com> - 2.7.7-1
- Relaese 2.7.7
* Tue Nov 01 2011 Michael Stahnke <stahnma@puppetlabs.com> - 2.7.7-0.1rc1
- Update for 2.7.7rc1
* Fri Oct 21 2011 Michael Stahnke <stahnma@puppetlabs.com> - 2.7.6-1
- 2.7.6 final
* Thu Oct 13 2011 Michael Stahnke <stahnma@puppetlabs.com> - 2.7.6-.1rc3
- New RC
* Fri Oct 07 2011 Michael Stahnke <stahnma@puppetlabs.com> - 2.7.6-0.1rc2
- New RC
* Mon Oct 03 2011 Michael Stahnke <stahnma@puppetlabs.com> - 2.7.6-0.1rc1
- New RC
* Fri Sep 30 2011 Michael Stahnke <stahnma@puppetlabs.com> - 2.7.5-1
- Fixes for CVE-2011-3869, 3870, 3871
* Wed Sep 28 2011 Michael Stahnke <stahnma@puppetlabs.com> - 2.7.4-1
- Fix for CVE-2011-3484
* Wed Jul 06 2011 Michael Stahnke <stahnma@puppetlabs.com> - 2.7.2-0.2.rc1
- Clean up rpmlint errors
- Put man pages in correct package
* Wed Jul 06 2011 Michael Stahnke <stahnma@puppetlabs.com> - 2.7.2-0.1.rc1
- Update to 2.7.2rc1
* Wed Jun 15 2011 Todd Zullinger <tmz@pobox.com> - 2.6.9-0.1.rc1
- Update rc versioning to ensure 2.6.9 final is newer to rpm
- sync changes with Fedora/EPEL
* Tue Jun 14 2011 Michael Stahnke <stahnma@puppetlabs.com> - 2.6.9rc1-1
- Update to 2.6.9rc1
* Thu Apr 14 2011 Todd Zullinger <tmz@pobox.com> - 2.6.8-1
- Update to 2.6.8
* Thu Mar 24 2011 Todd Zullinger <tmz@pobox.com> - 2.6.7-1
- Update to 2.6.7
* Wed Mar 16 2011 Todd Zullinger <tmz@pobox.com> - 2.6.6-1
- Update to 2.6.6
- Ensure %%pre exits cleanly
- Fix License tag, puppet is now GPLv2 only
- Create and own /usr/share/puppet/modules (#615432)
- Properly restart puppet agent/master daemons on upgrades from 0.25.x
- Require libselinux-utils when selinux support is enabled
- Support tmpfiles.d for Fedora >= 15 (#656677)
* Wed Feb 09 2011 Fedora Release Engineering <rel-eng@lists.fedoraproject.org> - 0.25.5-2
- Rebuilt for https://fedoraproject.org/wiki/Fedora_15_Mass_Rebuild
* Mon May 17 2010 Todd Zullinger <tmz@pobox.com> - 0.25.5-1
- Update to 0.25.5
- Adjust selinux conditional for EL-6
- Apply rundir-perms patch from tarball rather than including it separately
- Update URL's to reflect the new puppetlabs.com domain
* Fri Jan 29 2010 Todd Zullinger <tmz@pobox.com> - 0.25.4-1
- Update to 0.25.4
* Tue Jan 19 2010 Todd Zullinger <tmz@pobox.com> - 0.25.3-2
- Apply upstream patch to fix cron resources (upstream #2845)
* Mon Jan 11 2010 Todd Zullinger <tmz@pobox.com> - 0.25.3-1
- Update to 0.25.3
* Tue Jan 05 2010 Todd Zullinger <tmz@pobox.com> - 0.25.2-1.1
- Replace %%define with %%global for macros
* Tue Jan 05 2010 Todd Zullinger <tmz@pobox.com> - 0.25.2-1
- Update to 0.25.2
- Fixes CVE-2010-0156, tmpfile security issue (#502881)
- Install auth.conf, puppetqd manpage, and queuing examples/docs
* Wed Nov 25 2009 Jeroen van Meeuwen <j.van.meeuwen@ogd.nl> - 0.25.1-1
- New upstream version
* Tue Oct 27 2009 Todd Zullinger <tmz@pobox.com> - 0.25.1-0.3
- Update to 0.25.1
- Include the pi program and man page (R.I.Pienaar)
* Sat Oct 17 2009 Todd Zullinger <tmz@pobox.com> - 0.25.1-0.2.rc2
- Update to 0.25.1rc2
* Tue Sep 22 2009 Todd Zullinger <tmz@pobox.com> - 0.25.1-0.1.rc1
- Update to 0.25.1rc1
- Move puppetca to puppet package, it has uses on client systems
- Drop redundant %%doc from manpage %%file listings
* Fri Sep 04 2009 Todd Zullinger <tmz@pobox.com> - 0.25.0-1
- Update to 0.25.0
- Fix permissions on /var/log/puppet (#495096)
- Install emacs mode and vim syntax files (#491437)
- Install ext/ directory in %%{_datadir}/%%{name} (/usr/share/puppet)
* Mon May 04 2009 Todd Zullinger <tmz@pobox.com> - 0.25.0-0.1.beta1
- Update to 0.25.0beta1
- Make Augeas and SELinux requirements build time options
* Mon Mar 23 2009 Todd Zullinger <tmz@pobox.com> - 0.24.8-1
- Update to 0.24.8
- Quiet output from %%pre
- Use upstream install script
- Increase required facter version to >= 1.5
* Tue Dec 16 2008 Todd Zullinger <tmz@pobox.com> - 0.24.7-4
- Remove redundant useradd from %%pre
* Tue Dec 16 2008 Jeroen van Meeuwen <kanarip@kanarip.com> - 0.24.7-3
- New upstream version
- Set a static uid and gid (#472073, #471918, #471919)
- Add a conditional requirement on libselinux-ruby for Fedora >= 9
- Add a dependency on ruby-augeas
* Wed Oct 22 2008 Todd Zullinger <tmz@pobox.com> - 0.24.6-1
- Update to 0.24.6
- Require ruby-shadow on Fedora and RHEL >= 5
- Simplify Fedora/RHEL version checks for ruby(abi) and BuildArch
- Require chkconfig and initstripts for preun, post, and postun scripts
- Conditionally restart puppet in %%postun
- Ensure %%preun, %%post, and %%postun scripts exit cleanly
- Create puppet user/group according to Fedora packaging guidelines
- Quiet a few rpmlint complaints
- Remove useless %%pbuild macro
- Make specfile more like the Fedora/EPEL template
* Mon Jul 28 2008 David Lutterkort <dlutter@redhat.com> - 0.24.5-1
- Add /usr/bin/puppetdoc
* Thu Jul 24 2008 Brenton Leanhardt <bleanhar@redhat.com>
- New version
- man pages now ship with tarball
- examples/code moved to root examples dir in upstream tarball
* Tue Mar 25 2008 David Lutterkort <dlutter@redhat.com> - 0.24.4-1
- Add man pages (from separate tarball, upstream will fix to
include in main tarball)
* Mon Mar 24 2008 David Lutterkort <dlutter@redhat.com> - 0.24.3-1
- New version
* Wed Mar 5 2008 David Lutterkort <dlutter@redhat.com> - 0.24.2-1
- New version
* Sat Dec 22 2007 David Lutterkort <dlutter@redhat.com> - 0.24.1-1
- New version
* Mon Dec 17 2007 David Lutterkort <dlutter@redhat.com> - 0.24.0-2
- Use updated upstream tarball that contains yumhelper.py
* Fri Dec 14 2007 David Lutterkort <dlutter@redhat.com> - 0.24.0-1
- Fixed license
- Munge examples/ to make rpmlint happier
* Wed Aug 22 2007 David Lutterkort <dlutter@redhat.com> - 0.23.2-1
- New version
* Thu Jul 26 2007 David Lutterkort <dlutter@redhat.com> - 0.23.1-1
- Remove old config files
* Wed Jun 20 2007 David Lutterkort <dlutter@redhat.com> - 0.23.0-1
- Install one puppet.conf instead of old config files, keep old configs
around to ease update
- Use plain shell commands in install instead of macros
* Wed May 2 2007 David Lutterkort <dlutter@redhat.com> - 0.22.4-1
- New version
* Thu Mar 29 2007 David Lutterkort <dlutter@redhat.com> - 0.22.3-1
- Claim ownership of _sysconfdir/puppet (bz 233908)
* Mon Mar 19 2007 David Lutterkort <dlutter@redhat.com> - 0.22.2-1
- Set puppet's homedir to /var/lib/puppet, not /var/puppet
- Remove no-lockdir patch, not needed anymore
* Mon Feb 12 2007 David Lutterkort <dlutter@redhat.com> - 0.22.1-2
- Fix bogus config parameter in puppetd.conf
* Sat Feb 3 2007 David Lutterkort <dlutter@redhat.com> - 0.22.1-1
- New version
* Fri Jan 5 2007 David Lutterkort <dlutter@redhat.com> - 0.22.0-1
- New version
* Mon Nov 20 2006 David Lutterkort <dlutter@redhat.com> - 0.20.1-2
- Make require ruby(abi) and buildarch: noarch conditional for fedora 5 or
later to allow building on older fedora releases
* Mon Nov 13 2006 David Lutterkort <dlutter@redhat.com> - 0.20.1-1
- New version
* Mon Oct 23 2006 David Lutterkort <dlutter@redhat.com> - 0.20.0-1
- New version
* Tue Sep 26 2006 David Lutterkort <dlutter@redhat.com> - 0.19.3-1
- New version
* Mon Sep 18 2006 David Lutterkort <dlutter@redhat.com> - 0.19.1-1
- New version
* Thu Sep 7 2006 David Lutterkort <dlutter@redhat.com> - 0.19.0-1
- New version
* Tue Aug 1 2006 David Lutterkort <dlutter@redhat.com> - 0.18.4-2
- Use /usr/bin/ruby directly instead of /usr/bin/env ruby in
executables. Otherwise, initscripts break since pidof can't find the
right process
* Tue Aug 1 2006 David Lutterkort <dlutter@redhat.com> - 0.18.4-1
- New version
* Fri Jul 14 2006 David Lutterkort <dlutter@redhat.com> - 0.18.3-1
- New version
* Wed Jul 5 2006 David Lutterkort <dlutter@redhat.com> - 0.18.2-1
- New version
* Wed Jun 28 2006 David Lutterkort <dlutter@redhat.com> - 0.18.1-1
- Removed lsb-config.patch and yumrepo.patch since they are upstream now
* Mon Jun 19 2006 David Lutterkort <dlutter@redhat.com> - 0.18.0-1
- Patch config for LSB compliance (lsb-config.patch)
- Changed config moves /var/puppet to /var/lib/puppet, /etc/puppet/ssl
to /var/lib/puppet, /etc/puppet/clases.txt to /var/lib/puppet/classes.txt,
/etc/puppet/localconfig.yaml to /var/lib/puppet/localconfig.yaml
* Fri May 19 2006 David Lutterkort <dlutter@redhat.com> - 0.17.2-1
- Added /usr/bin/puppetrun to server subpackage
- Backported patch for yumrepo type (yumrepo.patch)
* Wed May 3 2006 David Lutterkort <dlutter@redhat.com> - 0.16.4-1
- Rebuilt
* Fri Apr 21 2006 David Lutterkort <dlutter@redhat.com> - 0.16.0-1
- Fix default file permissions in server subpackage
- Run puppetmaster as user puppet
- rebuilt for 0.16.0
* Mon Apr 17 2006 David Lutterkort <dlutter@redhat.com> - 0.15.3-2
- Don't create empty log files in post-install scriptlet
* Fri Apr 7 2006 David Lutterkort <dlutter@redhat.com> - 0.15.3-1
- Rebuilt for new version
* Wed Mar 22 2006 David Lutterkort <dlutter@redhat.com> - 0.15.1-1
- Patch0: Run puppetmaster as root; running as puppet is not ready
for primetime
* Mon Mar 13 2006 David Lutterkort <dlutter@redhat.com> - 0.15.0-1
- Commented out noarch; requires fix for bz184199
* Mon Mar 6 2006 David Lutterkort <dlutter@redhat.com> - 0.14.0-1
- Added BuildRequires for ruby
* Wed Mar 1 2006 David Lutterkort <dlutter@redhat.com> - 0.13.5-1
- Removed use of fedora-usermgmt. It is not required for Fedora Extras and
makes it unnecessarily hard to use this rpm outside of Fedora. Just
allocate the puppet uid/gid dynamically
* Sun Feb 19 2006 David Lutterkort <dlutter@redhat.com> - 0.13.0-4
- Use fedora-usermgmt to create puppet user/group. Use uid/gid 24. Fixed
problem with listing fileserver.conf and puppetmaster.conf twice
* Wed Feb 8 2006 David Lutterkort <dlutter@redhat.com> - 0.13.0-3
- Fix puppetd.conf
* Wed Feb 8 2006 David Lutterkort <dlutter@redhat.com> - 0.13.0-2
- Changes to run puppetmaster as user puppet
* Mon Feb 6 2006 David Lutterkort <dlutter@redhat.com> - 0.13.0-1
- Don't mark initscripts as config files
* Mon Feb 6 2006 David Lutterkort <dlutter@redhat.com> - 0.12.0-2
- Fix BuildRoot. Add dist to release
* Tue Jan 17 2006 David Lutterkort <dlutter@redhat.com> - 0.11.0-1
- Rebuild
* Thu Jan 12 2006 David Lutterkort <dlutter@redhat.com> - 0.10.2-1
- Updated for 0.10.2 Fixed minor kink in how Source is given
* Wed Jan 11 2006 David Lutterkort <dlutter@redhat.com> - 0.10.1-3
- Added basic fileserver.conf
* Wed Jan 11 2006 David Lutterkort <dlutter@redhat.com> - 0.10.1-1
- Updated. Moved installation of library files to sitelibdir. Pulled
initscripts into separate files. Folded tools rpm into server
* Thu Nov 24 2005 Duane Griffin <d.griffin@psenterprise.com>
- Added init scripts for the client
* Wed Nov 23 2005 Duane Griffin <d.griffin@psenterprise.com>
- First packaging
diff --git a/ext/suse/puppet.spec b/ext/suse/puppet.spec
index 486d01ae2..a1a0f7934 100644
--- a/ext/suse/puppet.spec
+++ b/ext/suse/puppet.spec
@@ -1,310 +1,310 @@
%{!?ruby_sitelibdir: %define ruby_sitelibdir %(ruby -rrbconfig -e 'puts Config::CONFIG["sitelibdir"]')}
%define pbuild %{_builddir}/%{name}-%{version}
%define confdir conf/suse
Summary: A network tool for managing many disparate systems
Name: puppet
Version: 3.0.0
Release: 1%{?dist}
License: Apache 2.0
Group: Productivity/Networking/System
URL: http://puppetlabs.com/projects/puppet/
Source0: http://puppetlabs.com/downloads/puppet/%{name}-%{version}.tar.gz
PreReq: %{insserv_prereq} %{fillup_prereq}
Requires: ruby >= 1.8.7
-Requires: facter >= 1.6.11
+Requires: facter >= 1:1.7.0
Requires: cron
Requires: logrotate
BuildRoot: %{_tmppath}/%{name}-%{version}-%{release}-root-%(%{__id_u} -n)
BuildRequires: ruby >= 1.8.7
BuildRequires: klogd
BuildRequires: sysconfig
%description
Puppet lets you centrally manage every important aspect of your system using a
cross-platform specification language that manages all the separate elements
normally aggregated in different files, like users, cron jobs, and hosts,
along with obviously discrete elements like packages, services, and files.
%package server
Group: Productivity/Networking/System
Summary: Server for the puppet system management tool
Requires: puppet = %{version}-%{release}
%description server
Provides the central puppet server daemon which provides manifests to clients.
The server can also function as a certificate authority and file server.
%prep
%setup -q -n %{name}-%{version}
%build
for f in bin/*; do
sed -i -e '1s,^#!.*ruby$,#!/usr/bin/ruby,' $f
done
%install
%{__install} -d -m0755 %{buildroot}%{_bindir}
%{__install} -d -m0755 %{buildroot}%{_confdir}
%{__install} -d -m0755 %{buildroot}%{ruby_sitelibdir}
%{__install} -d -m0755 %{buildroot}%{_sysconfdir}/puppet/manifests
%{__install} -d -m0755 %{buildroot}%{_docdir}/%{name}-%{version}
%{__install} -d -m0755 %{buildroot}%{_localstatedir}/lib/puppet
%{__install} -d -m0755 %{buildroot}%{_localstatedir}/run/puppet
%{__install} -d -m0755 %{buildroot}%{_localstatedir}/log/puppet
%{__install} -Dp -m0755 %{pbuild}/bin/* %{buildroot}%{_bindir}
%{__install} -Dp -m0644 %{pbuild}/lib/puppet.rb %{buildroot}%{ruby_sitelibdir}/puppet.rb
%{__cp} -a %{pbuild}/lib/puppet %{buildroot}%{ruby_sitelibdir}
find %{buildroot}%{ruby_sitelibdir} -type f -perm +ugo+x -exec chmod a-x '{}' \;
%{__cp} -a %{pbuild}/ext/redhat/client.sysconfig %{buildroot}%{_confdir}/client.sysconfig
%{__install} -Dp -m0644 %{buildroot}%{_confdir}/client.sysconfig %{buildroot}/var/adm/fillup-templates/sysconfig.puppet
%{__cp} -a %{pbuild}/ext/redhat/server.sysconfig %{buildroot}%{_confdir}/server.sysconfig
%{__install} -Dp -m0644 %{buildroot}%{_confdir}/server.sysconfig %{buildroot}/var/adm/fillup-templates/sysconfig.puppetmaster
%{__cp} -a %{pbuild}/ext/redhat/fileserver.conf %{buildroot}%{_confdir}/fileserver.conf
%{__install} -Dp -m0644 %{buildroot}%{_confdir}/fileserver.conf %{buildroot}%{_sysconfdir}/puppet/fileserver.conf
%{__cp} -a %{pbuild}/ext/redhat/puppet.conf %{buildroot}%{_confdir}/puppet.conf
%{__install} -Dp -m0644 %{buildroot}%{_confdir}/puppet.conf %{buildroot}%{_sysconfdir}/puppet/puppet.conf
%{__cp} -a %{pbuild}/ext/redhat/logrotate %{buildroot}%{_confdir}/logrotate
%{__install} -Dp -m0644 %{buildroot}%{_confdir}/logrotate %{buildroot}%{_sysconfdir}/logrotate.d/puppet
%{__install} -Dp -m0755 %{confdir}/client.init %{buildroot}%{_initrddir}/puppet
%{__install} -Dp -m0755 %{confdir}/server.init %{buildroot}%{_initrddir}/puppetmaster
%files
%defattr(-, root, root, 0755)
%{_bindir}/puppet
%{ruby_sitelibdir}/*
%{_initrddir}/puppet
/var/adm/fillup-templates/sysconfig.puppet
%config(noreplace) %{_sysconfdir}/puppet/puppet.conf
%doc COPYING LICENSE README examples
%config(noreplace) %{_sysconfdir}/logrotate.d/puppet
%dir %{_sysconfdir}/puppet
# These need to be owned by puppet so the server can
# write to them
%attr(-, puppet, puppet) %{_localstatedir}/run/puppet
%attr(-, puppet, puppet) %{_localstatedir}/log/puppet
%attr(-, puppet, puppet) %{_localstatedir}/lib/puppet
%files server
%defattr(-, root, root, 0755)
%{_initrddir}/puppetmaster
%config(noreplace) %{_sysconfdir}/puppet/*
%exclude %{_sysconfdir}/puppet/puppet.conf
/var/adm/fillup-templates/sysconfig.puppetmaster
%dir %{_sysconfdir}/puppet
%pre
/usr/sbin/groupadd -r puppet 2>/dev/null || :
/usr/sbin/useradd -g puppet -c "Puppet" \
-s /sbin/nologin -r -d /var/puppet puppet 2> /dev/null || :
%post
%{fillup_and_insserv -y puppet}
%post server
%{fillup_and_insserv -n -y puppetmaster}
%preun
%stop_on_removal puppet
%preun server
%stop_on_removal puppetmaster
%postun
%restart_on_update puppet
%{insserv_cleanup}
%postun server
%restart_on_update puppetmaster
%{insserv_cleanup}
%clean
%{__rm} -rf %{buildroot}
%changelog
* Mon Oct 08 2012 Matthaus Owens <matthaus@puppetlabs.com> - 3.0.0-1
- Update for deprecated binary removal, ruby version requirements
* Fri Aug 24 2012 Eric Sorenson <eric0@puppetlabs.com> - 3.0.0-0.1rc4
- Update facter version dependency
- Update for 3.0.0-0.1rc4
* Wed May 02 2012 Moses Mendoza <moses@puppetlabs.com> - 2.7.14-1
- Update for 2.7.14
* Mon Mar 12 2012 Michael Stahnke <stahnma@puppetlabs.com> - 2.7.12-1
- Update for 2.7.12
* Wed Jan 25 2012 Michael Stahnke <stahnma@puppetlabs.com> - 2.7.10-1
- Update for 2.7.10
* Wed Nov 30 2011 Michael Stahnke <stahnma@puppetlabs.com> - 2.7.8-0.1rc1
- Update for 2.7.8rc1
* Mon Nov 21 2011 Michael Stahnke <stahnma@puppetlabs.com> - 2.7.7-1
- Release 2.7.7
* Wed Jul 06 2011 Michael Stahnke <stahnma@puppetlabs.com> - 2.7.2-0.1rc1
- Updating to 2.7.2rc1
* Tue Sep 14 2010 Ben Kevan <ben.kevan@gmail.com> - 2.6.1
- New version to 2.6.1
- Add client.init and server.init from source since it's now included in the packages
- Change BuildRequires Ruby version to match Requires Ruby version
- Removed ruby-env patch, replaced with sed in prep
- Update urls to puppetlabs.com
* Wed Jul 21 2010 Ben Kevan <ben.kevan@gmail.com> - 2.6.0
- New version and ruby version bump
- Add puppetdoc to %_bindir (unknown why original suse package, excluded or forgot to add)
- Corrected patch for ruby environment
- Move binaries back to the correct directories
* Wed Jul 14 2010 Ben Kevan <ben.kevan@gmail.com> - 0.25.5
- New version.
- Use original client, server.init names
- Revert to puppetmaster
- Fixed client.init and server.init and included $null and Should-Stop for both
* Tue Mar 2 2010 Martin Vuk <martin.vuk@fri.uni-lj.si> - 0.25.4
- New version.
* Sun Aug 9 2009 Noah Fontes <nfontes@transtruct.org>
- Fix build on SLES 9.
- Enable puppet and puppet-server services by default.
* Sat Aug 8 2009 Noah Fontes <nfontes@transtruct.org>
- Fix a lot of relevant warnings from rpmlint.
- Build on OpenSUSE 11.1 correctly.
- Rename puppetmaster init scripts to puppet-server to correspond to the package name.
* Wed Apr 22 2009 Leo Eraly <leo@unstable.be> - 0.24.8
- New version.
* Tue Dec 9 2008 Leo Eraly <leo@unstable.be> - 0.24.6
- New version.
* Fri Sep 5 2008 Leo Eraly <leo@unstable.be> - 0.24.5
- New version.
* Fri Jun 20 2008 Martin Vuk <martin.vuk@fri.uni-lj.si> - 0.24.4
- Removed symlinks to old configuration files
* Fri Dec 14 2007 Martin Vuk <martin.vuk@fri.uni-lj.si> - 0.24.0
- New version.
* Fri Jun 29 2007 Martin Vuk <martin.vuk@fri.uni-lj.si> - 0.23.0
- New version.
* Wed May 2 2007 Martin Vuk <martin.vuk@fri.uni-lj.si> - 0.22.4
- New version. Includes provider for rug package manager.
* Wed Apr 25 2007 Martin Vuk <martin.vuk@fri.uni-lj.si> - 0.22.3
- New version. Added links /sbin/rcpuppet and /sbin/rcpuppetmaster
* Sun Jan 7 2007 Martin Vuk <martin.vuk@fri.uni-lj.si> - 0.22.0
- version bump
* Tue Oct 3 2006 Martin Vuk <martin.vuk@fri.uni-lj.si> - 0.19.3-3
- Made package arch dependant.
* Sat Sep 23 2006 Martin Vuk <martin.vuk@fri.uni-lj.si> - 0.19.3-1
- New version
* Sun Sep 17 2006 Martin Vuk <martin.vuk@fri.uni-lj.si> - 0.19.1-1
- New version
* Tue Aug 30 2006 Martin Vuk <martin.vuk@fri.uni-lj.si> - 0.19.0-1
- New version
- No need to patch anymore :-), since my changes went into official release.
* Tue Aug 3 2006 Martin Vuk <martin.vuk@fri.uni-lj.si> - 0.18.4-3
- Replaced puppet-bin.patch with %build section from David's spec
* Tue Aug 1 2006 Martin Vuk <martin.vuk@fri.uni-lj.si> - 0.18.4-2
- Added supprot for enabling services in SuSE
* Tue Aug 1 2006 Martin Vuk <martin.vuk@fri.uni-lj.si> - 0.18.4-1
- New version and support for SuSE
* Wed Jul 5 2006 David Lutterkort <dlutter@redhat.com> - 0.18.2-1
- New version
* Wed Jun 28 2006 David Lutterkort <dlutter@redhat.com> - 0.18.1-1
- Removed lsb-config.patch and yumrepo.patch since they are upstream now
* Mon Jun 19 2006 David Lutterkort <dlutter@redhat.com> - 0.18.0-1
- Patch config for LSB compliance (lsb-config.patch)
- Changed config moves /var/puppet to /var/lib/puppet, /etc/puppet/ssl
to /var/lib/puppet, /etc/puppet/clases.txt to /var/lib/puppet/classes.txt,
/etc/puppet/localconfig.yaml to /var/lib/puppet/localconfig.yaml
* Fri May 19 2006 David Lutterkort <dlutter@redhat.com> - 0.17.2-1
- Added /usr/bin/puppetrun to server subpackage
- Backported patch for yumrepo type (yumrepo.patch)
* Wed May 3 2006 David Lutterkort <dlutter@redhat.com> - 0.16.4-1
- Rebuilt
* Fri Apr 21 2006 David Lutterkort <dlutter@redhat.com> - 0.16.0-1
- Fix default file permissions in server subpackage
- Run puppetmaster as user puppet
- rebuilt for 0.16.0
* Mon Apr 17 2006 David Lutterkort <dlutter@redhat.com> - 0.15.3-2
- Don't create empty log files in post-install scriptlet
* Fri Apr 7 2006 David Lutterkort <dlutter@redhat.com> - 0.15.3-1
- Rebuilt for new version
* Wed Mar 22 2006 David Lutterkort <dlutter@redhat.com> - 0.15.1-1
- Patch0: Run puppetmaster as root; running as puppet is not ready
for primetime
* Mon Mar 13 2006 David Lutterkort <dlutter@redhat.com> - 0.15.0-1
- Commented out noarch; requires fix for bz184199
* Mon Mar 6 2006 David Lutterkort <dlutter@redhat.com> - 0.14.0-1
- Added BuildRequires for ruby
* Wed Mar 1 2006 David Lutterkort <dlutter@redhat.com> - 0.13.5-1
- Removed use of fedora-usermgmt. It is not required for Fedora Extras and
makes it unnecessarily hard to use this rpm outside of Fedora. Just
allocate the puppet uid/gid dynamically
* Sun Feb 19 2006 David Lutterkort <dlutter@redhat.com> - 0.13.0-4
- Use fedora-usermgmt to create puppet user/group. Use uid/gid 24. Fixed
problem with listing fileserver.conf and puppetmaster.conf twice
* Wed Feb 8 2006 David Lutterkort <dlutter@redhat.com> - 0.13.0-3
- Fix puppetd.conf
* Wed Feb 8 2006 David Lutterkort <dlutter@redhat.com> - 0.13.0-2
- Changes to run puppetmaster as user puppet
* Mon Feb 6 2006 David Lutterkort <dlutter@redhat.com> - 0.13.0-1
- Don't mark initscripts as config files
* Mon Feb 6 2006 David Lutterkort <dlutter@redhat.com> - 0.12.0-2
- Fix BuildRoot. Add dist to release
* Tue Jan 17 2006 David Lutterkort <dlutter@redhat.com> - 0.11.0-1
- Rebuild
* Thu Jan 12 2006 David Lutterkort <dlutter@redhat.com> - 0.10.2-1
- Updated for 0.10.2 Fixed minor kink in how Source is given
* Wed Jan 11 2006 David Lutterkort <dlutter@redhat.com> - 0.10.1-3
- Added basic fileserver.conf
* Wed Jan 11 2006 David Lutterkort <dlutter@redhat.com> - 0.10.1-1
- Updated. Moved installation of library files to sitelibdir. Pulled
initscripts into separate files. Folded tools rpm into server
* Thu Nov 24 2005 Duane Griffin <d.griffin@psenterprise.com>
- Added init scripts for the client
* Wed Nov 23 2005 Duane Griffin <d.griffin@psenterprise.com>
- First packaging
diff --git a/ext/upload_facts.rb b/ext/upload_facts.rb
index 786c921d8..6e4e30792 100755
--- a/ext/upload_facts.rb
+++ b/ext/upload_facts.rb
@@ -1,119 +1,119 @@
#!/usr/bin/env ruby
require 'net/https'
require 'openssl'
require 'openssl/x509'
require 'optparse'
require 'pathname'
require 'yaml'
require 'puppet'
require 'puppet/network/http_pool'
class Puppet::Application::UploadFacts < Puppet::Application
run_mode :master
option('--debug', '-d')
option('--verbose', '-v')
option('--logdest DEST', '-l DEST') do |arg|
Puppet::Util::Log.newdestination(arg)
options[:setdest] = true
end
option('--minutes MINUTES', '-m MINUTES') do |minutes|
options[:time_limit] = 60 * minutes.to_i
end
def help
print <<HELP
== Synopsis
Upload cached facts to the inventory service.
= Usage
upload_facts [-d|--debug] [-v|--verbose] [-m|--minutes <minutes>]
[-l|--logdest syslog|<file>|console]
= Description
This command will read YAML facts from the puppet master's YAML directory, and
save them to the configured facts_terminus. It is intended to be used with the
facts_terminus set to inventory_service, in order to ensure facts which have
been cached locally due to a temporary failure are still uploaded to the
inventory service.
= Usage Notes
upload_facts is intended to be run from cron, with the facts_terminus set to
inventory_service. The +--minutes+ argument should be set to the length of time
between upload_facts runs. This will ensure that only new YAML files are
uploaded.
= Options
-Note that any configuration parameter that's valid in the configuration file
+Note that any setting that's valid in the configuration file
is also a valid long argument. For example, 'server' is a valid configuration
parameter, so you can specify '--server <servername>' as an argument.
See the configuration file documentation at
http://docs.puppetlabs.com/references/stable/configuration.html for
the full list of acceptable parameters. A commented list of all
configuration options can also be generated by running puppet agent with
'--genconfig'.
minutes::
Limit the upload only to YAML files which have been added within the last n
minutes.
HELP
exit
end
def setup
# Handle the logging settings.
if options[:debug] or options[:verbose]
if options[:debug]
Puppet::Util::Log.level = :debug
else
Puppet::Util::Log.level = :info
end
Puppet::Util::Log.newdestination(:console) unless options[:setdest]
end
exit(Puppet.settings.print_configs ? 0 : 1) if Puppet.settings.print_configs?
end
def main
dir = Pathname.new(Puppet[:yamldir]) + 'facts'
cutoff = options[:time_limit] ? Time.now - options[:time_limit] : Time.at(0)
files = dir.children.select do |file|
file.extname == '.yaml' && file.mtime > cutoff
end
failed = false
terminus = Puppet::Node::Facts.indirection.terminus
files.each do |file|
facts = YAML.load_file(file)
request = Puppet::Indirector::Request.new(:facts, :save, facts)
# The terminus warns for us if we fail.
if terminus.save(request)
Puppet.info "Uploaded facts for #{facts.name} to inventory service"
else
failed = true
end
end
exit !failed
end
end
Puppet::Application::UploadFacts.new.run
diff --git a/ext/windows/service/daemon.rb b/ext/windows/service/daemon.rb
index 93701c2a6..84e4a283d 100755
--- a/ext/windows/service/daemon.rb
+++ b/ext/windows/service/daemon.rb
@@ -1,90 +1,169 @@
#!/usr/bin/env ruby
require 'fileutils'
require 'win32/daemon'
require 'win32/dir'
require 'win32/process'
+require 'win32/eventlog'
require 'windows/synchronize'
require 'windows/handle'
class WindowsDaemon < Win32::Daemon
include Windows::Synchronize
include Windows::Handle
include Windows::Process
+ @LOG_TO_FILE = false
LOG_FILE = File.expand_path(File.join(Dir::COMMON_APPDATA, 'PuppetLabs', 'puppet', 'var', 'log', 'windows.log'))
LEVELS = [:debug, :info, :notice, :err]
LEVELS.each do |level|
define_method("log_#{level}") do |msg|
log(msg, level)
end
end
def service_init
- FileUtils.mkdir_p(File.dirname(LOG_FILE))
end
- def service_main(*argv)
- args = argv.join(' ')
- @loglevel = LEVELS.index(argv.index('--debug') ? :debug : :notice)
+ def service_main(*argsv)
+ argsv = (argsv << ARGV).flatten.compact
+ args = argsv.join(' ')
+ @loglevel = LEVELS.index(argsv.index('--debug') ? :debug : :notice)
- log_notice("Starting service: #{args}")
+ @LOG_TO_FILE = (argsv.index('--logtofile') ? true : false)
- while running? do
- return if state != RUNNING
-
- log_notice('Service running')
+ if (@LOG_TO_FILE)
+ FileUtils.mkdir_p(File.dirname(LOG_FILE))
+ args = args.gsub("--logtofile","")
+ end
+ basedir = File.expand_path(File.join(File.dirname(__FILE__), '..'))
+
+ # The puppet installer registers a 'Puppet' event source. For the moment events will be logged with this key, but
+ # it may be a good idea to split the Service and Puppet events later so it's easier to read in the windows Event Log.
+ #
+ # Example code to register an event source;
+ # eventlogdll = File.expand_path(File.join(basedir, 'puppet', 'ext', 'windows', 'eventlog', 'puppetres.dll'))
+ # if (File.exists?(eventlogdll))
+ # Win32::EventLog.add_event_source(
+ # 'source' => "Application",
+ # 'key_name' => "Puppet Agent",
+ # 'category_count' => 3,
+ # 'event_message_file' => eventlogdll,
+ # 'category_message_file' => eventlogdll
+ # )
+ # end
+
+ puppet = File.join(basedir, 'bin', 'puppet.bat')
+ unless File.exists?(puppet)
+ log_err("File not found: '#{puppet}'")
+ return
+ end
+ log_debug("Using '#{puppet}'")
- basedir = File.expand_path(File.join(File.dirname(__FILE__), '..'))
- puppet = File.join(basedir, 'bin', 'puppet.bat')
- unless File.exists?(puppet)
- log_err("File not found: '#{puppet}'")
- return
- end
+ log_notice('Service started')
- log_debug("Using '#{puppet}'")
+ while running? do
begin
runinterval = %x{ "#{puppet}" agent --configprint runinterval }.to_i
if runinterval == 0
runinterval = 1800
log_err("Failed to determine runinterval, defaulting to #{runinterval} seconds")
end
rescue Exception => e
log_exception(e)
runinterval = 1800
end
- pid = Process.create(:command_line => "\"#{puppet}\" agent --onetime #{args}", :creation_flags => Process::CREATE_NEW_CONSOLE).process_id
- log_debug("Process created: #{pid}")
+ if state == RUNNING or state == IDLE
+ log_notice("Executing agent with arguments: #{args}")
+ pid = Process.create(:command_line => "\"#{puppet}\" agent --onetime #{args}", :creation_flags => Process::CREATE_NEW_CONSOLE).process_id
+ log_debug("Process created: #{pid}")
+ else
+ log_debug("Service is paused. Not invoking Puppet agent")
+ end
log_debug("Service waiting for #{runinterval} seconds")
sleep(runinterval)
- log_debug('Service resuming')
+ log_debug('Service woken up')
end
log_notice('Service stopped')
rescue Exception => e
log_exception(e)
end
def service_stop
log_notice('Service stopping')
Thread.main.wakeup
end
+ def service_pause
+ # The service will not stay in a paused stated, instead it will go back into a running state after a short period of time. This is an issue in the Win32-Service ruby code
+ # Raised bug https://github.com/djberg96/win32-service/issues/11 and is fixed in version 0.8.3.
+ # Because the Pause feature is so rarely used, there is no point in creating a workaround until puppet uses 0.8.3.
+ log_notice('Service pausing. The service will not stay paused. See Puppet Issue PUP-1471 for more information')
+ end
+
+ def service_resume
+ log_notice('Service resuming')
+ end
+
+ def service_shutdown
+ log_notice('Host shutting down')
+ end
+
+ # Interrogation handler is just for debug. Can be commented out or removed entirely.
+ # def service_interrogate
+ # log_debug('Service is being interrogated')
+ # end
+
def log_exception(e)
log_err(e.message)
log_err(e.backtrace.join("\n"))
end
def log(msg, level)
if LEVELS.index(level) >= @loglevel
- File.open(LOG_FILE, 'a') { |f| f.puts("#{Time.now} Puppet (#{level}): #{msg}") }
+ if (@LOG_TO_FILE)
+ File.open(LOG_FILE, 'a') { |f| f.puts("#{Time.now} Puppet (#{level}): #{msg}") }
+ end
+
+ case level
+ when :debug
+ report_windows_event(Win32::EventLog::INFO,0x01,msg.to_s)
+ when :info
+ report_windows_event(Win32::EventLog::INFO,0x01,msg.to_s)
+ when :notice
+ report_windows_event(Win32::EventLog::INFO,0x01,msg.to_s)
+ when :err
+ report_windows_event(Win32::EventLog::ERR,0x03,msg.to_s)
+ else
+ report_windows_event(Win32::EventLog::WARN,0x02,msg.to_s)
+ end
+ end
+ end
+
+ def report_windows_event(type,id,message)
+ begin
+ eventlog = nil
+ eventlog = Win32::EventLog.open("Application")
+ eventlog.report_event(
+ :source => "Puppet",
+ :event_type => type, # Win32::EventLog::INFO or WARN, ERROR
+ :event_id => id, # 0x01 or 0x02, 0x03 etc.
+ :data => message # "the message"
+ )
+ rescue Exception => e
+ # Ignore all errors
+ ensure
+ if (!eventlog.nil?)
+ eventlog.close
+ end
end
end
end
if __FILE__ == $0
WindowsDaemon.mainloop
end
diff --git a/lib/hiera_puppet.rb b/lib/hiera_puppet.rb
index d90b82089..fe4fecd90 100644
--- a/lib/hiera_puppet.rb
+++ b/lib/hiera_puppet.rb
@@ -1,87 +1,87 @@
require 'hiera'
require 'hiera/scope'
require 'puppet'
module HieraPuppet
module_function
def lookup(key, default, scope, override, resolution_type)
scope = Hiera::Scope.new(scope)
answer = hiera.lookup(key, default, scope, override, resolution_type)
if answer.nil?
raise(Puppet::ParseError, "Could not find data item #{key} in any Hiera data file and no default supplied")
end
answer
end
def parse_args(args)
# Functions called from Puppet manifests like this:
#
# hiera("foo", "bar")
#
# Are invoked internally after combining the positional arguments into a
# single array:
#
# func = function_hiera
# func(["foo", "bar"])
#
# Functions called from templates preserve the positional arguments:
#
# scope.function_hiera("foo", "bar")
#
# Deal with Puppet's special calling mechanism here.
if args[0].is_a?(Array)
args = args[0]
end
if args.empty?
raise(Puppet::ParseError, "Please supply a parameter to perform a Hiera lookup")
end
key = args[0]
default = args[1]
override = args[2]
return [key, default, override]
end
private
module_function
def hiera
@hiera ||= Hiera.new(:config => hiera_config)
end
def hiera_config
config = {}
if config_file = hiera_config_file
config = Hiera::Config.load(config_file)
end
config[:logger] = 'puppet'
config
end
def hiera_config_file
config_file = nil
if Puppet.settings[:hiera_config].is_a?(String)
expanded_config_file = File.expand_path(Puppet.settings[:hiera_config])
- if Puppet::FileSystem::File.exist?(expanded_config_file)
+ if Puppet::FileSystem.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 Puppet::FileSystem::File.exist?(expanded_config_file)
+ if Puppet::FileSystem.exist?(expanded_config_file)
config_file = expanded_config_file
end
end
config_file
end
end
diff --git a/lib/puppet.rb b/lib/puppet.rb
index aa47362f8..4c74230f6 100644
--- a/lib/puppet.rb
+++ b/lib/puppet.rb
@@ -1,184 +1,247 @@
require 'puppet/version'
# see the bottom of the file for further inclusions
# Also see the new Vendor support - towards the end
#
require 'facter'
require 'puppet/error'
require 'puppet/util'
require 'puppet/util/autoload'
require 'puppet/settings'
require 'puppet/util/feature'
require 'puppet/util/suidmanager'
require 'puppet/util/run_mode'
require 'puppet/external/pson/common'
require 'puppet/external/pson/version'
require 'puppet/external/pson/pure'
#------------------------------------------------------------
# the top-level module
#
# all this really does is dictate how the whole system behaves, through
# preferences for things like debugging
#
# it's also a place to find top-level commands like 'debug'
# The main Puppet class. Everything is contained here.
#
# @api public
module Puppet
require 'puppet/file_system'
+ require 'puppet/context'
+ require 'puppet/environments'
class << self
include Puppet::Util
attr_reader :features
attr_writer :name
end
# the hash that determines how our system behaves
@@settings = Puppet::Settings.new
# The services running in this process.
@services ||= []
require 'puppet/util/logging'
extend Puppet::Util::Logging
# The feature collection
@features = Puppet::Util::Feature.new('puppet/feature')
# Load the base features.
require 'puppet/feature/base'
# Store a new default value.
def self.define_settings(section, hash)
@@settings.define_settings(section, hash)
end
# Get the value for a setting
#
# @param [Symbol] param the setting to retrieve
#
# @api public
def self.[](param)
if param == :debug
return Puppet::Util::Log.level == :debug
else
return @@settings[param]
end
end
- # configuration parameter access and stuff
+ # setting access and stuff
def self.[]=(param,value)
@@settings[param] = value
end
def self.clear
@@settings.clear
end
def self.debug=(value)
if value
Puppet::Util::Log.level=(:debug)
else
Puppet::Util::Log.level=(:notice)
end
end
def self.settings
@@settings
end
def self.run_mode
# This sucks (the existence of this method); there are a lot of places in our code that branch based the value of
# "run mode", but there used to be some really confusing code paths that made it almost impossible to determine
# when during the lifecycle of a puppet application run the value would be set properly. A lot of the lifecycle
# stuff has been cleaned up now, but it still seems frightening that we rely so heavily on this value.
#
# I'd like to see about getting rid of the concept of "run_mode" entirely, but there are just too many places in
# the code that call this method at the moment... so I've settled for isolating it inside of the Settings class
# (rather than using a global variable, as we did previously...). Would be good to revisit this at some point.
#
# --cprice 2012-03-16
Puppet::Util::RunMode[@@settings.preferred_run_mode]
end
- # Load all of the configuration parameters.
+ # Load all of the settings.
require 'puppet/defaults'
def self.genmanifest
if Puppet[:genmanifest]
puts Puppet.settings.to_manifest
exit(0)
end
end
# Parse the config file for this process.
- # @deprecated Use {#initialize_settings}
+ # @deprecated Use {initialize_settings}
def self.parse_config()
Puppet.deprecation_warning("Puppet.parse_config is deprecated; please use Faces API (which will handle settings and state management for you), or (less desirable) call Puppet.initialize_settings")
Puppet.initialize_settings
end
# Initialize puppet's settings. This is intended only for use by external tools that are not
# built off of the Faces API or the Puppet::Util::Application class. It may also be used
# to initialize state so that a Face may be used programatically, rather than as a stand-alone
# command-line tool.
#
# @api public
# @param args [Array<String>] the command line arguments to use for initialization
# @return [void]
def self.initialize_settings(args = [])
do_initialize_settings_for_run_mode(:user, args)
end
# Initialize puppet's settings for a specified run_mode.
#
- # @deprecated Use {#initialize_settings}
+ # @deprecated Use {initialize_settings}
def self.initialize_settings_for_run_mode(run_mode)
Puppet.deprecation_warning("initialize_settings_for_run_mode may be removed in a future release, as may run_mode itself")
do_initialize_settings_for_run_mode(run_mode, [])
end
# private helper method to provide the implementation details of initializing for a run mode,
# but allowing us to control where the deprecation warning is issued
def self.do_initialize_settings_for_run_mode(run_mode, args)
Puppet.settings.initialize_global_settings(args)
run_mode = Puppet::Util::RunMode[run_mode]
Puppet.settings.initialize_app_defaults(Puppet::Settings.app_defaults_for_run_mode(run_mode))
+ Puppet.push_context(Puppet.base_context(Puppet.settings), "Initial context after settings initialization")
+ Puppet::Parser::Functions.reset
end
private_class_method :do_initialize_settings_for_run_mode
# Create a new type. Just proxy to the Type class. The mirroring query
# code was deprecated in 2008, but this is still in heavy use. I suppose
# this can count as a soft deprecation for the next dev. --daniel 2011-04-12
def self.newtype(name, options = {}, &block)
Puppet::Type.newtype(name, options, &block)
end
# Load vendored (setup paths, and load what is needed upfront).
# See the Vendor class for how to add additional vendored gems/code
require "puppet/vendor"
Puppet::Vendor.load_vendored
# Set default for YAML.load to unsafe so we don't affect programs
# requiring puppet -- in puppet we will call safe explicitly
SafeYAML::OPTIONS[:default_mode] = :unsafe
+
+ # The bindings used for initialization of puppet
+ # @api private
+ def self.base_context(settings)
+ environments = settings[:environmentpath]
+ modulepath = Puppet::Node::Environment.split_path(settings[:basemodulepath])
+
+ loaders = Puppet::Environments::Directories.from_path(environments, modulepath)
+ loaders << Puppet::Environments::Legacy.new
+
+ {
+ :environments => Puppet::Environments::Combined.new(*loaders)
+ }
+ end
+
+ # A simple set of bindings that is just enough to limp along to
+ # initialization where the {base_context} bindings are put in place
+ # @api private
+ def self.bootstrap_context
+ root_environment = Puppet::Node::Environment.create(:'*root*', [], '')
+ {
+ :current_environment => root_environment,
+ :root_environment => root_environment
+ }
+ end
+
+ # @param overrides [Hash] A hash of bindings to be merged with the parent context.
+ # @param description [String] A description of the context.
+ # @api private
+ def self.push_context(overrides, description = "")
+ @context.push(overrides, description)
+ end
+
+ # Return to the previous context.
+ # @raise [StackUnderflow] if the current context is the root
+ # @api private
+ def self.pop_context
+ @context.pop
+ end
+
+ # Lookup a binding by name or return a default value provided by a passed block (if given).
+ # @api private
+ def self.lookup(name, &block)
+ @context.lookup(name, &block)
+ end
+
+ # @param bindings [Hash] A hash of bindings to be merged with the parent context.
+ # @param description [String] A description of the context.
+ # @yield [] A block executed in the context of the temporarily pushed bindings.
+ # @api private
+ def self.override(bindings, description = "", &block)
+ @context.override(bindings, description, &block)
+ end
+
+ require 'puppet/node'
+
+ # The single instance used for normal operation
+ @context = Puppet::Context.new(bootstrap_context)
end
# This feels weird to me; I would really like for us to get to a state where there is never a "require" statement
# anywhere besides the very top of a file. That would not be possible at the moment without a great deal of
# effort, but I think we should strive for it and revisit this at some point. --cprice 2012-03-16
+require 'puppet/indirector'
require 'puppet/type'
-require 'puppet/parser'
require 'puppet/resource'
+require 'puppet/parser'
require 'puppet/network'
require 'puppet/ssl'
require 'puppet/module'
require 'puppet/data_binding'
require 'puppet/util/storage'
require 'puppet/status'
require 'puppet/file_bucket/file'
diff --git a/lib/puppet/agent.rb b/lib/puppet/agent.rb
index 14c6c693b..72be5c18d 100644
--- a/lib/puppet/agent.rb
+++ b/lib/puppet/agent.rb
@@ -1,118 +1,122 @@
require 'puppet/application'
# A general class for triggering a run of another
# class.
class Puppet::Agent
require 'puppet/agent/locker'
include Puppet::Agent::Locker
require 'puppet/agent/disabler'
include Puppet::Agent::Disabler
attr_reader :client_class, :client, :splayed, :should_fork
# Just so we can specify that we are "the" instance.
def initialize(client_class, should_fork=true)
@splayed = false
- @should_fork = should_fork
+ @should_fork = can_fork? && should_fork
@client_class = client_class
end
+ def can_fork?
+ Puppet.features.posix?
+ end
+
def needing_restart?
Puppet::Application.restart_requested?
end
# Perform a run with our client.
def run(client_options = {})
if running?
Puppet.notice "Run of #{client_class} already in progress; skipping (#{lockfile_path} exists)"
return
end
if disabled?
Puppet.notice "Skipping run of #{client_class}; administratively disabled (Reason: '#{disable_message}');\nUse 'puppet agent --enable' to re-enable."
return
end
result = nil
block_run = Puppet::Application.controlled_run do
splay client_options.fetch :splay, Puppet[:splay]
result = run_in_fork(should_fork) do
with_client do |client|
begin
client_args = client_options.merge(:pluginsync => Puppet[:pluginsync])
lock { client.run(client_args) }
rescue SystemExit,NoMemoryError
raise
rescue Exception => detail
Puppet.log_exception(detail, "Could not run #{client_class}: #{detail}")
end
end
end
true
end
Puppet.notice "Shutdown/restart in progress (#{Puppet::Application.run_status.inspect}); skipping run" unless block_run
result
end
def stopping?
Puppet::Application.stop_requested?
end
# Have we splayed already?
def splayed?
splayed
end
# Sleep when splay is enabled; else just return.
def splay(do_splay = Puppet[:splay])
return unless do_splay
return if splayed?
time = rand(Puppet[:splaylimit] + 1)
Puppet.info "Sleeping for #{time} seconds (splay is enabled)"
sleep(time)
@splayed = true
end
def run_in_fork(forking = true)
return yield unless forking or Puppet.features.windows?
child_pid = Kernel.fork do
$0 = "puppet agent: applying configuration"
begin
exit(yield)
rescue SystemExit
exit(-1)
rescue NoMemoryError
exit(-2)
end
end
exit_code = Process.waitpid2(child_pid)
case exit_code[1].exitstatus
when -1
raise SystemExit
when -2
raise NoMemoryError
end
exit_code[1].exitstatus
end
private
# Create and yield a client instance, keeping a reference
# to it during the yield.
def with_client
begin
@client = client_class.new
rescue SystemExit,NoMemoryError
raise
rescue Exception => detail
Puppet.log_exception(detail, "Could not create instance of #{client_class}: #{detail}")
return
end
yield @client
ensure
@client = nil
end
end
diff --git a/lib/puppet/application.rb b/lib/puppet/application.rb
index acef32134..602f27a21 100644
--- a/lib/puppet/application.rb
+++ b/lib/puppet/application.rb
@@ -1,476 +1,485 @@
require 'optparse'
require 'puppet/util/command_line'
require 'puppet/util/plugins'
require 'puppet/util/constant_inflector'
require 'puppet/error'
module Puppet
# This class handles all the aspects of a Puppet application/executable
# * setting up options
# * setting up logs
# * choosing what to run
# * representing execution status
#
# === Usage
# An application is a subclass of Puppet::Application.
#
# For legacy compatibility,
# Puppet::Application[:example].run
# is equivalent to
# Puppet::Application::Example.new.run
#
#
-# class Puppet::Application::Example << Puppet::Application
+# class Puppet::Application::Example < Puppet::Application
#
# def preinit
# # perform some pre initialization
# @all = false
# end
#
# # run_command is called to actually run the specified command
# def run_command
# send Puppet::Util::CommandLine.new.args.shift
# end
#
# # option uses metaprogramming to create a method
# # and also tells the option parser how to invoke that method
# option("--arg ARGUMENT") do |v|
# @args << v
# end
#
# option("--debug", "-d") do |v|
# @debug = v
# end
#
# option("--all", "-a:) do |v|
# @all = v
# end
#
# def handle_unknown(opt,arg)
# # last chance to manage an option
# ...
# # let's say to the framework we finally handle this option
# true
# end
#
# def read
# # read action
# end
#
# def write
# # writeaction
# end
#
# end
#
# === Preinit
# The preinit block is the first code to be called in your application, before option parsing,
# setup or command execution.
#
# === Options
# Puppet::Application uses +OptionParser+ to manage the application options.
# Options are defined with the +option+ method to which are passed various
# arguments, including the long option, the short option, a description...
# Refer to +OptionParser+ documentation for the exact format.
# * If the option method is given a block, this one will be called whenever
# the option is encountered in the command-line argument.
# * If the option method has no block, a default functionnality will be used, that
# stores the argument (or true/false if the option doesn't require an argument) in
# the global (to the application) options array.
# * If a given option was not defined by a the +option+ method, but it exists as a Puppet settings:
# * if +unknown+ was used with a block, it will be called with the option name and argument
# * if +unknown+ wasn't used, then the option/argument is handed to Puppet.settings.handlearg for
# a default behavior
#
# --help is managed directly by the Puppet::Application class, but can be overriden.
#
# === Setup
# Applications can use the setup block to perform any initialization.
# The default +setup+ behaviour is to: read Puppet configuration and manage log level and destination
#
# === What and how to run
# If the +dispatch+ block is defined it is called. This block should return the name of the registered command
# to be run.
# If it doesn't exist, it defaults to execute the +main+ command if defined.
#
# === Execution state
# The class attributes/methods of Puppet::Application serve as a global place to set and query the execution
# status of the application: stopping, restarting, etc. The setting of the application status does not directly
# affect its running status; it's assumed that the various components within the application will consult these
# settings appropriately and affect their own processing accordingly. Control operations (signal handlers and
# the like) should set the status appropriately to indicate to the overall system that it's the process of
# stopping or restarting (or just running as usual).
#
# So, if something in your application needs to stop the process, for some reason, you might consider:
#
# def stop_me!
# # indicate that we're stopping
# Puppet::Application.stop!
# # ...do stuff...
# end
#
# And, if you have some component that involves a long-running process, you might want to consider:
#
# def my_long_process(giant_list_to_munge)
# giant_list_to_munge.collect do |member|
# # bail if we're stopping
# return if Puppet::Application.stop_requested?
# process_member(member)
# end
# end
class Application
require 'puppet/util'
include Puppet::Util
DOCPATTERN = ::File.expand_path(::File.dirname(__FILE__) + "/util/command_line/*" )
CommandLineArgs = Struct.new(:subcommand_name, :args)
@loader = Puppet::Util::Autoload.new(self, 'puppet/application')
class << self
include Puppet::Util
attr_accessor :run_status
def clear!
self.run_status = nil
end
def stop!
self.run_status = :stop_requested
end
def restart!
self.run_status = :restart_requested
end
# Indicates that Puppet::Application.restart! has been invoked and components should
# do what is necessary to facilitate a restart.
def restart_requested?
:restart_requested == run_status
end
# Indicates that Puppet::Application.stop! has been invoked and components should do what is necessary
# for a clean stop.
def stop_requested?
:stop_requested == run_status
end
# Indicates that one of stop! or start! was invoked on Puppet::Application, and some kind of process
# shutdown/short-circuit may be necessary.
def interrupted?
[:restart_requested, :stop_requested].include? run_status
end
# Indicates that Puppet::Application believes that it's in usual running run_mode (no stop/restart request
# currently active).
def clear?
run_status.nil?
end
# Only executes the given block if the run status of Puppet::Application is clear (no restarts, stops,
# etc. requested).
# Upon block execution, checks the run status again; if a restart has been requested during the block's
# execution, then controlled_run will send a new HUP signal to the current process.
# Thus, long-running background processes can potentially finish their work before a restart.
def controlled_run(&block)
return unless clear?
result = block.call
Process.kill(:HUP, $PID) if restart_requested?
result
end
SHOULD_PARSE_CONFIG_DEPRECATION_MSG = "is no longer supported; config file parsing " +
"is now controlled by the puppet engine, rather than by individual applications. This " +
"method will be removed in a future version of puppet."
def should_parse_config
Puppet.deprecation_warning("should_parse_config " + SHOULD_PARSE_CONFIG_DEPRECATION_MSG)
end
def should_not_parse_config
Puppet.deprecation_warning("should_not_parse_config " + SHOULD_PARSE_CONFIG_DEPRECATION_MSG)
end
def should_parse_config?
Puppet.deprecation_warning("should_parse_config? " + SHOULD_PARSE_CONFIG_DEPRECATION_MSG)
true
end
# used to declare code that handle an option
def option(*options, &block)
long = options.find { |opt| opt =~ /^--/ }.gsub(/^--(?:\[no-\])?([^ =]+).*$/, '\1' ).gsub('-','_')
fname = "handle_#{long}".intern
if (block_given?)
define_method(fname, &block)
else
define_method(fname) do |value|
self.options["#{long}".to_sym] = value
end
end
self.option_parser_commands << [options, fname]
end
def banner(banner = nil)
@banner ||= banner
end
def option_parser_commands
@option_parser_commands ||= (
superclass.respond_to?(:option_parser_commands) ? superclass.option_parser_commands.dup : []
)
@option_parser_commands
end
# @return [Array<String>] the names of available applications
# @api public
def available_application_names
@loader.files_to_load.map do |fn|
::File.basename(fn, '.rb')
end.uniq
end
# Finds the class for a given application and loads the class. This does
# not create an instance of the application, it only gets a handle to the
# class. The code for the application is expected to live in a ruby file
# `puppet/application/#{name}.rb` that is available on the `$LOAD_PATH`.
#
# @param application_name [String] the name of the application to find (eg. "apply").
# @return [Class] the Class instance of the application that was found.
# @raise [Puppet::Error] if the application class was not found.
# @raise [LoadError] if there was a problem loading the application file.
# @api public
def find(application_name)
begin
require @loader.expand(application_name.to_s.downcase)
rescue LoadError => e
Puppet.log_and_raise(e, "Unable to find application '#{application_name}'. #{e}")
end
class_name = Puppet::Util::ConstantInflector.file2constant(application_name.to_s)
clazz = try_load_class(class_name)
################################################################
#### Begin 2.7.x backward compatibility hack;
#### eventually we need to issue a deprecation warning here,
#### and then get rid of this stanza in a subsequent release.
################################################################
if (clazz.nil?)
class_name = application_name.capitalize
clazz = try_load_class(class_name)
end
################################################################
#### End 2.7.x backward compatibility hack
################################################################
if clazz.nil?
raise Puppet::Error.new("Unable to load application class '#{class_name}' from file 'puppet/application/#{application_name}.rb'")
end
return clazz
end
# Given the fully qualified name of a class, attempt to get the class instance.
# @param [String] class_name the fully qualified name of the class to try to load
# @return [Class] the Class instance, or nil? if it could not be loaded.
def try_load_class(class_name)
return self.const_defined?(class_name) ? const_get(class_name) : nil
end
private :try_load_class
def [](name)
find(name).new
end
# Sets or gets the run_mode name. Sets the run_mode name if a mode_name is
# passed. Otherwise, gets the run_mode or a default run_mode
#
def run_mode( mode_name = nil)
if mode_name
Puppet.settings.preferred_run_mode = mode_name
end
return @run_mode if @run_mode and not mode_name
require 'puppet/util/run_mode'
@run_mode = Puppet::Util::RunMode[ mode_name || Puppet.settings.preferred_run_mode ]
end
# This is for testing only
def clear_everything_for_tests
@run_mode = @banner = @run_status = @option_parser_commands = nil
end
end
attr_reader :options, :command_line
# Every app responds to --version
# See also `lib/puppet/util/command_line.rb` for some special case early
# handling of this.
option("--version", "-V") do |arg|
puts "#{Puppet.version}"
exit
end
# Every app responds to --help
option("--help", "-h") do |v|
puts help
exit
end
def app_defaults()
Puppet::Settings.app_defaults_for_run_mode(self.class.run_mode).merge(
:name => name
)
end
def initialize_app_defaults()
Puppet.settings.initialize_app_defaults(app_defaults)
end
# override to execute code before running anything else
def preinit
end
def initialize(command_line = Puppet::Util::CommandLine.new)
@command_line = CommandLineArgs.new(command_line.subcommand_name, command_line.args.dup)
@options = {}
end
# Execute the application.
# @api public
# @return [void]
def run
# I don't really like the names of these lifecycle phases. It would be nice to change them to some more meaningful
# names, and make deprecated aliases. Also, Daniel suggests that we can probably get rid of this "plugin_hook"
# pattern, but we need to check with PE and the community first. --cprice 2012-03-16
#
exit_on_fail("get application-specific default settings") do
plugin_hook('initialize_app_defaults') { initialize_app_defaults }
end
- require 'puppet'
- require 'puppet/util/instrumentation'
- Puppet::Util::Instrumentation.init
-
- exit_on_fail("initialize") { plugin_hook('preinit') { preinit } }
- exit_on_fail("parse application options") { plugin_hook('parse_options') { parse_options } }
- exit_on_fail("prepare for execution") { plugin_hook('setup') { setup } }
- exit_on_fail("configure routes from #{Puppet[:route_file]}") { configure_indirector_routes }
- exit_on_fail("run") { plugin_hook('run_command') { run_command } }
+ new_context = Puppet.base_context(Puppet.settings)
+ configured_environment = new_context[:environments].get(Puppet[:environment])
+ configured_environment = configured_environment.override_from_commandline(Puppet.settings)
+ new_context[:current_environment] = configured_environment
+
+ # Setup a new context using the app's configuration
+ Puppet.override(new_context,
+ "New base context and current environment from application's configuration") do
+ require 'puppet'
+ require 'puppet/util/instrumentation'
+ Puppet::Util::Instrumentation.init
+
+ exit_on_fail("initialize") { plugin_hook('preinit') { preinit } }
+ exit_on_fail("parse application options") { plugin_hook('parse_options') { parse_options } }
+ exit_on_fail("prepare for execution") { plugin_hook('setup') { setup } }
+ exit_on_fail("configure routes from #{Puppet[:route_file]}") { configure_indirector_routes }
+ exit_on_fail("run") { plugin_hook('run_command') { run_command } }
+ end
end
def main
raise NotImplementedError, "No valid command or main"
end
def run_command
main
end
def setup
setup_logs
end
def setup_logs
if options[:debug] || options[:verbose]
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
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 Puppet::FileSystem::File.exist?(route_file)
+ if Puppet::FileSystem.exist?(route_file)
routes = YAML.load_file(route_file)
application_routes = routes[name.to_s]
Puppet::Indirector.configure_routes(application_routes) if application_routes
end
end
def parse_options
# Create an option parser
option_parser = OptionParser.new(self.class.banner)
# Here we're building up all of the options that the application may need to handle. The main
# puppet settings defined in "defaults.rb" have already been parsed once (in command_line.rb) by
# the time we get here; however, our app may wish to handle some of them specially, so we need to
# make the parser aware of them again. We might be able to make this a bit more efficient by
# re-using the parser object that gets built up in command_line.rb. --cprice 2012-03-16
# Add all global options to it.
Puppet.settings.optparse_addargs([]).each do |option|
option_parser.on(*option) do |arg|
handlearg(option[0], arg)
end
end
# Add options that are local to this application, which were
# created using the "option()" metaprogramming method. If there
# are any conflicts, this application's options will be favored.
self.class.option_parser_commands.each do |options, fname|
option_parser.on(*options) do |value|
# Call the method that "option()" created.
self.send(fname, value)
end
end
# Scan command line. We just hand any exceptions to our upper levels,
# rather than printing help and exiting, so that we can meaningfully
# respond with context-sensitive help if we want to. --daniel 2011-04-12
option_parser.parse!(self.command_line.args)
end
def handlearg(opt, val)
opt, val = Puppet::Settings.clean_opt(opt, val)
send(:handle_unknown, opt, val) if respond_to?(:handle_unknown)
end
# this is used for testing
def self.exit(code)
exit(code)
end
def name
self.class.to_s.sub(/.*::/,"").downcase.to_sym
end
def help
"No help available for puppet #{name}"
end
def plugin_hook(step,&block)
Puppet::Plugins.send("before_application_#{step}",:application_object => self)
x = yield
Puppet::Plugins.send("after_application_#{step}",:application_object => self, :return_value => x)
x
end
private :plugin_hook
end
end
diff --git a/lib/puppet/application/agent.rb b/lib/puppet/application/agent.rb
index d86f3f47f..7c7c11f4e 100644
--- a/lib/puppet/application/agent.rb
+++ b/lib/puppet/application/agent.rb
@@ -1,479 +1,479 @@
require 'puppet/application'
require 'puppet/run'
require 'puppet/daemon'
require 'puppet/util/pidlock'
class Puppet::Application::Agent < Puppet::Application
run_mode :agent
def app_defaults
super.merge({
:catalog_terminus => :rest,
:catalog_cache_terminus => :json,
:node_terminus => :rest,
:facts_terminus => :facter,
})
end
def preinit
# Do an initial trap, so that cancels don't get a stack trace.
Signal.trap(:INT) do
$stderr.puts "Cancelling startup"
exit(0)
end
{
:waitforcert => nil,
:detailed_exitcodes => false,
:verbose => false,
:debug => false,
:setdest => false,
:enable => false,
:disable => false,
:client => true,
:fqdn => nil,
:serve => [],
:digest => 'SHA256',
:graph => true,
:fingerprint => false,
}.each do |opt,val|
options[opt] = val
end
@argv = ARGV.dup
end
option("--disable [MESSAGE]") do |message|
options[:disable] = true
options[:disable_message] = message
end
option("--enable")
option("--debug","-d")
option("--fqdn FQDN","-f")
option("--test","-t")
option("--verbose","-v")
option("--fingerprint")
option("--digest DIGEST")
option("--no-client") do |arg|
options[:client] = false
end
option("--detailed-exitcodes") do |arg|
options[:detailed_exitcodes] = true
end
option("--logdest DEST", "-l DEST") do |arg|
handle_logdest_arg(arg)
end
option("--waitforcert WAITFORCERT", "-w") do |arg|
options[:waitforcert] = arg.to_i
end
def help
<<-'HELP'
puppet-agent(8) -- The puppet agent daemon
========
SYNOPSIS
--------
Retrieves the client configuration from the puppet master and applies it to
the local host.
This service may be run as a daemon, run periodically using cron (or something
similar), or run interactively for testing purposes.
USAGE
-----
puppet agent [--certname <name>] [-D|--daemonize|--no-daemonize]
[-d|--debug] [--detailed-exitcodes] [--digest <digest>] [--disable [message]] [--enable]
[--fingerprint] [-h|--help] [-l|--logdest syslog|<file>|console]
[--no-client] [--noop] [-o|--onetime] [-t|--test]
[-v|--verbose] [-V|--version] [-w|--waitforcert <seconds>]
DESCRIPTION
-----------
This is the main puppet client. Its job is to retrieve the local
machine's configuration from a remote server and apply it. In order to
successfully communicate with the remote server, the client must have a
certificate signed by a certificate authority that the server trusts;
the recommended method for this, at the moment, is to run a certificate
authority as part of the puppet server (which is the default). The
client will connect and request a signed certificate, and will continue
connecting until it receives one.
Once the client has a signed certificate, it will retrieve its
configuration and apply it.
USAGE NOTES
-----------
'puppet agent' does its best to find a compromise between interactive
use and daemon use. Run with no arguments and no configuration, it will
go into the background, attempt to get a signed certificate, and retrieve
and apply its configuration every 30 minutes.
Some flags are meant specifically for interactive use -- in particular,
'test', 'tags' or 'fingerprint' are useful. 'test' enables verbose
logging, causes the daemon to stay in the foreground, exits if the
server's configuration is invalid (this happens if, for instance, you've
left a syntax error on the server), and exits after running the
configuration once (rather than hanging around as a long-running
process).
'tags' allows you to specify what portions of a configuration you want
to apply. Puppet elements are tagged with all of the class or definition
names that contain them, and you can use the 'tags' flag to specify one
of these names, causing only configuration elements contained within
that class or definition to be applied. This is very useful when you are
testing new configurations -- for instance, if you are just starting to
manage 'ntpd', you would put all of the new elements into an 'ntpd'
class, and call puppet with '--tags ntpd', which would only apply that
small portion of the configuration during your testing, rather than
applying the whole thing.
'fingerprint' is a one-time flag. In this mode 'puppet agent' will run
once and display on the console (and in the log) the current certificate
(or certificate request) fingerprint. Providing the '--digest' option
allows to use a different digest algorithm to generate the fingerprint.
The main use is to verify that before signing a certificate request on
the master, the certificate request the master received is the same as
the one the client sent (to prevent against man-in-the-middle attacks
when signing certificates).
OPTIONS
-------
Note that any Puppet setting that's valid in the configuration file is also a
valid long argument. For example, 'server' is a valid setting, so you can
specify '--server <servername>' as an argument. Boolean settings translate into
'--setting' and '--no-setting' pairs.
See the configuration file documentation at
http://docs.puppetlabs.com/references/stable/configuration.html for the
full list of acceptable settings. A commented list of all settings can also be
generated by running puppet agent with '--genconfig'.
* --certname:
Set the certname (unique ID) of the client. The master reads this
unique identifying string, which is usually set to the node's
fully-qualified domain name, to determine which configurations the
node will receive. Use this option to debug setup problems or
implement unusual node identification schemes.
(This is a Puppet setting, and can go in puppet.conf.)
* --daemonize:
Send the process into the background. This is the default.
(This is a Puppet setting, and can go in puppet.conf. Note the special 'no-'
prefix for boolean settings on the command line.)
* --no-daemonize:
Do not send the process into the background.
(This is a Puppet setting, and can go in puppet.conf. Note the special 'no-'
prefix for boolean settings on the command line.)
* --debug:
Enable full debugging.
* --detailed-exitcodes:
Provide transaction information via exit codes. If this is enabled, an exit
code of '2' means there were changes, an exit code of '4' means there were
failures during the transaction, and an exit code of '6' means there were both
changes and failures.
* --digest:
Change the certificate fingerprinting digest algorithm. The default is
SHA256. Valid values depends on the version of OpenSSL installed, but
will likely contain MD5, MD2, SHA1 and SHA256.
* --disable:
Disable working on the local system. This puts a lock file in place,
causing 'puppet agent' not to work on the system until the lock file
is removed. This is useful if you are testing a configuration and do
not want the central configuration to override the local state until
everything is tested and committed.
Disable can also take an optional message that will be reported by the
'puppet agent' at the next disabled run.
'puppet agent' uses the same lock file while it is running, so no more
than one 'puppet agent' process is working at a time.
'puppet agent' exits after executing this.
* --enable:
Enable working on the local system. This removes any lock file,
causing 'puppet agent' to start managing the local system again
(although it will continue to use its normal scheduling, so it might
not start for another half hour).
'puppet agent' exits after executing this.
* --fingerprint:
Display the current certificate or certificate signing request
fingerprint and then exit. Use the '--digest' option to change the
digest algorithm used.
* --help:
Print this help message
* --logdest:
Where to send messages. Choose between syslog, the console, and a log
file. Defaults to sending messages to syslog, or the console if
debugging or verbosity is enabled.
* --masterport:
The port on which to contact the puppet master.
(This is a Puppet setting, and can go in puppet.conf.)
* --no-client:
Do not create a config client. This will cause the daemon to start
but not check configuration unless it is triggered with `puppet
kick`. This only makes sense when puppet agent is being run with
listen = true in puppet.conf or was started with the `--listen` option.
* --noop:
Use 'noop' mode where the daemon runs in a no-op or dry-run mode. This
is useful for seeing what changes Puppet will make without actually
executing the changes.
(This is a Puppet setting, and can go in puppet.conf. Note the special 'no-'
prefix for boolean settings on the command line.)
* --onetime:
Run the configuration once. Runs a single (normally daemonized) Puppet
run. Useful for interactively running puppet agent when used in
conjunction with the --no-daemonize option.
(This is a Puppet setting, and can go in puppet.conf. Note the special 'no-'
prefix for boolean settings on the command line.)
* --test:
Enable the most common options used for testing. These are 'onetime',
'verbose', 'ignorecache', 'no-daemonize', 'no-usecacheonfailure',
'detailed-exitcodes', 'no-splay', and 'show_diff'.
* --verbose:
Turn on verbose reporting.
* --version:
Print the puppet version number and exit.
* --waitforcert:
This option only matters for daemons that do not yet have certificates
and it is enabled by default, with a value of 120 (seconds). This
causes 'puppet agent' to connect to the server every 2 minutes and ask
it to sign a certificate request. This is useful for the initial setup
of a puppet client. You can turn off waiting for certificates by
specifying a time of 0.
(This is a Puppet setting, and can go in puppet.conf. Note the special 'no-'
prefix for boolean settings on the command line.)
EXAMPLE
-------
$ puppet agent --server puppet.domain.com
DIAGNOSTICS
-----------
Puppet agent accepts the following signals:
* SIGHUP:
Restart the puppet agent daemon.
* SIGINT and SIGTERM:
Shut down the puppet agent daemon.
* SIGUSR1:
Immediately retrieve and apply configurations from the puppet master.
* SIGUSR2:
Close file descriptors for log files and reopen them. Used with logrotate.
AUTHOR
------
Luke Kanies
COPYRIGHT
---------
Copyright (c) 2011 Puppet Labs, LLC Licensed under the Apache 2.0 License
HELP
end
def run_command
if options[:fingerprint]
fingerprint
else
# It'd be nice to daemonize later, but we have to daemonize before
# waiting for certificates so that we don't block
daemon = daemonize_process_when(Puppet[:daemonize])
wait_for_certificates
if Puppet[:onetime]
onetime(daemon)
else
main(daemon)
end
end
end
def fingerprint
host = Puppet::SSL::Host.new
unless cert = host.certificate || host.certificate_request
$stderr.puts "Fingerprint asked but no certificate nor certificate request have yet been issued"
exit(1)
return
end
unless digest = cert.digest(options[:digest].to_s)
raise ArgumentError, "Could not get fingerprint for digest '#{options[:digest]}'"
end
puts digest.to_s
end
def onetime(daemon)
if Puppet[:listen]
Puppet.notice "Ignoring --listen on onetime run"
end
unless options[:client]
Puppet.err "onetime is specified but there is no client"
exit(43)
return
end
daemon.set_signal_traps
begin
exitstatus = daemon.agent.run
rescue => detail
Puppet.log_exception(detail)
end
daemon.stop(:exit => false)
if not exitstatus
exit(1)
elsif options[:detailed_exitcodes] then
exit(exitstatus)
else
exit(0)
end
end
def main(daemon)
if Puppet[:listen]
setup_listen(daemon)
end
Puppet.notice "Starting Puppet client version #{Puppet.version}"
daemon.start
end
# Enable all of the most common test options.
def setup_test
Puppet.settings.handlearg("--ignorecache")
Puppet.settings.handlearg("--no-usecacheonfailure")
Puppet.settings.handlearg("--no-splay")
Puppet.settings.handlearg("--show_diff")
Puppet.settings.handlearg("--no-daemonize")
options[:verbose] = true
Puppet[:onetime] = true
options[:detailed_exitcodes] = true
end
def setup
setup_test if options[:test]
setup_logs
exit(Puppet.settings.print_configs ? 0 : 1) if Puppet.settings.print_configs?
if options[:fqdn]
Puppet[:certname] = options[:fqdn]
end
Puppet.settings.use :main, :agent, :ssl
# Always ignoreimport for agent. It really shouldn't even try to import,
# but this is just a temporary band-aid.
Puppet[:ignoreimport] = true
Puppet::Transaction::Report.indirection.terminus_class = :rest
# we want the last report to be persisted locally
Puppet::Transaction::Report.indirection.cache_class = :yaml
if Puppet[:catalog_cache_terminus]
Puppet::Resource::Catalog.indirection.cache_class = Puppet[:catalog_cache_terminus]
end
if options[:fingerprint]
# in fingerprint mode we just need
# access to the local files and we don't need a ca
Puppet::SSL::Host.ca_location = :none
else
Puppet::SSL::Host.ca_location = :remote
setup_agent
end
end
private
def enable_disable_client(agent)
if options[:enable]
agent.enable
elsif options[:disable]
agent.disable(options[:disable_message] || 'reason not specified')
end
exit(0)
end
def setup_listen(daemon)
Puppet.warning "Puppet --listen / kick is deprecated. See http://links.puppetlabs.com/puppet-kick-deprecation"
- unless Puppet::FileSystem::File.exist?(Puppet[:rest_authconfig])
+ unless Puppet::FileSystem.exist?(Puppet[:rest_authconfig])
Puppet.err "Will not start without authorization file #{Puppet[:rest_authconfig]}"
exit(14)
end
require 'puppet/network/server'
# No REST handlers yet.
server = Puppet::Network::Server.new(Puppet[:bindaddress], Puppet[:puppetport])
daemon.server = server
end
def setup_agent
# We need to make the client either way, we just don't start it
# if --no-client is set.
require 'puppet/agent'
require 'puppet/configurer'
agent = Puppet::Agent.new(Puppet::Configurer, (not(Puppet[:onetime])))
enable_disable_client(agent) if options[:enable] or options[:disable]
@agent = agent if options[:client]
end
def daemonize_process_when(should_daemonize)
daemon = Puppet::Daemon.new(Puppet::Util::Pidlock.new(Puppet[:pidfile]))
daemon.argv = @argv
daemon.agent = @agent
daemon.daemonize if should_daemonize
daemon
end
def wait_for_certificates
host = Puppet::SSL::Host.new
waitforcert = options[:waitforcert] || (Puppet[:onetime] ? 0 : Puppet[:waitforcert])
host.wait_for_cert(waitforcert)
end
end
diff --git a/lib/puppet/application/apply.rb b/lib/puppet/application/apply.rb
index 2eb4415b3..22f29558b 100644
--- a/lib/puppet/application/apply.rb
+++ b/lib/puppet/application/apply.rb
@@ -1,270 +1,290 @@
require 'puppet/application'
require 'puppet/configurer'
class Puppet::Application::Apply < Puppet::Application
option("--debug","-d")
option("--execute EXECUTE","-e") do |arg|
options[:code] = arg
end
option("--loadclasses","-L")
+ option("--test","-t")
option("--verbose","-v")
option("--use-nodes")
option("--detailed-exitcodes")
option("--write-catalog-summary")
option("--catalog catalog", "-c catalog") do |arg|
options[:catalog] = arg
end
option("--logdest LOGDEST", "-l") do |arg|
handle_logdest_arg(arg)
end
option("--parseonly") do |args|
puts "--parseonly has been removed. Please use 'puppet parser validate <manifest>'"
exit 1
end
def help
<<-'HELP'
puppet-apply(8) -- Apply Puppet manifests locally
========
SYNOPSIS
--------
Applies a standalone Puppet manifest to the local system.
USAGE
-----
puppet apply [-h|--help] [-V|--version] [-d|--debug] [-v|--verbose]
[-e|--execute] [--detailed-exitcodes] [-l|--logdest <file>] [--noop]
[--catalog <catalog>] [--write-catalog-summary] <file>
DESCRIPTION
-----------
This is the standalone puppet execution tool; use it to apply
individual manifests.
When provided with a modulepath, via command line or config file, puppet
apply can effectively mimic the catalog that would be served by puppet
master with access to the same modules, although there are some subtle
differences. When combined with scheduling and an automated system for
pushing manifests, this can be used to implement a serverless Puppet
site.
Most users should use 'puppet agent' and 'puppet master' for site-wide
manifests.
OPTIONS
-------
-Note that any configuration parameter that's valid in the configuration
+Note that any setting that's valid in the configuration
file is also a valid long argument. For example, 'tags' is a
-valid configuration parameter, so you can specify '--tags <class>,<tag>'
+valid setting, so you can specify '--tags <class>,<tag>'
as an argument.
See the configuration file documentation at
http://docs.puppetlabs.com/references/stable/configuration.html for the
full list of acceptable parameters. A commented list of all
configuration options can also be generated by running puppet with
'--genconfig'.
* --debug:
Enable full debugging.
* --detailed-exitcodes:
Provide transaction information via exit codes. If this is enabled, an exit
code of '2' means there were changes, an exit code of '4' means there were
failures during the transaction, and an exit code of '6' means there were both
changes and failures.
* --help:
Print this help message
* --loadclasses:
Load any stored classes. 'puppet agent' caches configured classes
(usually at /etc/puppet/classes.txt), and setting this option causes
all of those classes to be set in your puppet manifest.
* --logdest:
Where to send messages. Choose between syslog, the console, and a log
file. Defaults to sending messages to the console.
* --noop:
Use 'noop' mode where Puppet runs in a no-op or dry-run mode. This
is useful for seeing what changes Puppet will make without actually
executing the changes.
* --execute:
Execute a specific piece of Puppet code
+* --test:
+ Enable the most common options used for testing. These are 'verbose',
+ 'detailed-exitcodes' and 'show_diff'.
+
* --verbose:
Print extra information.
* --catalog:
Apply a JSON catalog (such as one generated with 'puppet master --compile'). You can
either specify a JSON file or pipe in JSON from standard input.
* --write-catalog-summary
After compiling the catalog saves the resource list and classes list to the node
in the state directory named classes.txt and resources.txt
EXAMPLE
-------
$ puppet apply -l /tmp/manifest.log manifest.pp
$ puppet apply --modulepath=/root/dev/modules -e "include ntpd::server"
$ puppet apply --catalog catalog.json
AUTHOR
------
Luke Kanies
COPYRIGHT
---------
Copyright (c) 2011 Puppet Labs, LLC Licensed under the Apache 2.0 License
HELP
end
def app_defaults
super.merge({
:default_file_terminus => :file_server,
})
end
def run_command
if options[:catalog]
apply
else
main
end
end
def apply
if options[:catalog] == "-"
text = $stdin.read
else
text = ::File.read(options[:catalog])
end
catalog = read_catalog(text)
apply_catalog(catalog)
end
def main
# Set our code or file to use.
if options[:code] or command_line.args.length == 0
Puppet[:code] = options[:code] || STDIN.read
else
manifest = command_line.args.shift
- raise "Could not find file #{manifest}" unless Puppet::FileSystem::File.exist?(manifest)
+ raise "Could not find file #{manifest}" unless Puppet::FileSystem.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
unless Puppet[:node_name_fact].empty?
# Collect our facts.
unless facts = Puppet::Node::Facts.indirection.find(Puppet[:node_name_value])
raise "Could not find facts for #{Puppet[:node_name_value]}"
end
Puppet[:node_name_value] = facts.values[Puppet[:node_name_fact]]
facts.name = Puppet[:node_name_value]
end
- # Find our Node
- unless node = Puppet::Node.indirection.find(Puppet[:node_name_value])
- raise "Could not find node #{Puppet[:node_name_value]}"
- end
+ configured_environment = Puppet.lookup(:current_environment)
+ apply_environment = manifest ?
+ configured_environment.override_with(:manifest => manifest) :
+ configured_environment
- # Merge in the facts.
- node.merge(facts.values) if facts
+ Puppet.override(:environments => Puppet::Environments::Static.new(apply_environment)) do
+ # Find our Node
+ unless node = Puppet::Node.indirection.find(Puppet[:node_name_value])
+ raise "Could not find node #{Puppet[:node_name_value]}"
+ end
- # Allow users to load the classes that puppet agent creates.
- if options[:loadclasses]
- file = Puppet[:classfile]
- if Puppet::FileSystem::File.exist?(file)
- unless FileTest.readable?(file)
- $stderr.puts "#{file} is not readable"
- exit(63)
+ # Merge in the facts.
+ node.merge(facts.values) if facts
+
+ # Allow users to load the classes that puppet agent creates.
+ if options[:loadclasses]
+ file = Puppet[:classfile]
+ if Puppet::FileSystem.exist?(file)
+ unless FileTest.readable?(file)
+ $stderr.puts "#{file} is not readable"
+ exit(63)
+ end
+ node.classes = ::File.read(file).split(/[\s\n]+/)
end
- node.classes = ::File.read(file).split(/[\s\n]+/)
end
- end
- begin
- # Compile our catalog
- starttime = Time.now
- catalog = Puppet::Resource::Catalog.indirection.find(node.name, :use_node => node)
+ begin
+ # Compile our catalog
+ starttime = Time.now
+ catalog = Puppet::Resource::Catalog.indirection.find(node.name, :use_node => node)
- # Translate it to a RAL catalog
- catalog = catalog.to_ral
+ # Translate it to a RAL catalog
+ catalog = catalog.to_ral
- catalog.finalize
+ catalog.finalize
- catalog.retrieval_duration = Time.now - starttime
+ catalog.retrieval_duration = Time.now - starttime
- if options[:write_catalog_summary]
- catalog.write_class_file
- catalog.write_resource_file
- end
+ if options[:write_catalog_summary]
+ catalog.write_class_file
+ catalog.write_resource_file
+ end
- exit_status = apply_catalog(catalog)
+ exit_status = apply_catalog(catalog)
- if not exit_status
+ if not exit_status
+ exit(1)
+ elsif options[:detailed_exitcodes] then
+ exit(exit_status)
+ else
+ exit(0)
+ end
+ rescue => detail
+ Puppet.log_exception(detail)
exit(1)
- elsif options[:detailed_exitcodes] then
- exit(exit_status)
- else
- exit(0)
end
- rescue => detail
- Puppet.log_exception(detail)
- exit(1)
end
end
+ # Enable all of the most common test options.
+ def setup_test
+ Puppet.settings.handlearg("--show_diff")
+ options[:verbose] = true
+ options[:detailed_exitcodes] = true
+ end
+
def setup
+ setup_test if options[:test]
+
exit(Puppet.settings.print_configs ? 0 : 1) if Puppet.settings.print_configs?
Puppet::Util::Log.newdestination(:console) unless options[:setdest]
Signal.trap(:INT) do
$stderr.puts "Exiting"
exit(1)
end
# we want the last report to be persisted locally
Puppet::Transaction::Report.indirection.cache_class = :yaml
set_log_level
if Puppet[:profile]
Puppet::Util::Profiler.current = Puppet::Util::Profiler::WallClock.new(Puppet.method(:debug), "apply")
end
end
private
def read_catalog(text)
begin
catalog = Puppet::Resource::Catalog.convert_from(Puppet::Resource::Catalog.default_format,text)
catalog = Puppet::Resource::Catalog.pson_create(catalog) unless catalog.is_a?(Puppet::Resource::Catalog)
rescue => detail
- raise Puppet::Error, "Could not deserialize catalog from pson: #{detail}"
+ raise Puppet::Error, "Could not deserialize catalog from pson: #{detail}", detail.backtrace
end
catalog.to_ral
end
def apply_catalog(catalog)
configurer = Puppet::Configurer.new
configurer.run(:catalog => catalog, :pluginsync => false)
end
end
diff --git a/lib/puppet/application/cert.rb b/lib/puppet/application/cert.rb
index 6ad901593..6f3fa4bc2 100644
--- a/lib/puppet/application/cert.rb
+++ b/lib/puppet/application/cert.rb
@@ -1,277 +1,277 @@
require 'puppet/application'
require 'puppet/ssl/certificate_authority/interface'
class Puppet::Application::Cert < Puppet::Application
run_mode :master
attr_accessor :all, :ca, :digest, :signed
def subcommand
@subcommand
end
def subcommand=(name)
# Handle the nasty, legacy mapping of "clean" to "destroy".
sub = name.to_sym
@subcommand = (sub == :clean ? :destroy : sub)
end
option("--clean", "-c") do |arg|
self.subcommand = "destroy"
end
option("--all", "-a") do |arg|
@all = true
end
option("--digest DIGEST") do |arg|
@digest = arg
end
option("--signed", "-s") do |arg|
@signed = true
end
option("--debug", "-d") do |arg|
Puppet::Util::Log.level = :debug
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|
options[:allow_dns_alt_names] = value
end
option("--verbose", "-v") do |arg|
Puppet::Util::Log.level = :info
end
def help
<<-'HELP'
puppet-cert(8) -- Manage certificates and requests
========
SYNOPSIS
--------
Standalone certificate authority. Capable of generating certificates,
but mostly used for signing certificate requests from puppet clients.
USAGE
-----
puppet cert <action> [-h|--help] [-V|--version] [-d|--debug] [-v|--verbose]
[--digest <digest>] [<host>]
DESCRIPTION
-----------
Because the puppet master service defaults to not signing client
certificate requests, this script is available for signing outstanding
requests. It can be used to list outstanding requests and then either
sign them individually or sign all of them.
ACTIONS
-------
Every action except 'list' and 'generate' requires a hostname to act on,
unless the '--all' option is set.
* clean:
Revoke a host's certificate (if applicable) and remove all files
related to that host from puppet cert's storage. This is useful when
rebuilding hosts, since new certificate signing requests will only be
honored if puppet cert does not have a copy of a signed certificate
for that host. If '--all' is specified then all host certificates,
both signed and unsigned, will be removed.
* fingerprint:
Print the DIGEST (defaults to the signing algorithm) fingerprint of a
host's certificate.
* generate:
Generate a certificate for a named client. A certificate/keypair will
be generated for each client named on the command line.
* list:
List outstanding certificate requests. If '--all' is specified, signed
certificates are also listed, prefixed by '+', and revoked or invalid
certificates are prefixed by '-' (the verification outcome is printed
in parenthesis).
* print:
Print the full-text version of a host's certificate.
* revoke:
Revoke the certificate of a client. The certificate can be specified either
by its serial number (given as a hexadecimal number prefixed by '0x') or by its
hostname. The certificate is revoked by adding it to the Certificate Revocation
List given by the 'cacrl' configuration option. Note that the puppet master
needs to be restarted after revoking certificates.
* sign:
Sign an outstanding certificate request.
* 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
-------
-Note that any configuration parameter that's valid in the configuration
+Note that any setting that's valid in the configuration
file is also a valid long argument. For example, 'ssldir' is a valid
-configuration parameter, so you can specify '--ssldir <directory>' as an
+setting, so you can specify '--ssldir <directory>' as an
argument.
See the configuration file documentation at
http://docs.puppetlabs.com/references/stable/configuration.html for the
full list of acceptable parameters. A commented list of all
configuration options can also be generated by running puppet cert with
'--genconfig'.
* --all:
Operate on all items. Currently only makes sense with the 'sign',
'clean', 'list', and 'fingerprint' actions.
* --digest:
- Set the digest for fingerprinting (defaults to the the digest used when
+ Set the digest for fingerprinting (defaults to the digest used when
signing the cert). Valid values depends on your openssl and openssl ruby
extension version.
* --debug:
Enable full debugging.
* --help:
Print this help message
* --verbose:
Enable verbosity.
* --version:
Print the puppet version number and exit.
EXAMPLE
-------
$ puppet cert list
culain.madstop.com
$ puppet cert sign culain.madstop.com
AUTHOR
------
Luke Kanies
COPYRIGHT
---------
Copyright (c) 2011 Puppet Labs, LLC Licensed under the Apache 2.0 License
HELP
end
def main
if @all
hosts = :all
elsif @signed
hosts = :signed
else
hosts = command_line.args.collect { |h| h.downcase }
end
begin
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)
end
end
def setup
require 'puppet/ssl/certificate_authority'
exit(Puppet.settings.print_configs ? 0 : 1) if Puppet.settings.print_configs?
Puppet::Util::Log.newdestination :console
if [:generate, :destroy].include? subcommand
Puppet::SSL::Host.ca_location = :local
else
Puppet::SSL::Host.ca_location = :only
end
# If we are generating, and the option came from the CLI, it gets added to
# the data. This will do the right thing for non-local certificates, in
# that the command line but *NOT* the config file option will apply.
if subcommand == :generate
if Puppet.settings.set_by_cli?(:dns_alt_names)
options[:dns_alt_names] = Puppet[:dns_alt_names]
end
end
begin
@ca = Puppet::SSL::CertificateAuthority.new
rescue => detail
Puppet.log_exception(detail)
exit(23)
end
end
def parse_options
# handle the bareword subcommand pattern.
result = super
unless self.subcommand then
if sub = self.command_line.args.shift then
self.subcommand = sub
else
puts help
exit
end
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 5eef49fef..854188ad1 100644
--- a/lib/puppet/application/device.rb
+++ b/lib/puppet/application/device.rb
@@ -1,239 +1,238 @@
require 'puppet/application'
require 'puppet/util/network_device'
-
class Puppet::Application::Device < Puppet::Application
run_mode :agent
attr_accessor :args, :agent, :host
def app_defaults
super.merge({
:catalog_terminus => :rest,
:catalog_cache_terminus => :json,
:node_terminus => :rest,
:facts_terminus => :network_device,
})
end
def preinit
# Do an initial trap, so that cancels don't get a stack trace.
Signal.trap(:INT) do
$stderr.puts "Cancelling startup"
exit(0)
end
{
:waitforcert => nil,
:detailed_exitcodes => false,
:verbose => false,
:debug => false,
:centrallogs => false,
:setdest => false,
}.each do |opt,val|
options[opt] = val
end
@args = {}
end
option("--centrallogging")
option("--debug","-d")
option("--verbose","-v")
option("--detailed-exitcodes") do |arg|
options[:detailed_exitcodes] = true
end
option("--logdest DEST", "-l DEST") do |arg|
handle_logdest_arg(arg)
end
option("--waitforcert WAITFORCERT", "-w") do |arg|
options[:waitforcert] = arg.to_i
end
option("--port PORT","-p") do |arg|
@args[:Port] = arg
end
def help
<<-'HELP'
puppet-device(8) -- Manage remote network devices
========
SYNOPSIS
--------
Retrieves all configurations from the puppet master and apply
them to the remote devices configured in /etc/puppet/device.conf.
Currently must be run out periodically, using cron or something similar.
USAGE
-----
puppet device [-d|--debug] [--detailed-exitcodes] [-V|--version]
[-h|--help] [-l|--logdest syslog|<file>|console]
[-v|--verbose] [-w|--waitforcert <seconds>]
DESCRIPTION
-----------
Once the client has a signed certificate for a given remote device, it will
retrieve its configuration and apply it.
USAGE NOTES
-----------
One need a /etc/puppet/device.conf file with the following content:
[remote.device.fqdn]
type <type>
url <url>
where:
* type: the current device type (the only value at this time is cisco)
* url: an url allowing to connect to the device
Supported url must conforms to:
scheme://user:password@hostname/?query
with:
* scheme: either ssh or telnet
* user: username, can be omitted depending on the switch/router configuration
* password: the connection password
* query: this is device specific. Cisco devices supports an enable parameter whose
value would be the enable password.
OPTIONS
-------
-Note that any configuration parameter that's valid in the configuration file
+Note that any setting that's valid in the configuration file
is also a valid long argument. For example, 'server' is a valid configuration
parameter, so you can specify '--server <servername>' as an argument.
* --debug:
Enable full debugging.
* --detailed-exitcodes:
Provide transaction information via exit codes. If this is enabled, an exit
code of '2' means there were changes, an exit code of '4' means there were
failures during the transaction, and an exit code of '6' means there were both
changes and failures.
* --help:
Print this help message
* --logdest:
Where to send messages. Choose between syslog, the console, and a log file.
Defaults to sending messages to syslog, or the console if debugging or
verbosity is enabled.
* --verbose:
Turn on verbose reporting.
* --waitforcert:
This option only matters for daemons that do not yet have certificates
and it is enabled by default, with a value of 120 (seconds). This causes
+puppet agent+ to connect to the server every 2 minutes and ask it to sign a
certificate request. This is useful for the initial setup of a puppet
client. You can turn off waiting for certificates by specifying a time
of 0.
EXAMPLE
-------
$ puppet device --server puppet.domain.com
AUTHOR
------
Brice Figureau
COPYRIGHT
---------
Copyright (c) 2011 Puppet Labs, LLC
Licensed under the Apache 2.0 License
HELP
end
def main
vardir = Puppet[:vardir]
confdir = Puppet[:confdir]
certname = Puppet[:certname]
# find device list
require 'puppet/util/network_device/config'
devices = Puppet::Util::NetworkDevice::Config.devices
if devices.empty?
Puppet.err "No device found in #{Puppet[:deviceconfig]}"
exit(1)
end
devices.each_value do |device|
begin
Puppet.info "starting applying configuration to #{device.name} at #{device.url}"
# override local $vardir and $certname
- Puppet.settings.set_value(:confdir, ::File.join(Puppet[:devicedir], device.name), :cli)
- Puppet.settings.set_value(:vardir, ::File.join(Puppet[:devicedir], device.name), :cli)
- Puppet.settings.set_value(:certname, device.name, :cli)
+ Puppet[:confdir] = ::File.join(Puppet[:devicedir], device.name)
+ Puppet[:vardir] = ::File.join(Puppet[:devicedir], device.name)
+ Puppet[:certname] = device.name
# this will reload and recompute default settings and create the devices sub vardir, or we hope so :-)
Puppet.settings.use :main, :agent, :ssl
# this init the device singleton, so that the facts terminus
# and the various network_device provider can use it
Puppet::Util::NetworkDevice.init(device)
# ask for a ssl cert if needed, but at least
# setup the ssl system for this device.
setup_host
require 'puppet/configurer'
configurer = Puppet::Configurer.new
configurer.run(:network_device => true, :pluginsync => Puppet[:pluginsync])
rescue => detail
Puppet.log_exception(detail)
ensure
- Puppet.settings.set_value(:vardir, vardir, :cli)
- Puppet.settings.set_value(:confdir, confdir, :cli)
- Puppet.settings.set_value(:certname, certname, :cli)
+ Puppet[:vardir] = vardir
+ Puppet[:confdir] = confdir
+ Puppet[:certname] = certname
Puppet::SSL::Host.reset
end
end
end
def setup_host
@host = Puppet::SSL::Host.new
waitforcert = options[:waitforcert] || (Puppet[:onetime] ? 0 : Puppet[:waitforcert])
@host.wait_for_cert(waitforcert)
end
def setup
setup_logs
args[:Server] = Puppet[:server]
if options[:centrallogs]
logdest = args[:Server]
logdest += ":" + args[:Port] if args.include?(:Port)
Puppet::Util::Log.newdestination(logdest)
end
Puppet.settings.use :main, :agent, :device, :ssl
# Always ignoreimport for agent. It really shouldn't even try to import,
# but this is just a temporary band-aid.
Puppet[:ignoreimport] = true
# We need to specify a ca location for all of the SSL-related
# indirected classes to work; in fingerprint mode we just need
# access to the local files and we don't need a ca.
Puppet::SSL::Host.ca_location = :remote
Puppet::Transaction::Report.indirection.terminus_class = :rest
if Puppet[:catalog_cache_terminus]
Puppet::Resource::Catalog.indirection.cache_class = Puppet[:catalog_cache_terminus].intern
end
end
end
diff --git a/lib/puppet/application/doc.rb b/lib/puppet/application/doc.rb
index 32a378e6a..99a2ad346 100644
--- a/lib/puppet/application/doc.rb
+++ b/lib/puppet/application/doc.rb
@@ -1,280 +1,280 @@
require 'puppet/application'
class Puppet::Application::Doc < Puppet::Application
run_mode :master
attr_accessor :unknown_args, :manifest
def preinit
{:references => [], :mode => :text, :format => :to_markdown }.each do |name,value|
options[name] = value
end
@unknown_args = []
@manifest = false
end
option("--all","-a")
option("--outputdir OUTPUTDIR","-o")
option("--verbose","-v")
option("--debug","-d")
option("--charset CHARSET")
option("--format FORMAT", "-f") do |arg|
method = "to_#{arg}"
require 'puppet/util/reference'
if Puppet::Util::Reference.method_defined?(method)
options[:format] = method
else
raise "Invalid output format #{arg}"
end
end
option("--mode MODE", "-m") do |arg|
require 'puppet/util/reference'
if Puppet::Util::Reference.modes.include?(arg) or arg.intern==:rdoc
options[:mode] = arg.intern
else
raise "Invalid output mode #{arg}"
end
end
option("--list", "-l") do |arg|
require 'puppet/util/reference'
puts Puppet::Util::Reference.references.collect { |r| Puppet::Util::Reference.reference(r).doc }.join("\n")
exit(0)
end
option("--reference REFERENCE", "-r") do |arg|
options[:references] << arg.intern
end
def help
<<-'HELP'
puppet-doc(8) -- Generate Puppet documentation and references
========
SYNOPSIS
--------
Generates a reference for all Puppet types. Largely meant for internal
Puppet Labs use.
WARNING: RDoc support is only available under Ruby 1.8.7 and earlier.
USAGE
-----
puppet doc [-a|--all] [-h|--help] [-o|--outputdir <rdoc-outputdir>]
[-m|--mode text|pdf|rdoc] [-r|--reference <reference-name>]
[--charset <charset>] [<manifest-file>]
DESCRIPTION
-----------
If mode is not 'rdoc', then this command generates a Markdown document
describing all installed Puppet types or all allowable arguments to
puppet executables. It is largely meant for internal use and is used to
generate the reference document available on the Puppet Labs web site.
In 'rdoc' mode, this command generates an html RDoc hierarchy describing
the manifests that are in 'manifestdir' and 'modulepath' configuration
directives. The generated documentation directory is doc by default but
can be changed with the 'outputdir' option.
If the command is run with the name of a manifest file as an argument,
puppet doc will output a single manifest's documentation on stdout.
WARNING: RDoc support is only available under Ruby 1.8.7 and earlier.
The internal API used to support manifest documentation has changed
radically in newer versions, and support is not yet available for
using those versions of RDoc.
OPTIONS
-------
* --all:
Output the docs for all of the reference types. In 'rdoc' mode, this also
outputs documentation for all resources.
* --help:
Print this help message
* --outputdir:
Used only in 'rdoc' mode. The directory to which the rdoc output should
be written.
* --mode:
Determine the output mode. Valid modes are 'text', 'pdf' and 'rdoc'. The 'pdf'
mode creates PDF formatted files in the /tmp directory. The default mode is
'text'.
* --reference:
Build a particular reference. Get a list of references by running
'puppet doc --list'.
* --charset:
Used only in 'rdoc' mode. It sets the charset used in the html files produced.
* --manifestdir:
Used only in 'rdoc' mode. The directory to scan for stand-alone manifests.
If not supplied, puppet doc will use the manifestdir from puppet.conf.
* --modulepath:
Used only in 'rdoc' mode. The directory or directories to scan for modules.
If not supplied, puppet doc will use the modulepath from puppet.conf.
* --environment:
Used only in 'rdoc' mode. The configuration environment from which
to read the modulepath and manifestdir settings, when reading said settings
from puppet.conf.
EXAMPLE
-------
$ puppet doc -r type > /tmp/type_reference.markdown
or
$ puppet doc --outputdir /tmp/rdoc --mode rdoc /path/to/manifests
or
$ puppet doc /etc/puppet/manifests/site.pp
or
$ puppet doc -m pdf -r configuration
AUTHOR
------
Luke Kanies
COPYRIGHT
---------
Copyright (c) 2011 Puppet Labs, LLC Licensed under the Apache 2.0 License
HELP
end
def handle_unknown( opt, arg )
@unknown_args << {:opt => opt, :arg => arg }
true
end
def run_command
return[:rdoc].include?(options[:mode]) ? send(options[:mode]) : other
end
def rdoc
exit_code = 0
files = []
unless @manifest
- env = Puppet::Node::Environment.new
+ env = Puppet.lookup(:environments).get(Puppet[:environment])
files += env.modulepath
files << ::File.dirname(env[:manifest])
end
files += command_line.args
Puppet.info "scanning: #{files.inspect}"
Puppet.settings[:document_all] = options[:all] || false
begin
require 'puppet/util/rdoc'
if @manifest
Puppet::Util::RDoc.manifestdoc(files)
else
options[:outputdir] = "doc" unless options[:outputdir]
Puppet::Util::RDoc.rdoc(options[:outputdir], files, options[:charset])
end
rescue => detail
Puppet.log_exception(detail, "Could not generate documentation: #{detail}")
exit_code = 1
end
exit exit_code
end
def other
text = ""
with_contents = options[:references].length <= 1
exit_code = 0
require 'puppet/util/reference'
options[:references].sort { |a,b| a.to_s <=> b.to_s }.each do |name|
raise "Could not find reference #{name}" unless section = Puppet::Util::Reference.reference(name)
begin
# Add the per-section text, but with no ToC
text += section.send(options[:format], with_contents)
rescue => detail
Puppet.log_exception(detail, "Could not generate reference #{name}: #{detail}")
exit_code = 1
next
end
end
text += Puppet::Util::Reference.footer unless with_contents # We've only got one reference
if options[:mode] == :pdf
Puppet::Util::Reference.pdf(text)
else
puts text
end
exit exit_code
end
def setup
# sole manifest documentation
if command_line.args.size > 0
options[:mode] = :rdoc
@manifest = true
end
if options[:mode] == :rdoc
setup_rdoc
else
setup_reference
end
setup_logging
end
def setup_reference
if options[:all]
# Don't add dynamic references to the "all" list.
require 'puppet/util/reference'
options[:references] = Puppet::Util::Reference.references.reject do |ref|
Puppet::Util::Reference.reference(ref).dynamic?
end
end
options[:references] << :type if options[:references].empty?
end
def setup_rdoc(dummy_argument=:work_arround_for_ruby_GC_bug)
# consume the unknown options
# and feed them as settings
if @unknown_args.size > 0
@unknown_args.each do |option|
# force absolute path for modulepath when passed on commandline
if option[:opt]=="--modulepath" or option[:opt] == "--manifestdir"
option[:arg] = option[:arg].split(::File::PATH_SEPARATOR).collect { |p| ::File.expand_path(p) }.join(::File::PATH_SEPARATOR)
end
Puppet.settings.handlearg(option[:opt], option[:arg])
end
end
end
def setup_logging
# Handle the logging settings.
if options[:debug]
Puppet::Util::Log.level = :debug
elsif options[:verbose]
Puppet::Util::Log.level = :info
else
Puppet::Util::Log.level = :warning
end
Puppet::Util::Log.newdestination(:console)
end
end
diff --git a/lib/puppet/application/filebucket.rb b/lib/puppet/application/filebucket.rb
index 509885602..3f59f7259 100644
--- a/lib/puppet/application/filebucket.rb
+++ b/lib/puppet/application/filebucket.rb
@@ -1,184 +1,184 @@
require 'puppet/application'
class Puppet::Application::Filebucket < Puppet::Application
option("--bucket BUCKET","-b")
option("--debug","-d")
option("--local","-l")
option("--remote","-r")
option("--verbose","-v")
attr :args
def help
<<-'HELP'
puppet-filebucket(8) -- Store and retrieve files in a filebucket
========
SYNOPSIS
--------
A stand-alone Puppet filebucket client.
USAGE
-----
puppet filebucket <mode> [-h|--help] [-V|--version] [-d|--debug]
[-v|--verbose] [-l|--local] [-r|--remote] [-s|--server <server>]
[-b|--bucket <directory>] <file> <file> ...
Puppet filebucket can operate in three modes, with only one mode per call:
backup:
Send one or more files to the specified file bucket. Each sent file is
printed with its resulting md5 sum.
get:
Return the text associated with an md5 sum. The text is printed to
stdout, and only one file can be retrieved at a time.
restore:
Given a file path and an md5 sum, store the content associated with
the sum into the specified file path. You can specify an entirely new
path to this argument; you are not restricted to restoring the content
to its original location.
DESCRIPTION
-----------
This is a stand-alone filebucket client for sending files to a local or
central filebucket.
Note that 'filebucket' defaults to using a network-based filebucket
available on the server named 'puppet'. To use this, you'll have to be
running as a user with valid Puppet certificates. Alternatively, you can
use your local file bucket by specifying '--local'.
OPTIONS
-------
-Note that any configuration parameter that's valid in the configuration
+Note that any setting that's valid in the configuration
file is also a valid long argument. For example, 'ssldir' is a valid
-configuration parameter, so you can specify '--ssldir <directory>' as an
+setting, so you can specify '--ssldir <directory>' as an
argument.
See the configuration file documentation at
http://docs.puppetlabs.com/references/stable/configuration.html for the
full list of acceptable parameters. A commented list of all
configuration options can also be generated by running puppet with
'--genconfig'.
* --debug:
Enable full debugging.
* --help:
Print this help message
* --local:
Use the local filebucket. This will use the default configuration
information.
* --remote:
Use a remote filebucket. This will use the default configuration
information.
* --server:
The server to send the file to, instead of locally.
* --verbose:
Print extra information.
* --version:
Print version information.
EXAMPLE
-------
$ puppet filebucket backup /etc/passwd
/etc/passwd: 429b225650b912a2ee067b0a4cf1e949
$ puppet filebucket restore /tmp/passwd 429b225650b912a2ee067b0a4cf1e949
AUTHOR
------
Luke Kanies
COPYRIGHT
---------
Copyright (c) 2011 Puppet Labs, LLC Licensed under the Apache 2.0 License
HELP
end
def run_command
@args = command_line.args
command = args.shift
return send(command) if %w{get backup restore}.include? command
help
end
def get
md5 = args.shift
out = @client.getfile(md5)
print out
end
def backup
raise "You must specify a file to back up" unless args.length > 0
args.each do |file|
- unless Puppet::FileSystem::File.exist?(file)
+ unless Puppet::FileSystem.exist?(file)
$stderr.puts "#{file}: no such file"
next
end
unless FileTest.readable?(file)
$stderr.puts "#{file}: cannot read file"
next
end
md5 = @client.backup(file)
puts "#{file}: #{md5}"
end
end
def restore
file = args.shift
md5 = args.shift
@client.restore(file, md5)
end
def setup
Puppet::Log.newdestination(:console)
@client = nil
@server = nil
Signal.trap(:INT) do
$stderr.puts "Cancelling"
exit(1)
end
if options[:debug]
Puppet::Log.level = :debug
elsif options[:verbose]
Puppet::Log.level = :info
end
exit(Puppet.settings.print_configs ? 0 : 1) if Puppet.settings.print_configs?
require 'puppet/file_bucket/dipper'
begin
if options[:local] or options[:bucket]
path = options[:bucket] || Puppet[:bucketdir]
@client = Puppet::FileBucket::Dipper.new(:Path => path)
else
@client = Puppet::FileBucket::Dipper.new(:Server => Puppet[:server])
end
rescue => detail
Puppet.log_exception(detail)
exit(1)
end
end
end
diff --git a/lib/puppet/application/kick.rb b/lib/puppet/application/kick.rb
index 960cdc867..f5e36dd3c 100644
--- a/lib/puppet/application/kick.rb
+++ b/lib/puppet/application/kick.rb
@@ -1,351 +1,351 @@
require 'puppet/application'
class Puppet::Application::Kick < Puppet::Application
attr_accessor :hosts, :tags, :classes
option("--all","-a")
option("--foreground","-f")
option("--debug","-d")
option("--ping","-P")
option("--test")
option("--ignoreschedules")
option("--host HOST") do |arg|
@hosts << arg
end
option("--tag TAG", "-t") do |arg|
@tags << arg
end
option("--class CLASS", "-c") do |arg|
@classes << arg
end
option("--no-fqdn", "-n") do |arg|
options[:fqdn] = false
end
option("--parallel PARALLEL", "-p") do |arg|
begin
options[:parallel] = Integer(arg)
rescue
$stderr.puts "Could not convert #{arg.inspect} to an integer"
exit(23)
end
end
def help
<<-'HELP'
puppet-kick(8) -- Remotely control puppet agent
========
SYNOPSIS
--------
Trigger a puppet agent run on a set of hosts.
USAGE
-----
puppet kick [-a|--all] [-c|--class <class>] [-d|--debug] [-f|--foreground]
[-h|--help] [--host <host>] [--no-fqdn] [--ignoreschedules]
[-t|--tag <tag>] [--test] [-p|--ping] <host> [<host> [...]]
DESCRIPTION
-----------
This script can be used to connect to a set of machines running 'puppet
agent' and trigger them to run their configurations. The most common
usage would be to specify a class of hosts and a set of tags, and
'puppet kick' would look up in LDAP all of the hosts matching that
class, then connect to each host and trigger a run of all of the objects
with the specified tags.
If you are not storing your host configurations in LDAP, you can specify
hosts manually.
You will most likely have to run 'puppet kick' as root to get access to
the SSL certificates.
'puppet kick' reads 'puppet master''s configuration file, so that it can
copy things like LDAP settings.
USAGE NOTES
-----------
Puppet kick needs the puppet agent on the target machine to be running as a
daemon, be configured to listen for incoming network connections, and have an
appropriate security configuration.
The specific changes required are:
* Set `listen = true` in the agent's `puppet.conf` file (or `--listen` on the
command line)
* Configure the node's firewall to allow incoming connections on port 8139
* Insert the following stanza at the top of the node's `auth.conf` file:
# Allow puppet kick access
path /run
method save
auth any
allow workstation.example.com
This example would allow the machine `workstation.example.com` to trigger a
Puppet run; adjust the "allow" directive to suit your site. You may also use
`allow *` to allow anyone to trigger a Puppet run, but that makes it possible
to interfere with your site by triggering excessive Puppet runs.
See `http://docs.puppetlabs.com/guides/rest_auth_conf.html` for more details
about security settings.
OPTIONS
-------
-Note that any configuration parameter that's valid in the configuration
+Note that any setting that's valid in the configuration
file is also a valid long argument. For example, 'ssldir' is a valid
-configuration parameter, so you can specify '--ssldir <directory>' as an
+setting, so you can specify '--ssldir <directory>' as an
argument.
See the configuration file documentation at
http://docs.puppetlabs.com/references/latest/configuration.html for
the full list of acceptable parameters. A commented list of all
configuration options can also be generated by running puppet master
with '--genconfig'.
* --all:
Connect to all available hosts. Requires LDAP support at this point.
* --class:
Specify a class of machines to which to connect. This only works if
you have LDAP configured, at the moment.
* --debug:
Enable full debugging.
* --foreground:
Run each configuration in the foreground; that is, when connecting to
a host, do not return until the host has finished its run. The default
is false.
* --help:
Print this help message
* --host:
A specific host to which to connect. This flag can be specified more
than once.
* --ignoreschedules:
Whether the client should ignore schedules when running its
configuration. This can be used to force the client to perform work it
would not normally perform so soon. The default is false.
* --parallel:
How parallel to make the connections. Parallelization is provided by
forking for each client to which to connect. The default is 1, meaning
serial execution.
* --puppetport:
Use the specified TCP port to connect to agents. Defaults to 8139.
* --tag:
Specify a tag for selecting the objects to apply. Does not work with
the --test option.
* --test:
Print the hosts you would connect to but do not actually connect. This
option requires LDAP support at this point.
* --ping:
Do an ICMP echo against the target host. Skip hosts that don't respond
to ping.
EXAMPLE
-------
$ sudo puppet kick -p 10 -t remotefile -t webserver host1 host2
AUTHOR
------
Luke Kanies
COPYRIGHT
---------
Copyright (c) 2011 Puppet Labs, LLC Licensed under the Apache 2.0 License
HELP
end
def run_command
@hosts += command_line.args
options[:test] ? test : main
end
def test
puts "Skipping execution in test mode"
exit(0)
end
def main
Puppet.warning "Failed to load ruby LDAP library. LDAP functionality will not be available" unless Puppet.features.ldap?
require 'puppet/util/ldap/connection'
todo = @hosts.dup
failures = []
# Now do the actual work
go = true
while go
# If we don't have enough children in process and we still have hosts left to
# do, then do the next host.
if @children.length < options[:parallel] and ! todo.empty?
host = todo.shift
pid = safe_posix_fork do
run_for_host(host)
end
@children[pid] = host
else
# Else, see if we can reap a process.
begin
pid = Process.wait
if host = @children[pid]
# Remove our host from the list of children, so the parallelization
# continues working.
@children.delete(pid)
failures << host if $CHILD_STATUS.exitstatus != 0
print "#{host} finished with exit code #{$CHILD_STATUS.exitstatus}\n"
else
$stderr.puts "Could not find host for PID #{pid} with status #{$CHILD_STATUS.exitstatus}"
end
rescue Errno::ECHILD
# There are no children left, so just exit unless there are still
# children left to do.
next unless todo.empty?
if failures.empty?
puts "Finished"
exit(0)
else
puts "Failed: #{failures.join(", ")}"
exit(3)
end
end
end
end
end
def run_for_host(host)
if options[:ping]
%x{ping -c 1 #{host}}
unless $CHILD_STATUS == 0
$stderr.print "Could not contact #{host}\n"
exit($CHILD_STATUS)
end
end
require 'puppet/run'
Puppet::Run.indirection.terminus_class = :rest
port = Puppet[:puppetport]
url = ["https://#{host}:#{port}", "production", "run", host].join('/')
print "Triggering #{host}\n"
begin
run_options = {
:tags => @tags,
:background => ! options[:foreground],
:ignoreschedules => options[:ignoreschedules]
}
run = Puppet::Run.indirection.save(Puppet::Run.new( run_options ), url)
puts "Getting status"
result = run.status
puts "status is #{result}"
rescue => detail
Puppet.log_exception(detail, "Host #{host} failed: #{detail}\n")
exit(2)
end
case result
when "success";
exit(0)
when "running"
$stderr.puts "Host #{host} is already running"
exit(3)
else
$stderr.puts "Host #{host} returned unknown answer '#{result}'"
exit(12)
end
end
def initialize(*args)
super
@hosts = []
@classes = []
@tags = []
end
def preinit
[:INT, :TERM].each do |signal|
Signal.trap(signal) do
$stderr.puts "Cancelling"
exit(1)
end
end
options[:parallel] = 1
options[:verbose] = true
options[:fqdn] = true
options[:ignoreschedules] = false
options[:foreground] = false
end
def setup
super()
raise Puppet::Error.new("Puppet kick is not supported on Microsoft Windows") if Puppet.features.microsoft_windows?
Puppet.warning "Puppet kick is deprecated. See http://links.puppetlabs.com/puppet-kick-deprecation"
if options[:debug]
Puppet::Util::Log.level = :debug
else
Puppet::Util::Log.level = :info
end
if Puppet[:node_terminus] == :ldap and (options[:all] or @classes)
if options[:all]
@hosts = Puppet::Node.indirection.search("whatever", :fqdn => options[:fqdn]).collect { |node| node.name }
puts "all: #{@hosts.join(", ")}"
else
@hosts = []
@classes.each do |klass|
list = Puppet::Node.indirection.search("whatever", :fqdn => options[:fqdn], :class => klass).collect { |node| node.name }
puts "#{klass}: #{list.join(", ")}"
@hosts += list
end
end
elsif ! @classes.empty?
$stderr.puts "You must be using LDAP to specify host classes"
exit(24)
end
@children = {}
# If we get a signal, then kill all of our children and get out.
[:INT, :TERM].each do |signal|
Signal.trap(signal) do
Puppet.notice "Caught #{signal}; shutting down"
@children.each do |pid, host|
Process.kill("INT", pid)
end
waitall
exit(1)
end
end
end
end
diff --git a/lib/puppet/application/master.rb b/lib/puppet/application/master.rb
index b8f27994f..0c6a780a7 100644
--- a/lib/puppet/application/master.rb
+++ b/lib/puppet/application/master.rb
@@ -1,302 +1,302 @@
require 'puppet/application'
require 'puppet/daemon'
require 'puppet/util/pidlock'
class Puppet::Application::Master < Puppet::Application
run_mode :master
option("--debug", "-d")
option("--verbose", "-v")
# internal option, only to be used by ext/rack/config.ru
option("--rack")
option("--compile host", "-c host") do |arg|
options[:node] = arg
end
option("--logdest DEST", "-l DEST") do |arg|
handle_logdest_arg(arg)
end
option("--parseonly") do |args|
puts "--parseonly has been removed. Please use 'puppet parser validate <manifest>'"
exit 1
end
def help
<<-'HELP'
puppet-master(8) -- The puppet master daemon
========
SYNOPSIS
--------
The central puppet server. Functions as a certificate authority by
default.
USAGE
-----
puppet master [-D|--daemonize|--no-daemonize] [-d|--debug] [-h|--help]
[-l|--logdest <file>|console|syslog] [-v|--verbose] [-V|--version]
[--compile <node-name>]
DESCRIPTION
-----------
This command starts an instance of puppet master, running as a daemon
and using Ruby's built-in Webrick webserver. Puppet master can also be
managed by other application servers; when this is the case, this
executable is not used.
OPTIONS
-------
Note that any Puppet setting that's valid in the configuration file is also a
valid long argument. For example, 'server' is a valid setting, so you can
specify '--server <servername>' as an argument. Boolean settings translate into
'--setting' and '--no-setting' pairs.
See the configuration file documentation at
http://docs.puppetlabs.com/references/stable/configuration.html for the
full list of acceptable settings. A commented list of all settings can also be
generated by running puppet master with '--genconfig'.
* --daemonize:
Send the process into the background. This is the default.
(This is a Puppet setting, and can go in puppet.conf. Note the special 'no-'
prefix for boolean settings on the command line.)
* --no-daemonize:
Do not send the process into the background.
(This is a Puppet setting, and can go in puppet.conf. Note the special 'no-'
prefix for boolean settings on the command line.)
* --debug:
Enable full debugging.
* --help:
Print this help message.
* --logdest:
Where to send messages. Choose between syslog, the console, and a log
file. Defaults to sending messages to syslog, or the console if
debugging or verbosity is enabled.
* --masterport:
The port on which to listen for traffic.
(This is a Puppet setting, and can go in puppet.conf.)
* --verbose:
Enable verbosity.
* --version:
Print the puppet version number and exit.
* --compile:
Compile a catalogue and output it in JSON from the puppet master. Uses
facts contained in the $vardir/yaml/ directory to compile the catalog.
EXAMPLE
-------
puppet master
DIAGNOSTICS
-----------
When running as a standalone daemon, puppet master accepts the
following signals:
* SIGHUP:
Restart the puppet master server.
* SIGINT and SIGTERM:
Shut down the puppet master server.
* SIGUSR2:
Close file descriptors for log files and reopen them. Used with logrotate.
AUTHOR
------
Luke Kanies
COPYRIGHT
---------
Copyright (c) 2012 Puppet Labs, LLC Licensed under the Apache 2.0 License
HELP
end
# Sets up the 'node_cache_terminus' default to use the Write Only Yaml terminus :write_only_yaml.
# If this is not wanted, the setting ´node_cache_terminus´ should be set to nil.
# @see Puppet::Node::WriteOnlyYaml
# @see #setup_node_cache
# @see puppet issue 16753
#
def app_defaults
super.merge({
:node_cache_terminus => :write_only_yaml,
:facts_terminus => 'yaml'
})
end
def preinit
Signal.trap(:INT) do
$stderr.puts "Canceling startup"
exit(0)
end
# save ARGV to protect us from it being smashed later by something
@argv = ARGV.dup
end
def run_command
if options[:node]
compile
else
main
end
end
def compile
Puppet::Util::Log.newdestination :console
begin
unless catalog = Puppet::Resource::Catalog.indirection.find(options[:node])
raise "Could not compile catalog for #{options[:node]}"
end
puts PSON::pretty_generate(catalog.to_resource, :allow_nan => true, :max_nesting => false)
rescue => detail
- $stderr.puts detail
+ Puppet.log_exception(detail, "Failed to compile catalog for node #{options[:node]}: #{detail}")
exit(30)
end
exit(0)
end
def main
require 'etc'
# Make sure we've got a localhost ssl cert
Puppet::SSL::Host.localhost
# And now configure our server to *only* hit the CA for data, because that's
# all it will have write access to.
Puppet::SSL::Host.ca_location = :only if Puppet::SSL::CertificateAuthority.ca?
if Puppet.features.root?
begin
Puppet::Util.chuser
rescue => detail
Puppet.log_exception(detail, "Could not change user to #{Puppet[:user]}: #{detail}")
exit(39)
end
end
if options[:rack]
start_rack_master
else
start_webrick_master
end
end
def setup_logs
# Handle the logging settings.
if options[:debug] or options[:verbose]
if options[:debug]
Puppet::Util::Log.level = :debug
else
Puppet::Util::Log.level = :info
end
unless Puppet[:daemonize] or options[:rack]
Puppet::Util::Log.newdestination(:console)
options[:setdest] = true
end
end
Puppet::Util::Log.newdestination(:syslog) unless options[:setdest]
end
def setup_terminuses
require 'puppet/file_serving/content'
require 'puppet/file_serving/metadata'
Puppet::FileServing::Content.indirection.terminus_class = :file_server
Puppet::FileServing::Metadata.indirection.terminus_class = :file_server
Puppet::FileBucket::File.indirection.terminus_class = :file
end
def setup_ssl
# Configure all of the SSL stuff.
if Puppet::SSL::CertificateAuthority.ca?
Puppet::SSL::Host.ca_location = :local
Puppet.settings.use :ca
Puppet::SSL::CertificateAuthority.instance
else
Puppet::SSL::Host.ca_location = :none
end
end
# Sets up a special node cache "write only yaml" that collects and stores node data in yaml
# but never finds or reads anything (this since a real cache causes stale data to be served
# in circumstances when the cache can not be cleared).
# @see puppet issue 16753
# @see Puppet::Node::WriteOnlyYaml
# @return [void]
def setup_node_cache
Puppet::Node.indirection.cache_class = Puppet[:node_cache_terminus]
end
def setup
raise Puppet::Error.new("Puppet master is not supported on Microsoft Windows") if Puppet.features.microsoft_windows?
setup_logs
exit(Puppet.settings.print_configs ? 0 : 1) if Puppet.settings.print_configs?
Puppet.settings.use :main, :master, :ssl, :metrics
setup_terminuses
setup_node_cache
setup_ssl
end
private
# Start a master that will be using WeBrick.
#
# This method will block until the master exits.
def start_webrick_master
require 'puppet/network/server'
daemon = Puppet::Daemon.new(Puppet::Util::Pidlock.new(Puppet[:pidfile]))
daemon.argv = @argv
daemon.server = Puppet::Network::Server.new(Puppet[:bindaddress], Puppet[:masterport])
daemon.daemonize if Puppet[:daemonize]
announce_start_of_master
daemon.start
end
# Start a master that will be used for a Rack container.
#
# This method immediately returns the Rack handler that must be returned to
# the calling Rack container
def start_rack_master
require 'puppet/network/http/rack'
announce_start_of_master
return Puppet::Network::HTTP::Rack.new()
end
def announce_start_of_master
Puppet.notice "Starting Puppet master version #{Puppet.version}"
end
end
diff --git a/lib/puppet/application/queue.rb b/lib/puppet/application/queue.rb
index e3820e621..1efca9be2 100644
--- a/lib/puppet/application/queue.rb
+++ b/lib/puppet/application/queue.rb
@@ -1,161 +1,161 @@
require 'puppet/application'
require 'puppet/util'
require 'puppet/daemon'
require 'puppet/util/pidlock'
class Puppet::Application::Queue < Puppet::Application
attr_accessor :daemon
def app_defaults()
super.merge( :pidfile => "$rundir/queue.pid" )
end
def preinit
@argv = ARGV.dup
# Do an initial trap, so that cancels don't get a stack trace.
# This exits with exit code 1
Signal.trap(:INT) do
$stderr.puts "Caught SIGINT; shutting down"
exit(1)
end
# This is a normal shutdown, so code 0
Signal.trap(:TERM) do
$stderr.puts "Caught SIGTERM; shutting down"
exit(0)
end
{
:verbose => false,
:debug => false
}.each do |opt,val|
options[opt] = val
end
end
option("--debug","-d")
option("--verbose","-v")
def help
<<-HELP
-puppet-queue(8) -- Queuing daemon for asynchronous storeconfigs
+puppet-queue(8) -- Deprecated queuing daemon for asynchronous storeconfigs
========
SYNOPSIS
--------
Retrieves serialized storeconfigs records from a queue and processes
-them in order.
+them in order. THIS FEATURE IS DEPRECATED; use PuppetDB instead.
USAGE
-----
puppet queue [-d|--debug] [-v|--verbose]
DESCRIPTION
-----------
This application runs as a daemon and processes storeconfigs data,
retrieving the data from a stomp server message queue and writing it to
-a database.
+a database. It was once necessary as a workaround for the poor performance
+of ActiveRecord-based storeconfigs, but has been supplanted by the PuppetDB
+service, which gives better performance with less complexity.
-For more information, including instructions for properly setting up
-your puppet master and message queue, see the documentation on setting
-up asynchronous storeconfigs at:
-http://projects.puppetlabs.com/projects/1/wiki/Using_Stored_Configuration
+For more information, see the PuppetDB documentation at
+http://docs.puppetlabs.com/puppetdb/latest
OPTIONS
-------
-Note that any configuration parameter that's valid in the configuration
+Note that any setting that's valid in the configuration
file is also a valid long argument. For example, 'server' is a valid
-configuration parameter, so you can specify '--server <servername>' as
+setting, so you can specify '--server <servername>' as
an argument.
See the configuration file documentation at
http://docs.puppetlabs.com/references/stable/configuration.html for the
full list of acceptable parameters. A commented list of all
configuration options can also be generated by running puppet queue with
'--genconfig'.
* --debug:
Enable full debugging.
* --help:
Print this help message
* --verbose:
Turn on verbose reporting.
* --version:
Print the puppet version number and exit.
EXAMPLE
-------
$ puppet queue
AUTHOR
------
Luke Kanies
COPYRIGHT
---------
Copyright (c) 2011 Puppet Labs, LLC Licensed under the Apache 2.0 License
HELP
end
option("--logdest DEST", "-l DEST") do |arg|
handle_logdest_arg(arg)
end
def main
require 'puppet/indirector/catalog/queue' # provides Puppet::Indirector::Queue.subscribe
Puppet.notice "Starting puppet queue #{Puppet.version}"
Puppet::Resource::Catalog::Queue.subscribe do |catalog|
# Once you have a Puppet::Resource::Catalog instance, passing it to save should suffice
# to put it through to the database via its active_record indirector (which is determined
# by the terminus_class = :active_record setting above)
Puppet::Util.benchmark(:notice, "Processing queued catalog for #{catalog.name}") do
begin
Puppet::Resource::Catalog.indirection.save(catalog)
rescue => detail
Puppet.log_exception(detail, "Could not save queued catalog for #{catalog.name}: #{detail}")
end
end
end
Thread.list.each { |thread| thread.join }
end
def setup
Puppet.warning "Puppet queue is deprecated. See http://links.puppetlabs.com/puppet-queue-deprecation"
unless Puppet.features.stomp?
raise ArgumentError, "Could not load the 'stomp' library, which must be present for queueing to work. You must install the required library."
end
setup_logs
exit(Puppet.settings.print_configs ? 0 : 1) if Puppet.settings.print_configs?
require 'puppet/resource/catalog'
Puppet::Resource::Catalog.indirection.terminus_class = :store_configs
daemon = Puppet::Daemon.new(Puppet::Util::Pidlock.new(Puppet[:pidfile]))
daemon.argv = @argv
daemon.daemonize if Puppet[:daemonize]
# We want to make sure that we don't have a cache
# class set up, because if storeconfigs is enabled,
# we'll get a loop of continually caching the catalog
# for storage again.
Puppet::Resource::Catalog.indirection.cache_class = nil
end
end
diff --git a/lib/puppet/application/resource.rb b/lib/puppet/application/resource.rb
index 35f9f155b..c03181671 100644
--- a/lib/puppet/application/resource.rb
+++ b/lib/puppet/application/resource.rb
@@ -1,229 +1,229 @@
require 'puppet/application'
class Puppet::Application::Resource < Puppet::Application
attr_accessor :host, :extra_params
def preinit
@extra_params = []
Facter.loadfacts
end
option("--debug","-d")
option("--verbose","-v")
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
option("--types", "-t") do |arg|
types = []
Puppet::Type.loadall
Puppet::Type.eachtype do |t|
next if t.name == :component
types << t.name.to_s
end
puts types.sort
exit
end
option("--param PARAM", "-p") do |arg|
@extra_params << arg.to_sym
end
def help
<<-'HELP'
puppet-resource(8) -- The resource abstraction layer shell
========
SYNOPSIS
--------
Uses the Puppet RAL to directly interact with the system.
USAGE
-----
puppet resource [-h|--help] [-d|--debug] [-v|--verbose] [-e|--edit]
[-H|--host <host>] [-p|--param <parameter>] [-t|--types] <type>
[<name>] [<attribute>=<value> ...]
DESCRIPTION
-----------
This command provides simple facilities for converting current system
state into Puppet code, along with some ability to modify the current
state using Puppet's RAL.
By default, you must at least provide a type to list, in which case
puppet resource will tell you everything it knows about all resources of
that type. You can optionally specify an instance name, and puppet
resource will only describe that single instance.
If given a type, a name, and a series of <attribute>=<value> pairs,
puppet resource will modify the state of the specified resource.
Alternately, if given a type, a name, and the '--edit' flag, puppet
resource will write its output to a file, open that file in an editor,
and then apply the saved file as a Puppet transaction.
OPTIONS
-------
-Note that any configuration parameter that's valid in the configuration
+Note that any setting that's valid in the configuration
file is also a valid long argument. For example, 'ssldir' is a valid
-configuration parameter, so you can specify '--ssldir <directory>' as an
+setting, so you can specify '--ssldir <directory>' as an
argument.
See the configuration file documentation at
http://docs.puppetlabs.com/references/stable/configuration.html for the
full list of acceptable parameters. A commented list of all
configuration options can also be generated by running puppet with
'--genconfig'.
* --debug:
Enable full debugging.
* --edit:
Write the results of the query to a file, open the file in an editor,
and read the file back in as an executable Puppet manifest.
* --host:
When specified, connect to the resource server on the named host
and retrieve the list of resouces of the type specified.
* --help:
Print this help message.
* --param:
Add more parameters to be outputted from queries.
* --types:
List all available types.
* --verbose:
Print extra information.
EXAMPLE
-------
This example uses `puppet resource` to return a Puppet configuration for
the user `luke`:
$ puppet resource user luke
user { 'luke':
home => '/home/luke',
uid => '100',
ensure => 'present',
comment => 'Luke Kanies,,,',
gid => '1000',
shell => '/bin/bash',
groups => ['sysadmin','audio','video','puppet']
}
AUTHOR
------
Luke Kanies
COPYRIGHT
---------
Copyright (c) 2011 Puppet Labs, LLC Licensed under the Apache 2.0 License
HELP
end
def main
type, name, params = parse_args(command_line.args)
raise "You cannot edit a remote host" if options[:edit] and @host
resources = find_or_save_resources(type, name, params)
text = resources.
map { |resource| resource.prune_parameters(:parameters_to_include => @extra_params).to_manifest }.
join("\n")
options[:edit] ?
handle_editing(text) :
(puts text)
end
def setup
Puppet::Util::Log.newdestination(:console)
set_log_level
end
private
def remote_key(type, name)
Puppet::Resource.indirection.terminus_class = :rest
port = Puppet[:puppetport]
["https://#{@host}:#{port}", "production", "resources", type, name].join('/')
end
def local_key(type, name)
[type, name].join('/')
end
def handle_editing(text)
require 'tempfile'
# Prefer the current directory, which is more likely to be secure
# and, in the case of interactive use, accessible to the user.
tmpfile = Tempfile.new('x2puppet', Dir.pwd)
begin
# sync write, so nothing buffers before we invoke the editor.
tmpfile.sync = true
tmpfile.puts text
# edit the content
system(ENV["EDITOR"] || 'vi', tmpfile.path)
# ...and, now, pass that file to puppet to apply. Because
# many editors rename or replace the original file we need to
# feed the pathname, not the file content itself, to puppet.
system('puppet apply -v ' + tmpfile.path)
ensure
# The temporary file will be safely removed.
tmpfile.close(true)
end
end
def parse_args(args)
type = args.shift or raise "You must specify the type to display"
Puppet::Type.type(type) or raise "Could not find type #{type}"
name = args.shift
params = {}
args.each do |setting|
if setting =~ /^(\w+)=(.+)$/
params[$1] = $2
else
raise "Invalid parameter setting #{setting}"
end
end
[type, name, params]
end
def find_or_save_resources(type, name, params)
key = @host ? remote_key(type, name) : local_key(type, name)
if name
if params.empty?
[ Puppet::Resource.indirection.find( key ) ]
else
resource = Puppet::Resource.new( type, name, :parameters => params )
# save returns [resource that was saved, transaction log from applying the resource]
save_result = Puppet::Resource.indirection.save(resource, key)
[ save_result.first ]
end
else
if type == "file"
raise "Listing all file instances is not supported. Please specify a file or directory, e.g. puppet resource file /etc"
end
Puppet::Resource.indirection.search( key, {} )
end
end
end
diff --git a/lib/puppet/bindings.rb b/lib/puppet/bindings.rb
index b6a8a3afa..a30cfe2b3 100644
--- a/lib/puppet/bindings.rb
+++ b/lib/puppet/bindings.rb
@@ -1,147 +1,147 @@
# This class allows registration of named bindings that are later contributed to a layer via
# a binding scheme.
#
# The intended use is for a .rb file to be placed in confdir's or module's `lib/bindings` directory structure, with a
# name corresponding to the symbolic bindings name.
#
# Here are two equivalent examples, the first using chained methods (which is compact for simple cases), and the
# second which uses a block.
#
# @example MyModule's lib/bindings/mymodule/default.rb
# Puppet::Bindings.newbindings('mymodule::default') do
# bind.integer.named('meaning of life').to(42)
# end
#
# @example Using blocks
# Puppet::Bindings.newbindings('mymodule::default') do
# bind do
# integer
# name 'meaning of life'
# to 42
# end
# end
#
# If access is needed to the scope, this can be declared as a block parameter.
# @example MyModule's lib/bindings/mymodule/default.rb with scope
# Puppet::Bindings.newbindings('mymodule::default') do |scope|
# bind.integer.named('meaning of life').to("#{scope['::fqdn']} also think it is 42")
# end
#
# If late evaluation is wanted, this can be achieved by binding a puppet expression.
# @example binding a puppet expression
# Puppet::Bindings.newbindings('mymodule::default') do |scope|
# bind.integer.named('meaning of life').to(puppet_string("${::fqdn} also think it is 42")
# end
#
# It is allowed to define methods in the block given to `newbindings`, these can be used when
# producing bindings. (Care should naturally be taken to not override any of the already defined methods).
# @example defining method to be used while creating bindings
# Puppet::Bindings.newbindings('mymodule::default') do
# def square(x)
# x * x
# end
# bind.integer.named('meaning of life squared').to(square(42))
# end
#
# For all details see {Puppet::Pops::Binder::BindingsFactory}, which is used behind the scenes.
# @api public
#
class Puppet::Bindings
extend Enumerable
Environment = Puppet::Node::Environment
# Constructs and registers a {Puppet::Pops::Binder::Bindings::NamedBindings NamedBindings} that later can be contributed
# to a bindings layer in a bindings configuration via a URI. The name is symbolic, fully qualified with module name, and at least one
# more qualifying name (where the name `default` is used in the default bindings configuration.
#
# The given block is called with a `self` bound to an instance of {Puppet::Pops::Binder::BindingsFactory::BindingsContainerBuilder}
# which most notably has a `#bind` method which it turn calls a block bound to an instance of
# {Puppet::Pops::Binder::BindingsFactory::BindingsBuilder}.
# Depending on the use-case a direct chaining method calls or nested blocks may be used.
#
# @example simple bindings
# Puppet::Bindings.newbindings('mymodule::default') do
# bind.name('meaning of life').to(42)
# bind.integer.named('port').to(8080)
# bind.integer.named('apache::port').to(8080)
# end
#
# The block form is more suitable for longer, more complex forms of bindings.
#
def self.newbindings(name, &block)
register_proc(name, block)
end
def self.register_proc(name, block)
- adapter = NamedBindingsAdapter.adapt(Environment.current)
+ adapter = NamedBindingsAdapter.adapt(Puppet.lookup(:current_environment))
adapter[name] = block
end
# Registers a named_binding under its name
# @param named_bindings [Puppet::Pops::Binder::Bindings::NamedBindings] The named bindings to register.
# @api public
#
def self.register(named_bindings)
- adapter = NamedBindingsAdapter.adapt(Environment.current)
+ adapter = NamedBindingsAdapter.adapt(Puppet.lookup(:current_environment))
adapter[named_bindings.name] = named_bindings
end
def self.resolve(scope, name)
entry = get(name)
return entry unless entry.is_a?(Proc)
named_bindings = Puppet::Pops::Binder::BindingsFactory.safe_named_bindings(name, scope, &entry).model
- adapter = NamedBindingsAdapter.adapt(Environment.current)
+ adapter = NamedBindingsAdapter.adapt(Puppet.lookup(:current_environment))
adapter[named_bindings.name] = named_bindings
named_bindings
end
# Returns the named bindings with the given name, or nil if no such bindings have been registered.
# @param name [String] The fully qualified name of a binding to get
# @return [Proc, Puppet::Pops::Binder::Bindings::NamedBindings] a Proc producing named bindings, or a named bindings directly
# @api public
#
def self.get(name)
- adapter = NamedBindingsAdapter.adapt(Environment.current)
+ adapter = NamedBindingsAdapter.adapt(Puppet.lookup(:current_environment))
adapter[name]
end
def self.[](name)
get(name)
end
# Supports Enumerable iteration (k,v) over the named bindings hash.
def self.each
- adapter = NamedBindingsAdapter.adapt(Environment.current)
+ adapter = NamedBindingsAdapter.adapt(Puppet.lookup(:current_environment))
adapter.each_pair {|k,v| yield k,v }
end
# A NamedBindingsAdapter holds a map of name to Puppet::Pops::Binder::Bindings::NamedBindings.
# It is intended to be used as an association between an Environment and named bindings.
#
class NamedBindingsAdapter < Puppet::Pops::Adaptable::Adapter
def initialize()
@named_bindings = {}
end
def [](name)
@named_bindings[name]
end
def has_name?(name)
@named_bindings.has_key?
end
def []=(name, value)
unless value.is_a?(Puppet::Pops::Binder::Bindings::NamedBindings) || value.is_a?(Proc)
raise ArgumentError, "Given value must be a NamedBindings, or a Proc producing one, got: #{value.class}."
end
@named_bindings[name] = value
end
def each_pair(&block)
@named_bindings.each_pair(&block)
end
end
-end
\ No newline at end of file
+end
diff --git a/lib/puppet/configurer.rb b/lib/puppet/configurer.rb
index 0c000c9ea..68252268f 100644
--- a/lib/puppet/configurer.rb
+++ b/lib/puppet/configurer.rb
@@ -1,265 +1,268 @@
# The client for interacting with the puppetmaster config server.
require 'sync'
require 'timeout'
require 'puppet/network/http_pool'
require 'puppet/util'
require 'securerandom'
class Puppet::Configurer
require 'puppet/configurer/fact_handler'
require 'puppet/configurer/plugin_handler'
include Puppet::Configurer::FactHandler
include Puppet::Configurer::PluginHandler
# For benchmarking
include Puppet::Util
attr_reader :compile_time, :environment
# Provide more helpful strings to the logging that the Agent does
def self.to_s
"Puppet configuration client"
end
class << self
# Puppet agent should only have one instance running, and we need a
# way to retrieve it.
attr_accessor :instance
include Puppet::Util
end
def execute_postrun_command
execute_from_setting(:postrun_command)
end
def execute_prerun_command
execute_from_setting(:prerun_command)
end
# Initialize and load storage
def init_storage
Puppet::Util::Storage.load
@compile_time ||= Puppet::Util::Storage.cache(:configuration)[:compile_time]
rescue => detail
Puppet.log_exception(detail, "Removing corrupt state file #{Puppet[:statefile]}: #{detail}")
begin
- Puppet::FileSystem::File.unlink(Puppet[:statefile])
+ Puppet::FileSystem.unlink(Puppet[:statefile])
retry
rescue => detail
- raise Puppet::Error.new("Cannot remove #{Puppet[:statefile]}: #{detail}")
+ raise Puppet::Error.new("Cannot remove #{Puppet[:statefile]}: #{detail}", detail)
end
end
# Just so we can specify that we are "the" instance.
def initialize
Puppet.settings.use(:main, :ssl, :agent)
self.class.instance = self
@running = false
@splayed = false
@environment = Puppet[:environment]
@transaction_uuid = SecureRandom.uuid
end
# Get the remote catalog, yo. Returns nil if no catalog can be found.
def retrieve_catalog(query_options)
query_options ||= {}
# First try it with no cache, then with the cache.
unless (Puppet[:use_cached_catalog] and result = retrieve_catalog_from_cache(query_options)) or result = retrieve_new_catalog(query_options)
if ! Puppet[:usecacheonfailure]
Puppet.warning "Not using cache on failed catalog"
return nil
end
result = retrieve_catalog_from_cache(query_options)
end
return nil unless result
convert_catalog(result, @duration)
end
# Convert a plain resource catalog into our full host catalog.
def convert_catalog(result, duration)
catalog = result.to_ral
catalog.finalize
catalog.retrieval_duration = duration
catalog.write_class_file
catalog.write_resource_file
catalog
end
def get_facts(options)
download_plugins if options[:pluginsync]
if Puppet::Resource::Catalog.indirection.terminus_class == :rest
# This is a bit complicated. We need the serialized and escaped facts,
# and we need to know which format they're encoded in. Thus, we
# get a hash with both of these pieces of information.
#
# facts_for_uploading may set Puppet[:node_name_value] as a side effect
return facts_for_uploading
end
end
def prepare_and_retrieve_catalog(options, query_options)
# set report host name now that we have the fact
options[:report].host = Puppet[:node_name_value]
unless catalog = (options.delete(:catalog) || retrieve_catalog(query_options))
Puppet.err "Could not retrieve catalog; skipping run"
return
end
catalog
end
# Retrieve (optionally) and apply a catalog. If a catalog is passed in
# the options, then apply that one, otherwise retrieve it.
def apply_catalog(catalog, options)
report = options[:report]
report.configuration_version = catalog.version
report.transaction_uuid = @transaction_uuid
report.environment = @environment
benchmark(:notice, "Finished catalog run") do
catalog.apply(options)
end
report.finalize_report
report
end
def get_transaction_uuid
{ :transaction_uuid => @transaction_uuid }
end
# The code that actually runs the catalog.
# This just passes any options on to the catalog,
# which accepts :tags and :ignoreschedules.
def run(options = {})
options[:report] ||= Puppet::Transaction::Report.new("apply")
report = options[:report]
init_storage
Puppet::Util::Log.newdestination(report)
begin
unless Puppet[:node_name_fact].empty?
query_options = get_facts(options)
end
- begin
- if node = Puppet::Node.indirection.find(Puppet[:node_name_value],
- :environment => @environment, :ignore_cache => true)
- if node.environment.to_s != @environment
- Puppet.warning "Local environment: \"#{@environment}\" doesn't match server specified node environment \"#{node.environment}\", switching agent to \"#{node.environment}\"."
- @environment = node.environment.to_s
- query_options = nil
+ # We only need to find out the environment to run in if we don't already have a catalog
+ unless options[:catalog]
+ begin
+ if node = Puppet::Node.indirection.find(Puppet[:node_name_value],
+ :environment => @environment, :ignore_cache => true)
+ if node.environment.to_s != @environment
+ Puppet.warning "Local environment: \"#{@environment}\" doesn't match server specified node environment \"#{node.environment}\", switching agent to \"#{node.environment}\"."
+ @environment = node.environment.to_s
+ query_options = nil
+ end
end
+ 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
- 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
query_options = get_facts(options) unless query_options
# add the transaction uuid to the catalog query options hash
query_options.merge! get_transaction_uuid if query_options
unless catalog = prepare_and_retrieve_catalog(options, query_options)
return nil
end
# Here we set the local environment based on what we get from the
# catalog. Since a change in environment means a change in facts, and
# facts may be used to determine which catalog we get, we need to
# rerun the process if the environment is changed.
tries = 0
while catalog.environment and not catalog.environment.empty? and catalog.environment != @environment
if tries > 3
raise Puppet::Error, "Catalog environment didn't stabilize after #{tries} fetches, aborting run"
end
Puppet.warning "Local environment: \"#{@environment}\" doesn't match server specified environment \"#{catalog.environment}\", restarting agent run with environment \"#{catalog.environment}\""
@environment = catalog.environment
return nil unless catalog = prepare_and_retrieve_catalog(options, query_options)
tries += 1
end
execute_prerun_command or return nil
apply_catalog(catalog, options)
report.exit_status
rescue => detail
Puppet.log_exception(detail, "Failed to apply catalog: #{detail}")
return nil
ensure
execute_postrun_command or return nil
end
ensure
# 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.
$env_module_directories = nil
Puppet::Util::Log.close(report)
send_report(report)
end
def send_report(report)
puts report.summary if Puppet[:summarize]
save_last_run_summary(report)
Puppet::Transaction::Report.indirection.save(report, nil, :environment => @environment) if Puppet[:report]
rescue => detail
Puppet.log_exception(detail, "Could not send report: #{detail}")
end
def save_last_run_summary(report)
mode = Puppet.settings.setting(:lastrunfile).mode
Puppet::Util.replace_file(Puppet[:lastrunfile], mode) do |fh|
fh.print YAML.dump(report.raw_summary)
end
rescue => detail
Puppet.log_exception(detail, "Could not save last run local report: #{detail}")
end
private
def execute_from_setting(setting)
return true if (command = Puppet[setting]) == ""
begin
Puppet::Util::Execution.execute([command])
true
rescue => detail
Puppet.log_exception(detail, "Could not run command from #{setting}: #{detail}")
false
end
end
def retrieve_catalog_from_cache(query_options)
result = nil
@duration = thinmark do
result = Puppet::Resource::Catalog.indirection.find(Puppet[:node_name_value], query_options.merge(:ignore_terminus => true, :environment => @environment))
end
Puppet.notice "Using cached catalog"
result
rescue => detail
Puppet.log_exception(detail, "Could not retrieve catalog from cache: #{detail}")
return nil
end
def retrieve_new_catalog(query_options)
result = nil
@duration = thinmark do
result = Puppet::Resource::Catalog.indirection.find(Puppet[:node_name_value], query_options.merge(:ignore_cache => true, :environment => @environment))
end
result
rescue SystemExit,NoMemoryError
raise
rescue Exception => detail
Puppet.log_exception(detail, "Could not retrieve catalog from remote server: #{detail}")
return nil
end
end
diff --git a/lib/puppet/configurer/fact_handler.rb b/lib/puppet/configurer/fact_handler.rb
index bb76146cc..5d20a5b91 100644
--- a/lib/puppet/configurer/fact_handler.rb
+++ b/lib/puppet/configurer/fact_handler.rb
@@ -1,37 +1,37 @@
require 'puppet/indirector/facts/facter'
require 'puppet/configurer'
require 'puppet/configurer/downloader'
# Break out the code related to facts. This module is
# just included into the agent, but having it here makes it
# easier to test.
module Puppet::Configurer::FactHandler
def find_facts
# This works because puppet agent configures Facts to use 'facter' for
# finding facts and the 'rest' terminus for caching them. Thus, we'll
# compile them and then "cache" them on the server.
begin
facts = Puppet::Node::Facts.indirection.find(Puppet[:node_name_value], :environment => @environment)
unless Puppet[:node_name_fact].empty?
Puppet[:node_name_value] = facts.values[Puppet[:node_name_fact]]
facts.name = Puppet[:node_name_value]
end
facts
rescue SystemExit,NoMemoryError
raise
rescue Exception => detail
message = "Could not retrieve local facts: #{detail}"
Puppet.log_exception(detail, message)
- raise Puppet::Error, message
+ raise Puppet::Error, message, detail.backtrace
end
end
def facts_for_uploading
facts = find_facts
text = facts.render(:pson)
{:facts_format => :pson, :facts => CGI.escape(text)}
end
end
diff --git a/lib/puppet/confine/any.rb b/lib/puppet/confine/any.rb
new file mode 100644
index 000000000..392181287
--- /dev/null
+++ b/lib/puppet/confine/any.rb
@@ -0,0 +1,26 @@
+class Puppet::Confine::Any < Puppet::Confine
+ def self.summarize(confines)
+ confines.inject(0) { |count, confine| count + confine.summary }
+ end
+
+ def pass?(value)
+ !! value
+ end
+
+ def message(value)
+ "0 confines (of #{value.length}) were true"
+ end
+
+ def summary
+ result.find_all { |v| v == true }.length
+ end
+
+ def valid?
+ if @values.any? { |value| pass?(value) }
+ true
+ else
+ Puppet.debug("#{label}: #{message(@values)}")
+ false
+ end
+ end
+end
diff --git a/lib/puppet/confine/exists.rb b/lib/puppet/confine/exists.rb
index 95a315514..6e29022fb 100644
--- a/lib/puppet/confine/exists.rb
+++ b/lib/puppet/confine/exists.rb
@@ -1,19 +1,19 @@
require 'puppet/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) : Puppet::FileSystem::File.exist?(value))
+ value && (for_binary? ? which(value) : Puppet::FileSystem.exist?(value))
end
def message(value)
"file #{value} does not exist"
end
def summary
result.zip(values).inject([]) { |array, args| val, f = args; array << f unless val; array }
end
end
diff --git a/lib/puppet/confiner.rb b/lib/puppet/confiner.rb
index 57176e799..50282fd97 100644
--- a/lib/puppet/confiner.rb
+++ b/lib/puppet/confiner.rb
@@ -1,45 +1,46 @@
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::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.
#
# * _fact_name_ => value of fact (or array of facts)
# * `:exists` => the path to an existing file
# * `:true` => a predicate code block returning true
# * `:false` => a predicate code block returning false
# * `:feature` => name of system feature that must be present
+ # * `:any` => an array of expressions that will be ORed together
#
# @example
# confine :operatingsystem => [:redhat, :fedora]
# confine :true { ... }
#
# @param hash [Hash<{Symbol => Object}>] hash of confines
# @return [void]
# @api public
#
def confine(hash)
confine_collection.confine(hash)
end
# @return [Puppet::ConfineCollection] the collection of confines
# @api private
#
def confine_collection
@confine_collection ||= Puppet::ConfineCollection.new(self.to_s)
end
# Checks whether this implementation is suitable for the current platform (or returns a summary
# of all confines if short == false).
# @return [Boolean. Hash] Returns whether the confines are all valid (if short == true), or a hash of all confines
# if short == false.
# @api public
#
def suitable?(short = true)
return(short ? confine_collection.valid? : confine_collection.summary)
end
end
diff --git a/lib/puppet/context.rb b/lib/puppet/context.rb
new file mode 100644
index 000000000..369027599
--- /dev/null
+++ b/lib/puppet/context.rb
@@ -0,0 +1,55 @@
+# Puppet::Context is a system for tracking services and contextual information
+# that puppet needs to be able to run. Values are "bound" in a context when it is created
+# and cannot be changed; however a child context can be created, using
+# {#override}, that provides a different value.
+#
+# @api private
+class Puppet::Context
+ require 'puppet/context/trusted_information'
+
+ class UndefinedBindingError < Puppet::Error; end
+ class StackUnderflow < Puppet::Error; end
+
+ # @api private
+ def initialize(initial_bindings)
+ @stack = []
+ @table = initial_bindings
+ @description = "root"
+ end
+
+ # @api private
+ def push(overrides, description = "")
+ @stack.push([@table, @description])
+ @table = @table.merge(overrides || {})
+ @description = description
+ end
+
+ # @api private
+ def pop
+ if @stack.empty?
+ raise(StackUnderflow, "Attempted to pop, but already at root of the context stack.")
+ else
+ (@table, @description) = @stack.pop
+ end
+ end
+
+ # @api private
+ def lookup(name, &block)
+ if @table.include?(name)
+ @table[name]
+ elsif block
+ block.call
+ else
+ raise UndefinedBindingError, "no '#{name}' in #{@table.inspect} at top of #{@stack.inspect}"
+ end
+ end
+
+ # @api private
+ def override(bindings, description = "", &block)
+ push(bindings, description)
+
+ yield
+ ensure
+ pop
+ end
+end
diff --git a/lib/puppet/context/trusted_information.rb b/lib/puppet/context/trusted_information.rb
new file mode 100644
index 000000000..35d3a7716
--- /dev/null
+++ b/lib/puppet/context/trusted_information.rb
@@ -0,0 +1,56 @@
+# @api private
+class Puppet::Context::TrustedInformation
+ # one of 'remote', 'local', or false, where 'remote' is authenticated via cert,
+ # 'local' is trusted by virtue of running on the same machine (not a remote
+ # request), and false is an unauthenticated remote request.
+ #
+ # @return [String, Boolean]
+ attr_reader :authenticated
+
+ # The validated certificate name used for the request
+ #
+ # @return [String]
+ attr_reader :certname
+
+ # Extra information that comes from the trusted certificate's extensions.
+ #
+ # @return [Hash{Object => Object}]
+ attr_reader :extensions
+
+ def initialize(authenticated, certname, extensions)
+ @authenticated = authenticated.freeze
+ @certname = certname.freeze
+ @extensions = extensions.freeze
+ end
+
+ def self.remote(authenticated, node_name, certificate)
+ if authenticated
+ extensions = {}
+ if certificate.nil?
+ Puppet.info('TrustedInformation expected a certificate, but none was given.')
+ else
+ extensions = Hash[certificate.custom_extensions.collect do |ext|
+ [ext['oid'].freeze, ext['value'].freeze]
+ end]
+ end
+ new('remote', node_name, extensions)
+ else
+ new(false, nil, {})
+ end
+ end
+
+ def self.local(node)
+ # Always trust local data by picking up the available parameters.
+ client_cert = node ? node.parameters['clientcert'] : nil
+
+ new('local', client_cert, {})
+ end
+
+ def to_h
+ {
+ 'authenticated'.freeze => authenticated,
+ 'certname'.freeze => certname,
+ 'extensions'.freeze => extensions
+ }.freeze
+ end
+end
diff --git a/lib/puppet/defaults.rb b/lib/puppet/defaults.rb
index 58320554e..d45d21a03 100644
--- a/lib/puppet/defaults.rb
+++ b/lib/puppet/defaults.rb
@@ -1,1804 +1,1875 @@
module Puppet
def self.default_diffargs
if (Facter.value(:kernel) == "AIX" && Facter.value(:kernelmajversion) == "5300")
""
else
"-u"
end
end
############################################################################################
# NOTE: For information about the available values for the ":type" property of settings,
# see the docs for Settings.define_settings
############################################################################################
AS_DURATION = %q{This setting can be a time interval in seconds (30 or 30s), minutes (30m), hours (6h), days (2d), or years (5y).}
STORECONFIGS_ONLY = %q{This setting is only used by the ActiveRecord storeconfigs and inventory backends, which are deprecated.}
define_settings(:main,
: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 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,
: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
default is essentially $0 without the path or `.rb`.",
}
)
define_settings(:main,
:logdir => {
:default => nil,
:type => :directory,
:mode => 0750,
:owner => "service",
:group => "service",
:desc => "The directory in which to store log files",
}
)
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,
:desc => "Whether to print stack traces on some errors",
},
:profile => {
:default => false,
:type => :boolean,
:desc => "Whether to enable experimental performance profiling",
},
:autoflush => {
:default => true,
:type => :boolean,
:desc => "Whether log files should always flush to disk.",
:hook => proc { |value| Log.autoflush = value }
},
:syslogfacility => {
:default => "daemon",
: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",
:type => :directory,
:mode => 01755,
:desc => "The directory where Puppet state is stored. Generally,
this directory can be removed without causing harm (although it
might result in spurious service restarts)."
},
:rundir => {
:default => nil,
:type => :directory,
:mode => 0755,
:owner => "service",
:group => "service",
:desc => "Where Puppet PID files are kept."
},
:genconfig => {
:default => false,
:type => :boolean,
: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
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
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
`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,
:type => :boolean,
:desc => "Whether to create the necessary user and group that puppet agent will run as.",
},
:manage_internal_file_permissions => {
:default => true,
:type => :boolean,
:desc => "Whether Puppet should manage the owner, group, and mode of files it uses internally",
},
:onetime => {
:default => false,
:type => :boolean,
: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
from the parent process.",
:call_hook => :on_define_and_write,
:hook => proc do |value|
ENV["PATH"] = "" if ENV["PATH"].nil?
ENV["PATH"] = value unless value == "none"
paths = ENV["PATH"].split(File::PATH_SEPARATOR)
Puppet::Util::Platform.default_paths.each do |path|
ENV["PATH"] += File::PATH_SEPARATOR + path unless paths.include?(path)
end
value
end
},
:libdir => {
:type => :directory,
:default => "$vardir/lib",
: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)
@oldlibdir = value
$LOAD_PATH << value
end
},
:ignoreimport => {
:default => false,
:type => :boolean,
: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
(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."
},
+ :environmentpath => {
+ :default => "$confdir/environments",
+ :desc => "A path of environment directories",
+ :type => :path,
+ },
:diff_args => {
:default => default_diffargs,
: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
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 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).",
:short => "D",
:hook => proc do |value|
if value and Puppet.features.microsoft_windows?
raise "Cannot daemonize on Windows"
end
end
},
:maximum_uid => {
:default => 4294967290,
: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",
:desc => "The YAML file containing indirector route configuration.",
},
:node_terminus => {
:type => :terminus,
:default => "plain",
:desc => "Where to find information about nodes.",
},
:node_cache_terminus => {
:type => :terminus,
:default => nil,
:desc => "How to store cached nodes.
- Valid values are (none), 'json', 'yaml' or write only yaml ('write_only_yaml').
+ Valid values are (none), 'json', 'msgpack', 'yaml' or write only yaml ('write_only_yaml').
The master application defaults to 'write_only_yaml', all others to none.",
},
:data_binding_terminus => {
:type => :terminus,
:default => "hiera",
:desc => "Where to retrive information about data.",
},
:hiera_config => {
:default => "$confdir/hiera.yaml",
:desc => "The hiera configuration file. Puppet only reads this file on startup, so you must restart the puppet master every time you edit it.",
:type => :file,
},
:binder => {
:default => false,
- :desc => "Turns the binding system on or off. This includes hiera-2 and data in modules. The binding system aggregates data from
- modules and other locations and makes them available for lookup. The binding system is experimental and any or all of it may change.",
+ :desc => "Turns the binding system on or off. This includes bindings in modules.
+ The binding system aggregates data from modules and other locations and makes them available for lookup.
+ The binding system is experimental and any or all of it may change.",
:type => :boolean,
},
:binder_config => {
:default => nil,
:desc => "The binder configuration file. Puppet reads this file on each request to configure the bindings system.
If set to nil (the default), a $confdir/binder_config.yaml is optionally loaded. If it does not exists, a default configuration
is used. If the setting :binding_config is specified, it must reference a valid and existing yaml file.",
:type => :file,
},
:catalog_terminus => {
:type => :terminus,
:default => "compiler",
:desc => "Where to get node catalogs. This is useful to change if, for instance,
you'd like to pre-compile catalogs and store them in memcached or some other easily-accessed store.",
},
:catalog_cache_terminus => {
:type => :terminus,
:default => nil,
- :desc => "How to store cached catalogs. Valid values are 'json' and 'yaml'. The agent application defaults to 'json'."
+ :desc => "How to store cached catalogs. Valid values are 'json', 'msgpack' and 'yaml'. The agent application defaults to 'json'."
},
:facts_terminus => {
:default => 'facter',
:desc => "The node facts terminus.",
:call_hook => :on_initialize_and_write,
:hook => proc do |value|
require 'puppet/node/facts'
# Cache to YAML if we're uploading facts away
if %w[rest inventory_service].include? value.to_s
Puppet.info "configuring the YAML fact cache because a remote terminus is active"
Puppet::Node::Facts.indirection.cache_class = :yaml
end
end
},
:inventory_terminus => {
:type => :terminus,
:default => "$facts_terminus",
:desc => "Should usually be the same as the facts terminus",
},
:default_file_terminus => {
:type => :terminus,
:default => "rest",
:desc => "The default source for files if no server is given in a
uri, e.g. puppet:///file. The default of `rest` causes the file to be
retrieved using the `server` setting. When running `apply` the default
is `file_server`, causing requests to be filled locally."
},
:httplog => {
:default => "$logdir/http.log",
:type => :file,
:owner => "root",
:mode => 0640,
:desc => "Where the puppet agent web server logs.",
},
:http_proxy_host => {
:default => "none",
:desc => "The HTTP proxy host to use for outgoing connections. Note: You
may need to use a FQDN for the server hostname when using a proxy.",
},
:http_proxy_port => {
:default => 3128,
:desc => "The HTTP proxy port to use for outgoing connections",
},
:filetimeout => {
:default => "15s",
:type => :duration,
:desc => "The minimum time to wait between checking for updates in
configuration files. This timeout determines how quickly Puppet checks whether
a file (such as manifests or templates) has changed on disk. #{AS_DURATION}",
},
:queue_type => {
:default => "stomp",
:desc => "Which type of queue to use for asynchronous processing.",
},
:queue_type => {
:default => "stomp",
:desc => "Which type of queue to use for asynchronous processing.",
},
:queue_source => {
:default => "stomp://localhost:61613/",
:desc => "Which type of queue to use for asynchronous processing. If your stomp server requires
authentication, you can include it in the URI as long as your stomp client library is at least 1.1.1",
},
:async_storeconfigs => {
:default => false,
:type => :boolean,
:desc => "Whether to use a queueing system to provide asynchronous database integration.
Requires that `puppet queue` be running.",
:hook => proc do |value|
if value
# This reconfigures the termini for Node, Facts, and Catalog
- Puppet.settings[:storeconfigs] = true
+ Puppet.settings.override_default(:storeconfigs, true)
# But then we modify the configuration
Puppet::Resource::Catalog.indirection.cache_class = :queue
- Puppet.settings[:catalog_cache_terminus] = :queue
+ Puppet.settings.override_default(:catalog_cache_terminus, :queue)
else
raise "Cannot disable asynchronous storeconfigs in a running process"
end
end
},
:thin_storeconfigs => {
:default => false,
:type => :boolean,
:desc =>
"Boolean; whether Puppet should store only facts and exported resources in the storeconfigs
database. This will improve the performance of exported resources with the older
`active_record` backend, but will disable external tools that search the storeconfigs database.
Thinning catalogs is generally unnecessary when using PuppetDB to store catalogs.",
:hook => proc do |value|
- Puppet.settings[:storeconfigs] = true if value
+ Puppet.settings.override_default(:storeconfigs, true) if value
end
},
:config_version => {
:default => "",
:desc => "How to determine the configuration version. By default, it will be the
time that the configuration is parsed, but you can provide a shell script to override how the
version is determined. The output of this script will be added to every log message in the
reports, allowing you to correlate changes on your hosts to the source version on the server.",
},
:zlib => {
:default => true,
:type => :boolean,
:desc => "Boolean; whether to use the zlib library",
},
:prerun_command => {
:default => "",
:desc => "A command to run before every agent run. If this command returns a non-zero
return code, the entire Puppet run will fail.",
},
:postrun_command => {
:default => "",
:desc => "A command to run after every agent run. If this command returns a non-zero
return code, the entire Puppet run will be considered to have failed, even though it might have
performed work during the normal run.",
},
:freeze_main => {
:default => false,
:type => :boolean,
: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.",
},
: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.",
+ },
+ :immutable_node_data => {
+ :default => '$trusted_node_data',
+ :type => :boolean,
+ :desc => "When true, also prevents $trusted and $facts from being overridden in any scope",
}
)
Puppet.define_settings(:module_tool,
:module_repository => {
:default => 'https://forge.puppetlabs.com',
:desc => "The module repository",
},
:module_working_dir => {
:default => '$vardir/puppet-module',
:desc => "The directory into which module tool data is stored",
},
:module_skeleton_dir => {
:default => '$module_working_dir/skeleton',
:desc => "The directory which the skeleton for module tool generate is stored.",
}
)
Puppet.define_settings(
:main,
# We have to downcase the fqdn, because the current ssl stuff (as oppsed to in master) doesn't have good facilities for
# manipulating naming.
:certname => {
:default => Puppet::Settings.default_certname.downcase, :desc => "The name to use when handling certificates. Defaults
to the fully qualified domain name.",
:call_hook => :on_define_and_write, # Call our hook with the default value, so we're always downcased
:hook => proc { |value| raise(ArgumentError, "Certificate names must be lower case; see #1168") unless value == value.downcase }},
:certdnsnames => {
:default => '',
:hook => proc do |value|
unless value.nil? or value == '' then
Puppet.warning <<WARN
The `certdnsnames` setting is no longer functional,
after CVE-2011-3872. We ignore the value completely.
For your own certificate request you can set `dns_alt_names` in the
configuration and it will apply locally. There is no configuration option to
set DNS alt names, or any other `subjectAltName` value, for another nodes
certificate.
Alternately you can use the `--dns_alt_names` command line option to set the
labels added while generating your own CSR.
WARN
end
end,
:desc => <<EOT
The `certdnsnames` setting is no longer functional,
after CVE-2011-3872. We ignore the value completely.
For your own certificate request you can set `dns_alt_names` in the
configuration and it will apply locally. There is no configuration option to
set DNS alt names, or any other `subjectAltName` value, for another nodes
certificate.
Alternately you can use the `--dns_alt_names` command line option to set the
labels added while generating your own CSR.
EOT
},
:dns_alt_names => {
:default => '',
:desc => <<EOT,
The comma-separated list of alternative DNS names to use for the local host.
When the node generates a CSR for itself, these are added to the request
as the desired `subjectAltName` in the certificate: additional DNS labels
that the certificate is also valid answering as.
This is generally required if you use a non-hostname `certname`, or if you
want to use `puppet kick` or `puppet resource -H` and the primary certname
does not match the DNS name you use to communicate with the host.
This is unnecessary for agents, unless you intend to use them as a server for
`puppet kick` or remote `puppet resource` management.
It is rarely necessary for servers; it is usually helpful only if you need to
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,
+ :mode => 0755,
:owner => "service",
+ :group => "service",
:desc => "The certificate directory."
},
:ssldir => {
:default => "$confdir/ssl",
:type => :directory,
:mode => 0771,
:owner => "service",
+ :group => "service",
:desc => "Where SSL certificates are kept."
},
:publickeydir => {
:default => "$ssldir/public_keys",
:type => :directory,
+ :mode => 0755,
:owner => "service",
+ :group => "service",
:desc => "The public key directory."
},
:requestdir => {
:default => "$ssldir/certificate_requests",
:type => :directory,
+ :mode => 0755,
:owner => "service",
+ :group => "service",
:desc => "Where host certificate requests are stored."
},
:privatekeydir => {
:default => "$ssldir/private_keys",
:type => :directory,
:mode => 0750,
:owner => "service",
+ :group => "service",
:desc => "The private key directory."
},
:privatedir => {
:default => "$ssldir/private",
:type => :directory,
:mode => 0750,
:owner => "service",
+ :group => "service",
:desc => "Where the client stores private certificate information."
},
:passfile => {
:default => "$privatedir/password",
:type => :file,
:mode => 0640,
:owner => "service",
+ :group => "service",
:desc => "Where puppet agent stores the password for its private key.
Generally unused."
},
:hostcsr => {
:default => "$ssldir/csr_$certname.pem",
:type => :file,
:mode => 0644,
:owner => "service",
+ :group => "service",
:desc => "Where individual hosts store and look for their certificate requests."
},
:hostcert => {
:default => "$certdir/$certname.pem",
:type => :file,
:mode => 0644,
:owner => "service",
+ :group => "service",
:desc => "Where individual hosts store and look for their certificates."
},
:hostprivkey => {
:default => "$privatekeydir/$certname.pem",
:type => :file,
- :mode => 0600,
+ :mode => 0640,
:owner => "service",
+ :group => "service",
:desc => "Where individual hosts store and look for their private key."
},
:hostpubkey => {
:default => "$publickeydir/$certname.pem",
:type => :file,
:mode => 0644,
:owner => "service",
+ :group => "service",
:desc => "Where individual hosts store and look for their public key."
},
:localcacert => {
:default => "$certdir/ca.pem",
:type => :file,
:mode => 0644,
:owner => "service",
+ :group => "service",
:desc => "Where each client stores the CA certificate."
},
:ssl_client_ca_auth => {
:type => :file,
:mode => 0644,
:owner => "service",
+ :group => "service",
:desc => "Certificate authorities who issue server certificates. SSL servers will not be
- considered authentic unless they posses a certificate issued by an authority
+ considered authentic unless they possess 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",
+ :group => "service",
:desc => "Certificate authorities who issue client certificates. SSL clients will not be
- considered authentic unless they posses a certificate issued by an authority
+ considered authentic unless they possess 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",
:type => :file,
:mode => 0644,
:owner => "service",
+ :group => "service",
:desc => "Where the host's certificate revocation list can be found.
This is distinct from the certificate authority's CRL."
},
: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.",
},
:certificate_expire_warning => {
:default => "60d",
:type => :duration,
:desc => "The window of time leading up to a certificate's expiration that a notification
will be logged. This applies to CA, master, and agent certificates. #{AS_DURATION}"
}
)
define_settings(
:ca,
:ca_name => {
:default => "Puppet CA: $certname",
:desc => "The name to use the Certificate Authority certificate.",
},
:cadir => {
:default => "$ssldir/ca",
:type => :directory,
:owner => "service",
:group => "service",
- :mode => 0770,
+ :mode => 0755,
:desc => "The root directory for the certificate authority."
},
:cacert => {
:default => "$cadir/ca_crt.pem",
:type => :file,
:owner => "service",
:group => "service",
- :mode => 0660,
+ :mode => 0644,
:desc => "The CA certificate."
},
:cakey => {
:default => "$cadir/ca_key.pem",
:type => :file,
:owner => "service",
:group => "service",
- :mode => 0660,
+ :mode => 0640,
:desc => "The CA private key."
},
:capub => {
:default => "$cadir/ca_pub.pem",
:type => :file,
:owner => "service",
:group => "service",
+ :mode => 0644,
:desc => "The CA public key."
},
:cacrl => {
:default => "$cadir/ca_crl.pem",
:type => :file,
:owner => "service",
:group => "service",
- :mode => 0664,
-
+ :mode => 0644,
:desc => "The certificate revocation list (CRL) for the CA. Will be used if present but otherwise ignored.",
},
:caprivatedir => {
:default => "$cadir/private",
:type => :directory,
:owner => "service",
:group => "service",
- :mode => 0770,
+ :mode => 0750,
:desc => "Where the CA stores private certificate information."
},
:csrdir => {
:default => "$cadir/requests",
:type => :directory,
:owner => "service",
:group => "service",
+ :mode => 0755,
:desc => "Where the CA stores certificate requests"
},
:signeddir => {
:default => "$cadir/signed",
:type => :directory,
:owner => "service",
:group => "service",
- :mode => 0770,
+ :mode => 0755,
:desc => "Where the CA stores signed certificates."
},
:capass => {
:default => "$caprivatedir/ca.pass",
:type => :file,
:owner => "service",
:group => "service",
- :mode => 0660,
+ :mode => 0640,
:desc => "Where the CA stores the password for the private key."
},
:serial => {
:default => "$cadir/serial",
:type => :file,
:owner => "service",
:group => "service",
:mode => 0644,
:desc => "Where the serial number for certificates is stored."
},
:autosign => {
:default => "$confdir/autosign.conf",
: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,
:desc => "Whether to allow a new certificate
request to overwrite an existing certificate.",
},
:ca_ttl => {
:default => "5y",
:type => :duration,
- :desc => "The default TTL for new certificates. If this setting is set, ca_days is ignored.
+ :desc => "The default TTL for new certificates.
#{AS_DURATION}"
},
:req_bits => {
:default => 4096,
:desc => "The bit length of the certificates.",
},
:keylength => {
:default => 4096,
:desc => "The bit length of keys.",
},
:cert_inventory => {
:default => "$cadir/inventory.txt",
:type => :file,
:mode => 0644,
:owner => "service",
:group => "service",
:desc => "The inventory file. This is a text file to which the CA writes a
complete listing of all certificates."
}
)
# Define the config default.
define_settings(:application,
:config_file_name => {
:type => :string,
:default => Puppet::Settings.default_config_file_name,
:desc => "The name of the puppet config file.",
},
:config => {
:type => :file,
:default => "$confdir/${config_file_name}",
:desc => "The configuration file for the current puppet application.",
},
: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.",
},
:bindaddress => {
:default => "0.0.0.0",
:desc => "The address a listening server should bind to.",
}
)
define_settings(:master,
:user => {
:default => "puppet",
:desc => "The user puppet master should run as.",
},
:group => {
:default => "puppet",
:desc => "The group puppet master should run as.",
},
:manifestdir => {
:default => "$confdir/manifests",
:type => :directory,
:desc => "Where puppet master looks for its manifests.",
},
:manifest => {
:default => "$manifestdir/site.pp",
:type => :file,
- :desc => "The entry-point manifest for puppet master.",
+ :desc => "The entry-point manifest file for puppet master or a directory of manifests
+ to be evaluated in alphabetical order.",
},
:code => {
:default => "",
:desc => "Code to parse directly. This is essentially only used
by `puppet`, and should only be set if you're writing your own Puppet
executable.",
},
:masterlog => {
:default => "$logdir/puppetmaster.log",
:type => :file,
:owner => "service",
:group => "service",
:mode => 0660,
:desc => "Where puppet master logs. This is generally not used,
since syslog is the default log destination."
},
:masterhttplog => {
:default => "$logdir/masterhttp.log",
:type => :file,
:owner => "service",
:group => "service",
:mode => 0660,
:create => true,
:desc => "Where the puppet master web server logs."
},
:masterport => {
:default => 8140,
:desc => "The port for puppet master traffic. For puppet master,
this is the port to listen on; for puppet agent, this is the port
to make requests on. Both applications use this setting to get the port.",
},
:node_name => {
:default => "cert",
:desc => "How the puppet master determines the client's identity
and sets the 'hostname', 'fqdn' and 'domain' facts for use in the manifest,
in particular for determining which 'node' statement applies to the client.
Possible values are 'cert' (use the subject's CN in the client's
certificate) and 'facter' (use the hostname that the client
reported in its facts)",
},
:bucketdir => {
:default => "$vardir/bucket",
:type => :directory,
:mode => 0750,
:owner => "service",
:group => "service",
:desc => "Where FileBucket files are stored."
},
:rest_authconfig => {
:default => "$confdir/auth.conf",
:type => :file,
:desc => "The configuration file that defines the rights to the different
rest indirections. This can be used as a fine-grained
authorization system for `puppet master`.",
},
:ca => {
:default => true,
:type => :boolean,
:desc => "Whether the master should function as a certificate authority.",
},
- :modulepath => {
+ :basemodulepath => {
:default => "$confdir/modules#{File::PATH_SEPARATOR}/usr/share/puppet/modules",
:type => :path,
+ :desc => "The base non-environment specific search path for modules, included
+ also in all directory environment and default legacy environment modulepaths.",
+ },
+ :modulepath => {
+ :default => "$basemodulepath",
+ :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 ';'.)",
},
:ssl_client_header => {
:default => "HTTP_X_CLIENT_DN",
:desc => "The header containing an authenticated client's SSL DN.
This header must be set by the proxy to the authenticated client's SSL
DN (e.g., `/CN=puppet.puppetlabs.com`). Puppet will parse out the Common
Name (CN) from the Distinguished Name (DN) and use the value of the CN
field for authorization.
Note that the name of the HTTP header gets munged by the web server
common gateway inteface: an `HTTP_` prefix is added, dashes are converted
to underscores, and all letters are uppercased. Thus, to use the
`X-Client-DN` header, this setting should be `HTTP_X_CLIENT_DN`.",
},
:ssl_client_verify_header => {
:default => "HTTP_X_CLIENT_VERIFY",
:desc => "The header containing the status message of the client
verification. This header must be set by the proxy to 'SUCCESS' if the
client successfully authenticated, and anything else otherwise.
Note that the name of the HTTP header gets munged by the web server
common gateway inteface: an `HTTP_` prefix is added, dashes are converted
to underscores, and all letters are uppercased. Thus, to use the
`X-Client-Verify` header, this setting should be
`HTTP_X_CLIENT_VERIFY`.",
},
# To make sure this directory is created before we try to use it on the server, we need
# it to be in the server section (#1138).
:yamldir => {
:default => "$vardir/yaml",
:type => :directory,
:owner => "service",
:group => "service",
:mode => "750",
:desc => "The directory in which YAML data is stored, usually in a subdirectory."},
:server_datadir => {
:default => "$vardir/server_data",
:type => :directory,
:owner => "service",
:group => "service",
:mode => "750",
:desc => "The directory in which serialized data is stored, usually in a subdirectory."},
:reports => {
:default => "store",
:desc => "The list of report handlers to use. When using multiple report handlers,
their names should be comma-separated, with whitespace allowed. (For example,
`reports = http, tagmail`.)
This setting is relevant to puppet master and puppet apply. The puppet
master will call these report handlers with the reports it receives from
agent nodes, and puppet apply will call them with its own report. (In
all cases, the node applying the catalog must have `report = true`.)
See the report reference for information on the built-in report
handlers; custom report handlers can also be loaded from modules.
(Report handlers are loaded from the lib directory, at
`puppet/reports/NAME.rb`.)",
},
:reportdir => {
:default => "$vardir/reports",
:type => :directory,
:mode => 0750,
:owner => "service",
:group => "service",
:desc => "The directory in which to store reports. Each node gets
a separate subdirectory in this directory. This setting is only
used when the `store` report processor is enabled (see the
`reports` setting)."},
:reporturl => {
:default => "http://localhost:3000/reports/upload",
:desc => "The URL that reports should be forwarded to. This setting
is only used when the `http` report processor is enabled (see the
`reports` setting).",
},
:fileserverconfig => {
:default => "$confdir/fileserver.conf",
:type => :file,
:desc => "Where the fileserver configuration is stored.",
},
:strict_hostname_checking => {
:default => false,
:desc => "Whether to only search for the complete
hostname as it is in the certificate when searching for node information
in the catalogs.",
}
)
define_settings(:metrics,
:rrddir => {
:type => :directory,
:default => "$vardir/rrd",
:mode => 0750,
:owner => "service",
:group => "service",
:desc => "The directory where RRD database files are stored.
Directories for each reporting host will be created under
this directory."
},
:rrdinterval => {
:default => "$runinterval",
:type => :duration,
:desc => "How often RRD should expect data.
This should match how often the hosts report back to the server. #{AS_DURATION}",
}
)
define_settings(:device,
:devicedir => {
:default => "$vardir/devices",
:type => :directory,
:mode => "750",
:desc => "The root directory of devices' $vardir.",
},
:deviceconfig => {
:default => "$confdir/device.conf",
:desc => "Path to the device config file for puppet device.",
}
)
define_settings(:agent,
:node_name_value => {
:default => "$certname",
:desc => "The explicit value used for the node name for all requests the agent
makes to the master. WARNING: This setting is mutually exclusive with
node_name_fact. Changing this setting also requires changes to the default
auth.conf configuration on the Puppet Master. Please see
http://links.puppetlabs.com/node_name_value for more information."
},
:node_name_fact => {
:default => "",
:desc => "The fact name used to determine the node name used for all requests the agent
makes to the master. WARNING: This setting is mutually exclusive with
node_name_value. Changing this setting also requires changes to the default
auth.conf configuration on the Puppet Master. Please see
http://links.puppetlabs.com/node_name_fact for more information.",
:hook => proc do |value|
if !value.empty? and Puppet[:node_name_value] != Puppet[:certname]
raise "Cannot specify both the node_name_value and node_name_fact settings"
end
end
},
:localconfig => {
:default => "$statedir/localconfig",
:type => :file,
:owner => "root",
:mode => 0660,
:desc => "Where puppet agent caches the local configuration. An
extension indicating the cache format is added automatically."},
:statefile => {
:default => "$statedir/state.yaml",
:type => :file,
:mode => 0660,
:desc => "Where puppet agent and puppet master store state associated
with the running configuration. In the case of puppet master,
this file reflects the state discovered through interacting
with clients."
},
:clientyamldir => {
:default => "$vardir/client_yaml",
:type => :directory,
:mode => "750",
:desc => "The directory in which client-side YAML data is stored."
},
:client_datadir => {
:default => "$vardir/client_data",
:type => :directory,
:mode => "750",
:desc => "The directory in which serialized data is stored on the client."
},
:classfile => {
:default => "$statedir/classes.txt",
:type => :file,
:owner => "root",
:mode => 0640,
:desc => "The file in which puppet agent stores a list of the classes
associated with the retrieved configuration. Can be loaded in
the separate `puppet` executable using the `--loadclasses`
option."},
:resourcefile => {
:default => "$statedir/resources.txt",
:type => :file,
:owner => "root",
:mode => 0640,
:desc => "The file in which puppet agent stores a list of the resources
associated with the retrieved configuration." },
:puppetdlog => {
:default => "$logdir/puppetd.log",
:type => :file,
:owner => "root",
:mode => 0640,
:desc => "The log file for puppet agent. This is generally not used."
},
:server => {
:default => "puppet",
:desc => "The puppet master server to which the puppet agent should connect."
},
:use_srv_records => {
:default => false,
:type => :boolean,
:desc => "Whether the server will search for SRV records in DNS for the current domain.",
},
:srv_domain => {
:default => "#{Puppet::Settings.domain_fact}",
:desc => "The domain which will be queried to find the SRV records of servers to use.",
},
:ignoreschedules => {
:default => false,
:type => :boolean,
: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.",
},
:noop => {
:default => false,
:type => :boolean,
:desc => "Whether to apply catalogs in noop mode, which allows Puppet to
partially simulate a normal run. This setting affects puppet agent and
puppet apply.
When running in noop mode, Puppet will check whether each resource is in sync,
like it does when running normally. However, if a resource attribute is not in
the desired state (as declared in the catalog), Puppet will take no
action, and will instead report the changes it _would_ have made. These
simulated changes will appear in the report sent to the puppet master, or
be shown on the console if running puppet agent or puppet apply in the
foreground. The simulated changes will not send refresh events to any
subscribing or notified resources, although Puppet will log that a refresh
event _would_ have been sent.
**Important note:**
[The `noop` metaparameter](http://docs.puppetlabs.com/references/latest/metaparameter.html#noop)
allows you to apply individual resources in noop mode, and will override
the global value of the `noop` setting. This means a resource with
`noop => false` _will_ be changed if necessary, even when running puppet
agent with `noop = true` or `--noop`. (Conversely, a resource with
`noop => true` will only be simulated, even when noop mode is globally disabled.)",
},
:runinterval => {
:default => "30m",
:type => :duration,
:desc => "How often puppet agent applies the catalog.
Note that a runinterval of 0 means \"run continuously\" rather than
\"never run.\" If you want puppet agent to never run, you should start
it with the `--no-client` option. #{AS_DURATION}",
},
:listen => {
:default => false,
:type => :boolean,
:desc => "Whether puppet agent should listen for
connections. If this is true, then puppet agent will accept incoming
REST API requests, subject to the default ACLs and the ACLs set in
the `rest_authconfig` file. Puppet agent can respond usefully to
requests on the `run`, `facts`, `certificate`, and `resource` endpoints.",
},
:ca_server => {
:default => "$server",
:desc => "The server to use for certificate
authority requests. It's a separate server because it cannot
and does not need to horizontally scale.",
},
:ca_port => {
:default => "$masterport",
:desc => "The port to use for the certificate authority.",
},
:catalog_format => {
:default => "",
:desc => "(Deprecated for 'preferred_serialization_format') What format to
use to dump the catalog. Only supports 'marshal' and 'yaml'. Only
matters on the client, since it asks the server for a specific format.",
:hook => proc { |value|
if value
Puppet.deprecation_warning "Setting 'catalog_format' is deprecated; use 'preferred_serialization_format' instead."
- Puppet.settings[:preferred_serialization_format] = value
+ Puppet.settings.override_default(:preferred_serialization_format, value)
end
}
},
:preferred_serialization_format => {
:default => "pson",
:desc => "The preferred means of serializing
ruby instances for passing over the wire. This won't guarantee that all
instances will be serialized using this method, since not all classes
can be guaranteed to support this format, but it will be used for all
classes that support it.",
},
:report_serialization_format => {
:default => "pson",
:type => :enum,
:values => ["pson", "yaml"],
:desc => "The serialization format to use when sending reports to the
`report_server`. Possible values are `pson` and `yaml`. This setting
affects puppet agent, but not puppet apply (which processes its own
reports).
This should almost always be set to `pson`. It can be temporarily set to
`yaml` to let agents using this Puppet version connect to a puppet master
running Puppet 3.0.0 through 3.2.x.
Note that this is set to 'yaml' automatically if the agent detects an
older master, so should never need to be set explicitly."
},
:legacy_query_parameter_serialization => {
:default => false,
:type => :boolean,
:desc => "The serialization format to use when sending file_metadata
query parameters. Older versions of puppet master expect certain query
parameters to be serialized as yaml, which is deprecated.
This should almost always be false. It can be temporarily set to true
to let agents using this Puppet version connect to a puppet master
running Puppet 3.0.0 through 3.2.x.
Note that this is set to true automatically if the agent detects an
older master, so should never need to be set explicitly."
},
: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.",
},
: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.",
},
: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.",
},
: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.",
},
: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.",
},
: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.",
},
: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.",
:hook => proc { |value|
if value
Puppet.deprecation_warning "The dynamicfacts setting is deprecated and will be ignored."
end
}
},
:splaylimit => {
:default => "$runinterval",
:type => :duration,
:desc => "The maximum time to delay before runs. Defaults to being the same as the
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.",
},
:clientbucketdir => {
:default => "$vardir/clientbucket",
:type => :directory,
:mode => 0750,
:desc => "Where FileBucket files are stored locally."
},
:configtimeout => {
: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}",
},
:report_server => {
:default => "$server",
:desc => "The server to send transaction reports to.",
},
:report_port => {
:default => "$masterport",
:desc => "The port to communicate with the report_server.",
},
:inventory_server => {
:default => "$server",
:desc => "The server to send facts to.",
},
:inventory_port => {
:default => "$masterport",
:desc => "The port to communicate with the inventory_server.",
},
:report => {
:default => true,
:type => :boolean,
:desc => "Whether to send reports after every transaction.",
},
:lastrunfile => {
:default => "$statedir/last_run_summary.yaml",
:type => :file,
:mode => 0644,
:desc => "Where puppet agent stores the last run report summary in yaml format."
},
:lastrunreport => {
:default => "$statedir/last_run_report.yaml",
:type => :file,
:mode => 0640,
:desc => "Where puppet agent stores the last run report in yaml format."
},
:graph => {
: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).",
},
:graphdir => {
:default => "$statedir/graphs",
:type => :directory,
:desc => "Where to store dot-outputted graphs.",
},
:http_compression => {
: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.",
},
:waitforcert => {
:default => "2m",
:type => :duration,
:desc => "How frequently puppet agent should ask for a signed certificate.
When starting for the first time, puppet agent will submit a certificate
signing request (CSR) to the server named in the `ca_server` setting
(usually the puppet master); this may be autosigned, or may need to be
approved by a human, depending on the CA server's configuration.
Puppet agent cannot apply configurations until its approved certificate is
available. Since the certificate may or may not be available immediately,
puppet agent will repeatedly try to fetch it at this interval. You can
turn off waiting for certificates by specifying a time of 0, in which case
puppet agent will exit if it cannot get a cert.
#{AS_DURATION}",
},
:ordering => {
:type => :enum,
:values => ["manifest", "title-hash", "random"],
:default => "title-hash",
:desc => "How unrelated resources should be ordered when applying a catalog.
Allowed values are `title-hash`, `manifest`, and `random`. This
setting affects puppet agent and puppet apply, but not puppet master.
* `title-hash` (the default) will order resources randomly, but will use
the same order across runs and across nodes.
* `manifest` will use the order in which the resources were declared in
their manifest files.
* `random` will order resources randomly and change their order with each
run. This can work like a fuzzer for shaking out undeclared dependencies.
Regardless of this setting's value, Puppet will always obey explicit
dependencies set with the before/require/notify/subscribe metaparameters
and the `->`/`~>` chaining arrows; this setting only affects the relative
ordering of _unrelated_ resources."
}
)
define_settings(:inspect,
:archive_files => {
:type => :boolean,
:default => false,
:desc => "During an inspect run, whether to archive files whose contents are audited to a file bucket.",
},
:archive_file_server => {
:default => "$server",
:desc => "During an inspect run, the file bucket server to archive files to if archive_files is set.",
}
)
# Plugin information.
define_settings(
:main,
:plugindest => {
:type => :directory,
:default => "$libdir",
:desc => "Where Puppet should store plugins that it pulls down from the central
server.",
},
:pluginsource => {
:default => "puppet://$server/plugins",
:desc => "From where to retrieve plugins. The standard Puppet `file` type
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,
:desc => "Whether plugins should be synced with the central server.",
},
:pluginsignore => {
:default => ".svn CVS .git",
:desc => "What files to ignore when pulling down plugins.",
}
)
# Central fact information.
define_settings(
:main,
:factpath => {
: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 ';'.)",
:call_hook => :on_initialize_and_write, # Call our hook with the default value, so we always get the value added to facter.
:hook => proc do |value|
paths = value.split(File::PATH_SEPARATOR)
Facter.search(*paths)
end
}
)
define_settings(
:tagmail,
:tagmap => {
:default => "$confdir/tagmail.conf",
:desc => "The mapping between reporting tags and email addresses.",
},
:sendmail => {
:default => which('sendmail') || '',
:desc => "Where to find the sendmail binary with which to send email.",
},
:reportfrom => {
:default => "report@" + [Facter["hostname"].value,Facter["domain"].value].join("."),
:desc => "The 'from' email address for the reports.",
},
:smtpserver => {
:default => "none",
:desc => "The server through which to send email reports.",
},
:smtpport => {
:default => 25,
:desc => "The TCP port through which to send email reports.",
},
:smtphelo => {
:default => Facter["fqdn"].value,
:desc => "The name by which we identify ourselves in SMTP HELO for reports.
If you send to a smtpserver which does strict HELO checking (as with Postfix's
`smtpd_helo_restrictions` access controls), you may need to ensure this resolves.",
}
)
define_settings(
:rails,
:dblocation => {
:default => "$statedir/clientconfigs.sqlite3",
:type => :file,
:mode => 0660,
:owner => "service",
:group => "service",
:desc => "The sqlite database file. #{STORECONFIGS_ONLY}"
},
:dbadapter => {
:default => "sqlite3",
:desc => "The type of database to use. #{STORECONFIGS_ONLY}",
},
:dbmigrate => {
:default => false,
:type => :boolean,
:desc => "Whether to automatically migrate the database. #{STORECONFIGS_ONLY}",
},
:dbname => {
:default => "puppet",
:desc => "The name of the database to use. #{STORECONFIGS_ONLY}",
},
:dbserver => {
:default => "localhost",
:desc => "The database server for caching. Only
used when networked databases are used.",
},
:dbport => {
:default => "",
:desc => "The database password for caching. 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}",
},
:dbpassword => {
:default => "puppet",
:desc => "The database password for caching. 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}",
},
: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}",
},
:railslog => {
:default => "$logdir/rails.log",
:type => :file,
:mode => 0600,
:owner => "service",
:group => "service",
:desc => "Where Rails-specific logs are sent. #{STORECONFIGS_ONLY}"
},
: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}",
}
)
define_settings(
:couchdb,
: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`.",
}
)
define_settings(
:transaction,
: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.",
},
: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.",
},
:summarize => {
:default => false,
:type => :boolean,
:desc => "Whether to print a transaction summary.",
}
)
define_settings(
:main,
: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.
This command makes it straightforward to store your node mapping
information in other data sources like databases.",
}
)
define_settings(
:ldap,
:ldapssl => {
: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.",
},
: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.",
},
:ldapserver => {
:default => "ldap",
:desc => "The LDAP server. Only used if `node_terminus` is set to `ldap`.",
},
:ldapport => {
:default => 389,
:desc => "The LDAP port. Only used if `node_terminus` is set to `ldap`.",
},
:ldapstring => {
:default => "(&(objectclass=puppetClient)(cn=%s))",
:desc => "The search string used to find an LDAP node.",
},
:ldapclassattrs => {
:default => "puppetclass",
:desc => "The LDAP attributes to use to define Puppet classes. Values
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.",
},
: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.",
},
:ldapparentattr => {
:default => "parentnode",
:desc => "The attribute to use to define the parent node.",
},
:ldapuser => {
:default => "",
:desc => "The user to use to connect to LDAP. Must be specified as a
full DN.",
},
:ldappassword => {
:default => "",
:desc => "The password to use to connect to LDAP.",
},
: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.",
}
)
define_settings(:master,
:storeconfigs => {
: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.
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.",
# Call our hook with the default value, so we always get the libdir set.
:call_hook => :on_initialize_and_write,
:hook => proc do |value|
require 'puppet/node'
require 'puppet/node/facts'
if value
if not Puppet.settings[:async_storeconfigs]
Puppet::Resource::Catalog.indirection.cache_class = :store_configs
- Puppet.settings[:catalog_cache_terminus] = :store_configs
+ Puppet.settings.override_default(:catalog_cache_terminus, :store_configs)
end
Puppet::Node::Facts.indirection.cache_class = :store_configs
Puppet::Resource.indirection.terminus_class = :store_configs
end
end
},
:storeconfigs_backend => {
: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."
}
)
define_settings(:parser,
:templatedir => {
:default => "$vardir/templates",
:type => :directory,
:desc => "Where Puppet looks for template files. Can be a list of colon-separated
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
},
: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),
+ 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 `future` parser is a "time travel to the future" allowing early
- exposure to new language features. What these fatures are will vary from
+ exposure to new language features. What these features are will vary from
release to release and they may be invididually configurable.
Available Since Puppet 3.2.
EOT
},
+ :evaluator => {
+ :default => "future",
+ :hook => proc do |value|
+ if !['future', 'current'].include?(value)
+ raise "evaluator can only be set to 'future' or 'current', got '#{value}'"
+ end
+ end,
+ :desc => <<-'EOT'
+ Which evaluator to use when compiling Puppet manifests. Valid values
+ are `current` and `future` (the default).
+
+ **Note:** This setting is only used when `parser = future`. It allows
+ testers to turn off the `future` evaluator when doing detailed tests and
+ comparisons of the new compilation system.
+
+ Evaluation is the second stage of catalog compilation. After the parser
+ converts a manifest to a model of expressions, the evaluator processes
+ each expression. (For example, a resource declaration signals the
+ evaluator to add a resource to the catalog).
+
+ The `future` parser and evaluator are slated to become default in Puppet
+ 4. Their purpose is to add new features and improve consistency
+ and reliability.
+
+ Available Since Puppet 3.5.
+ 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
},
: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
},
: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
+ },
+ :strict_variables => {
+ :default => false,
+ :type => :boolean,
+ :desc => <<-'EOT'
+ Makes the parser raise errors when referencing unknown variables. (This does not affect
+ referencing variables that are explicitly set to undef).
+ EOT
}
-
)
define_settings(:puppetdoc,
:document_all => {
:default => false,
:type => :boolean,
:desc => "Whether to document all resources when using `puppet doc` to
generate manifest documentation.",
}
)
end
diff --git a/lib/puppet/environments.rb b/lib/puppet/environments.rb
new file mode 100644
index 000000000..75471de89
--- /dev/null
+++ b/lib/puppet/environments.rb
@@ -0,0 +1,187 @@
+# @api private
+module Puppet::Environments
+ # @api private
+ module EnvironmentCreator
+ # Create an anonymous environment.
+ #
+ # @param module_path [String] A list of module directories separated by the
+ # PATH_SEPARATOR
+ # @param manifest [String] The path to the manifest
+ # @return A new environment with the `name` `:anonymous`
+ #
+ # @api private
+ def for(module_path, manifest)
+ Puppet::Node::Environment.create(:anonymous,
+ module_path.split(File::PATH_SEPARATOR),
+ manifest)
+ end
+ end
+
+ # @!macro [new] loader_search_paths
+ # A list of indicators of where the loader is getting its environments from.
+ # @return [Array<String>] The URIs of the load locations
+ #
+ # @!macro [new] loader_list
+ # @return [Array<Puppet::Node::Environment>] All of the environments known
+ # to the loader
+ #
+ # @!macro [new] loader_get
+ # Find a named environment
+ #
+ # @param name [String,Symbol] The name of environment to find
+ # @return [Puppet::Node::Environment, nil] the requested environment or nil
+ # if it wasn't found
+
+ # A source of pre-defined environments.
+ #
+ # @api private
+ class Static
+ include EnvironmentCreator
+
+ def initialize(*environments)
+ @environments = environments
+ end
+
+ # @!macro loader_search_paths
+ def search_paths
+ ["data:text/plain,internal"]
+ end
+
+ # @!macro loader_list
+ def list
+ @environments
+ end
+
+ # @!macro loader_get
+ def get(name)
+ @environments.find do |env|
+ env.name == name.intern
+ end
+ end
+ end
+
+ # Old-style environments that come either from explicit stanzas in
+ # puppet.conf or from dynamic environments created from use of `$environment`
+ # in puppet.conf.
+ #
+ # @example Explicit Stanza
+ # [environment_name]
+ # modulepath=/var/my_env/modules
+ #
+ # @example Dynamic Environments
+ # [master]
+ # modulepath=/var/$environment/modules
+ #
+ # @api private
+ class Legacy
+ include EnvironmentCreator
+
+ # @!macro loader_search_paths
+ def search_paths
+ ["file://#{Puppet[:config]}"]
+ end
+
+ # @note The list of environments for the Legacy environments is always
+ # empty.
+ #
+ # @!macro loader_list
+ def list
+ []
+ end
+
+ # @note Because the Legacy system cannot list out all of its environments,
+ # get is able to return environments that are not returned by a call to
+ # {#list}.
+ #
+ # @!macro loader_get
+ def get(name)
+ Puppet::Node::Environment.new(name)
+ end
+ end
+
+ # Reads environments from a directory on disk. Each environment is
+ # represented as a sub-directory. The environment's manifest setting is the
+ # `manifest` directory of the environment directory. The environment's
+ # modulepath setting is the global modulepath (from the `[master]` section
+ # for the master) prepended with the `modules` directory of the environment
+ # directory.
+ #
+ # @api private
+ class Directories
+ def initialize(environment_dir, global_module_path)
+ @environment_dir = environment_dir
+ @global_module_path = global_module_path
+ end
+
+ # Generate an array of directory loaders from a path string.
+ # @param path [String] path to environment directories
+ # @param global_module_path [String] the global modulepath setting
+ # @return [Array<Puppet::Environments::Directories>] An array
+ # of configured directory loaders.
+ def self.from_path(path, global_module_path)
+ environments = path.split(File::PATH_SEPARATOR)
+ environments.map do |dir|
+ Puppet::Environments::Directories.new(dir, global_module_path)
+ end
+ end
+
+ # @!macro loader_search_paths
+ def search_paths
+ ["file://#{@environment_dir}"]
+ end
+
+ # @!macro loader_list
+ def list
+ base = Puppet::FileSystem.path_string(@environment_dir)
+
+ if Puppet::FileSystem.directory?(@environment_dir)
+ Puppet::FileSystem.children(@environment_dir).select do |child|
+ name = Puppet::FileSystem.basename_string(child)
+ Puppet::FileSystem.directory?(child) &&
+ Puppet::Node::Environment.valid_name?(name)
+ end.collect do |child|
+ name = Puppet::FileSystem.basename_string(child)
+ Puppet::Node::Environment.create(
+ name.intern,
+ [File.join(base, name, "modules")] + @global_module_path,
+ File.join(base, name, "manifests"))
+ end
+ else
+ []
+ end
+ end
+
+ # @!macro loader_get
+ def get(name)
+ list.find { |env| env.name == name.intern }
+ end
+ end
+
+ # Combine together multiple loaders to act as one.
+ # @api private
+ class Combined
+ def initialize(*loaders)
+ @loaders = loaders
+ end
+
+ # @!macro loader_search_paths
+ def search_paths
+ @loaders.collect(&:search_paths).flatten
+ end
+
+ # @!macro loader_list
+ def list
+ @loaders.collect(&:list).flatten
+ end
+
+ # @!macro loader_get
+ def get(name)
+ @loaders.each do |loader|
+ if env = loader.get(name)
+ return env
+ end
+ end
+ nil
+ end
+ end
+end
diff --git a/lib/puppet/error.rb b/lib/puppet/error.rb
index 34da17219..c31c360a2 100644
--- a/lib/puppet/error.rb
+++ b/lib/puppet/error.rb
@@ -1,61 +1,62 @@
module Puppet
# The base class for all Puppet errors. It can wrap another exception
class Error < RuntimeError
attr_accessor :original
def initialize(message, original=nil)
super(message)
@original = original
end
end
module ExternalFileError
# This module implements logging with a filename and line number. Use this
# for errors that need to report a location in a non-ruby file that we
# parse.
attr_accessor :line, :file, :pos
# May be called with 3 arguments for message, file, line, and exception, or
# 4 args including the position on the line.
#
def initialize(message, file=nil, line=nil, pos=nil, original=nil)
if pos.kind_of? Exception
original = pos
pos = nil
end
super(message, original)
- @file = file
+ @file = file unless (file.is_a?(String) && file.empty?)
@line = line
@pos = pos
end
def to_s
msg = super
+ @file = nil if (@file.is_a?(String) && @file.empty?)
if @file and @line and @pos
"#{msg} at #{@file}:#{@line}:#{@pos}"
elsif @file and @line
"#{msg} at #{@file}:#{@line}"
elsif @line and @pos
"#{msg} at line #{@line}:#{@pos}"
elsif @line
"#{msg} at line #{@line}"
elsif @file
"#{msg} in #{@file}"
else
msg
end
end
end
class ParseError < Puppet::Error
include ExternalFileError
end
class ResourceError < Puppet::Error
include ExternalFileError
end
# 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/pson/common.rb b/lib/puppet/external/pson/common.rb
index 2ea2b0e49..832d3f7ee 100644
--- a/lib/puppet/external/pson/common.rb
+++ b/lib/puppet/external/pson/common.rb
@@ -1,385 +1,385 @@
require 'puppet/external/pson/version'
module PSON
class << self
# If _object_ is string-like parse the string and return the parsed result
# as a Ruby data structure. Otherwise generate a PSON text from the Ruby
# data structure object and return it.
#
# The _opts_ argument is passed through to generate/parse respectively, see
# generate and parse for their documentation.
def [](object, opts = {})
if object.respond_to? :to_str
PSON.parse(object.to_str, opts => {})
else
PSON.generate(object, opts => {})
end
end
# Returns the PSON parser class, that is used by PSON. This might be either
# PSON::Ext::Parser or PSON::Pure::Parser.
attr_reader :parser
# Set the PSON parser class _parser_ to be used by PSON.
def parser=(parser) # :nodoc:
@parser = parser
remove_const :Parser if const_defined? :Parser
const_set :Parser, parser
end
def registered_document_types
@registered_document_types ||= {}
end
# Register a class-constant for deserializaion.
def register_document_type(name,klass)
registered_document_types[name.to_s] = klass
end
# Return the constant located at _path_.
# Anything may be registered as a path by calling register_path, above.
# Otherwise, the format of _path_ has to be either ::A::B::C or A::B::C.
# In either of these cases A has to be defined in Object (e.g. the path
# must be an absolute namespace path. If the constant doesn't exist at
# the given path, an ArgumentError is raised.
def deep_const_get(path) # :nodoc:
path = path.to_s
registered_document_types[path] || path.split(/::/).inject(Object) do |p, c|
case
when c.empty? then p
when p.const_defined?(c) then p.const_get(c)
else raise ArgumentError, "can't find const for unregistered document type #{path}"
end
end
end
# Set the module _generator_ to be used by PSON.
def generator=(generator) # :nodoc:
@generator = generator
generator_methods = generator::GeneratorMethods
for const in generator_methods.constants
klass = deep_const_get(const)
modul = generator_methods.const_get(const)
klass.class_eval do
instance_methods(false).each do |m|
m.to_s == 'to_pson' and remove_method m
end
include modul
end
end
self.state = generator::State
const_set :State, self.state
end
# Returns the PSON generator modul, that is used by PSON. This might be
# either PSON::Ext::Generator or PSON::Pure::Generator.
attr_reader :generator
# Returns the PSON generator state class, that is used by PSON. This might
# be either PSON::Ext::Generator::State or PSON::Pure::Generator::State.
attr_accessor :state
# This is create identifier, that is used to decide, if the _pson_create_
# hook of a class should be called. It defaults to 'document_type'.
attr_accessor :create_id
end
self.create_id = 'document_type'
NaN = (-1.0) ** 0.5
Infinity = 1.0/0
MinusInfinity = -Infinity
# The base exception for PSON errors.
class PSONError < StandardError; end
# This exception is raised, if a parser error occurs.
class ParserError < PSONError; end
# This exception is raised, if the nesting of parsed datastructures is too
# deep.
class NestingError < ParserError; end
# This exception is raised, if a generator or unparser error occurs.
class GeneratorError < PSONError; end
# For backwards compatibility
UnparserError = GeneratorError
# If a circular data structure is encountered while unparsing
# this exception is raised.
class CircularDatastructure < GeneratorError; end
# This exception is raised, if the required unicode support is missing on the
# system. Usually this means, that the iconv library is not installed.
class MissingUnicodeSupport < PSONError; end
module_function
# Parse the PSON string _source_ into a Ruby data structure and return it.
#
# _opts_ can have the following
# keys:
# * *max_nesting*: The maximum depth of nesting allowed in the parsed data
# structures. Disable depth checking with :max_nesting => false, it defaults
# to 19.
# * *allow_nan*: If set to true, allow NaN, Infinity and -Infinity in
# defiance of RFC 4627 to be parsed by the Parser. This option defaults
# to false.
# * *create_additions*: If set to false, the Parser doesn't create
# additions even if a matching class and create_id was found. This option
# defaults to true.
def parse(source, opts = {})
PSON.parser.new(source, opts).parse
end
# Parse the PSON string _source_ into a Ruby data structure and return it.
# The bang version of the parse method, defaults to the more dangerous values
# for the _opts_ hash, so be sure only to parse trusted _source_ strings.
#
# _opts_ can have the following keys:
# * *max_nesting*: The maximum depth of nesting allowed in the parsed data
# structures. Enable depth checking with :max_nesting => anInteger. The parse!
# methods defaults to not doing max depth checking: This can be dangerous,
# if someone wants to fill up your stack.
# * *allow_nan*: If set to true, allow NaN, Infinity, and -Infinity in
# defiance of RFC 4627 to be parsed by the Parser. This option defaults
# to true.
# * *create_additions*: If set to false, the Parser doesn't create
# additions even if a matching class and create_id was found. This option
# defaults to true.
def parse!(source, opts = {})
opts = {
:max_nesting => false,
:allow_nan => true
}.update(opts)
PSON.parser.new(source, opts).parse
end
# Unparse the Ruby data structure _obj_ into a single line PSON string and
# return it. _state_ is
# * a PSON::State object,
# * or a Hash like object (responding to to_hash),
# * an object convertible into a hash by a to_h method,
# that is used as or to configure a State object.
#
# It defaults to a state object, that creates the shortest possible PSON text
# in one line, checks for circular data structures and doesn't allow NaN,
# Infinity, and -Infinity.
#
# A _state_ hash can have the following keys:
# * *indent*: a string used to indent levels (default: ''),
# * *space*: a string that is put after, a : or , delimiter (default: ''),
# * *space_before*: a string that is put before a : pair delimiter (default: ''),
# * *object_nl*: a string that is put at the end of a PSON object (default: ''),
# * *array_nl*: a string that is put at the end of a PSON array (default: ''),
# * *check_circular*: true if checking for circular data structures
# should be done (the default), false otherwise.
# * *allow_nan*: true if NaN, Infinity, and -Infinity should be
# generated, otherwise an exception is thrown, if these values are
# encountered. This options defaults to false.
# * *max_nesting*: The maximum depth of nesting allowed in the data
# structures from which PSON is to be generated. Disable depth checking
# with :max_nesting => false, it defaults to 19.
#
# See also the fast_generate for the fastest creation method with the least
# amount of sanity checks, and the pretty_generate method for some
# defaults for a pretty output.
def generate(obj, state = nil)
if state
state = State.from_state(state)
else
state = State.new
end
obj.to_pson(state)
end
# :stopdoc:
# I want to deprecate these later, so I'll first be silent about them, and
# later delete them.
alias unparse generate
module_function :unparse
# :startdoc:
# Unparse the Ruby data structure _obj_ into a single line PSON string and
# return it. This method disables the checks for circles in Ruby objects, and
# also generates NaN, Infinity, and, -Infinity float values.
#
# *WARNING*: Be careful not to pass any Ruby data structures with circles as
# _obj_ argument, because this will cause PSON to go into an infinite loop.
def fast_generate(obj)
obj.to_pson(nil)
end
# :stopdoc:
# I want to deprecate these later, so I'll first be silent about them, and later delete them.
alias fast_unparse fast_generate
module_function :fast_unparse
# :startdoc:
# Unparse the Ruby data structure _obj_ into a PSON string and return it. The
# returned string is a prettier form of the string returned by #unparse.
#
# The _opts_ argument can be used to configure the generator, see the
# generate method for a more detailed explanation.
def pretty_generate(obj, opts = nil)
state = PSON.state.new(
:indent => ' ',
:space => ' ',
:object_nl => "\n",
:array_nl => "\n",
:check_circular => true
)
if opts
if opts.respond_to? :to_hash
opts = opts.to_hash
elsif opts.respond_to? :to_h
opts = opts.to_h
else
raise TypeError, "can't convert #{opts.class} into Hash"
end
state.configure(opts)
end
obj.to_pson(state)
end
# :stopdoc:
# I want to deprecate these later, so I'll first be silent about them, and later delete them.
alias pretty_unparse pretty_generate
module_function :pretty_unparse
# :startdoc:
# Load a ruby data structure from a PSON _source_ and return it. A source can
# either be a string-like object, an IO like object, or an object responding
# to the read method. If _proc_ was given, it will be called with any nested
# Ruby object as an argument recursively in depth first order.
#
# This method is part of the implementation of the load/dump interface of
# Marshal and YAML.
def load(source, proc = nil)
if source.respond_to? :to_str
source = source.to_str
elsif source.respond_to? :to_io
source = source.to_io.read
else
source = source.read
end
result = parse(source, :max_nesting => false, :allow_nan => true)
recurse_proc(result, &proc) if proc
result
end
def recurse_proc(result, &proc)
case result
when Array
result.each { |x| recurse_proc x, &proc }
proc.call result
when Hash
result.each { |x, y| recurse_proc x, &proc; recurse_proc y, &proc }
proc.call result
else
proc.call result
end
end
private :recurse_proc
module_function :recurse_proc
alias restore load
module_function :restore
# Dumps _obj_ as a PSON string, i.e. calls generate on the object and returns
# the result.
#
# If anIO (an IO like object or an object that responds to the write method)
# was given, the resulting PSON is written to it.
#
# If the number of nested arrays or objects exceeds _limit_ an ArgumentError
# exception is raised. This argument is similar (but not exactly the
# same!) to the _limit_ argument in Marshal.dump.
#
# This method is part of the implementation of the load/dump interface of
# Marshal and YAML.
def dump(obj, anIO = nil, limit = nil)
if anIO and limit.nil?
anIO = anIO.to_io if anIO.respond_to?(:to_io)
unless anIO.respond_to?(:write)
limit = anIO
anIO = nil
end
end
limit ||= 0
result = generate(obj, :allow_nan => true, :max_nesting => limit)
if anIO
anIO.write result
anIO
else
result
end
rescue PSON::NestingError
- raise ArgumentError, "exceed depth limit"
+ raise ArgumentError, "exceed depth limit", $!.backtrace
end
# Provide a smarter wrapper for changing string encoding that works with
# both Ruby 1.8 (iconv) and 1.9 (String#encode). Thankfully they seem to
# have compatible input syntax, at least for the encodings we touch.
if String.method_defined?("encode")
def encode(to, from, string)
string.encode(to, from)
end
else
require 'iconv'
def encode(to, from, string)
Iconv.conv(to, from, string)
end
end
end
module ::Kernel
private
# Outputs _objs_ to STDOUT as PSON strings in the shortest form, that is in
# one line.
def j(*objs)
objs.each do |obj|
puts PSON::generate(obj, :allow_nan => true, :max_nesting => false)
end
nil
end
# Ouputs _objs_ to STDOUT as PSON strings in a pretty format, with
# indentation and over many lines.
def jj(*objs)
objs.each do |obj|
puts PSON::pretty_generate(obj, :allow_nan => true, :max_nesting => false)
end
nil
end
# If _object_ is string-like parse the string and return the parsed result as
# a Ruby data structure. Otherwise generate a PSON text from the Ruby data
# structure object and return it.
#
# The _opts_ argument is passed through to generate/parse respectively, see
# generate and parse for their documentation.
def PSON(object, opts = {})
if object.respond_to? :to_str
PSON.parse(object.to_str, opts)
else
PSON.generate(object, opts)
end
end
end
class ::Class
# Returns true, if this class can be used to create an instance
# from a serialised PSON string. The class has to implement a class
# method _pson_create_ that expects a hash as first parameter, which includes
# the required data.
def pson_creatable?
respond_to?(:pson_create)
end
end
diff --git a/lib/puppet/external/pson/pure/generator.rb b/lib/puppet/external/pson/pure/generator.rb
index e0eabffc3..bcf2fde2a 100644
--- a/lib/puppet/external/pson/pure/generator.rb
+++ b/lib/puppet/external/pson/pure/generator.rb
@@ -1,401 +1,401 @@
module PSON
MAP = {
"\x0" => '\u0000',
"\x1" => '\u0001',
"\x2" => '\u0002',
"\x3" => '\u0003',
"\x4" => '\u0004',
"\x5" => '\u0005',
"\x6" => '\u0006',
"\x7" => '\u0007',
"\b" => '\b',
"\t" => '\t',
"\n" => '\n',
"\xb" => '\u000b',
"\f" => '\f',
"\r" => '\r',
"\xe" => '\u000e',
"\xf" => '\u000f',
"\x10" => '\u0010',
"\x11" => '\u0011',
"\x12" => '\u0012',
"\x13" => '\u0013',
"\x14" => '\u0014',
"\x15" => '\u0015',
"\x16" => '\u0016',
"\x17" => '\u0017',
"\x18" => '\u0018',
"\x19" => '\u0019',
"\x1a" => '\u001a',
"\x1b" => '\u001b',
"\x1c" => '\u001c',
"\x1d" => '\u001d',
"\x1e" => '\u001e',
"\x1f" => '\u001f',
'"' => '\"',
'\\' => '\\\\',
} # :nodoc:
# Convert a UTF8 encoded Ruby string _string_ to a PSON string, encoded with
# UTF16 big endian characters as \u????, and return it.
if String.method_defined?(:force_encoding)
def utf8_to_pson(string) # :nodoc:
string = string.dup
string << '' # XXX workaround: avoid buffer sharing
string.force_encoding(Encoding::ASCII_8BIT)
string.gsub!(/["\\\x0-\x1f]/) { MAP[$MATCH] }
string
rescue => e
- raise GeneratorError, "Caught #{e.class}: #{e}"
+ raise GeneratorError, "Caught #{e.class}: #{e}", e.backtrace
end
else
def utf8_to_pson(string) # :nodoc:
string.gsub(/["\\\x0-\x1f]/n) { MAP[$MATCH] }
end
end
module_function :utf8_to_pson
module Pure
module Generator
# This class is used to create State instances, that are use to hold data
# while generating a PSON text from a Ruby data structure.
class State
# Creates a State object from _opts_, which ought to be Hash to create
# a new State instance configured by _opts_, something else to create
# an unconfigured instance. If _opts_ is a State object, it is just
# returned.
def self.from_state(opts)
case opts
when self
opts
when Hash
new(opts)
else
new
end
end
# Instantiates a new State object, configured by _opts_.
#
# _opts_ can have the following keys:
#
# * *indent*: a string used to indent levels (default: ''),
# * *space*: a string that is put after, a : or , delimiter (default: ''),
# * *space_before*: a string that is put before a : pair delimiter (default: ''),
# * *object_nl*: a string that is put at the end of a PSON object (default: ''),
# * *array_nl*: a string that is put at the end of a PSON array (default: ''),
# * *check_circular*: true if checking for circular data structures
# should be done (the default), false otherwise.
# * *check_circular*: true if checking for circular data structures
# should be done, false (the default) otherwise.
# * *allow_nan*: true if NaN, Infinity, and -Infinity should be
# generated, otherwise an exception is thrown, if these values are
# encountered. This options defaults to false.
def initialize(opts = {})
@seen = {}
@indent = ''
@space = ''
@space_before = ''
@object_nl = ''
@array_nl = ''
@check_circular = true
@allow_nan = false
configure opts
end
# This string is used to indent levels in the PSON text.
attr_accessor :indent
# This string is used to insert a space between the tokens in a PSON
# string.
attr_accessor :space
# This string is used to insert a space before the ':' in PSON objects.
attr_accessor :space_before
# This string is put at the end of a line that holds a PSON object (or
# Hash).
attr_accessor :object_nl
# This string is put at the end of a line that holds a PSON array.
attr_accessor :array_nl
# This integer returns the maximum level of data structure nesting in
# the generated PSON, max_nesting = 0 if no maximum is checked.
attr_accessor :max_nesting
def check_max_nesting(depth) # :nodoc:
return if @max_nesting.zero?
current_nesting = depth + 1
current_nesting > @max_nesting and
raise NestingError, "nesting of #{current_nesting} is too deep"
end
# Returns true, if circular data structures should be checked,
# otherwise returns false.
def check_circular?
@check_circular
end
# Returns true if NaN, Infinity, and -Infinity should be considered as
# valid PSON and output.
def allow_nan?
@allow_nan
end
# Returns _true_, if _object_ was already seen during this generating
# run.
def seen?(object)
@seen.key?(object.__id__)
end
# Remember _object_, to find out if it was already encountered (if a
# cyclic data structure is if a cyclic data structure is rendered).
def remember(object)
@seen[object.__id__] = true
end
# Forget _object_ for this generating run.
def forget(object)
@seen.delete object.__id__
end
# Configure this State instance with the Hash _opts_, and return
# itself.
def configure(opts)
@indent = opts[:indent] if opts.key?(:indent)
@space = opts[:space] if opts.key?(:space)
@space_before = opts[:space_before] if opts.key?(:space_before)
@object_nl = opts[:object_nl] if opts.key?(:object_nl)
@array_nl = opts[:array_nl] if opts.key?(:array_nl)
@check_circular = !!opts[:check_circular] if opts.key?(:check_circular)
@allow_nan = !!opts[:allow_nan] if opts.key?(:allow_nan)
if !opts.key?(:max_nesting) # defaults to 19
@max_nesting = 19
elsif opts[:max_nesting]
@max_nesting = opts[:max_nesting]
else
@max_nesting = 0
end
self
end
# Returns the configuration instance variables as a hash, that can be
# passed to the configure method.
def to_h
result = {}
for iv in %w{indent space space_before object_nl array_nl check_circular allow_nan max_nesting}
result[iv.intern] = instance_variable_get("@#{iv}")
end
result
end
end
module GeneratorMethods
module Object
# Converts this object to a string (calling #to_s), converts
# it to a PSON string, and returns the result. This is a fallback, if no
# special method #to_pson was defined for some object.
def to_pson(*) to_s.to_pson end
end
module Hash
# Returns a PSON string containing a PSON object, that is unparsed from
# this Hash instance.
# _state_ is a PSON::State object, that can also be used to configure the
# produced PSON string output further.
# _depth_ is used to find out nesting depth, to indent accordingly.
def to_pson(state = nil, depth = 0, *)
if state
state = PSON.state.from_state(state)
state.check_max_nesting(depth)
pson_check_circular(state) { pson_transform(state, depth) }
else
pson_transform(state, depth)
end
end
private
def pson_check_circular(state)
if state and state.check_circular?
state.seen?(self) and raise PSON::CircularDatastructure,
"circular data structures not supported!"
state.remember self
end
yield
ensure
state and state.forget self
end
def pson_shift(state, depth)
state and not state.object_nl.empty? or return ''
state.indent * depth
end
def pson_transform(state, depth)
delim = ','
if state
delim << state.object_nl
result = '{'
result << state.object_nl
result << map { |key,value|
s = pson_shift(state, depth + 1)
s << key.to_s.to_pson(state, depth + 1)
s << state.space_before
s << ':'
s << state.space
s << value.to_pson(state, depth + 1)
}.join(delim)
result << state.object_nl
result << pson_shift(state, depth)
result << '}'
else
result = '{'
result << map { |key,value|
key.to_s.to_pson << ':' << value.to_pson
}.join(delim)
result << '}'
end
result
end
end
module Array
# Returns a PSON string containing a PSON array, that is unparsed from
# this Array instance.
# _state_ is a PSON::State object, that can also be used to configure the
# produced PSON string output further.
# _depth_ is used to find out nesting depth, to indent accordingly.
def to_pson(state = nil, depth = 0, *)
if state
state = PSON.state.from_state(state)
state.check_max_nesting(depth)
pson_check_circular(state) { pson_transform(state, depth) }
else
pson_transform(state, depth)
end
end
private
def pson_check_circular(state)
if state and state.check_circular?
state.seen?(self) and raise PSON::CircularDatastructure,
"circular data structures not supported!"
state.remember self
end
yield
ensure
state and state.forget self
end
def pson_shift(state, depth)
state and not state.array_nl.empty? or return ''
state.indent * depth
end
def pson_transform(state, depth)
delim = ','
if state
delim << state.array_nl
result = '['
result << state.array_nl
result << map { |value|
pson_shift(state, depth + 1) << value.to_pson(state, depth + 1)
}.join(delim)
result << state.array_nl
result << pson_shift(state, depth)
result << ']'
else
'[' << map { |value| value.to_pson }.join(delim) << ']'
end
end
end
module Integer
# Returns a PSON string representation for this Integer number.
def to_pson(*) to_s end
end
module Float
# Returns a PSON string representation for this Float number.
def to_pson(state = nil, *)
case
when infinite?
if !state || state.allow_nan?
to_s
else
raise GeneratorError, "#{self} not allowed in PSON"
end
when nan?
if !state || state.allow_nan?
to_s
else
raise GeneratorError, "#{self} not allowed in PSON"
end
else
to_s
end
end
end
module String
# This string should be encoded with UTF-8 A call to this method
# returns a PSON string encoded with UTF16 big endian characters as
# \u????.
def to_pson(*)
'"' << PSON.utf8_to_pson(self) << '"'
end
# Module that holds the extinding methods if, the String module is
# included.
module Extend
# Raw Strings are PSON Objects (the raw bytes are stored in an array for the
# key "raw"). The Ruby String can be created by this module method.
def pson_create(o)
o['raw'].pack('C*')
end
end
# Extends _modul_ with the String::Extend module.
def self.included(modul)
modul.extend Extend
end
# This method creates a raw object hash, that can be nested into
# other data structures and will be unparsed as a raw string. This
# method should be used, if you want to convert raw strings to PSON
# instead of UTF-8 strings, e.g. binary data.
def to_pson_raw_object
{
PSON.create_id => self.class.name,
'raw' => self.unpack('C*'),
}
end
# This method creates a PSON text from the result of
# a call to to_pson_raw_object of this String.
def to_pson_raw(*args)
to_pson_raw_object.to_pson(*args)
end
end
module TrueClass
# Returns a PSON string for true: 'true'.
def to_pson(*) 'true' end
end
module FalseClass
# Returns a PSON string for false: 'false'.
def to_pson(*) 'false' end
end
module NilClass
# Returns a PSON string for nil: 'null'.
def to_pson(*) 'null' end
end
end
end
end
end
diff --git a/lib/puppet/external/pson/pure/parser.rb b/lib/puppet/external/pson/pure/parser.rb
index bcdaf7b64..43c6c5ffb 100644
--- a/lib/puppet/external/pson/pure/parser.rb
+++ b/lib/puppet/external/pson/pure/parser.rb
@@ -1,318 +1,318 @@
require 'strscan'
module PSON
module Pure
# This class implements the PSON parser that is used to parse a PSON string
# into a Ruby data structure.
class Parser < StringScanner
STRING = /" ((?:[^\x0-\x1f"\\] |
# escaped special characters:
\\["\\\/bfnrt] |
\\u[0-9a-fA-F]{4} |
# match all but escaped special characters:
\\[\x20-\x21\x23-\x2e\x30-\x5b\x5d-\x61\x63-\x65\x67-\x6d\x6f-\x71\x73\x75-\xff])*)
"/nx
INTEGER = /(-?0|-?[1-9]\d*)/
FLOAT = /(-?
(?:0|[1-9]\d*)
(?:
\.\d+(?i:e[+-]?\d+) |
\.\d+ |
(?i:e[+-]?\d+)
)
)/x
NAN = /NaN/
INFINITY = /Infinity/
MINUS_INFINITY = /-Infinity/
OBJECT_OPEN = /\{/
OBJECT_CLOSE = /\}/
ARRAY_OPEN = /\[/
ARRAY_CLOSE = /\]/
PAIR_DELIMITER = /:/
COLLECTION_DELIMITER = /,/
TRUE = /true/
FALSE = /false/
NULL = /null/
IGNORE = %r(
(?:
//[^\n\r]*[\n\r]| # line comments
/\* # c-style comments
(?:
[^*/]| # normal chars
/[^*]| # slashes that do not start a nested comment
\*[^/]| # asterisks that do not end this comment
/(?=\*/) # single slash before this comment's end
)*
\*/ # the End of this comment
|[ \t\r\n]+ # whitespaces: space, horicontal tab, lf, cr
)+
)mx
UNPARSED = Object.new
# Creates a new PSON::Pure::Parser instance for the string _source_.
#
# It will be configured by the _opts_ hash. _opts_ can have the following
# keys:
# * *max_nesting*: The maximum depth of nesting allowed in the parsed data
# structures. Disable depth checking with :max_nesting => false|nil|0,
# it defaults to 19.
# * *allow_nan*: If set to true, allow NaN, Infinity and -Infinity in
# defiance of RFC 4627 to be parsed by the Parser. This option defaults
# to false.
# * *create_additions*: If set to false, the Parser doesn't create
# additions even if a matching class and create_id was found. This option
# defaults to true.
# * *object_class*: Defaults to Hash
# * *array_class*: Defaults to Array
def initialize(source, opts = {})
source = convert_encoding source
super source
if !opts.key?(:max_nesting) # defaults to 19
@max_nesting = 19
elsif opts[:max_nesting]
@max_nesting = opts[:max_nesting]
else
@max_nesting = 0
end
@allow_nan = !!opts[:allow_nan]
ca = true
ca = opts[:create_additions] if opts.key?(:create_additions)
@create_id = ca ? PSON.create_id : nil
@object_class = opts[:object_class] || Hash
@array_class = opts[:array_class] || Array
end
alias source string
# Parses the current PSON string _source_ and returns the complete data
# structure as a result.
def parse
reset
obj = nil
until eos?
case
when scan(OBJECT_OPEN)
obj and raise ParserError, "source '#{peek(20)}' not in PSON!"
@current_nesting = 1
obj = parse_object
when scan(ARRAY_OPEN)
obj and raise ParserError, "source '#{peek(20)}' not in PSON!"
@current_nesting = 1
obj = parse_array
when skip(IGNORE)
;
else
raise ParserError, "source '#{peek(20)}' not in PSON!"
end
end
obj or raise ParserError, "source did not contain any PSON!"
obj
end
private
def convert_encoding(source)
if source.respond_to?(:to_str)
source = source.to_str
else
raise TypeError, "#{source.inspect} is not like a string"
end
if supports_encodings?(source)
if source.encoding == ::Encoding::ASCII_8BIT
b = source[0, 4].bytes.to_a
source =
case
when b.size >= 4 && b[0] == 0 && b[1] == 0 && b[2] == 0
source.dup.force_encoding(::Encoding::UTF_32BE).encode!(::Encoding::UTF_8)
when b.size >= 4 && b[0] == 0 && b[2] == 0
source.dup.force_encoding(::Encoding::UTF_16BE).encode!(::Encoding::UTF_8)
when b.size >= 4 && b[1] == 0 && b[2] == 0 && b[3] == 0
source.dup.force_encoding(::Encoding::UTF_32LE).encode!(::Encoding::UTF_8)
when b.size >= 4 && b[1] == 0 && b[3] == 0
source.dup.force_encoding(::Encoding::UTF_16LE).encode!(::Encoding::UTF_8)
else
source.dup
end
else
source = source.encode(::Encoding::UTF_8)
end
source.force_encoding(::Encoding::ASCII_8BIT)
else
b = source
source =
case
when b.size >= 4 && b[0] == 0 && b[1] == 0 && b[2] == 0
PSON.encode('utf-8', 'utf-32be', b)
when b.size >= 4 && b[0] == 0 && b[2] == 0
PSON.encode('utf-8', 'utf-16be', b)
when b.size >= 4 && b[1] == 0 && b[2] == 0 && b[3] == 0
PSON.encode('utf-8', 'utf-32le', b)
when b.size >= 4 && b[1] == 0 && b[3] == 0
PSON.encode('utf-8', 'utf-16le', b)
else
b
end
end
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 }
UNESCAPE_MAP.update(
{
?" => '"',
?\\ => '\\',
?/ => '/',
?b => "\b",
?f => "\f",
?n => "\n",
?r => "\r",
?t => "\t",
?u => nil,
})
def parse_string
if scan(STRING)
return '' if self[1].empty?
string = self[1].gsub(%r{(?:\\[\\bfnrt"/]|(?:\\u(?:[A-Fa-f\d]{4}))+|\\[\x20-\xff])}n) do |c|
if u = UNESCAPE_MAP[$MATCH[1]]
u
else # \uXXXX
bytes = ''
i = 0
while c[6 * i] == ?\\ && c[6 * i + 1] == ?u
bytes << c[6 * i + 2, 2].to_i(16) << c[6 * i + 4, 2].to_i(16)
i += 1
end
PSON.encode('utf-8', 'utf-16be', bytes)
end
end
string.force_encoding(Encoding::UTF_8) if string.respond_to?(:force_encoding)
string
else
UNPARSED
end
rescue => e
- raise GeneratorError, "Caught #{e.class}: #{e}"
+ raise GeneratorError, "Caught #{e.class}: #{e}", e.backtrace
end
def parse_value
case
when scan(FLOAT)
Float(self[1])
when scan(INTEGER)
Integer(self[1])
when scan(TRUE)
true
when scan(FALSE)
false
when scan(NULL)
nil
when (string = parse_string) != UNPARSED
string
when scan(ARRAY_OPEN)
@current_nesting += 1
ary = parse_array
@current_nesting -= 1
ary
when scan(OBJECT_OPEN)
@current_nesting += 1
obj = parse_object
@current_nesting -= 1
obj
when @allow_nan && scan(NAN)
NaN
when @allow_nan && scan(INFINITY)
Infinity
when @allow_nan && scan(MINUS_INFINITY)
MinusInfinity
else
UNPARSED
end
end
def parse_array
raise NestingError, "nesting of #@current_nesting is too deep" if
@max_nesting.nonzero? && @current_nesting > @max_nesting
result = @array_class.new
delim = false
until eos?
case
when (value = parse_value) != UNPARSED
delim = false
result << value
skip(IGNORE)
if scan(COLLECTION_DELIMITER)
delim = true
elsif match?(ARRAY_CLOSE)
;
else
raise ParserError, "expected ',' or ']' in array at '#{peek(20)}'!"
end
when scan(ARRAY_CLOSE)
raise ParserError, "expected next element in array at '#{peek(20)}'!" if delim
break
when skip(IGNORE)
;
else
raise ParserError, "unexpected token in array at '#{peek(20)}'!"
end
end
result
end
def parse_object
raise NestingError, "nesting of #@current_nesting is too deep" if
@max_nesting.nonzero? && @current_nesting > @max_nesting
result = @object_class.new
delim = false
until eos?
case
when (string = parse_string) != UNPARSED
skip(IGNORE)
raise ParserError, "expected ':' in object at '#{peek(20)}'!" unless scan(PAIR_DELIMITER)
skip(IGNORE)
unless (value = parse_value).equal? UNPARSED
result[string] = value
delim = false
skip(IGNORE)
if scan(COLLECTION_DELIMITER)
delim = true
elsif match?(OBJECT_CLOSE)
;
else
raise ParserError, "expected ',' or '}' in object at '#{peek(20)}'!"
end
else
raise ParserError, "expected value in object at '#{peek(20)}'!"
end
when scan(OBJECT_CLOSE)
raise ParserError, "expected next name, value pair in object at '#{peek(20)}'!" if delim
if @create_id and klassname = result[@create_id]
klass = PSON.deep_const_get klassname
break unless klass and klass.pson_creatable?
result = klass.pson_create(result)
end
break
when skip(IGNORE)
;
else
raise ParserError, "unexpected token in object at '#{peek(20)}'!"
end
end
result
end
end
end
end
diff --git a/lib/puppet/face/config.rb b/lib/puppet/face/config.rb
index b30c5baf8..a2412f086 100644
--- a/lib/puppet/face/config.rb
+++ b/lib/puppet/face/config.rb
@@ -1,48 +1,106 @@
require 'puppet/face'
+require 'puppet/settings/ini_file'
Puppet::Face.define(:config, '0.0.1') do
copyright "Puppet Labs", 2011
license "Apache 2 license; see COPYING"
- summary "Interact with Puppet's configuration options."
+ summary "Interact with Puppet's settings."
+
+ description "This subcommand can inspect and modify settings from Puppet's
+ 'puppet.conf' configuration file. For documentation about individual settings,
+ see http://docs.puppetlabs.com/references/latest/configuration.html."
+
+ option "--section SECTION_NAME" do
+ default_to { "main" }
+ summary "The section of the configuration file to interact with."
+ description <<-EOT
+ The section of the puppet.conf configuration file to interact with.
+
+ The three most commonly used sections are 'main', 'master', and 'agent'.
+ 'Main' is the default, and is used by all Puppet applications. Other
+ sections can override 'main' values for specific applications --- the
+ 'master' section affects puppet master and puppet cert, and the 'agent'
+ section affects puppet agent.
+
+ Less commonly used is the 'user' section, which affects puppet apply. Any
+ other section will be treated as the name of a legacy environment
+ (a deprecated feature), and can only include the 'manifest' and
+ 'modulepath' settings.
+ EOT
+ end
action(:print) do
- summary "Examine Puppet's current configuration settings."
+ summary "Examine Puppet's current settings."
arguments "(all | <setting> [<setting> ...]"
- returns <<-'EOT'
- A single value when called with one config setting, and a list of
- settings and values when called with multiple options or "all."
- EOT
description <<-'EOT'
- Prints the value of a single configuration option or a list of
- configuration options.
+ Prints the value of a single setting or a list of settings.
This action is an alternate interface to the information available with
`puppet <subcommand> --configprint`.
EOT
notes <<-'EOT'
- By default, this action reads the configuration in agent mode.
- Use the '--run_mode' and '--environment' flags to examine other
+ By default, this action reads the general configuration in the 'main'
+ section. Use the '--section' and '--environment' flags to examine other
configuration domains.
EOT
examples <<-'EOT'
Get puppet's runfile directory:
$ puppet config print rundir
Get a list of important directories from the master's config:
- $ puppet config print all --run_mode master | grep -E "(path|dir)"
+ $ puppet config print all --section master | grep -E "(path|dir)"
EOT
when_invoked do |*args|
- args.pop
+ options = args.pop
+
+ args = Puppet.settings.to_a.collect(&:first) if args.empty? || args == ['all']
+
+ values = Puppet.settings.values(Puppet[:environment].to_sym, options[:section].to_sym)
+ if args.length == 1
+ puts values.interpolate(args[0].to_sym)
+ else
+ args.each do |setting_name|
+ puts "#{setting_name} = #{values.interpolate(setting_name.to_sym)}"
+ end
+ end
+ nil
+ end
+ end
- args = [ "all" ] if args.empty?
+ action(:set) do
+ summary "Set Puppet's settings."
+ arguments "[setting_name] [setting_value]"
+ description <<-'EOT'
+ Updates values in the `puppet.conf` configuration file.
+ EOT
+ notes <<-'EOT'
+ By default, this action manipulates the configuration in the
+ 'main' section. Use the '--section' flag to manipulate other
+ configuration domains.
+ EOT
+ examples <<-'EOT'
+ Set puppet's runfile directory:
+
+ $ puppet config set rundir /var/run/puppet
+
+ Set the vardir for only the agent:
+
+ $ puppet config set vardir /var/lib/puppetagent --section agent
+ EOT
- Puppet.settings[:configprint] = args.join(",")
- Puppet.settings.print_config_options
+ when_invoked do |name, value, options|
+ path = Puppet::FileSystem.pathname(Puppet.settings.which_configuration_file)
+ Puppet::FileSystem.touch(path)
+ Puppet::FileSystem.open(path, nil, 'r+') do |file|
+ Puppet::Settings::IniFile.update(file) do |config|
+ config.set(options[:section], name, value)
+ end
+ end
nil
end
end
end
diff --git a/lib/puppet/face/file/store.rb b/lib/puppet/face/file/store.rb
index 76ebbc15b..dc43fee04 100644
--- a/lib/puppet/face/file/store.rb
+++ b/lib/puppet/face/file/store.rb
@@ -1,21 +1,21 @@
# Store a specified file in our filebucket.
Puppet::Face.define(:file, '0.0.1') do
action :store do |*args|
summary "Store a file in the local filebucket."
arguments "<file>"
returns "Nothing."
examples <<-EOT
Store a file:
$ puppet file store /root/.bashrc
EOT
when_invoked do |path, options|
- file = Puppet::FileBucket::File.new(Puppet::FileSystem::File.new(path).binread)
+ file = Puppet::FileBucket::File.new(Puppet::FileSystem.binread(path))
Puppet::FileBucket::File.indirection.terminus_class = :file
Puppet::FileBucket::File.indirection.save file
file.checksum
end
end
end
diff --git a/lib/puppet/face/help.rb b/lib/puppet/face/help.rb
index 44380ab30..e9dfe9037 100644
--- a/lib/puppet/face/help.rb
+++ b/lib/puppet/face/help.rb
@@ -1,193 +1,194 @@
require 'puppet/face'
require 'puppet/application/face_base'
require 'puppet/util/constant_inflector'
require 'pathname'
require 'erb'
Puppet::Face.define(:help, '0.0.1') do
copyright "Puppet Labs", 2011
license "Apache 2 license; see COPYING"
summary "Display Puppet help."
action(:help) do
summary "Display help about Puppet subcommands and their actions."
arguments "[<subcommand>] [<action>]"
returns "Short help text for the specified subcommand or action."
examples <<-'EOT'
Get help for an action:
$ puppet help
EOT
option "--version VERSION" do
summary "The version of the subcommand for which to show help."
end
default
when_invoked do |*args|
# Check our invocation, because we want varargs and can't do defaults
# yet. REVISIT: when we do option defaults, and positional options, we
# should rewrite this to use those. --daniel 2011-04-04
options = args.pop
if options.nil? or args.length > 2 then
if args.select { |x| x == 'help' }.length > 2 then
c = "\n %'(),-./=ADEFHILORSTUXY\\_`gnv|".split('')
i = <<-'EOT'.gsub(/\s*/, '').to_i(36)
3he6737w1aghshs6nwrivl8mz5mu9nywg9tbtlt081uv6fq5kvxse1td3tj1wvccmte806nb
cy6de2ogw0fqjymbfwi6a304vd56vlq71atwmqsvz3gpu0hj42200otlycweufh0hylu79t3
gmrijm6pgn26ic575qkexyuoncbujv0vcscgzh5us2swklsp5cqnuanlrbnget7rt3956kam
j8adhdrzqqt9bor0cv2fqgkloref0ygk3dekiwfj1zxrt13moyhn217yy6w4shwyywik7w0l
xtuevmh0m7xp6eoswin70khm5nrggkui6z8vdjnrgdqeojq40fya5qexk97g4d8qgw0hvokr
pli1biaz503grqf2ycy0ppkhz1hwhl6ifbpet7xd6jjepq4oe0ofl575lxdzjeg25217zyl4
nokn6tj5pq7gcdsjre75rqylydh7iia7s3yrko4f5ud9v8hdtqhu60stcitirvfj6zphppmx
7wfm7i9641d00bhs44n6vh6qvx39pg3urifgr6ihx3e0j1ychzypunyou7iplevitkyg6gbg
wm08oy1rvogcjakkqc1f7y1awdfvlb4ego8wrtgu9vzw4vmj59utwifn2ejcs569dh1oaavi
sc581n7jjg1dugzdu094fdobtx6rsvk3sfctvqnr36xctold
EOT
353.times{i,x=i.divmod(1184);a,b=x.divmod(37);print(c[a]*b)}
end
raise ArgumentError, "Puppet help only takes two (optional) arguments: a subcommand and an action"
end
version = :current
if options.has_key? :version then
if options[:version].to_s !~ /^current$/i then
version = options[:version]
else
if args.length == 0 then
raise ArgumentError, "Version only makes sense when a Faces subcommand is given"
end
end
end
return erb('global.erb').result(binding) if args.empty?
facename, actionname = args
if legacy_applications.include? facename then
if actionname then
raise ArgumentError, "Legacy subcommands don't take actions"
end
return render_application_help(facename)
else
return render_face_help(facename, actionname, version)
end
end
end
def render_application_help(applicationname)
return Puppet::Application[applicationname].help
end
def render_face_help(facename, actionname, version)
face, action = load_face_help(facename, actionname, version)
return template_for(face, action).result(binding)
end
def load_face_help(facename, actionname, version)
begin
face = Puppet::Face[facename.to_sym, version]
rescue Puppet::Error => detail
- fail ArgumentError, <<-MSG
+ msg = <<-MSG
Could not load help for the face #{facename}.
Please check the error logs for more information.
Detail: "#{detail.message}"
MSG
+ fail ArgumentError, msg, detail.backtrace
end
if actionname
action = face.get_action(actionname.to_sym)
if not action
fail ArgumentError, "Unable to load action #{actionname} from #{face}"
end
end
[face, action]
end
def template_for(face, action)
if action.nil?
erb('face.erb')
else
erb('action.erb')
end
end
def erb(name)
template = (Pathname(__FILE__).dirname + "help" + name)
erb = ERB.new(template.read, nil, '-')
erb.filename = template.to_s
return erb
end
# Return a list of applications that are not simply just stubs for Faces.
def legacy_applications
Puppet::Application.available_application_names.reject do |appname|
(is_face_app?(appname)) or (exclude_from_docs?(appname))
end.sort
end
# Return a list of all applications (both legacy and Face applications), along with a summary
# of their functionality.
# @return [Array] An Array of Arrays. The outer array contains one entry per application; each
# element in the outer array is a pair whose first element is a String containing the application
# name, and whose second element is a String containing the summary for that application.
def all_application_summaries()
Puppet::Application.available_application_names.sort.inject([]) do |result, appname|
next result if exclude_from_docs?(appname)
if (is_face_app?(appname))
begin
face = Puppet::Face[appname, :current]
result << [appname, face.summary]
rescue Puppet::Error
result << [ "! #{appname}", "! Subcommand unavailable due to error. Check error logs." ]
end
else
result << [appname, horribly_extract_summary_from(appname)]
end
end
end
def horribly_extract_summary_from(appname)
begin
help = Puppet::Application[appname].help.split("\n")
# Now we find the line with our summary, extract it, and return it. This
# depends on the implementation coincidence of how our pages are
# formatted. If we can't match the pattern we expect we return the empty
# string to ensure we don't blow up in the summary. --daniel 2011-04-11
while line = help.shift do
if md = /^puppet-#{appname}\([^\)]+\) -- (.*)$/.match(line) then
return md[1]
end
end
rescue Exception
# Damn, but I hate this: we just ignore errors here, no matter what
# class they are. Meh.
end
return ''
end
# This should absolutely be a private method, but for some reason it appears
# that you can't use the 'private' keyword inside of a Face definition.
# See #14205.
#private :horribly_extract_summary_from
def exclude_from_docs?(appname)
%w{face_base indirection_base}.include? appname
end
# This should absolutely be a private method, but for some reason it appears
# that you can't use the 'private' keyword inside of a Face definition.
# See #14205.
#private :exclude_from_docs?
def is_face_app?(appname)
clazz = Puppet::Application.find(appname)
clazz.ancestors.include?(Puppet::Application::FaceBase)
end
# This should probably be a private method, but for some reason it appears
# that you can't use the 'private' keyword inside of a Face definition.
# See #14205.
#private :is_face_app?
end
diff --git a/lib/puppet/face/help/action.erb b/lib/puppet/face/help/action.erb
index 2c4983ef1..bdac24300 100644
--- a/lib/puppet/face/help/action.erb
+++ b/lib/puppet/face/help/action.erb
@@ -1,85 +1,86 @@
+<%# encoding: UTF-8%>
<% if action.synopsis -%>
USAGE: <%= action.synopsis %>
<% end -%>
<%= action.short_description || action.summary || face.summary || "undocumented subcommand" %>
<% if action.returns -%>
RETURNS: <%= action.returns.strip %>
<% end -%>
OPTIONS:
<%# Remove these options once we can introspect them normally. -%>
--render-as FORMAT - The rendering format to use.
--verbose - Whether to log verbosely.
--debug - Whether to log debug information.
<% optionroom = 30
summaryroom = 80 - 5 - optionroom
disp_glob_opts = action.display_global_options.uniq
unless disp_glob_opts.empty?
disp_glob_opts.sort.each do |name|
option = name
desc = Puppet.settings.setting(option).desc
type = Puppet.settings.setting(option).default
type ||= Puppet.settings.setting(option).type.to_s.upcase -%>
<%= "--#{option} #{type}".ljust(optionroom) + ' - ' -%>
<% if !(desc) -%>
undocumented option
<% elsif desc.length <= summaryroom -%>
<%= desc %>
<%
else
words = desc.split
wrapped = ['']
i = 0
words.each do |word|
if wrapped[i].length + word.length <= summaryroom
wrapped[i] << word + ' '
else
i += 1
wrapped[i] = word + ' '
end
end -%>
<%= wrapped.shift.strip %>
<% wrapped.each do |line| -%>
<%= (' ' * (optionroom + 5) ) + line.strip %>
<% end
end
end
end
unless action.options.empty?
action.options.sort.each do |name|
option = action.get_option name -%>
<%= " " + option.optparse.join(" | ")[0,(optionroom - 1)].ljust(optionroom) + ' - ' -%>
<% if !(option.summary) -%>
undocumented option
<% elsif option.summary.length <= summaryroom -%>
<%= option.summary %>
<%
else
words = option.summary.split
wrapped = ['']
i = 0
words.each do |word|
if wrapped[i].length + word.length <= summaryroom
wrapped[i] << word + ' '
else
i += 1
wrapped[i] = word + ' '
end
end
-%>
<%= wrapped.shift.strip %>
<% wrapped.each do |line| -%>
<%= (' ' * (optionroom + 5) ) + line.strip %>
<% end
end
end -%>
<% end -%>
<% if face.respond_to? :indirection -%>
TERMINI: <%= face.class.terminus_classes(face.indirection.name).join(", ") %>
<% end -%>
See 'puppet man <%= face.name %>' or 'man puppet-<%= face.name %>' for full help.
diff --git a/lib/puppet/face/help/face.erb b/lib/puppet/face/help/face.erb
index caf84f02c..becdb049c 100644
--- a/lib/puppet/face/help/face.erb
+++ b/lib/puppet/face/help/face.erb
@@ -1,110 +1,111 @@
+<%# encoding: UTF-8%>
<% if face.synopsis -%>
USAGE: <%= face.synopsis %>
<% end -%>
<%= (face.short_description || face.summary || "undocumented subcommand").strip %>
OPTIONS:
<%# Remove these options once we can introspect them normally. -%>
--render-as FORMAT - The rendering format to use.
--verbose - Whether to log verbosely.
--debug - Whether to log debug information.
<% optionroom = 30
summaryroom = 80 - 5 - optionroom
disp_glob_opts = face.display_global_options.uniq
unless disp_glob_opts.empty?
disp_glob_opts.sort.each do |name|
option = name
desc = Puppet.settings.setting(option).desc
type = Puppet.settings.setting(option).default
type ||= Puppet.settings.setting(option).type.to_s.upcase -%>
<%= "--#{option} #{type}".ljust(optionroom) + ' - ' -%>
<% if !(desc) -%>
undocumented option
<% elsif desc.length <= summaryroom -%>
<%= desc %>
<% else
words = desc.split
wrapped = ['']
i = 0
words.each do |word|
if wrapped[i].length + word.length <= summaryroom
wrapped[i] << word + ' '
else
i += 1
wrapped[i] = word + ' '
end
end -%>
<%= wrapped.shift.strip %>
<% wrapped.each do |line| -%>
<%= (' ' * (optionroom + 5) ) + line.strip %>
<% end
end
end
end
unless face.options.empty?
face.options.sort.each do |name|
option = face.get_option name -%>
<%= " " + option.optparse.join(" | ")[0,(optionroom - 1)].ljust(optionroom) + ' - ' -%>
<% if !(option.summary) -%>
undocumented option
<% elsif option.summary.length <= summaryroom -%>
<%= option.summary %>
<%
else
words = option.summary.split
wrapped = ['']
i = 0
words.each do |word|
if wrapped[i].length + word.length <= summaryroom
wrapped[i] << word + ' '
else
i += 1
wrapped[i] = word + ' '
end
end
-%>
<%= wrapped.shift.strip %>
<% wrapped.each do |line| -%>
<%= (' ' * (optionroom + 5) ) + line.strip %>
<% end
end
end -%>
<% end -%>
ACTIONS:
<% padding = face.actions.map{|x| x.to_s.length}.max + 2
summaryroom = 80 - (padding + 4)
face.actions.each do |actionname|
action = face.get_action(actionname) -%>
<%= action.name.to_s.ljust(padding) + ' ' -%>
<% if !(action.summary) -%>
undocumented action
<% elsif action.summary.length <= summaryroom -%>
<%= action.summary %>
<% else
words = action.summary.split
wrapped = ['']
i = 0
words.each do |word|
if wrapped[i].length + word.length <= summaryroom
wrapped[i] << word + ' '
else
i += 1
wrapped[i] = word + ' '
end
end
-%>
<%= wrapped.shift.strip %>
<% wrapped.each do |line| -%>
<%= (' ' * (padding + 4) ) + line.strip %>
<% end
end
end -%>
<% if face.respond_to? :indirection -%>
TERMINI: <%= face.class.terminus_classes(face.indirection.name).join(", ") %>
<% end -%>
See 'puppet man <%= face.name %>' or 'man puppet-<%= face.name %>' for full help.
diff --git a/lib/puppet/face/help/global.erb b/lib/puppet/face/help/global.erb
index 6333ddcc4..9756ea5e3 100644
--- a/lib/puppet/face/help/global.erb
+++ b/lib/puppet/face/help/global.erb
@@ -1,15 +1,16 @@
+<%# encoding: UTF-8%>
Usage: puppet <subcommand> [options] <action> [options]
Available subcommands:
<%# NOTE: this is probably not a good long-term solution for this. We're only iterating over
applications to find the list of things we need to show help for... this works for now
because faces can't be run without an application stub. However, when #6753 is resolved,
all of the application stubs for faces will go away, and this will need to be updated
to reflect that. --cprice 2012-04-26 %>
<% all_application_summaries.each do |appname, summary| -%>
<%= appname.to_s.ljust(16) %> <%= summary %>
<% end -%>
See 'puppet help <subcommand> <action>' for help on a specific subcommand action.
See 'puppet help <subcommand>' for help on a specific subcommand.
Puppet v<%= Puppet.version %>
diff --git a/lib/puppet/face/help/man.erb b/lib/puppet/face/help/man.erb
index bd584889c..e835da0a9 100644
--- a/lib/puppet/face/help/man.erb
+++ b/lib/puppet/face/help/man.erb
@@ -1,151 +1,152 @@
+<%# encoding: UTF-8%>
puppet-<%= face.name %>(8) -- <%= face.summary || "Undocumented subcommand." %>
<%= '=' * (_erbout.length - 1) %>
<% if face.synopsis -%>
SYNOPSIS
--------
<%= face.synopsis %>
<% end
if face.description -%>
DESCRIPTION
-----------
<%= face.description.strip %>
<% end -%>
OPTIONS
-------
-Note that any configuration parameter that's valid in the configuration
+Note that any setting that's valid in the configuration
file is also a valid long argument, although it may or may not be
relevant to the present action. For example, `server` and `run_mode` are valid
-configuration parameters, so you can specify `--server <servername>`, or
+settings, so you can specify `--server <servername>`, or
`--run_mode <runmode>` as an argument.
See the configuration file documentation at
<http://docs.puppetlabs.com/references/stable/configuration.html> for the
full list of acceptable parameters. A commented list of all
configuration options can also be generated by running puppet with
`--genconfig`.
* --render-as FORMAT:
The format in which to render output. The most common formats are `json`,
`s` (string), `yaml`, and `console`, but other options such as `dot` are
sometimes available.
* --verbose:
Whether to log verbosely.
* --debug:
Whether to log debug information.
<% unless face.display_global_options.empty?
face.display_global_options.uniq.sort.each do |name|
option = name
- desc = Puppet.settings.setting(option).desc
+ desc = Puppet::Util::Docs.scrub(Puppet.settings.setting(option).desc)
type = Puppet.settings.setting(option).default
type ||= Puppet.settings.setting(option).type.to_s.upcase -%>
<%= "* --#{option} #{type}" %>:
-<%= desc.gsub(/^/, ' ') || ' undocumented setting' %>
+<%= (desc || 'Undocumented setting.').gsub(/^/, ' ') %>
<% end
end -%>
<% unless face.options.empty?
face.options.sort.each do |name|
option = face.get_option name -%>
<%= "* " + option.optparse.join(" | " ) %>:
-<%= option.description.gsub(/^/, ' ') || ' ' + option.summary %>
+<%= (option.description || option.summary || "Undocumented option.").gsub(/^/, ' ') %>
<% end
end -%>
ACTIONS
-------
<% face.actions.each do |actionname|
action = face.get_action(actionname) -%>
* `<%= action.name.to_s %>` - <%= action.summary %>:
<% if action.synopsis -%>
`SYNOPSIS`
<%= action.synopsis %>
<% end -%>
`DESCRIPTION`
<% if action.description -%>
<%= action.description.gsub(/^/, ' ') %>
<% else -%>
<%= action.summary || "Undocumented action." %>
<% end -%>
<% unique_options = action.options - face.options
unique_display_global_options = action.display_global_options - face.display_global_options
unless unique_options.empty? and unique_display_global_options.empty? -%>
`OPTIONS`
<% unique_display_global_options.uniq.sort.each do |name|
option = name
- desc = Puppet.settings.setting(option).desc
+ desc = Puppet::Util::Docs.scrub(Puppet.settings.setting(option).desc)
type = Puppet.settings.setting(option).default
type ||= Puppet.settings.setting(option).type.to_s.upcase -%>
<%= "<--#{option} #{type}>" %> -
-<%= desc.gsub(/^/, ' ') %>
+<%= (desc || "Undocumented setting.").gsub(/^/, ' ') %>
<% end -%>
<% unique_options.sort.each do |name|
option = action.get_option name
- text = (option.description || option.summary).chomp + "\n" -%>
+ text = (option.description || option.summary || "Undocumented option.").chomp + "\n" -%>
<%= '<' + option.optparse.join("> | <") + '>' %> -
<%= text.gsub(/^/, ' ') %>
<% end -%>
<% end -%>
<% if action.returns -%>
`RETURNS`
<%= action.returns.gsub(/^/, ' ') %>
<% end
if action.notes -%>
`NOTES`
<%= action.notes.gsub(/^/, ' ') %>
<% end
end
if face.examples or face.actions.any? {|actionname| face.get_action(actionname).examples} -%>
EXAMPLES
--------
<% end
if face.examples -%>
<%= face.examples %>
<% end
face.actions.each do |actionname|
action = face.get_action(actionname)
if action.examples -%>
`<%= action.name.to_s %>`
<%= action.examples.strip %>
<% end
end -%>
<% if face.notes or face.respond_to? :indirection -%>
NOTES
-----
<% if face.notes -%>
<%= face.notes.strip %>
<% end # notes
if face.respond_to? :indirection -%>
This subcommand is an indirector face, which exposes `find`, `search`, `save`,
and `destroy` actions for an indirected subsystem of Puppet. Valid termini for
this face include:
* `<%= face.class.terminus_classes(face.indirection.name).join("`\n* `") %>`
<% end # indirection
end # notes or indirection
unless face.authors.empty? -%>
AUTHOR
------
<%= face.authors.join("\n").gsub(/^/, ' * ') %>
<% end -%>
COPYRIGHT AND LICENSE
---------------------
<%= face.copyright %>
<%= face.license %>
diff --git a/lib/puppet/face/module/list.rb b/lib/puppet/face/module/list.rb
index 83523c78b..18c76709f 100644
--- a/lib/puppet/face/module/list.rb
+++ b/lib/puppet/face/module/list.rb
@@ -1,268 +1,274 @@
# encoding: UTF-8
Puppet::Face.define(:module, '1.0.0') do
action(:list) do
summary "List installed modules"
description <<-HEREDOC
Lists the installed puppet modules. By default, this action scans the
modulepath from puppet.conf's `[main]` block; use the --modulepath
option to change which directories are scanned.
The output of this action includes information from the module's
metadata, including version numbers and unmet module dependencies.
HEREDOC
returns "hash of paths to module objects"
option "--tree" do
summary "Whether to show dependencies as a tree view"
end
examples <<-'EOT'
List installed modules:
$ puppet module list
/etc/puppet/modules
├── bodepd-create_resources (v0.0.1)
├── puppetlabs-bacula (v0.0.2)
├── puppetlabs-mysql (v0.0.1)
├── puppetlabs-sqlite (v0.0.1)
└── puppetlabs-stdlib (v2.2.1)
/usr/share/puppet/modules (no modules installed)
List installed modules in a tree view:
$ puppet module list --tree
/etc/puppet/modules
└─┬ puppetlabs-bacula (v0.0.2)
├── puppetlabs-stdlib (v2.2.1)
├─┬ puppetlabs-mysql (v0.0.1)
│ └── bodepd-create_resources (v0.0.1)
└── puppetlabs-sqlite (v0.0.1)
/usr/share/puppet/modules (no modules installed)
List installed modules from a specified environment:
$ puppet module list --environment production
/etc/puppet/modules
├── bodepd-create_resources (v0.0.1)
├── puppetlabs-bacula (v0.0.2)
├── puppetlabs-mysql (v0.0.1)
├── puppetlabs-sqlite (v0.0.1)
└── puppetlabs-stdlib (v2.2.1)
/usr/share/puppet/modules (no modules installed)
List installed modules from a specified modulepath:
$ puppet module list --modulepath /usr/share/puppet/modules
/usr/share/puppet/modules (no modules installed)
EOT
when_invoked do |options|
- Puppet[:modulepath] = options[:modulepath] if options[:modulepath]
- environment = Puppet::Node::Environment.new(options[:environment])
-
- environment.modules_by_path
+ environment_from_options(options).modules_by_path
end
when_rendering :console do |modules_by_path, options|
output = ''
-
- Puppet[:modulepath] = options[:modulepath] if options[:modulepath]
- environment = Puppet::Node::Environment.new(options[:environment])
+ environment = environment_from_options(options)
error_types = {
:non_semantic_version => {
:title => "Non semantic version dependency"
},
:missing => {
:title => "Missing dependency"
},
:version_mismatch => {
:title => "Module '%s' (v%s) fails to meet some dependencies:"
}
}
@unmet_deps = {}
error_types.each_key do |type|
@unmet_deps[type] = Hash.new do |hash, key|
hash[key] = { :errors => [], :parent => nil }
end
end
# Prepare the unmet dependencies for display on the console.
environment.modules.sort_by {|mod| mod.name}.each do |mod|
unmet_grouped = Hash.new { |h,k| h[k] = [] }
unmet_grouped = mod.unmet_dependencies.inject(unmet_grouped) do |acc, dep|
acc[dep[:reason]] << dep
acc
end
unmet_grouped.each do |type, deps|
unless deps.empty?
unmet_grouped[type].sort_by { |dep| dep[:name] }.each do |dep|
dep_name = dep[:name].gsub('/', '-')
installed_version = dep[:mod_details][:installed_version]
version_constraint = dep[:version_constraint]
parent_name = dep[:parent][:name].gsub('/', '-')
parent_version = dep[:parent][:version]
msg = "'#{parent_name}' (#{parent_version})"
msg << " requires '#{dep_name}' (#{version_constraint})"
@unmet_deps[type][dep[:name]][:errors] << msg
@unmet_deps[type][dep[:name]][:parent] = {
:name => dep[:parent][:name],
:version => parent_version
}
@unmet_deps[type][dep[:name]][:version] = installed_version
end
end
end
end
# Display unmet dependencies by category.
error_display_order = [:non_semantic_version, :version_mismatch, :missing]
error_display_order.each do |type|
unless @unmet_deps[type].empty?
@unmet_deps[type].keys.sort_by {|dep| dep }.each do |dep|
name = dep.gsub('/', '-')
title = error_types[type][:title]
errors = @unmet_deps[type][dep][:errors]
version = @unmet_deps[type][dep][:version]
msg = case type
when :version_mismatch
title % [name, version] + "\n"
when :non_semantic_version
title + " '#{name}' (v#{version}):\n"
else
title + " '#{name}':\n"
end
errors.each { |error_string| msg << " #{error_string}\n" }
Puppet.warning msg.chomp
end
end
end
environment.modulepath.each do |path|
modules = modules_by_path[path]
no_mods = modules.empty? ? ' (no modules installed)' : ''
output << "#{path}#{no_mods}\n"
if options[:tree]
# The modules with fewest things depending on them will be the
# parent of the tree. Can't assume to start with 0 dependencies
# since dependencies may be cyclical.
modules_by_num_requires = modules.sort_by {|m| m.required_by.size}
@seen = {}
tree = list_build_tree(modules_by_num_requires, [], nil,
:label_unmet => true, :path => path, :label_invalid => false)
else
tree = []
modules.sort_by { |mod| mod.forge_name or mod.name }.each do |mod|
tree << list_build_node(mod, path, :label_unmet => false,
:path => path, :label_invalid => true)
end
end
output << Puppet::ModuleTool.format_tree(tree)
end
output
end
end
+ def environment_from_options(options)
+ environments = Puppet.lookup(:environments)
+ if options[:modulepath]
+ environments.for(options[:modulepath], '')
+ elsif options[:environment]
+ environments.get(options[:environment])
+ else
+ environments.get(Puppet[:environment])
+ end
+ end
+
# Prepare a list of module objects and their dependencies for print in a
# tree view.
#
# Returns an Array of Hashes
#
# Example:
#
# [
# {
# :text => "puppetlabs-bacula (v0.0.2)",
# :dependencies=> [
# { :text => "puppetlabs-stdlib (v2.2.1)", :dependencies => [] },
# {
# :text => "puppetlabs-mysql (v1.0.0)"
# :dependencies => [
# {
# :text => "bodepd-create_resources (v0.0.1)",
# :dependencies => []
# }
# ]
# },
# { :text => "puppetlabs-sqlite (v0.0.1)", :dependencies => [] },
# ]
# }
# ]
#
# When the above data structure is passed to Puppet::ModuleTool.build_tree
# you end up with something like this:
#
# /etc/puppet/modules
# └─┬ puppetlabs-bacula (v0.0.2)
# ├── puppetlabs-stdlib (v2.2.1)
# ├─┬ puppetlabs-mysql (v1.0.0)
# │ └── bodepd-create_resources (v0.0.1)
# └── puppetlabs-sqlite (v0.0.1)
#
def list_build_tree(list, ancestors=[], parent=nil, params={})
list.map do |mod|
next if @seen[(mod.forge_name or mod.name)]
node = list_build_node(mod, parent, params)
@seen[(mod.forge_name or mod.name)] = true
unless ancestors.include?(mod)
node[:dependencies] ||= []
missing_deps = mod.unmet_dependencies.select do |dep|
dep[:reason] == :missing
end
missing_deps.map do |mis_mod|
str = "#{colorize(:bg_red, 'UNMET DEPENDENCY')} #{mis_mod[:name].gsub('/', '-')} "
str << "(#{colorize(:cyan, mis_mod[:version_constraint])})"
node[:dependencies] << { :text => str }
end
node[:dependencies] += list_build_tree(mod.dependencies_as_modules,
ancestors + [mod], mod, params)
end
node
end.compact
end
# Prepare a module object for print in a tree view. Each node in the tree
# must be a Hash in the following format:
#
# { :text => "puppetlabs-mysql (v1.0.0)" }
#
# The value of a module's :text is affected by three (3) factors: the format
# of the tree, its dependency status, and the location in the modulepath
# relative to its parent.
#
# Returns a Hash
#
def list_build_node(mod, parent, params)
str = ''
str << (mod.forge_name ? mod.forge_name.gsub('/', '-') : mod.name)
str << ' (' + colorize(:cyan, mod.version ? "v#{mod.version}" : '???') + ')'
unless File.dirname(mod.path) == params[:path]
str << " [#{File.dirname(mod.path)}]"
end
if @unmet_deps[:version_mismatch].include?(mod.forge_name)
if params[:label_invalid]
str << ' ' + colorize(:red, 'invalid')
elsif parent.respond_to?(:forge_name)
unmet_parent = @unmet_deps[:version_mismatch][mod.forge_name][:parent]
if (unmet_parent[:name] == parent.forge_name &&
unmet_parent[:version] == "v#{parent.version}")
str << ' ' + colorize(:red, 'invalid')
end
end
end
{ :text => str }
end
end
diff --git a/lib/puppet/face/node/clean.rb b/lib/puppet/face/node/clean.rb
index 20d454e8a..903e93819 100644
--- a/lib/puppet/face/node/clean.rb
+++ b/lib/puppet/face/node/clean.rb
@@ -1,159 +1,159 @@
Puppet::Face.define(:node, '0.0.1') do
action(:clean) do
option "--[no-]unexport" do
summary "Whether to remove this node's exported resources from other nodes"
end
summary "Clean up everything a puppetmaster knows about a node."
arguments "<host1> [<host2> ...]"
description <<-'EOT'
Clean up everything a puppet master knows about a node, including certificates
and storeconfigs data.
The full list of info cleaned by this action is:
<Signed certificates> - ($vardir/ssl/ca/signed/node.domain.pem)
<Cached facts> - ($vardir/yaml/facts/node.domain.yaml)
<Cached node objects> - ($vardir/yaml/node/node.domain.yaml)
<Reports> - ($vardir/reports/node.domain)
<Stored configs> - (in database) The clean action can either remove all
data from a host in your storeconfigs database, or, with the
<--unexport> option, turn every exported resource supporting ensure to
absent so that any other host that collected those resources can remove
them. Without unexporting, a removed node's exported resources become
unmanaged by Puppet, and may linger as cruft unless you are purging
that resource type.
EOT
when_invoked do |*args|
nodes = args[0..-2]
options = args.last
raise "At least one node should be passed" if nodes.empty? || nodes == options
# This seems really bad; run_mode should be set as part of a class
# definition, and should not be modifiable beyond that. This is one of
# the only places left in the code that tries to manipulate it. Other
- # parts of code that handle certificates behave differently if the the
+ # parts of code that handle certificates behave differently if the
# run_mode is master. Those other behaviors are needed for cleaning the
# certificates correctly.
Puppet.settings.preferred_run_mode = "master"
if Puppet::SSL::CertificateAuthority.ca?
Puppet::SSL::Host.ca_location = :local
else
Puppet::SSL::Host.ca_location = :none
end
Puppet::Node::Facts.indirection.terminus_class = :yaml
Puppet::Node::Facts.indirection.cache_class = :yaml
Puppet::Node.indirection.terminus_class = :yaml
Puppet::Node.indirection.cache_class = :yaml
nodes.each { |node| cleanup(node.downcase, options[:unexport]) }
end
end
def cleanup(node, unexport)
clean_cert(node)
clean_cached_facts(node)
clean_cached_node(node)
clean_reports(node)
clean_storeconfigs(node, unexport)
end
# clean signed cert for +host+
def clean_cert(node)
if Puppet::SSL::CertificateAuthority.ca?
Puppet::Face[:ca, :current].revoke(node)
Puppet::Face[:ca, :current].destroy(node)
Puppet.info "#{node} certificates removed from ca"
else
Puppet.info "Not managing #{node} certs as this host is not a CA"
end
end
# clean facts for +host+
def clean_cached_facts(node)
Puppet::Node::Facts.indirection.destroy(node)
Puppet.info "#{node}'s facts removed"
end
# clean cached node +host+
def clean_cached_node(node)
Puppet::Node.indirection.destroy(node)
Puppet.info "#{node}'s cached node removed"
end
# clean node reports for +host+
def clean_reports(node)
Puppet::Transaction::Report.indirection.destroy(node)
Puppet.info "#{node}'s reports removed"
end
# clean storeconfig for +node+
def clean_storeconfigs(node, do_unexport=false)
return unless Puppet[:storeconfigs] && Puppet.features.rails?
require 'puppet/rails'
Puppet::Rails.connect
unless rails_node = Puppet::Rails::Host.find_by_name(node)
Puppet.notice "No entries found for #{node} in storedconfigs."
return
end
if do_unexport
unexport(rails_node)
Puppet.notice "Force #{node}'s exported resources to absent"
Puppet.warning "Please wait until all other hosts have checked out their configuration before finishing the cleanup with:"
Puppet.warning "$ puppet node clean #{node}"
else
rails_node.destroy
Puppet.notice "#{node} storeconfigs removed"
end
end
def unexport(node)
# fetch all exported resource
query = {:include => {:param_values => :param_name}}
query[:conditions] = [ "exported=? AND host_id=?", true, node.id ]
Puppet::Rails::Resource.find(:all, query).each do |resource|
if type_is_ensurable(resource)
line = 0
param_name = Puppet::Rails::ParamName.find_or_create_by_name("ensure")
if ensure_param = resource.param_values.find(
:first,
:conditions => [ 'param_name_id = ?', param_name.id ]
)
line = ensure_param.line.to_i
Puppet::Rails::ParamValue.delete(ensure_param.id);
end
# force ensure parameter to "absent"
resource.param_values.create(
:value => "absent",
:line => line,
:param_name => param_name
)
Puppet.info("#{resource.name} has been marked as \"absent\"")
end
end
end
def environment
- @environment ||= Puppet::Node::Environment.new
+ @environment ||= Puppet.lookup(:environments).get(Puppet[:environment])
end
def type_is_ensurable(resource)
if (type = Puppet::Type.type(resource.restype)) && type.validattr?(:ensure)
return true
else
type = environment.known_resource_types.find_definition('', resource.restype)
return true if type && type.arguments.keys.include?('ensure')
end
return false
end
end
diff --git a/lib/puppet/face/parser.rb b/lib/puppet/face/parser.rb
index e42919091..2950388e2 100644
--- a/lib/puppet/face/parser.rb
+++ b/lib/puppet/face/parser.rb
@@ -1,61 +1,67 @@
require 'puppet/face'
require 'puppet/parser'
Puppet::Face.define(:parser, '0.0.1') do
copyright "Puppet Labs", 2011
license "Apache 2 license; see COPYING"
summary "Interact directly with the parser."
action :validate do
summary "Validate the syntax of one or more Puppet manifests."
arguments "[<manifest>] [<manifest> ...]"
returns "Nothing, or the first syntax error encountered."
description <<-'EOT'
This action validates Puppet DSL syntax without compiling a catalog or
syncing any resources. If no manifest files are provided, it will
validate the default site manifest.
EOT
examples <<-'EOT'
Validate the default site manifest at /etc/puppet/manifests/site.pp:
$ puppet parser validate
Validate two arbitrary manifest files:
$ puppet parser validate init.pp vhost.pp
Validate from STDIN:
$ cat init.pp | puppet parser validate
EOT
when_invoked do |*args|
args.pop
files = args
if files.empty?
if not STDIN.tty?
Puppet[:code] = STDIN.read
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
- validate_manifest
+ missing_files << file if ! Puppet::FileSystem.exist?(file)
+ validate_manifest(file)
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
+ # @api private
+ def validate_manifest(manifest = nil)
+ configured_environment = Puppet.lookup(:environments).get(Puppet[:environment])
+ validation_environment = manifest ?
+ configured_environment.override_with(:manifest => manifest) :
+ configured_environment
+
+ validation_environment.known_resource_types.clear
+
rescue => detail
Puppet.log_exception(detail)
exit(1)
end
end
diff --git a/lib/puppet/face/status.rb b/lib/puppet/face/status.rb
index e8c87e98d..ffc44a396 100644
--- a/lib/puppet/face/status.rb
+++ b/lib/puppet/face/status.rb
@@ -1,53 +1,53 @@
require 'puppet/indirector/face'
Puppet::Indirector::Face.define(:status, '0.0.1') do
copyright "Puppet Labs", 2011
license "Apache 2 license; see COPYING"
summary "View puppet server status."
get_action(:destroy).summary "Invalid for this subcommand."
get_action(:save).summary "Invalid for this subcommand."
get_action(:save).description "Invalid for this subcommand."
get_action(:search).summary "Invalid for this subcommand."
find = get_action(:find)
find.default = true
find.summary "Check status of puppet master server."
find.arguments "<dummy_text>"
find.returns <<-'EOT'
A "true" response or a low-level connection error. When used from the Ruby
API: returns a Puppet::Status object.
EOT
find.description <<-'EOT'
Checks whether a Puppet server is properly receiving and processing
HTTP requests. This action is only useful when used with '--terminus
rest'; when invoked with the `local` terminus, `find` will always
return true.
Over REST, this action will query the configured puppet master by default.
To query other servers, including puppet agent nodes started with the
- <--listen> option, you can set set the global <--server> and <--masterport>
+ <--listen> option, you can set the global <--server> and <--masterport>
options on the command line; note that agent nodes listen on port 8139.
EOT
find.short_description <<-EOT
Checks whether a Puppet server is properly receiving and processing HTTP
requests. Due to a known bug, this action requires a dummy argument, the
content of which is irrelevant. This action is only useful when used with
'--terminus rest', and will always return true when invoked locally.
EOT
find.notes <<-'EOT'
This action requires that the server's `auth.conf` file allow find
access to the `status` REST terminus. Puppet agent does not use this
facility, and it is turned off by default. See
<http://docs.puppetlabs.com/guides/rest_auth_conf.html> for more details.
Although this action always returns an unnamed status object, it requires a
dummy argument. This is a known bug.
EOT
find.examples <<-'EOT'
Check the status of the configured puppet master:
$ puppet status find x --terminus rest
EOT
end
diff --git a/lib/puppet/feature/external_facts.rb b/lib/puppet/feature/external_facts.rb
index 1e2ad575e..fb0b4145e 100644
--- a/lib/puppet/feature/external_facts.rb
+++ b/lib/puppet/feature/external_facts.rb
@@ -1,5 +1,5 @@
-require 'facter/util/config'
+require 'facter'
Puppet.features.add(:external_facts) {
- Facter::Util::Config.respond_to?(:external_facts_dirs=)
+ Facter.respond_to?(:search_external)
}
diff --git a/lib/puppet/feature/libuser.rb b/lib/puppet/feature/libuser.rb
index 29a745de4..5d8f8685d 100644
--- a/lib/puppet/feature/libuser.rb
+++ b/lib/puppet/feature/libuser.rb
@@ -1,8 +1,8 @@
require 'puppet/util/feature'
require 'puppet/util/libuser'
Puppet.features.add(:libuser) {
File.executable?("/usr/sbin/lgroupadd") and
File.executable?("/usr/sbin/luseradd") and
- Puppet::FileSystem::File.exist?(Puppet::Util::Libuser.getconf)
+ Puppet::FileSystem.exist?(Puppet::Util::Libuser.getconf)
}
diff --git a/lib/puppet/feature/msgpack.rb b/lib/puppet/feature/msgpack.rb
index 8c7b7f963..3971a5e5b 100644
--- a/lib/puppet/feature/msgpack.rb
+++ b/lib/puppet/feature/msgpack.rb
@@ -1 +1,3 @@
+require 'puppet/util/feature'
+
Puppet.features.add(:msgpack, :libs => ["msgpack"])
diff --git a/lib/puppet/feature/rails.rb b/lib/puppet/feature/rails.rb
index c1537effb..b76b7c187 100644
--- a/lib/puppet/feature/rails.rb
+++ b/lib/puppet/feature/rails.rb
@@ -1,47 +1,47 @@
require 'puppet/util/feature'
Puppet.features.add(:rails) do
begin
# Turn off the constant watching parts of ActiveSupport, which have a huge
# cost in terms of the system watching loaded code to figure out if it was
# a missing content, and which we don't actually *use* anywhere.
#
# In fact, we *can't* depend on the feature: we don't require
# ActiveSupport, just load it if we use rails, if we depend on a feature
# that it offers. --daniel 2012-07-16
require 'active_support'
begin
require 'active_support/dependencies'
ActiveSupport::Dependencies.unhook!
ActiveSupport::Dependencies.mechanism = :require
rescue LoadError, ScriptError, StandardError => e
# ignore any failure - worst case we run without disabling the CPU
# sucking features, so are slower but ... not actually failed, just
# because some random future version of ActiveRecord changes.
Puppet.debug("disabling ActiveSupport::Dependencies failed: #{e}")
end
require 'active_record'
require 'active_record/version'
rescue LoadError
- if Puppet::FileSystem::File.exist?("/usr/share/rails")
+ if Puppet::FileSystem.exist?("/usr/share/rails")
count = 0
Dir.entries("/usr/share/rails").each do |dir|
libdir = File.join("/usr/share/rails", dir, "lib")
- if Puppet::FileSystem::File.exist?(libdir) and ! $LOAD_PATH.include?(libdir)
+ if Puppet::FileSystem.exist?(libdir) and ! $LOAD_PATH.include?(libdir)
count += 1
$LOAD_PATH << libdir
end
end
retry if count > 0
end
end
unless (Puppet::Util.activerecord_version >= 2.1)
Puppet.info "ActiveRecord 2.1 or later required for StoreConfigs"
false
else
true
end
end
diff --git a/lib/puppet/file_bucket/dipper.rb b/lib/puppet/file_bucket/dipper.rb
index 5d4d9382a..d1852f74f 100644
--- a/lib/puppet/file_bucket/dipper.rb
+++ b/lib/puppet/file_bucket/dipper.rb
@@ -1,109 +1,109 @@
require 'pathname'
require 'puppet/file_bucket'
require 'puppet/file_bucket/file'
require 'puppet/indirector/request'
class Puppet::FileBucket::Dipper
# This is a transitional implementation that uses REST
# to access remote filebucket files.
attr_accessor :name
# Create our bucket client
def initialize(hash = {})
# Emulate the XMLRPC client
server = hash[:Server]
port = hash[:Port] || Puppet[:masterport]
environment = Puppet[:environment]
if hash.include?(:Path)
@local_path = hash[:Path]
@rest_path = nil
else
@local_path = nil
@rest_path = "https://#{server}:#{port}/#{environment}/file_bucket_file/"
end
end
def local?
!! @local_path
end
# Back up a file to our bucket
def backup(file)
- file_handle = Puppet::FileSystem::File.new(file)
- raise(ArgumentError, "File #{file} does not exist") unless file_handle.exist?
- contents = file_handle.binread
+ file_handle = Puppet::FileSystem.pathname(file)
+ raise(ArgumentError, "File #{file} does not exist") unless Puppet::FileSystem.exist?(file_handle)
+ contents = Puppet::FileSystem.binread(file_handle)
begin
file_bucket_file = Puppet::FileBucket::File.new(contents, :bucket_path => @local_path)
files_original_path = absolutize_path(file)
dest_path = "#{@rest_path}#{file_bucket_file.name}/#{files_original_path}"
file_bucket_path = "#{@rest_path}#{file_bucket_file.checksum_type}/#{file_bucket_file.checksum_data}/#{files_original_path}"
# Make a HEAD request for the file so that we don't waste time
# uploading it if it already exists in the bucket.
unless Puppet::FileBucket::File.indirection.head(file_bucket_path)
Puppet::FileBucket::File.indirection.save(file_bucket_file, dest_path)
end
return file_bucket_file.checksum_data
rescue => detail
message = "Could not back up #{file}: #{detail}"
Puppet.log_exception(detail, message)
- raise Puppet::Error, message
+ raise Puppet::Error, message, detail.backtrace
end
end
# Retrieve a file by sum.
def getfile(sum)
source_path = "#{@rest_path}md5/#{sum}"
file_bucket_file = Puppet::FileBucket::File.indirection.find(source_path, :bucket_path => @local_path)
raise Puppet::Error, "File not found" unless file_bucket_file
file_bucket_file.to_s
end
# Restore the file
def restore(file,sum)
restore = true
- file_handle = Puppet::FileSystem::File.new(file)
- if file_handle.exist?
- cursum = Digest::MD5.hexdigest(file_handle.binread)
+ file_handle = Puppet::FileSystem.pathname(file)
+ if Puppet::FileSystem.exist?(file_handle)
+ cursum = Digest::MD5.hexdigest(Puppet::FileSystem.binread(file_handle))
# if the checksum has changed...
# this might be extra effort
if cursum == sum
restore = false
end
end
if restore
if newcontents = getfile(sum)
newsum = Digest::MD5.hexdigest(newcontents)
changed = nil
- if file_handle.exist? and ! file_handle.writable?
- changed = Puppet::FileSystem::File.new(file).stat.mode
+ if Puppet::FileSystem.exist?(file_handle) and ! Puppet::FileSystem.writable?(file_handle)
+ changed = Puppet::FileSystem.stat(file_handle).mode
::File.chmod(changed | 0200, file)
end
::File.open(file, ::File::WRONLY|::File::TRUNC|::File::CREAT) { |of|
of.binmode
of.print(newcontents)
}
::File.chmod(changed, file) if changed
else
Puppet.err "Could not find file with checksum #{sum}"
return nil
end
return newsum
else
return nil
end
end
private
def absolutize_path( path )
Pathname.new(path).realpath
end
end
diff --git a/lib/puppet/file_bucket/file.rb b/lib/puppet/file_bucket/file.rb
index 50e5d4d92..3445753cb 100644
--- a/lib/puppet/file_bucket/file.rb
+++ b/lib/puppet/file_bucket/file.rb
@@ -1,88 +1,92 @@
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.
# There are mechanisms to save and load this file locally and remotely in puppet/indirector/filebucketfile/*
# There is a compatibility class that emulates pre-indirector filebuckets in Puppet::FileBucket::Dipper
extend Puppet::Indirector
indirects :file_bucket_file, :terminus_class => :selector
attr :contents
attr :bucket_path
def self.supported_formats
[:s, :pson]
end
def self.default_format
# This should really be :raw, like is done for Puppet::FileServing::Content
# but this class hasn't historically supported `from_raw`, so switching
# would break compatibility between newer 3.x agents talking to older 3.x
# masters. However, to/from_s has been supported and achieves the desired
# result without breaking compatibility.
:s
end
def initialize(contents, options = {})
raise ArgumentError.new("contents must be a String, got a #{contents.class}") unless contents.is_a?(String)
@contents = contents
@bucket_path = options.delete(:bucket_path)
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
def checksum
"{#{checksum_type}}#{checksum_data}"
end
def checksum_data
@checksum_data ||= Digest::MD5.hexdigest(contents)
end
def to_s
contents
end
def name
"#{checksum_type}/#{checksum_data}"
end
def self.from_s(contents)
self.new(contents)
end
+ def to_data_hash
+ { "contents" => contents }
+ end
+
+ def self.from_data_hash(data)
+ self.new(data["contents"])
+ end
+
def to_pson
Puppet.deprecation_warning("Serializing Puppet::FileBucket::File objects to pson is deprecated.")
to_data_hash.to_pson
end
- def to_data_hash
- { "contents" => contents }
- end
-
# This method is deprecated, but cannot be removed for awhile, otherwise
# older agents sending pson couldn't backup to filebuckets on newer masters
def self.from_pson(pson)
Puppet.deprecation_warning("Deserializing Puppet::FileBucket::File objects from pson is deprecated. Upgrade to a newer version.")
- self.new(pson["contents"])
+ self.from_data_hash(pson)
end
end
diff --git a/lib/puppet/file_serving/base.rb b/lib/puppet/file_serving/base.rb
index f04a7a453..c2e2c1eff 100644
--- a/lib/puppet/file_serving/base.rb
+++ b/lib/puppet/file_serving/base.rb
@@ -1,95 +1,95 @@
require 'puppet/file_serving'
require 'puppet/util'
require 'puppet/util/methodhelper'
# The base class for Content and Metadata; provides common
# functionality like the behaviour around links.
class Puppet::FileServing::Base
include Puppet::Util::MethodHelper
# This is for external consumers to store the source that was used
# to retrieve the metadata.
attr_accessor :source
# Does our file exist?
def exist?
stat
return true
rescue
return false
end
# 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 == "."
full_path = path
else
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 = {})
self.path = path
@links = :manage
set_options(options)
end
# Determine how we deal with links.
attr_reader :links
def links=(value)
value = value.to_sym
value = :manage if value == :ignore
raise(ArgumentError, ":links can only be set to :manage or :follow") unless [:manage, :follow].include?(value)
@links = value
end
# Set our base path.
attr_reader :path
def path=(path)
raise ArgumentError.new("Paths must be fully qualified") unless Puppet::FileServing::Base.absolute?(path)
@path = path
end
# Set a relative path; this is used for recursion, and sets
# the file's path relative to the initial recursion point.
attr_reader :relative_path
def relative_path=(path)
raise ArgumentError.new("Relative paths must not be fully qualified") if Puppet::FileServing::Base.absolute?(path)
@relative_path = path
end
# Stat our file, using the appropriate link-sensitive method.
def stat
@stat_method ||= self.links == :manage ? :lstat : :stat
- Puppet::FileSystem::File.new(full_path).send(@stat_method)
+ Puppet::FileSystem.send(@stat_method, full_path)
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' => to_data_hash,
'metadata' => {
'api_version' => 1
}
}
end
def self.absolute?(path)
Puppet::Util.absolute_path?(path, :posix) or (Puppet.features.microsoft_windows? and Puppet::Util.absolute_path?(path, :windows))
end
end
diff --git a/lib/puppet/file_serving/configuration.rb b/lib/puppet/file_serving/configuration.rb
index c386a7e0a..4ac897376 100644
--- a/lib/puppet/file_serving/configuration.rb
+++ b/lib/puppet/file_serving/configuration.rb
@@ -1,109 +1,109 @@
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'
def self.configuration
@configuration ||= new
end
Mount = Puppet::FileServing::Mount
private_class_method :new
attr_reader :mounts
#private :mounts
# Find the right mount. Does some shenanigans to support old-style module
# mounts.
def find_mount(mount_name, environment)
# Reparse the configuration if necessary.
readconfig
# This can be nil.
mounts[mount_name]
end
def initialize
@mounts = {}
@config_file = nil
# We don't check to see if the file is modified the first time,
# because we always want to parse at first.
readconfig(false)
end
# Is a given mount available?
def mounted?(name)
@mounts.include?(name)
end
# Split the path into the separate mount point and path.
def split_path(request)
# Reparse the configuration if necessary.
readconfig
mount_name, path = request.key.split(File::Separator, 2)
raise(ArgumentError, "Cannot find file: Invalid mount '#{mount_name}'") unless mount_name =~ %r{^[-\w]+$}
raise(ArgumentError, "Cannot find file: Invalid relative path '#{path}'") if path and path.split('/').include?('..')
return nil unless mount = find_mount(mount_name, request.environment)
if mount.name == "modules" and mount_name != "modules"
# yay backward-compatibility
path = "#{mount_name}/#{path}"
end
if path == ""
path = nil
elsif path
# Remove any double slashes that might have occurred
path = path.gsub(/\/+/, "/")
end
return mount, path
end
def umount(name)
@mounts.delete(name) if @mounts.include? name
end
private
def mk_default_mounts
@mounts["modules"] ||= Mount::Modules.new("modules")
@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 Puppet::FileSystem::File.exist?(config)
+ return unless Puppet::FileSystem.exist?(config)
@parser ||= Puppet::FileServing::Configuration::Parser.new(config)
return if check and ! @parser.changed?
# Don't assign the mounts hash until we're sure the parsing succeeded.
begin
newmounts = @parser.parse
@mounts = newmounts
rescue => detail
Puppet.log_exception(detail, "Error parsing fileserver configuration: #{detail}; using old configuration")
end
ensure
# Make sure we've got our plugins and modules.
mk_default_mounts
end
end
diff --git a/lib/puppet/file_serving/configuration/parser.rb b/lib/puppet/file_serving/configuration/parser.rb
index aa09aab50..ccb6b3568 100644
--- a/lib/puppet/file_serving/configuration/parser.rb
+++ b/lib/puppet/file_serving/configuration/parser.rb
@@ -1,121 +1,121 @@
require 'puppet/file_serving/configuration'
require 'puppet/util/watched_file'
class Puppet::FileServing::Configuration::Parser
Mount = Puppet::FileServing::Mount
MODULES = 'modules'
# Parse our configuration file.
def parse
- raise("File server configuration #{@file} does not exist") unless Puppet::FileSystem::File.exist?(@file)
+ raise("File server configuration #{@file} does not exist") unless Puppet::FileSystem.exist?(@file)
raise("Cannot read file server configuration #{@file}") unless FileTest.readable?(@file)
@mounts = {}
@count = 0
File.open(@file) { |f|
mount = nil
f.each_line { |line|
# Have the count increment at the top, in case we throw exceptions.
@count += 1
case line
when /^\s*#/; next # skip comments
when /^\s*$/; next # skip blank lines
when /\[([-\w]+)\]/
mount = newmount($1)
when /^\s*(\w+)\s+(.+?)(\s*#.*)?$/
var = $1
value = $2
value.strip!
raise(ArgumentError, "Fileserver configuration file does not use '=' as a separator") if value =~ /^=/
case var
when "path"
path(mount, value)
when "allow"
allow(mount, value)
when "deny"
deny(mount, value)
else
- raise ArgumentError.new("Invalid argument '#{var}'", @count, @file)
+ raise ArgumentError.new("Invalid argument '#{var}' in #{@file.filename}, line #{@count}")
end
else
- raise ArgumentError.new("Invalid line '#{line.chomp}'", @count, @file)
+ raise ArgumentError.new("Invalid line '#{line.chomp}' at #{@file.filename}, line #{@count}")
end
}
}
validate
@mounts
end
def initialize(filename)
@file = Puppet::Util::WatchedFile.new(filename)
end
def changed?
@file.changed?
end
private
# Allow a given pattern access to a mount.
def allow(mount, value)
value.split(/\s*,\s*/).each { |val|
begin
mount.info "allowing #{val} access"
mount.allow(val)
rescue Puppet::AuthStoreError => detail
raise ArgumentError.new(detail.to_s, @count, @file)
end
}
end
# Deny a given pattern access to a mount.
def deny(mount, value)
value.split(/\s*,\s*/).each { |val|
begin
mount.info "denying #{val} access"
mount.deny(val)
rescue Puppet::AuthStoreError => detail
raise ArgumentError.new(detail.to_s, @count, @file)
end
}
end
# Create a new mount.
def newmount(name)
raise ArgumentError, "#{@mounts[name]} is already mounted at #{name}", @count, @file if @mounts.include?(name)
case name
when "modules"
mount = Mount::Modules.new(name)
when "plugins"
mount = Mount::Plugins.new(name)
else
mount = Mount::File.new(name)
end
@mounts[name] = mount
mount
end
# Set the path for a mount.
def path(mount, value)
if mount.respond_to?(:path=)
begin
mount.path = value
rescue ArgumentError => detail
Puppet.log_exception(detail, "Removing mount \"#{mount.name}\": #{detail}")
@mounts.delete(mount.name)
end
else
Puppet.warning "The '#{mount.name}' module can not have a path. Ignoring attempt to set it"
end
end
# Make sure all of our mounts are valid. We have to do this after the fact
# because details are added over time as the file is parsed.
def validate
@mounts.each { |name, mount| mount.validate }
end
end
diff --git a/lib/puppet/file_serving/content.rb b/lib/puppet/file_serving/content.rb
index fc5a70ea5..00e9dc8ce 100644
--- a/lib/puppet/file_serving/content.rb
+++ b/lib/puppet/file_serving/content.rb
@@ -1,45 +1,45 @@
require 'puppet/indirector'
require 'puppet/file_serving'
require 'puppet/file_serving/base'
# A class that handles retrieving file contents.
# It only reads the file when its content is specifically
# asked for.
class Puppet::FileServing::Content < Puppet::FileServing::Base
extend Puppet::Indirector
indirects :file_content, :terminus_class => :selector
attr_writer :content
def self.supported_formats
[:raw]
end
def self.from_raw(content)
instance = new("/this/is/a/fake/path")
instance.content = content
instance
end
# BF: we used to fetch the file content here, but this is counter-productive
# for puppetmaster streaming of file content. So collect just returns itself
- def collect
+ def collect(source_permissions = nil)
return if stat.ftype == "directory"
self
end
# Read the content of our file in.
def content
unless @content
# This stat can raise an exception, too.
raise(ArgumentError, "Cannot read the contents of links unless following links") if stat.ftype == "symlink"
- @content = Puppet::FileSystem::File.new(full_path).binread
+ @content = Puppet::FileSystem.binread(full_path)
end
@content
end
def to_raw
File.new(full_path, "rb")
end
end
diff --git a/lib/puppet/file_serving/fileset.rb b/lib/puppet/file_serving/fileset.rb
index 20957831f..a1d9d54d7 100644
--- a/lib/puppet/file_serving/fileset.rb
+++ b/lib/puppet/file_serving/fileset.rb
@@ -1,172 +1,172 @@
require 'find'
require 'puppet/file_serving'
require 'puppet/file_serving/metadata'
# Operate recursively on a path, returning a set of file paths.
class Puppet::FileServing::Fileset
attr_reader :path, :ignore, :links
attr_accessor :recurse, :recurselimit, :checksum_type
# Produce a hash of files, with merged so that earlier files
# with the same postfix win. E.g., /dir1/subfile beats /dir2/subfile.
# It's a hash because we need to know the relative path of each file,
# and the base directory.
# This will probably only ever be used for searching for plugins.
def self.merge(*filesets)
result = {}
filesets.each do |fileset|
fileset.files.each do |file|
result[file] ||= fileset.path
end
end
result
end
def initialize(path, options = {})
if Puppet.features.microsoft_windows?
# REMIND: UNC path
path = path.chomp(File::SEPARATOR) unless path =~ /^[A-Za-z]:\/$/
else
path = path.chomp(File::SEPARATOR) unless path == File::SEPARATOR
end
raise ArgumentError.new("Fileset paths must be fully qualified: #{path}") unless Puppet::Util.absolute_path?(path)
@path = path
# Set our defaults.
self.ignore = []
self.links = :manage
@recurse = false
@recurselimit = :infinite
if options.is_a?(Puppet::Indirector::Request)
initialize_from_request(options)
else
initialize_from_hash(options)
end
raise ArgumentError.new("Fileset paths must exist") unless valid?(path)
raise ArgumentError.new("Fileset recurse parameter must not be a number anymore, please use recurselimit") if @recurse.is_a?(Integer)
end
# Return a list of all files in our fileset. This is different from the
# normal definition of find in that we support specific levels
# of recursion, which means we need to know when we're going another
# level deep, which Find doesn't do.
def files
files = perform_recursion
# Now strip off the leading path, so each file becomes relative, and remove
# any slashes that might end up at the beginning of the path.
result = files.collect { |file| file.sub(%r{^#{Regexp.escape(@path)}/*}, '') }
# And add the path itself.
result.unshift(".")
result
end
def ignore=(values)
values = [values] unless values.is_a?(Array)
- @ignore = values
+ @ignore = values.collect(&:to_s)
end
def links=(links)
links = links.to_sym
raise(ArgumentError, "Invalid :links value '#{links}'") unless [:manage, :follow].include?(links)
@links = links
@stat_method = @links == :manage ? :lstat : :stat
end
private
def initialize_from_hash(options)
options.each do |option, value|
method = option.to_s + "="
begin
send(method, value)
rescue NoMethodError
- raise ArgumentError, "Invalid option '#{option}'"
+ raise ArgumentError, "Invalid option '#{option}'", $!.backtrace
end
end
end
def initialize_from_request(request)
[:links, :ignore, :recurse, :recurselimit, :checksum_type].each do |param|
if request.options.include?(param) # use 'include?' so the values can be false
value = request.options[param]
elsif request.options.include?(param.to_s)
value = request.options[param.to_s]
end
next if value.nil?
value = true if value == "true"
value = false if value == "false"
value = Integer(value) if value.is_a?(String) and value =~ /^\d+$/
send(param.to_s + "=", value)
end
end
FileSetEntry = Struct.new(:depth, :path, :ignored, :stat_method) do
def down_level(to)
FileSetEntry.new(depth + 1, File.join(path, to), ignored, stat_method)
end
def basename
File.basename(path)
end
def children
return [] unless directory?
Dir.entries(path).
reject { |child| ignore?(child) }.
collect { |child| down_level(child) }
end
def ignore?(child)
return true if child == "." || child == ".."
return false if ignored == [nil]
ignored.any? { |pattern| File.fnmatch?(pattern, child) }
end
def directory?
- Puppet::FileSystem::File.new(path).send(stat_method).directory?
+ Puppet::FileSystem.send(stat_method, path).directory?
rescue Errno::ENOENT, Errno::EACCES
false
end
end
# Pull the recursion logic into one place. It's moderately hairy, and this
# allows us to keep the hairiness apart from what we do with the files.
def perform_recursion
current_dirs = [FileSetEntry.new(0, @path, @ignore, @stat_method)]
result = []
while entry = current_dirs.shift
if continue_recursion_at?(entry.depth + 1)
entry.children.each do |child|
result << child.path
current_dirs << child
end
end
end
result
end
def valid?(path)
- Puppet::FileSystem::File.new(path).send(@stat_method)
+ Puppet::FileSystem.send(@stat_method, path)
true
rescue Errno::ENOENT, Errno::EACCES
false
end
def continue_recursion_at?(depth)
# recurse if told to, and infinite recursion or current depth not at the limit
self.recurse && (self.recurselimit == :infinite || depth <= self.recurselimit)
end
end
diff --git a/lib/puppet/file_serving/metadata.rb b/lib/puppet/file_serving/metadata.rb
index 2073c53ca..e88da85cf 100644
--- a/lib/puppet/file_serving/metadata.rb
+++ b/lib/puppet/file_serving/metadata.rb
@@ -1,145 +1,199 @@
require 'puppet'
require 'puppet/indirector'
require 'puppet/file_serving'
require 'puppet/file_serving/base'
require 'puppet/util/checksums'
# A class that handles retrieving file metadata.
class Puppet::FileServing::Metadata < Puppet::FileServing::Base
include Puppet::Util::Checksums
extend Puppet::Indirector
indirects :file_metadata, :terminus_class => :selector
attr_reader :path, :owner, :group, :mode, :checksum_type, :checksum, :ftype, :destination
PARAM_ORDER = [:mode, :ftype, :owner, :group]
def checksum_type=(type)
raise(ArgumentError, "Unsupported checksum type #{type}") unless respond_to?("#{type}_file")
@checksum_type = type
end
class MetaStat
extend Forwardable
- def initialize(stat)
+ def initialize(stat, source_permissions = nil)
@stat = stat
+ @source_permissions_ignore = source_permissions == :ignore
end
- def_delegator :@stat, :uid, :owner
- def_delegator :@stat, :gid, :group
- def_delegators :@stat, :mode, :ftype
+ def owner
+ @source_permissions_ignore ? Process.euid : @stat.uid
+ end
+
+ def group
+ @source_permissions_ignore ? Process.egid : @stat.gid
+ end
+
+ def mode
+ @source_permissions_ignore ? 0644 : @stat.mode
+ end
+
+ def_delegators :@stat, :ftype
end
class WindowsStat < MetaStat
if Puppet.features.microsoft_windows?
require 'puppet/util/windows/security'
end
- def initialize(stat, path)
- super(stat)
+ def initialize(stat, path, source_permissions = nil)
+ super(stat, source_permissions)
@path = path
end
{ :owner => 'S-1-5-32-544',
:group => 'S-1-0-0',
:mode => 0644
}.each do |method, default_value|
define_method method do
- Puppet::Util::Windows::Security.send("get_#{method}", @path) || default_value
+ return default_value if @source_permissions_ignore
+
+ # this code remains for when source_permissions is not set to :ignore
+ begin
+ Puppet::Util::Windows::Security.send("get_#{method}", @path) || default_value
+ rescue Puppet::Util::Windows::Error => detail
+ # Very carefully catch only this specific error that result from
+ # trying to read permissions on a symlinked file that is on a volume
+ # that does not support ACLs.
+ #
+ # Unfortunately readlink method will not return the target path when
+ # the given path is not the symlink.
+ #
+ # For instance, consider:
+ # symlink c:\link points to c:\target
+ # FileSystem.readlink('c:/link') returns 'c:/target'
+ # FileSystem.readlink('c:/link/file') will NOT return 'c:/target/file'
+ #
+ # Since detecting this up front is costly, since the path in question
+ # needs to be recursively split and tested at each depth in the path,
+ # we catch the standard error that will result from trying to read a
+ # file that doesn't have a DACL - 1336 is ERROR_INVALID_DACL
+ #
+ # Note that this affects any manually created symlinks as well as
+ # paths like puppet:///modules
+ return default_value if detail.code == 1336
+
+ # Also handle a VirtualBox bug where ERROR_INVALID_FUNCTION is
+ # returned when following a symlink to a volume that is not NTFS.
+ # It appears that the VirtualBox file system is not propagating
+ # the standard Win32 error code above like it should.
+ #
+ # Apologies to all who enter this code path at a later date
+ if detail.code == 1 && Facter.value(:virtual) == 'virtualbox'
+ return default_value
+ end
+
+ raise
+ end
end
end
end
- def collect_stat(path)
+ def collect_stat(path, source_permissions)
stat = stat()
if Puppet.features.microsoft_windows?
- WindowsStat.new(stat, path)
+ WindowsStat.new(stat, path, source_permissions)
else
- MetaStat.new(stat)
+ MetaStat.new(stat, source_permissions)
end
end
# Retrieve the attributes for this file, relative to a base directory.
- # Note that Puppet::FileSystem::File.new(path).stat raises Errno::ENOENT
+ # Note that Puppet::FileSystem.stat(path) raises Errno::ENOENT
# if the file is absent and this method does not catch that exception.
- def collect
+ def collect(source_permissions = nil)
real_path = full_path
- stat = collect_stat(real_path)
+ stat = collect_stat(real_path, source_permissions)
@owner = stat.owner
@group = stat.group
@ftype = stat.ftype
# We have to mask the mode, yay.
@mode = stat.mode & 007777
case stat.ftype
when "file"
@checksum = ("{#{@checksum_type}}") + send("#{@checksum_type}_file", real_path).to_s
when "directory" # Always just timestamp the directory.
@checksum_type = "ctime"
@checksum = ("{#{@checksum_type}}") + send("#{@checksum_type}_file", path).to_s
when "link"
- @destination = Puppet::FileSystem::File.new(real_path).readlink
+ @destination = Puppet::FileSystem.readlink(real_path)
@checksum = ("{#{@checksum_type}}") + send("#{@checksum_type}_file", real_path).to_s rescue nil
else
raise ArgumentError, "Cannot manage files of type #{stat.ftype}"
end
end
def initialize(path,data={})
@owner = data.delete('owner')
@group = data.delete('group')
@mode = data.delete('mode')
if checksum = data.delete('checksum')
@checksum_type = checksum['type']
@checksum = checksum['value']
end
@checksum_type ||= "md5"
@ftype = data.delete('type')
@destination = data.delete('destination')
super(path,data)
end
def to_data_hash
super.update(
{
'owner' => owner,
'group' => group,
'mode' => mode,
'checksum' => {
'type' => checksum_type,
'value' => checksum
},
'type' => ftype,
'destination' => destination,
}
)
end
+ def self.from_data_hash(data)
+ new(data.delete('path'), data)
+ end
+
PSON.register_document_type('FileMetadata',self)
def to_pson_data_hash
{
'document_type' => 'FileMetadata',
'data' => to_data_hash,
'metadata' => {
'api_version' => 1
}
}
end
def to_pson(*args)
to_pson_data_hash.to_pson(*args)
end
def self.from_pson(data)
- new(data.delete('path'), data)
+ Puppet.deprecation_warning("from_pson is being removed in favour of from_data_hash.")
+ self.from_data_hash(data)
end
end
diff --git a/lib/puppet/file_serving/mount/file.rb b/lib/puppet/file_serving/mount/file.rb
index 535a90bde..2b48c88ea 100644
--- a/lib/puppet/file_serving/mount/file.rb
+++ b/lib/puppet/file_serving/mount/file.rb
@@ -1,121 +1,121 @@
require 'puppet/file_serving/mount'
class Puppet::FileServing::Mount::File < Puppet::FileServing::Mount
def self.localmap
@localmap ||= {
"h" => Facter.value("hostname"),
"H" => [
Facter.value("hostname"),
Facter.value("domain")
].join("."),
"d" => Facter.value("domain")
}
end
def complete_path(relative_path, node)
full_path = path(node)
raise ArgumentError.new("Mounts without paths are not usable") unless full_path
# If there's no relative path name, then we're serving the mount itself.
return full_path unless relative_path
file = ::File.join(full_path, relative_path)
- if !(Puppet::FileSystem::File.exist?(file) or Puppet::FileSystem::File.new(file).symlink?)
+ if !(Puppet::FileSystem.exist?(file) or Puppet::FileSystem.symlink?(file))
Puppet.info("File does not exist or is not accessible: #{file}")
return nil
end
file
end
# Return an instance of the appropriate class.
def find(short_file, request)
complete_path(short_file, request.node)
end
# Return the path as appropriate, expanding as necessary.
def path(node = nil)
if expandable?
return expand(@path, node)
else
return @path
end
end
# Set the path.
def path=(path)
# FIXME: For now, just don't validate paths with replacement
# patterns in them.
if path =~ /%./
# Mark that we're expandable.
@expandable = true
else
raise ArgumentError, "#{path} does not exist or is not a directory" unless FileTest.directory?(path)
raise ArgumentError, "#{path} is not readable" unless FileTest.readable?(path)
@expandable = false
end
@path = path
end
def search(path, request)
return nil unless path = complete_path(path, request.node)
[path]
end
# Verify our configuration is valid. This should really check to
# make sure at least someone will be allowed, but, eh.
def validate
raise ArgumentError.new("Mounts without paths are not usable") if @path.nil?
end
private
# Create a map for a specific node.
def clientmap(node)
{
"h" => node.sub(/\..*$/, ""),
"H" => node,
"d" => node.sub(/[^.]+\./, "") # domain name
}
end
# Replace % patterns as appropriate.
def expand(path, node = nil)
# This map should probably be moved into a method.
map = nil
if node
map = clientmap(node)
else
Puppet.notice "No client; expanding '#{path}' with local host"
# Else, use the local information
map = localmap
end
path.gsub(/%(.)/) do |v|
key = $1
if key == "%"
"%"
else
map[key] || v
end
end
end
# Do we have any patterns in our path, yo?
def expandable?
if defined?(@expandable)
@expandable
else
false
end
end
# Cache this manufactured map, since if it's used it's likely
# to get used a lot.
def localmap
self.class.localmap
end
end
diff --git a/lib/puppet/file_system.rb b/lib/puppet/file_system.rb
index 78e0f71db..6db15ee37 100644
--- a/lib/puppet/file_system.rb
+++ b/lib/puppet/file_system.rb
@@ -1,6 +1,366 @@
module Puppet::FileSystem
require 'puppet/file_system/path_pattern'
- require 'puppet/file_system/file'
+ require 'puppet/file_system/file_impl'
require 'puppet/file_system/memory_file'
+ require 'puppet/file_system/memory_impl'
require 'puppet/file_system/tempfile'
+
+ # create instance of the file system implementation to use for the current platform
+ @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.new()
+
+ # Allows overriding the filesystem for the duration of the given block.
+ # The filesystem will only contain the given file(s).
+ #
+ # @param files [Puppet::FileSystem::MemoryFile] the files to have available
+ #
+ # @api private
+ #
+ def self.overlay(*files, &block)
+ old_impl = @impl
+ @impl = Puppet::FileSystem::MemoryImpl.new(*files)
+ yield
+ ensure
+ @impl = old_impl
+ end
+
+ # Opens the given path with given mode, and options and optionally yields it to the given block.
+ #
+ # @api public
+ #
+ def self.open(path, mode, options, &block)
+ @impl.open(assert_path(path), mode, options, &block)
+ end
+
+ # @return [Object] The directory of this file as an opaque handle
+ #
+ # @api public
+ #
+ def self.dir(path)
+ @impl.dir(assert_path(path))
+ end
+
+ # @return [String] The directory of this file as a String
+ #
+ # @api public
+ #
+ def self.dir_string(path)
+ @impl.path_string(@impl.dir(assert_path(path)))
+ end
+
+ # @return [Boolean] Does the directory of the given path exist?
+ def self.dir_exist?(path)
+ @impl.exist?(@impl.dir(assert_path(path)))
+ end
+
+ # Creates all directories down to (inclusive) the dir of the given path
+ def self.dir_mkpath(path)
+ @impl.mkpath(@impl.dir(assert_path(path)))
+ end
+
+ # @return [Object] the name of the file as a opaque handle
+ #
+ # @api public
+ #
+ def self.basename(path)
+ @impl.basename(assert_path(path))
+ end
+
+ # @return [String] the name of the file
+ #
+ # @api public
+ #
+ def self.basename_string(path)
+ @impl.path_string(@impl.basename(assert_path(path)))
+ end
+
+ # @return [Integer] the size of the file
+ #
+ # @api public
+ #
+ def self.size(path)
+ @impl.size(assert_path(path))
+ 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 path [Pathname] the path to the file to operate on
+ # @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 acquire the lock
+ #
+ # @api public
+ #
+ def self.exclusive_open(path, mode, options = 'r', timeout = 300, &block)
+ @impl.exclusive_open(assert_path(path), mode, options, timeout, &block)
+ end
+
+ # Processes each line of the file by yielding it to the given block
+ #
+ # @api public
+ #
+ def self.each_line(path, &block)
+ @impl.each_line(assert_path(path), &block)
+ end
+
+ # @return [String] The contents of the file
+ #
+ # @api public
+ #
+ def self.read(path)
+ @impl.read(assert_path(path))
+ end
+
+ # @return [String] The binary contents of the file
+ #
+ # @api public
+ #
+ def self.binread(path)
+ @impl.binread(assert_path(path))
+ end
+
+ # Determines 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.
+ #
+ # @api public
+ #
+ def self.exist?(path)
+ @impl.exist?(assert_path(path))
+ end
+
+ # Determines if a file is a directory.
+ #
+ # @return [Boolean] true if the given file is a directory.
+ #
+ # @api public
+ def self.directory?(path)
+ @impl.directory?(assert_path(path))
+ end
+
+ # Determines if a file is a file.
+ #
+ # @return [Boolean] true if the given file is a file.
+ #
+ # @api public
+ def self.file?(path)
+ @impl.file?(assert_path(path))
+ end
+
+ # Determines 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
+ #
+ # @api public
+ #
+ def self.executable?(path)
+ @impl.executable?(assert_path(path))
+ end
+
+ # @return [Boolean] Whether the file is writable by the current process
+ #
+ # @api public
+ #
+ def self.writable?(path)
+ @impl.writable?(assert_path(path))
+ end
+
+ # Touches the file. On most systems this updates the mtime of the file.
+ #
+ # @api public
+ #
+ def self.touch(path)
+ @impl.touch(assert_path(path))
+ end
+
+ # Creates directories for all parts of the given path.
+ #
+ # @api public
+ #
+ def self.mkpath(path)
+ @impl.mkpath(assert_path(path))
+ end
+
+ # @return [Array<Object>] references to all of the children of the given
+ # directory path, excluding `.` and `..`.
+ # @api public
+ def self.children(path)
+ @impl.children(assert_path(path))
+ 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
+ #
+ # @api public
+ #
+ def self.symlink(path, dest, options = {})
+ @impl.symlink(assert_path(path), dest, options)
+ end
+
+ # @return [Boolean] true if the file is a symbolic link.
+ #
+ # @api public
+ #
+ def self.symlink?(path)
+ @impl.symlink?(assert_path(path))
+ end
+
+ # @return [String] the name of the file referenced by the given link.
+ #
+ # @api public
+ #
+ def self.readlink(path)
+ @impl.readlink(assert_path(path))
+ end
+
+ # Deletes the given paths, returning the number of names passed as arguments.
+ # See also Dir::rmdir.
+ #
+ # @raise an exception on any error.
+ #
+ # @return [Integer] the number of paths passed as arguments
+ #
+ # @api public
+ #
+ def self.unlink(*paths)
+ @impl.unlink(*(paths.map {|p| assert_path(p) }))
+ end
+
+ # @return [File::Stat] object for the named file.
+ #
+ # @api public
+ #
+ def self.stat(path)
+ @impl.stat(assert_path(path))
+ end
+
+ # @return [Integer] the size of the file
+ #
+ # @api public
+ #
+ def self.size(path)
+ @impl.size(assert_path(path))
+ end
+
+ # @return [File::Stat] Same as stat, but does not follow the last symbolic
+ # link. Instead, reports on the link itself.
+ #
+ # @api public
+ #
+ def self.lstat(path)
+ @impl.lstat(assert_path(path))
+ end
+
+ # Compares 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
+ #
+ # @api public
+ #
+ def self.compare_stream(path, stream)
+ @impl.compare_stream(assert_path(path), stream)
+ end
+
+ # Produces an opaque pathname "handle" object representing the given path.
+ # Different implementations of the underlying file system may use different runtime
+ # objects. The produced "handle" should be used in all other operations
+ # that take a "path". No operation should be directly invoked on the returned opaque object
+ #
+ # @param path [String] The string representation of the path
+ # @return [Object] An opaque path handle on which no operations should be directly performed
+ #
+ # @api public
+ #
+ def self.pathname(path)
+ @impl.pathname(path)
+ end
+
+ # Asserts that the given path is of the expected type produced by #pathname
+ #
+ # @raise [ArgumentError] when path is not of the expected type
+ #
+ # @api public
+ #
+ def self.assert_path(path)
+ @impl.assert_path(path)
+ end
+
+ # Produces a string representation of the opaque path handle.
+ #
+ # @param path [Object] a path handle produced by {#pathname}
+ # @return [String] a string representation of the path
+ #
+ def self.path_string(path)
+ @impl.path_string(path)
+ end
+
+ # Create and open a file for write only if it doesn't exist.
+ #
+ # @see Puppet::FileSystem::open
+ #
+ # @raise [Errno::EEXIST] path already exists.
+ #
+ # @api public
+ #
+ def self.exclusive_create(path, mode, &block)
+ @impl.exclusive_create(assert_path(path), mode, &block)
+ end
+
+ # Changes permission bits on the named path to the bit pattern represented
+ # by mode.
+ #
+ # @param mode [Integer] The mode to apply to the file if it is created
+ # @param path [String] The path to the file, can also accept [PathName]
+ #
+ # @raise [Errno::ENOENT]: path doesn't exist
+ #
+ # @api public
+ #
+ def self.chmod(mode, path)
+ @impl.chmod(mode, path)
+ end
end
diff --git a/lib/puppet/file_system/file.rb b/lib/puppet/file_system/file.rb
deleted file mode 100644
index 57dd5341d..000000000
--- a/lib/puppet/file_system/file.rb
+++ /dev/null
@@ -1,271 +0,0 @@
-# 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 [String] the name of the file
- # @api public
- def basename
- @path.basename.to_s
- 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
-
- def to_s
- @path.to_s
- end
-end
diff --git a/lib/puppet/file_system/file18.rb b/lib/puppet/file_system/file18.rb
index 99c2d5e06..aa4889c79 100644
--- a/lib/puppet/file_system/file18.rb
+++ b/lib/puppet/file_system/file18.rb
@@ -1,5 +1,5 @@
-class Puppet::FileSystem::File18 < Puppet::FileSystem::File
- def binread
- ::File.open(@path, 'rb') { |f| f.read }
+class Puppet::FileSystem::File18 < Puppet::FileSystem::FileImpl
+ def binread(path)
+ ::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
index ca011613e..fce9a6a82 100644
--- a/lib/puppet/file_system/file19.rb
+++ b/lib/puppet/file_system/file19.rb
@@ -1,5 +1,5 @@
-class Puppet::FileSystem::File19 < Puppet::FileSystem::File
- def binread
- @path.binread
+class Puppet::FileSystem::File19 < Puppet::FileSystem::FileImpl
+ def binread(path)
+ path.binread
end
end
diff --git a/lib/puppet/file_system/file19windows.rb b/lib/puppet/file_system/file19windows.rb
index 87f9915fe..7ebba2cf4 100644
--- a/lib/puppet/file_system/file19windows.rb
+++ b/lib/puppet/file_system/file19windows.rb
@@ -1,113 +1,108 @@
require 'puppet/file_system/file19'
require 'puppet/util/windows'
class Puppet::FileSystem::File19Windows < Puppet::FileSystem::File19
- def self.exist?(path)
+ def 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 = {})
+ def symlink(path, dest, options = {})
raise_if_symlinks_unsupported
- dest_exists = self.class.exist?(dest) # returns false on dangling symlink
+ dest_exists = 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)
+ Puppet::Util::Windows::File.symlink(path, dest)
end
0
end
- def symlink?
+ def symlink?(path)
return false if ! Puppet.features.manages_symlinks?
- Puppet::Util::Windows::File.symlink?(@path)
+ Puppet::Util::Windows::File.symlink?(path)
end
- def readlink
+ def readlink(path)
raise_if_symlinks_unsupported
- Puppet::Util::Windows::File.readlink(@path)
+ Puppet::Util::Windows::File.readlink(path)
end
- def self.unlink(*file_names)
+ def 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)
+ def stat(path)
+ Puppet::Util::Windows::File.stat(path)
end
- def stat
+ def lstat(path)
if ! Puppet.features.manages_symlinks?
- return super
+ return Puppet::Util::Windows::File.stat(path)
end
- Puppet::Util::Windows::File.stat(@path)
+ Puppet::Util::Windows::File.lstat(path)
end
- def lstat
- if ! Puppet.features.manages_symlinks?
- return Puppet::Util::Windows::File.stat(@path)
- end
- Puppet::Util::Windows::File.lstat(@path)
+ def chmod(mode, path)
+ Puppet::Util::Windows::Security.set_mode(mode, path.to_s)
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/file_impl.rb b/lib/puppet/file_system/file_impl.rb
new file mode 100644
index 000000000..d4cd605b7
--- /dev/null
+++ b/lib/puppet/file_system/file_impl.rb
@@ -0,0 +1,145 @@
+# Abstract implementation of the Puppet::FileSystem
+#
+class Puppet::FileSystem::FileImpl
+
+ def pathname(path)
+ path.is_a?(Pathname) ? path : Pathname.new(path)
+ end
+
+ def assert_path(path)
+ return path if path.is_a?(Pathname)
+
+ # Some paths are string, or in the case of WatchedFile, it pretends to be
+ # one by implementing to_str.
+ if path.respond_to?(:to_str)
+ Pathname.new(path)
+ else
+ raise ArgumentError, "FileSystem implementation expected Pathname, got: '#{path.class}'"
+ end
+ end
+
+ def path_string(path)
+ path.to_s
+ end
+
+ def open(path, mode, options, &block)
+ ::File.open(path, options, mode, &block)
+ end
+
+ def dir(path)
+ path.dirname
+ end
+
+ def basename(path)
+ path.basename.to_s
+ end
+
+ def size(path)
+ path.size
+ end
+
+ def exclusive_create(path, mode, &block)
+ opt = File::CREAT | File::EXCL | File::WRONLY
+ self.open(path, mode, opt, &block)
+ end
+
+ def exclusive_open(path, 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(path, &block)
+ ::File.open(path) do |f|
+ f.each_line do |line|
+ yield line
+ end
+ end
+ end
+
+ def read(path)
+ path.read
+ end
+
+ def binread(path)
+ raise NotImplementedError
+ end
+
+ def exist?(path)
+ ::File.exist?(path)
+ end
+
+ def directory?(path)
+ ::File.directory?(path)
+ end
+
+ def file?(path)
+ ::File.file?(path)
+ end
+
+ def executable?(path)
+ ::File.executable?(path)
+ end
+
+ def writable?(path)
+ path.writable?
+ end
+
+ def touch(path)
+ ::FileUtils.touch(path)
+ end
+
+ def mkpath(path)
+ path.mkpath
+ end
+
+ def children(path)
+ path.children
+ end
+
+ def symlink(path, dest, options = {})
+ FileUtils.symlink(path, dest, options)
+ end
+
+ def symlink?(path)
+ File.symlink?(path)
+ end
+
+ def readlink(path)
+ File.readlink(path)
+ end
+
+ def unlink(*paths)
+ File.unlink(*paths)
+ end
+
+ def stat(path)
+ File.stat(path)
+ end
+
+ def lstat(path)
+ File.lstat(path)
+ end
+
+ def compare_stream(path, stream)
+ open(path, 0, 'rb') { |this| FileUtils.compare_stream(this, stream) }
+ end
+
+ def chmod(mode, path)
+ FileUtils.chmod(mode, path)
+ end
+end
diff --git a/lib/puppet/file_system/memory_file.rb b/lib/puppet/file_system/memory_file.rb
index 4605a7a01..d2715c00a 100644
--- a/lib/puppet/file_system/memory_file.rb
+++ b/lib/puppet/file_system/memory_file.rb
@@ -1,31 +1,45 @@
# An in-memory file abstraction. Commonly used with Puppet::FileSystem::File#overlay
# @api private
class Puppet::FileSystem::MemoryFile
- attr_reader :path
+ attr_reader :path, :children
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]
+ def self.a_directory(path, children = [])
+ new(path,
+ :exist? => true,
+ :excutable? => true,
+ :directory? => true,
+ :children => children)
end
- def exist?; @exist; end
- def executable?; @executable; end
+ def initialize(path, properties)
+ @path = path
+ @properties = properties
+ @children = (properties[:children] || []).collect do |child|
+ child.duplicate_as(File.join(@path, child.path))
+ end
+ end
+
+ def directory?; @properties[:directory?]; end
+ def exist?; @properties[:exist?]; end
+ def executable?; @properties[:executable?]; end
def each_line(&block)
- StringIO.new(@content).each_line(&block)
+ StringIO.new(@properties[:content]).each_line(&block)
+ end
+
+ def duplicate_as(other_path)
+ self.class.new(other_path, @properties)
end
end
diff --git a/lib/puppet/file_system/memory_impl.rb b/lib/puppet/file_system/memory_impl.rb
new file mode 100644
index 000000000..19abf66a6
--- /dev/null
+++ b/lib/puppet/file_system/memory_impl.rb
@@ -0,0 +1,64 @@
+class Puppet::FileSystem::MemoryImpl
+ def initialize(*files)
+ @files = files + all_children_of(files)
+ end
+
+ def exist?(path)
+ path.exist?
+ end
+
+ def directory?(path)
+ path.directory?
+ end
+
+ def file?(path)
+ path.file?
+ end
+
+ def executable?(path)
+ path.executable?
+ end
+
+ def children(path)
+ path.children
+ end
+
+ def each_line(path, &block)
+ path.each_line(&block)
+ end
+
+ def pathname(path)
+ find(path)
+ end
+
+ def basename(path)
+ path.duplicate_as(File.basename(path_string(path)))
+ end
+
+ def path_string(object)
+ object.path
+ end
+
+ def assert_path(path)
+ if path.is_a?(Puppet::FileSystem::MemoryFile)
+ path
+ else
+ find(path) or raise ArgumentError, "Unable to find registered object for #{path.inspect}"
+ end
+ end
+
+ private
+
+ def find(path)
+ @files.find { |file| file.path == path }
+ end
+
+ def all_children_of(files)
+ children = files.collect(&:children).flatten
+ if children.empty?
+ []
+ else
+ children + all_children_of(children)
+ end
+ end
+end
diff --git a/lib/puppet/forge.rb b/lib/puppet/forge.rb
index 3c6626328..e5b54d4c5 100644
--- a/lib/puppet/forge.rb
+++ b/lib/puppet/forge.rb
@@ -1,105 +1,106 @@
require 'net/http'
require 'open-uri'
require 'pathname'
require 'uri'
-require 'puppet/forge/cache'
-require 'puppet/forge/repository'
-require 'puppet/forge/errors'
class Puppet::Forge
+ require 'puppet/forge/errors'
+ require 'puppet/forge/cache'
+ require 'puppet/forge/repository'
+
include Puppet::Forge::Errors
# +consumer_name+ is a name to be used for identifying the consumer of the
# forge and +consumer_semver+ is a SemVer object to identify the version of
# the consumer
def initialize(consumer_name, consumer_semver)
@consumer_name = consumer_name
@consumer_semver = consumer_semver
end
# Return a list of module metadata hashes that match the search query.
# This return value is used by the module_tool face install search,
# and displayed to on the console.
#
# Example return value:
#
# [
# {
# "author" => "puppetlabs",
# "name" => "bacula",
# "tag_list" => ["backup", "bacula"],
# "releases" => [{"version"=>"0.0.1"}, {"version"=>"0.0.2"}],
# "full_name" => "puppetlabs/bacula",
# "version" => "0.0.2",
# "project_url" => "http://github.com/puppetlabs/puppetlabs-bacula",
# "desc" => "bacula"
# }
# ]
#
# @param term [String] search term
# @return [Array] modules found
# @raise [Puppet::Forge::Errors::CommunicationError] if there is a network
# related error
# @raise [Puppet::Forge::Errors::SSLVerifyError] if there is a problem
# verifying the remote SSL certificate
# @raise [Puppet::Forge::Errors::ResponseError] if the repository returns a
# bad HTTP response
def search(term)
server = Puppet.settings[:module_repository]
Puppet.notice "Searching #{server} ..."
response = repository.make_http_request("/modules.json?q=#{URI.escape(term)}")
case response.code
when "200"
matches = PSON.parse(response.body)
else
raise ResponseError.new(:uri => uri.to_s, :input => term, :response => response)
end
matches
end
# Return a list of module metadata hashes for the module requested and all
# of its dependencies.
#
# @param author [String] module's author name
# @param mod_name [String] module name
# @param version [String] optional module version number
# @return [Array] module and dependency metadata
# @raise [Puppet::Forge::Errors::CommunicationError] if there is a network
# related error
# @raise [Puppet::Forge::Errors::SSLVerifyError] if there is a problem
# verifying the remote SSL certificate
# @raise [Puppet::Forge::Errors::ResponseError] if the repository returns
# an error in its API response or a bad HTTP response
def remote_dependency_info(author, mod_name, version)
version_string = version ? "&version=#{version}" : ''
response = repository.make_http_request("/api/v1/releases.json?module=#{author}/#{mod_name}#{version_string}")
json = PSON.parse(response.body) rescue {}
case response.code
when "200"
return json
else
error = json['error']
if error && error =~ /^Module #{author}\/#{mod_name} has no release/
return []
else
raise ResponseError.new(:uri => uri.to_s, :input => "#{author}/#{mod_name}", :message => error, :response => response)
end
end
end
def retrieve(release)
repository.retrieve(release)
end
def uri
repository.uri
end
def repository
version = "#{@consumer_name}/#{[@consumer_semver.major, @consumer_semver.minor, @consumer_semver.tiny].join('.')}#{@consumer_semver.special}"
@repository ||= Puppet::Forge::Repository.new(Puppet[:module_repository], version)
end
private :repository
end
diff --git a/lib/puppet/forge/repository.rb b/lib/puppet/forge/repository.rb
index 75b7a9f4c..eb2e490e4 100644
--- a/lib/puppet/forge/repository.rb
+++ b/lib/puppet/forge/repository.rb
@@ -1,136 +1,135 @@
require 'net/https'
require 'digest/sha1'
require 'uri'
require 'puppet/util/http_proxy'
-require 'puppet/forge/errors'
if Puppet.features.zlib? && Puppet[:zlib]
require 'zlib'
end
class Puppet::Forge
# = Repository
#
# This class is a file for accessing remote repositories with modules.
class Repository
include Puppet::Forge::Errors
attr_reader :uri, :cache
# List of Net::HTTP exceptions to catch
NET_HTTP_EXCEPTIONS = [
EOFError,
Errno::ECONNABORTED,
Errno::ECONNREFUSED,
Errno::ECONNRESET,
Errno::EINVAL,
Errno::ETIMEDOUT,
Net::HTTPBadResponse,
Net::HTTPHeaderSyntaxError,
Net::ProtocolError,
SocketError,
]
if Puppet.features.zlib? && Puppet[:zlib]
NET_HTTP_EXCEPTIONS << Zlib::GzipFile::Error
end
# Instantiate a new repository instance rooted at the +url+.
# The agent will report +consumer_version+ in the User-Agent to
# the repository.
def initialize(url, consumer_version)
@uri = url.is_a?(::URI) ? url : ::URI.parse(url)
@cache = Cache.new(self)
@consumer_version = consumer_version
end
# Return a Net::HTTPResponse read for this +request_path+.
def make_http_request(request_path)
request = Net::HTTP::Get.new(URI.escape(@uri.path + request_path), { "User-Agent" => user_agent })
if ! @uri.user.nil? && ! @uri.password.nil?
request.basic_auth(@uri.user, @uri.password)
end
return read_response(request)
end
# Return a Net::HTTPResponse read from this HTTPRequest +request+.
#
# @param request [Net::HTTPRequest] request to make
# @return [Net::HTTPResponse] response from request
# @raise [Puppet::Forge::Errors::CommunicationError] if there is a network
# related error
# @raise [Puppet::Forge::Errors::SSLVerifyError] if there is a problem
# verifying the remote SSL certificate
def read_response(request)
http_object = get_http_object
http_object.start do |http|
http.request(request)
end
rescue *NET_HTTP_EXCEPTIONS => e
raise CommunicationError.new(:uri => @uri.to_s, :original => e)
rescue OpenSSL::SSL::SSLError => e
if e.message =~ /certificate verify failed/
raise SSLVerifyError.new(:uri => @uri.to_s, :original => e)
else
raise e
end
end
# Return a Net::HTTP::Proxy object constructed from the settings provided
# accessing the repository.
#
# This method optionally configures SSL correctly if the URI scheme is
# 'https', including setting up the root certificate store so remote server
# SSL certificates can be validated.
#
# @return [Net::HTTP::Proxy] object constructed from repo settings
def get_http_object
proxy_class = Net::HTTP::Proxy(Puppet::Util::HttpProxy.http_proxy_host, Puppet::Util::HttpProxy.http_proxy_port)
proxy = proxy_class.new(@uri.host, @uri.port)
if @uri.scheme == 'https'
cert_store = OpenSSL::X509::Store.new
cert_store.set_default_paths
proxy.use_ssl = true
proxy.verify_mode = OpenSSL::SSL::VERIFY_PEER
proxy.cert_store = cert_store
end
proxy
end
# Return the local file name containing the data downloaded from the
# repository at +release+ (e.g. "myuser-mymodule").
def retrieve(release)
uri = @uri.dup
uri.path = uri.path.chomp('/') + release
return cache.retrieve(uri)
end
# Return the URI string for this repository.
def to_s
return @uri.to_s
end
# Return the cache key for this repository, this a hashed string based on
# the URI.
def cache_key
return @cache_key ||= [
@uri.to_s.gsub(/[^[:alnum:]]+/, '_').sub(/_$/, ''),
Digest::SHA1.hexdigest(@uri.to_s)
].join('-')
end
def user_agent
"#{@consumer_version} Puppet/#{Puppet.version} (#{Facter.value(:operatingsystem)} #{Facter.value(:operatingsystemrelease)}) #{ruby_version}"
end
private :user_agent
def ruby_version
"Ruby/#{RUBY_VERSION}-p#{RUBY_PATCHLEVEL} (#{RUBY_RELEASE_DATE}; #{RUBY_PLATFORM})"
end
private :ruby_version
end
end
diff --git a/lib/puppet/graph/relationship_graph.rb b/lib/puppet/graph/relationship_graph.rb
index 54a38ad54..3c04faa12 100644
--- a/lib/puppet/graph/relationship_graph.rb
+++ b/lib/puppet/graph/relationship_graph.rb
@@ -1,246 +1,246 @@
# The relationship graph is the final form of a puppet catalog in
# which all dependency edges are explicitly in the graph. This form of the
# catalog is used to traverse the graph in the order in which resources are
# managed.
#
# @api private
class Puppet::Graph::RelationshipGraph < Puppet::Graph::SimpleGraph
attr_reader :blockers
def initialize(prioritizer)
super()
@prioritizer = prioritizer
@ready = Puppet::Graph::RbTreeMap.new
@generated = {}
@done = {}
@blockers = {}
@providerless_types = []
end
def populate_from(catalog)
add_all_resources_as_vertices(catalog)
build_manual_dependencies
build_autorequire_dependencies(catalog)
write_graph(:relationships) if catalog.host_config?
replace_containers_with_anchors(catalog)
write_graph(:expanded_relationships) if catalog.host_config?
end
def add_vertex(vertex, priority = nil)
super(vertex)
if priority
@prioritizer.record_priority_for(vertex, priority)
else
@prioritizer.generate_priority_for(vertex)
end
end
def add_relationship(f, t, label=nil)
super(f, t, label)
@ready.delete(@prioritizer.priority_of(t))
end
def remove_vertex!(vertex)
super
@prioritizer.forget(vertex)
end
def resource_priority(resource)
@prioritizer.priority_of(resource)
end
# Enqueue the initial set of resources, those with no dependencies.
def enqueue_roots
vertices.each do |v|
@blockers[v] = direct_dependencies_of(v).length
enqueue(v) if @blockers[v] == 0
end
end
# Decrement the blocker count for the resource by 1. If the number of
# blockers is unknown, count them and THEN decrement by 1.
def unblock(resource)
@blockers[resource] ||= direct_dependencies_of(resource).select { |r2| !@done[r2] }.length
if @blockers[resource] > 0
@blockers[resource] -= 1
else
resource.warning "appears to have a negative number of dependencies"
end
@blockers[resource] <= 0
end
def clear_blockers
@blockers.clear
end
def enqueue(*resources)
resources.each do |resource|
@ready[@prioritizer.priority_of(resource)] = resource
end
end
def finish(resource)
direct_dependents_of(resource).each do |v|
enqueue(v) if unblock(v)
end
@done[resource] = true
end
def next_resource
@ready.delete_min
end
def traverse(options = {}, &block)
continue_while = options[:while] || lambda { true }
pre_process = options[:pre_process] || lambda { |resource| }
overly_deferred_resource_handler = options[:overly_deferred_resource_handler] || lambda { |resource| }
canceled_resource_handler = options[:canceled_resource_handler] || lambda { |resource| }
teardown = options[:teardown] || lambda {}
report_cycles_in_graph
enqueue_roots
deferred_resources = []
while continue_while.call() && (resource = next_resource)
if resource.suitable?
made_progress = true
pre_process.call(resource)
yield resource
finish(resource)
else
deferred_resources << resource
end
if @ready.empty? and deferred_resources.any?
if made_progress
enqueue(*deferred_resources)
else
deferred_resources.each do |resource|
overly_deferred_resource_handler.call(resource)
finish(resource)
end
end
made_progress = false
deferred_resources = []
end
end
if !continue_while.call()
while (resource = next_resource)
canceled_resource_handler.call(resource)
finish(resource)
end
end
teardown.call()
end
private
def add_all_resources_as_vertices(catalog)
catalog.resources.each do |vertex|
add_vertex(vertex)
end
end
def build_manual_dependencies
vertices.each do |vertex|
vertex.builddepends.each do |edge|
add_edge(edge)
end
end
end
def build_autorequire_dependencies(catalog)
vertices.each do |vertex|
vertex.autorequire(catalog).each do |edge|
# don't let automatic relationships conflict with manual ones.
next if edge?(edge.source, edge.target)
if edge?(edge.target, edge.source)
vertex.debug "Skipping automatic relationship with #{edge.source}"
else
vertex.debug "Autorequiring #{edge.source}"
add_edge(edge)
end
end
end
end
# Impose our container information on another graph by using it
- # to replace any container vertices X with a pair of verticies
- # { admissible_X and completed_X } such that that
+ # to replace any container vertices X with a pair of vertices
+ # { admissible_X and completed_X } such that
#
# 0) completed_X depends on admissible_X
# 1) contents of X each depend on admissible_X
# 2) completed_X depends on each on the contents of X
- # 3) everything which depended on X depens on completed_X
+ # 3) everything which depended on X depends on completed_X
# 4) admissible_X depends on everything X depended on
# 5) the containers and their edges must be removed
#
# Note that this requires attention to the possible case of containers
# which contain or depend on other containers, but has the advantage
# that the number of new edges created scales linearly with the number
- # of contained verticies regardless of how containers are related;
+ # of contained vertices regardless of how containers are related;
# alternatives such as replacing container-edges with content-edges
- # scale as the product of the number of external dependences, which is
+ # scale as the product of the number of external dependencies, which is
# to say geometrically in the case of nested / chained containers.
#
Default_label = { :callback => :refresh, :event => :ALL_EVENTS }
def replace_containers_with_anchors(catalog)
stage_class = Puppet::Type.type(:stage)
whit_class = Puppet::Type.type(:whit)
component_class = Puppet::Type.type(:component)
containers = catalog.resources.find_all { |v| (v.is_a?(component_class) or v.is_a?(stage_class)) and vertex?(v) }
#
# These two hashes comprise the aforementioned attention to the possible
# case of containers that contain / depend on other containers; they map
- # containers to their sentinels but pass other verticies through. Thus we
- # can "do the right thing" for references to other verticies that may or
+ # containers to their sentinels but pass other vertices through. Thus we
+ # can "do the right thing" for references to other vertices that may or
# may not be containers.
#
admissible = Hash.new { |h,k| k }
completed = Hash.new { |h,k| k }
containers.each { |x|
admissible[x] = whit_class.new(:name => "admissible_#{x.ref}", :catalog => catalog)
completed[x] = whit_class.new(:name => "completed_#{x.ref}", :catalog => catalog)
priority = @prioritizer.priority_of(x)
add_vertex(admissible[x], priority)
add_vertex(completed[x], priority)
}
#
# Implement the six requirements listed above
#
containers.each { |x|
contents = catalog.adjacent(x, :direction => :out)
add_edge(admissible[x],completed[x]) if contents.empty? # (0)
contents.each { |v|
add_edge(admissible[x],admissible[v],Default_label) # (1)
add_edge(completed[v], completed[x], Default_label) # (2)
}
# (3) & (5)
adjacent(x,:direction => :in,:type => :edges).each { |e|
add_edge(completed[e.source],admissible[x],e.label)
remove_edge! e
}
# (4) & (5)
adjacent(x,:direction => :out,:type => :edges).each { |e|
add_edge(completed[x],admissible[e.target],e.label)
remove_edge! e
}
}
containers.each { |x| remove_vertex! x } # (5)
end
end
diff --git a/lib/puppet/indirector.rb b/lib/puppet/indirector.rb
index 65f852ba8..9385a615d 100644
--- a/lib/puppet/indirector.rb
+++ b/lib/puppet/indirector.rb
@@ -1,60 +1,61 @@
# Manage indirections to termini. They are organized in terms of indirections -
# - e.g., configuration, node, file, certificate -- and each indirection has one
# or more terminus types defined. The indirection is configured via the
# +indirects+ method, which will be called by the class extending itself
# with this module.
module Puppet::Indirector
# LAK:FIXME We need to figure out how to handle documentation for the
# different indirection types.
require 'puppet/indirector/indirection'
require 'puppet/indirector/terminus'
+ require 'puppet/indirector/code'
require 'puppet/indirector/envelope'
require 'puppet/network/format_support'
def self.configure_routes(application_routes)
application_routes.each do |indirection_name, termini|
indirection_name = indirection_name.to_sym
terminus_name = termini["terminus"]
cache_name = termini["cache"]
Puppet::Indirector::Terminus.terminus_class(indirection_name, terminus_name || cache_name)
indirection = Puppet::Indirector::Indirection.instance(indirection_name)
raise "Indirection #{indirection_name} does not exist" unless indirection
indirection.terminus_class = terminus_name if terminus_name
indirection.cache_class = cache_name if cache_name
end
end
# Declare that the including class indirects its methods to
# this terminus. The terminus name must be the name of a Puppet
# default, not the value -- if it's the value, then it gets
# evaluated at parse time, which is before the user has had a chance
# to override it.
def indirects(indirection, options = {})
raise(ArgumentError, "Already handling indirection for #{@indirection.name}; cannot also handle #{indirection}") if @indirection
# populate this class with the various new methods
extend ClassMethods
include Puppet::Indirector::Envelope
include Puppet::Network::FormatSupport
# record the indirected class name for documentation purposes
options[:indirected_class] = name
# instantiate the actual Terminus for that type and this name (:ldap, w/ args :node)
# & hook the instantiated Terminus into this class (Node: @indirection = terminus)
@indirection = Puppet::Indirector::Indirection.new(self, indirection, options)
end
module ClassMethods
attr_reader :indirection
end
# Helper definition for indirections that handle filenames.
BadNameRegexp = Regexp.union(/^\.\./,
%r{[\\/]},
"\0",
/(?i)^[a-z]:/)
end
diff --git a/lib/puppet/indirector/catalog/compiler.rb b/lib/puppet/indirector/catalog/compiler.rb
index b2a3c251a..abe714b2c 100644
--- a/lib/puppet/indirector/catalog/compiler.rb
+++ b/lib/puppet/indirector/catalog/compiler.rb
@@ -1,202 +1,175 @@
require 'puppet/node'
require 'puppet/resource/catalog'
require 'puppet/indirector/code'
require 'puppet/util/profiler'
require 'yaml'
class Puppet::Resource::Catalog::Compiler < Puppet::Indirector::Code
desc "Compiles catalogs on demand using Puppet's compiler."
include Puppet::Util
attr_accessor :code
def extract_facts_from_request(request)
return unless text_facts = request.options[:facts]
unless format = request.options[:facts_format]
raise ArgumentError, "Facts but no fact format provided for #{request.key}"
end
Puppet::Util::Profiler.profile("Found facts") do
# If the facts were encoded as yaml, then the param reconstitution system
# in Network::HTTP::Handler will automagically deserialize the value.
if text_facts.is_a?(Puppet::Node::Facts)
facts = text_facts
else
- # We unescape here because the corrosponding code in Puppet::Configurer::FactHandler escapes
+ # We unescape here because the corresponding code in Puppet::Configurer::FactHandler escapes
facts = Puppet::Node::Facts.convert_from(format, CGI.unescape(text_facts))
end
unless facts.name == request.key
raise Puppet::Error, "Catalog for #{request.key.inspect} was requested with fact definition for the wrong node (#{facts.name.inspect})."
end
facts.add_timestamp
Puppet::Node::Facts.indirection.save(facts)
end
end
# Compile a node's catalog.
def find(request)
extract_facts_from_request(request)
node = node_from_request(request)
- node.trusted_data = trusted_hash_from_request(request)
+ node.trusted_data = Puppet.lookup(:trusted_information) { Puppet::Context::TrustedInformation.local(node) }.to_h
if catalog = compile(node)
return catalog
else
# This shouldn't actually happen; we should either return
# a config or raise an exception.
return nil
end
end
-
# filter-out a catalog to remove exported resources
def filter(catalog)
return catalog.filter { |r| r.virtual? } if catalog.respond_to?(:filter)
catalog
end
def initialize
Puppet::Util::Profiler.profile("Setup server facts for compiling") do
set_server_facts
end
end
# Is our compiler part of a network, or are we just local?
def networked?
Puppet.run_mode.master?
end
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.
node.merge(@server_facts)
end
# Compile the actual catalog.
def compile(node)
str = "Compiled catalog for #{node.name}"
str += " in environment #{node.environment}" if node.environment
config = nil
benchmark(:notice, str) do
Puppet::Util::Profiler.profile(str) do
begin
config = Puppet::Parser::Compiler.compile(node)
rescue Puppet::Error => detail
Puppet.err(detail.to_s) if networked?
raise
end
end
end
config
end
# Turn our host name into a node object.
def find_node(name, environment)
Puppet::Util::Profiler.profile("Found node information") do
node = nil
begin
node = Puppet::Node.indirection.find(name, :environment => environment)
rescue => detail
message = "Failed when searching for node #{name}: #{detail}"
Puppet.log_exception(detail, message)
- raise Puppet::Error, message
+ raise Puppet::Error, message, detail.backtrace
end
# Add any external data to the node.
if node
add_node_data(node)
end
node
end
end
# Extract the node from the request, or use the request
# to find the node.
def node_from_request(request)
if node = request.options[:use_node]
if request.remote?
raise Puppet::Error, "Invalid option use_node for a remote request"
else
return node
end
end
# We rely on our authorization system to determine whether the connected
# node is allowed to compile the catalog's node referenced by key.
# By default the REST authorization system makes sure only the connected node
# can compile his catalog.
# This allows for instance monitoring systems or puppet-load to check several
# node's catalog with only one certificate and a modification to auth.conf
# If no key is provided we can only compile the currently connected node.
name = request.key || request.node
if node = find_node(name, request.environment)
return node
end
raise ArgumentError, "Could not find node '#{name}'; cannot compile"
end
# Initialize our server fact hash; we add these to each client, and they
# won't change while we're running, so it's safe to cache the values.
def set_server_facts
@server_facts = {}
# Add our server version to the fact list
@server_facts["serverversion"] = Puppet.version.to_s
# And then add the server name and IP
{"servername" => "fqdn",
"serverip" => "ipaddress"
}.each do |var, fact|
if value = Facter.value(fact)
@server_facts[var] = value
else
Puppet.warning "Could not retrieve fact #{fact}"
end
end
if @server_facts["servername"].nil?
host = Facter.value(:hostname)
if domain = Facter.value(:domain)
@server_facts["servername"] = [host, domain].join(".")
else
@server_facts["servername"] = host
end
end
end
end
diff --git a/lib/puppet/indirector/catalog/msgpack.rb b/lib/puppet/indirector/catalog/msgpack.rb
new file mode 100644
index 000000000..a0e5bfd76
--- /dev/null
+++ b/lib/puppet/indirector/catalog/msgpack.rb
@@ -0,0 +1,6 @@
+require 'puppet/resource/catalog'
+require 'puppet/indirector/msgpack'
+
+class Puppet::Resource::Catalog::Msgpack < Puppet::Indirector::Msgpack
+ desc "Store catalogs as flat files, serialized using MessagePack."
+end
diff --git a/lib/puppet/indirector/catalog/static_compiler.rb b/lib/puppet/indirector/catalog/static_compiler.rb
index fe3bfb5d7..a8b6bb582 100644
--- a/lib/puppet/indirector/catalog/static_compiler.rb
+++ b/lib/puppet/indirector/catalog/static_compiler.rb
@@ -1,218 +1,214 @@
require 'puppet/node'
require 'puppet/resource/catalog'
-require 'puppet/indirector/code'
+require 'puppet/indirector/catalog/compiler'
-class Puppet::Resource::Catalog::StaticCompiler < Puppet::Indirector::Code
+class Puppet::Resource::Catalog::StaticCompiler < Puppet::Resource::Catalog::Compiler
desc %q{Compiles catalogs on demand using the optional static compiler. This
functions similarly to the normal compiler, but it replaces puppet:/// file
URLs with explicit metadata and file content hashes, expecting puppet agent
to fetch the exact specified content from the filebucket. This guarantees
that a given catalog will always result in the same file states. It also
decreases catalog application time and fileserver load, at the cost of
increased compilation time.
This terminus works today, but cannot be used without additional
configuration. Specifically:
* You must create a special filebucket resource --- with the title `puppet`
and the `path` attribute set to `false` --- in site.pp or somewhere else
where it will be added to every node's catalog. Using `puppet` as the title
is mandatory; the static compiler treats this title as magical.
filebucket { puppet:
path => false,
}
* You must set `catalog_terminus = static_compiler` in the puppet
master's puppet.conf.
* The puppet master's auth.conf must allow authenticated nodes to access the
`file_bucket_file` endpoint. This is enabled by default (see the
`path /file` rule), but if you have made your auth.conf more restrictive,
you may need to re-enable it.)
* If you are using multiple puppet masters, you must configure load balancer
affinity for agent nodes. This is because puppet masters other than the one
that compiled a given catalog may not have stored the required file contents
in their filebuckets.}
- def compiler
- @compiler ||= indirection.terminus(:compiler)
- end
-
def find(request)
- return nil unless catalog = compiler.find(request)
+ return nil unless catalog = super
raise "Did not get catalog back" unless catalog.is_a?(model)
catalog.resources.find_all { |res| res.type == "File" }.each do |resource|
next unless source = resource[:source]
next unless source =~ /^puppet:/
file = resource.to_ral
if file.recurse?
add_children(request.key, catalog, resource, file)
else
find_and_replace_metadata(request.key, resource, file)
end
end
catalog
end
# Take a resource with a fileserver based file source remove the source
# parameter, and insert the file metadata into the resource.
#
# This method acts to do the fileserver metadata retrieval in advance, while
# the file source is local and doesn't require an HTTP request. It retrieves
# the file metadata for a given file resource, removes the source parameter
# from the resource, inserts the metadata into the file resource, and uploads
# the file contents of the source to the file bucket.
#
# @param host [String] The host name of the node requesting this catalog
# @param resource [Puppet::Resource] The resource to replace the metadata in
# @param file [Puppet::Type::File] The file RAL associated with the resource
def find_and_replace_metadata(host, resource, file)
# We remove URL info from it, so it forces a local copy
# rather than routing through the network.
# Weird, but true.
newsource = file[:source][0].sub("puppet:///", "")
file[:source][0] = newsource
raise "Could not get metadata for #{resource[:source]}" unless metadata = file.parameter(:source).metadata
replace_metadata(host, resource, metadata)
end
# Rewrite a given file resource with the metadata from a fileserver based file
#
# This performs the actual metadata rewrite for the given file resource and
# uploads the content of the source file to the filebucket.
#
# @param host [String] The host name of the node requesting this catalog
# @param resource [Puppet::Resource] The resource to add the metadata to
# @param metadata [Puppet::FileServing::Metadata] The metadata of the given fileserver based file
def replace_metadata(host, resource, metadata)
[:mode, :owner, :group].each do |param|
resource[param] ||= metadata.send(param)
end
resource[:ensure] = metadata.ftype
if metadata.ftype == "file"
unless resource[:content]
resource[:content] = metadata.checksum
resource[:checksum] = metadata.checksum_type
end
end
store_content(resource) if resource[:ensure] == "file"
old_source = resource.delete(:source)
Puppet.info "Metadata for #{resource} in catalog for '#{host}' added from '#{old_source}'"
end
# Generate children resources for a recursive file and add them to the catalog.
#
# @param host [String] The host name of the node requesting this catalog
# @param catalog [Puppet::Resource::Catalog]
# @param resource [Puppet::Resource]
# @param file [Puppet::Type::File] The file RAL associated with the resource
def add_children(host, catalog, resource, file)
file = resource.to_ral
children = get_child_resources(host, catalog, resource, file)
remove_existing_resources(children, catalog)
children.each do |name, res|
catalog.add_resource res
catalog.add_edge(resource, res)
end
end
# Given a recursive file resource, recursively generate its children resources
#
# @param host [String] The host name of the node requesting this catalog
# @param catalog [Puppet::Resource::Catalog]
# @param resource [Puppet::Resource]
# @param file [Puppet::Type::File] The file RAL associated with the resource
#
# @return [Array<Puppet::Resource>] The recursively generated File resources for the given resource
def get_child_resources(host, catalog, resource, file)
sourceselect = file[:sourceselect]
children = {}
source = resource[:source]
# This is largely a copy of recurse_remote in File
total = file[:source].collect do |source|
next unless result = file.perform_recursion(source)
return if top = result.find { |r| r.relative_path == "." } and top.ftype != "directory"
result.each { |data| data.source = "#{source}/#{data.relative_path}" }
break result if result and ! result.empty? and sourceselect == :first
result
end.flatten.compact
# This only happens if we have sourceselect == :all
unless sourceselect == :first
found = []
total.reject! do |data|
result = found.include?(data.relative_path)
found << data.relative_path unless found.include?(data.relative_path)
result
end
end
total.each do |meta|
# This is the top-level parent directory
if meta.relative_path == "."
replace_metadata(host, resource, meta)
next
end
children[meta.relative_path] ||= Puppet::Resource.new(:file, File.join(file[:path], meta.relative_path))
# I think this is safe since it's a URL, not an actual file
children[meta.relative_path][:source] = source + "/" + meta.relative_path
resource.each do |param, value|
# These should never be passed to our children.
unless [:parent, :ensure, :recurse, :recurselimit, :target, :alias, :source].include? param
children[meta.relative_path][param] = value
end
end
replace_metadata(host, children[meta.relative_path], meta)
end
children
end
# Remove any file resources in the catalog that will be duplicated by the
# given file resources.
#
# @param children [Array<Puppet::Resource>]
# @param catalog [Puppet::Resource::Catalog]
def remove_existing_resources(children, catalog)
existing_names = catalog.resources.collect { |r| r.to_s }
both = (existing_names & children.keys).inject({}) { |hash, name| hash[name] = true; hash }
both.each { |name| children.delete(name) }
end
# Retrieve the source of a file resource using a fileserver based source and
# upload it to the filebucket.
#
# @param resource [Puppet::Resource]
def store_content(resource)
@summer ||= Object.new
@summer.extend(Puppet::Util::Checksums)
type = @summer.sumtype(resource[:content])
sum = @summer.sumdata(resource[:content])
if Puppet::FileBucket::File.indirection.find("#{type}/#{sum}")
Puppet.info "Content for '#{resource[:source]}' already exists"
else
Puppet.info "Storing content for source '#{resource[:source]}'"
content = Puppet::FileServing::Content.indirection.find(resource[:source])
file = Puppet::FileBucket::File.new(content.content)
Puppet::FileBucket::File.indirection.save(file)
end
end
end
diff --git a/lib/puppet/indirector/data_binding/hiera.rb b/lib/puppet/indirector/data_binding/hiera.rb
index 9ff640b2e..7fbc63782 100644
--- a/lib/puppet/indirector/data_binding/hiera.rb
+++ b/lib/puppet/indirector/data_binding/hiera.rb
@@ -1,50 +1,50 @@
require 'puppet/indirector/code'
require 'hiera/scope'
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)
+ if Puppet::FileSystem.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 dba4d60bd..a043c425e 100644
--- a/lib/puppet/indirector/direct_file_server.rb
+++ b/lib/puppet/indirector/direct_file_server.rb
@@ -1,19 +1,19 @@
require 'puppet/file_serving/terminus_helper'
require 'puppet/indirector/terminus'
class Puppet::Indirector::DirectFileServer < Puppet::Indirector::Terminus
include Puppet::FileServing::TerminusHelper
def find(request)
- return nil unless Puppet::FileSystem::File.exist?(request.key)
+ return nil unless Puppet::FileSystem.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 Puppet::FileSystem::File.exist?(request.key)
+ return nil unless Puppet::FileSystem.exist?(request.key)
path2instances(request, request.key)
end
end
diff --git a/lib/puppet/indirector/exec.rb b/lib/puppet/indirector/exec.rb
index 19f028340..4a65ebeb4 100644
--- a/lib/puppet/indirector/exec.rb
+++ b/lib/puppet/indirector/exec.rb
@@ -1,38 +1,38 @@
require 'puppet/indirector/terminus'
require 'puppet/util'
class Puppet::Indirector::Exec < Puppet::Indirector::Terminus
# Look for external node definitions.
def find(request)
name = request.key
external_command = command
# Make sure it's an array
raise Puppet::DevError, "Exec commands must be an array" unless external_command.is_a?(Array)
# Make sure it's fully qualified.
raise ArgumentError, "You must set the exec parameter to a fully qualified command" unless Puppet::Util.absolute_path?(external_command[0])
# Add our name to it.
external_command << name
begin
output = execute(external_command, :failonfail => true, :combine => false)
rescue Puppet::ExecutionFailure => detail
- raise Puppet::Error, "Failed to find #{name} via exec: #{detail}"
+ raise Puppet::Error, "Failed to find #{name} via exec: #{detail}", detail.backtrace
end
if output =~ /\A\s*\Z/ # all whitespace
Puppet.debug "Empty response for #{name} from #{self.name} terminus"
return nil
else
return output
end
end
private
# Proxy the execution, so it's easier to test.
def execute(command, arguments)
Puppet::Util::Execution.execute(command,arguments)
end
end
diff --git a/lib/puppet/indirector/face.rb b/lib/puppet/indirector/face.rb
index 2bf1174b6..10fa27ef0 100644
--- a/lib/puppet/indirector/face.rb
+++ b/lib/puppet/indirector/face.rb
@@ -1,138 +1,139 @@
require 'puppet/face'
class Puppet::Indirector::Face < Puppet::Face
option "--terminus TERMINUS" do
summary "The indirector terminus to use."
description <<-EOT
Indirector faces expose indirected subsystems of Puppet. These
subsystems are each able to retrieve and alter a specific type of data
(with the familiar actions of `find`, `search`, `save`, and `destroy`)
from an arbitrary number of pluggable backends. In Puppet parlance,
these backends are called terminuses.
Almost all indirected subsystems have a `rest` terminus that interacts
with the puppet master's data. Most of them have additional terminuses
for various local data models, which are in turn used by the indirected
subsystem on the puppet master whenever it receives a remote request.
The terminus for an action is often determined by context, but
occasionally needs to be set explicitly. See the "Notes" section of this
face's manpage for more details.
EOT
before_action do |action, args, options|
set_terminus(options[:terminus])
end
after_action do |action, args, options|
indirection.reset_terminus_class
end
end
def self.indirections
Puppet::Indirector::Indirection.instances.collect { |t| t.to_s }.sort
end
def self.terminus_classes(indirection)
Puppet::Indirector::Terminus.terminus_classes(indirection.to_sym).collect { |t| t.to_s }.sort
end
def call_indirection_method(method, key, options)
begin
result = indirection.__send__(method, key, options)
rescue => detail
message = "Could not call '#{method}' on '#{indirection_name}': #{detail}"
Puppet.log_exception(detail, message)
- raise message
+ raise RuntimeError, message, detail.backtrace
end
return result
end
option "--extra HASH" do
summary "Extra arguments to pass to the indirection request"
description <<-EOT
A terminus can take additional arguments to refine the operation, which
are passed as an arbitrary hash to the back-end. Anything passed as
the extra value is just send direct to the back-end.
EOT
default_to do Hash.new end
end
action :destroy do
summary "Delete an object."
arguments "<key>"
when_invoked {|key, options| call_indirection_method :destroy, key, options[:extra] }
end
action :find do
summary "Retrieve an object by name."
arguments "<key>"
when_invoked {|key, options| call_indirection_method :find, key, options[:extra] }
end
action :save do
summary "API only: create or overwrite an object."
arguments "<key>"
description <<-EOT
API only: create or overwrite an object. As the Faces framework does not
currently accept data from STDIN, save actions cannot currently be invoked
from the command line.
EOT
when_invoked {|key, options| call_indirection_method :save, key, options[:extra] }
end
action :search do
summary "Search for an object or retrieve multiple objects."
arguments "<query>"
when_invoked {|key, options| call_indirection_method :search, key, options[:extra] }
end
# Print the configuration for the current terminus class
action :info do
summary "Print the default terminus class for this face."
description <<-EOT
Prints the default terminus class for this subcommand. Note that different
run modes may have different default termini; when in doubt, specify the
run mode with the '--run_mode' option.
EOT
when_invoked do |options|
if t = indirection.terminus_class
"Run mode '#{Puppet.run_mode.name}': #{t}"
else
"No default terminus class for run mode '#{Puppet.run_mode.name}'"
end
end
end
attr_accessor :from
def indirection_name
@indirection_name || name.to_sym
end
# Here's your opportunity to override the indirection name. By default it
# will be the same name as the face.
def set_indirection_name(name)
@indirection_name = name
end
# Return an indirection associated with a face, if one exists;
# One usually does.
def indirection
unless @indirection
@indirection = Puppet::Indirector::Indirection.instance(indirection_name)
@indirection or raise "Could not find terminus for #{indirection_name}"
end
@indirection
end
def set_terminus(from)
begin
indirection.terminus_class = from
rescue => detail
- raise "Could not set '#{indirection.name}' terminus to '#{from}' (#{detail}); valid terminus types are #{self.class.terminus_classes(indirection.name).join(", ") }"
+ msg = "Could not set '#{indirection.name}' terminus to '#{from}' (#{detail}); valid terminus types are #{self.class.terminus_classes(indirection.name).join(", ") }"
+ raise detail, msg, detail.backtrace
end
end
end
diff --git a/lib/puppet/indirector/facts/facter.rb b/lib/puppet/indirector/facts/facter.rb
index 4b44b5812..9f70a7c46 100644
--- a/lib/puppet/indirector/facts/facter.rb
+++ b/lib/puppet/indirector/facts/facter.rb
@@ -1,91 +1,91 @@
require 'puppet/node/facts'
require 'puppet/indirector/code'
class Puppet::Node::Facts::Facter < Puppet::Indirector::Code
desc "Retrieve facts from Facter. This provides a somewhat abstract interface
between Puppet and Facter. It's only `somewhat` abstract because it always
returns the local host's facts, regardless of what you attempt to find."
private
def self.reload_facter
Facter.clear
Facter.loadfacts
end
def self.load_fact_plugins
# Add any per-module fact directories to the factpath
module_fact_dirs = Puppet[:modulepath].split(File::PATH_SEPARATOR).collect do |d|
["lib", "plugins"].map do |subdirectory|
Dir.glob("#{d}/*/#{subdirectory}/facter")
end
end.flatten
dirs = module_fact_dirs + Puppet[:factpath].split(File::PATH_SEPARATOR)
dirs.uniq.each do |dir|
load_facts_in_dir(dir)
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
+ Facter.search_external external_facts_dirs
end
def self.load_facts_in_dir(dir)
return unless FileTest.directory?(dir)
Dir.chdir(dir) do
Dir.glob("*.rb").each do |file|
fqfile = ::File.join(dir, file)
begin
Puppet.info "Loading facts in #{fqfile}"
::Timeout::timeout(Puppet[:configtimeout]) do
load file
end
rescue SystemExit,NoMemoryError
raise
rescue Exception => detail
Puppet.warning "Could not load fact file #{fqfile}: #{detail}"
end
end
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)
result.add_local_facts
Puppet[:stringify_facts] ? result.stringify : result.sanitize
result
end
def save(facts)
raise Puppet::DevError, "You cannot save facts to the code store; it is only used for getting facts from Facter"
end
end
diff --git a/lib/puppet/indirector/file_bucket_file/file.rb b/lib/puppet/indirector/file_bucket_file/file.rb
index 59be12451..f9669e987 100644
--- a/lib/puppet/indirector/file_bucket_file/file.rb
+++ b/lib/puppet/indirector/file_bucket_file/file.rb
@@ -1,128 +1,138 @@
require 'puppet/indirector/code'
require 'puppet/file_bucket/file'
require 'puppet/util/checksums'
require 'fileutils'
module Puppet::FileBucketFile
class File < Puppet::Indirector::Code
include Puppet::Util::Checksums
desc "Store files in a directory set based on their checksums."
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 Puppet::FileSystem.exist?(contents_file) && 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}`
+ raise "could not find diff_with #{request.options[:diff_with]}" unless Puppet::FileSystem.exist?(other_contents_file)
+ return `diff #{Puppet::FileSystem.path_string(contents_file).inspect} #{Puppet::FileSystem.path_string(other_contents_file).inspect}`
else
Puppet.info "FileBucket read #{checksum}"
- model.new(contents_file.binread)
+ model.new(Puppet::FileSystem.binread(contents_file))
end
else
nil
end
end
def head(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')
- contents_file.exist? && matches(paths_file, files_original_path)
+ Puppet::FileSystem.exist?(contents_file) && matches(paths_file, files_original_path)
end
def save(request)
instance = request.instance
_, 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, contents_file, paths_file)
# don't echo the request content back to the agent
model.new('')
end
def validate_key(request)
# There are no ACLs on filebucket files so validating key is not important
end
private
+ # @param paths_file [Object] Opaque file path
+ # @param files_original_path [String]
+ #
def matches(paths_file, files_original_path)
- paths_file.open(0640, 'a+') do |f|
+ Puppet::FileSystem.open(paths_file, 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
file_handle.rewind
file_handle.each_line do |line|
return true if line.chomp == files_original_path
end
return false
end
+ # @param contents_file [Object] Opaque file path
+ # @param paths_file [Object] Opaque file path
+ #
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
+ unless Puppet::FileSystem.dir_exist?(paths_file)
+ Puppet::FileSystem.dir_mkpath(paths_file)
end
- paths_file.exclusive_open(0640, 'a+') do |f|
- if contents_file.exist?
+ Puppet::FileSystem.exclusive_open(paths_file, 0640, 'a+') do |f|
+ if Puppet::FileSystem.exist?(contents_file)
verify_identical_file!(contents_file, bucket_file)
- contents_file.touch
+ Puppet::FileSystem.touch(contents_file)
else
- contents_file.open(0440, 'wb') do |of|
+ Puppet::FileSystem.open(contents_file, 0440, 'wb') do |of|
of.write(bucket_file.contents)
end
end
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)
checksum_type, checksum, path = request.key.split(/\//, 3)
if path == '' # Treat "md5/<checksum>/" like "md5/<checksum>"
path = nil
end
raise "Unsupported checksum type #{checksum_type.inspect}" if checksum_type != 'md5'
raise "Invalid checksum #{checksum.inspect}" if checksum !~ /^[0-9a-f]{32}$/
[checksum, path]
end
+ # @return [Object] Opaque path as constructed by the Puppet::FileSystem
+ #
def path_for(bucket_path, digest, subfile = nil)
bucket_path ||= Puppet[:bucketdir]
dir = ::File.join(digest[0..7].split(""))
basedir = ::File.join(bucket_path, dir, digest)
- Puppet::FileSystem::File.new(subfile ? ::File.join(basedir, subfile) : basedir)
+ Puppet::FileSystem.pathname(subfile ? ::File.join(basedir, subfile) : basedir)
end
+ # @param contents_file [Object] Opaque file path
+ # @param bucket_file [IO]
def verify_identical_file!(contents_file, bucket_file)
- if bucket_file.contents.size == contents_file.size
- if contents_file.compare_stream(bucket_file.stream)
+ if bucket_file.contents.size == Puppet::FileSystem.size(contents_file)
+ if Puppet::FileSystem.compare_stream(contents_file, bucket_file.stream)
Puppet.info "FileBucket got a duplicate file #{bucket_file.checksum}"
return
end
end
# If the contents or sizes don't match, then we've found a conflict.
# Unlikely, but quite bad.
raise Puppet::FileBucket::BucketError, "Got passed new contents for sum #{bucket_file.checksum}"
end
end
end
diff --git a/lib/puppet/indirector/file_metadata/file.rb b/lib/puppet/indirector/file_metadata/file.rb
index 9d8f839b3..78dfe5786 100644
--- a/lib/puppet/indirector/file_metadata/file.rb
+++ b/lib/puppet/indirector/file_metadata/file.rb
@@ -1,22 +1,22 @@
require 'puppet/file_serving/metadata'
require 'puppet/indirector/file_metadata'
require 'puppet/indirector/direct_file_server'
class Puppet::Indirector::FileMetadata::File < Puppet::Indirector::DirectFileServer
desc "Retrieve file metadata directly from the local filesystem."
def find(request)
return unless data = super
- data.collect
+ data.collect(request.options[:source_permissions])
data
end
def search(request)
return unless result = super
result.each { |instance| instance.collect }
result
end
end
diff --git a/lib/puppet/indirector/file_server.rb b/lib/puppet/indirector/file_server.rb
index 9516a404c..389951dc3 100644
--- a/lib/puppet/indirector/file_server.rb
+++ b/lib/puppet/indirector/file_server.rb
@@ -1,65 +1,65 @@
require 'puppet/file_serving/configuration'
require 'puppet/file_serving/fileset'
require 'puppet/file_serving/terminus_helper'
require 'puppet/indirector/terminus'
# Look files up using the file server.
class Puppet::Indirector::FileServer < Puppet::Indirector::Terminus
include Puppet::FileServing::TerminusHelper
# Is the client authorized to perform this action?
def authorized?(request)
return false unless [:find, :search].include?(request.method)
mount, file_path = configuration.split_path(request)
# If we're not serving this mount, then access is denied.
return false unless mount
mount.allowed?(request.node, request.ip)
end
# Find our key using the fileserver.
def find(request)
mount, relative_path = configuration.split_path(request)
return nil unless mount
# The mount checks to see if the file exists, and returns nil
# if not.
return nil unless path = mount.find(relative_path, request)
result = model.new(path)
result.links = request.options[:links] if request.options[:links]
- result.collect
+ result.collect(request.options[:source_permissions])
result
end
# Search for files. This returns an array rather than a single
# file.
def search(request)
mount, relative_path = configuration.split_path(request)
unless mount and paths = mount.search(relative_path, request)
Puppet.info "Could not find filesystem info for file '#{request.key}' in environment #{request.environment}"
return nil
end
filesets = paths.collect do |path|
# Filesets support indirector requests as an options collection
Puppet::FileServing::Fileset.new(path, request)
end
Puppet::FileServing::Fileset.merge(*filesets).collect do |file, base_path|
inst = model.new(base_path, :relative_path => file)
inst.links = request.options[:links] if request.options[:links]
inst.collect
inst
end
end
private
# Our fileserver configuration, if needed.
def configuration
Puppet::FileServing::Configuration.configuration
end
end
diff --git a/lib/puppet/indirector/indirection.rb b/lib/puppet/indirector/indirection.rb
index 0c0cb2075..a22f465ac 100644
--- a/lib/puppet/indirector/indirection.rb
+++ b/lib/puppet/indirector/indirection.rb
@@ -1,336 +1,336 @@
require 'puppet/util/docs'
require 'puppet/util/profiler'
require 'puppet/util/methodhelper'
require 'puppet/indirector/envelope'
require 'puppet/indirector/request'
require 'puppet/util/instrumentation/instrumentable'
# The class that connects functional classes with their different collection
# back-ends. Each indirection has a set of associated terminus classes,
# each of which is a subclass of Puppet::Indirector::Terminus.
class Puppet::Indirector::Indirection
include Puppet::Util::MethodHelper
include Puppet::Util::Docs
extend Puppet::Util::Instrumentation::Instrumentable
attr_accessor :name, :model
attr_reader :termini
probe :find, :label => Proc.new { |parent, key, *args| "find_#{parent.name}_#{parent.terminus_class}" }, :data => Proc.new { |parent, key, *args| { :key => key }}
probe :save, :label => Proc.new { |parent, key, *args| "save_#{parent.name}_#{parent.terminus_class}" }, :data => Proc.new { |parent, key, *args| { :key => key }}
probe :search, :label => Proc.new { |parent, key, *args| "search_#{parent.name}_#{parent.terminus_class}" }, :data => Proc.new { |parent, key, *args| { :key => key }}
probe :destroy, :label => Proc.new { |parent, key, *args| "destroy_#{parent.name}_#{parent.terminus_class}" }, :data => Proc.new { |parent, key, *args| { :key => key }}
@@indirections = []
# Find an indirection by name. This is provided so that Terminus classes
# can specifically hook up with the indirections they are associated with.
def self.instance(name)
@@indirections.find { |i| i.name == name }
end
# Return a list of all known indirections. Used to generate the
# reference.
def self.instances
@@indirections.collect { |i| i.name }
end
# Find an indirected model by name. This is provided so that Terminus classes
# can specifically hook up with the indirections they are associated with.
def self.model(name)
return nil unless match = @@indirections.find { |i| i.name == name }
match.model
end
# Create and return our cache terminus.
def cache
raise(Puppet::DevError, "Tried to cache when no cache class was set") unless cache_class
terminus(cache_class)
end
# Should we use a cache?
def cache?
cache_class ? true : false
end
attr_reader :cache_class
# Define a terminus class to be used for caching.
def cache_class=(class_name)
validate_terminus_class(class_name) if class_name
@cache_class = class_name
end
# This is only used for testing.
def delete
@@indirections.delete(self) if @@indirections.include?(self)
end
# Set the time-to-live for instances created through this indirection.
def ttl=(value)
raise ArgumentError, "Indirection TTL must be an integer" unless value.is_a?(Fixnum)
@ttl = value
end
# Default to the runinterval for the ttl.
def ttl
@ttl ||= Puppet[:runinterval]
end
# Calculate the expiration date for a returned instance.
def expiration
Time.now + ttl
end
# Generate the full doc string.
def doc
text = ""
text << scrub(@doc) << "\n\n" if @doc
text << "* **Indirected Class**: `#{@indirected_class}`\n";
if terminus_setting
text << "* **Terminus Setting**: #{terminus_setting}\n"
end
text
end
def initialize(model, name, options = {})
@model = model
@name = name
@termini = {}
@cache_class = nil
@terminus_class = nil
raise(ArgumentError, "Indirection #{@name} is already defined") if @@indirections.find { |i| i.name == @name }
@@indirections << self
@indirected_class = options.delete(:indirected_class)
if mod = options[:extend]
extend(mod)
options.delete(:extend)
end
# This is currently only used for cache_class and terminus_class.
set_options(options)
end
# Set up our request object.
def request(*args)
Puppet::Indirector::Request.new(self.name, *args)
end
# Return the singleton terminus for this indirection.
def terminus(terminus_name = nil)
# Get the name of the terminus.
raise Puppet::DevError, "No terminus specified for #{self.name}; cannot redirect" unless terminus_name ||= terminus_class
termini[terminus_name] ||= make_terminus(terminus_name)
end
# This can be used to select the terminus class.
attr_accessor :terminus_setting
# Determine the terminus class.
def terminus_class
unless @terminus_class
if setting = self.terminus_setting
self.terminus_class = Puppet.settings[setting]
else
raise Puppet::DevError, "No terminus class nor terminus setting was provided for indirection #{self.name}"
end
end
@terminus_class
end
def reset_terminus_class
@terminus_class = nil
end
# Specify the terminus class to use.
def terminus_class=(klass)
validate_terminus_class(klass)
@terminus_class = klass
end
# This is used by terminus_class= and cache=.
def validate_terminus_class(terminus_class)
raise ArgumentError, "Invalid terminus name #{terminus_class.inspect}" unless terminus_class and terminus_class.to_s != ""
unless Puppet::Indirector::Terminus.terminus_class(self.name, terminus_class)
raise ArgumentError, "Could not find terminus #{terminus_class} for indirection #{self.name}"
end
end
# Expire a cached object, if one is cached. Note that we don't actually
# remove it, we expire it and write it back out to disk. This way people
# can still use the expired object if they want.
def expire(key, options={})
request = request(:expire, key, nil, options)
return nil unless cache?
return nil unless instance = cache.find(request(:find, key, nil, options))
Puppet.info "Expiring the #{self.name} cache of #{instance.name}"
# Set an expiration date in the past
instance.expiration = Time.now - 60
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={})
request = request(:find, key, nil, options)
terminus = prepare(request)
result = find_in_cache(request)
if not result.nil?
result
elsif request.ignore_terminus?
nil
else
# Otherwise, return the result from the terminus, caching if
# appropriate.
result = terminus.find(request)
if not result.nil?
result.expiration ||= self.expiration if result.respond_to?(:expiration)
- if cache? and request.use_cache?
+ if cache?
Puppet.info "Caching #{self.name} for #{request.key}"
cache.save request(:save, key, result, options)
end
filtered = result
if terminus.respond_to?(:filter)
Puppet::Util::Profiler.profile("Filtered result for #{self.name} #{request.key}") do
filtered = terminus.filter(result)
end
end
filtered
end
end
end
# Search for an instance in the appropriate terminus, and return a
# boolean indicating whether the instance was found.
def head(key, options={})
request = request(:head, key, nil, options)
terminus = prepare(request)
# Look in the cache first, then in the terminus. Force the result
# to be a boolean.
!!(find_in_cache(request) || terminus.head(request))
end
def find_in_cache(request)
# See if our instance is in the cache and up to date.
return nil unless cache? and ! request.ignore_cache? and cached = cache.find(request)
if cached.expired?
Puppet.info "Not using expired #{self.name} for #{request.key} from cache; expired at #{cached.expiration}"
return nil
end
Puppet.debug "Using cached #{self.name} for #{request.key}"
cached
rescue => detail
Puppet.log_exception(detail, "Cached #{self.name} for #{request.key} failed: #{detail}")
nil
end
# Remove something via the terminus.
def destroy(key, options={})
request = request(:destroy, key, nil, options)
terminus = prepare(request)
result = terminus.destroy(request)
if cache? and cache.find(request(:find, key, nil, options))
# Reuse the existing request, since it's equivalent.
cache.destroy(request)
end
result
end
# Search for more than one instance. Should always return an array.
def search(key, options={})
request = request(:search, key, nil, options)
terminus = prepare(request)
if result = terminus.search(request)
raise Puppet::DevError, "Search results from terminus #{terminus.name} are not an array" unless result.is_a?(Array)
result.each do |instance|
next unless instance.respond_to? :expiration
instance.expiration ||= self.expiration
end
return result
end
end
# Save the instance in the appropriate terminus. This method is
# normally an instance method on the indirected class.
def save(instance, key = nil, options={})
request = request(:save, key, instance, options)
terminus = prepare(request)
result = terminus.save(request)
# If caching is enabled, save our document there
cache.save(request) if cache?
result
end
private
# Check authorization if there's a hook available; fail if there is one
# and it returns false.
def check_authorization(request, terminus)
# At this point, we're assuming authorization makes no sense without
# client information.
return unless request.node
# This is only to authorize via a terminus-specific authorization hook.
return unless terminus.respond_to?(:authorized?)
unless terminus.authorized?(request)
msg = "Not authorized to call #{request.method} on #{request}"
msg += " with #{request.options.inspect}" unless request.options.empty?
raise ArgumentError, msg
end
end
# Setup a request, pick the appropriate terminus, check the request's authorization, and return it.
def prepare(request)
# Pick our terminus.
if respond_to?(:select_terminus)
unless terminus_name = select_terminus(request)
raise ArgumentError, "Could not determine appropriate terminus for #{request}"
end
else
terminus_name = terminus_class
end
dest_terminus = terminus(terminus_name)
check_authorization(request, dest_terminus)
dest_terminus.validate(request)
dest_terminus
end
# Create a new terminus instance.
def make_terminus(terminus_class)
# Load our terminus class.
unless klass = Puppet::Indirector::Terminus.terminus_class(self.name, terminus_class)
raise ArgumentError, "Could not find terminus #{terminus_class} for indirection #{self.name}"
end
klass.new
end
end
diff --git a/lib/puppet/indirector/json.rb b/lib/puppet/indirector/json.rb
index 515ad33b6..3bfd83112 100644
--- a/lib/puppet/indirector/json.rb
+++ b/lib/puppet/indirector/json.rb
@@ -1,76 +1,76 @@
require 'puppet/indirector/terminus'
require 'puppet/util'
# The base class for JSON indirection terminus implementations.
#
# This should generally be preferred to the YAML base for any future
# implementations, since it is ~ three times faster despite being pure Ruby
# rather than a C implementation.
class Puppet::Indirector::JSON < Puppet::Indirector::Terminus
def find(request)
load_json_from_file(path(request.key), request.key)
end
def save(request)
filename = path(request.key)
FileUtils.mkdir_p(File.dirname(filename))
Puppet::Util.replace_file(filename, 0660) {|f| f.print to_json(request.instance) }
rescue TypeError => detail
Puppet.log_exception "Could not save #{self.name} #{request.key}: #{detail}"
end
def destroy(request)
- Puppet::FileSystem::File.unlink(path(request.key))
+ Puppet::FileSystem.unlink(path(request.key))
rescue => detail
unless detail.is_a? Errno::ENOENT
- raise Puppet::Error, "Could not destroy #{self.name} #{request.key}: #{detail}"
+ raise Puppet::Error, "Could not destroy #{self.name} #{request.key}: #{detail}", detail.backtrace
end
1 # emulate success...
end
def search(request)
Dir.glob(path(request.key)).collect do |file|
load_json_from_file(file, request.key)
end
end
# Return the path to a given node's file.
def path(name, ext = '.json')
if name =~ Puppet::Indirector::BadNameRegexp then
Puppet.crit("directory traversal detected in #{self.class}: #{name.inspect}")
raise ArgumentError, "invalid key"
end
base = Puppet.run_mode.master? ? Puppet[:server_datadir] : Puppet[:client_datadir]
File.join(base, self.class.indirection_name.to_s, name.to_s + ext)
end
private
def load_json_from_file(file, key)
json = nil
begin
json = File.read(file)
rescue Errno::ENOENT
return nil
rescue => detail
- raise Puppet::Error, "Could not read JSON data for #{indirection.name} #{key}: #{detail}"
+ raise Puppet::Error, "Could not read JSON data for #{indirection.name} #{key}: #{detail}", detail.backtrace
end
begin
return from_json(json)
rescue => detail
- raise Puppet::Error, "Could not parse JSON data for #{indirection.name} #{key}: #{detail}"
+ raise Puppet::Error, "Could not parse JSON data for #{indirection.name} #{key}: #{detail}", detail.backtrace
end
end
def from_json(text)
model.convert_from('pson', text)
end
def to_json(object)
object.render('pson')
end
end
diff --git a/lib/puppet/indirector/key/file.rb b/lib/puppet/indirector/key/file.rb
index 40f8a331d..81c428236 100644
--- a/lib/puppet/indirector/key/file.rb
+++ b/lib/puppet/indirector/key/file.rb
@@ -1,46 +1,49 @@
require 'puppet/indirector/ssl_file'
require 'puppet/ssl/key'
class Puppet::SSL::Key::File < Puppet::Indirector::SslFile
desc "Manage SSL private and public keys on disk."
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)
Puppet[:capub]
else
File.join(Puppet[:publickeydir], name.to_s + ".pem")
end
end
# Remove the public key, in addition to the private key
def destroy(request)
super
- return unless Puppet::FileSystem::File.exist?(public_key_path(request.key))
+ key_path = Puppet::FileSystem.pathname(public_key_path(request.key))
+ return unless Puppet::FileSystem.exist?(key_path)
begin
- Puppet::FileSystem::File.unlink(public_key_path(request.key))
+ Puppet::FileSystem.unlink(key_path)
rescue => detail
- raise Puppet::Error, "Could not remove #{request.key} public key: #{detail}"
+ raise Puppet::Error, "Could not remove #{request.key} public key: #{detail}", detail.backtrace
end
end
# Save the public key, in addition to the private key.
def save(request)
super
begin
- Puppet.settings.setting(:publickeydir).open_file(public_key_path(request.key), 'w') { |f| f.print request.instance.content.public_key.to_pem }
+ Puppet.settings.setting(:publickeydir).open_file(public_key_path(request.key), 'w') do |f|
+ f.print request.instance.content.public_key.to_pem
+ end
rescue => detail
- raise Puppet::Error, "Could not write #{request.key}: #{detail}"
+ raise Puppet::Error, "Could not write #{request.key}: #{detail}", detail.backtrace
end
end
end
diff --git a/lib/puppet/indirector/ldap.rb b/lib/puppet/indirector/ldap.rb
index 696cb4091..293c05e2b 100644
--- a/lib/puppet/indirector/ldap.rb
+++ b/lib/puppet/indirector/ldap.rb
@@ -1,79 +1,79 @@
require 'puppet/indirector/terminus'
require 'puppet/util/ldap/connection'
class Puppet::Indirector::Ldap < Puppet::Indirector::Terminus
# Perform our ldap search and process the result.
def find(request)
ldapsearch(search_filter(request.key)) { |entry| return process(entry) } || nil
end
# Process the found entry. We assume that we don't just want the
# ldap object.
def process(entry)
raise Puppet::DevError, "The 'process' method has not been overridden for the LDAP terminus for #{self.name}"
end
# Default to all attributes.
def search_attributes
nil
end
def search_base
Puppet[:ldapbase]
end
# The ldap search filter to use.
def search_filter(name)
raise Puppet::DevError, "No search string set for LDAP terminus for #{self.name}"
end
# Find the ldap node, return the class list and parent node specially,
# and everything else in a parameter hash.
def ldapsearch(filter)
raise ArgumentError.new("You must pass a block to ldapsearch") unless block_given?
found = false
count = 0
begin
connection.search(search_base, 2, filter, search_attributes) do |entry|
found = true
yield entry
end
rescue SystemExit,NoMemoryError
raise
rescue Exception => detail
if count == 0
# Try reconnecting to ldap if we get an exception and we haven't yet retried.
count += 1
@connection = nil
Puppet.warning "Retrying LDAP connection"
retry
else
error = Puppet::Error.new("LDAP Search failed")
error.set_backtrace(detail.backtrace)
raise error
end
end
found
end
# Create an ldap connection.
def connection
unless @connection
raise Puppet::Error, "Could not set up LDAP Connection: Missing ruby/ldap libraries" unless Puppet.features.ldap?
begin
conn = Puppet::Util::Ldap::Connection.instance
conn.start
@connection = conn.connection
rescue => detail
message = "Could not connect to LDAP: #{detail}"
Puppet.log_exception(detail, message)
- raise Puppet::Error, message
+ raise Puppet::Error, message, detail.backtrace
end
end
@connection
end
end
diff --git a/lib/puppet/indirector/memory.rb b/lib/puppet/indirector/memory.rb
index 0e3afaef7..c16b7e975 100644
--- a/lib/puppet/indirector/memory.rb
+++ b/lib/puppet/indirector/memory.rb
@@ -1,30 +1,34 @@
require 'puppet/indirector/terminus'
# Manage a memory-cached list of instances.
class Puppet::Indirector::Memory < Puppet::Indirector::Terminus
def initialize
+ clear
+ end
+
+ def clear
@instances = {}
end
def destroy(request)
raise ArgumentError.new("Could not find #{request.key} to destroy") unless @instances.include?(request.key)
@instances.delete(request.key)
end
def find(request)
@instances[request.key]
end
def search(request)
found_keys = @instances.keys.find_all { |key| key.include?(request.key) }
found_keys.collect { |key| @instances[key] }
end
def head(request)
not find(request).nil?
end
def save(request)
@instances[request.key] = request.instance
end
end
diff --git a/lib/puppet/indirector/msgpack.rb b/lib/puppet/indirector/msgpack.rb
new file mode 100644
index 000000000..bcc62866c
--- /dev/null
+++ b/lib/puppet/indirector/msgpack.rb
@@ -0,0 +1,82 @@
+require 'puppet/indirector/terminus'
+require 'puppet/util'
+
+# The base class for MessagePack indirection terminus implementations.
+#
+# This should generally be preferred to the PSON base for any future
+# implementations, since it is ~ 30 times faster
+class Puppet::Indirector::Msgpack < Puppet::Indirector::Terminus
+ def initialize(*args)
+ if ! Puppet.features.msgpack?
+ raise "MessagePack terminus not supported without msgpack library"
+ end
+ super
+ end
+
+ def find(request)
+ load_msgpack_from_file(path(request.key), request.key)
+ end
+
+ def save(request)
+ filename = path(request.key)
+ FileUtils.mkdir_p(File.dirname(filename))
+
+ Puppet::Util.replace_file(filename, 0660) {|f| f.print to_msgpack(request.instance) }
+ rescue TypeError => detail
+ Puppet.log_exception "Could not save #{self.name} #{request.key}: #{detail}"
+ end
+
+ def destroy(request)
+ Puppet::FileSystem.unlink(path(request.key))
+ rescue => detail
+ unless detail.is_a? Errno::ENOENT
+ raise Puppet::Error, "Could not destroy #{self.name} #{request.key}: #{detail}", detail.backtrace
+ end
+ 1 # emulate success...
+ end
+
+ def search(request)
+ Dir.glob(path(request.key)).collect do |file|
+ load_msgpack_from_file(file, request.key)
+ end
+ end
+
+ # Return the path to a given node's file.
+ def path(name, ext = '.msgpack')
+ if name =~ Puppet::Indirector::BadNameRegexp then
+ Puppet.crit("directory traversal detected in #{self.class}: #{name.inspect}")
+ raise ArgumentError, "invalid key"
+ end
+
+ base = Puppet.run_mode.master? ? Puppet[:server_datadir] : Puppet[:client_datadir]
+ File.join(base, self.class.indirection_name.to_s, name.to_s + ext)
+ end
+
+ private
+
+ def load_msgpack_from_file(file, key)
+ msgpack = nil
+
+ begin
+ msgpack = File.read(file)
+ rescue Errno::ENOENT
+ return nil
+ rescue => detail
+ raise Puppet::Error, "Could not read MessagePack data for #{indirection.name} #{key}: #{detail}", detail.backtrace
+ end
+
+ begin
+ return from_msgpack(msgpack)
+ rescue => detail
+ raise Puppet::Error, "Could not parse MessagePack data for #{indirection.name} #{key}: #{detail}", detail.backtrace
+ end
+ end
+
+ def from_msgpack(text)
+ model.convert_from('msgpack', text)
+ end
+
+ def to_msgpack(object)
+ object.render('msgpack')
+ end
+end
diff --git a/lib/puppet/indirector/node/exec.rb b/lib/puppet/indirector/node/exec.rb
index d74a76204..d4faf74f4 100644
--- a/lib/puppet/indirector/node/exec.rb
+++ b/lib/puppet/indirector/node/exec.rb
@@ -1,69 +1,69 @@
require 'puppet/node'
require 'puppet/indirector/exec'
class Puppet::Node::Exec < Puppet::Indirector::Exec
desc "Call an external program to get node information. See
the [External Nodes](http://docs.puppetlabs.com/guides/external_nodes.html) page for more information."
include Puppet::Util
def command
command = Puppet[:external_nodes]
raise ArgumentError, "You must set the 'external_nodes' parameter to use the external node terminus" unless command != "none"
command.split
end
# Look for external node definitions.
def find(request)
output = super or return nil
# Translate the output to ruby.
result = translate(request.key, output)
# Set the requested environment if it wasn't overridden
# If we don't do this it gets set to the local default
result[:environment] ||= request.environment.name
create_node(request.key, result)
end
private
# Proxy the execution, so it's easier to test.
def execute(command, arguments)
Puppet::Util::Execution.execute(command,arguments)
end
# Turn our outputted objects into a Puppet::Node instance.
def create_node(name, result)
node = Puppet::Node.new(name)
set = false
[:parameters, :classes, :environment].each do |param|
if value = result[param]
node.send(param.to_s + "=", value)
set = true
end
end
node.fact_merge
node
end
# Translate the yaml string into Ruby objects.
def translate(name, output)
YAML.load(output).inject({}) do |hash, data|
case data[0]
when String
hash[data[0].intern] = data[1]
when Symbol
hash[data[0]] = data[1]
else
raise Puppet::Error, "key is a #{data[0].class}, not a string or symbol"
end
hash
end
rescue => detail
- raise Puppet::Error, "Could not load external node results for #{name}: #{detail}"
+ raise Puppet::Error, "Could not load external node results for #{name}: #{detail}", detail.backtrace
end
end
diff --git a/lib/puppet/indirector/node/ldap.rb b/lib/puppet/indirector/node/ldap.rb
index f1a79ef6e..37f9c22dd 100644
--- a/lib/puppet/indirector/node/ldap.rb
+++ b/lib/puppet/indirector/node/ldap.rb
@@ -1,257 +1,257 @@
require 'puppet/node'
require 'puppet/indirector/ldap'
class Puppet::Node::Ldap < Puppet::Indirector::Ldap
desc "Search in LDAP for node configuration information. See
- the [LDAP Nodes](http://projects.puppetlabs.com/projects/puppet/wiki/Ldap_Nodes) page for more information. This will first
+ the [LDAP Nodes](http://docs.puppetlabs.com/guides/ldap_nodes.html) page for more information. This will first
search for whatever the certificate name is, then (if that name
contains a `.`) for the short name, then `default`."
# The attributes that Puppet class information is stored in.
def class_attributes
Puppet[:ldapclassattrs].split(/\s*,\s*/)
end
# Separate this out so it's relatively atomic. It's tempting to call
# process instead of name2hash() here, but it ends up being
# difficult to test because all exceptions get caught by ldapsearch.
# LAK:NOTE Unfortunately, the ldap support is too stupid to throw anything
# but LDAP::ResultError, even on bad connections, so we are rough-handed
# with our error handling.
def name2hash(name)
info = nil
ldapsearch(search_filter(name)) { |entry| info = entry2hash(entry) }
info
end
# Look for our node in ldap.
def find(request)
names = [request.key]
names << request.key.sub(/\..+/, '') if request.key.include?(".") # we assume it's an fqdn
names << "default"
node = nil
names.each do |name|
next unless info = name2hash(name)
merge_parent(info) if info[:parent]
- info[:environment] ||= request.environment.to_s
+ info[:environment] ||= request.environment
node = info2node(request.key, info)
break
end
node
end
# Find more than one node. LAK:NOTE This is a bit of a clumsy API, because the 'search'
# method currently *requires* a key. It seems appropriate in some cases but not others,
# and I don't really know how to get rid of it as a requirement but allow it when desired.
def search(request)
if classes = request.options[:class]
classes = [classes] unless classes.is_a?(Array)
filter = "(&(objectclass=puppetClient)(puppetclass=" + classes.join(")(puppetclass=") + "))"
else
filter = "(objectclass=puppetClient)"
end
infos = []
ldapsearch(filter) { |entry| infos << entry2hash(entry, request.options[:fqdn]) }
return infos.collect do |info|
merge_parent(info) if info[:parent]
- info[:environment] ||= request.environment.to_s
+ info[:environment] ||= request.environment
info2node(info[:name], info)
end
end
# The parent attribute, if we have one.
def parent_attribute
if pattr = Puppet[:ldapparentattr] and ! pattr.empty?
pattr
else
nil
end
end
# The attributes that Puppet will stack as array over the full
# hierarchy.
def stacked_attributes(dummy_argument=:work_arround_for_ruby_GC_bug)
Puppet[:ldapstackedattrs].split(/\s*,\s*/)
end
# Convert the found entry into a simple hash.
def entry2hash(entry, fqdn = false)
result = {}
cn = entry.dn[ /cn\s*=\s*([^,\s]+)/i,1]
dcs = entry.dn.scan(/dc\s*=\s*([^,\s]+)/i)
result[:name] = fqdn ? ([cn]+dcs).join('.') : cn
result[:parent] = get_parent_from_entry(entry) if parent_attribute
result[:classes] = get_classes_from_entry(entry)
result[:stacked] = get_stacked_values_from_entry(entry)
result[:parameters] = get_parameters_from_entry(entry)
result[:environment] = result[:parameters]["environment"] if result[:parameters]["environment"]
result[:stacked_parameters] = {}
if result[:stacked]
result[:stacked].each do |value|
param = value.split('=', 2)
result[:stacked_parameters][param[0]] = param[1]
end
end
if result[:stacked_parameters]
result[:stacked_parameters].each do |param, value|
result[:parameters][param] = value unless result[:parameters].include?(param)
end
end
result[:parameters] = convert_parameters(result[:parameters])
result
end
# Default to all attributes.
def search_attributes
ldapattrs = Puppet[:ldapattrs]
# results in everything getting returned
return nil if ldapattrs == "all"
search_attrs = class_attributes + ldapattrs.split(/\s*,\s*/)
if pattr = parent_attribute
search_attrs << pattr
end
search_attrs
end
# The ldap search filter to use.
def search_filter(name)
filter = Puppet[:ldapstring]
if filter.include? "%s"
# Don't replace the string in-line, since that would hard-code our node
# info.
filter = filter.gsub('%s', name)
end
filter
end
private
# Add our hash of ldap information to the node instance.
def add_to_node(node, information)
node.classes = information[:classes].uniq unless information[:classes].nil? or information[:classes].empty?
node.parameters = information[:parameters] unless information[:parameters].nil? or information[:parameters].empty?
node.environment = information[:environment] if information[:environment]
end
def convert_parameters(parameters)
result = {}
parameters.each do |param, value|
if value.is_a?(Array)
result[param] = value.collect { |v| convert(v) }
else
result[param] = convert(value)
end
end
result
end
# Convert any values if necessary.
def convert(value)
case value
when Integer, Fixnum, Bignum; value
when "true"; true
when "false"; false
else
value
end
end
# Find information for our parent and merge it into the current info.
def find_and_merge_parent(parent, information)
parent_info = name2hash(parent) || raise(Puppet::Error.new("Could not find parent node '#{parent}'"))
information[:classes] += parent_info[:classes]
parent_info[:parameters].each do |param, value|
# Specifically test for whether it's set, so false values are handled correctly.
information[:parameters][param] = value unless information[:parameters].include?(param)
end
information[:environment] ||= parent_info[:environment]
parent_info[:parent]
end
# Take a name and a hash, and return a node instance.
def info2node(name, info)
node = Puppet::Node.new(name)
add_to_node(node, info)
node.fact_merge
node
end
def merge_parent(info)
parent = info[:parent]
# Preload the parent array with the node name.
parents = [info[:name]]
while parent
raise ArgumentError, "Found loop in LDAP node parents; #{parent} appears twice" if parents.include?(parent)
parents << parent
parent = find_and_merge_parent(parent, info)
end
info
end
def get_classes_from_entry(entry)
result = class_attributes.inject([]) do |array, attr|
if values = entry.vals(attr)
values.each do |v| array << v end
end
array
end
result.uniq
end
def get_parameters_from_entry(entry)
stacked_params = stacked_attributes
entry.to_hash.inject({}) do |hash, ary|
unless stacked_params.include?(ary[0]) # don't add our stacked parameters to the main param list
if ary[1].length == 1
hash[ary[0]] = ary[1].shift
else
hash[ary[0]] = ary[1]
end
end
hash
end
end
def get_parent_from_entry(entry)
pattr = parent_attribute
return nil unless values = entry.vals(pattr)
if values.length > 1
raise Puppet::Error,
"Node entry #{entry.dn} specifies more than one parent: #{values.inspect}"
end
return(values.empty? ? nil : values.shift)
end
def get_stacked_values_from_entry(entry)
stacked_attributes.inject([]) do |result, attr|
if values = entry.vals(attr)
result += values
end
result
end
end
end
diff --git a/lib/puppet/indirector/node/msgpack.rb b/lib/puppet/indirector/node/msgpack.rb
new file mode 100644
index 000000000..bf38a708a
--- /dev/null
+++ b/lib/puppet/indirector/node/msgpack.rb
@@ -0,0 +1,7 @@
+require 'puppet/node'
+require 'puppet/indirector/msgpack'
+
+class Puppet::Node::Msgpack < Puppet::Indirector::Msgpack
+ desc "Store node information as flat files, serialized using MessagePack,
+ or deserialize stored MessagePack nodes."
+end
diff --git a/lib/puppet/indirector/node/yaml.rb b/lib/puppet/indirector/node/yaml.rb
index 5a316b62e..b72abab24 100644
--- a/lib/puppet/indirector/node/yaml.rb
+++ b/lib/puppet/indirector/node/yaml.rb
@@ -1,7 +1,22 @@
require 'puppet/node'
require 'puppet/indirector/yaml'
class Puppet::Node::Yaml < Puppet::Indirector::Yaml
desc "Store node information as flat files, serialized using YAML,
or deserialize stored YAML nodes."
+
+ protected
+
+ def fix(object)
+ # This looks very strange because when the object is read from disk the
+ # environment is a string and by assigning it back onto the object it gets
+ # converted to a Puppet::Node::Environment.
+ #
+ # The Puppet::Node class can't handle this itself because we are loading
+ # with just straight YAML, which doesn't give the object a chance to modify
+ # things as it is loaded. Instead YAML simply sets the instance variable
+ # and leaves it at that.
+ object.environment = object.environment
+ object
+ end
end
diff --git a/lib/puppet/indirector/queue.rb b/lib/puppet/indirector/queue.rb
index 8b9d8ab61..1d5776bdb 100644
--- a/lib/puppet/indirector/queue.rb
+++ b/lib/puppet/indirector/queue.rb
@@ -1,79 +1,80 @@
require 'puppet/indirector/terminus'
require 'puppet/util/queue'
require 'puppet/util'
# Implements the <tt>:queue</tt> abstract indirector terminus type, for storing
# model instances to a message queue, presumably for the purpose of out-of-process
# handling of changes related to the model.
#
# Relies upon Puppet::Util::Queue for registry and client object management,
# and specifies a default queue type of <tt>:stomp</tt>, appropriate for use with a variety of message brokers.
#
# It's up to the queue client type to instantiate itself correctly based on Puppet configuration information.
#
# A single queue client is maintained for the abstract terminus, meaning that you can only use one type
# of queue client, one message broker solution, etc., with the indirection mechanism.
#
# Per-indirection queues are assumed, based on the indirection name. If the <tt>:catalog</tt> indirection makes
# use of this <tt>:queue</tt> terminus, queue operations work against the "catalog" queue. It is up to the queue
# client library to handle queue creation as necessary (for a number of popular queuing solutions, queue
# creation is automatic and not a concern).
class Puppet::Indirector::Queue < Puppet::Indirector::Terminus
extend ::Puppet::Util::Queue
include Puppet::Util
def initialize(*args)
super
end
# Queue has no idiomatic "find"
def find(request)
nil
end
# Place the request on the queue
def save(request)
result = nil
benchmark :info, "Queued #{indirection.name} for #{request.key}" do
result = client.publish_message(queue, request.instance.render(:pson))
end
result
rescue => detail
- raise Puppet::Error, "Could not write #{request.key} to queue: #{detail}\nInstance::#{request.instance}\n client : #{client}"
+ msg = "Could not write #{request.key} to queue: #{detail}\nInstance::#{request.instance}\n client : #{client}"
+ raise Puppet::Error, msg, detail.backtrace
end
def self.queue
indirection_name
end
def queue
self.class.queue
end
# Returns the singleton queue client object.
def client
self.class.client
end
# converts the _message_ from deserialized format to an actual model instance.
def self.intern(message)
result = nil
benchmark :info, "Loaded queued #{indirection.name}" do
result = model.convert_from(:pson, message)
end
result
end
# Provides queue subscription functionality; for a given indirection, use this method on the terminus
# to subscribe to the indirection-specific queue. Your _block_ will be executed per new indirection
# model received from the queue, with _obj_ being the model instance.
def self.subscribe
client.subscribe(queue) do |msg|
begin
yield(self.intern(msg))
rescue => detail
- Puppet.log_exception(detail, "Error occured with subscription to queue #{queue} for indirection #{indirection_name}: #{detail}")
+ Puppet.log_exception(detail, "Error occurred with subscription to queue #{queue} for indirection #{indirection_name}: #{detail}")
end
end
end
end
diff --git a/lib/puppet/indirector/report/msgpack.rb b/lib/puppet/indirector/report/msgpack.rb
new file mode 100644
index 000000000..057b359c7
--- /dev/null
+++ b/lib/puppet/indirector/report/msgpack.rb
@@ -0,0 +1,11 @@
+require 'puppet/transaction/report'
+require 'puppet/indirector/msgpack'
+
+class Puppet::Transaction::Report::Msgpack < Puppet::Indirector::Msgpack
+ desc "Store last report as a flat file, serialized using MessagePack."
+
+ # Force report to be saved there
+ def path(name,ext='.msgpack')
+ Puppet[:lastrunreport]
+ end
+end
diff --git a/lib/puppet/indirector/request.rb b/lib/puppet/indirector/request.rb
index 4eacee11b..a67753f68 100644
--- a/lib/puppet/indirector/request.rb
+++ b/lib/puppet/indirector/request.rb
@@ -1,315 +1,312 @@
require 'cgi'
require 'uri'
require 'puppet/indirector'
require 'puppet/util/pson'
require 'puppet/network/resolver'
# This class encapsulates all of the information you need to make an
# Indirection call, and as a result also handles REST calls. It's somewhat
# analogous to an HTTP Request object, except tuned for our Indirector.
class Puppet::Indirector::Request
attr_accessor :key, :method, :options, :instance, :node, :ip, :authenticated, :ignore_cache, :ignore_terminus
attr_accessor :server, :port, :uri, :protocol
attr_reader :indirection_name
+ # trusted_information is specifically left out because we can't serialize it
+ # and keep it "trusted"
OPTION_ATTRIBUTES = [:ip, :node, :authenticated, :ignore_terminus, :ignore_cache, :instance, :environment]
::PSON.register_document_type('IndirectorRequest',self)
- def self.from_pson(json)
- raise ArgumentError, "No indirection name provided in json data" unless indirection_name = json['type']
- raise ArgumentError, "No method name provided in json data" unless method = json['method']
- raise ArgumentError, "No key provided in json data" unless key = json['key']
+ def self.from_data_hash(data)
+ raise ArgumentError, "No indirection name provided in data" unless indirection_name = data['type']
+ raise ArgumentError, "No method name provided in data" unless method = data['method']
+ raise ArgumentError, "No key provided in data" unless key = data['key']
- request = new(indirection_name, method, key, nil, json['attributes'])
+ request = new(indirection_name, method, key, nil, data['attributes'])
- if instance = json['instance']
+ if instance = data['instance']
klass = Puppet::Indirector::Indirection.instance(request.indirection_name).model
if instance.is_a?(klass)
request.instance = instance
else
- request.instance = klass.from_pson(instance)
+ request.instance = klass.from_data_hash(instance)
end
end
request
end
+ def self.from_pson(json)
+ Puppet.deprecation_warning("from_pson is being removed in favour of from_data_hash.")
+ self.from_data_hash(json)
+ end
+
def to_data_hash
result = {
'type' => indirection_name,
'method' => method,
'key' => key
}
attributes = {}
OPTION_ATTRIBUTES.each do |key|
next unless value = send(key)
attributes[key] = value
end
options.each do |opt, value|
attributes[opt] = value
end
result['attributes'] = attributes unless attributes.empty?
result['instance'] = instance if instance
result
end
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?
def authenticated?
# Double negative, so we just get true or false
! ! authenticated
end
def environment
- @environment ||= Puppet::Node::Environment.new
+ @environment ||= Puppet.lookup(:environments).get(Puppet[:environment])
end
def environment=(env)
@environment = if env.is_a?(Puppet::Node::Environment)
env
else
- Puppet::Node::Environment.new(env)
+ Puppet.lookup(:environments).get(env)
end
end
def escaped_key
URI.escape(key)
end
# LAK:NOTE This is a messy interface to the cache, and it's only
# used by the Configurer class. I decided it was better to implement
# it now and refactor later, when we have a better design, than
# to spend another month coming up with a design now that might
# not be any better.
def ignore_cache?
ignore_cache
end
def ignore_terminus?
ignore_terminus
end
def initialize(indirection_name, method, key, instance, options = {})
@instance = instance
options ||= {}
self.indirection_name = indirection_name
self.method = method
options = options.inject({}) { |hash, ary| hash[ary[0].to_sym] = ary[1]; hash }
set_attributes(options)
@options = options
if key
# If the request key is a URI, then we need to treat it specially,
# because it rewrites the key. We could otherwise strip server/port/etc
# info out in the REST class, but it seemed bad design for the REST
# class to rewrite the key.
if key.to_s =~ /^\w+:\// and not Puppet::Util.absolute_path?(key.to_s) # it's a URI
set_uri_key(key)
else
@key = key
end
end
@key = @instance.name if ! @key and @instance
end
# Look up the indirection based on the name provided.
def indirection
Puppet::Indirector::Indirection.instance(indirection_name)
end
def indirection_name=(name)
@indirection_name = name.to_sym
end
-
def model
raise ArgumentError, "Could not find indirection '#{indirection_name}'" unless i = indirection
i.model
end
- # Should we allow use of the cached object?
- def use_cache?
- if defined?(@use_cache)
- ! ! use_cache
- else
- true
- end
- end
-
# Are we trying to interact with multiple resources, or just one?
def plural?
method == :search
end
# Create the query string, if options are present.
def query_string
return "" if options.nil? || options.empty?
# For backward compatibility with older (pre-3.3) masters,
# this puppet option allows serialization of query parameter
# arrays as yaml. This can be removed when we remove yaml
# support entirely.
if Puppet.settings[:legacy_query_parameter_serialization]
replace_arrays_with_yaml
end
"?" + encode_params(expand_into_parameters(options.to_a))
end
def replace_arrays_with_yaml
options.each do |key, value|
case value
when Array
options[key] = YAML.dump(value)
end
end
end
def expand_into_parameters(data)
data.inject([]) do |params, key_value|
key, value = key_value
expanded_value = case value
when Array
value.collect { |val| [key, val] }
else
[key_value]
end
params.concat(expand_primitive_types_into_parameters(expanded_value))
end
end
def expand_primitive_types_into_parameters(data)
data.inject([]) do |params, key_value|
key, value = key_value
case value
when nil
params
when true, false, String, Symbol, Fixnum, Bignum, Float
params << [key, value]
else
raise ArgumentError, "HTTP REST queries cannot handle values of type '#{value.class}'"
end
end
end
def encode_params(params)
params.collect do |key, value|
"#{key}=#{CGI.escape(value.to_s)}"
end.join("&")
end
def to_hash
result = options.dup
OPTION_ATTRIBUTES.each do |attribute|
if value = send(attribute)
result[attribute] = value
end
end
result
end
def to_s
return(uri ? uri : "/#{indirection_name}/#{key}")
end
def do_request(srv_service=:puppet, default_server=Puppet.settings[:server], default_port=Puppet.settings[:masterport], &block)
# We were given a specific server to use, so just use that one.
# This happens if someone does something like specifying a file
# source using a puppet:// URI with a specific server.
return yield(self) if !self.server.nil?
if Puppet.settings[:use_srv_records]
Puppet::Network::Resolver.each_srv_record(Puppet.settings[:srv_domain], srv_service) do |srv_server, srv_port|
begin
self.server = srv_server
self.port = srv_port
return yield(self)
rescue SystemCallError => e
Puppet.warning "Error connecting to #{srv_server}:#{srv_port}: #{e.message}"
end
end
end
# ... Fall back onto the default server.
Puppet.debug "No more servers left, falling back to #{default_server}:#{default_port}" if Puppet.settings[:use_srv_records]
self.server = default_server
self.port = default_port
return yield(self)
end
def remote?
self.node or self.ip
end
private
def set_attributes(options)
OPTION_ATTRIBUTES.each do |attribute|
if options.include?(attribute.to_sym)
send(attribute.to_s + "=", options[attribute])
options.delete(attribute)
end
end
end
# Parse the key as a URI, setting attributes appropriately.
def set_uri_key(key)
@uri = key
begin
uri = URI.parse(URI.escape(key))
rescue => detail
- raise ArgumentError, "Could not understand URL #{key}: #{detail}"
+ raise ArgumentError, "Could not understand URL #{key}: #{detail}", detail.backtrace
end
# Just short-circuit these to full paths
if uri.scheme == "file"
@key = Puppet::Util.uri_to_path(uri)
return
end
@server = uri.host if uri.host
# If the URI class can look up the scheme, it will provide a port,
# otherwise it will default to '0'.
if uri.port.to_i == 0 and uri.scheme == "puppet"
@port = Puppet.settings[:masterport].to_i
else
@port = uri.port.to_i
end
@protocol = uri.scheme
if uri.scheme == 'puppet'
@key = URI.unescape(uri.path.sub(/^\//, ''))
return
end
env, indirector, @key = URI.unescape(uri.path.sub(/^\//, '')).split('/',3)
@key ||= ''
self.environment = env unless env == ''
end
end
diff --git a/lib/puppet/indirector/resource/rest.rb b/lib/puppet/indirector/resource/rest.rb
index 9992fc057..66384796b 100644
--- a/lib/puppet/indirector/resource/rest.rb
+++ b/lib/puppet/indirector/resource/rest.rb
@@ -1,17 +1,17 @@
require 'puppet/indirector/status'
require 'puppet/indirector/rest'
# @deprecated
class Puppet::Resource::Rest < Puppet::Indirector::REST
desc "Maniuplate resources remotely? Undocumented."
private
def deserialize_save(content_type, body)
# Body is [ral_res.to_resource, transaction.report]
format = Puppet::Network::FormatHandler.format_for(content_type)
ary = format.intern(Array, body)
- [Puppet::Resource.from_pson(ary[0]), Puppet::Transaction::Report.from_pson(ary[1])]
+ [Puppet::Resource.from_data_hash(ary[0]), Puppet::Transaction::Report.from_data_hash(ary[1])]
end
end
diff --git a/lib/puppet/indirector/resource_type/parser.rb b/lib/puppet/indirector/resource_type/parser.rb
index 2d5eb040d..884fd2f03 100644
--- a/lib/puppet/indirector/resource_type/parser.rb
+++ b/lib/puppet/indirector/resource_type/parser.rb
@@ -1,111 +1,101 @@
require 'puppet/resource/type'
require 'puppet/indirector/code'
require 'puppet/indirector/resource_type'
# The main terminus for Puppet::Resource::Type
#
# This exposes the known resource types from within Puppet. Only find
# and search are supported. When a request is received, Puppet will
# attempt to load all resource types (by parsing manifests and modules) and
# returns a description of the resource types found. The format of these
# objects is documented at {Puppet::Resource::Type}.
#
# @api public
class Puppet::Indirector::ResourceType::Parser < Puppet::Indirector::Code
desc "Return the data-form of a resource type."
# Find will return the first resource_type with the given name. It is
# not possible to specify the kind of the resource type.
#
# @param request [Puppet::Indirector::Request] The request object.
# The only parameters used from the request are `environment` and
# `key`, which corresponds to the resource type's `name` field.
# @return [Puppet::Resource::Type, nil]
# @api public
def find(request)
- begin
- # This is a fix in 3.x that will be replaced with the use of a context
- # (That is not available until 3.5).
- $squelsh_parse_errors = true
+ Puppet.override(:squelch_parse_errors => true) do
krt = resource_types_in(request.environment)
# This is a bit ugly.
[:hostclass, :definition, :node].each do |type|
# We have to us 'find_<type>' here because it will
# load any missing types from disk, whereas the plain
# '<type>' method only returns from memory.
if r = krt.send("find_#{type}", [""], request.key)
return r
end
end
nil
- ensure
- $squelsh_parse_errors = false
end
end
# Search for resource types using a regular expression. Unlike `find`, this
# allows you to filter the results by the "kind" of the resource type
# ("class", "defined_type", or "node"). All three are searched if no
# `kind` filter is given. This also accepts the special string "`*`"
# to return all resource type objects.
#
# @param request [Puppet::Indirector::Request] The request object. The
# `key` field holds the regular expression used to search, and
# `options[:kind]` holds the kind query parameter to filter the
# result as described above. The `environment` field specifies the
# environment used to load resources.
#
# @return [Array<Puppet::Resource::Type>, nil]
#
# @api public
def search(request)
- begin
- # This is a fix in 3.x that will be replaced with the use of a context
- # (That is not available until 3.5).
- $squelsh_parse_errors = true
+ Puppet.override(:squelch_parse_errors => true) do
krt = resource_types_in(request.environment)
# Make sure we've got all of the types loaded.
krt.loader.import_all
result_candidates = case request.options[:kind]
when "class"
krt.hostclasses.values
when "defined_type"
krt.definitions.values
when "node"
krt.nodes.values
when nil
result_candidates = [krt.hostclasses.values, krt.definitions.values, krt.nodes.values]
else
raise ArgumentError, "Unrecognized kind filter: " +
"'#{request.options[:kind]}', expected one " +
" of 'class', 'defined_type', or 'node'."
end
result = result_candidates.flatten.reject { |t| t.name == "" }
return nil if result.empty?
return result if request.key == "*"
# Strip the regex of any wrapping slashes that might exist
key = request.key.sub(/^\//, '').sub(/\/$/, '')
begin
regex = Regexp.new(key)
rescue => detail
- raise ArgumentError, "Invalid regex '#{request.key}': #{detail}"
+ raise ArgumentError, "Invalid regex '#{request.key}': #{detail}", detail.backtrace
end
result.reject! { |t| t.name.to_s !~ regex }
return nil if result.empty?
result
end
- ensure
- $squelsh_parse_errors = false
end
def resource_types_in(environment)
environment.check_for_reparse
environment.known_resource_types
end
end
diff --git a/lib/puppet/indirector/rest.rb b/lib/puppet/indirector/rest.rb
index d70b43bdf..fbae86344 100644
--- a/lib/puppet/indirector/rest.rb
+++ b/lib/puppet/indirector/rest.rb
@@ -1,247 +1,246 @@
require 'net/http'
require 'uri'
require 'puppet/network/http'
require 'puppet/network/http_pool'
require 'puppet/network/http/api/v1'
require 'puppet/network/http/compression'
# Access objects via REST
class Puppet::Indirector::REST < Puppet::Indirector::Terminus
- include Puppet::Network::HTTP::API::V1
include Puppet::Network::HTTP::Compression.module
class << self
attr_reader :server_setting, :port_setting
end
# Specify the setting that we should use to get the server name.
def self.use_server_setting(setting)
@server_setting = setting
end
# Specify the setting that we should use to get the port.
def self.use_port_setting(setting)
@port_setting = setting
end
# Specify the service to use when doing SRV record lookup
def self.use_srv_service(service)
@srv_service = service
end
def self.srv_service
@srv_service || :puppet
end
def self.server
Puppet.settings[server_setting || :server]
end
def self.port
Puppet.settings[port_setting || :masterport].to_i
end
# Provide appropriate headers.
def headers
add_accept_encoding({"Accept" => model.supported_formats.join(", ")})
end
def add_profiling_header(headers)
if (Puppet[:profile])
headers[Puppet::Network::HTTP::HEADER_ENABLE_PROFILING] = "true"
end
headers
end
def network(request)
Puppet::Network::HttpPool.http_instance(request.server || self.class.server,
request.port || self.class.port)
end
def http_get(request, path, headers = nil, *args)
http_request(:get, request, path, add_profiling_header(headers), *args)
end
def http_post(request, path, data, headers = nil, *args)
http_request(:post, request, path, data, add_profiling_header(headers), *args)
end
def http_head(request, path, headers = nil, *args)
http_request(:head, request, path, add_profiling_header(headers), *args)
end
def http_delete(request, path, headers = nil, *args)
http_request(:delete, request, path, add_profiling_header(headers), *args)
end
def http_put(request, path, data, headers = nil, *args)
http_request(:put, request, path, data, add_profiling_header(headers), *args)
end
def http_request(method, request, *args)
conn = network(request)
conn.send(method, *args)
end
def find(request)
- uri, body = request_to_uri_and_body(request)
+ uri, body = Puppet::Network::HTTP::API::V1.request_to_uri_and_body(request)
uri_with_query_string = "#{uri}?#{body}"
response = do_request(request) do |request|
# WEBrick in Ruby 1.9.1 only supports up to 1024 character lines in an HTTP request
# http://redmine.ruby-lang.org/issues/show/3991
if "GET #{uri_with_query_string} HTTP/1.1\r\n".length > 1024
http_post(request, uri, body, headers)
else
http_get(request, uri_with_query_string, headers)
end
end
if is_http_200?(response)
check_master_version(response)
content_type, body = parse_response(response)
result = deserialize_find(content_type, body)
result.name = request.key if result.respond_to?(:name=)
result
else
nil
end
end
def head(request)
response = do_request(request) do |request|
- http_head(request, indirection2uri(request), headers)
+ http_head(request, Puppet::Network::HTTP::API::V1.indirection2uri(request), headers)
end
if is_http_200?(response)
check_master_version(response)
true
else
false
end
end
def search(request)
response = do_request(request) do |request|
- http_get(request, indirection2uri(request), headers)
+ http_get(request, Puppet::Network::HTTP::API::V1.indirection2uri(request), headers)
end
if is_http_200?(response)
check_master_version(response)
content_type, body = parse_response(response)
deserialize_search(content_type, body) || []
else
[]
end
end
def destroy(request)
raise ArgumentError, "DELETE does not accept options" unless request.options.empty?
response = do_request(request) do |request|
- http_delete(request, indirection2uri(request), headers)
+ http_delete(request, Puppet::Network::HTTP::API::V1.indirection2uri(request), headers)
end
if is_http_200?(response)
check_master_version(response)
content_type, body = parse_response(response)
deserialize_destroy(content_type, body)
else
nil
end
end
def save(request)
raise ArgumentError, "PUT does not accept options" unless request.options.empty?
response = do_request(request) do |request|
- http_put(request, indirection2uri(request), request.instance.render, headers.merge({ "Content-Type" => request.instance.mime }))
+ http_put(request, Puppet::Network::HTTP::API::V1.indirection2uri(request), request.instance.render, headers.merge({ "Content-Type" => request.instance.mime }))
end
if is_http_200?(response)
check_master_version(response)
content_type, body = parse_response(response)
deserialize_save(content_type, body)
else
nil
end
end
# Encapsulate call to request.do_request with the arguments from this class
# Then yield to the code block that was called in
# We certainly could have retained the full request.do_request(...) { |r| ... }
# but this makes the code much cleaner and we only then actually make the call
# to request.do_request from here, thus if we change what we pass or how we
# get it, we only need to change it here.
def do_request(request)
request.do_request(self.class.srv_service, self.class.server, self.class.port) { |request| yield(request) }
end
def validate_key(request)
# Validation happens on the remote end
end
private
def is_http_200?(response)
case response.code
when "404"
false
when /^2/
true
else
# Raise the http error if we didn't get a 'success' of some kind.
raise convert_to_http_error(response)
end
end
def convert_to_http_error(response)
message = "Error #{response.code} on SERVER: #{(response.body||'').empty? ? response.message : uncompress_body(response)}"
Net::HTTPError.new(message, response)
end
def check_master_version response
if !response[Puppet::Network::HTTP::HEADER_PUPPET_VERSION] &&
(Puppet[:legacy_query_parameter_serialization] == false || Puppet[:report_serialization_format] != "yaml")
Puppet.notice "Using less secure serialization of reports and query parameters for compatibility"
Puppet.notice "with older puppet master. To remove this notice, please upgrade your master(s) "
Puppet.notice "to Puppet 3.3 or newer."
Puppet.notice "See http://links.puppetlabs.com/deprecate_yaml_on_network for more information."
Puppet[:legacy_query_parameter_serialization] = true
Puppet[:report_serialization_format] = "yaml"
end
end
# Returns the content_type, stripping any appended charset, and the
# body, decompressed if necessary (content-encoding is checked inside
# uncompress_body)
def parse_response(response)
if response['content-type']
[ response['content-type'].gsub(/\s*;.*$/,''),
body = uncompress_body(response) ]
else
raise "No content type in http response; cannot parse"
end
end
def deserialize_find(content_type, body)
model.convert_from(content_type, body)
end
def deserialize_search(content_type, body)
model.convert_from_multiple(content_type, body)
end
def deserialize_destroy(content_type, body)
model.convert_from(content_type, body)
end
def deserialize_save(content_type, body)
nil
end
def environment
- Puppet::Node::Environment.new
+ Puppet.lookup(:environments).get(Puppet[:environment])
end
end
diff --git a/lib/puppet/indirector/ssl_file.rb b/lib/puppet/indirector/ssl_file.rb
index a4ca4bd77..695dc421d 100644
--- a/lib/puppet/indirector/ssl_file.rb
+++ b/lib/puppet/indirector/ssl_file.rb
@@ -1,180 +1,180 @@
require 'puppet/ssl'
class Puppet::Indirector::SslFile < Puppet::Indirector::Terminus
# Specify the directory in which multiple files are stored.
def self.store_in(setting)
@directory_setting = setting
end
# Specify a single file location for storing just one file.
# This is used for things like the CRL.
def self.store_at(setting)
@file_setting = setting
end
# Specify where a specific ca file should be stored.
def self.store_ca_at(setting)
@ca_setting = setting
end
class << self
attr_reader :directory_setting, :file_setting, :ca_setting
end
# The full path to where we should store our files.
def self.collection_directory
return nil unless directory_setting
Puppet.settings[directory_setting]
end
# The full path to an individual file we would be managing.
def self.file_location
return nil unless file_setting
Puppet.settings[file_setting]
end
# The full path to a ca file we would be managing.
def self.ca_location
return nil unless ca_setting
Puppet.settings[ca_setting]
end
# We assume that all files named 'ca' are pointing to individual ca files,
# rather than normal host files. It's a bit hackish, but all the other
# solutions seemed even more hackish.
def ca?(name)
name == Puppet::SSL::Host.ca_name
end
def initialize
Puppet.settings.use(:main, :ssl)
(collection_directory || file_location) or raise Puppet::DevError, "No file or directory setting provided; terminus #{self.class.name} cannot function"
end
def path(name)
if name =~ Puppet::Indirector::BadNameRegexp then
Puppet.crit("directory traversal detected in #{self.class}: #{name.inspect}")
raise ArgumentError, "invalid key"
end
if ca?(name) and ca_location
ca_location
elsif collection_directory
File.join(collection_directory, name.to_s + ".pem")
else
file_location
end
end
# Remove our file.
def destroy(request)
- path = path(request.key)
- return false unless Puppet::FileSystem::File.exist?(path)
+ path = Puppet::FileSystem.pathname(path(request.key))
+ return false unless Puppet::FileSystem.exist?(path)
Puppet.notice "Removing file #{model} #{request.key} at '#{path}'"
begin
- Puppet::FileSystem::File.unlink(path)
+ Puppet::FileSystem.unlink(path)
rescue => detail
- raise Puppet::Error, "Could not remove #{request.key}: #{detail}"
+ raise Puppet::Error, "Could not remove #{request.key}: #{detail}", detail.backtrace
end
end
# Find the file on disk, returning an instance of the model.
def find(request)
filename = rename_files_with_uppercase(path(request.key))
filename ? create_model(request.key, filename) : nil
end
# Save our file to disk.
def save(request)
path = path(request.key)
dir = File.dirname(path)
raise Puppet::Error.new("Cannot save #{request.key}; parent directory #{dir} does not exist") unless FileTest.directory?(dir)
raise Puppet::Error.new("Cannot save #{request.key}; parent directory #{dir} is not writable") unless FileTest.writable?(dir)
write(request.key, path) { |f| f.print request.instance.to_s }
end
# Search for more than one file. At this point, it just returns
# an instance for every file in the directory.
def search(request)
dir = collection_directory
Dir.entries(dir).
select { |file| file =~ /\.pem$/ }.
collect { |file| create_model(file.sub(/\.pem$/, ''), File.join(dir, file)) }.
compact
end
private
def create_model(name, path)
result = model.new(name)
result.read(path)
result
end
# Demeterish pointers to class info.
def collection_directory
self.class.collection_directory
end
def file_location
self.class.file_location
end
def ca_location
self.class.ca_location
end
# A hack method to deal with files that exist with a different case.
# Just renames it; doesn't read it in or anything.
# LAK:NOTE This is a copy of the method in sslcertificates/support.rb,
# 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 Puppet::FileSystem::File.exist?(file)
+ return file if Puppet::FileSystem.exist?(file)
dir, short = File.split(file)
- return nil unless Puppet::FileSystem::File.exist?(dir)
+ return nil unless Puppet::FileSystem.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|
other.downcase == short
end
return nil unless real_file
full_file = File.join(dir, real_file)
Puppet.deprecation_warning "Automatic downcasing and renaming of ssl files is deprecated; please request the file using its correct case: #{full_file}"
File.rename(full_file, file)
file
end
# Yield a filehandle set up appropriately, either with our settings doing
# the work or opening a filehandle manually.
def write(name, path)
if ca?(name) and ca_location
Puppet.settings.setting(self.class.ca_setting).open('w') { |f| yield f }
elsif file_location
Puppet.settings.setting(self.class.file_setting).open('w') { |f| yield f }
elsif setting = self.class.directory_setting
begin
Puppet.settings.setting(setting).open_file(path, 'w') { |f| yield f }
rescue => detail
- raise Puppet::Error, "Could not write #{path} to #{setting}: #{detail}"
+ raise Puppet::Error, "Could not write #{path} to #{setting}: #{detail}", detail.backtrace
end
else
raise Puppet::DevError, "You must provide a setting to determine where the files are stored"
end
end
end
# LAK:NOTE This has to be at the end, because classes like SSL::Key use this
# class, and this require statement loads those, which results in a load loop
# and lots of failures.
require 'puppet/ssl/host'
diff --git a/lib/puppet/indirector/yaml.rb b/lib/puppet/indirector/yaml.rb
index 9c4e6f102..9a6be8895 100644
--- a/lib/puppet/indirector/yaml.rb
+++ b/lib/puppet/indirector/yaml.rb
@@ -1,67 +1,63 @@
require 'puppet/indirector/terminus'
require 'puppet/util/yaml'
# The base class for YAML indirection termini.
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 Puppet::FileSystem::File.exist?(file)
+ return nil unless Puppet::FileSystem.exist?(file)
begin
- return Puppet::Util::Yaml.load_file(file)
+ return fix(Puppet::Util::Yaml.load_file(file))
rescue Puppet::Util::Yaml::YamlLoadError => detail
- raise Puppet::Error, "Could not parse YAML data for #{indirection.name} #{request.key}: #{detail}"
+ raise Puppet::Error, "Could not parse YAML data for #{indirection.name} #{request.key}: #{detail}", detail.backtrace
end
end
# Convert our object to YAML and store it to the disk.
def save(request)
raise ArgumentError.new("You can only save objects that respond to :name") unless request.instance.respond_to?(:name)
file = path(request.key)
basedir = File.dirname(file)
# This is quite likely a bad idea, since we're not managing ownership or modes.
- Dir.mkdir(basedir) unless Puppet::FileSystem::File.exist?(basedir)
+ Dir.mkdir(basedir) unless Puppet::FileSystem.exist?(basedir)
begin
Puppet::Util::Yaml.dump(request.instance, file)
rescue TypeError => detail
Puppet.err "Could not save #{self.name} #{request.key}: #{detail}"
end
end
# Return the path to a given node's file.
def path(name,ext='.yaml')
if name =~ Puppet::Indirector::BadNameRegexp then
Puppet.crit("directory traversal detected in #{self.class}: #{name.inspect}")
raise ArgumentError, "invalid key"
end
base = Puppet.run_mode.master? ? Puppet[:yamldir] : Puppet[:clientyamldir]
File.join(base, self.class.indirection_name.to_s, name.to_s + ext)
end
def destroy(request)
file_path = path(request.key)
- Puppet::FileSystem::File.unlink(file_path) if Puppet::FileSystem::File.exist?(file_path)
+ Puppet::FileSystem.unlink(file_path) if Puppet::FileSystem.exist?(file_path)
end
def search(request)
Dir.glob(path(request.key,'')).collect do |file|
- YAML.load_file(file)
+ fix(Puppet::Util::Yaml.load_file(file))
end
end
- private
+ protected
- def from_yaml(text)
- YAML.load(text)
- end
-
- def to_yaml(object)
- YAML.dump(object)
+ def fix(object)
+ object
end
end
diff --git a/lib/puppet/metatype/manager.rb b/lib/puppet/metatype/manager.rb
index cef899b6e..af1379f0b 100644
--- a/lib/puppet/metatype/manager.rb
+++ b/lib/puppet/metatype/manager.rb
@@ -1,179 +1,184 @@
require 'puppet'
require 'puppet/util/classgen'
require 'puppet/node/environment'
# This module defines methods dealing with Type management.
# This module gets included into the Puppet::Type class, it's just split out here for clarity.
# @api public
#
module Puppet::MetaType
module Manager
include Puppet::Util::ClassGen
# An implementation specific method that removes all type instances during testing.
# @note Only use this method for testing purposes.
# @api private
#
def allclear
@types.each { |name, type|
type.clear
}
end
# Iterates over all already loaded Type subclasses.
# @yield [t] a block receiving each type
# @yieldparam t [Puppet::Type] each defined type
# @yieldreturn [Object] the last returned object is also returned from this method
# @return [Object] the last returned value from the block.
def eachtype
@types.each do |name, type|
# Only consider types that have names
#if ! type.parameters.empty? or ! type.validproperties.empty?
yield type
#end
end
end
# Loads all types.
# @note Should only be used for purposes such as generating documentation as this is potentially a very
# expensive operation.
# @return [void]
#
def loadall
typeloader.loadall
end
# Defines a new type or redefines an existing type with the given name.
# A convenience method on the form `new<name>` where name is the name of the type is also created.
# (If this generated method happens to clash with an existing method, a warning is issued and the original
# method is kept).
#
# @param name [String] the name of the type to create or redefine.
- # @param options [Hash] options passed on to {Puppet::Util::ClassGen#genclass} as the option `:attributes` after
- # first having removed any present `:parent` option.
- # @option options [Puppet::Type] :parent the parent (super type) of this type. If nil, the default is
+ # @param options [Hash] options passed on to {Puppet::Util::ClassGen#genclass} as the option `:attributes`.
+ # @option options [Puppet::Type]
# Puppet::Type. This option is not passed on as an attribute to genclass.
# @yield [ ] a block evaluated in the context of the created class, thus allowing further detailing of
# that class.
# @return [Class<inherits Puppet::Type>] the created subclass
# @see Puppet::Util::ClassGen.genclass
#
# @dsl type
# @api public
def newtype(name, options = {}, &block)
# Handle backward compatibility
unless options.is_a?(Hash)
Puppet.warning "Puppet::Type.newtype(#{name}) now expects a hash as the second argument, not #{options.inspect}"
- options = {:parent => options}
end
# First make sure we don't have a method sitting around
name = name.intern
newmethod = "new#{name}"
# Used for method manipulation.
selfobj = singleton_class
@types ||= {}
if @types.include?(name)
if self.respond_to?(newmethod)
# Remove the old newmethod
selfobj.send(:remove_method,newmethod)
end
end
options = symbolize_options(options)
- if parent = options[:parent]
+
+ if options.include?(:parent)
+ Puppet.deprecation_warning "option :parent is deprecated. It has no effect"
options.delete(:parent)
end
# Then create the class.
klass = genclass(
name,
- :parent => (parent || Puppet::Type),
+ :parent => Puppet::Type,
:overwrite => true,
:hash => @types,
:attributes => options,
&block
)
# Now define a "new<type>" method for convenience.
if self.respond_to? newmethod
# Refuse to overwrite existing methods like 'newparam' or 'newtype'.
Puppet.warning "'new#{name.to_s}' method already exists; skipping"
else
selfobj.send(:define_method, newmethod) do |*args|
klass.new(*args)
end
end
# If they've got all the necessary methods defined and they haven't
# already added the property, then do so now.
klass.ensurable if klass.ensurable? and ! klass.validproperty?(:ensure)
# Now set up autoload any providers that might exist for this type.
klass.providerloader = Puppet::Util::Autoload.new(klass, "puppet/provider/#{klass.name.to_s}")
# We have to load everything so that we can figure out the default provider.
klass.providerloader.loadall
klass.providify unless klass.providers.empty?
klass
end
# Removes an existing type.
# @note Only use this for testing.
# @api private
def rmtype(name)
# Then create the class.
rmclass(name, :hash => @types)
singleton_class.send(:remove_method, "new#{name}") if respond_to?("new#{name}")
end
# Returns a Type instance by name.
# This will load the type if not already defined.
# @param [String, Symbol] name of the wanted Type
# @return [Puppet::Type, nil] the type or nil if the type was not defined and could not be loaded
#
def type(name)
+ # Avoid loading if name obviously is not a type name
+ if name.to_s.include?(':')
+ return nil
+ end
+
@types ||= {}
# We are overwhelmingly symbols here, which usually match, so it is worth
# having this special-case to return quickly. Like, 25K symbols vs. 300
# strings in this method. --daniel 2012-07-17
return @types[name] if @types[name]
# Try mangling the name, if it is a string.
if name.is_a? String
name = name.downcase.intern
return @types[name] if @types[name]
end
# Try loading the type.
- if typeloader.load(name, Puppet::Node::Environment.current)
+ if typeloader.load(name, Puppet.lookup(:current_environment))
Puppet.warning "Loaded puppet/type/#{name} but no class was created" unless @types.include? name
end
# ...and I guess that is that, eh.
return @types[name]
end
# Creates a loader for Puppet types.
# Defaults to an instance of {Puppet::Util::Autoload} if no other auto loader has been set.
# @return [Puppet::Util::Autoload] the loader to use.
# @api private
def typeloader
unless defined?(@typeloader)
@typeloader = Puppet::Util::Autoload.new(self, "puppet/type", :wrap => false)
end
@typeloader
end
end
end
diff --git a/lib/puppet/module.rb b/lib/puppet/module.rb
index 39274c044..c579eecd8 100644
--- a/lib/puppet/module.rb
+++ b/lib/puppet/module.rb
@@ -1,333 +1,338 @@
require 'puppet/util/logging'
require 'semver'
-require 'puppet/module_tool/applications'
+require 'json'
# Support for modules
class Puppet::Module
class Error < Puppet::Error; end
class MissingModule < Error; end
class IncompatibleModule < Error; end
class UnsupportedPlatform < Error; end
class IncompatiblePlatform < Error; end
class MissingMetadata < Error; end
class InvalidName < Error; end
class InvalidFilePattern < Error; end
include Puppet::Util::Logging
FILETYPES = {
"manifests" => "manifests",
"files" => "files",
"templates" => "templates",
"plugins" => "lib",
"pluginfacts" => "facts.d",
}
# Find and return the +module+ that +path+ belongs to. If +path+ is
# absolute, or if there is no module whose name is the first component
# of +path+, return +nil+
def self.find(modname, environment = nil)
return nil unless modname
- Puppet::Node::Environment.new(environment).module(modname)
+ env = Puppet.lookup(:environments).get(environment || Puppet[:environment])
+ env.module(modname)
end
attr_reader :name, :environment, :path
attr_writer :environment
attr_accessor :dependencies, :forge_name
attr_accessor :source, :author, :version, :license, :puppetversion, :summary, :description, :project_page
def initialize(name, path, environment)
@name = name
@path = path
@environment = environment
assert_validity
load_metadata if has_metadata?
validate_puppet_version
@absolute_path_to_manifests = Puppet::FileSystem::PathPattern.absolute(manifests)
end
def has_metadata?
return false unless metadata_file
- return false unless Puppet::FileSystem::File.exist?(metadata_file)
+ return false unless Puppet::FileSystem.exist?(metadata_file)
begin
- metadata = PSON.parse(File.read(metadata_file))
- rescue PSON::PSONError => e
+ metadata = JSON.parse(File.read(metadata_file))
+ rescue JSON::JSONError => 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
FILETYPES.each do |type, location|
# A boolean method to let external callers determine if
# we have files of a given type.
define_method(type +'?') do
type_subpath = subpath(location)
- unless Puppet::FileSystem::File.exist?(type_subpath)
+ unless Puppet::FileSystem.exist?(type_subpath)
Puppet.debug("No #{type} found in subpath '#{type_subpath}' " +
"(file / directory does not exist)")
return false
end
return true
end
# A method for returning a given file of a given type.
# e.g., file = mod.manifest("my/manifest.pp")
#
# If the file name is nil, then the base directory for the
# file type is passed; this is used for fileserving.
define_method(type.sub(/s$/, '')) do |file|
# If 'file' is nil then they're asking for the base path.
# This is used for things like fileserving.
if file
full_path = File.join(subpath(location), file)
else
full_path = subpath(location)
end
- return nil unless Puppet::FileSystem::File.exist?(full_path)
+ return nil unless Puppet::FileSystem.exist?(full_path)
return full_path
end
# Return the base directory for the given type
define_method(type) do
subpath(location)
end
end
def license_file
return @license_file if defined?(@license_file)
return @license_file = nil unless path
@license_file = File.join(path, "License")
end
def load_metadata
- data = PSON.parse File.read(metadata_file)
+ data = JSON.parse File.read(metadata_file)
@forge_name = data['name'].gsub('-', '/') if data['name']
[:source, :author, :version, :license, :puppetversion, :dependencies].each do |attr|
unless value = data[attr.to_s]
unless attr == :puppetversion
raise MissingMetadata, "No #{attr} module metadata provided for #{self.name}"
end
end
# NOTICE: The fallback to `versionRequirement` is something we'd like to
# not have to support, but we have a reasonable number of releases that
# don't use `version_requirement`. When we can deprecate this, we should.
if attr == :dependencies
value.tap do |dependencies|
dependencies.each do |dep|
dep['version_requirement'] ||= dep['versionRequirement'] || '>= 0.0.0'
end
end
end
send(attr.to_s + "=", value)
end
end
# Return the list of manifests matching the given glob pattern,
# defaulting to 'init.{pp,rb}' for empty modules.
def match_manifests(rest)
if rest
wanted_manifests = wanted_manifests_from(rest)
searched_manifests = wanted_manifests.glob.reject { |f| FileTest.directory?(f) }
else
searched_manifests = []
end
# (#4220) Always ensure init.pp in case class is defined there.
init_manifests = [manifest("init.pp"), manifest("init.rb")].compact
init_manifests + searched_manifests
end
def all_manifests
- return [] unless Puppet::FileSystem::File.exist?(manifests)
+ return [] unless Puppet::FileSystem.exist?(manifests)
Dir.glob(File.join(manifests, '**', '*.{rb,pp}'))
end
def metadata_file
return @metadata_file if defined?(@metadata_file)
return @metadata_file = nil unless path
@metadata_file = File.join(path, "metadata.json")
end
def modulepath
File.dirname(path) if path
end
# Find all plugin directories. This is used by the Plugins fileserving mount.
def plugin_directory
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]
end
def to_s
result = "Module #{name}"
result += "(#{path})" if path
result
end
def dependencies_as_modules
dependent_modules = []
dependencies and dependencies.each do |dep|
author, dep_name = dep["name"].split('/')
found_module = environment.module(dep_name)
dependent_modules << found_module if found_module
end
dependent_modules
end
def required_by
environment.module_requirements[self.forge_name] || {}
end
def has_local_changes?
+ Puppet.deprecation_warning("This method is being removed.")
+ require 'puppet/module_tool/applications'
changes = Puppet::ModuleTool::Applications::Checksummer.run(path)
!changes.empty?
end
def local_changes
+ Puppet.deprecation_warning("This method is being removed.")
+ require 'puppet/module_tool/applications'
Puppet::ModuleTool::Applications::Checksummer.run(path)
end
# Identify and mark unmet dependencies. A dependency will be marked unmet
# for the following reasons:
#
# * not installed and is thus considered missing
# * installed and does not meet the version requirements for this module
# * installed and doesn't use semantic versioning
#
# Returns a list of hashes representing the details of an unmet dependency.
#
# Example:
#
# [
# {
# :reason => :missing,
# :name => 'puppetlabs-mysql',
# :version_constraint => 'v0.0.1',
# :mod_details => {
# :installed_version => '0.0.1'
# }
# :parent => {
# :name => 'puppetlabs-bacula',
# :version => 'v1.0.0'
# }
# }
# ]
#
def unmet_dependencies
unmet_dependencies = []
return unmet_dependencies unless dependencies
dependencies.each do |dependency|
forge_name = dependency['name']
version_string = dependency['version_requirement'] || '>= 0.0.0'
dep_mod = begin
environment.module_by_forge_name(forge_name)
rescue
nil
end
error_details = {
:name => forge_name,
:version_constraint => version_string.gsub(/^(?=\d)/, "v"),
:parent => {
:name => self.forge_name,
:version => self.version.gsub(/^(?=\d)/, "v")
},
:mod_details => {
:installed_version => dep_mod.nil? ? nil : dep_mod.version
}
}
unless dep_mod
error_details[:reason] = :missing
unmet_dependencies << error_details
next
end
if version_string
begin
required_version_semver_range = SemVer[version_string]
actual_version_semver = SemVer.new(dep_mod.version)
rescue ArgumentError
error_details[:reason] = :non_semantic_version
unmet_dependencies << error_details
next
end
unless required_version_semver_range.include? actual_version_semver
error_details[:reason] = :version_mismatch
unmet_dependencies << error_details
next
end
end
end
unmet_dependencies
end
def validate_puppet_version
return unless puppetversion and puppetversion != Puppet.version
raise IncompatibleModule, "Module #{self.name} is only compatible with Puppet version #{puppetversion}, not #{Puppet.version}"
end
private
def wanted_manifests_from(pattern)
begin
extended = File.extname(pattern).empty? ? "#{pattern}.{pp,rb}" : pattern
relative_pattern = Puppet::FileSystem::PathPattern.relative(extended)
rescue Puppet::FileSystem::PathPattern::InvalidPattern => error
raise Puppet::Module::InvalidFilePattern.new(
"The pattern \"#{pattern}\" to find manifests in the module \"#{name}\" " +
"is invalid and potentially unsafe.", error)
end
relative_pattern.prefix_with(@absolute_path_to_manifests)
end
def subpath(type)
File.join(path, type)
end
def assert_validity
raise InvalidName, "Invalid module name #{name}; module names must be alphanumeric (plus '-'), not '#{name}'" unless name =~ /^[-\w]+$/
end
def ==(other)
self.name == other.name &&
self.version == other.version &&
self.path == other.path &&
self.environment == other.environment
end
end
diff --git a/lib/puppet/module_tool.rb b/lib/puppet/module_tool.rb
index 98876b6dd..8ea5754cd 100644
--- a/lib/puppet/module_tool.rb
+++ b/lib/puppet/module_tool.rb
@@ -1,148 +1,143 @@
# encoding: UTF-8
# Load standard libraries
require 'pathname'
require 'fileutils'
require 'puppet/util/colors'
module Puppet
module ModuleTool
require 'puppet/module_tool/tar'
extend Puppet::Util::Colors
# Directory and names that should not be checksummed.
ARTIFACTS = ['pkg', /^\./, /^~/, /^#/, 'coverage', 'metadata.json', 'REVISION']
FULL_MODULE_NAME_PATTERN = /\A([^-\/|.]+)[-|\/](.+)\z/
REPOSITORY_URL = Puppet.settings[:module_repository]
# Is this a directory that shouldn't be checksummed?
#
# TODO: Should this be part of Checksums?
# TODO: Rename this method to reflect its purpose?
# TODO: Shouldn't this be used when building packages too?
def self.artifact?(path)
case File.basename(path)
when *ARTIFACTS
true
else
false
end
end
# Return the +username+ and +modname+ for a given +full_module_name+, or raise an
# ArgumentError if the argument isn't parseable.
def self.username_and_modname_from(full_module_name)
if matcher = full_module_name.match(FULL_MODULE_NAME_PATTERN)
return matcher.captures
else
raise ArgumentError, "Not a valid full name: #{full_module_name}"
end
end
# Find the module root when given a path by checking each directory up from
# its current location until it finds one that contains a file called
# 'Modulefile'.
#
# @param path [Pathname, String] path to start from
# @return [Pathname, nil] the root path of the module directory or nil if
# we cannot find one
def self.find_module_root(path)
path = Pathname.new(path) if path.class == String
path.expand_path.ascend do |p|
return p if is_module_root?(p)
end
nil
end
# Analyse path to see if it is a module root directory by detecting a
# file named 'Modulefile' in the directory.
#
# @param path [Pathname, String] path to analyse
# @return [Boolean] true if the path is a module root, false otherwise
def self.is_module_root?(path)
path = Pathname.new(path) if path.class == String
FileTest.file?(path + 'Modulefile')
end
# Builds a formatted tree from a list of node hashes containing +:text+
# and +:dependencies+ keys.
def self.format_tree(nodes, level = 0)
str = ''
nodes.each_with_index do |node, i|
last_node = nodes.length - 1 == i
deps = node[:dependencies] || []
str << (indent = " " * level)
str << (last_node ? "└" : "├")
str << "─"
str << (deps.empty? ? "─" : "┬")
str << " #{node[:text]}\n"
branch = format_tree(deps, level + 1)
branch.gsub!(/^#{indent} /, indent + '│') unless last_node
str << branch
end
return str
end
def self.build_tree(mods, dir)
mods.each do |mod|
version_string = mod[:version][:vstring].sub(/^(?!v)/, 'v')
if mod[:action] == :upgrade
previous_version = mod[:previous_version].sub(/^(?!v)/, 'v')
version_string = "#{previous_version} -> #{version_string}"
end
mod[:text] = "#{mod[:module]} (#{colorize(:cyan, version_string)})"
mod[:text] += " [#{mod[:path]}]" unless mod[:path] == dir
build_tree(mod[:dependencies], dir)
end
end
+ # @param options [Hash<Symbol,String>] This hash will contain any
+ # command-line arguments that are not Settings, as those will have already
+ # been extracted by the underlying application code.
+ #
+ # @note Unfortunately the whole point of this method is the side effect of
+ # modifying the options parameter. This same hash is referenced both
+ # when_invoked and when_rendering. For this reason, we are not returning
+ # a duplicate.
+ #
+ # An :environment_instance and a :target_dir are added/updated in the
+ # options parameter.
+ #
+ # @api private
def self.set_option_defaults(options)
- sep = File::PATH_SEPARATOR
-
- if options[:environment]
- Puppet.settings[:environment] = options[:environment]
- else
- options[:environment] = Puppet.settings[:environment]
- end
+ current_environment = Puppet.lookup(:current_environment)
+ modulepath = [options[:target_dir]] + current_environment.full_modulepath
- if options[:modulepath]
- Puppet.settings[:modulepath] = options[:modulepath]
- else
- # (#14872) make sure the module path of the desired environment is used
- # when determining the default value of the --target-dir option
- Puppet.settings[:modulepath] = options[:modulepath] =
- Puppet.settings.value(:modulepath, options[:environment])
- end
+ face_environment = current_environment.override_with(
+ :modulepath => modulepath.compact
+ )
- if options[:target_dir]
- options[:target_dir] = File.expand_path(options[:target_dir])
- # prepend the target dir to the module path
- Puppet.settings[:modulepath] = options[:modulepath] =
- options[:target_dir] + sep + options[:modulepath]
- else
- # default to the first component of the module path
- options[:target_dir] =
- File.expand_path(options[:modulepath].split(sep).first)
- end
+ options[:environment_instance] = face_environment
+ # Note: environment will have expanded the path
+ options[:target_dir] = face_environment.full_modulepath.first
end
end
end
# Load remaining libraries
require 'puppet/module_tool/errors'
require 'puppet/module_tool/applications'
require 'puppet/module_tool/checksums'
require 'puppet/module_tool/contents_description'
require 'puppet/module_tool/dependency'
require 'puppet/module_tool/metadata'
require 'puppet/module_tool/modulefile'
require 'puppet/module_tool/skeleton'
-require 'puppet/forge/cache'
require 'puppet/forge'
diff --git a/lib/puppet/module_tool/applications/application.rb b/lib/puppet/module_tool/applications/application.rb
index 93338be53..f2fb3389f 100644
--- a/lib/puppet/module_tool/applications/application.rb
+++ b/lib/puppet/module_tool/applications/application.rb
@@ -1,89 +1,89 @@
require 'net/http'
require 'semver'
require 'puppet/util/colors'
module Puppet::ModuleTool
module Applications
class Application
include Puppet::Util::Colors
def self.run(*args)
new(*args).run
end
attr_accessor :options
def initialize(options = {})
@options = options
end
def run
raise NotImplementedError, "Should be implemented in child classes."
end
def discuss(response, success, failure)
case response
when Net::HTTPOK, Net::HTTPCreated
Puppet.notice success
else
errors = PSON.parse(response.body)['error'] rescue "HTTP #{response.code}, #{response.body}"
Puppet.warning "#{failure} (#{errors})"
end
end
def metadata(require_modulefile = false)
unless @metadata
unless @path
raise ArgumentError, "Could not determine module path"
end
@metadata = Puppet::ModuleTool::Metadata.new
contents = ContentsDescription.new(@path)
contents.annotate(@metadata)
checksums = Checksums.new(@path)
checksums.annotate(@metadata)
modulefile_path = File.join(@path, 'Modulefile')
if File.file?(modulefile_path)
Puppet::ModuleTool::ModulefileReader.evaluate(@metadata, modulefile_path)
elsif require_modulefile
raise ArgumentError, "No Modulefile found."
end
extra_metadata_path = File.join(@path, 'metadata.json')
if File.file?(extra_metadata_path)
File.open(extra_metadata_path) do |f|
begin
@metadata.extra_metadata = PSON.load(f)
rescue PSON::ParserError
- raise ArgumentError, "Could not parse JSON #{extra_metadata_path}"
+ raise ArgumentError, "Could not parse JSON #{extra_metadata_path}", $!.backtrace
end
end
end
end
@metadata
end
def load_modulefile!
@metadata = nil
metadata(true)
end
def parse_filename(filename)
if match = /^((.*?)-(.*?))-(\d+\.\d+\.\d+.*?)$/.match(File.basename(filename,'.tar.gz'))
module_name, author, shortname, version = match.captures
else
raise ArgumentError, "Could not parse filename to obtain the username, module name and version. (#{@release_name})"
end
unless SemVer.valid?(version)
raise ArgumentError, "Invalid version format: #{version} (Semantic Versions are acceptable: http://semver.org)"
end
return {
:module_name => module_name,
:author => author,
:dir_name => shortname,
:version => version
}
end
end
end
end
diff --git a/lib/puppet/module_tool/applications/generator.rb b/lib/puppet/module_tool/applications/generator.rb
index 6b5bda98f..a27d5722c 100644
--- a/lib/puppet/module_tool/applications/generator.rb
+++ b/lib/puppet/module_tool/applications/generator.rb
@@ -1,141 +1,142 @@
require 'pathname'
require 'fileutils'
require 'erb'
module Puppet::ModuleTool
module Applications
class Generator < Application
def initialize(full_module_name, options = {})
begin
@metadata = Metadata.new(:full_module_name => full_module_name)
rescue ArgumentError
- raise "Could not generate directory #{full_module_name.inspect}, you must specify a dash-separated username and module name."
+ msg = "Could not generate directory #{full_module_name.inspect}, you must specify a dash-separated username and module name."
+ raise $!, msg, $!.backtrace
end
super(options)
end
def skeleton
@skeleton ||= Skeleton.new
end
def get_binding
binding
end
def run
if destination.directory?
raise ArgumentError, "#{destination} already exists."
end
Puppet.notice "Generating module at #{Dir.pwd}/#{@metadata.dashed_name}"
files_created = []
skeleton.path.find do |path|
if path == skeleton
destination.mkpath
else
node = Node.on(path, self)
if node
node.install!
files_created << node.target
else
Puppet.notice "Could not generate from #{path}"
end
end
end
# Return an array of Pathname objects representing file paths of files
# and directories just generated. This return value is used by the
# module_tool face generate action, and displayed on the console.
#
# Example return value:
#
# [
# #<Pathname:puppetlabs-apache>,
# #<Pathname:puppetlabs-apache/tests>,
# #<Pathname:puppetlabs-apache/tests/init.pp>,
# #<Pathname:puppetlabs-apache/spec>,
# #<Pathname:puppetlabs-apache/spec/spec_helper.rb>,
# #<Pathname:puppetlabs-apache/spec/spec.opts>,
# #<Pathname:puppetlabs-apache/README>,
# #<Pathname:puppetlabs-apache/Modulefile>,
# #<Pathname:puppetlabs-apache/metadata.json>,
# #<Pathname:puppetlabs-apache/manifests>,
# #<Pathname:puppetlabs-apache/manifests/init.pp"
# ]
#
files_created
end
def destination
@destination ||= Pathname.new(@metadata.dashed_name)
end
class Node
def self.types
@types ||= []
end
def self.inherited(klass)
types << klass
end
def self.on(path, generator)
klass = types.detect { |t| t.matches?(path) }
if klass
klass.new(path, generator)
end
end
def initialize(source, generator)
@generator = generator
@source = source
end
def read
@source.read
end
def target
target = @generator.destination + @source.relative_path_from(@generator.skeleton.path)
components = target.to_s.split(File::SEPARATOR).map do |part|
part == 'NAME' ? @generator.metadata.name : part
end
Pathname.new(components.join(File::SEPARATOR))
end
def install!
raise NotImplementedError, "Abstract"
end
end
class DirectoryNode < Node
def self.matches?(path)
path.directory?
end
def install!
target.mkpath
end
end
class ParsedFileNode < Node
def self.matches?(path)
path.file? && path.extname == '.erb'
end
def target
path = super
path.parent + path.basename('.erb')
end
def contents
template = ERB.new(read)
template.result(@generator.send(:get_binding))
end
def install!
target.open('w') { |f| f.write contents }
end
end
class FileNode < Node
def self.matches?(path)
path.file?
end
def install!
FileUtils.cp(@source, target)
end
end
end
end
end
diff --git a/lib/puppet/module_tool/applications/installer.rb b/lib/puppet/module_tool/applications/installer.rb
index 772d7a831..e8ab06777 100644
--- a/lib/puppet/module_tool/applications/installer.rb
+++ b/lib/puppet/module_tool/applications/installer.rb
@@ -1,187 +1,192 @@
require 'open-uri'
require 'pathname'
require 'fileutils'
require 'tmpdir'
require 'semver'
require 'puppet/forge'
require 'puppet/module_tool'
require 'puppet/module_tool/shared_behaviors'
require 'puppet/module_tool/install_directory'
module Puppet::ModuleTool
module Applications
class Installer < Application
include Puppet::ModuleTool::Errors
include Puppet::Forge::Errors
def initialize(name, forge, install_dir, options = {})
super(options)
@action = :install
- @environment = Puppet::Node::Environment.new(Puppet.settings[:environment])
+ @environment = options[:environment_instance]
@force = options[:force]
@ignore_dependencies = options[:force] || options[:ignore_dependencies]
@name = name
@forge = forge
@install_dir = install_dir
end
def run
results = {}
begin
if is_module_package?(@name)
@source = :filesystem
@filename = File.expand_path(@name)
- raise MissingPackageError, :requested_package => @filename unless Puppet::FileSystem::File.exist?(@filename)
+ raise MissingPackageError, :requested_package => @filename unless Puppet::FileSystem.exist?(@filename)
parsed = parse_filename(@filename)
@module_name = parsed[:module_name]
@version = parsed[:version]
else
@source = :repository
@module_name = @name.gsub('/', '-')
@version = options[:version]
end
results = {
:module_name => @module_name,
:module_version => @version,
:install_dir => options[:target_dir],
}
@install_dir.prepare(@module_name, @version || 'latest')
cached_paths = get_release_packages
unless @graph.empty?
Puppet.notice 'Installing -- do not interrupt ...'
cached_paths.each do |hash|
hash.each do |dir, path|
Unpacker.new(path, @options.merge(:target_dir => dir)).run
end
end
end
rescue ModuleToolError, ForgeError => err
results[:error] = {
:oneline => err.message,
:multiline => err.multiline,
}
else
results[:result] = :success
results[:installed_modules] = @graph
ensure
results[:result] ||= :failure
end
results
end
private
include Puppet::ModuleTool::Shared
# Return a Pathname object representing the path to the module
# release package in the `Puppet.settings[:module_working_dir]`.
def get_release_packages
get_local_constraints
if !@force && @installed.include?(@module_name)
raise AlreadyInstalledError,
:module_name => @module_name,
:installed_version => @installed[@module_name].first.version,
:requested_version => @version || (@conditions[@module_name].empty? ? :latest : :best),
- :local_changes => @installed[@module_name].first.local_changes
+ :local_changes => Puppet::ModuleTool::Applications::Checksummer.run(@installed[@module_name].first.path)
end
if @ignore_dependencies && @source == :filesystem
@urls = {}
@remote = { "#{@module_name}@#{@version}" => { } }
@versions = {
@module_name => [
{ :vstring => @version, :semver => SemVer.new(@version) }
]
}
else
get_remote_constraints(@forge)
end
@graph = resolve_constraints({ @module_name => @version })
@graph.first[:tarball] = @filename if @source == :filesystem
resolve_install_conflicts(@graph) unless @force
# This clean call means we never "cache" the module we're installing, but this
# is desired since module authors can easily rerelease modules different content but the same
# version number, meaning someone with the old content cached will be very confused as to why
# they can't get new content.
# Long term we should just get rid of this caching behavior and cleanup downloaded modules after they install
# but for now this is a quick fix to disable caching
Puppet::Forge::Cache.clean
download_tarballs(@graph, @graph.last[:path], @forge)
end
#
# Resolve installation conflicts by checking if the requested module
# or one of its dependencies conflicts with an installed module.
#
# Conflicts occur under the following conditions:
#
# When installing 'puppetlabs-foo' and an existing directory in the
# target install path contains a 'foo' directory and we cannot determine
# the "full name" of the installed module.
#
# When installing 'puppetlabs-foo' and 'pete-foo' is already installed.
# This is considered a conflict because 'puppetlabs-foo' and 'pete-foo'
# install into the same directory 'foo'.
#
def resolve_install_conflicts(graph, is_dependency = false)
+ Puppet.debug("Resolving conflicts for #{graph.map {|n| n[:module]}.join(',')}")
+
graph.each do |release|
@environment.modules_by_path[options[:target_dir]].each do |mod|
if mod.has_metadata?
metadata = {
:name => mod.forge_name.gsub('/', '-'),
:version => mod.version
}
next if release[:module] == metadata[:name]
else
metadata = nil
end
if release[:module] =~ /-#{mod.name}$/
dependency_info = {
:name => release[:module],
:version => release[:version][:vstring]
}
dependency = is_dependency ? dependency_info : nil
all_versions = @versions["#{@module_name}"].sort_by { |h| h[:semver] }
versions = all_versions.select { |x| x[:semver].special == '' }
versions = all_versions if versions.empty?
latest_version = versions.last[:vstring]
raise InstallConflictError,
:requested_module => @module_name,
:requested_version => @version || "latest: v#{latest_version}",
:dependency => dependency,
:directory => mod.path,
:metadata => metadata
end
+ end
- resolve_install_conflicts(release[:dependencies], true)
+ deps = release[:dependencies]
+ if deps && !deps.empty?
+ resolve_install_conflicts(deps, true)
end
end
end
#
# Check if a file is a vaild module package.
# ---
# FIXME: Checking for a valid module package should be more robust and
# use the actual metadata contained in the package. 03132012 - Hightower
# +++
#
def is_module_package?(name)
filename = File.expand_path(name)
filename =~ /.tar.gz$/
end
end
end
end
diff --git a/lib/puppet/module_tool/applications/uninstaller.rb b/lib/puppet/module_tool/applications/uninstaller.rb
index 006a88b8e..c07c98737 100644
--- a/lib/puppet/module_tool/applications/uninstaller.rb
+++ b/lib/puppet/module_tool/applications/uninstaller.rb
@@ -1,107 +1,110 @@
module Puppet::ModuleTool
module Applications
class Uninstaller < Application
include Puppet::ModuleTool::Errors
def initialize(name, options)
@name = name
@options = options
@errors = Hash.new {|h, k| h[k] = {}}
@unfiltered = []
@installed = []
@suggestions = []
- @environment = Puppet::Node::Environment.new(options[:environment])
+ @environment = options[:environment_instance]
end
def run
results = {
:module_name => @name,
:requested_version => @version,
}
begin
find_installed_module
validate_module
FileUtils.rm_rf(@installed.first.path, :secure => true)
results[:affected_modules] = @installed
results[:result] = :success
rescue ModuleToolError => err
results[:error] = {
:oneline => err.message,
:multiline => err.multiline,
}
rescue => e
results[:error] = {
:oneline => e.message,
:multiline => e.respond_to?(:multiline) ? e.multiline : [e.to_s, e.backtrace].join("\n")
}
ensure
results[:result] ||= :failure
end
results
end
private
def find_installed_module
@environment.modules_by_path.values.flatten.each do |mod|
mod_name = (mod.forge_name || mod.name).gsub('/', '-')
if mod_name == @name
@unfiltered << {
:name => mod_name,
:version => mod.version,
:path => mod.modulepath,
}
if @options[:version] && mod.version
next unless SemVer[@options[:version]].include?(SemVer.new(mod.version))
end
@installed << mod
elsif mod_name =~ /#{@name}/
@suggestions << mod_name
end
end
if @installed.length > 1
raise MultipleInstalledError,
:action => :uninstall,
:module_name => @name,
:installed_modules => @installed.sort_by { |mod| @environment.modulepath.index(mod.modulepath) }
elsif @installed.empty?
if @unfiltered.empty?
raise NotInstalledError,
:action => :uninstall,
:suggestions => @suggestions,
:module_name => @name
else
raise NoVersionMatchesError,
:installed_modules => @unfiltered.sort_by { |mod| @environment.modulepath.index(mod[:path]) },
:version_range => @options[:version],
:module_name => @name
end
end
end
def validate_module
mod = @installed.first
- if !@options[:force] && mod.has_metadata? && mod.has_local_changes?
- raise LocalChangesError,
- :action => :uninstall,
- :module_name => (mod.forge_name || mod.name).gsub('/', '-'),
- :requested_version => @options[:version],
- :installed_version => mod.version
+ if !@options[:force] && mod.has_metadata?
+ changes = Puppet::ModuleTool::Applications::Checksummer.run(mod.path)
+ if !changes.empty?
+ raise LocalChangesError,
+ :action => :uninstall,
+ :module_name => (mod.forge_name || mod.name).gsub('/', '-'),
+ :requested_version => @options[:version],
+ :installed_version => mod.version
+ end
end
if !@options[:force] && !mod.required_by.empty?
raise ModuleIsRequiredError,
:module_name => (mod.forge_name || mod.name).gsub('/', '-'),
:required_by => mod.required_by,
:requested_version => @options[:version],
:installed_version => mod.version
end
end
end
end
end
diff --git a/lib/puppet/module_tool/applications/unpacker.rb b/lib/puppet/module_tool/applications/unpacker.rb
index 8fd5431b2..f141610b0 100644
--- a/lib/puppet/module_tool/applications/unpacker.rb
+++ b/lib/puppet/module_tool/applications/unpacker.rb
@@ -1,60 +1,60 @@
require 'pathname'
require 'tmpdir'
module Puppet::ModuleTool
module Applications
class Unpacker < Application
def initialize(filename, options = {})
@filename = Pathname.new(filename)
parsed = parse_filename(filename)
@module_name = parsed[:module_name]
super(options)
@module_path = Pathname(options[:target_dir])
@module_dir = @module_path + parsed[:dir_name]
end
def run
extract_module_to_install_dir
# Return the Pathname object representing the directory where the
# module release archive was unpacked the to, and the module release
# name.
@module_dir
end
# Obtain a suitable temporary path for building and unpacking tarballs
#
# @return [Pathname] path to temporary build location
def build_dir
Puppet::Forge::Cache.base_path + "tmp-unpacker-#{Digest::SHA1.hexdigest(@filename.basename.to_s)}"
end
private
def extract_module_to_install_dir
delete_existing_installation_or_abort!
build_dir.mkpath
begin
begin
Puppet::ModuleTool::Tar.instance(@module_name).unpack(@filename.to_s, build_dir.to_s, [@module_path.stat.uid, @module_path.stat.gid].join(':'))
rescue Puppet::ExecutionFailure => e
- raise RuntimeError, "Could not extract contents of module archive: #{e.message}"
+ raise RuntimeError, "Could not extract contents of module archive: #{e.message}", e.backtrace
end
# grab the first directory
extracted = build_dir.children.detect { |c| c.directory? }
FileUtils.mv extracted, @module_dir
ensure
build_dir.rmtree
end
end
def delete_existing_installation_or_abort!
return unless @module_dir.exist?
FileUtils.rm_rf(@module_dir, :secure => true)
end
end
end
end
diff --git a/lib/puppet/module_tool/applications/upgrader.rb b/lib/puppet/module_tool/applications/upgrader.rb
index 226f24050..b2e617ba4 100644
--- a/lib/puppet/module_tool/applications/upgrader.rb
+++ b/lib/puppet/module_tool/applications/upgrader.rb
@@ -1,110 +1,113 @@
module Puppet::ModuleTool
module Applications
class Upgrader < Application
include Puppet::ModuleTool::Errors
def initialize(name, forge, options)
@action = :upgrade
- @environment = Puppet::Node::Environment.new(Puppet.settings[:environment])
+ @environment = options[:environment_instance]
@module_name = name
@options = options
@force = options[:force]
@ignore_dependencies = options[:force] || options[:ignore_dependencies]
@version = options[:version]
@forge = forge
end
def run
begin
results = { :module_name => @module_name }
get_local_constraints
if @installed[@module_name].length > 1
raise MultipleInstalledError,
:action => :upgrade,
:module_name => @module_name,
:installed_modules => @installed[@module_name].sort_by { |mod| @environment.modulepath.index(mod.modulepath) }
elsif @installed[@module_name].empty?
raise NotInstalledError,
:action => :upgrade,
:module_name => @module_name
end
@module = @installed[@module_name].last
results[:installed_version] = @module.version ? @module.version.sub(/^(?=\d)/, 'v') : nil
results[:requested_version] = @version || (@conditions[@module_name].empty? ? :latest : :best)
dir = @module.modulepath
Puppet.notice "Found '#{@module_name}' (#{colorize(:cyan, results[:installed_version] || '???')}) in #{dir} ..."
- if !@options[:force] && @module.has_metadata? && @module.has_local_changes?
- raise LocalChangesError,
- :action => :upgrade,
- :module_name => @module_name,
- :requested_version => @version || (@conditions[@module_name].empty? ? :latest : :best),
- :installed_version => @module.version
+ if !@options[:force] && @module.has_metadata?
+ changes = Puppet::ModuleTool::Applications::Checksummer.run(@module.path)
+ if !changes.empty?
+ raise LocalChangesError,
+ :action => :upgrade,
+ :module_name => @module_name,
+ :requested_version => @version || (@conditions[@module_name].empty? ? :latest : :best),
+ :installed_version => @module.version
+ end
end
begin
get_remote_constraints(@forge)
rescue => e
- raise UnknownModuleError, results.merge(:repository => @forge.uri)
+ raise UnknownModuleError, results.merge(:repository => @forge.uri), e.backtrace
else
raise UnknownVersionError, results.merge(:repository => @forge.uri) if @remote.empty?
end
if !@options[:force] && @versions["#{@module_name}"].last[:vstring].sub(/^(?=\d)/, 'v') == (@module.version || '0.0.0').sub(/^(?=\d)/, 'v')
raise VersionAlreadyInstalledError,
:module_name => @module_name,
:requested_version => @version || ((@conditions[@module_name].empty? ? 'latest' : 'best') + ": #{@versions["#{@module_name}"].last[:vstring].sub(/^(?=\d)/, 'v')}"),
:installed_version => @installed[@module_name].last.version,
:conditions => @conditions[@module_name] + [{ :module => :you, :version => @version }]
end
@graph = resolve_constraints({ @module_name => @version })
# This clean call means we never "cache" the module we're installing, but this
# is desired since module authors can easily rerelease modules different content but the same
# version number, meaning someone with the old content cached will be very confused as to why
# they can't get new content.
# Long term we should just get rid of this caching behavior and cleanup downloaded modules after they install
# but for now this is a quick fix to disable caching
Puppet::Forge::Cache.clean
tarballs = download_tarballs(@graph, @graph.last[:path], @forge)
unless @graph.empty?
Puppet.notice 'Upgrading -- do not interrupt ...'
tarballs.each do |hash|
hash.each do |dir, path|
Unpacker.new(path, @options.merge(:target_dir => dir)).run
end
end
end
results[:result] = :success
results[:base_dir] = @graph.first[:path]
results[:affected_modules] = @graph
rescue VersionAlreadyInstalledError => e
results[:result] = :noop
results[:error] = {
:oneline => e.message,
:multiline => e.multiline
}
rescue => e
results[:error] = {
:oneline => e.message,
:multiline => e.respond_to?(:multiline) ? e.multiline : [e.to_s, e.backtrace].join("\n")
}
ensure
results[:result] ||= :failure
end
return results
end
private
include Puppet::ModuleTool::Shared
end
end
end
diff --git a/lib/puppet/module_tool/checksums.rb b/lib/puppet/module_tool/checksums.rb
index 044357a8f..4731a734e 100644
--- a/lib/puppet/module_tool/checksums.rb
+++ b/lib/puppet/module_tool/checksums.rb
@@ -1,52 +1,52 @@
require 'digest/md5'
module Puppet::ModuleTool
# = Checksums
#
# This class proides methods for generating checksums for data and adding
# them to +Metadata+.
class Checksums
include Enumerable
# Instantiate object with string +path+ to create checksums from.
def initialize(path)
@path = Pathname.new(path)
end
# Return checksum for the +Pathname+.
def checksum(pathname)
- return Digest::MD5.hexdigest(Puppet::FileSystem::File.new(pathname).binread)
+ return Digest::MD5.hexdigest(Puppet::FileSystem.binread(pathname))
end
# Return checksums for object's +Pathname+, generate if it's needed.
# Result is a hash of path strings to checksum strings.
def data
unless @data
@data = {}
@path.find do |descendant|
if Puppet::ModuleTool.artifact?(descendant)
Find.prune
elsif descendant.file?
path = descendant.relative_path_from(@path)
@data[path.to_s] = checksum(descendant)
end
end
end
return @data
end
# TODO: Why?
def each(&block)
data.each(&block)
end
# Update +Metadata+'s checksums with this object's.
def annotate(metadata)
metadata.checksums.replace(data)
end
# TODO: Move the Checksummer#run checksum checking to here?
end
end
diff --git a/lib/puppet/module_tool/contents_description.rb b/lib/puppet/module_tool/contents_description.rb
index 0d19cd3ef..7fd6505f4 100644
--- a/lib/puppet/module_tool/contents_description.rb
+++ b/lib/puppet/module_tool/contents_description.rb
@@ -1,82 +1,84 @@
+require 'puppet/module_tool'
+
module Puppet::ModuleTool
# = ContentsDescription
#
# This class populates +Metadata+'s Puppet type information.
class ContentsDescription
# Instantiate object for string +module_path+.
def initialize(module_path)
@module_path = module_path
end
# Update +Metadata+'s Puppet type information.
def annotate(metadata)
metadata.types.replace data.clone
end
# Return types for this module. Result is an array of hashes, each of which
# describes a Puppet type. The type description hash structure is:
# * :name => Name of this Puppet type.
# * :doc => Documentation for this type.
# * :properties => Array of hashes representing the type's properties, each
# containing :name and :doc.
# * :parameters => Array of hashes representing the type's parameters, each
# containing :name and :doc.
# * :providers => Array of hashes representing the types providers, each
# containing :name and :doc.
# TODO Write a TypeDescription to encapsulate these structures and logic?
def data
unless @data
@data = []
type_names = []
for module_filename in Dir[File.join(@module_path, "lib/puppet/type/*.rb")]
require module_filename
type_name = File.basename(module_filename, ".rb")
type_names << type_name
for provider_filename in Dir[File.join(@module_path, "lib/puppet/provider/#{type_name}/*.rb")]
require provider_filename
end
end
type_names.each do |type_name|
if type = Puppet::Type.type(type_name.to_sym)
type_hash = {:name => type_name, :doc => type.doc}
type_hash[:properties] = attr_doc(type, :property)
type_hash[:parameters] = attr_doc(type, :param)
if type.providers.size > 0
type_hash[:providers] = provider_doc(type)
end
@data << type_hash
else
Puppet.warning "Could not find/load type: #{type_name}"
end
end
end
@data
end
# Return an array of hashes representing this +type+'s attrs of +kind+
# (e.g. :param or :property), each containing :name and :doc.
def attr_doc(type, kind)
[].tap do |attrs|
type.allattrs.each do |name|
if type.attrtype(name) == kind && name != :provider
attrs.push(:name => name, :doc => type.attrclass(name).doc)
end
end
end
end
# Return an array of hashes representing this +type+'s providers, each
# containing :name and :doc.
def provider_doc(type)
[].tap do |providers|
type.providers.sort.each do |prov|
providers.push(:name => prov, :doc => type.provider(prov).doc)
end
end
end
end
end
diff --git a/lib/puppet/module_tool/dependency.rb b/lib/puppet/module_tool/dependency.rb
index 222e714c8..7535a17d9 100644
--- a/lib/puppet/module_tool/dependency.rb
+++ b/lib/puppet/module_tool/dependency.rb
@@ -1,30 +1,29 @@
+require 'puppet/module_tool'
+require 'puppet/network/format_support'
+
module Puppet::ModuleTool
class Dependency
+ include Puppet::Network::FormatSupport
attr_reader :full_module_name, :username, :name, :version_requirement, :repository
# Instantiates a new module dependency with a +full_module_name+ (e.g.
# "myuser-mymodule"), and optional +version_requirement+ (e.g. "0.0.1") and
# optional repository (a URL string).
def initialize(full_module_name, version_requirement = nil, repository = nil)
@full_module_name = full_module_name
# TODO: add error checking, the next line raises ArgumentError when +full_module_name+ is invalid
@username, @name = Puppet::ModuleTool.username_and_modname_from(full_module_name)
@version_requirement = version_requirement
@repository = repository ? Puppet::Forge::Repository.new(repository) : nil
end
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
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/errors.rb b/lib/puppet/module_tool/errors.rb
index b2b36ada6..3f917bd35 100644
--- a/lib/puppet/module_tool/errors.rb
+++ b/lib/puppet/module_tool/errors.rb
@@ -1,9 +1,11 @@
+require 'puppet/module_tool'
+
module Puppet::ModuleTool
module Errors
require 'puppet/module_tool/errors/base'
require 'puppet/module_tool/errors/installer'
require 'puppet/module_tool/errors/uninstaller'
require 'puppet/module_tool/errors/upgrader'
require 'puppet/module_tool/errors/shared'
end
end
diff --git a/lib/puppet/module_tool/install_directory.rb b/lib/puppet/module_tool/install_directory.rb
index d52ceaa87..a53a542fa 100644
--- a/lib/puppet/module_tool/install_directory.rb
+++ b/lib/puppet/module_tool/install_directory.rb
@@ -1,41 +1,44 @@
+require 'puppet/module_tool'
+require 'puppet/module_tool/errors'
+
module Puppet
module ModuleTool
# Control the install location for modules.
class InstallDirectory
include Puppet::ModuleTool::Errors
def initialize(target_directory)
@target_directory = target_directory
end
# prepare the module install location. This will create the location if
# needed.
def prepare(module_name, version)
return if @target_directory.directory?
begin
@target_directory.mkpath
Puppet.notice "Created target directory #{@target_directory}"
rescue SystemCallError => orig_error
raise converted_to_friendly_error(module_name, version, orig_error)
end
end
private
ERROR_MAPPINGS = {
Errno::EACCES => PermissionDeniedCreateInstallDirectoryError,
Errno::EEXIST => InstallPathExistsNotDirectoryError,
}
def converted_to_friendly_error(module_name, version, orig_error)
return orig_error if not ERROR_MAPPINGS.include?(orig_error.class)
ERROR_MAPPINGS[orig_error.class].new(orig_error,
:requested_module => module_name,
:requested_version => version,
:directory => @target_directory.to_s)
end
end
end
end
diff --git a/lib/puppet/module_tool/metadata.rb b/lib/puppet/module_tool/metadata.rb
index 650043802..bc3ccb376 100644
--- a/lib/puppet/module_tool/metadata.rb
+++ b/lib/puppet/module_tool/metadata.rb
@@ -1,157 +1,155 @@
require 'puppet/util/methodhelper'
+require 'puppet/module_tool'
+require 'puppet/network/format_support'
module Puppet::ModuleTool
# = Metadata
#
# This class provides a data structure representing a module's metadata.
# It provides some basic parsing, but other data is injected into it using
# +annotate+ methods in other classes.
class Metadata
include Puppet::Util::MethodHelper
+ include Puppet::Network::FormatSupport
# The full name of the module, which is a dash-separated combination of the
# +username+ and module +name+.
attr_reader :full_module_name
# The name of the user that owns this module.
attr_reader :username
# The name of this module. See also +full_module_name+.
attr_reader :name
# The version of this module.
attr_reader :version
# Instantiate from a hash, whose keys are setters in this class.
def initialize(settings={})
set_options(settings)
end
# Set the full name of this module, and from it, the +username+ and
# module +name+.
def full_module_name=(full_module_name)
@full_module_name = full_module_name
@username, @name = Puppet::ModuleTool::username_and_modname_from(full_module_name)
end
# Return an array of the module's Dependency objects.
def dependencies
return @dependencies ||= []
end
def author
@author || @username
end
def author=(author)
@author = author
end
def source
@source || 'UNKNOWN'
end
def source=(source)
@source = source
end
def license
@license || 'Apache License, Version 2.0'
end
def license=(license)
@license = license
end
def summary
@summary || 'UNKNOWN'
end
def summary=(summary)
@summary = summary
end
def description
@description || 'UNKNOWN'
end
def description=(description)
@description = description
end
def extra_metadata
@extra_metadata || {}
end
def extra_metadata=(extra_metadata)
@extra_metadata = extra_metadata
end
def project_page
@project_page || 'UNKNOWN'
end
def project_page=(project_page)
@project_page = project_page
end
# Return an array of the module's Puppet types, each one is a hash
# containing :name and :doc.
def types
return @types ||= []
end
# Return module's file checksums.
def checksums
return @checksums ||= {}
end
# Return the dashed name of the module, which may either be the
# dash-separated combination of the +username+ and module +name+, or just
# the module +name+.
def dashed_name
return [@username, @name].compact.join('-')
end
# Return the release name, which is the combination of the +dashed_name+
# of the module and its +version+ number.
def release_name
return [dashed_name, @version].join('-')
end
# Set the version of this module, ensure a string like '0.1.0' see the
# Semantic Versions here: http://semver.org
def version=(version)
if SemVer.valid?(version)
@version = version
else
raise ArgumentError, "Invalid version format: #{@version} (Semantic Versions are acceptable: http://semver.org)"
end
end
def to_data_hash()
return extra_metadata.merge({
'name' => @full_module_name,
'version' => @version,
'source' => source,
'author' => author,
'license' => license,
'summary' => summary,
'description' => description,
'project_page' => project_page,
'dependencies' => dependencies,
'types' => types,
'checksums' => checksums
})
end
def to_hash()
to_data_hash
end
-
- # Return the PSON record representing this instance.
- def to_pson(*args)
- return to_data_hash.to_pson(*args)
- end
end
end
diff --git a/lib/puppet/module_tool/modulefile.rb b/lib/puppet/module_tool/modulefile.rb
index 321fcac37..46abc0d90 100644
--- a/lib/puppet/module_tool/modulefile.rb
+++ b/lib/puppet/module_tool/modulefile.rb
@@ -1,75 +1,78 @@
+require 'puppet/module_tool'
+require 'puppet/module_tool/dependency'
+
module Puppet::ModuleTool
# = Modulefile
#
# This class provides the DSL used for evaluating the module's 'Modulefile'.
# These methods are used to concisely define this module's attributes, which
# are later rendered as PSON into a 'metadata.json' file.
class ModulefileReader
# Read the +filename+ and eval its Ruby code to set values in the Metadata
# +metadata+ instance.
def self.evaluate(metadata, filename)
builder = new(metadata)
if File.file?(filename)
builder.instance_eval(File.read(filename.to_s), filename.to_s, 1)
else
Puppet.warning "No Modulefile: #{filename}"
end
return builder
end
# Instantiate with the Metadata +metadata+ instance.
def initialize(metadata)
@metadata = metadata
end
# Set the +full_module_name+ (e.g. "myuser-mymodule"), which will also set the
# +username+ and module +name+. Required.
def name(name)
@metadata.full_module_name = name
end
# Set the module +version+ (e.g., "0.1.0"). Required.
def version(version)
@metadata.version = version
end
# Add a dependency with the full_module_name +name+ (e.g. "myuser-mymodule"), an
# optional +version_requirement+ (e.g. "0.1.0") and +repository+ (a URL
# string). Optional. Can be called multiple times to add many dependencies.
def dependency(name, version_requirement = nil, repository = nil)
@metadata.dependencies << Dependency.new(name, version_requirement, repository)
end
# Set the source
def source(source)
@metadata.source = source
end
# Set the author or default to +username+
def author(author)
@metadata.author = author
end
# Set the license
def license(license)
@metadata.license = license
end
# Set the summary
def summary(summary)
@metadata.summary = summary
end
# Set the description
def description(description)
@metadata.description = description
end
# Set the project page
def project_page(project_page)
@metadata.project_page = project_page
end
end
end
diff --git a/lib/puppet/module_tool/shared_behaviors.rb b/lib/puppet/module_tool/shared_behaviors.rb
index 2b79b4ed4..99ce52900 100644
--- a/lib/puppet/module_tool/shared_behaviors.rb
+++ b/lib/puppet/module_tool/shared_behaviors.rb
@@ -1,163 +1,168 @@
+require 'open-uri'
+
+require 'puppet/module_tool'
+require 'puppet/module_tool/errors'
+
module Puppet::ModuleTool::Shared
include Puppet::ModuleTool::Errors
def get_local_constraints
@local = Hash.new { |h,k| h[k] = { } }
@conditions = Hash.new { |h,k| h[k] = [] }
@installed = Hash.new { |h,k| h[k] = [] }
@environment.modules_by_path.values.flatten.each do |mod|
mod_name = (mod.forge_name || mod.name).gsub('/', '-')
@installed[mod_name] << mod
d = @local["#{mod_name}@#{mod.version}"]
(mod.dependencies || []).each do |hash|
name, conditions = hash['name'], hash['version_requirement']
name = name.gsub('/', '-')
d[name] = conditions
@conditions[name] << {
:module => mod_name,
:version => mod.version,
:dependency => conditions
}
end
end
end
def get_remote_constraints(forge)
@remote = Hash.new { |h,k| h[k] = { } }
@urls = {}
@versions = Hash.new { |h,k| h[k] = [] }
Puppet.notice "Downloading from #{forge.uri} ..."
author, modname = Puppet::ModuleTool.username_and_modname_from(@module_name)
info = forge.remote_dependency_info(author, modname, @options[:version])
info.each do |pair|
mod_name, releases = pair
mod_name = mod_name.gsub('/', '-')
releases.each do |rel|
semver = SemVer.new(rel['version'] || '0.0.0') rescue SemVer::MIN
@versions[mod_name] << { :vstring => rel['version'], :semver => semver }
@versions[mod_name].sort! { |a, b| a[:semver] <=> b[:semver] }
@urls["#{mod_name}@#{rel['version']}"] = rel['file']
d = @remote["#{mod_name}@#{rel['version']}"]
(rel['dependencies'] || []).each do |name, conditions|
d[name.gsub('/', '-')] = conditions
end
end
end
end
def implicit_version(mod)
return :latest if @conditions[mod].empty?
if @conditions[mod].all? { |c| c[:queued] || c[:module] == :you }
return :latest
end
return :best
end
def annotated_version(mod, versions)
if versions.empty?
return implicit_version(mod)
else
return "#{implicit_version(mod)}: #{versions.last}"
end
end
def resolve_constraints(dependencies, source = [{:name => :you}], seen = {}, action = @action)
dependencies = dependencies.map do |mod, range|
source.last[:dependency] = range
@conditions[mod] << {
:module => source.last[:name],
:version => source.last[:version],
:dependency => range,
:queued => true
}
if @force
range = SemVer[@version] rescue SemVer['>= 0.0.0']
else
range = (@conditions[mod]).map do |r|
SemVer[r[:dependency]] rescue SemVer['>= 0.0.0']
end.inject(&:&)
end
if @action == :install && seen.include?(mod)
next if range === seen[mod][:semver]
req_module = @module_name
req_versions = @versions["#{@module_name}"].map { |v| v[:semver] }
raise InvalidDependencyCycleError,
:module_name => mod,
:source => (source + [{ :name => mod, :version => source.last[:dependency] }]),
:requested_module => req_module,
:requested_version => @version || annotated_version(req_module, req_versions),
:conditions => @conditions
end
if !(@force || @installed[mod].empty? || source.last[:name] == :you)
next if range === SemVer.new(@installed[mod].first.version)
action = :upgrade
elsif @installed[mod].empty?
action = :install
end
if action == :upgrade
@conditions.each { |_, conds| conds.delete_if { |c| c[:module] == mod } }
end
versions = @versions["#{mod}"].select { |h| range === h[:semver] }
valid_versions = versions.select { |x| x[:semver].special == '' }
valid_versions = versions if valid_versions.empty?
unless version = valid_versions.last
req_module = @module_name
req_versions = @versions["#{@module_name}"].map { |v| v[:semver] }
raise NoVersionsSatisfyError,
:requested_name => req_module,
:requested_version => @version || annotated_version(req_module, req_versions),
:installed_version => @installed[@module_name].empty? ? nil : @installed[@module_name].first.version,
:dependency_name => mod,
:conditions => @conditions[mod],
:action => @action
end
seen[mod] = version
{
:module => mod,
:version => version,
:action => action,
:previous_version => @installed[mod].empty? ? nil : @installed[mod].first.version,
:file => @urls["#{mod}@#{version[:vstring]}"],
:path => action == :install ? @options[:target_dir] : (@installed[mod].empty? ? @options[:target_dir] : @installed[mod].first.modulepath),
:dependencies => []
}
end.compact
dependencies.each do |mod|
deps = @remote["#{mod[:module]}@#{mod[:version][:vstring]}"].sort_by(&:first)
mod[:dependencies] = resolve_constraints(deps, source + [{ :name => mod[:module], :version => mod[:version][:vstring] }], seen, :install)
end unless @ignore_dependencies
return dependencies
end
def download_tarballs(graph, default_path, forge)
graph.map do |release|
begin
if release[:tarball]
cache_path = Pathname(release[:tarball])
else
cache_path = forge.retrieve(release[:file])
end
rescue OpenURI::HTTPError => e
- raise RuntimeError, "Could not download module: #{e.message}"
+ raise RuntimeError, "Could not download module: #{e.message}", e.backtrace
end
[
{ (release[:path] ||= default_path) => cache_path},
*download_tarballs(release[:dependencies], default_path, forge)
]
end.flatten
end
end
diff --git a/lib/puppet/module_tool/skeleton.rb b/lib/puppet/module_tool/skeleton.rb
index c5e26e35f..4eaf4446f 100644
--- a/lib/puppet/module_tool/skeleton.rb
+++ b/lib/puppet/module_tool/skeleton.rb
@@ -1,34 +1,37 @@
+require 'pathname'
+require 'puppet/module_tool'
+
module Puppet::ModuleTool
# = Skeleton
#
# This class provides methods for finding templates for the 'generate' action.
class Skeleton
# TODO Review whether the 'freeze' feature should be fixed or deleted.
# def freeze!
# FileUtils.rm_fr custom_path rescue nil
# FileUtils.cp_r default_path, custom_path
# end
# Return Pathname with 'generate' templates.
def path
paths.detect { |path| path.directory? }
end
# Return Pathnames to look for 'generate' templates.
def paths
@paths ||= [ custom_path, default_path ]
end
# Return Pathname of custom templates directory.
def custom_path
Pathname(Puppet.settings[:module_skeleton_dir])
end
# Return Pathname of default template directory.
def default_path
Pathname(__FILE__).dirname + 'skeleton/templates/generator'
end
end
end
diff --git a/lib/puppet/module_tool/tar.rb b/lib/puppet/module_tool/tar.rb
index 6b3257cf4..6b844545a 100644
--- a/lib/puppet/module_tool/tar.rb
+++ b/lib/puppet/module_tool/tar.rb
@@ -1,18 +1,21 @@
+require 'puppet/module_tool'
+require 'puppet/util'
+
module Puppet::ModuleTool::Tar
require 'puppet/module_tool/tar/gnu'
require 'puppet/module_tool/tar/solaris'
require 'puppet/module_tool/tar/mini'
def self.instance(module_name)
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
elsif Puppet.features.minitar? && Puppet.features.zlib?
Mini.new(module_name)
else
raise RuntimeError, 'No suitable tar implementation found'
end
end
end
diff --git a/lib/puppet/network/auth_config_parser.rb b/lib/puppet/network/auth_config_parser.rb
index 2af06051c..cb7199ee3 100644
--- a/lib/puppet/network/auth_config_parser.rb
+++ b/lib/puppet/network/auth_config_parser.rb
@@ -1,84 +1,84 @@
require 'puppet/network/rights'
module Puppet::Network
class AuthConfigParser
def self.new_from_file(file)
self.new(File.read(file))
end
def initialize(string)
@string = string
end
def parse
Puppet::Network::AuthConfig.new(parse_rights)
end
def parse_rights
rights = Puppet::Network::Rights.new
right = nil
count = 1
@string.each_line { |line|
case line.chomp
when /^\s*#/, /^\s*$/
# skip comments and blank lines
when /^path\s+((?:~\s+)?[^ ]+)\s*$/ # "path /path" or "path ~ regex"
name = $1.chomp
right = rights.newright(name, count, @file)
when /^\s*(allow(?:_ip)?|deny(?:_ip)?|method|environment|auth(?:enticated)?)\s+(.+?)(\s*#.*)?$/
if right.nil?
raise Puppet::ConfigurationError, "Missing or invalid 'path' before right directive at line #{count} of #{@file}"
end
parse_right_directive(right, $1, $2, count)
else
raise Puppet::ConfigurationError, "Invalid line #{count}: #{line}"
end
count += 1
}
# Verify each of the rights are valid.
# We let the check raise an error, so that it can raise an error
# pointing to the specific problem.
rights.each { |name, right|
right.valid?
}
rights
end
def parse_right_directive(right, var, value, count)
value.strip!
case var
when "allow"
modify_right(right, :allow, value, "allowing %s access", count)
when "deny"
modify_right(right, :deny, value, "denying %s access", count)
when "allow_ip"
modify_right(right, :allow_ip, value, "allowing IP %s access", count)
when "deny_ip"
modify_right(right, :deny_ip, value, "denying IP %s access", count)
when "method"
modify_right(right, :restrict_method, value, "allowing 'method' %s", count)
when "environment"
modify_right(right, :restrict_environment, value, "adding environment %s", count)
when /auth(?:enticated)?/
modify_right(right, :restrict_authenticated, value, "adding authentication %s", count)
else
raise Puppet::ConfigurationError,
"Invalid argument '#{var}' at line #{count}"
end
end
def modify_right(right, method, value, msg, count)
value.split(/\s*,\s*/).each do |val|
begin
val.strip!
right.info msg % val
right.send(method, val)
rescue Puppet::AuthStoreError => detail
- raise Puppet::ConfigurationError, "#{detail} at line #{count} of #{@file}"
+ raise Puppet::ConfigurationError, "#{detail} at line #{count} of #{@file}", detail.backtrace
end
end
end
end
end
diff --git a/lib/puppet/network/authconfig.rb b/lib/puppet/network/authconfig.rb
index 527774598..f99c12952 100644
--- a/lib/puppet/network/authconfig.rb
+++ b/lib/puppet/network/authconfig.rb
@@ -1,73 +1,76 @@
require 'puppet/network/rights'
module Puppet
class ConfigurationError < Puppet::Error; end
class Network::AuthConfig
attr_accessor :rights
DEFAULT_ACL = [
{ :acl => "~ ^\/catalog\/([^\/]+)$", :method => :find, :allow => '$1', :authenticated => true },
{ :acl => "~ ^\/node\/([^\/]+)$", :method => :find, :allow => '$1', :authenticated => true },
# this one will allow all file access, and thus delegate
# to fileserver.conf
{ :acl => "/file" },
{ :acl => "/certificate_revocation_list/ca", :method => :find, :authenticated => true },
{ :acl => "~ ^\/report\/([^\/]+)$", :method => :save, :allow => '$1', :authenticated => true },
# These allow `auth any`, because if you can do them anonymously you
# should probably also be able to do them when trusted.
{ :acl => "/certificate/ca", :method => :find, :authenticated => :any },
{ :acl => "/certificate/", :method => :find, :authenticated => :any },
{ :acl => "/certificate_request", :method => [:find, :save], :authenticated => :any },
{ :acl => "/status", :method => [:find], :authenticated => true },
+
+ # API V2.0
+ { :acl => "/v2.0/environments", :method => :find, :allow => '*', :authenticated => true },
]
# Just proxy the setting methods to our rights stuff
[:allow, :deny].each do |method|
define_method(method) do |*args|
@rights.send(method, *args)
end
end
# force regular ACLs to be present
def insert_default_acl
DEFAULT_ACL.each do |acl|
unless rights[acl[:acl]]
Puppet.info "Inserting default '#{acl[:acl]}' (auth #{acl[:authenticated]}) ACL"
mk_acl(acl)
end
end
# queue an empty (ie deny all) right for every other path
# actually this is not strictly necessary as the rights system
# denies not explicitely allowed paths
unless rights["/"]
rights.newright("/").restrict_authenticated(:any)
end
end
def mk_acl(acl)
right = @rights.newright(acl[:acl])
right.allow(acl[:allow] || "*")
if method = acl[:method]
method = [method] unless method.is_a?(Array)
method.each { |m| right.restrict_method(m) }
end
right.restrict_authenticated(acl[:authenticated]) unless acl[:authenticated].nil?
end
# check whether this request is allowed in our ACL
# raise an Puppet::Network::AuthorizedError if the request
# is denied.
- def check_authorization(indirection, method, key, params)
- if authorization_failure_exception = @rights.is_request_forbidden_and_why?(indirection, method, key, params)
+ def check_authorization(method, path, params)
+ if authorization_failure_exception = @rights.is_request_forbidden_and_why?(method, path, params)
Puppet.warning("Denying access: #{authorization_failure_exception}")
raise authorization_failure_exception
end
end
def initialize(rights=nil)
@rights = rights || Puppet::Network::Rights.new
insert_default_acl
end
end
end
diff --git a/lib/puppet/network/authentication.rb b/lib/puppet/network/authentication.rb
index f10e461d3..c7f9ace87 100644
--- a/lib/puppet/network/authentication.rb
+++ b/lib/puppet/network/authentication.rb
@@ -1,30 +1,35 @@
require 'puppet/ssl/certificate_authority'
require 'puppet/util/log/rate_limited_logger'
# Place for any authentication related bits
module Puppet::Network::Authentication
# Create a rate-limited logger for the expiration warning that uses the run interval
# as the minimum amount of time before a warning about the same cert can be logged again.
# This is a class variable so that all classes that include the module share the same logger.
@@logger = Puppet::Util::Log::RateLimitedLogger.new(Puppet[:runinterval])
# Check the expiration of known certificates and optionally any that are specified as part of a request
def warn_if_near_expiration(*certs)
# Check CA cert if we're functioning as a CA
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 Puppet::FileSystem::File.exist?(Puppet[:hostcert])
+ # Depending on the run mode, the localhost certificate will be for the
+ # master or the agent. Don't load the certificate if the CA cert is not
+ # present: infinite recursion will occur as another authenticated request
+ # will be spawned to download the CA cert.
+ if [Puppet[:hostcert], Puppet[:localcacert]].all? {|path| Puppet::FileSystem.exist?(path) }
+ certs << Puppet::SSL::Host.localhost.certificate
+ end
# Remove nil values for caller convenience
certs.compact.each do |cert|
# Allow raw OpenSSL certificate instances or Puppet certificate wrappers to be specified
cert = Puppet::SSL::Certificate.from_instance(cert) if cert.is_a?(OpenSSL::X509::Certificate)
raise ArgumentError, "Invalid certificate '#{cert.inspect}'" unless cert.is_a?(Puppet::SSL::Certificate)
if cert.near_expiration?
@@logger.warning("Certificate '#{cert.unmunged_name}' will expire on #{cert.expiration.strftime('%Y-%m-%dT%H:%M:%S%Z')}")
end
end
end
end
diff --git a/lib/puppet/network/authorization.rb b/lib/puppet/network/authorization.rb
index 43b8d0633..82e4bc3f7 100644
--- a/lib/puppet/network/authorization.rb
+++ b/lib/puppet/network/authorization.rb
@@ -1,34 +1,34 @@
require 'puppet/network/client_request'
require 'puppet/network/authconfig'
require 'puppet/network/auth_config_parser'
module Puppet::Network
class AuthConfigLoader
# Create our config object if necessary. If there's no configuration file
# we install our defaults
def self.authconfig
@auth_config_file ||= Puppet::Util::WatchedFile.new(Puppet[:rest_authconfig])
if (not @auth_config) or @auth_config_file.changed?
begin
@auth_config = Puppet::Network::AuthConfigParser.new_from_file(Puppet[:rest_authconfig]).parse
rescue Errno::ENOENT, Errno::ENOTDIR
@auth_config = Puppet::Network::AuthConfig.new
end
end
@auth_config
end
end
module Authorization
def authconfig
AuthConfigLoader.authconfig
end
# Verify that our client has access.
- def check_authorization(indirection, method, key, params)
- authconfig.check_authorization(indirection, method, key, params)
+ def check_authorization(method, path, params)
+ authconfig.check_authorization(method, path, params)
end
end
end
diff --git a/lib/puppet/network/format_support.rb b/lib/puppet/network/format_support.rb
index 79f7fe665..6a42fd4d7 100644
--- a/lib/puppet/network/format_support.rb
+++ b/lib/puppet/network/format_support.rb
@@ -1,120 +1,124 @@
require 'puppet/network/format_handler'
# Provides network serialization support when included
# @api public
module Puppet::Network::FormatSupport
def self.included(klass)
klass.extend(ClassMethods)
end
module ClassMethods
def convert_from(format, data)
get_format(format).intern(self, data)
rescue => err
raise Puppet::Network::FormatHandler::FormatError, "Could not intern from #{format}: #{err}", err.backtrace
end
def convert_from_multiple(format, data)
get_format(format).intern_multiple(self, data)
rescue => err
raise Puppet::Network::FormatHandler::FormatError, "Could not intern_multiple from #{format}: #{err}", err.backtrace
end
def render_multiple(format, instances)
get_format(format).render_multiple(instances)
rescue => err
raise Puppet::Network::FormatHandler::FormatError, "Could not render_multiple to #{format}: #{err}", err.backtrace
end
def default_format
supported_formats[0]
end
def support_format?(name)
Puppet::Network::FormatHandler.format(name).supported?(self)
end
def supported_formats
result = format_handler.formats.collect do |f|
format_handler.format(f)
end.find_all do |f|
f.supported?(self)
end.sort do |a, b|
# It's an inverse sort -- higher weight formats go first.
b.weight <=> a.weight
end.collect do |f|
f.name
end
result = put_preferred_format_first(result)
Puppet.debug "#{friendly_name} supports formats: #{result.join(' ')}"
result
end
# @api private
def get_format(format_name)
format_handler.format_for(format_name)
end
private
def format_handler
Puppet::Network::FormatHandler
end
def friendly_name
if self.respond_to? :indirection
indirection.name
else
self
end
end
def put_preferred_format_first(list)
preferred_format = Puppet.settings[:preferred_serialization_format].to_sym
if list.include?(preferred_format)
list.delete(preferred_format)
list.unshift(preferred_format)
else
Puppet.debug "Value of 'preferred_serialization_format' (#{preferred_format}) is invalid for #{friendly_name}, using default (#{list.first})"
end
list
end
end
def to_msgpack(*args)
to_data_hash.to_msgpack(*args)
end
+ def to_pson(*args)
+ to_data_hash.to_pson(*args)
+ end
+
def render(format = nil)
format ||= self.class.default_format
self.class.get_format(format).render(self)
rescue => err
raise Puppet::Network::FormatHandler::FormatError, "Could not render to #{format}: #{err}", err.backtrace
end
def mime(format = nil)
format ||= self.class.default_format
self.class.get_format(format).mime
rescue => err
raise Puppet::Network::FormatHandler::FormatError, "Could not mime to #{format}: #{err}", err.backtrace
end
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 62e40d376..e636b30e8 100644
--- a/lib/puppet/network/formats.rb
+++ b/lib/puppet/network/formats.rb
@@ -1,221 +1,216 @@
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
+Puppet::Network::FormatHandler.create_serialized_formats(:msgpack, :weight => 20, :mime => "application/x-msgpack", :required_methods => [:render_method, :intern_method], :intern_method => :from_data_hash) do
+
+ confine :feature => :msgpack
+
def intern(klass, text)
data = MessagePack.unpack(text)
return data if data.is_a?(klass)
- klass.from_pson(data)
+ klass.from_data_hash(data)
end
def intern_multiple(klass, text)
MessagePack.unpack(text).collect do |data|
- klass.from_pson(data)
+ klass.from_data_hash(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)
data_to_instance(klass, data)
end
def intern_multiple(klass, text)
data = YAML.load(text, :safe => true, :deserialize_symbols => true)
unless data.respond_to?(:collect)
raise Puppet::Network::FormatHandler::FormatError, "Serialized YAML did not contain a collection of instances when calling intern_multiple"
end
data.collect do |datum|
data_to_instance(klass, datum)
end
end
def data_to_instance(klass, data)
return data if data.is_a?(klass)
unless data.is_a? Hash
raise Puppet::Network::FormatHandler::FormatError, "Serialized YAML did not contain a valid instance of #{klass}"
end
- klass.from_pson(data)
+ klass.from_data_hash(data)
end
def render(instance)
instance.to_yaml
end
# Yaml monkey-patches Array, so this works.
def render_multiple(instances)
instances.to_yaml
end
def supported?(klass)
true
end
end
# This is a "special" format which is used for the moment only when sending facts
# as REST GET parameters (see Puppet::Configurer::FactHandler).
# This format combines a yaml serialization, then zlib compression and base64 encoding.
Puppet::Network::FormatHandler.create_serialized_formats(:b64_zlib_yaml) do
require 'base64'
def use_zlib?
Puppet.features.zlib? && Puppet[:zlib]
end
def requiring_zlib
if use_zlib?
yield
else
raise Puppet::Error, "the zlib library is not installed or is disabled."
end
end
def intern(klass, text)
requiring_zlib do
Puppet::Network::FormatHandler.format(:yaml).intern(klass, decode(text))
end
end
def intern_multiple(klass, text)
requiring_zlib do
Puppet::Network::FormatHandler.format(:yaml).intern_multiple(klass, decode(text))
end
end
def render(instance)
encode(instance.to_yaml)
end
def render_multiple(instances)
encode(instances.to_yaml)
end
def supported?(klass)
true
end
def decode(data)
Zlib::Inflate.inflate(Base64.decode64(data))
end
def encode(text)
requiring_zlib do
Base64.encode64(Zlib::Deflate.deflate(text, Zlib::BEST_COMPRESSION))
end
end
end
Puppet::Network::FormatHandler.create(:s, :mime => "text/plain", :extension => "txt")
# A very low-weight format so it'll never get chosen automatically.
Puppet::Network::FormatHandler.create(:raw, :mime => "application/x-raw", :weight => 1) do
def intern_multiple(klass, text)
raise NotImplementedError
end
def render_multiple(instances)
raise NotImplementedError
end
# LAK:NOTE The format system isn't currently flexible enough to handle
# what I need to support raw formats just for individual instances (rather
# than both individual and collections), but we don't yet have enough data
# to make a "correct" design.
# So, we hack it so it works for singular but fail if someone tries it
# on plurals.
def supported?(klass)
true
end
end
-Puppet::Network::FormatHandler.create_serialized_formats(:pson, :weight => 10, :required_methods => [:render_method, :intern_method]) do
+Puppet::Network::FormatHandler.create_serialized_formats(:pson, :weight => 10, :required_methods => [:render_method, :intern_method], :intern_method => :from_data_hash) do
def intern(klass, text)
data_to_instance(klass, PSON.parse(text))
end
def intern_multiple(klass, text)
PSON.parse(text).collect do |data|
data_to_instance(klass, data)
end
end
# PSON monkey-patches Array, so this works.
def render_multiple(instances)
instances.to_pson
end
# If they pass class information, we want to ignore it. By default,
# we'll include class information but we won't rely on it - we don't
# want class names to be required because we then can't change our
# internal class names, which is bad.
def data_to_instance(klass, data)
if data.is_a?(Hash) and d = data['data']
data = d
end
return data if data.is_a?(klass)
- klass.from_pson(data)
+ klass.from_data_hash(data)
end
end
# This is really only ever going to be used for Catalogs.
Puppet::Network::FormatHandler.create_serialized_formats(:dot, :required_methods => [:render_method])
Puppet::Network::FormatHandler.create(:console,
:mime => 'text/x-console-text',
:weight => 0) do
def json
@json ||= Puppet::Network::FormatHandler.format(:pson)
end
def render(datum)
# String to String
return datum if datum.is_a? String
return datum if datum.is_a? Numeric
# Simple hash to table
if datum.is_a? Hash and datum.keys.all? { |x| x.is_a? String or x.is_a? Numeric }
output = ''
column_a = datum.empty? ? 2 : datum.map{ |k,v| k.to_s.length }.max + 2
datum.sort_by { |k,v| k.to_s } .each do |key, value|
output << key.to_s.ljust(column_a)
output << json.render(value).
chomp.gsub(/\n */) { |x| x + (' ' * column_a) }
output << "\n"
end
return output
end
# Print one item per line for arrays
if datum.is_a? Array
output = ''
datum.each do |item|
output << item.to_s
output << "\n"
end
return output
end
# ...or pretty-print the inspect outcome.
return json.render(datum)
end
def render_multiple(data)
data.collect(&:render).join("\n")
end
end
diff --git a/lib/puppet/network/http.rb b/lib/puppet/network/http.rb
index 3519efd11..a2712b8d5 100644
--- a/lib/puppet/network/http.rb
+++ b/lib/puppet/network/http.rb
@@ -1,4 +1,15 @@
module Puppet::Network::HTTP
HEADER_ENABLE_PROFILING = "X-Puppet-Profiling"
HEADER_PUPPET_VERSION = "X-Puppet-Version"
+
+ require 'puppet/network/http/issues'
+ require 'puppet/network/http/error'
+ require 'puppet/network/http/route'
+ require 'puppet/network/http/api'
+ require 'puppet/network/http/api/v1'
+ require 'puppet/network/http/api/v2'
+ require 'puppet/network/http/handler'
+ require 'puppet/network/http/response'
+ require 'puppet/network/http/request'
+ require 'puppet/network/http/memory_response'
end
diff --git a/lib/puppet/network/http/api.rb b/lib/puppet/network/http/api.rb
index 8b1b747ac..ba0b0b7af 100644
--- a/lib/puppet/network/http/api.rb
+++ b/lib/puppet/network/http/api.rb
@@ -1,4 +1,2 @@
-require 'puppet/network/http'
-
class Puppet::Network::HTTP::API
end
diff --git a/lib/puppet/network/http/api/v1.rb b/lib/puppet/network/http/api/v1.rb
index 29146ff9b..6bbdd87c3 100644
--- a/lib/puppet/network/http/api/v1.rb
+++ b/lib/puppet/network/http/api/v1.rb
@@ -1,83 +1,218 @@
-require 'puppet/network/http/api'
+require 'puppet/network/authorization'
+
+class Puppet::Network::HTTP::API::V1
+ include Puppet::Network::Authorization
-module Puppet::Network::HTTP::API::V1
# How we map http methods and the indirection name in the URI
# to an indirection method.
METHOD_MAP = {
"GET" => {
:plural => :search,
:singular => :find
},
"POST" => {
:singular => :find,
},
"PUT" => {
:singular => :save
},
"DELETE" => {
:singular => :destroy
},
"HEAD" => {
:singular => :head
}
}
+ def self.routes
+ Puppet::Network::HTTP::Route.path(/.*/).any(new)
+ end
+
+ # handle an HTTP request
+ def call(request, response)
+ indirection_name, method, key, params = uri2indirection(request.method, request.path, request.params)
+ certificate = request.client_cert
+
+ check_authorization(method, "/#{indirection_name}/#{key}", params)
+
+ indirection = Puppet::Indirector::Indirection.instance(indirection_name.to_sym)
+ raise ArgumentError, "Could not find indirection '#{indirection_name}'" unless indirection
+
+ if !indirection.allow_remote_requests?
+ # TODO: should we tell the user we found an indirection but it doesn't
+ # allow remote requests, or just pretend there's no handler at all? what
+ # are the security implications for the former?
+ raise Puppet::Network::HTTP::Error::HTTPNotFoundError.new("No handler for #{indirection.name}", :NO_INDIRECTION_REMOTE_REQUESTS)
+ end
+
+ trusted = Puppet::Context::TrustedInformation.remote(params[:authenticated], params[:node], certificate)
+ Puppet.override(:trusted_information => trusted) do
+ send("do_#{method}", indirection, key, params, request, response)
+ end
+ rescue Puppet::Network::HTTP::Error::HTTPError => e
+ return do_http_control_exception(response, e)
+ rescue Exception => e
+ return do_exception(response, e)
+ end
+
def uri2indirection(http_method, uri, params)
environment, indirection, key = uri.split("/", 4)[1..-1] # the first field is always nil because of the leading slash
- raise ArgumentError, "The environment must be purely alphanumeric, not '#{environment}'" unless environment =~ /^\w+$/
+ raise ArgumentError, "The environment must be purely alphanumeric, not '#{environment}'" unless Puppet::Node::Environment.valid_name?(environment)
raise ArgumentError, "The indirection name must be purely alphanumeric, not '#{indirection}'" unless indirection =~ /^\w+$/
method = indirection_method(http_method, indirection)
- params[:environment] = Puppet::Node::Environment.new(environment)
+ configured_environment = Puppet.lookup(:environments).get(environment)
+ configured_environment = configured_environment.override_from_commandline(Puppet.settings)
+ params[:environment] = configured_environment
+
params.delete(:bucket_path)
raise ArgumentError, "No request key specified in #{uri}" if key == "" or key.nil?
key = URI.unescape(key)
[indirection, method, key, params]
end
- def indirection2uri(request)
- indirection = request.method == :search ? pluralize(request.indirection_name.to_s) : request.indirection_name.to_s
- "/#{request.environment.to_s}/#{indirection}/#{request.escaped_key}#{request.query_string}"
+ private
+
+ def do_http_control_exception(response, exception)
+ msg = exception.message
+ Puppet.info(msg)
+ response.respond_with(exception.status, "text/plain", msg)
end
- def request_to_uri_and_body(request)
- indirection = request.method == :search ? pluralize(request.indirection_name.to_s) : request.indirection_name.to_s
- ["/#{request.environment.to_s}/#{indirection}/#{request.escaped_key}", request.query_string.sub(/^\?/,'')]
+ def do_exception(response, exception, status=400)
+ if exception.is_a?(Puppet::Network::AuthorizationError)
+ # make sure we return the correct status code
+ # for authorization issues
+ status = 403 if status == 400
+ end
+
+ Puppet.log_exception(exception)
+
+ response.respond_with(status, "text/plain", exception.to_s)
+ end
+
+ # Execute our find.
+ def do_find(indirection, key, params, request, response)
+ unless result = indirection.find(key, params)
+ raise Puppet::Network::HTTP::Error::HTTPNotFoundError.new("Could not find #{indirection.name} #{key}", Puppet::Network::HTTP::Issues::RESOURCE_NOT_FOUND)
+ end
+
+ format = accepted_response_formatter_for(indirection.model, request)
+
+ rendered_result = result
+ if result.respond_to?(:render)
+ Puppet::Util::Profiler.profile("Rendered result in #{format}") do
+ rendered_result = result.render(format)
+ end
+ end
+
+ Puppet::Util::Profiler.profile("Sent response") do
+ response.respond_with(200, format, rendered_result)
+ end
+ end
+
+ # Execute our head.
+ def do_head(indirection, key, params, request, response)
+ unless indirection.head(key, params)
+ raise Puppet::Network::HTTP::Error::HTTPNotFoundError.new("Could not find #{indirection.name} #{key}", Puppet::Network::HTTP::Issues::RESOURCE_NOT_FOUND)
+ end
+
+ # No need to set a response because no response is expected from a
+ # HEAD request. All we need to do is not die.
+ end
+
+ # Execute our search.
+ def do_search(indirection, key, params, request, response)
+ result = indirection.search(key, params)
+
+ if result.nil?
+ raise Puppet::Network::HTTP::Error::HTTPNotFoundError.new("Could not find instances in #{indirection.name} with '#{key}'", Puppet::Network::HTTP::Issues::RESOURCE_NOT_FOUND)
+ end
+
+ format = accepted_response_formatter_for(indirection.model, request)
+
+ response.respond_with(200, format, indirection.model.render_multiple(format, result))
+ end
+
+ # Execute our destroy.
+ def do_destroy(indirection, key, params, request, response)
+ formatter = accepted_response_formatter_or_yaml_for(indirection.model, request)
+
+ result = indirection.destroy(key, params)
+
+ response.respond_with(200, formatter, formatter.render(result))
+ end
+
+ # Execute our save.
+ 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 = indirection.save(sent_object, key)
+
+ response.respond_with(200, formatter, formatter.render(result))
+ end
+
+ def accepted_response_formatter_for(model_class, request)
+ accepted_formats = request.headers['accept'] or raise Puppet::Network::HTTP::Error::HTTPNotAcceptableError.new("Missing required Accept header", Puppet::Network::HTTP::Issues::MISSING_HEADER_FIELD)
+ request.response_formatter_for(model_class.supported_formats, accepted_formats)
+ end
+
+ def accepted_response_formatter_or_yaml_for(model_class, request)
+ accepted_formats = request.headers['accept'] || "yaml"
+ request.response_formatter_for(model_class.supported_formats, accepted_formats)
+ end
+
+ def read_body_into_model(model_class, request)
+ data = request.body.to_s
+
+ format = request.format
+ model_class.convert_from(format, data)
end
def indirection_method(http_method, indirection)
raise ArgumentError, "No support for http method #{http_method}" unless METHOD_MAP[http_method]
unless method = METHOD_MAP[http_method][plurality(indirection)]
raise ArgumentError, "No support for plurality #{plurality(indirection)} for #{http_method} operations"
end
method
end
- def pluralize(indirection)
+ def self.indirection2uri(request)
+ indirection = request.method == :search ? pluralize(request.indirection_name.to_s) : request.indirection_name.to_s
+ "/#{request.environment.to_s}/#{indirection}/#{request.escaped_key}#{request.query_string}"
+ end
+
+ def self.request_to_uri_and_body(request)
+ indirection = request.method == :search ? pluralize(request.indirection_name.to_s) : request.indirection_name.to_s
+ ["/#{request.environment.to_s}/#{indirection}/#{request.escaped_key}", request.query_string.sub(/^\?/,'')]
+ end
+
+ def self.pluralize(indirection)
return(indirection == "status" ? "statuses" : indirection + "s")
end
def plurality(indirection)
# NOTE This specific hook for facts is ridiculous, but it's a *many*-line
# fix to not need this, and our goal is to move away from the complication
# that leads to the fix being too long.
return :singular if indirection == "facts"
return :singular if indirection == "status"
return :singular if indirection == "certificate_status"
return :plural if indirection == "inventory"
result = (indirection =~ /s$|_search$/) ? :plural : :singular
indirection.sub!(/s$|_search$/, '')
indirection.sub!(/statuse$/, 'status')
result
end
end
diff --git a/lib/puppet/network/http/api/v2.rb b/lib/puppet/network/http/api/v2.rb
new file mode 100644
index 000000000..ef9672df4
--- /dev/null
+++ b/lib/puppet/network/http/api/v2.rb
@@ -0,0 +1,32 @@
+module Puppet::Network::HTTP::API::V2
+ require 'puppet/network/http/api/v2/environments'
+ require 'puppet/network/http/api/v2/authorization'
+
+ def self.routes
+ path(%r{^/v2\.0}).
+ get(Authorization.new).
+ chain(ENVIRONMENTS, NOT_FOUND)
+ end
+
+ private
+
+ def self.path(path)
+ Puppet::Network::HTTP::Route.path(path)
+ end
+
+ def self.provide(&block)
+ lambda do |request, response|
+ block.call.call(request, response)
+ end
+ end
+
+ NOT_FOUND = Puppet::Network::HTTP::Route.
+ path(/.*/).
+ any(lambda do |req, res|
+ raise Puppet::Network::HTTP::Error::HTTPNotFoundError.new(req.path, Puppet::Network::HTTP::Issues::HANDLER_NOT_FOUND)
+ end)
+
+ ENVIRONMENTS = path(%r{^/environments$}).get(provide do
+ Environments.new(Puppet.lookup(:environments))
+ end)
+end
diff --git a/lib/puppet/network/http/api/v2/authorization.rb b/lib/puppet/network/http/api/v2/authorization.rb
new file mode 100644
index 000000000..010376a06
--- /dev/null
+++ b/lib/puppet/network/http/api/v2/authorization.rb
@@ -0,0 +1,13 @@
+class Puppet::Network::HTTP::API::V2::Authorization
+ include Puppet::Network::Authorization
+
+ def call(request, response)
+ raise Puppet::Network::HTTP::Error::HTTPNotAuthorizedError, "Only GET requests are authorized for V2 endpoints" unless request.method == "GET"
+
+ begin
+ check_authorization(:find, request.path, request.params)
+ rescue Puppet::Network::AuthorizationError => e
+ raise Puppet::Network::HTTP::Error::HTTPNotAuthorizedError, e.message, e.backtrace
+ end
+ end
+end
diff --git a/lib/puppet/network/http/api/v2/environments.rb b/lib/puppet/network/http/api/v2/environments.rb
new file mode 100644
index 000000000..6331be857
--- /dev/null
+++ b/lib/puppet/network/http/api/v2/environments.rb
@@ -0,0 +1,21 @@
+require 'json'
+
+class Puppet::Network::HTTP::API::V2::Environments
+ def initialize(env_loader)
+ @env_loader = env_loader
+ end
+
+ def call(request, response)
+ response.respond_with(200, "application/json", JSON.dump({
+ "search_paths" => @env_loader.search_paths,
+ "environments" => Hash[@env_loader.list.collect do |env|
+ [env.name, {
+ "settings" => {
+ "modulepath" => env.full_modulepath,
+ "manifest" => env.manifest
+ }
+ }]
+ end]
+ }))
+ end
+end
diff --git a/lib/puppet/network/http/connection.rb b/lib/puppet/network/http/connection.rb
index 24d712b6a..a2ce6eef3 100644
--- a/lib/puppet/network/http/connection.rb
+++ b/lib/puppet/network/http/connection.rb
@@ -1,197 +1,254 @@
require 'net/https'
require 'puppet/ssl/host'
require 'puppet/ssl/configuration'
require 'puppet/ssl/validator'
require 'puppet/network/authentication'
require 'uri'
module Puppet::Network::HTTP
# This will be raised if too many redirects happen for a given HTTP request
class RedirectionLimitExceededException < Puppet::Error ; end
# This class provides simple methods for issuing various types of HTTP
# requests. It's interface is intended to mirror Ruby's Net::HTTP
# object, but it provides a few important bits of additional
# functionality. Notably:
#
# * Any HTTPS requests made using this class will use Puppet's SSL
# certificate configuration for their authentication, and
# * Provides some useful error handling for any SSL errors that occur
# during a request.
+ # @api public
class Connection
include Puppet::Network::Authentication
OPTION_DEFAULTS = {
:use_ssl => true,
:verify => nil,
- :redirect_limit => 10
+ :redirect_limit => 10,
}
@@openssl_initialized = false
# 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 [#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
+ # @param options [Hash] options influencing the properties of the created
+ # connection,
+ # @option options [Boolean] :use_ssl true to connect with SSL, false
+ # otherwise, defaults to true
+ # @option options [#setup_connection] :verify An object that will configure
+ # any verification to do on the connection
+ # @option options [Fixnum] :redirect_limit 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
+ # @note The correct way to obtain a connection is to use one of the factory
+ # methods on {Puppet::Network::HttpPool}
# @api private
def initialize(host, port, options = {})
@host = host
@port = port
unknown_options = options.keys - OPTION_DEFAULTS.keys
raise Puppet::Error, "Unrecognized option(s): #{unknown_options.map(&:inspect).sort.join(', ')}" unless unknown_options.empty?
options = OPTION_DEFAULTS.merge(options)
@use_ssl = options[:use_ssl]
@verify = options[:verify]
@redirect_limit = options[:redirect_limit]
end
- def get(*args)
- request(:get, *args)
+ # @!macro [new] common_options
+ # @param options [Hash] options influencing the request made
+ # @option options [Hash{Symbol => String}] :basic_auth The basic auth
+ # :username and :password to use for the request
+
+ # @param path [String]
+ # @param headers [Hash{String => String}]
+ # @!macro common_options
+ # @api public
+ def get(path, headers = {}, options = {})
+ request_with_redirects(Net::HTTP::Get.new(path, headers), options)
end
- def post(*args)
- request(:post, *args)
+ # @param path [String]
+ # @param data [String]
+ # @param headers [Hash{String => String}]
+ # @!macro common_options
+ # @api public
+ def post(path, data, headers = nil, options = {})
+ request = Net::HTTP::Post.new(path, headers)
+ request.body = data
+ request_with_redirects(request, options)
end
- def head(*args)
- request(:head, *args)
+ # @param path [String]
+ # @param headers [Hash{String => String}]
+ # @!macro common_options
+ # @api public
+ def head(path, headers = {}, options = {})
+ request_with_redirects(Net::HTTP::Head.new(path, headers), options)
end
- def delete(*args)
- request(:delete, *args)
+ # @param path [String]
+ # @param headers [Hash{String => String}]
+ # @!macro common_options
+ # @api public
+ def delete(path, headers = {'Depth' => 'Infinity'}, options = {})
+ request_with_redirects(Net::HTTP::Delete.new(path, headers), options)
end
- def put(*args)
- request(:put, *args)
+ # @param path [String]
+ # @param data [String]
+ # @param headers [Hash{String => String}]
+ # @!macro common_options
+ # @api public
+ def put(path, data, headers = nil, options = {})
+ request = Net::HTTP::Put.new(path, headers)
+ request.body = data
+ request_with_redirects(request, options)
end
def request(method, *args)
- current_args = args.dup
- @redirect_limit.times do |redirection|
- response = execute_request(method, *args)
- return response unless [301, 302, 307].include?(response.code.to_i)
-
- # handle the redirection
- location = URI.parse(response['location'])
- @connection = initialize_connection(location.host, location.port, location.scheme == 'https')
-
- # update to the current request path
- current_args = [location.path] + current_args.drop(1)
- # and try again...
- end
- raise RedirectionLimitExceededException, "Too many HTTP redirections for #{@host}:#{@port}"
+ self.send(method, *args)
end
# TODO: These are proxies for the Net::HTTP#request_* methods, which are
# almost the same as the "get", "post", etc. methods that we've ported above,
# but they are able to accept a code block and will yield to it. For now
# we're not funneling these proxy implementations through our #request
# method above, so they will not inherit the same error handling. In the
# future we may want to refactor these so that they are funneled through
# that method and do inherit the error handling.
def request_get(*args, &block)
connection.request_get(*args, &block)
end
def request_head(*args, &block)
connection.request_head(*args, &block)
end
def request_post(*args, &block)
connection.request_post(*args, &block)
end
# end of Net::HTTP#request_* proxies
def address
connection.address
end
def port
connection.port
end
def use_ssl?
connection.use_ssl?
end
private
+ def request_with_redirects(request, options)
+ current_request = request
+ @redirect_limit.times do |redirection|
+ apply_options_to(current_request, options)
+
+ response = execute_request(current_request)
+ return response unless [301, 302, 307].include?(response.code.to_i)
+
+ # handle the redirection
+ location = URI.parse(response['location'])
+ @connection = initialize_connection(location.host, location.port, location.scheme == 'https')
+
+ # update to the current request path
+ current_request = current_request.class.new(location.path)
+ current_request.body = request.body
+ request.each do |header, value|
+ current_request[header] = value
+ end
+
+ # and try again...
+ end
+ raise RedirectionLimitExceededException, "Too many HTTP redirections for #{@host}:#{@port}"
+ end
+
+ def apply_options_to(request, options)
+ if options[:basic_auth]
+ request.basic_auth(options[:basic_auth][:user], options[:basic_auth][:password])
+ end
+ end
+
def connection
@connection || initialize_connection(@host, @port, @use_ssl)
end
- def execute_request(method, *args)
- response = connection.send(method, *args)
+ def execute_request(request)
+ response = connection.request(request)
# Check the peer certs and warn if they're nearing expiration.
warn_if_near_expiration(*@verify.peer_certs)
response
rescue OpenSSL::SSL::SSLError => error
if error.message.include? "certificate verify failed"
msg = error.message
msg << ": [" + @verify.verify_errors.join('; ') + "]"
- raise Puppet::Error, msg
- elsif error.message =~ /hostname (\w+ )?not match/
+ raise Puppet::Error, msg, error.backtrace
+ elsif error.message =~ /hostname.*not match.*server certificate/
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
+ msg = "Server hostname '#{connection.address}' did not match server certificate; expected #{msg}"
- raise Puppet::Error, "Server hostname '#{connection.address}' did not match server certificate; expected #{msg}"
+ raise Puppet::Error, msg, error.backtrace
else
raise
end
end
def initialize_connection(host, port, use_ssl)
args = [host, port]
if Puppet[:http_proxy_host] == "none"
args << nil << nil
else
args << Puppet[:http_proxy_host] << Puppet[:http_proxy_port]
end
@connection = create_connection(*args)
# Pop open the http client a little; older versions of Net::HTTP(s) didn't
# give us a reader for ca_file... Grr...
class << @connection; attr_accessor :ca_file; end
@connection.use_ssl = use_ssl
# Use configured timeout (#1176)
@connection.read_timeout = Puppet[:configtimeout]
@connection.open_timeout = Puppet[:configtimeout]
cert_setup
@connection
end
# Use cert information from a Puppet client to set up the http object.
def cert_setup
# PUP-1411, make sure that openssl is initialized before we try to connect
if ! @@openssl_initialized
OpenSSL::SSL::SSLContext.new
@@openssl_initialized = true
end
@verify.setup_connection(@connection)
end
# This method largely exists for testing purposes, so that we can
# mock the actual HTTP connection.
def create_connection(*args)
Net::HTTP.new(*args)
end
end
end
diff --git a/lib/puppet/network/http/error.rb b/lib/puppet/network/http/error.rb
new file mode 100644
index 000000000..4e521d130
--- /dev/null
+++ b/lib/puppet/network/http/error.rb
@@ -0,0 +1,69 @@
+require 'json'
+
+module Puppet::Network::HTTP::Error
+ Issues = Puppet::Network::HTTP::Issues
+
+ class HTTPError < Exception
+ attr_reader :status, :issue_kind
+
+ def initialize(message, status, issue_kind)
+ super(message)
+ @status = status
+ @issue_kind = issue_kind
+ end
+
+ def to_json
+ JSON({:message => message, :issue_kind => @issue_kind})
+ end
+ end
+
+ class HTTPNotAcceptableError < HTTPError
+ CODE = 406
+ def initialize(message, issue_kind = Issues::RUNTIME_ERROR)
+ super("Not Acceptable: " + message, CODE, issue_kind)
+ end
+ end
+
+ class HTTPNotFoundError < HTTPError
+ CODE = 404
+ def initialize(message, issue_kind = Issues::RUNTIME_ERROR)
+ super("Not Found: " + message, CODE, issue_kind)
+ end
+ end
+
+ class HTTPNotAuthorizedError < HTTPError
+ CODE = 403
+ def initialize(message, issue_kind = Issues::RUNTIME_ERROR)
+ super("Not Authorized: " + message, CODE, issue_kind)
+ end
+ end
+
+ class HTTPBadRequestError < HTTPError
+ CODE = 400
+ def initialize(message, issue_kind = Issues::RUNTIME_ERROR)
+ super("Bad Request: " + message, CODE, issue_kind)
+ end
+ end
+
+ class HTTPMethodNotAllowedError < HTTPError
+ CODE = 405
+ def initialize(message, issue_kind = Issues::RUNTIME_ERROR)
+ super("Method Not Allowed: " + message, CODE, issue_kind)
+ end
+ end
+
+ class HTTPServerError < HTTPError
+ CODE = 500
+
+ attr_reader :backtrace
+
+ def initialize(original_error, issue_kind = Issues::RUNTIME_ERROR)
+ super("Server Error: " + original_error.message, CODE, issue_kind)
+ @backtrace = original_error.backtrace
+ end
+
+ def to_json
+ JSON({:message => message, :issue_kind => @issue_kind, :stacktrace => self.backtrace})
+ end
+ end
+end
diff --git a/lib/puppet/network/http/handler.rb b/lib/puppet/network/http/handler.rb
index 3b86195d0..82e873ea0 100644
--- a/lib/puppet/network/http/handler.rb
+++ b/lib/puppet/network/http/handler.rb
@@ -1,348 +1,180 @@
module Puppet::Network::HTTP
end
require 'puppet/network/http'
require 'puppet/network/http/api/v1'
-require 'puppet/network/authorization'
require 'puppet/network/authentication'
require 'puppet/network/rights'
require 'puppet/util/profiler'
require 'resolv'
module Puppet::Network::HTTP::Handler
- include Puppet::Network::HTTP::API::V1
- include Puppet::Network::Authorization
include Puppet::Network::Authentication
+ include Puppet::Network::HTTP::Issues
# These shouldn't be allowed to be set by clients
# in the query string, for security reasons.
DISALLOWED_KEYS = ["node", "ip"]
- class HTTPError < Exception
- attr_reader :status
-
- def initialize(message, status)
- super(message)
- @status = status
- end
- end
-
- class HTTPNotAcceptableError < HTTPError
- def initialize(message)
- super("Not Acceptable: " + message, 406)
+ def register(routes)
+ # There's got to be a simpler way to do this, right?
+ dupes = {}
+ routes.each { |r| dupes[r.path_matcher] = (dupes[r.path_matcher] || 0) + 1 }
+ dupes = dupes.collect { |pm, count| pm if count > 1 }.compact
+ if dupes.count > 0
+ raise ArgumentError, "Given multiple routes with identical path regexes: #{dupes.map{ |rgx| rgx.inspect }.join(', ')}"
end
- end
- class HTTPNotFoundError < HTTPError
- def initialize(message)
- super("Not Found: " + message, 404)
+ @routes = routes
+ Puppet.debug("Routes Registered:")
+ @routes.each do |route|
+ Puppet.debug(route.inspect)
end
end
- attr_reader :server, :handler
-
# Retrieve all headers from the http request, as a hash with the header names
# (lower-cased) as the keys
def headers(request)
raise NotImplementedError
end
- # Retrieve the accept header from the http request.
- def accept_header(request)
- raise NotImplementedError
- end
-
- # Retrieve the Content-Type header from the http request.
- def content_type_header(request)
- raise NotImplementedError
- end
-
- def request_format(request)
- if header = content_type_header(request)
- header.gsub!(/\s*;.*$/,'') # strip any charset
- format = Puppet::Network::FormatHandler.mime(header)
- raise "Client sent a mime-type (#{header}) that doesn't correspond to a format we support" if format.nil?
- report_if_deprecated(format)
- return format.name.to_s if format.suitable?
- end
-
- raise "No Content-Type header was received, it isn't possible to unserialize the request"
- end
-
def format_to_mime(format)
format.is_a?(Puppet::Network::Format) ? format.mime : format
end
- def initialize_for_puppet(server)
- @server = server
- end
-
# handle an HTTP request
def process(request, response)
+ new_response = Puppet::Network::HTTP::Response.new(self, response)
+
request_headers = headers(request)
request_params = params(request)
request_method = http_method(request)
request_path = path(request)
+ new_request = Puppet::Network::HTTP::Request.new(request_headers, request_params, request_method, request_path, request_path, client_cert(request), body(request))
+
response[Puppet::Network::HTTP::HEADER_PUPPET_VERSION] = Puppet.version
configure_profiler(request_headers, request_params)
+ warn_if_near_expiration(new_request.client_cert)
Puppet::Util::Profiler.profile("Processed request #{request_method} #{request_path}") do
- indirection_name, method, key, params = uri2indirection(request_method, request_path, request_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}"
+ if route = @routes.find { |route| route.matches?(new_request) }
+ route.process(new_request, new_response)
+ else
+ raise Puppet::Network::HTTP::Error::HTTPNotFoundError.new("No route for #{new_request.method} #{new_request.path}", HANDLER_NOT_FOUND)
end
-
- send("do_#{method}", indirection, key, params, request, response)
end
- rescue HTTPError => e
- return do_http_control_exception(response, e)
+
+ rescue Puppet::Network::HTTP::Error::HTTPError => e
+ Puppet.info(e.message)
+ new_response.respond_with(e.status, "application/json", e.to_json)
rescue Exception => e
- return do_exception(response, e)
+ http_e = Puppet::Network::HTTP::Error::HTTPServerError.new(e)
+ Puppet.err(http_e.message)
+ new_response.respond_with(http_e.status, "application/json", http_e.to_json)
ensure
cleanup(request)
end
# Set the response up, with the body and status.
def set_response(response, body, status = 200)
raise NotImplementedError
end
# Set the specified format as the content type of the response.
def set_content_type(response, format)
raise NotImplementedError
end
- def do_exception(response, exception, status=400)
- if exception.is_a?(Puppet::Network::AuthorizationError)
- # make sure we return the correct status code
- # for authorization issues
- status = 403 if status == 400
- end
-
- Puppet.log_exception(exception)
-
- set_content_type(response, "text/plain")
- set_response(response, exception.to_s, status)
- end
-
- # Execute our find.
- 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(indirection.model, request)
- set_content_type(response, format)
-
- rendered_result = result
- if result.respond_to?(:render)
- Puppet::Util::Profiler.profile("Rendered result in #{format}") do
- rendered_result = result.render(format)
- end
- end
-
- Puppet::Util::Profiler.profile("Sent response") do
- set_response(response, rendered_result)
- end
- end
-
- # Execute our head.
- 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
- # HEAD request. All we need to do is not die.
- end
-
- # Execute our search.
- 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}'"
- end
-
- format = accepted_response_formatter_for(indirection.model, request)
- set_content_type(response, format)
-
- set_response(response, indirection.model.render_multiple(format, result))
- end
-
- # Execute our destroy.
- def do_destroy(indirection, key, params, request, response)
- formatter = accepted_response_formatter_or_yaml_for(indirection.model, request)
-
- result = indirection.destroy(key, params)
-
- set_content_type(response, formatter)
- set_response(response, formatter.render(result))
- end
-
- # Execute our save.
- 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 = indirection.save(sent_object, key)
-
- set_content_type(response, formatter)
- set_response(response, formatter.render(result))
- end
-
# resolve node name from peer's ip address
# this is used when the request is unauthenticated
def resolve_node(result)
begin
return Resolv.getname(result[:ip])
rescue => detail
Puppet.err "Could not resolve #{result[:ip]}: #{detail}"
end
result[:ip]
end
private
- def do_http_control_exception(response, exception)
- msg = exception.message
- Puppet.info(msg)
- set_content_type(response, "text/plain")
- set_response(response, msg, exception.status)
- end
-
- def report_if_deprecated(format)
- if format.name == :yaml || format.name == :b64_zlib_yaml
- Puppet.deprecation_warning("YAML in network requests is deprecated and will be removed in a future version. See http://links.puppetlabs.com/deprecate_yaml_on_network")
- end
- end
-
- def accepted_response_formatter_for(model_class, request)
- accepted_formats = accept_header(request) or raise HTTPNotAcceptableError, "Missing required Accept header"
- response_formatter_for(model_class, request, accepted_formats)
- end
-
- def accepted_response_formatter_or_yaml_for(model_class, request)
- accepted_formats = accept_header(request) || "yaml"
- response_formatter_for(model_class, request, accepted_formats)
- end
-
- def response_formatter_for(model_class, request, accepted_formats)
- formatter = Puppet::Network::FormatHandler.most_suitable_format_for(
- accepted_formats.split(/\s*,\s*/),
- model_class.supported_formats)
-
- if formatter.nil?
- raise HTTPNotAcceptableError, "No supported formats are acceptable (Accept: #{accepted_formats})"
- end
-
- report_if_deprecated(formatter)
- formatter
- end
-
- def read_body_into_model(model_class, request)
- data = body(request).to_s
-
- format = request_format(request)
- model_class.convert_from(format, data)
- end
-
- def get?(request)
- http_method(request) == 'GET'
- end
-
- def put?(request)
- http_method(request) == 'PUT'
- end
-
- def delete?(request)
- http_method(request) == 'DELETE'
- end
-
# methods to be overridden by the including web server class
def http_method(request)
raise NotImplementedError
end
def path(request)
raise NotImplementedError
end
def request_key(request)
raise NotImplementedError
end
def body(request)
raise NotImplementedError
end
def params(request)
raise NotImplementedError
end
def client_cert(request)
raise NotImplementedError
end
def cleanup(request)
# By default, there is nothing to cleanup.
end
def decode_params(params)
params.select { |key, _| allowed_parameter?(key) }.inject({}) do |result, ary|
param, value = ary
result[param.to_sym] = parse_parameter_value(param, value)
result
end
end
def allowed_parameter?(name)
not (name.nil? || name.empty? || DISALLOWED_KEYS.include?(name))
end
def parse_parameter_value(param, value)
case value
when /^---/
Puppet.debug("Found YAML while processing request parameter #{param} (value: <#{value}>)")
Puppet.deprecation_warning("YAML in network requests is deprecated and will be removed in a future version. See http://links.puppetlabs.com/deprecate_yaml_on_network")
YAML.load(value, :safe => true, :deserialize_symbols => true)
when Array
value.collect { |v| parse_primitive_parameter_value(v) }
else
parse_primitive_parameter_value(value)
end
end
def parse_primitive_parameter_value(value)
case value
when "true"
true
when "false"
false
when /^\d+$/
Integer(value)
when /^\d+\.\d+$/
value.to_f
else
value
end
end
def configure_profiler(request_headers, request_params)
if (request_headers.has_key?(Puppet::Network::HTTP::HEADER_ENABLE_PROFILING.downcase) or Puppet[:profile])
Puppet::Util::Profiler.current = Puppet::Util::Profiler::WallClock.new(Puppet.method(:debug), request_params.object_id)
else
Puppet::Util::Profiler.current = Puppet::Util::Profiler::NONE
end
end
end
diff --git a/lib/puppet/network/http/issues.rb b/lib/puppet/network/http/issues.rb
new file mode 100644
index 000000000..9582675a8
--- /dev/null
+++ b/lib/puppet/network/http/issues.rb
@@ -0,0 +1,9 @@
+module Puppet::Network::HTTP::Issues
+ NO_INDIRECTION_REMOTE_REQUESTS = :NO_INDIRECTION_REMOTE_REQUESTS
+ HANDLER_NOT_FOUND = :HANDLER_NOT_FOUND
+ RESOURCE_NOT_FOUND = :RESOURCE_NOT_FOUND
+ RUNTIME_ERROR = :RUNTIME_ERROR
+ MISSING_HEADER_FIELD = :MISSING_HEADER_FIELD
+ UNSUPPORTED_FORMAT = :UNSUPPORTED_FORMAT
+ UNSUPPORTED_METHOD = :UNSUPPORTED_METHOD
+end
diff --git a/lib/puppet/network/http/memory_response.rb b/lib/puppet/network/http/memory_response.rb
new file mode 100644
index 000000000..8171cfbb7
--- /dev/null
+++ b/lib/puppet/network/http/memory_response.rb
@@ -0,0 +1,13 @@
+class Puppet::Network::HTTP::MemoryResponse
+ attr_reader :code, :type, :body
+
+ def initialize
+ @body = ""
+ end
+
+ def respond_with(code, type, body)
+ @code = code
+ @type = type
+ @body += body
+ end
+end
diff --git a/lib/puppet/network/http/rack/httphandler.rb b/lib/puppet/network/http/rack/httphandler.rb
deleted file mode 100644
index e4fa93aa5..000000000
--- a/lib/puppet/network/http/rack/httphandler.rb
+++ /dev/null
@@ -1,13 +0,0 @@
-require 'openssl'
-require 'puppet/ssl/certificate'
-
-class Puppet::Network::HTTP::RackHttpHandler
-
- # do something useful with request (a Rack::Request) and use
- # response to fill your Rack::Response
- def process(request, response)
- raise NotImplementedError, "Your RackHttpHandler subclass is supposed to override service(request)"
- end
-
-end
-
diff --git a/lib/puppet/network/http/rack/rest.rb b/lib/puppet/network/http/rack/rest.rb
index 83d61866b..23d73bf93 100644
--- a/lib/puppet/network/http/rack/rest.rb
+++ b/lib/puppet/network/http/rack/rest.rb
@@ -1,144 +1,136 @@
require 'openssl'
require 'cgi'
require 'puppet/network/http/handler'
-require 'puppet/network/http/rack/httphandler'
require 'puppet/util/ssl'
-class Puppet::Network::HTTP::RackREST < Puppet::Network::HTTP::RackHttpHandler
-
+class Puppet::Network::HTTP::RackREST
include Puppet::Network::HTTP::Handler
- HEADER_ACCEPT = 'HTTP_ACCEPT'.freeze
ContentType = 'Content-Type'.freeze
CHUNK_SIZE = 8192
class RackFile
def initialize(file)
@file = file
end
def each
while chunk = @file.read(CHUNK_SIZE)
yield chunk
end
end
def close
@file.close
end
end
def initialize(args={})
super()
- initialize_for_puppet(args)
+ register([Puppet::Network::HTTP::API::V2.routes, Puppet::Network::HTTP::API::V1.routes])
end
def set_content_type(response, format)
response[ContentType] = format_to_mime(format)
end
# produce the body of the response
def set_response(response, result, status = 200)
response.status = status
unless result.is_a?(File)
response.write result
else
response["Content-Length"] = result.stat.size.to_s
response.body = RackFile.new(result)
end
end
# Retrieve all headers from the http request, as a map.
def headers(request)
- request.env.select {|k,v| k.start_with? 'HTTP_'}.inject({}) do |m, (k,v)|
+ headers = request.env.select {|k,v| k.start_with? 'HTTP_'}.inject({}) do |m, (k,v)|
m[k.sub(/^HTTP_/, '').gsub('_','-').downcase] = v
m
end
- end
-
- # Retrieve the accept header from the http request.
- def accept_header(request)
- request.env[HEADER_ACCEPT]
- end
-
- # Retrieve the accept header from the http request.
- def content_type_header(request)
- request.content_type
+ headers['content-type'] = request.content_type
+ headers
end
# Return which HTTP verb was used in this request.
def http_method(request)
request.request_method
end
# Return the query params for this request.
def params(request)
if request.post?
params = request.params
else
# rack doesn't support multi-valued query parameters,
# e.g. ignore, so parse them ourselves
params = CGI.parse(request.query_string)
convert_singular_arrays_to_value(params)
end
result = decode_params(params)
result.merge(extract_client_info(request))
end
# what path was requested? (this is, without any query parameters)
def path(request)
request.path
end
# return the request body
def body(request)
request.body.read
end
def client_cert(request)
# This environment variable is set by mod_ssl, note that it
# requires the `+ExportCertData` option in the `SSLOptions` directive
cert = request.env['SSL_CLIENT_CERT']
# NOTE: The SSL_CLIENT_CERT environment variable will be the empty string
# when Puppet agent nodes have not yet obtained a signed certificate.
- return nil if cert.nil? or cert.empty?
- OpenSSL::X509::Certificate.new(cert)
+ if cert.nil? || cert.empty?
+ nil
+ else
+ Puppet::SSL::Certificate.from_instance(OpenSSL::X509::Certificate.new(cert))
+ end
end
# Passenger freaks out if we finish handling the request without reading any
# part of the body, so make sure we have.
def cleanup(request)
request.body.read(1)
nil
end
def extract_client_info(request)
result = {}
result[:ip] = request.ip
# if we find SSL info in the headers, use them to get a hostname from the CN.
# try this with :ssl_client_header, which defaults should work for
# Apache with StdEnvVars.
subj_str = request.env[Puppet[:ssl_client_header]]
subject = Puppet::Util::SSL.subject_from_dn(subj_str || "")
if cn = Puppet::Util::SSL.cn_from_subject(subject)
result[:node] = cn
result[:authenticated] = (request.env[Puppet[:ssl_client_verify_header]] == 'SUCCESS')
else
result[:node] = resolve_node(result)
result[:authenticated] = false
end
result
end
def convert_singular_arrays_to_value(hash)
hash.each do |key, value|
if value.size == 1
hash[key] = value.first
end
end
end
end
diff --git a/lib/puppet/network/http/request.rb b/lib/puppet/network/http/request.rb
new file mode 100644
index 000000000..c17673b72
--- /dev/null
+++ b/lib/puppet/network/http/request.rb
@@ -0,0 +1,56 @@
+Puppet::Network::HTTP::Request = Struct.new(:headers, :params, :method, :path, :routing_path, :client_cert, :body) do
+ def self.from_hash(hash)
+ symbol_members = members.collect(&:intern)
+ unknown = hash.keys - symbol_members
+ if unknown.empty?
+ new(hash[:headers] || {},
+ hash[:params] || {},
+ hash[:method] || "GET",
+ hash[:path],
+ hash[:routing_path] || hash[:path],
+ hash[:client_cert],
+ hash[:body])
+ else
+ raise ArgumentError, "Unknown arguments: #{unknown.collect(&:inspect).join(', ')}"
+ end
+ end
+
+ def route_into(prefix)
+ self.class.new(headers, params, method, path, routing_path.sub(prefix, ''), client_cert, body)
+ end
+
+ def format
+ if header = headers['content-type']
+ header.gsub!(/\s*;.*$/,'') # strip any charset
+ format = Puppet::Network::FormatHandler.mime(header)
+ if format.nil?
+ raise "Client sent a mime-type (#{header}) that doesn't correspond to a format we support"
+ else
+ report_if_deprecated(format)
+ return format.name.to_s if format.suitable?
+ end
+ end
+
+ raise "No Content-Type header was received, it isn't possible to unserialize the request"
+ end
+
+ def response_formatter_for(supported_formats, accepted_formats = headers['accept'])
+ formatter = Puppet::Network::FormatHandler.most_suitable_format_for(
+ accepted_formats.split(/\s*,\s*/),
+ supported_formats)
+
+ if formatter.nil?
+ raise Puppet::Network::HTTP::Error::HTTPNotAcceptableError.new("No supported formats are acceptable (Accept: #{accepted_formats})", Puppet::Network::HTTP::Issues::UNSUPPORTED_FORMAT)
+ end
+
+ report_if_deprecated(formatter)
+
+ formatter
+ end
+
+ def report_if_deprecated(format)
+ if format.name == :yaml || format.name == :b64_zlib_yaml
+ Puppet.deprecation_warning("YAML in network requests is deprecated and will be removed in a future version. See http://links.puppetlabs.com/deprecate_yaml_on_network")
+ end
+ end
+end
diff --git a/lib/puppet/network/http/response.rb b/lib/puppet/network/http/response.rb
new file mode 100644
index 000000000..f73399d4b
--- /dev/null
+++ b/lib/puppet/network/http/response.rb
@@ -0,0 +1,11 @@
+class Puppet::Network::HTTP::Response
+ def initialize(handler, response)
+ @handler = handler
+ @response = response
+ end
+
+ def respond_with(code, type, body)
+ @handler.set_content_type(@response, type)
+ @handler.set_response(@response, body, code)
+ end
+end
diff --git a/lib/puppet/network/http/route.rb b/lib/puppet/network/http/route.rb
new file mode 100644
index 000000000..e73d5b593
--- /dev/null
+++ b/lib/puppet/network/http/route.rb
@@ -0,0 +1,91 @@
+class Puppet::Network::HTTP::Route
+ MethodNotAllowedHandler = lambda do |req, res|
+ raise Puppet::Network::HTTP::Error::HTTPMethodNotAllowedError.new("method #{req.method} not allowed for route #{req.path}", Puppet::Network::HTTP::Issues::UNSUPPORTED_METHOD)
+ end
+
+ attr_reader :path_matcher
+
+ def self.path(path_matcher)
+ new(path_matcher)
+ end
+
+ def initialize(path_matcher)
+ @path_matcher = path_matcher
+ @method_handlers = {
+ :GET => [MethodNotAllowedHandler],
+ :HEAD => [MethodNotAllowedHandler],
+ :OPTIONS => [MethodNotAllowedHandler],
+ :POST => [MethodNotAllowedHandler],
+ :PUT => [MethodNotAllowedHandler]
+ }
+ @chained = []
+ end
+
+ def get(*handlers)
+ @method_handlers[:GET] = handlers
+ return self
+ end
+
+ def head(*handlers)
+ @method_handlers[:HEAD] = handlers
+ return self
+ end
+
+ def options(*handlers)
+ @method_handlers[:OPTIONS] = handlers
+ return self
+ end
+
+ def post(*handlers)
+ @method_handlers[:POST] = handlers
+ return self
+ end
+
+ def put(*handlers)
+ @method_handlers[:PUT] = handlers
+ return self
+ end
+
+ def any(*handlers)
+ @method_handlers.each do |method, registered_handlers|
+ @method_handlers[method] = handlers
+ end
+ return self
+ end
+
+ def chain(*routes)
+ @chained = routes
+ self
+ end
+
+ def matches?(request)
+ Puppet.debug("Evaluating match for #{self.inspect}")
+ if match(request.routing_path)
+ return true
+ else
+ Puppet.debug("Did not match path (#{request.routing_path.inspect})")
+ end
+ return false
+ end
+
+ def process(request, response)
+ @method_handlers[request.method.upcase.intern].each do |handler|
+ handler.call(request, response)
+ end
+
+ subrequest = request.route_into(match(request.routing_path).to_s)
+ if chained_route = @chained.find { |route| route.matches?(subrequest) }
+ chained_route.process(subrequest, response)
+ end
+ end
+
+ def inspect
+ "Route #{@path_matcher.inspect}"
+ end
+
+ private
+
+ def match(path)
+ @path_matcher.match(path)
+ end
+end
diff --git a/lib/puppet/network/http/webrick.rb b/lib/puppet/network/http/webrick.rb
index f9e0b26db..820d4556c 100644
--- a/lib/puppet/network/http/webrick.rb
+++ b/lib/puppet/network/http/webrick.rb
@@ -1,119 +1,119 @@
require 'webrick'
require 'webrick/https'
require 'puppet/network/http/webrick/rest'
require 'thread'
require 'puppet/ssl/certificate'
require 'puppet/ssl/certificate_revocation_list'
require 'puppet/ssl/configuration'
class Puppet::Network::HTTP::WEBrick
def initialize
@listening = false
end
def listen(address, port)
arguments = {:BindAddress => address, :Port => port, :DoNotReverseLookup => true}
arguments.merge!(setup_logger)
arguments.merge!(setup_ssl)
BasicSocket.do_not_reverse_lookup = true
@server = WEBrick::HTTPServer.new(arguments)
@server.listeners.each { |l| l.start_immediately = false }
- @server.mount('/', Puppet::Network::HTTP::WEBrickREST, :this_value_is_apparently_necessary_but_unused)
+ @server.mount('/', Puppet::Network::HTTP::WEBrickREST)
raise "WEBrick server is already listening" if @listening
@listening = true
@thread = Thread.new do
@server.start do |sock|
timeout = 10.0
if ! IO.select([sock],nil,nil,timeout)
raise "Client did not send data within %.1f seconds of connecting" % timeout
end
sock.accept
@server.run(sock)
end
end
sleep 0.1 until @server.status == :Running
end
def unlisten
raise "WEBrick server is not listening" unless @listening
@server.shutdown
wait_for_shutdown
@server = nil
@listening = false
end
def listening?
@listening
end
def wait_for_shutdown
@thread.join
end
# Configure our http log file.
def setup_logger
# Make sure the settings are all ready for us.
Puppet.settings.use(:main, :ssl, :application)
if Puppet.run_mode.master?
file = Puppet[:masterhttplog]
else
file = Puppet[:httplog]
end
# open the log manually to prevent file descriptor leak
file_io = ::File.open(file, "a+")
file_io.sync = true
if defined?(Fcntl::FD_CLOEXEC)
file_io.fcntl(Fcntl::F_SETFD, Fcntl::FD_CLOEXEC)
end
args = [file_io]
args << WEBrick::Log::DEBUG if Puppet::Util::Log.level == :debug
logger = WEBrick::Log.new(*args)
return :Logger => logger, :AccessLog => [
[logger, WEBrick::AccessLog::COMMON_LOG_FORMAT ],
[logger, WEBrick::AccessLog::REFERER_LOG_FORMAT ]
]
end
# Add all of the ssl cert information.
def setup_ssl
results = {}
# Get the cached copy. We know it's been generated, too.
host = Puppet::SSL::Host.localhost
raise Puppet::Error, "Could not retrieve certificate for #{host.name} and not running on a valid certificate authority" unless host.certificate
results[:SSLPrivateKey] = host.key.content
results[:SSLCertificate] = host.certificate.content
results[:SSLStartImmediately] = true
results[:SSLEnable] = true
results[:SSLOptions] = OpenSSL::SSL::OP_NO_SSLv2
raise Puppet::Error, "Could not find CA certificate" unless Puppet::SSL::Certificate.indirection.find(Puppet::SSL::CA_NAME)
results[:SSLCACertificateFile] = ssl_configuration.ca_auth_file
results[:SSLVerifyClient] = OpenSSL::SSL::VERIFY_PEER
results[:SSLCertificateStore] = host.ssl_store
results
end
private
def ssl_configuration
@ssl_configuration ||= Puppet::SSL::Configuration.new(
Puppet[:localcacert],
:ca_chain_file => Puppet[:ssl_server_ca_chain],
:ca_auth_file => Puppet[:ssl_server_ca_auth])
end
end
diff --git a/lib/puppet/network/http/webrick/rest.rb b/lib/puppet/network/http/webrick/rest.rb
index 0ed3db36c..66987151a 100644
--- a/lib/puppet/network/http/webrick/rest.rb
+++ b/lib/puppet/network/http/webrick/rest.rb
@@ -1,100 +1,95 @@
require 'puppet/network/http/handler'
require 'resolv'
require 'webrick'
require 'puppet/util/ssl'
class Puppet::Network::HTTP::WEBrickREST < WEBrick::HTTPServlet::AbstractServlet
include Puppet::Network::HTTP::Handler
- def initialize(server, handler)
+ def initialize(server)
raise ArgumentError, "server is required" unless server
+ register([Puppet::Network::HTTP::API::V2.routes, Puppet::Network::HTTP::API::V1.routes])
super(server)
- initialize_for_puppet(:server => server, :handler => handler)
end
# Retrieve the request parameters, including authentication information.
def params(request)
params = request.query || {}
params = Hash[params.collect do |key, value|
all_values = value.list
[key, all_values.length == 1 ? value : all_values]
end]
params = decode_params(params)
params.merge(client_information(request))
end
# WEBrick uses a service method to respond to requests. Simply delegate to the handler response method.
def service(request, response)
process(request, response)
end
def headers(request)
result = {}
request.each do |k, v|
result[k.downcase] = v
end
result
end
- def accept_header(request)
- request["accept"]
- end
-
- def content_type_header(request)
- request["content-type"]
- end
-
def http_method(request)
request.request_method
end
def path(request)
request.path
end
def body(request)
request.body
end
def client_cert(request)
- request.client_cert
+ if cert = request.client_cert
+ Puppet::SSL::Certificate.from_instance(cert)
+ else
+ nil
+ end
end
# Set the specified format as the content type of the response.
def set_content_type(response, format)
response["content-type"] = format_to_mime(format)
end
def set_response(response, result, status = 200)
response.status = status
if status >= 200 and status != 304
response.body = result
response["content-length"] = result.stat.size if result.is_a?(File)
end
- response.reason_phrase = result if status < 200 or status >= 300
end
# Retrieve node/cert/ip information from the request object.
def client_information(request)
result = {}
if peer = request.peeraddr and ip = peer[3]
result[:ip] = ip
end
# If they have a certificate (which will almost always be true)
# then we get the hostname from the cert, instead of via IP
# info
result[:authenticated] = false
if cert = request.client_cert and cn = Puppet::Util::SSL.cn_from_subject(cert.subject)
result[:node] = cn
result[:authenticated] = true
else
result[:node] = resolve_node(result)
end
result
end
end
diff --git a/lib/puppet/network/http_pool.rb b/lib/puppet/network/http_pool.rb
index 97094c9ad..67ef7faef 100644
--- a/lib/puppet/network/http_pool.rb
+++ b/lib/puppet/network/http_pool.rb
@@ -1,53 +1,53 @@
require 'puppet/network/http/connection'
module Puppet::Network; end
# This module contains the factory methods that should be used for getting a
-# Puppet::Network::HTTP::Connection instance.
+# {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.
+# @note 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 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)
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..d7275babf 100644
--- a/lib/puppet/network/rights.rb
+++ b/lib/puppet/network/rights.rb
@@ -1,219 +1,219 @@
require 'puppet/network/authstore'
require 'puppet/error'
module Puppet::Network
# this exception is thrown when a request is not authenticated
class AuthorizationError < Puppet::Error; end
# Rights class manages a list of ACLs for paths.
class Rights
# Check that name is allowed or not
def allowed?(name, *args)
!is_forbidden_and_why?(name, :node => args[0], :ip => args[1])
end
- def is_request_forbidden_and_why?(indirection, method, key, params)
+ def is_request_forbidden_and_why?(method, path, params)
methods_to_check = if method == :head
# :head is ok if either :find or :save is ok.
[:find, :save]
else
[method]
end
authorization_failure_exceptions = methods_to_check.map do |method|
- is_forbidden_and_why?("/#{indirection}/#{key}", params.merge({:method => method}))
+ is_forbidden_and_why?(path, params.merge({:method => method}))
end
if authorization_failure_exceptions.include? nil
# One of the methods we checked is ok, therefore this request is ok.
nil
else
# Just need to return any of the failure exceptions.
authorization_failure_exceptions.first
end
end
def is_forbidden_and_why?(name, args = {})
res = :nomatch
right = @rights.find do |acl|
found = false
# an acl can return :dunno, which means "I'm not qualified to answer your question,
# please ask someone else". This is used when for instance an acl matches, but not for the
# current rest method, where we might think some other acl might be more specific.
if match = acl.match?(name)
args[:match] = match
if (res = acl.allowed?(args[:node], args[:ip], args)) != :dunno
# return early if we're allowed
return nil if res
# we matched, select this acl
found = true
end
end
found
end
# if we end up here, then that means we either didn't match or failed, in any
# case will return an error to the outside world
host_description = args[:node] ? "#{args[:node]}(#{args[:ip]})" : args[:ip]
msg = "#{host_description} access to #{name} [#{args[:method]}]"
if args[:authenticated]
msg += " authenticated "
end
if right
msg += " at #{right.file}:#{right.line}"
end
AuthorizationError.new("Forbidden request: #{msg}")
end
def initialize
@rights = []
end
def [](name)
@rights.find { |acl| acl == name }
end
def empty?
@rights.empty?
end
def include?(name)
@rights.include?(name)
end
def each
@rights.each { |r| yield r.name,r }
end
# Define a new right to which access can be provided.
def newright(name, line=nil, file=nil)
add_right( Right.new(name, line, file) )
end
private
def add_right(right)
@rights << right
right
end
# Retrieve a right by name.
def right(name)
self[name]
end
# A right.
class Right < Puppet::Network::AuthStore
attr_accessor :name, :key
# Overriding Object#methods sucks for debugging. If we're in here in the
# future, it would be nice to rename Right#methods
attr_accessor :methods, :environment, :authentication
attr_accessor :line, :file
ALL = [:save, :destroy, :find, :search]
Puppet::Util.logmethods(self, true)
def initialize(name, line, file)
@methods = []
@environment = []
@authentication = true # defaults to authenticated
@name = name
@line = line || 0
@file = file
@methods = ALL
case name
when /^\//
@key = Regexp.new("^" + Regexp.escape(name))
when /^~/ # this is a regex
@name = name.gsub(/^~\s+/,'')
@key = Regexp.new(@name)
else
raise ArgumentError, "Unknown right type '#{name}'"
end
super()
end
def to_s
"access[#{@name}]"
end
# There's no real check to do at this point
def valid?
true
end
# does this right is allowed for this triplet?
# if this right is too restrictive (ie we don't match this access method)
# then return :dunno so that upper layers have a chance to try another right
# tailored to the given method
def allowed?(name, ip, args = {})
if not @methods.include?(args[:method])
return :dunno
elsif @environment.size > 0 and not @environment.include?(args[:environment])
return :dunno
elsif (@authentication and not args[:authenticated])
return :dunno
end
begin
# make sure any capture are replaced if needed
interpolate(args[:match]) if args[:match]
res = super(name,ip)
ensure
reset_interpolation
end
res
end
# restrict this right to some method only
def restrict_method(m)
m = m.intern if m.is_a?(String)
raise ArgumentError, "'#{m}' is not an allowed value for method directive" unless ALL.include?(m)
# if we were allowing all methods, then starts from scratch
if @methods === ALL
@methods = []
end
raise ArgumentError, "'#{m}' is already in the '#{name}' ACL" if @methods.include?(m)
@methods << m
end
- def restrict_environment(env)
- env = Puppet::Node::Environment.new(env)
+ def restrict_environment(environment)
+ env = Puppet.lookup(:environments).get(environment)
raise ArgumentError, "'#{env}' is already in the '#{name}' ACL" if @environment.include?(env)
@environment << env
end
def restrict_authenticated(authentication)
case authentication
when "yes", "on", "true", true
authentication = true
when "no", "off", "false", false, "all" ,"any", :all, :any
authentication = false
else
raise ArgumentError, "'#{name}' incorrect authenticated value: #{authentication}"
end
@authentication = authentication
end
def match?(key)
# otherwise match with the regex
self.key.match(key)
end
def ==(name)
self.name == name.gsub(/^~\s+/,'')
end
end
end
end
diff --git a/lib/puppet/node.rb b/lib/puppet/node.rb
index 09b307927..fbb57b2dd 100644
--- a/lib/puppet/node.rb
+++ b/lib/puppet/node.rb
@@ -1,162 +1,171 @@
require 'puppet/indirector'
# A class for managing nodes, including their facts and environment.
class Puppet::Node
require 'puppet/node/facts'
require 'puppet/node/environment'
# Set up indirection, so that nodes can be looked for in
# the node sources.
extend Puppet::Indirector
- # Adds the environment getter and setter, with some instance/string conversion
- include Puppet::Node::Environment::Helper
-
# Use the node source as the indirection terminus.
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, :trusted_data
attr_reader :time, :facts
::PSON.register_document_type('Node',self)
- def self.from_pson(pson)
- raise ArgumentError, "No name provided in serialized data" unless name = pson['name']
+ def self.from_data_hash(data)
+ raise ArgumentError, "No name provided in serialized data" unless name = data['name']
node = new(name)
- node.classes = pson['classes']
- node.parameters = pson['parameters']
- node.environment = pson['environment']
+ node.classes = data['classes']
+ node.parameters = data['parameters']
+ node.environment = data['environment']
node
end
+ def self.from_pson(pson)
+ Puppet.deprecation_warning("from_pson is being removed in favour of from_data_hash.")
+ self.from_data_hash(pson)
+ end
+
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' => to_data_hash,
}
end
def to_pson(*args)
to_pson_data_hash.to_pson(*args)
end
def environment
- return super if @environment
-
- if env = parameters["environment"]
+ if @environment
+ @environment
+ elsif env = parameters["environment"]
self.environment = env
- return super
+ @environment
+ else
+ Puppet.lookup(:environments).get(Puppet[:environment])
end
+ end
- # Else, return the default
- Puppet::Node::Environment.new
+ def environment=(env)
+ if env.is_a?(String) or env.is_a?(Symbol)
+ @environment = Puppet.lookup(:environments).get(env)
+ else
+ @environment = env
+ end
end
def initialize(name, options = {})
raise ArgumentError, "Node names cannot be nil" unless name
@name = name
if classes = options[:classes]
if classes.is_a?(String)
@classes = [classes]
else
@classes = classes
end
else
@classes = []
end
@parameters = options[:parameters] || {}
@facts = options[:facts]
if env = options[:environment]
self.environment = env
end
@time = Time.now
end
# 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
error = Puppet::Error.new("Could not retrieve facts for #{name}: #{detail}")
error.set_backtrace(detail.backtrace)
raise error
end
# Merge any random parameters into our parameter list.
def merge(params)
params.each do |name, value|
@parameters[name] = value unless @parameters.include?(name)
end
@parameters["environment"] ||= self.environment.name.to_s
end
# Calculate the list of names we might use for looking
# up our node. This is only used for AST nodes.
def names
return [name] if Puppet.settings[:strict_hostname_checking]
names = []
names += split_name(name) if name.include?(".")
# First, get the fqdn
unless fqdn = parameters["fqdn"]
if parameters["hostname"] and parameters["domain"]
fqdn = parameters["hostname"] + "." + parameters["domain"]
else
Puppet.warning "Host is missing hostname and/or domain: #{name}"
end
end
# Now that we (might) have the fqdn, add each piece to the name
# list to search, in order of longest to shortest.
names += split_name(fqdn) if fqdn
# And make sure the node name is first, since that's the most
# likely usage.
# The name is usually the Certificate CN, but it can be
# set to the 'facter' hostname instead.
if Puppet[:node_name] == 'cert'
names.unshift name
else
names.unshift parameters["hostname"]
end
names.uniq
end
def split_name(name)
list = name.split(".")
tmp = []
list.each_with_index do |short, i|
tmp << list[0..i].join(".")
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 666219376..84a046922 100644
--- a/lib/puppet/node/environment.rb
+++ b/lib/puppet/node/environment.rb
@@ -1,468 +1,492 @@
require 'puppet/util'
require 'puppet/util/cacher'
require 'monitor'
require 'puppet/parser/parser_factory'
# Just define it, so this class has fewer load dependencies.
class Puppet::Node
end
# Puppet::Node::Environment acts as a container for all configuration
# that is expected to vary between environments.
#
-# ## Global variables
-#
-# The Puppet::Node::Environment uses a number of global variables.
-#
-# ### `$environment`
-#
-# The 'environment' global variable represents the current environment that's
-# being used in the compiler.
-#
# ## The root environment
#
# In addition to normal environments that are defined by the user,there is a
# special 'root' environment. It is defined as an instance variable on the
# Puppet::Node::Environment metaclass. The environment name is `*root*` and can
-# be accessed by calling {Puppet::Node::Environment.root}.
+# be accessed by looking up the `:root_environment` using {Puppet.lookup}.
#
# The primary purpose of the root environment is to contain parser functions
# that are not bound to a specific environment. The main case for this is for
# logging functions. Logging functions are attached to the 'root' environment
# when {Puppet::Parser::Functions.reset} is called.
-#
-# The root environment is also used as a fallback environment when the
-# current environment has been requested by {Puppet::Node::Environment.current}
-# requested and no environment was set by {Puppet::Node::Environment.current=}
class Puppet::Node::Environment
- # This defines a mixin for classes that have an environment. It implements
- # `environment` and `environment=` that respects the semantics of the
- # Puppet::Node::Environment class
- #
- # @api public
- module Helper
-
- def environment
- Puppet::Node::Environment.new(@environment)
- end
-
- def environment=(env)
- if env.is_a?(String) or env.is_a?(Symbol)
- @environment = env
- else
- @environment = env.name
- end
- end
- end
-
include Puppet::Util::Cacher
- # @!attribute seen
- # @scope class
- # @api private
- # @return [Hash<Symbol, Puppet::Node::Environment>] All memoized environments
- @seen = {}
+ # @api private
+ def self.seen
+ @seen ||= {}
+ end
# Create a new environment with the given name, or return an existing one
#
# The environment class memoizes instances so that attempts to instantiate an
# environment with the same name with an existing environment will return the
# existing environment.
#
# @overload self.new(environment)
# @param environment [Puppet::Node::Environment]
# @return [Puppet::Node::Environment] the environment passed as the param,
# this is implemented so that a calling class can use strings or
# environments interchangeably.
#
# @overload self.new(string)
# @param string [String, Symbol]
# @return [Puppet::Node::Environment] An existing environment if it exists,
# else a new environment with that name
#
# @overload self.new()
# @return [Puppet::Node::Environment] The environment as set by
# Puppet.settings[:environment]
#
# @api public
def self.new(name = nil)
return name if name.is_a?(self)
name ||= Puppet.settings.value(:environment)
raise ArgumentError, "Environment name must be specified" unless name
symbol = name.to_sym
- return @seen[symbol] if @seen[symbol]
+ return seen[symbol] if seen[symbol]
- obj = self.allocate
- obj.send :initialize, symbol
- @seen[symbol] = obj
+ obj = self.create(symbol,
+ split_path(Puppet.settings.value(:modulepath, symbol)),
+ Puppet.settings.value(:manifest, symbol))
+ seen[symbol] = obj
end
- # Retrieve the environment for the current thread
- #
- # @note This should only used when a catalog is being compiled.
+ # Create a new environment with the given name
#
- # @api private
+ # @param name [Symbol] the name of the
+ # @param modulepath [Array<String>] the list of paths from which to load modules
+ # @param manifest [String] the path to the manifest for the environment
+ # @return [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
- $environment || root
+ # @api public
+ def self.create(name, modulepath, manifest)
+ obj = self.allocate
+ obj.send(:initialize,
+ name,
+ expand_dirs(extralibs() + modulepath),
+ File.expand_path(manifest))
+ obj
end
- # Set the environment for the current thread
- #
- # @note This should only set when a catalog is being compiled. Under normal
- # This value is initially set in {Puppet::Parser::Compiler#environment}
+ # Instantiate a new environment
#
- # @note Setting this affects global state during catalog compilation, and
- # changing the current environment during compilation can cause unexpected
- # and generally very bad behaviors.
+ # @note {Puppet::Node::Environment.new} is overridden to return memoized
+ # objects, so this will not be invoked with the normal Ruby initialization
+ # semantics.
#
- # @api private
+ # @param name [Symbol] The environment name
+ def initialize(name, modulepath, manifest)
+ @name = name
+ @modulepath = modulepath
+ @manifest = manifest
+ end
+
+ # Creates a new Puppet::Node::Environment instance, overriding any of the passed
+ # parameters.
#
- # @param env [Puppet::Node::Environment]
- def self.current=(env)
- $environment = new(env)
+ # @param env_params [Hash<{Symbol => String,Array<String>}>] new environment
+ # parameters (:modulepath, :manifest)
+ # @return [Puppet::Node::Environment]
+ def override_with(env_params)
+ return self.class.create(name,
+ env_params[:modulepath] || modulepath,
+ env_params[:manifest] || manifest)
end
+ # Creates a new Puppet::Node::Environment instance, overriding manfiest
+ # and modulepath from the passed settings if they were originally set from
+ # the commandline, or returns self if there is nothing to override.
+ #
+ # @param settings [Puppet::Settings] an initialized puppet settings instance
+ # @return [Puppet::Node::Environment] new overridden environment or self if
+ # there are no commandline changes from settings.
+ def override_from_commandline(settings)
+ overrides = {}
+ overrides[:modulepath] = self.class.split_path(settings[:modulepath]) if settings.set_by_cli?(:modulepath)
+ if settings.set_by_cli?(:manifest) ||
+ (settings.set_by_cli?(:manifestdir) && settings[:manifest].start_with?(settings[:manifestdir]))
+ overrides[:manifest] = settings[:manifest]
+ end
+ overrides.empty? ?
+ self :
+ self.override_with(overrides)
+ end
- # @return [Puppet::Node::Environment] The `*root*` environment.
+ # Retrieve the environment for the current process.
#
- # This is only used for handling functions that are not attached to a
- # specific environment.
+ # @note This should only used when a catalog is being compiled.
#
# @api private
- def self.root
- @root
+ #
+ # @return [Puppet::Node::Environment] the currently set environment if one
+ # has been explicitly set, else it will return the '*root*' environment
+ def self.current
+ Puppet.deprecation_warning("Puppet::Node::Environment.current has been replaced by Puppet.lookup(:current_environment), see http://links.puppetlabs.com/current-env-deprecation")
+ Puppet.lookup(:current_environment)
+ end
+
+ # @param [String] name Environment name to check for valid syntax.
+ # @return [Boolean] true if name is valid
+ # @api public
+ def self.valid_name?(name)
+ !!name.match(/\A\w+\Z/)
end
# Clear all memoized environments and the 'current' environment
#
# @api private
def self.clear
- @seen.clear
- $environment = nil
+ seen.clear
end
# @!attribute [r] name
# @api public
# @return [Symbol] the human readable environment name that serves as the
# environment identifier
attr_reader :name
+ # @api public
+ # @return [Array<String>] All directories present on disk in the modulepath
+ def modulepath
+ @modulepath.find_all do |p|
+ Puppet::FileSystem.directory?(p)
+ end
+ end
+
+ # @api public
+ # @return [Array<String>] All directories in the modulepath (even if they are not present on disk)
+ def full_modulepath
+ @modulepath
+ end
+
+ # @!attribute [r] manifest
+ # @api public
+ # @return [String] path to the manifest file or directory.
+ attr_reader :manifest
+
# Return an environment-specific Puppet setting.
#
# @api public
#
# @param param [String, Symbol] The environment setting to look up
# @return [Object] The resolved setting value
def [](param)
Puppet.settings.value(param, self.name)
end
- # Instantiate a new environment
- #
- # @note {Puppet::Node::Environment.new} is overridden to return memoized
- # objects, so this will not be invoked with the normal Ruby initialization
- # semantics.
- #
- # @param name [Symbol] The environment name
- def initialize(name)
- @name = name
- 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 `$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
def known_resource_types
if @known_resource_types.nil?
@known_resource_types = Puppet::Resource::TypeCollection.new(self)
@known_resource_types.import_ast(perform_initial_import(), '')
end
@known_resource_types
end
# Yields each modules' plugin directory if the plugin directory (modulename/lib)
# is present on the filesystem.
#
# @yield [String] Yields the plugin directory from each module to the block.
# @api public
def each_plugin_directory(&block)
modules.map(&:plugin_directory).each do |lib|
lib = Puppet::Util::Autoload.cleanpath(lib)
yield lib if File.directory?(lib)
end
end
# Locate a module instance by the module name alone.
#
# @api public
#
# @param name [String] The module name
# @return [Puppet::Module, nil] The module if found, else nil
def module(name)
modules.find {|mod| mod.name == name}
end
# Locate a module instance by the full forge name (EG authorname/module)
#
# @api public
#
# @param forge_name [String] The module name
# @return [Puppet::Module, nil] The module if found, else nil
def module_by_forge_name(forge_name)
author, modname = forge_name.split('/')
found_mod = self.module(modname)
found_mod and found_mod.forge_name == forge_name ?
found_mod :
nil
end
- # @!attribute [r] modulepath
- # Return all existent directories in the modulepath for this environment
- # @note This value is cached so that the filesystem doesn't have to be
- # re-enumerated every time this method is invoked, since that
- # enumeration could be a costly operation and this method is called
- # frequently. The cache expiry is determined by `Puppet[:filetimeout]`.
- # @see Puppet::Util::Cacher.cached_attr
- # @api public
- # @return [Array<String>] All directories present in the modulepath
- cached_attr(:modulepath, Puppet[:filetimeout]) do
- dirs = self[:modulepath].split(File::PATH_SEPARATOR)
- dirs = ENV["PUPPETLIB"].split(File::PATH_SEPARATOR) + dirs if ENV["PUPPETLIB"]
- validate_dirs(dirs)
- end
-
# @!attribute [r] modules
# Return all modules for this environment in the order they appear in the
# modulepath.
# @note If multiple modules with the same name are present they will
# both be added, but methods like {#module} and {#module_by_forge_name}
# will return the first matching entry in this list.
# @note This value is cached so that the filesystem doesn't have to be
# re-enumerated every time this method is invoked, since that
# enumeration could be a costly operation and this method is called
# frequently. The cache expiry is determined by `Puppet[:filetimeout]`.
# @see Puppet::Util::Cacher.cached_attr
# @api public
# @return [Array<Puppet::Module>] All modules for this environment
cached_attr(:modules, Puppet[:filetimeout]) do
module_references = []
seen_modules = {}
modulepath.each do |path|
Dir.entries(path).each do |name|
warn_about_mistaken_path(path, name)
next if module_references.include?(name)
if not seen_modules[name]
module_references << {:name => name, :path => File.join(path, name)}
seen_modules[name] = true
end
end
end
module_references.collect do |reference|
begin
Puppet::Module.new(reference[:name], reference[:path], self)
rescue Puppet::Module::Error
nil
end
end.compact
end
# Generate a warning if the given directory in a module path entry is named `lib`.
#
# @api private
#
# @param path [String] The module directory containing the given directory
# @param name [String] The directory name
def warn_about_mistaken_path(path, name)
if name == "lib"
Puppet.debug("Warning: Found directory named 'lib' in module path ('#{path}/lib'); unless " +
"you are expecting to load a module named 'lib', your module path may be set " +
"incorrectly.")
end
end
# Modules broken out by directory in the modulepath
#
# @note This method _changes_ the current working directory while enumerating
# the modules. This seems rather dangerous.
#
# @api public
#
# @return [Hash<String, Array<Puppet::Module>>] A hash whose keys are file
# paths, and whose values is an array of Puppet Modules for that path
def modules_by_path
modules_by_path = {}
modulepath.each do |path|
Dir.chdir(path) do
module_names = Dir.glob('*').select do |d|
FileTest.directory?(d) && (File.basename(d) =~ /\A\w+(-\w+)*\Z/)
end
modules_by_path[path] = module_names.sort.map do |name|
Puppet::Module.new(name, File.join(path, name), self)
end
end
end
modules_by_path
end
# All module requirements for all modules in the environment modulepath
#
# @api public
#
# @comment This has nothing to do with an environment. It seems like it was
# stuffed into the first convenient class that vaguely involved modules.
#
# @example
# environment.module_requirements
# # => {
# # 'username/amodule' => [
# # {
# # 'name' => 'username/moduledep',
# # 'version' => '1.2.3',
# # 'version_requirement' => '>= 1.0.0',
# # },
# # {
# # 'name' => 'username/anotherdep',
# # 'version' => '4.5.6',
# # 'version_requirement' => '>= 3.0.0',
# # }
# # ]
# # }
# #
#
# @return [Hash<String, Array<Hash<String, String>>>] See the method example
# for an explanation of the return value.
def module_requirements
deps = {}
modules.each do |mod|
next unless mod.forge_name
deps[mod.forge_name] ||= []
mod.dependencies and mod.dependencies.each do |mod_dep|
deps[mod_dep['name']] ||= []
dep_details = {
'name' => mod.forge_name,
'version' => mod.version,
'version_requirement' => mod_dep['version_requirement']
}
deps[mod_dep['name']] << dep_details
end
end
deps.each do |mod, mod_deps|
deps[mod] = mod_deps.sort_by {|d| d['name']}
end
deps
end
+ # Set a periodic watcher on the file, so we can tell if it has changed.
+ # @param filename [File,String] File instance or filename
+ # @api private
+ def watch_file(file)
+ known_resource_types.watch_file(file.to_s)
+ end
def check_for_reparse
- if @known_resource_types && @known_resource_types.require_reparse?
+ if (Puppet[:code] != @parsed_code) || (@known_resource_types && @known_resource_types.require_reparse?)
+ @parsed_code = nil
@known_resource_types = nil
end
end
# @return [String] The stringified value of the `name` instance variable
# @api public
def to_s
name.to_s
end
# @return [Symbol] The `name` value, cast to a string, then cast to a symbol.
#
# @api public
#
# @note the `name` instance variable is a Symbol, but this casts the value
# to a String and then converts it back into a Symbol which will needlessly
# create an object that needs to be garbage collected
def to_sym
to_s.to_sym
end
# Return only the environment name when serializing.
#
# The only thing we care about when serializing an environment is its
# identity; everything else is ephemeral and should not be stored or
# transmitted.
#
# @api public
def to_zaml(z)
self.to_s.to_zaml(z)
end
- # Validate a list of file paths and return the paths that are directories on the filesystem
- #
- # @api private
- #
- # @param dirs [Array<String>] The file paths to validate
- # @return [Array<String>] All file paths that exist and are directories
- def validate_dirs(dirs)
+ def self.split_path(path_string)
+ path_string.split(File::PATH_SEPARATOR)
+ end
+
+ def ==(other)
+ return true if other.kind_of?(Puppet::Node::Environment) &&
+ self.name == other.name &&
+ self.full_modulepath == other.full_modulepath &&
+ self.manifest == other.manifest
+ end
+
+ def hash
+ [self.class, name, full_modulepath, manifest].hash
+ end
+
+ private
+
+ def self.extralibs()
+ if ENV["PUPPETLIB"]
+ split_path(ENV["PUPPETLIB"])
+ else
+ []
+ end
+ end
+
+ def self.expand_dirs(dirs)
dirs.collect do |dir|
File.expand_path(dir)
- end.find_all do |p|
- FileTest.directory?(p)
end
end
- private
-
# Reparse the manifests for the given environment
#
# There are two sources that can be used for the initial parse:
#
# 1. The value of `Puppet.settings[:code]`: Puppet can take a string from
# its settings and parse that as a manifest. This is used by various
# Puppet applications to read in a manifest and pass it to the
# environment as a side effect. This is attempted first.
# 2. The contents of `Puppet.settings[:manifest]`: Puppet will try to load
# the environment manifest. By default this is `$manifestdir/site.pp`
#
# @note This method will return an empty hostclass if
# `Puppet.settings[:ignoreimport]` is set to true.
#
# @return [Puppet::Parser::AST::Hostclass] The AST hostclass object
# representing the 'main' hostclass
def perform_initial_import
- return empty_parse_result if Puppet.settings[:ignoreimport]
-# parser = Puppet::Parser::Parser.new(self)
+ return empty_parse_result if Puppet[:ignoreimport]
parser = Puppet::Parser::ParserFactory.parser(self)
- if code = Puppet.settings.uninterpolated_value(:code, name.to_s) and code != ""
- parser.string = code
+ @parsed_code = Puppet[:code]
+ if @parsed_code != ""
+ parser.string = @parsed_code
+ parser.parse
else
- file = Puppet.settings.value(:manifest, name.to_s)
- parser.file = file
+ file = self.manifest
+ # if the manifest file is a reference to a directory, parse and combine all .pp files in that
+ # directory
+ if File.directory?(file)
+ parse_results = Dir.entries(file).find_all { |f| f =~ /\.pp$/ }.sort.map do |pp_file|
+ parser.file = File.join(file, pp_file)
+ parser.parse
+ end
+ # Use a parser type specific merger to concatenate the results
+ Puppet::Parser::AST::Hostclass.new('', :code => Puppet::Parser::ParserFactory.code_merger.concatenate(parse_results))
+ else
+ parser.file = file
+ parser.parse
+ end
end
- parser.parse
rescue => detail
@known_resource_types.parse_failed = true
msg = "Could not parse for environment #{self}: #{detail}"
error = Puppet::Error.new(msg)
error.set_backtrace(detail.backtrace)
raise error
end
# Return an empty toplevel hostclass to indicate that no file was loaded
#
# This is used as the return value of {#perform_initial_import} when
# `Puppet.settings[:ignoreimport]` is true.
#
# @return [Puppet::Parser::AST::Hostclass]
def empty_parse_result
return Puppet::Parser::AST::Hostclass.new('')
end
-
- @root = new(:'*root*')
end
diff --git a/lib/puppet/node/facts.rb b/lib/puppet/node/facts.rb
index 2be4e68b9..be9871377 100644
--- a/lib/puppet/node/facts.rb
+++ b/lib/puppet/node/facts.rb
@@ -1,154 +1,155 @@
require 'time'
require 'puppet/node'
require 'puppet/indirector'
require 'puppet/util/pson'
# Manage a given node's facts. This either accepts facts and stores them, or
# returns facts for a given node.
class Puppet::Node::Facts
# Set up indirection, so that nodes can be looked for in
# the node sources.
extend Puppet::Indirector
extend Puppet::Util::Pson
# We want to expire any cached nodes if the facts are saved.
module NodeExpirer
def save(instance, key = nil, options={})
Puppet::Node.indirection.expire(instance.name, options)
super
end
end
indirects :facts, :terminus_setting => :facts_terminus, :extend => NodeExpirer
attr_accessor :name, :values
def add_local_facts
values["clientcert"] = Puppet.settings[:certname]
values["clientversion"] = Puppet.version.to_s
values["clientnoop"] = Puppet.settings[:noop]
end
def initialize(name, values = {})
@name = name
@values = values
add_timestamp
end
def initialize_from_hash(data)
@name = data['name']
@values = data['values']
# Timestamp will be here in YAML
timestamp = data['values']['_timestamp']
@values.delete_if do |key, val|
key =~ /^_/
end
#Timestamp will be here in pson
timestamp ||= data['timestamp']
timestamp = Time.parse(timestamp) if timestamp.is_a? String
self.timestamp = timestamp
self.expiration = data['expiration']
if expiration.is_a? String
self.expiration = Time.parse(expiration)
end
end
# Convert all fact values into strings.
def stringify
values.each do |fact, value|
values[fact] = value.to_s
end
end
# Sanitize fact values by converting everything not a string, boolean
# numeric, array or hash into strings.
def sanitize
values.each do |fact, value|
values[fact] = sanitize_fact value
end
end
def ==(other)
return false unless self.name == other.name
strip_internal == other.send(:strip_internal)
end
- def self.from_pson(data)
+ def self.from_data_hash(data)
new_facts = allocate
new_facts.initialize_from_hash(data)
new_facts
end
+ def self.from_pson(data)
+ Puppet.deprecation_warning("from_pson is being removed in favour of from_data_hash.")
+ self.from_data_hash(data)
+ end
+
def to_data_hash
result = {
'name' => name,
'values' => strip_internal,
}
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
end
- def to_pson(*args)
- to_data_hash.to_pson(*args)
- end
-
# Add internal data to the facts for storage.
def add_timestamp
self.timestamp = Time.now
end
def timestamp=(time)
self.values['_timestamp'] = time
end
def timestamp
self.values['_timestamp']
end
# Strip out that internal data.
def strip_internal
newvals = values.dup
newvals.find_all { |name, value| name.to_s =~ /^_/ }.each { |name, value| newvals.delete(name) }
newvals
end
private
def sanitize_fact(fact)
if fact.is_a? Hash then
ret = {}
fact.each_pair { |k,v| ret[sanitize_fact k]=sanitize_fact v }
ret
elsif fact.is_a? Array then
fact.collect { |i| sanitize_fact i }
elsif fact.is_a? Numeric \
or fact.is_a? TrueClass \
or fact.is_a? FalseClass \
or fact.is_a? String
fact
else
fact.to_s
end
end
end
diff --git a/lib/puppet/parameter.rb b/lib/puppet/parameter.rb
index f265a99ae..59e18de8e 100644
--- a/lib/puppet/parameter.rb
+++ b/lib/puppet/parameter.rb
@@ -1,585 +1,585 @@
require 'puppet/util/methodhelper'
require 'puppet/util/logging'
require 'puppet/util/docs'
# The Parameter class is the implementation of a resource's attributes of _parameter_ kind.
# The Parameter class is also the base class for {Puppet::Property}, and is used to describe meta-parameters
# (parameters that apply to all resource types).
# A Parameter (in contrast to a Property) has a single value where a property has both a current and a wanted value.
# The Parameter class methods are used to configure and create an instance of Parameter that represents
# one particular attribute data type; its valid value(s), and conversion to/from internal form.
#
# The intention is that a new parameter is created by using the DSL method {Puppet::Type.newparam}, or
# {Puppet::Type.newmetaparam} if the parameter should be applicable to all resource types.
#
# A Parameter that does not specify and valid values (via {newvalues}) accepts any value.
#
# @see Puppet::Type
# @see Puppet::Property
# @api public
#
class Puppet::Parameter
include Puppet::Util
include Puppet::Util::Errors
include Puppet::Util::Logging
include Puppet::Util::MethodHelper
require 'puppet/parameter/value_collection'
class << self
include Puppet::Util
include Puppet::Util::Docs
# Unused?
# @todo The term "validater" only appears in this location in the Puppet code base. There is `validate`
# which seems to works fine without this attribute declaration.
# @api private
#
attr_reader :validater
# Unused?
# @todo The term "munger" only appears in this location in the Puppet code base. There is munge and unmunge
# and they seem to work perfectly fine without this attribute declaration.
# @api private
#
attr_reader :munger
# @return [Symbol] The parameter name as given when it was created.
attr_reader :name
# @return [Object] The default value of the parameter as determined by the {defaultto} method, or nil if no
# default has been set.
attr_reader :default
# @comment This somewhat odd documentation construct is because the getter and setter are not
# orthogonal; the setter uses varargs and this confuses yard. To overcome the problem both the
# getter and the setter are documented here. If this issues is fixed, a todo will be displayed
# for the setter method, and the setter documentation can be moved there.
# Since the attribute is actually RW it should perhaps instead just be implemented as a setter
# and a getter method (and no attr_xxx declaration).
#
# @!attribute [rw] required_features
# @return [Array<Symbol>] The names of the _provider features_ required for this parameter to work.
# the returned names are always all lower case symbols.
# @overload required_features
# Returns the required _provider features_ as an array of lower case symbols
# @overload required_features=(*args)
# @param *args [Symbol] one or more names of required provider features
# Sets the required_provider_features_ from one or more values, or array. The given arguments
# are flattened, and internalized.
# @api public
# @dsl type
#
attr_reader :required_features
# @return [Puppet::Parameter::ValueCollection] The set of valid values (or an empty set that accepts any value).
# @api private
#
attr_reader :value_collection
# @return [Boolean] Flag indicating whether this parameter is a meta-parameter or not.
attr_accessor :metaparam
# Defines how the `default` value of a parameter is computed.
# The computation of the parameter's default value is defined by providing a value or a block.
# A default of `nil` can not be used.
# @overload defaultto(value)
# Defines the default value with a literal value
# @param value [Object] the literal value to use as the default value
# @overload defaultto({|| ... })
# Defines that the default value is produced by the given block. The given block
# should produce the default value.
# @raise [Puppet::DevError] if value is nil, and no block is given.
# @return [void]
# @see Parameter.default
# @dsl type
# @api public
#
def defaultto(value = nil, &block)
if block
define_method(:default, &block)
else
if value.nil?
raise Puppet::DevError,
"Either a default value or block must be provided"
end
define_method(:default) do value end
end
end
# Produces a documentation string.
# If an enumeration of _valid values_ has been defined, it is appended to the documentation
# for this parameter specified with the {desc} method.
# @return [String] Returns a documentation string.
# @api public
#
def doc
@doc ||= ""
unless defined?(@addeddocvals)
@doc = Puppet::Util::Docs.scrub(@doc)
if vals = value_collection.doc
@doc << "\n\n#{vals}"
end
if f = self.required_features
@doc << "\n\nRequires features #{f.flatten.collect { |f| f.to_s }.join(" ")}."
end
@addeddocvals = true
end
@doc
end
# Removes the `default` method if defined.
# Has no effect if the default method is not defined.
# This method is intended to be used in a DSL scenario where a parameter inherits from a parameter
# with a default value that is not wanted in the derived parameter (otherwise, simply do not define
# a default value method).
#
# @return [void]
# @see desc
# @api public
# @dsl type
#
def nodefault
undef_method :default if public_method_defined? :default
end
# Sets the documentation for this parameter.
# @param str [String] The documentation string to set
# @return [String] the given `str` parameter
# @see doc
# @dsl type
# @api public
#
def desc(str)
@doc = str
end
# Initializes the instance variables.
# Clears the internal value collection (set of allowed values).
# @return [void]
# @api private
#
def initvars
@value_collection = ValueCollection.new
end
# @overload munge {|| ... }
# Defines an optional method used to convert the parameter value from DSL/string form to an internal form.
# If a munge method is not defined, the DSL/string value is used as is.
# @note This adds a method with the name `unsafe_munge` in the created parameter class. Later this method is
# called in a context where exceptions will be rescued and handled.
# @dsl type
# @api public
#
def munge(&block)
# I need to wrap the unsafe version in begin/rescue parameterments,
# but if I directly call the block then it gets bound to the
# class's context, not the instance's, thus the two methods,
# instead of just one.
define_method(:unsafe_munge, &block)
end
# @overload unmunge {|| ... }
# Defines an optional method used to convert the parameter value to DSL/string form from an internal form.
# If an `unmunge` method is not defined, the internal form is used.
# @see munge
# @note This adds a method with the name `unmunge` in the created parameter class.
# @dsl type
# @api public
#
def unmunge(&block)
define_method(:unmunge, &block)
end
# Sets a marker indicating that this parameter is the _namevar_ (unique identifier) of the type
# where the parameter is contained.
# This also makes the parameter a required value. The marker can not be unset once it has been set.
# @return [void]
# @dsl type
# @api public
#
def isnamevar
@isnamevar = true
@required = true
end
# @return [Boolean] Returns whether this parameter is the _namevar_ or not.
# @api public
#
def isnamevar?
@isnamevar
end
# Sets a marker indicating that this parameter is required.
# Once set, it is not possible to make a parameter optional.
# @return [void]
# @dsl type
# @api public
#
def isrequired
@required = true
end
# @comment This method is not picked up by yard as it has a different signature than
# expected for an attribute (varargs). Instead, this method is documented as an overload
# of the attribute required_features. (Not ideal, but better than nothing).
# @todo If this text appears in documentation - see comment in source and makes corrections - it means
# that an issue in yardoc has been fixed.
#
def required_features=(*args)
@required_features = args.flatten.collect { |a| a.to_s.downcase.intern }
end
# Returns whether this parameter is required or not.
# A parameter is required if a call has been made to the DSL method {isrequired}.
# @return [Boolean] Returns whether this parameter is required or not.
# @api public
#
def required?
@required
end
# @overload validate {|| ... }
# Defines an optional method that is used to validate the parameter's DSL/string value.
# Validation should raise appropriate exceptions, the return value of the given block is ignored.
# The easiest way to raise an appropriate exception is to call the method {Puppet::Util::Errors.fail} with
# the message as an argument.
# To validate the munged value instead, just munge the value (`munge(value)`).
#
# @return [void]
# @dsl type
# @api public
#
def validate(&block)
define_method(:unsafe_validate, &block)
end
# Defines valid values for the parameter (enumeration or regular expressions).
# The set of valid values for the parameter can be limited to a (mix of) literal values and
# regular expression patterns.
# @note Each call to this method adds to the set of valid values
# @param names [Symbol, Regexp] The set of valid literal values and/or patterns for the parameter.
# @return [void]
# @dsl type
# @api public
#
def newvalues(*names)
@value_collection.newvalues(*names)
end
# Makes the given `name` an alias for the given `other` name.
# Or said differently, the valid value `other` can now also be referred to via the given `name`.
# Aliasing may affect how the parameter's value is serialized/stored (it may store the `other` value
# instead of the alias).
# @api public
# @dsl type
#
def aliasvalue(name, other)
@value_collection.aliasvalue(name, other)
end
end
# Creates instance (proxy) methods that delegates to a class method with the same name.
# @api private
#
def self.proxymethods(*values)
values.each { |val|
define_method(val) do
self.class.send(val)
end
}
end
# @!method required?
# (see required?)
# @!method isnamevar?
# (see isnamevar?)
#
proxymethods("required?", "isnamevar?")
# @return [Puppet::Resource] A reference to the resource this parameter is an attribute of (the _associated resource_).
attr_accessor :resource
# @comment LAK 2007-05-09: Keep the @parent around for backward compatibility.
# @return [Puppet::Parameter] A reference to the parameter's parent kept for backwards compatibility.
# @api private
#
attr_accessor :parent
# Returns a string representation of the resource's containment path in
# the catalog.
# @return [String]
def path
@path ||= '/' + pathbuilder.join('/')
end
# @return [Integer] Returns the result of calling the same method on the associated resource.
def line
resource.line
end
# @return [Integer] Returns the result of calling the same method on the associated resource.
def file
resource.file
end
# @return [Integer] Returns the result of calling the same method on the associated resource.
def version
resource.version
end
# Initializes the parameter with a required resource reference and optional attribute settings.
# The option `:resource` must be specified or an exception is raised. Any additional options passed
# are used to initialize the attributes of this parameter by treating each key in the `options` hash as
# the name of the attribute to set, and the value as the value to set.
# @param options [Hash{Symbol => Object]] Options, where `resource` is required
# @option options [Puppet::Resource] :resource The resource this parameter holds a value for. Required.
# @raise [Puppet::DevError] If resource is not specified in the options hash.
# @api public
# @note A parameter should be created via the DSL method {Puppet::Type::newparam}
#
def initialize(options = {})
options = symbolize_options(options)
if resource = options[:resource]
self.resource = resource
options.delete(:resource)
else
raise Puppet::DevError, "No resource set for #{self.class.name}"
end
set_options(options)
end
# Writes the given `msg` to the log with the loglevel indicated by the associated resource's
# `loglevel` parameter.
# @todo is loglevel a metaparameter? it is looked up with `resource[:loglevel]`
# @return [void]
# @api public
def log(msg)
send_log(resource[:loglevel], msg)
end
# @return [Boolean] Returns whether this parameter is a meta-parameter or not.
def metaparam?
self.class.metaparam
end
# @!attribute [r] name
# @return [Symbol] The parameter's name as given when it was created.
# @note Since a Parameter defines the name at the class level, each Parameter class must be
# unique within a type's inheritance chain.
# @comment each parameter class must define the name method, and parameter
# instances do not change that name this implicitly means that a given
# object can only have one parameter instance of a given parameter
# class
def name
self.class.name
end
# @return [Boolean] Returns true if this parameter, the associated resource, or overall puppet mode is `noop`.
# @todo How is noop mode set for a parameter? Is this of value in DSL to inhibit a parameter?
#
def noop
@noop ||= false
tmp = @noop || self.resource.noop || Puppet[:noop] || false
#debug "noop is #{tmp}"
tmp
end
# Returns an array of strings representing the containment heirarchy
# (types/classes) that make up the path to the resource from the root
# of the catalog. This is mostly used for logging purposes.
#
# @api private
def pathbuilder
if @resource
return [@resource.pathbuilder, self.name]
else
return [self.name]
end
end
# This is the default implementation of `munge` that simply produces the value (if it is valid).
# The DSL method {munge} should be used to define an overriding method if munging is required.
#
# @api private
#
def unsafe_munge(value)
self.class.value_collection.munge(value)
end
# Unmunges the value by transforming it from internal form to DSL form.
# This is the default implementation of `unmunge` that simply returns the value without processing.
# The DSL method {unmunge} should be used to define an overriding method if required.
# @return [Object] the unmunged value
#
def unmunge(value)
value
end
# Munges the value to internal form.
# This implementation of `munge` provides exception handling around the specified munging of this parameter.
# @note This method should not be overridden. Use the DSL method {munge} to define a munging method
# if required.
# @param value [Object] the DSL value to munge
# @return [Object] the munged (internal) value
#
def munge(value)
begin
ret = unsafe_munge(value)
rescue Puppet::Error => detail
Puppet.debug "Reraising #{detail}"
raise
rescue => detail
raise Puppet::DevError, "Munging failed for value #{value.inspect} in class #{self.name}: #{detail}", detail.backtrace
end
ret
end
# This is the default implementation of `validate` that may be overridden by the DSL method {validate}.
# If no valid values have been defined, the given value is accepted, else it is validated against
# the literal values (enumerator) and/or patterns defined by calling {newvalues}.
#
# @param value [Object] the value to check for validity
# @raise [ArgumentError] if the value is not valid
# @return [void]
# @api private
#
def unsafe_validate(value)
self.class.value_collection.validate(value)
end
# Performs validation of the given value against the rules defined by this parameter.
# @return [void]
# @todo Better description of when the various exceptions are raised.ArgumentError is rescued and
# changed into Puppet::Error.
# @raise [ArgumentError, TypeError, Puppet::DevError, Puppet::Error] under various conditions
# A protected validation method that only ever raises useful exceptions.
# @api public
#
def validate(value)
begin
unsafe_validate(value)
rescue ArgumentError => detail
- fail detail.to_s
+ self.fail Puppet::Error, detail.to_s, detail
rescue Puppet::Error, TypeError
raise
rescue => detail
raise Puppet::DevError, "Validate method failed for class #{self.name}: #{detail}", detail.backtrace
end
end
# Sets the associated resource to nil.
# @todo Why - what is the intent/purpose of this?
# @return [nil]
#
def remove
@resource = nil
end
# @return [Object] Gets the value of this parameter after performing any specified unmunging.
def value
unmunge(@value) unless @value.nil?
end
# Sets the given value as the value of this parameter.
# @todo This original comment _"All of the checking should possibly be
# late-binding (e.g., users might not exist when the value is assigned
# but might when it is asked for)."_ does not seem to be correct, the implementation
# calls both validate and munge on the given value, so no late binding.
#
# The given value is validated and then munged (if munging has been specified). The result is store
# as the value of this arameter.
# @return [Object] The given `value` after munging.
# @raise (see #validate)
#
def value=(value)
validate(value)
@value = munge(value)
end
# @return [Puppet::Provider] Returns the provider of the associated resource.
# @todo The original comment says = _"Retrieve the resource's provider.
# Some types don't have providers, in which case we return the resource object itself."_
# This does not seem to be true, the default implementation that sets this value may be
# {Puppet::Type.provider=} which always gets either the name of a provider or an instance of one.
#
def provider
@resource.provider
end
# @return [Array<Symbol>] Returns an array of the associated resource's symbolic tags (including the parameter itself).
# Returns an array of the associated resource's symbolic tags (including the parameter itself).
# At a minimun, the array contains the name of the parameter. If the associated resource
# has tags, these tags are also included in the array.
# @todo The original comment says = _"The properties need to return tags so that logs correctly
# collect them."_ what if anything of that is of interest to document. Should tags and their relationship
# to logs be described. This is a more general concept.
#
def tags
unless defined?(@tags)
@tags = []
# This might not be true in testing
@tags = @resource.tags if @resource.respond_to? :tags
@tags << self.name.to_s
end
@tags
end
# @return [String] The name of the parameter in string form.
def to_s
name.to_s
end
# Produces a String with the value formatted for display to a human.
# When the parameter value is a:
#
# * **single valued parameter value** the result is produced on the
# form `'value'` where _value_ is the string form of the parameter's value.
#
# * **Array** the list of values is enclosed in `[]`, and
# each produced value is separated by a comma.
#
# * **Hash** value is output with keys in sorted order enclosed in `{}` with each entry formatted
# on the form `'k' => v` where
# `k` is the key in string form and _v_ is the value of the key. Entries are comma separated.
#
# For both Array and Hash this method is called recursively to format contained values.
# @note this method does not protect against infinite structures.
#
# @return [String] The formatted value in string form.
#
def self.format_value_for_display(value)
if value.is_a? Array
formatted_values = value.collect {|value| format_value_for_display(value)}.join(', ')
"[#{formatted_values}]"
elsif value.is_a? Hash
# Sorting the hash keys for display is largely for having stable
# output to test against, but also helps when scanning for hash
# keys, since they will be in ASCIIbetical order.
hash = value.keys.sort {|a,b| a.to_s <=> b.to_s}.collect do |k|
"'#{k}' => #{format_value_for_display(value[k])}"
end.join(', ')
"{#{hash}}"
else
"'#{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/parser/ast.rb b/lib/puppet/parser/ast.rb
index 438cb49b4..b7e324687 100644
--- a/lib/puppet/parser/ast.rb
+++ b/lib/puppet/parser/ast.rb
@@ -1,130 +1,130 @@
# the parent class for all of our syntactical objects
require 'puppet'
require 'puppet/util/autoload'
# The base class for all of the objects that make up the parse trees.
# Handles things like file name, line #, and also does the initialization
# for all of the parameters of all of the child objects.
class Puppet::Parser::AST
# Do this so I don't have to type the full path in all of the subclasses
AST = Puppet::Parser::AST
include Puppet::Util::Errors
include Puppet::Util::MethodHelper
include Puppet::Util::Docs
attr_accessor :parent, :scope, :file, :line, :pos
def inspect
"( #{self.class} #{self.to_s} #{@children.inspect} )"
end
# don't fetch lexer comment by default
def use_docs
self.class.use_docs
end
# allow our subclass to specify they want documentation
class << self
attr_accessor :use_docs
def associates_doc
self.use_docs = true
end
end
# Evaluate the current object. Just a stub method, since the subclass
# should override this method.
def evaluate(*options)
- raise Puppet::DevError, "Did not override #evaluate in #{self.class}"
end
# Throw a parse error.
def parsefail(message)
self.fail(Puppet::ParseError, message)
end
# Wrap a statemp in a reusable way so we always throw a parse error.
def parsewrap
exceptwrap :type => Puppet::ParseError do
yield
end
end
# The version of the evaluate method that should be called, because it
# correctly handles errors. It is critical to use this method because
# it can enable you to catch the error where it happens, rather than
# much higher up the stack.
def safeevaluate(*options)
# We duplicate code here, rather than using exceptwrap, because this
# is called so many times during parsing.
begin
return self.evaluate(*options)
rescue Puppet::Error => detail
raise adderrorcontext(detail)
rescue => detail
error = Puppet::ParseError.new(detail.to_s, nil, nil, detail)
# We can't use self.fail here because it always expects strings,
# not exceptions.
raise adderrorcontext(error, detail)
end
end
# Initialize the object. Requires a hash as the argument, and
# takes each of the parameters of the hash and calls the settor
# method for them. This is probably pretty inefficient and should
# likely be changed at some point.
def initialize(args)
set_options(args)
end
# evaluate ourselves, and match
def evaluate_match(value, scope)
obj = self.safeevaluate(scope)
obj = obj.downcase if obj.respond_to?(:downcase)
value = value.downcase if value.respond_to?(:downcase)
obj = Puppet::Parser::Scope.number?(obj) || obj
value = Puppet::Parser::Scope.number?(value) || value
# "" == undef for case/selector/if
obj == value or (obj == "" and value == :undef) or (obj == :undef and value == "")
end
end
# And include all of the AST subclasses.
require 'puppet/parser/ast/arithmetic_operator'
require 'puppet/parser/ast/astarray'
require 'puppet/parser/ast/asthash'
require 'puppet/parser/ast/boolean_operator'
require 'puppet/parser/ast/branch'
require 'puppet/parser/ast/caseopt'
require 'puppet/parser/ast/casestatement'
require 'puppet/parser/ast/collection'
require 'puppet/parser/ast/collexpr'
require 'puppet/parser/ast/comparison_operator'
require 'puppet/parser/ast/definition'
require 'puppet/parser/ast/else'
require 'puppet/parser/ast/function'
require 'puppet/parser/ast/hostclass'
require 'puppet/parser/ast/ifstatement'
require 'puppet/parser/ast/in_operator'
require 'puppet/parser/ast/lambda'
require 'puppet/parser/ast/leaf'
require 'puppet/parser/ast/match_operator'
require 'puppet/parser/ast/method_call'
require 'puppet/parser/ast/minus'
require 'puppet/parser/ast/node'
require 'puppet/parser/ast/nop'
require 'puppet/parser/ast/not'
require 'puppet/parser/ast/relationship'
require 'puppet/parser/ast/resource'
require 'puppet/parser/ast/resource_defaults'
require 'puppet/parser/ast/resource_instance'
require 'puppet/parser/ast/resource_override'
require 'puppet/parser/ast/resource_reference'
require 'puppet/parser/ast/resourceparam'
require 'puppet/parser/ast/selector'
require 'puppet/parser/ast/tag'
require 'puppet/parser/ast/vardef'
+require 'puppet/parser/code_merger'
diff --git a/lib/puppet/parser/ast/block_expression.rb b/lib/puppet/parser/ast/block_expression.rb
index ac6173c39..b6b7e66b4 100644
--- a/lib/puppet/parser/ast/block_expression.rb
+++ b/lib/puppet/parser/ast/block_expression.rb
@@ -1,45 +1,40 @@
require 'puppet/parser/ast/branch'
class Puppet::Parser::AST
class BlockExpression < Branch
include Enumerable
# Evaluate contained expressions, produce result of the last
def evaluate(scope)
result = nil
@children.each do |child|
- # Skip things that respond to :instantiate (classes, nodes,
- # and definitions), because they have already been
- # instantiated.
- if !child.respond_to?(:instantiate)
- result = child.safeevaluate(scope)
- end
+ result = child.safeevaluate(scope)
end
result
end
# Return a child by index.
def [](index)
@children[index]
end
def push(*ary)
ary.each { |child|
#Puppet.debug "adding %s(%s) of type %s to %s" %
# [child, child.object_id, child.class.to_s.sub(/.+::/,''),
# self.object_id]
@children.push(child)
}
self
end
def sequence_with(other)
Puppet::Parser::AST::BlockExpression.new(:children => self.children + other.children)
end
def to_s
"[" + @children.collect { |c| c.to_s }.join(', ') + "]"
end
end
end
diff --git a/lib/puppet/parser/ast/collexpr.rb b/lib/puppet/parser/ast/collexpr.rb
index 6032094ab..9f266ed47 100644
--- a/lib/puppet/parser/ast/collexpr.rb
+++ b/lib/puppet/parser/ast/collexpr.rb
@@ -1,57 +1,109 @@
require 'puppet'
require 'puppet/parser/ast/branch'
require 'puppet/parser/collector'
# An object that collects stored objects from the central cache and returns
# them to the current host, yo.
class Puppet::Parser::AST
class CollExpr < AST::Branch
attr_accessor :test1, :test2, :oper, :form, :type, :parens
- # We return an object that does a late-binding evaluation.
def evaluate(scope)
+ if Puppet[:parser] == 'future'
+ evaluate4x(scope)
+ else
+ evaluate3x(scope)
+ end
+ end
+
+ # We return an object that does a late-binding evaluation.
+ def evaluate3x(scope)
# Make sure our contained expressions have all the info they need.
[@test1, @test2].each do |t|
if t.is_a?(self.class)
t.form ||= self.form
t.type ||= self.type
end
end
# The code is only used for virtual lookups
match1, code1 = @test1.safeevaluate scope
match2, code2 = @test2.safeevaluate scope
# First build up the virtual code.
# If we're a conjunction operator, then we're calling code. I did
# some speed comparisons, and it's at least twice as fast doing these
# case statements as doing an eval here.
code = proc do |resource|
case @oper
when "and"; code1.call(resource) and code2.call(resource)
when "or"; code1.call(resource) or code2.call(resource)
when "=="
if match1 == "tag"
resource.tagged?(match2)
else
if resource[match1].is_a?(Array)
resource[match1].include?(match2)
else
resource[match1] == match2
end
end
when "!="; resource[match1] != match2
end
end
match = [match1, @oper, match2]
return match, code
end
+ # Late binding evaluation of a collect expression (as done in 3x), but with proper Puppet Langauge
+ # semantics for equals and include
+ #
+ def evaluate4x(scope)
+ # Make sure our contained expressions have all the info they need.
+ [@test1, @test2].each do |t|
+ if t.is_a?(self.class)
+ t.form ||= self.form
+ t.type ||= self.type
+ end
+ end
+
+ # The code is only used for virtual lookups
+ match1, code1 = @test1.safeevaluate scope
+ match2, code2 = @test2.safeevaluate scope
+
+ # First build up the virtual code.
+ # If we're a conjunction operator, then we're calling code. I did
+ # some speed comparisons, and it's at least twice as fast doing these
+ # case statements as doing an eval here.
+ code = proc do |resource|
+ case @oper
+ when "and"; code1.call(resource) and code2.call(resource)
+ when "or"; code1.call(resource) or code2.call(resource)
+ when "=="
+ if match1 == "tag"
+ resource.tagged?(match2)
+ else
+ if resource[match1].is_a?(Array)
+ @@compare_operator.include?(resource[match1], match2)
+ else
+ @@compare_operator.equals(resource[match1], match2)
+ end
+ end
+ when "!="; ! @@compare_operator.equals(resource[match1], match2)
+ end
+ end
+
+ match = [match1, @oper, match2]
+ return match, code
+ end
+
def initialize(hash = {})
super
-
+ if Puppet[:parser] == "future"
+ @@compare_operator ||= Puppet::Pops::Evaluator::CompareOperator.new
+ end
raise ArgumentError, "Invalid operator #{@oper}" unless %w{== != and or}.include?(@oper)
end
end
end
diff --git a/lib/puppet/parser/ast/lambda.rb b/lib/puppet/parser/ast/lambda.rb
index 976f817cd..64d78d49b 100644
--- a/lib/puppet/parser/ast/lambda.rb
+++ b/lib/puppet/parser/ast/lambda.rb
@@ -1,126 +1,135 @@
require 'puppet/parser/ast/block_expression'
class Puppet::Parser::AST
# A block of statements/expressions with additional parameters
# Requires scope to contain the values for the defined parameters when evaluated
# If evaluated without a prepared scope, the lambda will behave like its super class.
#
class Lambda < AST::BlockExpression
# The lambda parameters.
# These are encoded as an array where each entry is an array of one or two object. The first
# is the parameter name, and the optional second object is the value expression (that will
# be evaluated when bound to a scope).
# The value expression is the default value for the parameter. All default values must be
# at the end of the parameter list.
#
# @return [Array<Array<String,String>>] list of parameter names with optional value expression
attr_accessor :parameters
# Evaluates each expression/statement and produce the last expression evaluation result
# @return [Object] what the last expression evaluated to
def evaluate(scope)
if @children.is_a? Puppet::Parser::AST::ASTArray
result = nil
@children.each {|expr| result = expr.evaluate(scope) }
result
else
@children.evaluate(scope)
end
end
# Calls the lambda.
# Assigns argument values in a nested local scope that should be used to evaluate the lambda
# and then evaluates the lambda.
# @param scope [Puppet::Scope] the calling scope
# @return [Object] the result of evaluating the expression(s) in the lambda
#
def call(scope, *args)
raise Puppet::ParseError, "Too many arguments: #{args.size} for #{parameters.size}" unless args.size <= parameters.size
# associate values with parameters
merged = parameters.zip(args)
# calculate missing arguments
missing = parameters.slice(args.size, parameters.size - args.size).select {|e| e.size == 1}
unless missing.empty?
optional = parameters.count { |p| p.size == 2 }
raise Puppet::ParseError, "Too few arguments; #{args.size} for #{optional > 0 ? ' min ' : ''}#{parameters.size - optional}"
end
evaluated = merged.collect do |m|
# m can be one of
# m = [["name"], "given"]
# | [["name", default_expr], "given"]
#
# "given" is always an optional entry. If a parameter was provided then
# the entry will be in the array, otherwise the m array will be a
# single element.
given_argument = m[1]
argument_name = m[0][0]
default_expression = m[0][1]
value = if m.size == 1
default_expression.safeevaluate(scope)
else
given_argument
end
[argument_name, value]
end
# Store the evaluated name => value associations in a new inner/local/ephemeral scope
# (This is made complicated due to the fact that the implementation of scope is overloaded with
# functionality and an inner ephemeral scope must be used (as opposed to just pushing a local scope
# on a scope "stack").
# Ensure variable exists with nil value if error occurs.
# Some ruby implementations does not like creating variable on return
result = nil
begin
elevel = scope.ephemeral_level
scope.ephemeral_from(Hash[evaluated], file, line)
result = safeevaluate(scope)
ensure
scope.unset_ephemeral_var(elevel)
end
result
end
# Validates the lambda.
# Validation checks if parameters with default values are at the end of the list. (It is illegal
# to have a parameter with default value followed by one without).
#
# @raise [Puppet::ParseError] if a parameter with a default comes before a parameter without default value
#
def validate
params = parameters || []
defaults = params.drop_while {|p| p.size < 2 }
trailing = defaults.drop_while {|p| p.size == 2 }
raise Puppet::ParseError, "Lambda parameters with default values must be placed last" unless trailing.empty?
end
# Returns the number of parameters (required and optional)
# @return [Integer] the total number of accepted parameters
def parameter_count
@parameters.size
end
# Returns the number of optional parameters.
# @return [Integer] the number of optional accepted parameters
def optional_parameter_count
@parameters.count {|p| p.size == 2 }
end
def initialize(options)
super(options)
# ensure there is an empty parameters structure if not given by creator
@parameters = [] unless options[:parameters]
validate
end
def to_s
result = ["{|"]
result += @parameters.collect {|p| "#{p[0]}" + (p.size == 2 && p[1]) ? p[1].to_s() : '' }.join(', ')
result << "| ... }"
result.join('')
end
+
+ # marker method checked with respond_to :puppet_lambda
+ def puppet_lambda()
+ true
+ end
+
+ def parameter_names
+ @parameters.collect {|p| p[0] }
+ end
end
end
diff --git a/lib/puppet/parser/ast/leaf.rb b/lib/puppet/parser/ast/leaf.rb
index af9f792cc..b8049afbf 100644
--- a/lib/puppet/parser/ast/leaf.rb
+++ b/lib/puppet/parser/ast/leaf.rb
@@ -1,217 +1,232 @@
class Puppet::Parser::AST
# The base class for all of the leaves of the parse trees. These
# basically just have types and values. Both of these parameters
# are simple values, not AST objects.
class Leaf < AST
attr_accessor :value, :type
# Return our value.
def evaluate(scope)
@value
end
def match(value)
@value == value
end
def to_s
@value.to_s unless @value.nil?
end
end
# The boolean class. True or false. Converts the string it receives
# to a Ruby boolean.
class Boolean < AST::Leaf
def initialize(hash)
super
unless @value == true or @value == false
raise Puppet::DevError, "'#{@value}' is not a boolean"
end
@value
end
end
# The base string class.
class String < AST::Leaf
def evaluate(scope)
@value.dup
end
def to_s
@value.inspect
end
end
# An uninterpreted string.
class FlatString < AST::Leaf
def evaluate(scope)
@value
end
def to_s
@value.inspect
end
end
class Concat < AST::Leaf
def evaluate(scope)
@value.collect { |x| x.evaluate(scope) }.collect{ |x| x == :undef ? '' : x }.join
end
def to_s
"#{@value.map { |s| s.to_s.gsub(/^"(.*)"$/, '\1') }.join}"
end
end
# The 'default' option on case statements and selectors.
class Default < AST::Leaf; end
# Capitalized words; used mostly for type-defaults, but also
# get returned by the lexer any other time an unquoted capitalized
# word is found.
class Type < AST::Leaf; end
# Lower-case words.
class Name < AST::Leaf; end
# double-colon separated class names
class ClassName < AST::Leaf; end
# undef values; equiv to nil
class Undef < AST::Leaf; end
# Host names, either fully qualified or just the short name, or even a regex
class HostName < AST::Leaf
def initialize(hash)
super
# Note that this is an AST::Regex, not a Regexp
unless @value.is_a?(Regex)
@value = @value.to_s.downcase
@value =~ /[^-\w.]/ and
raise Puppet::DevError, "'#{@value}' is not a valid hostname"
end
end
# implementing eql? and hash so that when an HostName is stored
# in a hash it has the same hashing properties as the underlying value
def eql?(value)
value = value.value if value.is_a?(HostName)
@value.eql?(value)
end
def hash
@value.hash
end
end
# A simple variable. This object is only used during interpolation;
# the VarDef class is used for assignment.
class Variable < Name
# Looks up the value of the object in the scope tree (does
# not include syntactical constructs, like '$' and '{}').
def evaluate(scope)
parsewrap do
if scope.include?(@value)
scope[@value, {:file => file, :line => line}]
else
:undef
end
end
end
def to_s
"\$#{value}"
end
end
class HashOrArrayAccess < AST::Leaf
attr_accessor :variable, :key
def evaluate_container(scope)
container = variable.respond_to?(:evaluate) ? variable.safeevaluate(scope) : variable
if container.is_a?(Hash) || container.is_a?(Array)
container
elsif container.is_a?(::String)
scope[container, {:file => file, :line => line}]
else
raise Puppet::ParseError, "#{variable} is #{container.inspect}, not a hash or array"
end
end
def evaluate_key(scope)
key.respond_to?(:evaluate) ? key.safeevaluate(scope) : key
end
def array_index_or_key(object, key)
if object.is_a?(Array)
raise Puppet::ParseError, "#{key} is not an integer, but is used as an index of an array" unless key = Puppet::Parser::Scope.number?(key)
end
key
end
def evaluate(scope)
object = evaluate_container(scope)
accesskey = evaluate_key(scope)
raise Puppet::ParseError, "#{variable} is not a hash or array when accessing it with #{accesskey}" unless object.is_a?(Hash) or object.is_a?(Array)
result = object[array_index_or_key(object, accesskey)]
result.nil? ? :undef : result
end
# Assign value to this hashkey or array index
def assign(scope, value)
object = evaluate_container(scope)
accesskey = evaluate_key(scope)
if object.is_a?(Hash) and object.include?(accesskey)
raise Puppet::ParseError, "Assigning to the hash '#{variable}' with an existing key '#{accesskey}' is forbidden"
end
+ mutation_deprecation()
+
# assign to hash or array
object[array_index_or_key(object, accesskey)] = value
end
def to_s
"\$#{variable.to_s}[#{key.to_s}]"
end
+
+ def mutation_deprecation
+ deprecation_location_text =
+ if file && line
+ " at #{file}:#{line}"
+ elsif file
+ " in file #{file}"
+ elsif line
+ " at #{line}"
+ end
+ Puppet.warning(["The use of mutating operations on Array/Hash is deprecated#{deprecation_location_text}.",
+ " See http://links.puppetlabs.com/puppet-mutation-deprecation"].join(''))
+ end
end
class Regex < AST::Leaf
def initialize(hash)
super
@value = Regexp.new(@value) unless @value.is_a?(Regexp)
end
# we're returning self here to wrap the regexp and to be used in places
# where a string would have been used, without modifying any client code.
# For instance, in many places we have the following code snippet:
# val = @val.safeevaluate(@scope)
# if val.match(otherval)
# ...
# end
# this way, we don't have to modify this test specifically for handling
# regexes.
def evaluate(scope)
self
end
def evaluate_match(value, scope, options = {})
value = value == :undef ? '' : value.to_s
if matched = @value.match(value)
scope.ephemeral_from(matched, options[:file], options[:line])
end
matched
end
def match(value)
@value.match(value)
end
def to_s
"/#{@value.source}/"
end
end
end
diff --git a/lib/puppet/parser/ast/pops_bridge.rb b/lib/puppet/parser/ast/pops_bridge.rb
new file mode 100644
index 000000000..4e57137a6
--- /dev/null
+++ b/lib/puppet/parser/ast/pops_bridge.rb
@@ -0,0 +1,168 @@
+require 'puppet/parser/ast/top_level_construct'
+require 'puppet/pops'
+
+# The AST::Bridge contains classes that bridges between the new Pops based model
+# and the 3.x AST. This is required to be able to reuse the Puppet::Resource::Type which is
+# fundamental for the rest of the logic.
+#
+class Puppet::Parser::AST::PopsBridge
+
+ # Bridges to one Pops Model Expression
+ # The @value is the expression
+ # This is used to represent the body of a class, definition, or node, and for each parameter's default value
+ # expression.
+ #
+ class Expression < Puppet::Parser::AST::Leaf
+
+ def initialize args
+ super
+ @@evaluator ||= Puppet::Pops::Parser::EvaluatingParser::Transitional.new()
+ end
+
+ def to_s
+ Puppet::Pops::Model::ModelTreeDumper.new.dump(@value)
+ end
+
+ def evaluate(scope)
+ @@evaluator.evaluate(scope, @value)
+ end
+
+ # Adapts to 3x where top level constructs needs to have each to iterate over children. Short circuit this
+ # by yielding self. By adding this there is no need to wrap a pops expression inside an AST::BlockExpression
+ #
+ def each
+ yield self
+ end
+
+ def sequence_with(other)
+ if value.nil?
+ # This happens when testing and not having a complete setup
+ other
+ else
+ # When does this happen ? Ever ?
+ raise "sequence_with called on Puppet::Parser::AST::PopsBridge::Expression - please report use case"
+ # What should be done if the above happens (We don't want this to happen).
+ # Puppet::Parser::AST::BlockExpression.new(:children => [self] + other.children)
+ end
+ end
+
+ # The 3x requires code plugged in to an AST to have this in certain positions in the tree. The purpose
+ # is to either print the content, or to look for things that needs to be defined. This implementation
+ # cheats by always returning an empty array. (This allows simple files to not require a "Program" at the top.
+ #
+ def children
+ []
+ end
+ end
+
+ # Bridges the top level "Program" produced by the pops parser.
+ # Its main purpose is to give one point where all definitions are instantiated (actually defined since the
+ # Puppet 3x terminology is somewhat misleading - the definitions are instantiated, but instances of the created types
+ # are not created, that happens when classes are included / required, nodes are matched and when resources are instantiated
+ # by a resource expression (which is also used to instantiate a host class).
+ #
+ class Program < Puppet::Parser::AST::TopLevelConstruct
+ attr_reader :program_model, :context
+
+ def initialize(program_model, context = {})
+ @program_model = program_model
+ @context = context
+ @ast_transformer ||= Puppet::Pops::Model::AstTransformer.new(@context[:file])
+ @@evaluator ||= Puppet::Pops::Parser::EvaluatingParser::Transitional.new()
+ end
+
+ # This is the 3x API, the 3x AST searches through all code to find the instructions that can be instantiated.
+ # This Pops-model based instantiation relies on the parser to build this list while parsing (which is more
+ # efficient as it avoids one full scan of all logic via recursive enumeration/yield)
+ #
+ def instantiate(modname)
+ @program_model.definitions.collect do |d|
+ case d
+ when Puppet::Pops::Model::HostClassDefinition
+ instantiate_HostClassDefinition(d, modname)
+ when Puppet::Pops::Model::ResourceTypeDefinition
+ instantiate_ResourceTypeDefinition(d, modname)
+ when Puppet::Pops::Model::NodeDefinition
+ instantiate_NodeDefinition(d, modname)
+ else
+ raise Puppet::ParseError("Internal Error: Unknown type of definition - got '#{d.class}'")
+ end
+ end.flatten() # flatten since node definition may have returned an array
+ end
+
+ def evaluate(scope)
+ @@evaluator.evaluate(scope, program_model)
+ end
+
+ # Adapts to 3x where top level constructs needs to have each to iterate over children. Short circuit this
+ # by yielding self. This means that the HostClass container will call this bridge instance with `instantiate`.
+ #
+ def each
+ yield self
+ end
+
+ private
+
+ def instantiate_Parameter(o)
+ # 3x needs parameters as an array of `[name]` or `[name, value_expr]`
+ # One problem is that the parameter evaluation takes place in the wrong context in 3x (the caller's and
+ # can thus reference all sorts of information. Here the value expression is wrapped in an AST Bridge to a Pops
+ # expression since the Pops side can not control the evaluation
+ if o.value
+ [ o.name, Expression.new(:value => o.value) ]
+ else
+ [ o.name ]
+ end
+ end
+
+ # Produces a hash with data for Definition and HostClass
+ def args_from_definition(o, modname)
+ args = {
+ :arguments => o.parameters.collect {|p| instantiate_Parameter(p) },
+ :module_name => modname
+ }
+ unless is_nop?(o.body)
+ args[:code] = Expression.new(:value => o.body)
+ end
+ @ast_transformer.merge_location(args, o)
+ end
+
+ def instantiate_HostClassDefinition(o, modname)
+ args = args_from_definition(o, modname)
+ args[:parent] = o.parent_class
+ Puppet::Resource::Type.new(:hostclass, o.name, @context.merge(args))
+ end
+
+ def instantiate_ResourceTypeDefinition(o, modname)
+ Puppet::Resource::Type.new(:definition, o.name, @context.merge(args_from_definition(o, modname)))
+ end
+
+ def instantiate_NodeDefinition(o, modname)
+ args = { :module_name => modname }
+
+ unless is_nop?(o.body)
+ args[:code] = Expression.new(:value => o.body)
+ end
+
+ unless is_nop?(o.parent)
+ args[:parent] = @ast_transformer.hostname(o.parent)
+ end
+
+ host_matches = @ast_transformer.hostname(o.host_matches)
+ @ast_transformer.merge_location(args, o)
+ host_matches.collect do |name|
+ Puppet::Resource::Type.new(:node, name, @context.merge(args))
+ end
+ end
+
+ def code()
+ Expression.new(:value => @value)
+ end
+
+ def is_nop?(o)
+ @ast_transformer.is_nop?(o)
+ end
+
+ end
+
+end
diff --git a/lib/puppet/parser/code_merger.rb b/lib/puppet/parser/code_merger.rb
new file mode 100644
index 000000000..e8913b88b
--- /dev/null
+++ b/lib/puppet/parser/code_merger.rb
@@ -0,0 +1,13 @@
+
+class Puppet::Parser::CodeMerger
+
+ # Concatenates the logic in the array of parse results into one parse result
+ # @return Puppet::Parser::AST::BlockExpression
+ #
+ def concatenate(parse_results)
+ children = parse_results.select {|x| !x.nil? && x.code}.reduce([]) do |memo, parsed_class|
+ memo + parsed_class.code.children
+ end
+ Puppet::Parser::AST::BlockExpression.new(:children => children)
+ end
+end
diff --git a/lib/puppet/parser/collector.rb b/lib/puppet/parser/collector.rb
index 07c462b7f..a5f4cb0ff 100644
--- a/lib/puppet/parser/collector.rb
+++ b/lib/puppet/parser/collector.rb
@@ -1,177 +1,177 @@
# An object that collects stored objects from the central cache and returns
# them to the current host, yo.
class Puppet::Parser::Collector
attr_accessor :type, :scope, :vquery, :equery, :form
attr_accessor :resources, :overrides, :collected
# Call the collection method, mark all of the returned objects as
# non-virtual, optionally applying parameter overrides. The collector can
# also delete himself from the compiler if there is no more resources to
# collect (valid only for resource fixed-set collector which get their
# resources from +collect_resources+ and not from the catalog)
def evaluate
# Shortcut if we're not using storeconfigs and they're trying to collect
# exported resources.
if form == :exported and Puppet[:storeconfigs] != true
Puppet.warning "Not collecting exported resources without storeconfigs"
return false
end
if self.resources
unless objects = collect_resources and ! objects.empty?
return false
end
else
method = "collect_#{@form.to_s}"
objects = send(method).each do |obj|
obj.virtual = false
end
return false if objects.empty?
end
# we have an override for the collected resources
if @overrides and !objects.empty?
# force the resource to be always child of any other resource
overrides[:source].meta_def(:child_of?) do |klass|
true
end
# tell the compiler we have some override for him unless we already
# overrided those resources
objects.each do |res|
unless @collected.include?(res.ref)
newres = Puppet::Parser::Resource.
new(res.type, res.title,
:parameters => overrides[:parameters],
:file => overrides[:file],
:line => overrides[:line],
:source => overrides[:source],
:scope => overrides[:scope])
scope.compiler.add_override(newres)
end
end
end
# filter out object that this collector has previously found.
objects.reject! { |o| @collected.include?(o.ref) }
return false if objects.empty?
# keep an eye on the resources we have collected
objects.inject(@collected) { |c,o| c[o.ref]=o; c }
# return our newly collected resources
objects
end
def initialize(scope, type, equery, vquery, form)
@scope = scope
@vquery = vquery
@equery = equery
# initialisation
@collected = {}
# Canonize the type
@type = Puppet::Resource.new(type, "whatever").type
unless [:exported, :virtual].include?(form)
raise ArgumentError, "Invalid query form #{form}"
end
@form = form
end
# add a resource override to the soon to be exported/realized resources
def add_override(hash)
raise ArgumentError, "Exported resource try to override without parameters" unless hash[:parameters]
# schedule an override for an upcoming collection
@overrides = hash
end
private
# Collect exported objects.
def collect_exported
resources = []
time = Puppet::Util.thinmark do
# First get everything from the export table. Just reuse our
# collect_virtual method but tell it to use 'exported? for the test.
resources = collect_virtual(true).reject { |r| ! r.virtual? }
# key is '#{type}/#{name}', and host and filter.
found = Puppet::Resource.indirection.
- search(@type, :host => @scope.host, :filter => @equery, :scope => @scope)
+ search(@type, :host => @scope.compiler.node.name, :filter => @equery, :scope => @scope)
found_resources = found.map {|x| x.is_a?(Puppet::Parser::Resource) ? x : x.to_resource(@scope)}
found_resources.each do |item|
if existing = @scope.findresource(item.type, item.title)
unless existing.collector_id == item.collector_id
# unless this is the one we've already collected
raise Puppet::ParseError,
"Another local or imported resource exists with the type and title #{item.ref}"
end
else
item.exported = false
@scope.compiler.add_resource(@scope, item)
resources << item
end
end
end
scope.debug("Collected %s %s resource%s in %.2f seconds" %
[resources.length, @type, resources.length == 1 ? "" : "s", time])
resources
end
def collect_resources
@resources = [@resources] unless @resources.is_a?(Array)
method = "collect_#{form.to_s}_resources"
send(method)
end
def collect_exported_resources
raise Puppet::ParseError, "realize() is not yet implemented for exported resources"
end
# Collect resources directly; this is the result of using 'realize',
# which specifies resources, rather than using a normal collection.
def collect_virtual_resources
return [] unless defined?(@resources) and ! @resources.empty?
result = @resources.dup.collect do |ref|
if res = @scope.findresource(ref.to_s)
@resources.delete(ref)
res
end
end.reject { |r| r.nil? }.each do |res|
res.virtual = false
end
# If there are no more resources to find, delete this from the list
# of collections.
@scope.compiler.delete_collection(self) if @resources.empty?
result
end
# Collect just virtual objects, from our local compiler.
def collect_virtual(exported = false)
scope.compiler.resources.find_all do |resource|
resource.type == @type and (exported ? resource.exported? : true) and match?(resource)
end
end
# Does the resource match our tests? We don't yet support tests,
# so it's always true at the moment.
def match?(resource)
if self.vquery
return self.vquery.call(resource)
else
return true
end
end
end
diff --git a/lib/puppet/parser/compiler.rb b/lib/puppet/parser/compiler.rb
index 9d6bb0f6f..b85deb94a 100644
--- a/lib/puppet/parser/compiler.rb
+++ b/lib/puppet/parser/compiler.rb
@@ -1,557 +1,555 @@
require 'forwardable'
require 'puppet/node'
require 'puppet/resource/catalog'
require 'puppet/util/errors'
require 'puppet/resource/type_collection_helper'
# Maintain a graph of scopes, along with a bunch of data
# about the individual catalog we're compiling.
class Puppet::Parser::Compiler
extend Forwardable
include Puppet::Util
include Puppet::Util::Errors
include Puppet::Util::MethodHelper
include Puppet::Resource::TypeCollectionHelper
def self.compile(node)
$env_module_directories = nil
node.environment.check_for_reparse
new(node).compile.to_resource
rescue => detail
message = "#{detail} on node #{node.name}"
Puppet.log_exception(detail, message)
raise Puppet::Error, message, detail.backtrace
end
attr_reader :node, :facts, :collections, :catalog, :resources, :relationships, :topscope
# The injector that provides lookup services, or nil if accessed before the compiler has started compiling and
# bootstrapped. The injector is initialized and available before any manifests are evaluated.
#
# @return [Puppet::Pops::Binder::Injector, nil] The injector that provides lookup services for this compiler/environment
# @api public
#
attr_accessor :injector
# The injector that provides lookup services during the creation of the {#injector}.
# @return [Puppet::Pops::Binder::Injector, nil] The injector that provides lookup services during injector creation
# for this compiler/environment
#
# @api private
#
attr_accessor :boot_injector
# Add a collection to the global list.
def_delegator :@collections, :<<, :add_collection
def_delegator :@relationships, :<<, :add_relationship
# Store a resource override.
def add_override(override)
# If possible, merge the override in immediately.
if resource = @catalog.resource(override.ref)
resource.merge(override)
else
# Otherwise, store the override for later; these
# get evaluated in Resource#finish.
@resource_overrides[override.ref] << override
end
end
def add_resource(scope, resource)
@resources << resource
# Note that this will fail if the resource is not unique.
@catalog.add_resource(resource)
if not resource.class? and resource[:stage]
raise ArgumentError, "Only classes can set 'stage'; normal resources like #{resource} cannot change run stage"
end
# Stages should not be inside of classes. They are always a
# top-level container, regardless of where they appear in the
# manifest.
return if resource.stage?
# This adds a resource to the class it lexically appears in in the
# manifest.
unless resource.class?
return @catalog.add_edge(scope.resource, resource)
end
end
# Do we use nodes found in the code, vs. the external node sources?
def_delegator :known_resource_types, :nodes?, :ast_nodes?
# Store the fact that we've evaluated a class
def add_class(name)
@catalog.add_class(name) unless name == ""
end
# Return a list of all of the defined classes.
def_delegator :@catalog, :classes, :classlist
# Compiler our catalog. This mostly revolves around finding and evaluating classes.
# This is the main entry into our catalog.
def compile
- # Set the client's parameters into the top scope.
- Puppet::Util::Profiler.profile("Compile: Set node parameters") { set_node_parameters }
+ Puppet.override({ :current_environment => environment }, "For compiling #{node.name}") do
+ # Set the client's parameters into the top scope.
+ Puppet::Util::Profiler.profile("Compile: Set node parameters") { set_node_parameters }
- Puppet::Util::Profiler.profile("Compile: Created settings scope") { create_settings_scope }
+ Puppet::Util::Profiler.profile("Compile: Created settings scope") { create_settings_scope }
- if is_binder_active?
- Puppet::Util::Profiler.profile("Compile: Created injector") { create_injector }
- end
+ if is_binder_active?
+ Puppet::Util::Profiler.profile("Compile: Created injector") { create_injector }
+ end
- Puppet::Util::Profiler.profile("Compile: Evaluated main") { evaluate_main }
+ Puppet::Util::Profiler.profile("Compile: Evaluated main") { evaluate_main }
- Puppet::Util::Profiler.profile("Compile: Evaluated AST node") { evaluate_ast_node }
+ Puppet::Util::Profiler.profile("Compile: Evaluated AST node") { evaluate_ast_node }
- Puppet::Util::Profiler.profile("Compile: Evaluated node classes") { evaluate_node_classes }
+ Puppet::Util::Profiler.profile("Compile: Evaluated node classes") { evaluate_node_classes }
- Puppet::Util::Profiler.profile("Compile: Evaluated generators") { evaluate_generators }
+ Puppet::Util::Profiler.profile("Compile: Evaluated generators") { evaluate_generators }
- Puppet::Util::Profiler.profile("Compile: Finished catalog") { finish }
+ Puppet::Util::Profiler.profile("Compile: Finished catalog") { finish }
- fail_on_unevaluated
+ fail_on_unevaluated
- @catalog
+ @catalog
+ end
end
def_delegator :@collections, :delete, :delete_collection
# Return the node's environment.
def environment
- unless defined?(@environment)
- unless node.environment.is_a? Puppet::Node::Environment
- raise Puppet::DevError, "node #{node} has an invalid environment!"
- end
- @environment = node.environment
+ unless node.environment.is_a? Puppet::Node::Environment
+ raise Puppet::DevError, "node #{node} has an invalid environment!"
end
- Puppet::Node::Environment.current = @environment
- @environment
+ node.environment
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
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
# find, raise an error. This method really just creates resource objects
# that point back to the classes, and then the resources are themselves
# evaluated later in the process.
#
# Sometimes we evaluate classes with a fully qualified name already, in which
# case, we tell scope.find_hostclass we've pre-qualified the name so it
# doesn't need to search its namespaces again. This gets around a weird
# edge case of duplicate class names, one at top scope and one nested in our
# namespace and the wrong one (or both!) getting selected. See ticket #13349
# for more detail. --jeffweiss 26 apr 2012
def evaluate_classes(classes, scope, lazy_evaluate = true, fqname = false)
raise Puppet::DevError, "No source for scope passed to evaluate_classes" unless scope.source
class_parameters = nil
# if we are a param class, save the classes hash
# and transform classes to be the keys
if classes.class == Hash
class_parameters = classes
classes = classes.keys
end
classes.each do |name|
# If we can find the class, then make a resource that will evaluate it.
if klass = scope.find_hostclass(name, :assume_fqname => fqname)
# If parameters are passed, then attempt to create a duplicate resource
# so the appropriate error is thrown.
if class_parameters
resource = klass.ensure_in_catalog(scope, class_parameters[name] || {})
else
next if scope.class_scope(klass)
resource = klass.ensure_in_catalog(scope)
end
# If they've disabled lazy evaluation (which the :include function does),
# then evaluate our resource immediately.
resource.evaluate unless lazy_evaluate
else
raise Puppet::Error, "Could not find class #{name} for #{node.name}"
end
end
end
def evaluate_relationships
@relationships.each { |rel| rel.evaluate(catalog) }
end
# Return a resource by either its ref or its type and title.
def_delegator :@catalog, :resource, :findresource
def initialize(node, options = {})
@node = node
set_options(options)
initvars
end
# Create a new scope, with either a specified parent scope or
# using the top scope.
def newscope(parent, options = {})
parent ||= topscope
scope = Puppet::Parser::Scope.new(self, options)
scope.parent = parent
scope
end
# Return any overrides for the given resource.
def resource_overrides(resource)
@resource_overrides[resource.ref]
end
def injector
create_injector if @injector.nil?
@injector
end
def boot_injector
create_boot_injector(nil) if @boot_injector.nil?
@boot_injector
end
# Creates the boot injector from registered system, default, and injector config.
# @return [Puppet::Pops::Binder::Injector] the created boot injector
# @api private Cannot be 'private' since it is called from the BindingsComposer.
#
def create_boot_injector(env_boot_bindings)
assert_binder_active()
- boot_contribution = Puppet::Pops::Binder::SystemBindings.injector_boot_contribution(env_boot_bindings)
- final_contribution = Puppet::Pops::Binder::SystemBindings.final_contribution
- binder = Puppet::Pops::Binder::Binder.new()
- binder.define_categories(boot_contribution.effective_categories)
- binder.define_layers(Puppet::Pops::Binder::BindingsFactory.layered_bindings(final_contribution, boot_contribution))
- @boot_injector = Puppet::Pops::Binder::Injector.new(binder)
+ pb = Puppet::Pops::Binder
+ boot_contribution = pb::SystemBindings.injector_boot_contribution(env_boot_bindings)
+ final_contribution = pb::SystemBindings.final_contribution
+ binder = pb::Binder.new(pb::BindingsFactory.layered_bindings(final_contribution, boot_contribution))
+ @boot_injector = pb::Injector.new(binder)
end
# Answers if Puppet Binder should be active or not, and if it should and is not active, then it is activated.
# @return [Boolean] true if the Puppet Binder should be activated
def is_binder_active?
should_be_active = Puppet[:binder] || Puppet[:parser] == 'future'
if should_be_active
# TODO: this should be in a central place, not just for ParserFactory anymore...
Puppet::Parser::ParserFactory.assert_rgen_installed()
@@binder_loaded ||= false
unless @@binder_loaded
require 'puppet/pops'
require 'puppetx'
@@binder_loaded = true
end
end
should_be_active
end
private
# If ast nodes are enabled, then see if we can find and evaluate one.
def evaluate_ast_node
return unless ast_nodes?
# Now see if we can find the node.
astnode = nil
@node.names.each do |name|
break if astnode = known_resource_types.node(name.to_s.downcase)
end
unless (astnode ||= known_resource_types.node("default"))
raise Puppet::ParseError, "Could not find default node or by name with '#{node.names.join(", ")}'"
end
# Create a resource to model this node, and then add it to the list
# of resources.
resource = astnode.ensure_in_catalog(topscope)
resource.evaluate
@node_scope = topscope.class_scope(astnode)
end
# Evaluate our collections and return true if anything returned an object.
# The 'true' is used to continue a loop, so it's important.
def evaluate_collections
return false if @collections.empty?
exceptwrap do
# We have to iterate over a dup of the array because
# collections can delete themselves from the list, which
# changes its length and causes some collections to get missed.
Puppet::Util::Profiler.profile("Evaluated collections") do
found_something = false
@collections.dup.each do |collection|
found_something = true if collection.evaluate
end
found_something
end
end
end
# Make sure all of our resources have been evaluated into native resources.
# We return true if any resources have, so that we know to continue the
# evaluate_generators loop.
def evaluate_definitions
exceptwrap do
Puppet::Util::Profiler.profile("Evaluated definitions") do
!unevaluated_resources.each do |resource|
Puppet::Util::Profiler.profile("Evaluated resource #{resource}") do
resource.evaluate
end
end.empty?
end
end
end
# Iterate over collections and resources until we're sure that the whole
# compile is evaluated. This is necessary because both collections
# and defined resources can generate new resources, which themselves could
# be defined resources.
def evaluate_generators
count = 0
loop do
done = true
Puppet::Util::Profiler.profile("Iterated (#{count + 1}) on generators") do
# Call collections first, then definitions.
done = false if evaluate_collections
done = false if evaluate_definitions
end
break if done
count += 1
if count > 1000
raise Puppet::ParseError, "Somehow looped more than 1000 times while evaluating host catalog"
end
end
end
# Find and evaluate our main object, if possible.
def evaluate_main
@main = known_resource_types.find_hostclass([""], "") || known_resource_types.add(Puppet::Resource::Type.new(:hostclass, ""))
@topscope.source = @main
@main_resource = Puppet::Parser::Resource.new("class", :main, :scope => @topscope, :source => @main)
@topscope.resource = @main_resource
add_resource(@topscope, @main_resource)
@main_resource.evaluate
end
# Make sure the entire catalog is evaluated.
def fail_on_unevaluated
fail_on_unevaluated_overrides
fail_on_unevaluated_resource_collections
end
# If there are any resource overrides remaining, then we could
# not find the resource they were supposed to override, so we
# want to throw an exception.
def fail_on_unevaluated_overrides
remaining = @resource_overrides.values.flatten.collect(&:ref)
if !remaining.empty?
fail Puppet::ParseError,
"Could not find resource(s) #{remaining.join(', ')} for overriding"
end
end
# Make sure we don't have any remaining collections that specifically
# look for resources, because we want to consider those to be
# parse errors.
def fail_on_unevaluated_resource_collections
remaining = @collections.collect(&:resources).flatten.compact
if !remaining.empty?
raise Puppet::ParseError, "Failed to realize virtual resources #{remaining.join(', ')}"
end
end
# Make sure all of our resources and such have done any last work
# necessary.
def finish
evaluate_relationships
resources.each do |resource|
# Add in any resource overrides.
if overrides = resource_overrides(resource)
overrides.each do |over|
resource.merge(over)
end
# Remove the overrides, so that the configuration knows there
# are none left.
overrides.clear
end
resource.finish if resource.respond_to?(:finish)
end
add_resource_metaparams
end
def add_resource_metaparams
unless main = catalog.resource(:class, :main)
raise "Couldn't find main"
end
names = Puppet::Type.metaparams.select do |name|
!Puppet::Parser::Resource.relationship_parameter?(name)
end
data = {}
catalog.walk(main, :out) do |source, target|
if source_data = data[source] || metaparams_as_data(source, names)
# only store anything in the data hash if we've actually got
# data
data[source] ||= source_data
source_data.each do |param, value|
target[param] = value if target[param].nil?
end
data[target] = source_data.merge(metaparams_as_data(target, names))
end
target.tag(*(source.tags))
end
end
def metaparams_as_data(resource, params)
data = nil
params.each do |param|
unless resource[param].nil?
# Because we could be creating a hash for every resource,
# and we actually probably don't often have any data here at all,
# we're optimizing a bit by only creating a hash if there's
# any data to put in it.
data ||= {}
data[param] = resource[param]
end
end
data
end
# Set up all of our internal variables.
def initvars
# The list of overrides. This is used to cache overrides on objects
# that don't exist yet. We store an array of each override.
@resource_overrides = Hash.new do |overs, ref|
overs[ref] = []
end
# The list of collections that have been created. This is a global list,
# but they each refer back to the scope that created them.
@collections = []
# The list of relationships to evaluate.
@relationships = []
# For maintaining the relationship between scopes and their resources.
@catalog = Puppet::Resource::Catalog.new(@node.name)
@catalog.version = known_resource_types.version
@catalog.environment = @node.environment.to_s
# Create our initial scope and a resource that will evaluate main.
@topscope = Puppet::Parser::Scope.new(self)
@catalog.add_resource(Puppet::Parser::Resource.new("stage", :main, :scope => @topscope))
# local resource array to maintain resource ordering
@resources = []
# Make sure any external node classes are in our class list
if @node.classes.class == Hash
@catalog.add_class(*@node.classes.keys)
else
@catalog.add_class(*@node.classes)
end
end
# Set the node's parameters into the top-scope as variables.
def set_node_parameters
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
+ if(Puppet[:immutable_node_data])
+ facts_hash = node.facts.nil? ? {} : node.facts.values
+ @topscope.set_facts(facts_hash)
+ end
end
def create_settings_scope
unless settings_type = environment.known_resource_types.hostclass("settings")
settings_type = Puppet::Resource::Type.new :hostclass, "settings"
environment.known_resource_types.add(settings_type)
end
settings_resource = Puppet::Parser::Resource.new("class", "settings", :scope => @topscope)
@catalog.add_resource(settings_resource)
settings_type.evaluate_code(settings_resource)
scope = @topscope.class_scope(settings_type)
Puppet.settings.each do |name, setting|
next if name.to_s == "name"
scope[name.to_s] = environment[name]
end
end
# Return an array of all of the unevaluated resources. These will be definitions,
# which need to get evaluated into native resources.
def unevaluated_resources
# The order of these is significant for speed due to short-circuting
resources.reject { |resource| resource.evaluated? or resource.virtual? or resource.builtin_type? }
end
# Creates the injector from bindings found in the current environment.
# @return [void]
# @api private
#
def create_injector
assert_binder_active()
composer = Puppet::Pops::Binder::BindingsComposer.new()
layered_bindings = composer.compose(topscope)
- binder = Puppet::Pops::Binder::Binder.new()
- binder.define_categories(composer.effective_categories(topscope))
- binder.define_layers(layered_bindings)
- @injector = Puppet::Pops::Binder::Injector.new(binder)
+ @injector = Puppet::Pops::Binder::Injector.new(Puppet::Pops::Binder::Binder.new(layered_bindings))
end
def assert_binder_active
unless is_binder_active?
raise ArgumentError, "The Puppet Binder is only available when either '--binder true' or '--parser future' is used"
end
end
end
diff --git a/lib/puppet/parser/e4_parser_adapter.rb b/lib/puppet/parser/e4_parser_adapter.rb
new file mode 100644
index 000000000..01822db13
--- /dev/null
+++ b/lib/puppet/parser/e4_parser_adapter.rb
@@ -0,0 +1,81 @@
+require 'puppet/pops'
+
+module Puppet; module Parser; end; end;
+# Adapts an egrammar/eparser to respond to the public API of the classic parser
+# and makes use of the new evaluator.
+#
+class Puppet::Parser::E4ParserAdapter
+
+ # Empty adapter fulfills watch_file contract without doing anything.
+ # @api private
+ class NullFileWatcher
+ def watch_file(file)
+ #nop
+ end
+ end
+
+ # @param file_watcher [#watch_file] something that can watch a file
+ def initialize(file_watcher = nil)
+ @file_watcher = file_watcher || NullFileWatcher.new
+ @file = ''
+ @string = ''
+ @use = :undefined
+ @@evaluating_parser ||= Puppet::Pops::Parser::EvaluatingParser::Transitional.new()
+ end
+
+ def file=(file)
+ @file = file
+ @use = :file
+ # watch if possible, but only if the file is something worth watching
+ @file_watcher.watch_file(file) if !file.nil? && file != ''
+ end
+
+ def parse(string = nil)
+ self.string= string if string
+
+ if @file =~ /\.rb$/ && @use != :string
+ # Will throw an error
+ parse_ruby_file
+ end
+
+ parse_result =
+ if @use == :string
+ # Parse with a source_file to set in created AST objects (it was either given, or it may be unknown
+ # if caller did not set a file and the present a string.
+ #
+ @@evaluating_parser.parse_string(@string, @file || "unknown-source-location")
+ else
+ @@evaluating_parser.parse_file(@file)
+ end
+
+ # the parse_result may be
+ # * empty / nil (no input)
+ # * a Model::Program
+ # * a Model::Expression
+ #
+ model = parse_result.nil? ? nil : parse_result.current
+ args = {}
+ Puppet::Pops::Model::AstTransformer.new(@file).merge_location(args, model)
+
+ ast_code =
+ if model.is_a? Puppet::Pops::Model::Program
+ Puppet::Parser::AST::PopsBridge::Program.new(model, args)
+ else
+ args[:value] = model
+ Puppet::Parser::AST::PopsBridge::Expression.new(args)
+ end
+
+ # Create the "main" class for the content - this content will get merged with all other "main" content
+ Puppet::Parser::AST::Hostclass.new('', :code => ast_code)
+
+ end
+
+ def string=(string)
+ @string = string
+ @use = :string
+ end
+
+ def parse_ruby_file
+ raise Puppet::ParseError, "Ruby DSL is no longer supported. Attempt to parse #{@file}"
+ end
+end
diff --git a/lib/puppet/parser/e_parser_adapter.rb b/lib/puppet/parser/e_parser_adapter.rb
index beec14752..fe0e28b55 100644
--- a/lib/puppet/parser/e_parser_adapter.rb
+++ b/lib/puppet/parser/e_parser_adapter.rb
@@ -1,120 +1,119 @@
require 'puppet/pops'
module Puppet; module Parser; end; end;
# Adapts an egrammar/eparser to respond to the public API of the classic parser
#
class Puppet::Parser::EParserAdapter
def initialize(classic_parser)
@classic_parser = classic_parser
@file = ''
@string = ''
@use = :undefined
end
def file=(file)
@classic_parser.file = file
@file = file
@use = :file
end
def parse(string = nil)
if @file =~ /\.rb$/
return parse_ruby_file
else
self.string= string if string
parser = Puppet::Pops::Parser::Parser.new()
parse_result = if @use == :string
parser.parse_string(@string)
else
parser.parse_file(@file)
end
# Compute the source_file to set in created AST objects (it was either given, or it may be unknown
# if caller did not set a file and the present a string.
#
source_file = @file || "unknown-source-location"
# Validate
validate(parse_result)
-
# Transform the result, but only if not nil
parse_result = Puppet::Pops::Model::AstTransformer.new(source_file, @classic_parser).transform(parse_result) if parse_result
if parse_result && !parse_result.is_a?(Puppet::Parser::AST::BlockExpression)
# Need to transform again, if result is not wrapped in something iterable when handed off to
# a new Hostclass as its code.
parse_result = Puppet::Parser::AST::BlockExpression.new(:children => [parse_result]) if parse_result
end
end
Puppet::Parser::AST::Hostclass.new('', :code => parse_result)
end
def validate(parse_result)
# TODO: This is too many hoops to jump through... ugly API
# could reference a ValidatorFactory.validator_3_1(acceptor) instead.
# and let the factory abstract the rest.
#
return unless parse_result
acceptor = Puppet::Pops::Validation::Acceptor.new
validator = Puppet::Pops::Validation::ValidatorFactory_3_1.new().validator(acceptor)
validator.validate(parse_result)
max_errors = Puppet[:max_errors]
max_warnings = Puppet[:max_warnings] + 1
max_deprecations = Puppet[:max_deprecations] + 1
# If there are warnings output them
warnings = acceptor.warnings
if warnings.size > 0
formatter = Puppet::Pops::Validation::DiagnosticFormatterPuppetStyle.new
emitted_w = 0
emitted_dw = 0
acceptor.warnings.each {|w|
if w.severity == :deprecation
# Do *not* call Puppet.deprecation_warning it is for internal deprecation, not
# deprecation of constructs in manifests! (It is not designed for that purpose even if
# used throughout the code base).
#
Puppet.warning(formatter.format(w)) if emitted_dw < max_deprecations
emitted_dw += 1
else
Puppet.warning(formatter.format(w)) if emitted_w < max_warnings
emitted_w += 1
end
break if emitted_w > max_warnings && emitted_dw > max_deprecations # but only then
}
end
# If there were errors, report the first found. Use a puppet style formatter.
errors = acceptor.errors
if errors.size > 0
formatter = Puppet::Pops::Validation::DiagnosticFormatterPuppetStyle.new
if errors.size == 1 || max_errors <= 1
# raise immediately
raise Puppet::ParseError.new(formatter.format(errors[0]))
end
emitted = 0
errors.each do |e|
Puppet.err(formatter.format(e))
emitted += 1
break if emitted >= max_errors
end
warnings_message = warnings.size > 0 ? ", and #{warnings.size} warnings" : ""
giving_up_message = "Found #{errors.size} errors#{warnings_message}. Giving up"
exception = Puppet::ParseError.new(giving_up_message)
exception.file = errors[0].file
raise exception
end
end
def string=(string)
@classic_parser.string = string
@string = string
@use = :string
end
def parse_ruby_file
@classic_parser.parse
end
end
diff --git a/lib/puppet/parser/files.rb b/lib/puppet/parser/files.rb
index 49f36019f..605bbeb69 100644
--- a/lib/puppet/parser/files.rb
+++ b/lib/puppet/parser/files.rb
@@ -1,89 +1,94 @@
require 'puppet/module'
module Puppet; module Parser; module Files
module_function
# Return a list of manifests as absolute filenames matching the given
# pattern.
#
# @param pattern [String] A reference for a file in a module. It is the format "<modulename>/<file glob>"
# @param environment [Puppet::Node::Environment] the environment of modules
#
# @return [Array(String, Array<String>)] the module name and the list of files found
# @api private
def find_manifests_in_modules(pattern, environment)
module_name, file_pattern = split_file_path(pattern)
begin
- if mod = Puppet::Module.find(module_name, environment)
+ if mod = environment.module(module_name)
return [mod.name, mod.match_manifests(file_pattern)]
end
rescue Puppet::Module::InvalidName
# one of the modules being loaded might have an invalid name and so
# looking for one might blow up since we load them lazily.
end
[nil, []]
end
# Find the concrete file denoted by +file+. If +file+ is absolute,
# return it directly. Otherwise try to find relative to the +templatedir+
# config param. If that fails try to find it as a template in a
# module.
# In all cases, an absolute path is returned, which does not
# necessarily refer to an existing file
- def find_template(template, environment = nil)
+ #
+ # @api private
+ def find_template(template, environment)
if template == File.expand_path(template)
return template
end
if template_paths = templatepath(environment)
# If we can find the template in :templatedir, we return that.
template_paths.collect { |path|
File::join(path, template)
}.each do |f|
- return f if Puppet::FileSystem::File.exist?(f)
+ return f if Puppet::FileSystem.exist?(f)
end
end
# check in the default template dir, if there is one
if td_file = find_template_in_module(template, environment)
return td_file
end
nil
end
- def find_template_in_module(template, environment = nil)
+ # @api private
+ def find_template_in_module(template, environment)
path, file = split_file_path(template)
# Because templates don't have an assumed template name, like manifests do,
# we treat templates with no name as being templates in the main template
# directory.
return nil unless file
- if mod = Puppet::Module.find(path, environment) and t = mod.template(file)
+ if mod = environment.module(path) and t = mod.template(file)
return t
end
nil
end
# Return an array of paths by splitting the +templatedir+ config
# parameter.
- def templatepath(environment = nil)
- dirs = Puppet.settings.value(:templatedir, environment).split(File::PATH_SEPARATOR)
+ # @api private
+ def templatepath(environment)
+ dirs = Puppet.settings.value(:templatedir, environment.to_s).split(File::PATH_SEPARATOR)
dirs.select do |p|
File::directory?(p)
end
end
# Split the path into the module and the rest of the path, or return
# nil if the path is empty or absolute (starts with a /).
+ # @api private
def split_file_path(path)
if path == "" or Puppet::Util.absolute_path?(path)
nil
else
path.split(File::SEPARATOR, 2)
end
end
end; end; end
diff --git a/lib/puppet/parser/functions.rb b/lib/puppet/parser/functions.rb
index 360387727..d7ea3e0a9 100644
--- a/lib/puppet/parser/functions.rb
+++ b/lib/puppet/parser/functions.rb
@@ -1,242 +1,249 @@
require 'puppet/util/autoload'
require 'puppet/parser/scope'
# A module for managing parser functions. Each specified function
# is added to a central module that then gets included into the Scope
# class.
#
# @api public
module Puppet::Parser::Functions
Environment = Puppet::Node::Environment
class << self
include Puppet::Util
end
# Reset the list of loaded functions.
#
# @api private
def self.reset
@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|
- newfunction(level, :doc => "Log a message on the server at level #{level.to_s}.") do |vals|
+ newfunction(level,
+ :environment => Puppet.lookup(:root_environment),
+ :doc => "Log a message on the server at level #{level.to_s}.") do |vals|
send(level, vals.join(" "))
end
end
end
# Accessor for singleton autoloader
#
# @api private
def self.autoloader
@autoloader ||= Puppet::Util::Autoload.new(
self, "puppet/parser/functions", :wrap => false
)
end
# Get the module that functions are mixed into corresponding to an
# environment
#
# @api private
- def self.environment_module(env = nil)
- if env and ! env.is_a?(Puppet::Node::Environment)
- env = Puppet::Node::Environment.new(env)
- end
- @modules[ (env || Environment.current || Environment.root).name ] ||= Module.new
+ def self.environment_module(env)
+ @modules[env.name] ||= Module.new
end
# Create a new Puppet DSL function.
#
# **The {newfunction} method provides a public API.**
#
# This method is used both internally inside of Puppet to define parser
# functions. For example, template() is defined in
# {file:lib/puppet/parser/functions/template.rb template.rb} using the
# {newfunction} method. Third party Puppet modules such as
# [stdlib](https://forge.puppetlabs.com/puppetlabs/stdlib) use this method to
# extend the behavior and functionality of Puppet.
#
# See also [Docs: Custom
# Functions](http://docs.puppetlabs.com/guides/custom_functions.html)
#
# @example Define a new Puppet DSL Function
# >> Puppet::Parser::Functions.newfunction(:double, :arity => 1,
# :doc => "Doubles an object, typically a number or string.",
# :type => :rvalue) {|i| i[0]*2 }
# => {:arity=>1, :type=>:rvalue,
# :name=>"function_double",
# :doc=>"Doubles an object, typically a number or string."}
#
# @example Invoke the double function from irb as is done in RSpec examples:
- # >> scope = Puppet::Parser::Scope.new_for_test_harness('example')
+ # >> require 'puppet_spec/scope'
+ # >> scope = PuppetSpec::Scope.create_test_scope_for_node('example')
# => Scope()
# >> scope.function_double([2])
# => 4
# >> scope.function_double([4])
# => 8
# >> scope.function_double([])
# ArgumentError: double(): Wrong number of arguments given (0 for 1)
# >> scope.function_double([4,8])
# ArgumentError: double(): Wrong number of arguments given (2 for 1)
# >> scope.function_double(["hello"])
# => "hellohello"
#
# @param [Symbol] name the name of the function represented as a ruby Symbol.
# The {newfunction} method will define a Ruby method based on this name on
# the parser scope instance.
#
# @param [Proc] block the block provided to the {newfunction} method will be
# executed when the Puppet DSL function is evaluated during catalog
# compilation. The arguments to the function will be passed as an array to
# the first argument of the block. The return value of the block will be
# the return value of the Puppet DSL function for `:rvalue` functions.
#
# @option options [:rvalue, :statement] :type (:statement) the type of function.
# Either `:rvalue` for functions that return a value, or `:statement` for
# functions that do not return a value.
#
# @option options [String] :doc ('') the documentation for the function.
# This string will be extracted by documentation generation tools.
#
# @option options [Integer] :arity (-1) the
# [arity](http://en.wikipedia.org/wiki/Arity) of the function. When
# specified as a positive integer the function is expected to receive
# _exactly_ the specified number of arguments. When specified as a
# negative number, the function is expected to receive _at least_ the
# absolute value of the specified number of arguments incremented by one.
# For example, a function with an arity of `-4` is expected to receive at
# minimum 3 arguments. A function with the default arity of `-1` accepts
# zero or more arguments. A function with an arity of 2 must be provided
# with exactly two arguments, no more and no less. Added in Puppet 3.1.0.
#
+ # @option options [Puppet::Node::Environment] :environment (nil) can
+ # explicitly pass the environment we wanted the function added to. Only used
+ # to set logging functions in root environment
+ #
# @return [Hash] describing the function.
#
# @api public
def self.newfunction(name, options = {}, &block)
name = name.intern
+ environment = options[:environment] || Puppet.lookup(:current_environment)
- Puppet.warning "Overwriting previous definition for function #{name}" if get_function(name)
+ Puppet.warning "Overwriting previous definition for function #{name}" if get_function(name, environment)
arity = options[:arity] || -1
ftype = options[:type] || :statement
unless ftype == :statement or ftype == :rvalue
raise Puppet::DevError, "Invalid statement type #{ftype.inspect}"
end
# the block must be installed as a method because it may use "return",
# which is not allowed from procs.
real_fname = "real_function_#{name}"
- environment_module.send(:define_method, real_fname, &block)
+ environment_module(environment).send(:define_method, real_fname, &block)
fname = "function_#{name}"
- environment_module.send(:define_method, fname) do |*args|
+ environment_module(environment).send(:define_method, fname) do |*args|
Puppet::Util::Profiler.profile("Called #{name}") do
if args[0].is_a? Array
if arity >= 0 and args[0].size != arity
raise ArgumentError, "#{name}(): Wrong number of arguments given (#{args[0].size} for #{arity})"
elsif arity < 0 and args[0].size < (arity+1).abs
raise ArgumentError, "#{name}(): Wrong number of arguments given (#{args[0].size} for minimum #{(arity+1).abs})"
end
self.send(real_fname, args[0])
else
raise ArgumentError, "custom functions must be called with a single array that contains the arguments. For example, function_example([1]) instead of function_example(1)"
end
end
end
func = {:arity => arity, :type => ftype, :name => fname}
func[:doc] = options[:doc] if options[:doc]
- add_function(name, func)
+ add_function(name, func, environment)
func
end
# Determine if a function is defined
#
# @param [Symbol] name the function
+ # @param [Puppet::Node::Environment] environment the environment to find the function in
#
# @return [Symbol, false] The name of the function if it's defined,
# otherwise false.
#
# @api public
- def self.function(name)
+ def self.function(name, environment = Puppet.lookup(:current_environment))
name = name.intern
func = nil
- unless func = get_function(name)
- autoloader.load(name, Environment.current)
- func = get_function(name)
+ unless func = get_function(name, environment)
+ autoloader.load(name, environment)
+ func = get_function(name, environment)
end
if func
func[:name]
else
false
end
end
- def self.functiondocs
+ def self.functiondocs(environment = Puppet.lookup(:current_environment))
autoloader.loadall
ret = ""
- merged_functions.sort { |a,b| a[0].to_s <=> b[0].to_s }.each do |name, hash|
+ merged_functions(environment).sort { |a,b| a[0].to_s <=> b[0].to_s }.each do |name, hash|
ret << "#{name}\n#{"-" * name.to_s.length}\n"
if hash[:doc]
ret << Puppet::Util::Docs.scrub(hash[:doc])
else
ret << "Undocumented.\n"
end
ret << "\n\n- *Type*: #{hash[:type]}\n\n"
end
ret
end
# Determine whether a given function returns a value.
#
# @param [Symbol] name the function
+ # @param [Puppet::Node::Environment] environment The environment to find the function in
+ # @return [Boolean] whether it is an rvalue function
#
# @api public
- def self.rvalue?(name)
- func = get_function(name)
+ def self.rvalue?(name, environment = Puppet.lookup(:current_environment))
+ func = get_function(name, environment)
func ? func[:type] == :rvalue : false
end
# Return the number of arguments a function expects.
#
# @param [Symbol] name the function
+ # @param [Puppet::Node::Environment] environment The environment to find the function in
# @return [Integer] The arity of the function. See {newfunction} for
# the meaning of negative values.
#
# @api public
- def self.arity(name)
- func = get_function(name)
+ def self.arity(name, environment = Puppet.lookup(:current_environment))
+ func = get_function(name, environment)
func ? func[:arity] : -1
end
class << self
private
- def merged_functions
- @functions[Environment.root].merge(@functions[Environment.current])
+ def merged_functions(environment)
+ @functions[Puppet.lookup(:root_environment)].merge(@functions[environment])
end
- def get_function(name)
+ def get_function(name, environment)
name = name.intern
- merged_functions[name]
+ merged_functions(environment)[name]
end
- def add_function(name, func)
+ def add_function(name, func, environment)
name = name.intern
- @functions[Environment.current][name] = func
+ @functions[environment][name] = func
end
end
-
- reset # initialize the class instance variables
end
diff --git a/lib/puppet/parser/functions/collect.rb b/lib/puppet/parser/functions/collect.rb
index e30a80bb1..fea42a4df 100644
--- a/lib/puppet/parser/functions/collect.rb
+++ b/lib/puppet/parser/functions/collect.rb
@@ -1,15 +1,15 @@
Puppet::Parser::Functions::newfunction(
:collect,
:type => :rvalue,
:arity => 2,
:doc => <<-'ENDHEREDOC') do |args|
The 'collect' function has been renamed to 'map'. Please update your manifests.
The collect function is reserved for future use.
- Removed as of 3.4
- requires `parser = future`.
ENDHEREDOC
raise NotImplementedError,
"The 'collect' function has been renamed to 'map'. Please update your manifests."
-end
\ No newline at end of file
+end
diff --git a/lib/puppet/parser/functions/defined.rb b/lib/puppet/parser/functions/defined.rb
index 314cc45a0..a1484fcad 100644
--- a/lib/puppet/parser/functions/defined.rb
+++ b/lib/puppet/parser/functions/defined.rb
@@ -1,49 +1,73 @@
# Test whether a given class or definition is defined
Puppet::Parser::Functions::newfunction(:defined, :type => :rvalue, :arity => -2, :doc => "Determine whether
a given class or resource type is defined. This function can also determine whether a
- specific resource has been declared. Returns true or false. Accepts class names,
- type names, and resource references.
+ specific resource has been declared, or whether a variable has been assigned a value
+ (including undef...as opposed to never having been assigned anything). Returns true
+ or false. Accepts class names, type names, resource references, and variable
+ reference strings of the form '$name'. When more than one argument is
+ supplied, defined() returns true if any are defined.
The `defined` function checks both native and defined types, including types
provided as plugins via modules. Types and classes are both checked using their names:
defined(\"file\")
defined(\"customtype\")
defined(\"foo\")
defined(\"foo::bar\")
+ defined(\'$name\')
Resource declarations are checked using resource references, e.g.
`defined( File['/tmp/myfile'] )`. Checking whether a given resource
has been declared is, unfortunately, dependent on the parse order of
the configuration, and the following code will not work:
if defined(File['/tmp/foo']) {
- notify(\"This configuration includes the /tmp/foo file.\")
+ notify { \"This configuration includes the /tmp/foo file.\":}
}
- file {\"/tmp/foo\":
+ file { \"/tmp/foo\":
ensure => present,
}
However, this order requirement refers to parse order only, and ordering of
resources in the configuration graph (e.g. with `before` or `require`) does not
- affect the behavior of `defined`.") do |vals|
- result = false
+ affect the behavior of `defined`.
+
+ If the future parser is in effect, you may also search using types:
+
+ defined(Resource[\'file\',\'/some/file\'])
+ defined(File[\'/some/file\'])
+ defined(Class[\'foo\'])
+
+ - Since 2.7.0
+ - Since 3.6.0 variable reference and future parser types") do |vals|
vals = [vals] unless vals.is_a?(Array)
- vals.each do |val|
+ vals.any? do |val|
case val
when String
- if Puppet::Type.type(val) or find_definition(val) or find_hostclass(val)
- result = true
- break
+ if m = /^\$(.+)$/.match(val)
+ exist?(m[1])
+ else
+ find_resource_type(val) or find_definition(val) or find_hostclass(val)
end
when Puppet::Resource
- if findresource(val.to_s)
- result = true
- break
- end
+ compiler.findresource(val.type, val.title)
else
- raise ArgumentError, "Invalid argument of type '#{val.class}' to 'defined'"
+ if Puppet[:parser] == 'future'
+ case val
+ when Puppet::Pops::Types::PResourceType
+ raise ArgumentError, "The given resource type is a reference to all kind of types" if val.type_name.nil?
+ if val.title.nil?
+ find_builtin_resource_type(val.type_name) || find_definition(val.type_name)
+ else
+ compiler.findresource(val.type_name, val.title)
+ end
+ when Puppet::Pops::Types::PHostClassType
+ raise ArgumentError, "The given class type is a reference to all classes" if val.class_name.nil?
+ find_hostclass(val.class_name)
+ end
+ else
+ raise ArgumentError, "Invalid argument of type '#{val.class}' to 'defined'"
+ end
end
end
- result
end
diff --git a/lib/puppet/parser/functions/each.rb b/lib/puppet/parser/functions/each.rb
index 31cf67b2b..39d26ba38 100644
--- a/lib/puppet/parser/functions/each.rb
+++ b/lib/puppet/parser/functions/each.rb
@@ -1,95 +1,109 @@
Puppet::Parser::Functions::newfunction(
:each,
: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
+ This function takes two mandatory arguments: the first should be an Array or a Hash or something that is
+ of enumerable type (integer, Integer range, or String), and the second
a parameterized block as produced by the puppet syntax:
$a.each |$x| { ... }
+ each($a) |$x| { ... }
- When the first argument is an Array, the parameterized block should define one or two block parameters.
+ When the first argument is an Array (or of enumerable type other than Hash), 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.each |$index, $value| { ... }
+ each($a) |$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.each |$entry| { ..."key ${$entry[0]}, value ${$entry[1]}" }
$a.each |$key, $value| { ..."key ${key}, value ${value}" }
- - Since 3.2
+ *Examples*
+
+ [1,2,3].each |$val| { ... } # 1, 2, 3
+ [5,6,7].each |$index, $val| { ... } # (0, 5), (1, 6), (2, 7)
+ {a=>1, b=>2, c=>3}].each |$val| { ... } # ['a', 1], ['b', 2], ['c', 3]
+ {a=>1, b=>2, c=>3}.each |$key, $val| { ... } # ('a', 1), ('b', 2), ('c', 3)
+ Integer[ 10, 20 ].each |$index, $value| { ... } # (0, 10), (1, 11) ...
+ "hello".each |$char| { ... } # 'h', 'e', 'l', 'l', 'o'
+ 3.each |$number| { ... } # 0, 1, 2
+
+ - Since 3.2 for Array and Hash
+ - Since 3.5 for other enumerables
- 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
+ def foreach_Hash(o, scope, pblock, serving_size)
+ 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, index, enumerator.next)
- index = index +1
+ pblock.call(scope, *enumerator.next)
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
+ def foreach_Enumerator(enumerator, scope, pblock, serving_size)
+ index = 0
if serving_size == 1
- (o.size).times do
- pblock.call(scope, enumerator.next)
+ begin
+ loop { pblock.call(scope, enumerator.next) }
+ rescue StopIteration
end
else
- (o.size).times do
- pblock.call(scope, *enumerator.next)
+ begin
+ loop do
+ pblock.call(scope, index, enumerator.next)
+ index = index +1
+ end
+ rescue StopIteration
end
end
- o
end
- raise ArgumentError, ("each(): wrong number of arguments (#{args.length}; must be 2)") if args.length != 2
+ raise ArgumentError, ("each(): wrong number of arguments (#{args.length}; expected 2, got #{args.length})") if args.length != 2
receiver = args[0]
pblock = args[1]
- raise ArgumentError, ("each(): wrong argument type (#{args[1].class}; must be a parameterized block.") unless pblock.is_a? Puppet::Parser::AST::Lambda
+ raise ArgumentError, ("each(): wrong argument type (#{args[1].class}; must be a parameterized block.") unless pblock.respond_to?(:puppet_lambda)
+
+ serving_size = pblock.parameter_count
+ if serving_size == 0
+ raise ArgumentError, "each(): block must define at least one parameter; value. Block has 0."
+ end
case receiver
- when Array
- foreach_Array(receiver, self, pblock)
when Hash
- foreach_Hash(receiver, self, pblock)
+ if serving_size > 2
+ raise ArgumentError, "each(): block must define at most two parameters; key, value. Block has #{serving_size}; "+
+ pblock.parameter_names.join(', ')
+ end
+ foreach_Hash(receiver, self, pblock, serving_size)
else
- raise ArgumentError, ("each(): wrong argument type (#{args[0].class}; must be an Array or a Hash.")
+ if serving_size > 2
+ raise ArgumentError, "each(): block must define at most two parameters; index, value. Block has #{serving_size}; "+
+ pblock.parameter_names.join(', ')
+ end
+ enum = Puppet::Pops::Types::Enumeration.enumerator(receiver)
+ unless enum
+ raise ArgumentError, ("each(): wrong argument type (#{receiver.class}; must be something enumerable.")
+ end
+ foreach_Enumerator(enum, self, pblock, serving_size)
end
+ # each always produces the receiver
+ receiver
end
diff --git a/lib/puppet/parser/functions/epp.rb b/lib/puppet/parser/functions/epp.rb
new file mode 100644
index 000000000..da6d5f3ff
--- /dev/null
+++ b/lib/puppet/parser/functions/epp.rb
@@ -0,0 +1,41 @@
+Puppet::Parser::Functions::newfunction(:epp, :type => :rvalue, :arity => -2, :doc =>
+"Evaluates an Embedded Puppet Template (EPP) file and returns the rendered text result as a String.
+
+EPP support the following tags:
+
+* `<%= puppet expression %>` - This tag renders the value of the expression it contains.
+* `<% puppet expression(s) %>` - This tag will execute the expression(s) it contains, but renders nothing.
+* `<%# comment %>` - The tag and its content renders nothing.
+* `<%%` or `%%>` - Renders a literal `<%` or `%>` respectively.
+* `<%-` - Same as `<%` but suppresses any leading whitespace.
+* `-%>` - Same as `%>` but suppresses any trailing whitespace on the same line (including line break).
+* `<%-( parameters )-%>` - When placed as the first tag declares the template's parameters.
+
+File based EPP supports the following visibilities of variables in scope:
+
+* Global scope (i.e. top + node scopes) - global scope is always visible
+* Global + all given arguments - if the EPP template does not declare parameters, and arguments are given
+* Global + declared parameters - if the EPP declares parameters, given argument names must match
+
+EPP supports parameters by placing an optional parameter list as the very first element in the EPP. As an example,
+`<%- ($x, $y, $z='unicorn') -%>` when placed first in the EPP text declares that the parameters `x` and `y` must be
+given as template arguments when calling `inline_epp`, and that `z` if not given as a template argument
+defaults to `'unicorn'`. Template parameters are available as variables, e.g.arguments `$x`, `$y` and `$z` in the example.
+Note that `<%-` must be used or any leading whitespace will be interpreted as text
+
+Arguments are passed to the template by calling `epp` with a Hash as the last argument, where parameters
+are bound to values, e.g. `epp('...', {'x'=>10, 'y'=>20})`. Excess arguments may be given
+(i.e. undeclared parameters) only if the EPP templates does not declare any parameters at all.
+Template parameters shadow variables in outer scopes. File based epp does never have access to variables in the
+scope where the `epp` function is called from.
+
+- See function inline_epp for examples of EPP
+- Since 3.5
+- Requires Future Parser") do |arguments|
+ # Requires future parser
+ unless Puppet[:parser] == "future"
+ raise ArgumentError, "epp(): function is only available when --parser future is in effect"
+ end
+ Puppet::Pops::Evaluator::EppEvaluator.epp(self, arguments[0], self.compiler.environment.to_s, arguments[1])
+
+end
diff --git a/lib/puppet/parser/functions/extlookup.rb b/lib/puppet/parser/functions/extlookup.rb
index 293a9ea62..359c9b452 100644
--- a/lib/puppet/parser/functions/extlookup.rb
+++ b/lib/puppet/parser/functions/extlookup.rb
@@ -1,153 +1,153 @@
require 'csv'
module Puppet::Parser::Functions
newfunction(:extlookup,
:type => :rvalue,
:arity => -2,
:doc => "This is a parser function to read data from external files, this version
uses CSV files but the concept can easily be adjust for databases, yaml
or any other queryable data source.
The object of this is to make it obvious when it's being used, rather than
magically loading data in when a module is loaded I prefer to look at the code
and see statements like:
$snmp_contact = extlookup(\"snmp_contact\")
The above snippet will load the snmp_contact value from CSV files, this in its
own is useful but a common construct in puppet manifests is something like this:
case $domain {
\"myclient.com\": { $snmp_contact = \"John Doe <john@myclient.com>\" }
default: { $snmp_contact = \"My Support <support@my.com>\" }
}
Over time there will be a lot of this kind of thing spread all over your manifests
and adding an additional client involves grepping through manifests to find all the
places where you have constructs like this.
This is a data problem and shouldn't be handled in code, and using this function you
can do just that.
First you configure it in site.pp:
$extlookup_datadir = \"/etc/puppet/manifests/extdata\"
$extlookup_precedence = [\"%{fqdn}\", \"domain_%{domain}\", \"common\"]
The array tells the code how to resolve values, first it will try to find it in
web1.myclient.com.csv then in domain_myclient.com.csv and finally in common.csv
Now create the following data files in /etc/puppet/manifests/extdata:
domain_myclient.com.csv:
snmp_contact,John Doe <john@myclient.com>
root_contact,support@%{domain}
client_trusted_ips,192.168.1.130,192.168.10.0/24
common.csv:
snmp_contact,My Support <support@my.com>
root_contact,support@my.com
Now you can replace the case statement with the simple single line to achieve
the exact same outcome:
$snmp_contact = extlookup(\"snmp_contact\")
The above code shows some other features, you can use any fact or variable that
is in scope by simply using %{varname} in your data files, you can return arrays
by just having multiple values in the csv after the initial variable name.
In the event that a variable is nowhere to be found a critical error will be raised
that will prevent your manifest from compiling, this is to avoid accidentally putting
in empty values etc. You can however specify a default value:
$ntp_servers = extlookup(\"ntp_servers\", \"1.${country}.pool.ntp.org\")
In this case it will default to \"1.${country}.pool.ntp.org\" if nothing is defined in
any data file.
You can also specify an additional data file to search first before any others at use
time, for example:
$version = extlookup(\"rsyslog_version\", \"present\", \"packages\")
package{\"rsyslog\": ensure => $version }
This will look for a version configured in packages.csv and then in the rest as configured
by $extlookup_precedence if it's not found anywhere it will default to `present`, this kind
of use case makes puppet a lot nicer for managing large amounts of packages since you do not
need to edit a load of manifests to do simple things like adjust a desired version number.
Precedence values can have variables embedded in them in the form %{fqdn}, you could for example do:
$extlookup_precedence = [\"hosts/%{fqdn}\", \"common\"]
This will result in /path/to/extdata/hosts/your.box.com.csv being searched.
This is for back compatibility to interpolate variables with %. % interpolation is a workaround for a problem that has been fixed: Puppet variable interpolation at top scope used to only happen on each run.") do |args|
key = args[0]
default = args[1]
datafile = args[2]
raise ArgumentError, ("extlookup(): wrong number of arguments (#{args.length}; must be <= 3)") if args.length > 3
extlookup_datadir = undef_as('',self['::extlookup_datadir'])
extlookup_precedence = undef_as([],self['::extlookup_precedence']).collect { |var| var.gsub(/%\{(.+?)\}/) { self["::#{$1}"] } }
datafiles = Array.new
# if we got a custom data file, put it first in the array of search files
if datafile != ""
- datafiles << extlookup_datadir + "/#{datafile}.csv" if Puppet::FileSystem::File.exist?(extlookup_datadir + "/#{datafile}.csv")
+ datafiles << extlookup_datadir + "/#{datafile}.csv" if Puppet::FileSystem.exist?(extlookup_datadir + "/#{datafile}.csv")
end
extlookup_precedence.each do |d|
datafiles << extlookup_datadir + "/#{d}.csv"
end
desired = nil
datafiles.each do |file|
if desired.nil?
- if Puppet::FileSystem::File.exist?(file)
+ if Puppet::FileSystem.exist?(file)
result = CSV.read(file).find_all do |r|
r[0] == key
end
# return just the single result if theres just one,
# else take all the fields in the csv and build an array
if result.length > 0
if result[0].length == 2
val = result[0][1].to_s
# parse %{}'s in the CSV into local variables using the current scope
while val =~ /%\{(.+?)\}/
val.gsub!(/%\{#{$1}\}/, self[$1])
end
desired = val
elsif result[0].length > 1
length = result[0].length
cells = result[0][1,length]
# Individual cells in a CSV result are a weird data type and throws
# puppets yaml parsing, so just map it all to plain old strings
desired = cells.map do |c|
# parse %{}'s in the CSV into local variables using the current scope
while c =~ /%\{(.+?)\}/
c.gsub!(/%\{#{$1}\}/, self[$1])
end
c.to_s
end
end
end
end
end
end
desired || default or raise Puppet::ParseError, "No match found for '#{key}' in any data file during extlookup()"
end
end
diff --git a/lib/puppet/parser/functions/file.rb b/lib/puppet/parser/functions/file.rb
index 89d78f8ba..17401fc8b 100644
--- a/lib/puppet/parser/functions/file.rb
+++ b/lib/puppet/parser/functions/file.rb
@@ -1,23 +1,23 @@
# Returns the contents of a file
Puppet::Parser::Functions::newfunction(
:file, :arity => -2, :type => :rvalue,
:doc => "Return the contents of a file. Multiple files
can be passed, and the first file that exists will be read in."
) do |vals|
ret = nil
vals.each do |file|
unless Puppet::Util.absolute_path?(file)
raise Puppet::ParseError, "Files must be fully qualified"
end
- if Puppet::FileSystem::File.exist?(file)
+ if Puppet::FileSystem.exist?(file)
ret = File.read(file)
break
end
end
if ret
ret
else
raise Puppet::ParseError, "Could not find any files from #{vals.join(", ")}"
end
end
diff --git a/lib/puppet/parser/functions/filter.rb b/lib/puppet/parser/functions/filter.rb
index 7894fa48f..5760a8a75 100644
--- a/lib/puppet/parser/functions/filter.rb
+++ b/lib/puppet/parser/functions/filter.rb
@@ -1,48 +1,100 @@
require 'puppet/parser/ast/lambda'
Puppet::Parser::Functions::newfunction(
: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 or hash (same type as left operand)
- with the entries for which the block evaluates to true.
+ argument and returns an array or hash (same type as left operand for array/hash, and array for
+ other enumerable types) 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:
+ This function takes two mandatory arguments: the first should be an Array, a Hash, or an
+ Enumerable object (integer, Integer range, or String),
+ and the second a parameterized block as produced by the puppet syntax:
$a.filter |$x| { ... }
+ filter($a) |$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.
+ When the first argument is something other than a Hash, 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*
# selects all that end with berry
$a = ["raspberry", "blueberry", "orange"]
- $a.filter |$x| { $x =~ /berry$/ }
+ $a.filter |$x| { $x =~ /berry$/ } # rasberry, blueberry
+
+ If the block defines two parameters, they will be set to `index, value` (with index starting at 0) for all
+ enumerables except Hash, and to `key, value` for a Hash.
+
+ *Examples*
+
+ # selects all that end with 'berry' at an even numbered index
+ $a = ["raspberry", "blueberry", "orange"]
+ $a.filter |$index, $x| { $index % 2 == 0 and $x =~ /berry$/ } # raspberry
- - Since 3.4
- - requires `parser = future`.
+ # selects all that end with 'berry' and value >= 1
+ $a = {"raspberry"=>0, "blueberry"=>1, "orange"=>1}
+ $a.filter |$key, $x| { $x =~ /berry$/ and $x >= 1 } # blueberry
+
+ - Since 3.4 for Array and Hash
+ - Since 3.5 for other enumerables
+ - requires `parser = future`
ENDHEREDOC
+ def filter_Enumerator(enumerator, scope, pblock, serving_size)
+ result = []
+ index = 0
+ if serving_size == 1
+ begin
+ loop { pblock.call(scope, it = enumerator.next) == true ? result << it : nil }
+ rescue StopIteration
+ end
+ else
+ begin
+ loop do
+ pblock.call(scope, index, it = enumerator.next) == true ? result << it : nil
+ index = index +1
+ end
+ rescue StopIteration
+ end
+ end
+ result
+ end
+
receiver = args[0]
pblock = args[1]
- raise ArgumentError, ("filter(): 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.respond_to?(:puppet_lambda)
+ serving_size = pblock.parameter_count
+ if serving_size == 0
+ raise ArgumentError, "filter(): block must define at least one parameter; value. Block has 0."
+ end
case receiver
- when Array
- receiver.select {|x| pblock.call(self, x) }
when Hash
- result = receiver.select {|x, y| pblock.call(self, [x, y]) }
+ if serving_size > 2
+ raise ArgumentError, "filter(): block must define at most two parameters; key, value. Block has #{serving_size}; "+
+ pblock.parameter_names.join(', ')
+ end
+ if serving_size == 1
+ result = receiver.select {|x, y| pblock.call(self, [x, y]) }
+ else
+ result = receiver.select {|x, y| pblock.call(self, x, y) }
+ end
# Ruby 1.8.7 returns Array
result = Hash[result] unless result.is_a? Hash
result
else
- raise ArgumentError, ("filter(): wrong argument type (#{receiver.class}; must be an Array or a Hash.")
+ if serving_size > 2
+ raise ArgumentError, "filter(): block must define at most two parameters; index, value. Block has #{serving_size}; "+
+ pblock.parameter_names.join(', ')
+ end
+ enum = Puppet::Pops::Types::Enumeration.enumerator(receiver)
+ unless enum
+ raise ArgumentError, ("filter(): wrong argument type (#{receiver.class}; must be something enumerable.")
+ end
+ filter_Enumerator(enum, self, pblock, serving_size)
end
end
diff --git a/lib/puppet/parser/functions/generate.rb b/lib/puppet/parser/functions/generate.rb
index 71bd0b19f..35c752c0b 100644
--- a/lib/puppet/parser/functions/generate.rb
+++ b/lib/puppet/parser/functions/generate.rb
@@ -1,37 +1,37 @@
# Runs an external command and returns the results
Puppet::Parser::Functions::newfunction(:generate, :arity => -2, :type => :rvalue,
:doc => "Calls an external command on the Puppet master and returns
the results of the command. Any arguments are passed to the external command as
arguments. If the generator does not exit with return code of 0,
the generator is considered to have failed and a parse error is
thrown. Generators can only have file separators, alphanumerics, dashes,
and periods in them. This function will attempt to protect you from
malicious generator calls (e.g., those with '..' in them), but it can
never be entirely safe. No subshell is used to execute
generators, so all shell metacharacters are passed directly to
the generator.") do |args|
raise Puppet::ParseError, "Generators must be fully qualified" unless Puppet::Util.absolute_path?(args[0])
if Puppet.features.microsoft_windows?
valid = args[0] =~ /^[a-z]:(?:[\/\\][-.~\w]+)+$/i
else
valid = args[0] =~ /^[-\/\w.+]+$/
end
unless valid
raise Puppet::ParseError,
"Generators can only contain alphanumerics, file separators, and dashes"
end
if args[0] =~ /\.\./
raise Puppet::ParseError,
"Can not use generators with '..' in them."
end
begin
Dir.chdir(File.dirname(args[0])) { Puppet::Util::Execution.execute(args) }
rescue Puppet::ExecutionFailure => detail
- raise Puppet::ParseError, "Failed to execute generator #{args[0]}: #{detail}"
+ raise Puppet::ParseError, "Failed to execute generator #{args[0]}: #{detail}", detail.backtrace
end
end
diff --git a/lib/puppet/parser/functions/include.rb b/lib/puppet/parser/functions/include.rb
index ea7acd936..29ef45d40 100644
--- a/lib/puppet/parser/functions/include.rb
+++ b/lib/puppet/parser/functions/include.rb
@@ -1,46 +1,47 @@
# Include the specified classes
-Puppet::Parser::Functions::newfunction(:include, :arity => -2, :doc => "Declares one or more classes, causing the resources in them to be
+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
+does not create a dependency relationship between the declared class and the
surrounding class; for that, see the `require` function.") do |vals|
if vals.is_a?(Array)
# Protect against array inside array
vals = vals.flatten
else
vals = [vals]
end
# The 'false' disables lazy evaluation.
klasses = compiler.evaluate_classes(vals, self, false)
missing = vals.find_all do |klass|
! klasses.include?(klass)
end
unless missing.empty?
# Throw an error if we didn't evaluate all of the classes.
str = "Could not find class"
str += "es" if missing.length > 1
str += " " + missing.join(", ")
if n = namespaces and ! n.empty? and n != [""]
str += " in namespaces #{@namespaces.join(", ")}"
end
self.fail Puppet::ParseError, str
end
end
diff --git a/lib/puppet/parser/functions/inline_epp.rb b/lib/puppet/parser/functions/inline_epp.rb
new file mode 100644
index 000000000..cfc1597a1
--- /dev/null
+++ b/lib/puppet/parser/functions/inline_epp.rb
@@ -0,0 +1,79 @@
+Puppet::Parser::Functions::newfunction(:inline_epp, :type => :rvalue, :arity => -2, :doc =>
+"Evaluates an Embedded Puppet Template (EPP) string and returns the rendered text result as a String.
+
+EPP support the following tags:
+
+* `<%= puppet expression %>` - This tag renders the value of the expression it contains.
+* `<% puppet expression(s) %>` - This tag will execute the expression(s) it contains, but renders nothing.
+* `<%# comment %>` - The tag and its content renders nothing.
+* `<%%` or `%%>` - Renders a literal `<%` or `%>` respectively.
+* `<%-` - Same as `<%` but suppresses any leading whitespace.
+* `-%>` - Same as `%>` but suppresses any trailing whitespace on the same line (including line break).
+* `<%-( parameters )-%>` - When placed as the first tag declares the template's parameters.
+
+Inline EPP supports the following visibilities of variables in scope which depends on how EPP parameters
+are used - see further below:
+
+* Global scope (i.e. top + node scopes) - global scope is always visible
+* Global + Enclosing scope - if the EPP template does not declare parameters, and no arguments are given
+* Global + all given arguments - if the EPP template does not declare parameters, and arguments are given
+* Global + declared parameters - if the EPP declares parameters, given argument names must match
+
+EPP supports parameters by placing an optional parameter list as the very first element in the EPP. As an example,
+`<%-( $x, $y, $z='unicorn' )-%>` when placed first in the EPP text declares that the parameters `x` and `y` must be
+given as template arguments when calling `inline_epp`, and that `z` if not given as a template argument
+defaults to `'unicorn'`. Template parameters are available as variables, e.g.arguments `$x`, `$y` and `$z` in the example.
+Note that `<%-` must be used or any leading whitespace will be interpreted as text
+
+Arguments are passed to the template by calling `inline_epp` with a Hash as the last argument, where parameters
+are bound to values, e.g. `inline_epp('...', {'x'=>10, 'y'=>20})`. Excess arguments may be given
+(i.e. undeclared parameters) only if the EPP templates does not declare any parameters at all.
+Template parameters shadow variables in outer scopes.
+
+Note: An inline template is best stated using a single-quoted string, or a heredoc since a double-quoted string
+is subject to expression interpolation before the string is parsed as an EPP template. Here are examples
+(using heredoc to define the EPP text):
+
+ # produces 'Hello local variable world!'
+ $x ='local variable'
+ inline_epptemplate(@(END:epp))
+ <%-( $x )-%>
+ Hello <%= $x %> world!
+ END
+
+ # produces 'Hello given argument world!'
+ $x ='local variable world'
+ inline_epptemplate(@(END:epp), { x =>'given argument'})
+ <%-( $x )-%>
+ Hello <%= $x %> world!
+ END
+
+ # produces 'Hello given argument world!'
+ $x ='local variable world'
+ inline_epptemplate(@(END:epp), { x =>'given argument'})
+ <%-( $x )-%>
+ Hello <%= $x %>!
+ END
+
+ # results in error, missing value for y
+ $x ='local variable world'
+ inline_epptemplate(@(END:epp), { x =>'given argument'})
+ <%-( $x, $y )-%>
+ Hello <%= $x %>!
+ END
+
+ # Produces 'Hello given argument planet'
+ $x ='local variable world'
+ inline_epptemplate(@(END:epp), { x =>'given argument'})
+ <%-( $x, $y=planet)-%>
+ Hello <%= $x %> <%= $y %>!
+ END
+
+- Since 3.5
+- Requires Future Parser") do |arguments|
+ # Requires future parser
+ unless Puppet[:parser] == "future"
+ raise ArgumentError, "inline_epp(): function is only available when --parser future is in effect"
+ end
+ Puppet::Pops::Evaluator::EppEvaluator.inline_epp(self, arguments[0], arguments[1])
+end
diff --git a/lib/puppet/parser/functions/inline_template.rb b/lib/puppet/parser/functions/inline_template.rb
index 03a3a3879..b3ab9d30e 100644
--- a/lib/puppet/parser/functions/inline_template.rb
+++ b/lib/puppet/parser/functions/inline_template.rb
@@ -1,21 +1,21 @@
Puppet::Parser::Functions::newfunction(:inline_template, :type => :rvalue, :arity => -2, :doc =>
"Evaluate a template string and return its value. See
[the templating docs](http://docs.puppetlabs.com/guides/templating.html) for
more information. Note that if multiple template strings are specified, their
output is all concatenated and returned as the output of the function.") do |vals|
require 'erb'
vals.collect do |string|
# Use a wrapper, so the template can't get access to the full
# Scope object.
wrapper = Puppet::Parser::TemplateWrapper.new(self)
begin
wrapper.result(string)
rescue => detail
raise Puppet::ParseError,
- "Failed to parse inline template: #{detail}"
+ "Failed to parse inline template: #{detail}", detail.backtrace
end
end.join("")
end
diff --git a/lib/puppet/parser/functions/lookup.rb b/lib/puppet/parser/functions/lookup.rb
index 47ab719a0..55d56f452 100644
--- a/lib/puppet/parser/functions/lookup.rb
+++ b/lib/puppet/parser/functions/lookup.rb
@@ -1,44 +1,144 @@
Puppet::Parser::Functions.newfunction(:lookup, :type => :rvalue, :arity => -2, :doc => <<-'ENDHEREDOC') do |args|
-Looks up data defined using Puppet Bindings.
-The function is callable with one or two arguments and optionally with a lambda to process the result.
-The second argument can be a type specification; a String that describes the type of the produced result.
-If a value is found, an assert is made that the value is compliant with the specified type.
+Looks up data defined using Puppet Bindings and Hiera.
+The function is callable with one to three arguments and optionally with a code block to further process the result.
-When called with one argument; the name:
+The lookup function can be called in one of these ways:
- lookup('the_name')
+ lookup(name)
+ lookup(name, type)
+ lookup(name, type, default)
+ lookup(options_hash)
+ lookup(name, options_hash)
+
+The function may optionally be called with a code block / lambda with the following signatures:
+
+ lookup(...) |$result| { ... }
+ lookup(...) |$name, $result| { ... }
+ lookup(...) |$name, $result, $default| { ... }
+
+The longer signatures are useful when the block needs to raise an error (it can report the name), or
+if it needs to know if the given default value was selected.
+
+The code block receives the following three arguments:
+
+* The `$name` is the last name that was looked up (*the* name if only one name was looked up)
+* The `$result` is the looked up value (or the default value if not found).
+* The `$default` is the given default value (`undef` if not given).
+
+The block, if present, is called with the result from the lookup. The value produced by the block is also what is
+produced by the `lookup` function.
+When a block is used, it is the users responsibility to call `error` if the result does not meet additional
+criteria, or if an undef value is not acceptable. If a value is not found, and a default has been
+specified, the default value is given to the block.
-When called with two arguments; the name, and the expected type:
+The content of the options hash is:
- lookup('the_name', 'String')
+* `name` - The name or array of names to lookup (first found is returned)
+* `type` - The type to assert (a Type or a type specification in string form)
+* `default` - The default value if there was no value found (must comply with the data type)
+* `accept_undef` - (default `false`) An `undef` result is accepted if this options is set to `true`.
+* `override` - a hash with map from names to values that are used instead of the underlying bindings. If the name
+ is found here it wins. Defaults to an empty hash.
+* `extra` - a hash with map from names to values that are used as a last resort to obtain a value. Defaults to an
+ empty hash.
-Using a lambda to process the looked up result.
+When the call is on the form `lookup(name, options_hash)`, or `lookup(name, type, options_hash)`, the given name
+argument wins over the `options_hash['name']`.
- lookup('the_name') |$result| { if $result == undef { 'Jane Doe' } else { $result }}
+The search order is `override` (if given), then `binder`, then `hiera` and finally `extra` (if given). The first to produce
+a value other than undef for a given name wins.
The type specification is one of:
-* the basic types; 'Integer', 'String', 'Float', 'Boolean', or 'Pattern' (regular expression)
-* an Array with an optional element type given in '[]', that when not given defaults to '[Data]'
-* a Hash with optional key and value types given in '[]', where key type defaults to 'Literal' and value to 'Data', if
- only one type is given, the key defaults to 'Literal'
-* the abstract type 'Literal' which is one of the basic types
-* the abstract type 'Data' which is 'Literal', or type compatible with Array[Data], or Hash[Literal, Data]
-* the abstract type 'Collection' which is Array or Hash of any element type.
-* the abstract type 'Object' which is any kind of type
+ * A type in the Puppet Type System, e.g.:
+ * `Integer`, an integral value with optional range e.g.:
+ * `Integer[0, default]` - 0 or positive
+ * `Integer[default, -1]` - negative,
+ * `Integer[1,100]` - value between 1 and 100 inclusive
+ * `String`- any string
+ * `Float` - floating point number (same signature as for Integer for `Integer` ranges)
+ * `Boolean` - true of false (strict)
+ * `Array` - an array (of Data by default), or parameterized as `Array[<element_type>]`, where
+ `<element_type>` is the expected type of elements
+ * `Hash`, - a hash (of default `Literal` keys and `Data` values), or parameterized as
+ `Hash[<value_type>]`, `Hash[<key_type>, <value_type>]`, where `<key_type>`, and
+ `<value_type>` are the types of the keys and values respectively
+ (key is `Literal` by default).
+ * `Data` - abstract type representing any `Literal`, `Array[Data]`, or `Hash[Literal, Data]`
+ * `Pattern[<p1>, <p2>, ..., <pn>]` - an enumeration of valid patterns (one or more) where
+ a pattern is a regular expression string or regular expression,
+ e.g. `Pattern['.com$', '.net$']`, `Pattern[/[a-z]+[0-9]+/]`
+ * `Enum[<s1>, <s2>, ..., <sn>]`, - an enumeration of exact string values (one or more)
+ e.g. `Enum[blue, red, green]`.
+ * `Variant[<t1>, <t2>,...<tn>]` - matches one of the listed types (at least one must be given)
+ e.g. `Variant[Integer[8000,8999], Integer[20000, 99999]]` to accept a value in either range
+ * `Regexp`- a regular expression (i.e. the result is a regular expression, not a string
+ matching a regular expression).
+ * A string containing a type description - one of the types as shown above but in string form.
+
+If the function is called without specifying a default value, and nothing is bound to the given name
+an error is raised unless the option `accept_undef` is true. If a block is given it must produce an acceptable
+value (or call `error`). If the block does not produce an acceptable value an error is
+raised.
+
+Examples:
+
+When called with one argument; **the name**, it
+returns the bound value with the given name after having asserted it has the default datatype `Data`:
+
+ lookup('the_name')
+
+When called with two arguments; **the name**, and **the expected type**, it
+returns the bound value with the given name after having asserted it has the given data
+type ('String' in the example):
+
+ lookup('the_name', 'String') # 3.x
+ lookup('the_name', String) # parser future
+
+When called with three arguments, **the name**, the **expected type**, and a **default**, it
+returns the bound value with the given name, or the default after having asserted the value
+has the given data type (`String` in the example above):
+
+ lookup('the_name', 'String', 'Fred') # 3x
+ lookup('the_name', String, 'Fred') # parser future
+
+Using a lambda to process the looked up result - asserting that it starts with an upper case letter:
+
+ # only with parser future
+ lookup('the_size', Integer[1,100]) |$result| {
+ if $large_value_allowed and $result > 10
+ { error 'Values larger than 10 are not allowed'}
+ $result
+ }
+
+Including the name in the error
+
+ # only with parser future
+ lookup('the_size', Integer[1,100]) |$name, $result| {
+ if $large_value_allowed and $result > 10
+ { error 'The bound value for '${name}' can not be larger than 10 in this configuration'}
+ $result
+ }
+
+When using a block, the value it produces is also asserted against the given type, and it may not be
+`undef` unless the option `'accept_undef'` is `true`.
+
+All options work as the corresponding (direct) argument. The `first_found` option and
+`accept_undef` are however only available as options.
+
+Using first_found semantics option to return the first name that has a bound value:
+
+ lookup(['apache::port', 'nginx::port'], 'Integer', 80)
+
+If you want to make lookup return undef when no value was found instead of raising an error:
+
+ $are_you_there = lookup('peekaboo', { accept_undef => true} )
+ $are_you_there = lookup('peekaboo', { accept_undef => true}) |$result| { $result }
ENDHEREDOC
unless Puppet[:binder] || Puppet[:parser] == 'future'
raise Puppet::ParseError, "The lookup function is only available with settings --binder true, or --parser future"
end
- type_parser = Puppet::Pops::Types::TypeParser.new
- pblock = args[-1] if args[-1].is_a?(Puppet::Parser::AST::Lambda)
- type_name = args[1] unless args[1].is_a?(Puppet::Parser::AST::Lambda)
- type = type_parser.parse( type_name || "Data")
- result = compiler.injector.lookup(self, type, args[0])
- if pblock
- result = pblock.call(self, result.nil? ? :undef : result)
- end
- result.nil? ? :undef : result
+ Puppet::Pops::Binder::Lookup.lookup(self, args)
end
diff --git a/lib/puppet/parser/functions/map.rb b/lib/puppet/parser/functions/map.rb
index 3c871bc85..8bc9fd383 100644
--- a/lib/puppet/parser/functions/map.rb
+++ b/lib/puppet/parser/functions/map.rb
@@ -1,44 +1,96 @@
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:
+ This function takes two mandatory arguments: the first should be an Array, Hash, or of Enumerable type
+ (integer, Integer range, or String), and the second a parameterized block as produced by the puppet syntax:
$a.map |$x| { ... }
+ map($a) |$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]`.
+ When the first argument `$a` is an Array or of enumerable type, 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`.
+ When using a block with 2 parameters, the element's index (starting from 0) for an array, and the key for a hash
+ is given to the block's first parameter, and the value is given to the block's second parameter.args.
+
+ *Examples*
+
+ # Turns hash into array of values
+ $a.map |$key,$val|{ $val }
+
+ # Turns hash into array of keys
+ $a.map |$key,$val|{ $key }
+
+ - Since 3.4 for Array and Hash
+ - Since 3.5 for other enumerables, and support for blocks with 2 parameters
+ - requires `parser = future`
ENDHEREDOC
+ def map_Enumerator(enumerator, scope, pblock, serving_size)
+ result = []
+ index = 0
+ if serving_size == 1
+ begin
+ loop { result << pblock.call(scope, enumerator.next) }
+ rescue StopIteration
+ end
+ else
+ begin
+ loop do
+ result << pblock.call(scope, index, enumerator.next)
+ index = index +1
+ end
+ rescue StopIteration
+ end
+ end
+ result
+ end
+
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
-
+ raise ArgumentError, ("map(): wrong argument type (#{pblock.class}; must be a parameterized block.") unless pblock.respond_to?(:puppet_lambda)
+ serving_size = pblock.parameter_count
+ if serving_size == 0
+ raise ArgumentError, "map(): block must define at least one parameter; value. Block has 0."
+ end
case receiver
- when Array
when Hash
+ if serving_size > 2
+ raise ArgumentError, "map(): block must define at most two parameters; key, value.args Block has #{serving_size}; "+
+ pblock.parameter_names.join(', ')
+ end
+ if serving_size == 1
+ result = receiver.map {|x, y| pblock.call(self, [x, y]) }
+ else
+ result = receiver.map {|x, y| pblock.call(self, x, y) }
+ end
else
- raise ArgumentError, ("map(): wrong argument type (#{receiver.class}; must be an Array or a Hash.")
- end
+ if serving_size > 2
+ raise ArgumentError, "map(): block must define at most two parameters; index, value. Block has #{serving_size}; "+
+ pblock.parameter_names.join(', ')
+ end
- receiver.to_a.map {|x| pblock.call(self, x) }
+ enum = Puppet::Pops::Types::Enumeration.enumerator(receiver)
+ unless enum
+ raise ArgumentError, ("map(): wrong argument type (#{receiver.class}; must be something enumerable.")
+ end
+ result = map_Enumerator(enum, self, pblock, serving_size)
+ end
+ result
end
diff --git a/lib/puppet/parser/functions/reduce.rb b/lib/puppet/parser/functions/reduce.rb
index afa321058..078ebc2e9 100644
--- a/lib/puppet/parser/functions/reduce.rb
+++ b/lib/puppet/parser/functions/reduce.rb
@@ -1,76 +1,100 @@
Puppet::Parser::Functions::newfunction(
:reduce,
:type => :rvalue,
:arity => -2,
:doc => <<-'ENDHEREDOC') do |args|
Applies a parameterized block to each element in a sequence of entries from the first
- argument (_the collection_) and returns the last result of the invocation of the parameterized block.
+ argument (_the enumerable_) and returns the last result of the invocation of the parameterized block.
- This function takes two mandatory arguments: the first should be an Array or a Hash, and the last
- a parameterized block as produced by the puppet syntax:
+ This function takes two mandatory arguments: the first should be an Array, Hash, or something of
+ enumerable type, and the last a parameterized block as produced by the puppet syntax:
$a.reduce |$memo, $x| { ... }
+ reduce($a) |$memo, $x| { ... }
- When the first argument is an Array, the block is called with each entry in turn. When the first argument
- is a hash each entry is converted to an array with `[key, value]` before being fed to the block. An optional
- 'start memo' value may be supplied as an argument between the array/hash and mandatory block.
+ When the first argument is an Array or someting of an enumerable type, the block is called with each entry in turn.
+ When the first argument is a hash each entry is converted to an array with `[key, value]` before being
+ fed to the block. An optional 'start memo' value may be supplied as an argument between the array/hash
+ and mandatory block.
+
+ $a.reduce(start) |$memo, $x| { ... }
+ reduce($a, start) |$memo, $x| { ... }
If no 'start memo' is given, the first invocation of the parameterized block will be given the first and second
- elements of the collection, and if the collection has fewer than 2 elements, the first
+ elements of the enumeration, and if the enumerable has fewer than 2 elements, the first
element is produced as the result of the reduction without invocation of the block.
- On each subsequent invocations, the produced value of the invoked parameterized block is given as the memo in the
+ On each subsequent invocation, the produced value of the invoked parameterized block is given as the memo in the
next invocation.
*Examples*
# Reduce an array
$a = [1,2,3]
$a.reduce |$memo, $entry| { $memo + $entry }
#=> 6
# Reduce hash values
$a = {a => 1, b => 2, c => 3}
$a.reduce |$memo, $entry| { [sum, $memo[1]+$entry[1]] }
#=> [sum, 6]
+ # reverse a string
+ "abc".reduce |$memo, $char| { "$char$memo" }
+ #=>"cbe"
+
It is possible to provide a starting 'memo' as an argument.
*Examples*
# Reduce an array
$a = [1,2,3]
$a.reduce(4) |$memo, $entry| { $memo + $entry }
#=> 10
# Reduce hash values
$a = {a => 1, b => 2, c => 3}
$a.reduce([na, 4]) |$memo, $entry| { [sum, $memo[1]+$entry[1]] }
#=> [sum, 10]
- - Since 3.2
+ *Examples*
+
+ Integer[1,4].reduce |$memo, $x| { $memo + $x }
+ #=> 10
+
+ - Since 3.2 for Array and Hash
+ - Since 3.5 for additional enumerable types
- requires `parser = future`.
ENDHEREDOC
require 'puppet/parser/ast/lambda'
+
case args.length
when 2
pblock = args[1]
when 3
pblock = args[2]
else
- raise ArgumentError, ("reduce(): wrong number of arguments (#{args.length}; must be 2 or 3)")
+ raise ArgumentError, ("reduce(): wrong number of arguments (#{args.length}; expected 2 or 3, got #{args.length})")
end
- unless pblock.is_a? Puppet::Parser::AST::Lambda
- raise ArgumentError, ("reduce(): wrong argument type (#{args[1].class}; must be a parameterized block.")
+ unless pblock.respond_to?(:puppet_lambda)
+ raise ArgumentError, ("reduce(): wrong argument type (#{pblock.class}; must be a parameterized block.")
end
receiver = args[0]
- unless [Array, Hash].include?(receiver.class)
- raise ArgumentError, ("collect(): wrong argument type (#{args[0].class}; must be an Array or a Hash.")
+ enum = Puppet::Pops::Types::Enumeration.enumerator(receiver)
+ unless enum
+ raise ArgumentError, ("reduce(): wrong argument type (#{receiver.class}; must be something enumerable.")
end
+
+ serving_size = pblock.parameter_count
+ if serving_size != 2
+ raise ArgumentError, "reduce(): block must define 2 parameters; memo, value. Block has #{serving_size}; "+
+ pblock.parameter_names.join(', ')
+ end
+
if args.length == 3
- receiver.reduce(args[1]) {|memo, x| pblock.call(self, memo, x) }
+ enum.reduce(args[1]) {|memo, x| pblock.call(self, memo, x) }
else
- receiver.reduce {|memo, x| pblock.call(self, memo, x) }
+ enum.reduce {|memo, x| pblock.call(self, memo, x) }
end
end
diff --git a/lib/puppet/parser/functions/select.rb b/lib/puppet/parser/functions/select.rb
index 659f2013c..93924f9d0 100644
--- a/lib/puppet/parser/functions/select.rb
+++ b/lib/puppet/parser/functions/select.rb
@@ -1,15 +1,15 @@
Puppet::Parser::Functions::newfunction(
:select,
:type => :rvalue,
:arity => 2,
:doc => <<-'ENDHEREDOC') do |args|
The 'select' function has been renamed to 'filter'. Please update your manifests.
The select function is reserved for future use.
- Removed as of 3.4
- requires `parser = future`.
ENDHEREDOC
raise NotImplementedError,
"The 'select' function has been renamed to 'filter'. Please update your manifests."
-end
\ No newline at end of file
+end
diff --git a/lib/puppet/parser/functions/slice.rb b/lib/puppet/parser/functions/slice.rb
index 505ab7261..bcc830f74 100644
--- a/lib/puppet/parser/functions/slice.rb
+++ b/lib/puppet/parser/functions/slice.rb
@@ -1,97 +1,116 @@
Puppet::Parser::Functions::newfunction(
:slice,
:type => :rvalue,
:arity => -2,
:doc => <<-'ENDHEREDOC') do |args|
Applies a parameterized block to each _slice_ of elements in a sequence of selected entries from the first
argument and returns the first argument, or if no block is given returns a new array with a concatenation of
the slices.
- This function takes two mandatory arguments: the first, `$a`, should be an Array or a Hash, and the second, `$n`,
- the number of elements to include in each slice. The optional third argument should be a
- a parameterized block as produced by the puppet syntax:
+ This function takes two mandatory arguments: the first, `$a`, should be an Array, Hash, or something of
+ enumerable type (integer, Integer range, or String), and the second, `$n`, the number of elements to include
+ in each slice. The optional third argument should be a a parameterized block as produced by the puppet syntax:
$a.slice($n) |$x| { ... }
+ slice($a) |$x| { ... }
The parameterized block should have either one parameter (receiving an array with the slice), or the same number
of parameters as specified by the slice size (each parameter receiving its part of the slice).
In case there are fewer remaining elements than the slice size for the last slice it will contain the remaining
- elements. When the block has multiple parameters, excess parameters are set to :undef for an array, and to
- empty arrays for a Hash.
+ elements. When the block has multiple parameters, excess parameters are set to :undef for an array or
+ enumerable type, and to empty arrays for a Hash.
$a.slice(2) |$first, $second| { ... }
- When the first argument is a Hash, each key,value entry is counted as one, e.g, a slice size of 2 will produce
- an array of two arrays with key, value.
+ When the first argument is a Hash, each `key,value` entry is counted as one, e.g, a slice size of 2 will produce
+ an array of two arrays with key, and value.
$a.slice(2) |$entry| { notice "first ${$entry[0]}, second ${$entry[1]}" }
$a.slice(2) |$first, $second| { notice "first ${first}, second ${second}" }
When called without a block, the function produces a concatenated result of the slices.
- slice($[1,2,3,4,5,6], 2) # produces [[1,2], [3,4], [5,6]]
+ slice([1,2,3,4,5,6], 2) # produces [[1,2], [3,4], [5,6]]
+ slice(Integer[1,6], 2) # produces [[1,2], [3,4], [5,6]]
+ slice(4,2) # produces [[0,1], [2,3]]
+ slice('hello',2) # produces [[h, e], [l, l], [o]]
- - Since 3.2
+ - Since 3.2 for Array and Hash
+ - Since 3.5 for additional enumerable types
- requires `parser = future`.
ENDHEREDOC
require 'puppet/parser/ast/lambda'
require 'puppet/parser/scope'
def each_Common(o, slice_size, filler, scope, pblock)
serving_size = pblock ? pblock.parameter_count : 1
if serving_size == 0
- raise ArgumentError, "Block must define at least one parameter."
+ raise ArgumentError, "slice(): block must define at least one parameter. Block has 0."
end
unless serving_size == 1 || serving_size == slice_size
- raise ArgumentError, "Block must define one parameter, or the same number of parameters as the given size of the slice (#{slice_size})."
+ raise ArgumentError, "slice(): block must define one parameter, or " +
+ "the same number of parameters as the given size of the slice (#{slice_size}). Block has #{serving_size}; "+
+ pblock.parameter_names.join(', ')
end
enumerator = o.each_slice(slice_size)
result = []
if serving_size == 1
- ((o.size.to_f / slice_size).ceil).times do
+ begin
if pblock
- pblock.call(scope, enumerator.next)
+ loop do
+ pblock.call(scope, enumerator.next)
+ end
else
- result << enumerator.next
+ loop do
+ result << enumerator.next
+ end
end
+ rescue StopIteration
end
else
- ((o.size.to_f / slice_size).ceil).times do
- a = enumerator.next
- if a.size < serving_size
- a = a.dup.fill(filler, a.length...serving_size)
+ begin
+ loop do
+ a = enumerator.next
+ if a.size < serving_size
+ a = a.dup.fill(filler, a.length...serving_size)
+ end
+ pblock.call(scope, *a)
end
- pblock.call(scope, *a)
+ rescue StopIteration
end
end
if pblock
o
else
result
end
end
+
raise ArgumentError, ("slice(): wrong number of arguments (#{args.length}; must be 2 or 3)") unless args.length == 2 || args.length == 3
if args.length >= 2
begin
slice_size = Puppet::Parser::Scope.number?(args[1])
rescue
raise ArgumentError, ("slice(): wrong argument type (#{args[1]}; must be number.")
end
end
raise ArgumentError, ("slice(): wrong argument type (#{args[1]}; must be number.") unless slice_size
raise ArgumentError, ("slice(): wrong argument value: #{slice_size}; is not a positive integer number > 0") unless slice_size.is_a?(Fixnum) && slice_size > 0
receiver = args[0]
# the block is optional, ok if nil, function then produces an array
pblock = args[2]
- raise ArgumentError, ("slice(): wrong argument type (#{args[2].class}; must be a parameterized block.") unless pblock.is_a?(Puppet::Parser::AST::Lambda) || args.length == 2
+ raise ArgumentError, ("slice(): wrong argument type (#{args[2].class}; must be a parameterized block.") unless pblock.respond_to?(:puppet_lambda) || args.length == 2
case receiver
- when Array
- each_Common(receiver, slice_size, :undef, self, pblock)
when Hash
each_Common(receiver, slice_size, [], self, pblock)
else
- raise ArgumentError, ("slice(): wrong argument type (#{args[0].class}; must be an Array or a Hash.")
+ enum = Puppet::Pops::Types::Enumeration.enumerator(receiver)
+ if enum.nil?
+ raise ArgumentError, ("slice(): given type '#{tc.string(receiver)}' is not enumerable")
+ end
+ result = each_Common(enum, slice_size, :undef, self, pblock)
+ pblock ? receiver : result
end
end
diff --git a/lib/puppet/parser/grammar.ra b/lib/puppet/parser/grammar.ra
index 6c1228d71..c7b83fe6b 100644
--- a/lib/puppet/parser/grammar.ra
+++ b/lib/puppet/parser/grammar.ra
@@ -1,803 +1,806 @@
# vim: syntax=ruby
# the parser
class Puppet::Parser::Parser
token STRING DQPRE DQMID DQPOST
token LBRACK RBRACK LBRACE RBRACE SYMBOL FARROW COMMA TRUE
token FALSE EQUALS APPENDS LESSEQUAL NOTEQUAL DOT COLON LLCOLLECT RRCOLLECT
token QMARK LPAREN RPAREN ISEQUAL GREATEREQUAL GREATERTHAN LESSTHAN
token IF ELSE IMPORT DEFINE ELSIF VARIABLE CLASS INHERITS NODE BOOLEAN
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 MODULO
prechigh
right NOT
nonassoc UMINUS
left IN MATCH NOMATCH
left TIMES DIV MODULO
left MINUS PLUS
left LSHIFT RSHIFT
left NOTEQUAL ISEQUAL
left GREATEREQUAL GREATERTHAN LESSTHAN LESSEQUAL
left AND
left OR
preclow
rule
program: statements_and_declarations
| nil
statements_and_declarations: statement_or_declaration {
result = ast AST::BlockExpression, :children => (val[0] ? [val[0]] : [])
}
| statements_and_declarations statement_or_declaration {
if val[1]
val[0].push(val[1])
end
result = val[0]
}
# statements is like statements_and_declarations, but it doesn't allow
# nested definitions, classes, or nodes.
statements: statements_and_declarations {
val[0].each do |stmt|
if stmt.is_a?(AST::TopLevelConstruct)
error "Classes, definitions, and nodes may only appear at toplevel or inside other classes", \
:line => stmt.context[:line], :file => stmt.context[:file]
end
end
result = val[0]
}
# The main list of valid statements
statement_or_declaration: resource
| virtualresource
| collection
| assignment
| casestatement
| ifstatement_begin
| unlessstatement
| import
| fstatement
| definition
| hostclass
| nodedef
| resourceoverride
| append
| relationship
keyword: AND
| CASE
| CLASS
| DEFAULT
| DEFINE
| ELSE
| ELSIF
| IF
| IN
| IMPORT
| INHERITS
| NODE
| OR
| UNDEF
| UNLESS
relationship: relationship_side edge relationship_side {
result = AST::Relationship.new(val[0], val[2], val[1][:value], ast_context)
}
| relationship edge relationship_side {
result = AST::Relationship.new(val[0], val[2], val[1][:value], ast_context)
}
relationship_side: resource
| resourceref
| collection
| variable
| quotedtext
| selector
| casestatement
| hasharrayaccesses
edge: IN_EDGE | OUT_EDGE | IN_EDGE_SUB | OUT_EDGE_SUB
fstatement: NAME LPAREN expressions RPAREN {
result = ast AST::Function,
:name => val[0][:value],
:line => val[0][:line],
:arguments => val[2],
:ftype => :statement
}
| NAME LPAREN expressions COMMA RPAREN {
result = ast AST::Function,
:name => val[0][:value],
:line => val[0][:line],
:arguments => val[2],
:ftype => :statement
} | NAME LPAREN RPAREN {
result = ast AST::Function,
:name => val[0][:value],
:line => val[0][:line],
:arguments => AST::ASTArray.new({}),
:ftype => :statement
}
| NAME funcvalues {
result = ast AST::Function,
:name => val[0][:value],
:line => val[0][:line],
:arguments => val[1],
:ftype => :statement
}
funcvalues: rvalue { result = aryfy(val[0]) }
# This rvalue could be an expression
| funcvalues COMMA rvalue {
val[0].push(val[2])
result = val[0]
}
expressions: expression { result = aryfy(val[0]) }
| expressions comma expression { result = val[0].push(val[2]) }
rvalue: quotedtext
| name
| type
| boolean
| selector
| variable
| array
| hasharrayaccesses
| resourceref
| funcrvalue
| undef
resource: classname LBRACE resourceinstances endsemi RBRACE {
@lexer.commentpop
result = ast(AST::Resource, :type => val[0], :instances => val[2])
} | classname LBRACE params endcomma RBRACE {
# This is a deprecated syntax.
error "All resource specifications require names"
} | type LBRACE params endcomma RBRACE {
# a defaults setting for a type
@lexer.commentpop
result = ast(AST::ResourceDefaults, :type => val[0].value, :parameters => val[2])
}
# Override a value set elsewhere in the configuration.
resourceoverride: resourceref LBRACE anyparams endcomma RBRACE {
@lexer.commentpop
result = ast AST::ResourceOverride, :object => val[0], :parameters => val[2]
}
# Exported and virtual resources; these don't get sent to the client
# unless they get collected elsewhere in the db.
virtualresource: at resource {
type = val[0]
if (type == :exported and ! Puppet[:storeconfigs])
Puppet.warning addcontext("You cannot collect without storeconfigs being set")
end
error "Defaults are not virtualizable" if val[1].is_a? AST::ResourceDefaults
method = type.to_s + "="
# Just mark our resource as exported and pass it through.
val[1].send(method, true)
result = val[1]
}
at: AT { result = :virtual }
| AT AT { result = :exported }
# A collection statement. Currently supports no arguments at all, but eventually
# will, I assume.
collection: type collectrhand LBRACE anyparams endcomma RBRACE {
@lexer.commentpop
type = val[0].value.downcase
args = {:type => type}
if val[1].is_a?(AST::CollExpr)
args[:query] = val[1]
args[:query].type = type
args[:form] = args[:query].form
else
args[:form] = val[1]
end
if args[:form] == :exported and ! Puppet[:storeconfigs]
Puppet.warning addcontext("You cannot collect exported resources without storeconfigs being set; the collection will be ignored")
end
args[:override] = val[3]
result = ast AST::Collection, args
}
| type collectrhand {
type = val[0].value.downcase
args = {:type => type }
if val[1].is_a?(AST::CollExpr)
args[:query] = val[1]
args[:query].type = type
args[:form] = args[:query].form
else
args[:form] = val[1]
end
if args[:form] == :exported and ! Puppet[:storeconfigs]
Puppet.warning addcontext("You cannot collect exported resources without storeconfigs being set; the collection will be ignored")
end
result = ast AST::Collection, args
}
collectrhand: LCOLLECT collstatements RCOLLECT {
if val[1]
result = val[1]
result.form = :virtual
else
result = :virtual
end
}
| LLCOLLECT collstatements RRCOLLECT {
if val[1]
result = val[1]
result.form = :exported
else
result = :exported
end
}
# A mini-language for handling collection comparisons. This is organized
# to avoid the need for precedence indications.
collstatements: nil
| collstatement
| collstatements colljoin collstatement {
result = ast AST::CollExpr, :test1 => val[0], :oper => val[1], :test2 => val[2]
}
collstatement: collexpr
| LPAREN collstatements RPAREN {
result = val[1]
result.parens = true
}
colljoin: AND { result=val[0][:value] }
| OR { result=val[0][:value] }
collexpr: colllval ISEQUAL expression {
result = ast AST::CollExpr, :test1 => val[0], :oper => val[1][:value], :test2 => val[2]
#result = ast AST::CollExpr
#result.push *val
}
| colllval NOTEQUAL expression {
result = ast AST::CollExpr, :test1 => val[0], :oper => val[1][:value], :test2 => val[2]
#result = ast AST::CollExpr
#result.push *val
}
colllval: variable
| name
resourceinst: resourcename COLON params endcomma {
result = ast AST::ResourceInstance, :title => val[0], :parameters => val[2]
}
resourceinstances: resourceinst { result = aryfy(val[0]) }
| resourceinstances SEMIC resourceinst {
val[0].push val[2]
result = val[0]
}
endsemi: # nothing
| SEMIC
undef: UNDEF {
result = ast AST::Undef, :value => :undef
}
name: NAME {
result = ast AST::Name, :value => val[0][:value], :line => val[0][:line]
}
type: CLASSREF {
result = ast AST::Type, :value => val[0][:value], :line => val[0][:line]
}
resourcename: quotedtext
| name
| type
| selector
| variable
| array
| hasharrayaccesses
assignment: VARIABLE EQUALS expression {
raise Puppet::ParseError, "Cannot assign to variables in other namespaces" if val[0][:value] =~ /::/
# this is distinct from referencing a variable
variable = ast AST::Name, :value => val[0][:value], :line => val[0][:line]
result = ast AST::VarDef, :name => variable, :value => val[2], :line => val[0][:line]
}
| hasharrayaccess EQUALS expression {
result = ast AST::VarDef, :name => val[0], :value => val[2]
}
append: VARIABLE APPENDS expression {
variable = ast AST::Name, :value => val[0][:value], :line => val[0][:line]
result = ast AST::VarDef, :name => variable, :value => val[2], :append => true, :line => val[0][:line]
}
params: # nothing
{
result = ast AST::ASTArray
}
| param { result = aryfy(val[0]) }
| params COMMA param {
val[0].push(val[2])
result = val[0]
}
param_name: NAME
| keyword
| BOOLEAN
param: param_name FARROW expression {
result = ast AST::ResourceParam, :param => val[0][:value], :line => val[0][:line], :value => val[2]
}
addparam: NAME PARROW expression {
result = ast AST::ResourceParam, :param => val[0][:value], :line => val[0][:line], :value => val[2],
:add => true
}
anyparam: param
| addparam
anyparams: # nothing
{
result = ast AST::ASTArray
}
| anyparam { result = aryfy(val[0]) }
| anyparams COMMA anyparam {
val[0].push(val[2])
result = val[0]
}
# We currently require arguments in these functions.
funcrvalue: NAME LPAREN expressions RPAREN {
result = ast AST::Function,
:name => val[0][:value], :line => val[0][:line],
:arguments => val[2],
:ftype => :rvalue
} | NAME LPAREN RPAREN {
result = ast AST::Function,
:name => val[0][:value], :line => val[0][:line],
:arguments => AST::ASTArray.new({}),
:ftype => :rvalue
}
quotedtext: STRING { result = ast AST::String, :value => val[0][:value], :line => val[0][:line] }
| DQPRE dqrval { result = ast AST::Concat, :value => [ast(AST::String,val[0])]+val[1], :line => val[0][:line] }
dqrval: expression dqtail { result = [val[0]] + val[1] }
dqtail: DQPOST { result = [ast(AST::String,val[0])] }
| DQMID dqrval { result = [ast(AST::String,val[0])] + val[1] }
boolean: BOOLEAN {
result = ast AST::Boolean, :value => val[0][:value], :line => val[0][:line]
}
resourceref: NAME LBRACK expressions RBRACK {
Puppet.warning addcontext("Deprecation notice: Resource references should now be capitalized")
result = ast AST::ResourceReference, :type => val[0][:value], :line => val[0][:line], :title => val[2]
} | type LBRACK expressions RBRACK {
result = ast AST::ResourceReference, :type => val[0].value, :title => val[2]
}
unlessstatement: UNLESS expression LBRACE statements RBRACE {
@lexer.commentpop
args = {
:test => ast(AST::Not, :value => val[1]),
:statements => val[3]
}
result = ast AST::IfStatement, args
}
| UNLESS expression LBRACE RBRACE {
@lexer.commentpop
args = {
:test => ast(AST::Not, :value => val[1]),
:statements => ast(AST::Nop)
}
result = ast AST::IfStatement, args
}
ifstatement_begin: IF ifstatement {
result = val[1]
}
ifstatement: expression LBRACE statements RBRACE else {
@lexer.commentpop
args = {
:test => val[0],
:statements => val[2]
}
args[:else] = val[4] if val[4]
result = ast AST::IfStatement, args
}
| expression LBRACE RBRACE else {
@lexer.commentpop
args = {
:test => val[0],
:statements => ast(AST::Nop)
}
args[:else] = val[3] if val[3]
result = ast AST::IfStatement, args
}
else: # nothing
| ELSIF ifstatement {
result = ast AST::Else, :statements => val[1]
}
| ELSE LBRACE statements RBRACE {
@lexer.commentpop
result = ast AST::Else, :statements => val[2]
}
| ELSE LBRACE RBRACE {
@lexer.commentpop
result = ast AST::Else, :statements => ast(AST::Nop)
}
# Unlike yacc/bison, it seems racc
# gives tons of shift/reduce warnings
# with the following syntax:
#
# expression: ...
# | expression arithop expressio { ... }
#
# arithop: PLUS | MINUS | DIVIDE | TIMES ...
#
# So I had to develop the expression by adding one rule
# per operator :-(
expression: rvalue
| hash
| expression IN expression {
result = ast AST::InOperator, :lval => val[0], :rval => val[2]
}
| expression MATCH regex {
result = ast AST::MatchOperator, :operator => val[1][:value], :lval => val[0], :rval => val[2]
}
| expression NOMATCH regex {
result = ast AST::MatchOperator, :operator => val[1][:value], :lval => val[0], :rval => val[2]
}
| expression PLUS expression {
result = ast AST::ArithmeticOperator, :operator => val[1][:value], :lval => val[0], :rval => val[2]
}
| expression MINUS expression {
result = ast AST::ArithmeticOperator, :operator => val[1][:value], :lval => val[0], :rval => val[2]
}
| expression DIV expression {
result = ast AST::ArithmeticOperator, :operator => val[1][:value], :lval => val[0], :rval => val[2]
}
| expression TIMES expression {
result = ast AST::ArithmeticOperator, :operator => val[1][:value], :lval => val[0], :rval => val[2]
}
| expression MODULO expression {
result = ast AST::ArithmeticOperator, :operator => val[1][:value], :lval => val[0], :rval => val[2]
}
| expression LSHIFT expression {
result = ast AST::ArithmeticOperator, :operator => val[1][:value], :lval => val[0], :rval => val[2]
}
| expression RSHIFT expression {
result = ast AST::ArithmeticOperator, :operator => val[1][:value], :lval => val[0], :rval => val[2]
}
| MINUS expression =UMINUS {
result = ast AST::Minus, :value => val[1]
}
| expression NOTEQUAL expression {
result = ast AST::ComparisonOperator, :operator => val[1][:value], :lval => val[0], :rval => val[2]
}
| expression ISEQUAL expression {
result = ast AST::ComparisonOperator, :operator => val[1][:value], :lval => val[0], :rval => val[2]
}
| expression GREATERTHAN expression {
result = ast AST::ComparisonOperator, :operator => val[1][:value], :lval => val[0], :rval => val[2]
}
| expression GREATEREQUAL expression {
result = ast AST::ComparisonOperator, :operator => val[1][:value], :lval => val[0], :rval => val[2]
}
| expression LESSTHAN expression {
result = ast AST::ComparisonOperator, :operator => val[1][:value], :lval => val[0], :rval => val[2]
}
| expression LESSEQUAL expression {
result = ast AST::ComparisonOperator, :operator => val[1][:value], :lval => val[0], :rval => val[2]
}
| NOT expression {
result = ast AST::Not, :value => val[1]
}
| expression AND expression {
result = ast AST::BooleanOperator, :operator => val[1][:value], :lval => val[0], :rval => val[2]
}
| expression OR expression {
result = ast AST::BooleanOperator, :operator => val[1][:value], :lval => val[0], :rval => val[2]
}
| LPAREN expression RPAREN {
result = val[1]
}
casestatement: CASE expression LBRACE caseopts RBRACE {
@lexer.commentpop
result = ast AST::CaseStatement, :test => val[1], :options => val[3]
}
caseopts: caseopt { result = aryfy(val[0]) }
| caseopts caseopt {
val[0].push val[1]
result = val[0]
}
caseopt: casevalues COLON LBRACE statements RBRACE {
@lexer.commentpop
result = ast AST::CaseOpt, :value => val[0], :statements => val[3]
} | casevalues COLON LBRACE RBRACE {
@lexer.commentpop
result = ast(
AST::CaseOpt,
:value => val[0],
:statements => ast(AST::BlockExpression)
)
}
casevalues: selectlhand { result = aryfy(val[0]) }
| casevalues COMMA selectlhand {
val[0].push(val[2])
result = val[0]
}
selector: selectlhand QMARK svalues {
result = ast AST::Selector, :param => val[0], :values => val[2]
}
svalues: selectval
| LBRACE sintvalues endcomma RBRACE {
@lexer.commentpop
result = val[1]
}
sintvalues: selectval
| sintvalues comma selectval {
if val[0].instance_of?(AST::ASTArray)
val[0].push(val[2])
result = val[0]
else
result = ast AST::ASTArray, :children => [val[0],val[2]]
end
}
selectval: selectlhand FARROW rvalue {
result = ast AST::ResourceParam, :param => val[0], :value => val[2]
}
selectlhand: name
| type
| quotedtext
| variable
| funcrvalue
| boolean
| undef
| hasharrayaccess
| DEFAULT {
result = ast AST::Default, :value => val[0][:value], :line => val[0][:line]
}
| regex
# These are only used for importing, and we don't interpolate there.
string: STRING { result = [val[0][:value]] }
strings: string
| strings COMMA string { result = val[0] += val[2] }
import: IMPORT strings {
val[1].each do |file|
import(file)
end
result = nil
}
# Disable definition inheritance for now. 8/27/06, luke
#definition: DEFINE NAME argumentlist parent LBRACE statements RBRACE {
definition: DEFINE classname argumentlist LBRACE statements RBRACE {
@lexer.commentpop
result = Puppet::Parser::AST::Definition.new(classname(val[1]),
ast_context(true).merge(:arguments => val[2], :code => val[4],
:line => val[0][:line]))
@lexer.indefine = false
#} | DEFINE NAME argumentlist parent LBRACE RBRACE {
} | DEFINE classname argumentlist LBRACE RBRACE {
@lexer.commentpop
result = Puppet::Parser::AST::Definition.new(classname(val[1]),
ast_context(true).merge(:arguments => val[2], :line => val[0][:line]))
@lexer.indefine = false
}
#hostclass: CLASS NAME argumentlist parent LBRACE statements RBRACE {
hostclass: CLASS classname argumentlist classparent LBRACE statements_and_declarations RBRACE {
@lexer.commentpop
# Our class gets defined in the parent namespace, not our own.
@lexer.namepop
result = Puppet::Parser::AST::Hostclass.new(classname(val[1]),
ast_context(true).merge(:arguments => val[2], :parent => val[3],
:code => val[5], :line => val[0][:line]))
} | CLASS classname argumentlist classparent LBRACE RBRACE {
@lexer.commentpop
# Our class gets defined in the parent namespace, not our own.
@lexer.namepop
result = Puppet::Parser::AST::Hostclass.new(classname(val[1]),
ast_context(true).merge(:arguments => val[2], :parent => val[3],
:line => val[0][:line]))
}
nodedef: NODE hostnames nodeparent LBRACE statements RBRACE {
@lexer.commentpop
result = Puppet::Parser::AST::Node.new(val[1],
ast_context(true).merge(:parent => val[2], :code => val[4],
:line => val[0][:line]))
} | NODE hostnames nodeparent LBRACE RBRACE {
@lexer.commentpop
result = Puppet::Parser::AST::Node.new(val[1], ast_context(true).merge(:parent => val[2], :line => val[0][:line]))
}
classname: NAME { result = val[0][:value] }
| CLASS { result = "class" }
# Multiple hostnames, as used for node names. These are all literal
# strings, not AST objects.
hostnames: nodename {
result = [result]
}
| hostnames COMMA nodename {
result = val[0]
result << val[2]
}
nodename: hostname {
result = ast AST::HostName, :value => val[0]
}
hostname: NAME { result = val[0][:value] }
| STRING { result = val[0][:value] }
| DEFAULT { result = val[0][:value] }
| regex
nil: {
result = nil
}
nothing: {
result = ast AST::ASTArray, :children => []
}
argumentlist: nil
| LPAREN nothing RPAREN {
result = nil
}
| LPAREN arguments endcomma RPAREN {
result = val[1]
result = [result] unless result[0].is_a?(Array)
}
arguments: argument
| arguments COMMA argument {
result = val[0]
result = [result] unless result[0].is_a?(Array)
result << val[2]
}
argument:
VARIABLE EQUALS expression { result = [val[0][:value], val[2]] }
| VARIABLE { result = [val[0][:value]] }
nodeparent: nil
| INHERITS hostname {
result = val[1]
}
classparent: nil
| INHERITS classnameordefault {
result = val[1]
}
classnameordefault: classname | DEFAULT
variable: VARIABLE {
result = ast AST::Variable, :value => val[0][:value], :line => val[0][:line]
}
array: LBRACK expressions RBRACK { result = val[1] }
| LBRACK expressions COMMA RBRACK { result = val[1] }
| LBRACK RBRACK { result = ast AST::ASTArray }
comma: FARROW
| COMMA
endcomma: # nothing
| COMMA { result = nil }
regex: REGEX {
result = ast AST::Regex, :value => val[0][:value]
}
hash: LBRACE hashpairs RBRACE {
+ @lexer.commentpop
if val[1].instance_of?(AST::ASTHash)
result = val[1]
else
result = ast AST::ASTHash, { :value => val[1] }
end
}
| LBRACE hashpairs COMMA RBRACE {
+ @lexer.commentpop
if val[1].instance_of?(AST::ASTHash)
result = val[1]
else
result = ast AST::ASTHash, { :value => val[1] }
end
} | LBRACE RBRACE {
+ @lexer.commentpop
result = ast AST::ASTHash
}
hashpairs: hashpair
| hashpairs COMMA hashpair {
if val[0].instance_of?(AST::ASTHash)
result = val[0].merge(val[2])
else
result = ast AST::ASTHash, :value => val[0]
result.merge(val[2])
end
}
hashpair: key FARROW expression {
result = ast AST::ASTHash, { :value => { val[0] => val[2] } }
}
key: NAME { result = val[0][:value] }
| quotedtext { result = val[0] }
hasharrayaccess: VARIABLE LBRACK expression RBRACK {
result = ast AST::HashOrArrayAccess, :variable => val[0][:value], :key => val[2]
}
hasharrayaccesses: hasharrayaccess
| hasharrayaccesses LBRACK expression RBRACK {
result = ast AST::HashOrArrayAccess, :variable => val[0], :key => val[2]
}
end
---- header ----
require 'puppet'
require 'puppet/parser/lexer'
require 'puppet/parser/ast'
module Puppet
class ParseError < Puppet::Error; end
class ImportError < Racc::ParseError; end
class AlreadyImportedError < ImportError; end
end
---- inner ----
# It got too annoying having code in a file that needs to be compiled.
require 'puppet/parser/parser_support'
# Make emacs happy
# Local Variables:
# mode: ruby
# End:
diff --git a/lib/puppet/parser/lexer.rb b/lib/puppet/parser/lexer.rb
index 65cbef92e..986183241 100644
--- a/lib/puppet/parser/lexer.rb
+++ b/lib/puppet/parser/lexer.rb
@@ -1,607 +1,608 @@
# the scanner/lexer
require 'forwardable'
require 'strscan'
require 'puppet'
require 'puppet/util/methodhelper'
module Puppet
class LexError < RuntimeError; end
end
module Puppet::Parser; end
class Puppet::Parser::Lexer
extend Forwardable
attr_reader :last, :file, :lexing_context, :token_queue
attr_accessor :line, :indefine
alias :indefine? :indefine
# Returns the position on the line.
# This implementation always returns nil. It is here for API reasons in Puppet::Error
# which needs to support both --parser current, and --parser future.
#
def pos
# Make the lexer comply with newer API. It does not produce a pos...
nil
end
def lex_error msg
raise Puppet::LexError.new(msg)
end
class Token
ALWAYS_ACCEPTABLE = Proc.new { |context| true }
include Puppet::Util::MethodHelper
attr_accessor :regex, :name, :string, :skip, :incr_line, :skip_text, :accumulate
alias skip? skip
alias accumulate? accumulate
def initialize(string_or_regex, name, options = {})
if string_or_regex.is_a?(String)
@name, @string = name, string_or_regex
@regex = Regexp.new(Regexp.escape(string_or_regex))
else
@name, @regex = name, string_or_regex
end
set_options(options)
@acceptable_when = ALWAYS_ACCEPTABLE
end
def to_s
string or @name.to_s
end
def acceptable?(context={})
@acceptable_when.call(context)
end
# Define when the token is able to match.
# This provides context that cannot be expressed otherwise, such as feature flags.
#
# @param block [Proc] a proc that given a context returns a boolean
def acceptable_when(block)
@acceptable_when = block
end
end
# Maintain a list of tokens.
class TokenList
extend Forwardable
attr_reader :regex_tokens, :string_tokens
def_delegator :@tokens, :[]
# Create a new token.
def add_token(name, regex, options = {}, &block)
raise(ArgumentError, "Token #{name} already exists") if @tokens.include?(name)
token = Token.new(regex, name, options)
@tokens[token.name] = token
if token.string
@string_tokens << token
@tokens_by_string[token.string] = token
else
@regex_tokens << token
end
token.meta_def(:convert, &block) if block_given?
token
end
def initialize
@tokens = {}
@regex_tokens = []
@string_tokens = []
@tokens_by_string = {}
end
# Look up a token by its value, rather than name.
def lookup(string)
@tokens_by_string[string]
end
# Define more tokens.
def add_tokens(hash)
hash.each do |regex, name|
add_token(name, regex)
end
end
# Sort our tokens by length, so we know once we match, we're done.
# This helps us avoid the O(n^2) nature of token matching.
def sort_tokens
@string_tokens.sort! { |a, b| b.string.length <=> a.string.length }
end
# Yield each token name and value in turn.
def each
@tokens.each {|name, value| yield name, value }
end
end
TOKENS = TokenList.new
TOKENS.add_tokens(
'[' => :LBRACK,
']' => :RBRACK,
'{' => :LBRACE,
'}' => :RBRACE,
'(' => :LPAREN,
')' => :RPAREN,
'=' => :EQUALS,
'+=' => :APPENDS,
'==' => :ISEQUAL,
'>=' => :GREATEREQUAL,
'>' => :GREATERTHAN,
'<' => :LESSTHAN,
'<=' => :LESSEQUAL,
'!=' => :NOTEQUAL,
'!' => :NOT,
',' => :COMMA,
'.' => :DOT,
':' => :COLON,
'@' => :AT,
'<<|' => :LLCOLLECT,
'|>>' => :RRCOLLECT,
'->' => :IN_EDGE,
'<-' => :OUT_EDGE,
'~>' => :IN_EDGE_SUB,
'<~' => :OUT_EDGE_SUB,
'<|' => :LCOLLECT,
'|>' => :RCOLLECT,
';' => :SEMIC,
'?' => :QMARK,
'\\' => :BACKSLASH,
'=>' => :FARROW,
'+>' => :PARROW,
'+' => :PLUS,
'-' => :MINUS,
'/' => :DIV,
'*' => :TIMES,
'%' => :MODULO,
'<<' => :LSHIFT,
'>>' => :RSHIFT,
'=~' => :MATCH,
'!~' => :NOMATCH,
%r{((::){0,1}[A-Z][-\w]*)+} => :CLASSREF,
"<string>" => :STRING,
"<dqstring up to first interpolation>" => :DQPRE,
"<dqstring between two interpolations>" => :DQMID,
"<dqstring after final interpolation>" => :DQPOST,
"<boolean>" => :BOOLEAN
)
module Contextual
QUOTE_TOKENS = [:DQPRE,:DQMID]
REGEX_INTRODUCING_TOKENS = [:NODE,:LBRACE,:RBRACE,:MATCH,:NOMATCH,:COMMA]
NOT_INSIDE_QUOTES = Proc.new do |context|
!QUOTE_TOKENS.include? context[:after]
end
INSIDE_QUOTES = Proc.new do |context|
QUOTE_TOKENS.include? context[:after]
end
IN_REGEX_POSITION = Proc.new do |context|
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
VARIABLE_AND_DASHES_ALLOWED = Proc.new do |context|
Contextual::DASHED_VARIABLES_ALLOWED.call(context) and TOKENS[:VARIABLE].acceptable?(context)
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|
[TOKENS[:NAME], value]
end
TOKENS[:NUMBER].acceptable_when Contextual::NOT_INSIDE_QUOTES
TOKENS.add_token :NAME, %r{((::)?[a-z0-9][-\w]*)(::[a-z0-9][-\w]*)*} do |lexer, value|
string_token = self
# we're looking for keywords here
if tmp = KEYWORDS.lookup(value)
string_token = tmp
if [:TRUE, :FALSE].include?(string_token.name)
value = eval(value)
string_token = TOKENS[:BOOLEAN]
end
end
[string_token, value]
end
[:NAME, :CLASSREF].each do |name_token|
TOKENS[name_token].acceptable_when Contextual::NOT_INSIDE_QUOTES
end
TOKENS.add_token :COMMENT, %r{#.*}, :accumulate => true, :skip => true do |lexer,value|
value.sub!(/# ?/,'')
[self, value]
end
TOKENS.add_token :MLCOMMENT, %r{/\*(.*?)\*/}m, :accumulate => true, :skip => true do |lexer, value|
lexer.line += value.count("\n")
value.sub!(/^\/\* ?/,'')
value.sub!(/ ?\*\/$/,'')
[self,value]
end
TOKENS.add_token :REGEX, %r{/[^/\n]*/} do |lexer, value|
# Make sure we haven't matched an escaped /
while value[-2..-2] == '\\'
other = lexer.scan_until(%r{/})
value += other
end
regex = value.sub(%r{\A/}, "").sub(%r{/\Z}, '').gsub("\\/", "/")
[self, Regexp.new(regex)]
end
TOKENS[:REGEX].acceptable_when Contextual::IN_REGEX_POSITION
TOKENS.add_token :RETURN, "\n", :skip => true, :incr_line => true, :skip_text => true
TOKENS.add_token :SQUOTE, "'" do |lexer, value|
[TOKENS[:STRING], lexer.slurpstring(value,["'"],:ignore_invalid_escapes).first ]
end
DQ_initial_token_types = {'$' => :DQPRE,'"' => :STRING}
DQ_continuation_token_types = {'$' => :DQMID,'"' => :DQPOST}
TOKENS.add_token :DQUOTE, /"/ do |lexer, value|
lexer.tokenize_interpolated_string(DQ_initial_token_types)
end
TOKENS.add_token :DQCONT, /\}/ do |lexer, value|
lexer.tokenize_interpolated_string(DQ_continuation_token_types)
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)
[TOKENS[:VARIABLE], value[1..-1]]
end
TOKENS[:DOLLAR_VAR_WITH_DASH].acceptable_when Contextual::DASHED_VARIABLES_ALLOWED
TOKENS.add_token :DOLLAR_VAR, %r{\$(::)?(\w+::)*\w+} do |lexer, value|
[TOKENS[:VARIABLE],value[1..-1]]
end
TOKENS.add_token :VARIABLE_WITH_DASH, %r{(?:::)?(?:[-\w]+::)*[-\w]+} do |lexer, value|
lexer.warn_if_variable_has_hyphen(value)
[TOKENS[:VARIABLE], value]
end
TOKENS[:VARIABLE_WITH_DASH].acceptable_when Contextual::VARIABLE_AND_DASHES_ALLOWED
TOKENS.add_token :VARIABLE, %r{(::)?(\w+::)*\w+}
TOKENS[:VARIABLE].acceptable_when Contextual::INSIDE_QUOTES
TOKENS.sort_tokens
@@pairs = {
"{" => "}",
"(" => ")",
"[" => "]",
"<|" => "|>",
"<<|" => "|>>"
}
KEYWORDS = TokenList.new
KEYWORDS.add_tokens(
"case" => :CASE,
"class" => :CLASS,
"default" => :DEFAULT,
"define" => :DEFINE,
"import" => :IMPORT,
"if" => :IF,
"elsif" => :ELSIF,
"else" => :ELSE,
"inherits" => :INHERITS,
"node" => :NODE,
"and" => :AND,
"or" => :OR,
"undef" => :UNDEF,
"false" => :FALSE,
"true" => :TRUE,
"in" => :IN,
"unless" => :UNLESS
)
def clear
initvars
end
def expected
return nil if @expected.empty?
name = @expected[-1]
TOKENS.lookup(name) or lex_error "Could not find expected token #{name}"
end
# scan the whole file
# basically just used for testing
def fullscan
array = []
self.scan { |token, str|
# Ignore any definition nesting problems
@indefine = false
array.push([token,str])
}
array
end
def file=(file)
@file = file
@line = 1
- contents = Puppet::FileSystem::File.exist?(file) ? File.read(file) : ""
+ contents = Puppet::FileSystem.exist?(file) ? Puppet::FileSystem.read(file) : ""
@scanner = StringScanner.new(contents)
end
def_delegator :@token_queue, :shift, :shift_token
def find_string_token
# We know our longest string token is three chars, so try each size in turn
# until we either match or run out of chars. This way our worst-case is three
# tries, where it is otherwise the number of string token we have. Also,
# the lookups are optimized hash lookups, instead of regex scans.
#
s = @scanner.peek(3)
token = TOKENS.lookup(s[0,3]) || TOKENS.lookup(s[0,2]) || TOKENS.lookup(s[0,1])
[ token, token && @scanner.scan(token.regex) ]
end
# Find the next token that matches a regex. We look for these first.
def find_regex_token
best_token = nil
best_length = 0
# I tried optimizing based on the first char, but it had
# a slightly negative affect and was a good bit more complicated.
TOKENS.regex_tokens.each do |token|
if length = @scanner.match?(token.regex) and token.acceptable?(lexing_context)
# We've found a longer match
if length > best_length
best_length = length
best_token = token
end
end
end
return best_token, @scanner.scan(best_token.regex) if best_token
end
# Find the next token, returning the string and the token.
def find_token
shift_token || find_regex_token || find_string_token
end
def initialize
initvars
end
def initvars
@line = 1
@previous_token = nil
@scanner = nil
@file = nil
# AAARRGGGG! okay, regexes in ruby are bloody annoying
# no one else has "\n" =~ /\s/
@skip = %r{[ \t\r]+}
@namestack = []
@token_queue = []
@indefine = false
@expected = []
@commentstack = [ ['', @line] ]
@lexing_context = {
:after => nil,
:start_of_line => true,
:string_interpolation_depth => 0
}
end
# Make any necessary changes to the token and/or value.
def munge_token(token, value)
@line += 1 if token.incr_line
skip if token.skip_text
return if token.skip and not token.accumulate?
token, value = token.convert(self, value) if token.respond_to?(:convert)
return unless token
if token.accumulate?
comment = @commentstack.pop
comment[0] << value + "\n"
@commentstack.push(comment)
end
return if token.skip
return token, { :value => value, :line => @line }
end
# Handling the namespace stack
def_delegator :@namestack, :pop, :namepop
# This value might have :: in it, but we don't care -- it'll be handled
# normally when joining, and when popping we want to pop this full value,
# however long the namespace is.
def_delegator :@namestack, :<<, :namestack
# Collect the current namespace.
def namespace
@namestack.join("::")
end
def_delegator :@scanner, :rest
# this is the heart of the lexer
def scan
#Puppet.debug("entering scan")
lex_error "Invalid or empty string" unless @scanner
# Skip any initial whitespace.
skip
until token_queue.empty? and @scanner.eos? do
matched_token, value = find_token
# error out if we didn't match anything at all
lex_error "Could not match #{@scanner.rest[/^(\S+|\s+|.*)/]}" unless matched_token
newline = matched_token.name == :RETURN
# this matches a blank line; eat the previously accumulated comments
getcomment if lexing_context[:start_of_line] and newline
lexing_context[:start_of_line] = newline
final_token, token_value = munge_token(matched_token, value)
unless final_token
skip
next
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
+ final_token_name = final_token.name
+ 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
value = token_value[:value]
- if match = @@pairs[value] and final_token.name != :DQUOTE and final_token.name != :SQUOTE
+ if match = @@pairs[value] and final_token_name != :DQUOTE and final_token_name != :SQUOTE
@expected << match
- elsif exp = @expected[-1] and exp == value and final_token.name != :DQUOTE and final_token.name != :SQUOTE
+ elsif exp = @expected[-1] and exp == value and final_token_name != :DQUOTE and final_token_name != :SQUOTE
@expected.pop
end
- if final_token.name == :LBRACE or final_token.name == :LPAREN
+ if final_token_name == :LBRACE or final_token_name == :LPAREN
commentpush
end
- if final_token.name == :RPAREN
+ if final_token_name == :RPAREN
commentpop
end
- yield [final_token.name, token_value]
+ yield [final_token_name, token_value]
if @previous_token
namestack(value) if @previous_token.name == :CLASS and value != '{'
if @previous_token.name == :DEFINE
if indefine?
msg = "Cannot nest definition #{value} inside #{@indefine}"
self.indefine = false
raise Puppet::ParseError, msg
end
@indefine = value
end
end
@previous_token = final_token
skip
end
@scanner = nil
# This indicates that we're done parsing.
yield [false,false]
end
# Skip any skipchars in our remaining string.
def skip
@scanner.skip(@skip)
end
# Provide some limited access to the scanner, for those
# tokens that need it.
def_delegator :@scanner, :scan_until
# we've encountered the start of a string...
# slurp in the rest of the string and return it
def slurpstring(terminators,escapes=%w{ \\ $ ' " r n t s }+["\n"],ignore_invalid_escapes=false)
# we search for the next quote that isn't preceded by a
# backslash; the caret is there to match empty strings
str = @scanner.scan_until(/([^\\]|^|[^\\])([\\]{2})*[#{terminators}]/) or lex_error "Unclosed quote after '#{last}' in '#{rest}'"
@line += str.count("\n") # literal carriage returns add to the line count.
str.gsub!(/\\(.)/m) {
ch = $1
if escapes.include? ch
case ch
when 'r'; "\r"
when 'n'; "\n"
when 't'; "\t"
when 's'; " "
when "\n"; ''
else ch
end
else
Puppet.warning "Unrecognised escape sequence '\\#{ch}'#{file && " in file #{file}"}#{line && " at line #{line}"}" unless ignore_invalid_escapes
"\\#{ch}"
end
}
[ str[0..-2],str[-1,1] ]
end
def tokenize_interpolated_string(token_type,preamble='')
value,terminator = slurpstring('"$')
token_queue << [TOKENS[token_type[terminator]],preamble+value]
variable_regex = if Puppet[:allow_variables_with_dashes]
TOKENS[:VARIABLE_WITH_DASH].regex
else
TOKENS[:VARIABLE].regex
end
if terminator != '$' or @scanner.scan(/\{/)
token_queue.shift
elsif var_name = @scanner.scan(variable_regex)
warn_if_variable_has_hyphen(var_name)
token_queue << [TOKENS[:VARIABLE],var_name]
tokenize_interpolated_string(DQ_continuation_token_types)
else
tokenize_interpolated_string(token_type,token_queue.pop.last + terminator)
end
end
# just parse a string, not a whole file
def string=(string)
@scanner = StringScanner.new(string)
end
# returns the content of the currently accumulated content cache
def commentpop
@commentstack.pop[0]
end
def getcomment(line = nil)
comment = @commentstack.last
if line.nil? or comment[1] <= line
@commentstack.pop
@commentstack.push(['', @line])
return comment[0]
end
''
end
def commentpush
@commentstack.push(['', @line])
end
def warn_if_variable_has_hyphen(var_name)
if var_name.include?('-')
Puppet.deprecation_warning("Using `-` in variable names is deprecated at #{file || '<string>'}:#{line}. See http://links.puppetlabs.com/puppet-hyphenated-variable-deprecation")
end
end
end
diff --git a/lib/puppet/parser/parser.rb b/lib/puppet/parser/parser.rb
index cb6790ef1..16872ee34 100644
--- a/lib/puppet/parser/parser.rb
+++ b/lib/puppet/parser/parser.rb
@@ -1,2556 +1,2559 @@
#
# DO NOT MODIFY!!!!
# This file is automatically generated by Racc 1.4.9
# from Racc grammer file "".
#
require 'racc/parser.rb'
require 'puppet'
require 'puppet/parser/lexer'
require 'puppet/parser/ast'
module Puppet
class ParseError < Puppet::Error; end
class ImportError < Racc::ParseError; end
class AlreadyImportedError < ImportError; end
end
module Puppet
module Parser
class Parser < Racc::Parser
-module_eval(<<'...end grammar.ra/module_eval...', 'grammar.ra', 797)
+module_eval(<<'...end grammar.ra/module_eval...', 'grammar.ra', 799)
# It got too annoying having code in a file that needs to be compiled.
require 'puppet/parser/parser_support'
# Make emacs happy
# Local Variables:
# mode: ruby
# End:
...end grammar.ra/module_eval...
##### State transition tables begin ###
clist = [
'35,36,199,198,246,159,-130,86,-112,82,277,357,361,379,356,215,210,159',
'276,-197,360,158,85,158,211,213,212,214,39,248,48,49,267,33,50,158,51',
'37,26,-129,40,46,30,35,36,32,84,217,216,31,398,203,204,206,205,208,209',
'-122,201,202,52,-130,-130,-130,-130,200,38,207,35,36,278,39,86,48,49',
'350,33,50,90,51,37,26,89,40,46,30,35,36,32,-178,94,253,31,257,280,257',
'364,274,273,92,93,215,210,52,257,255,226,336,62,38,211,213,212,214,39',
'338,48,49,254,33,50,339,51,37,26,347,40,46,30,35,36,32,-180,217,216',
'31,310,203,204,206,205,208,209,341,201,202,52,35,36,274,273,200,38,207',
'223,303,-178,39,304,48,49,-177,33,50,185,51,37,26,95,40,46,30,35,36',
'32,190,-184,281,31,397,189,344,90,201,202,226,89,215,210,52,200,357',
'250,32,356,38,211,213,212,214,39,-179,48,49,268,33,50,90,51,37,26,89',
'40,46,30,35,36,32,185,217,216,31,395,203,204,206,205,208,209,190,201',
'202,52,119,189,201,202,200,38,207,201,202,200,39,119,48,49,200,33,50',
'185,51,37,26,267,40,46,30,35,36,32,190,185,90,31,392,189,89,119,265',
'375,118,337,190,120,52,257,280,189,302,-96,38,118,257,263,120,39,-185',
'48,49,52,33,50,52,51,37,26,-123,40,46,30,35,36,32,52,103,118,31,252',
'120,279,206,205,251,257,280,201,202,52,250,243,243,262,200,38,207,257',
'263,52,137,135,139,134,136,80,132,140,141,177,168,240,131,162,35,36',
'82,32,180,142,130,163,271,94,-184,274,273,203,204,206,205,-183,52,-181',
'201,202,62,138,144,-180,353,200,39,207,48,49,354,33,50,330,51,37,26',
'-182,40,46,30,35,36,32,-177,52,-179,31,367,203,204,206,205,157,368,370',
'201,202,52,35,36,371,372,200,38,207,122,327,326,39,110,48,49,109,33',
'50,267,51,37,26,334,40,46,30,35,36,32,91,81,62,31,377,80,90,323,-179',
'37,128,382,40,46,52,35,36,32,383,-180,38,31,385,61,-228,39,387,48,49',
'388,33,50,52,51,37,26,323,40,46,30,35,36,32,319,110,393,31,308,80,90',
'317,305,37,128,158,40,46,52,53,399,32,400,,38,31,,,,39,,48,49,,33,50',
'52,51,37,26,,40,46,30,230,,32,,,,31,,,215,210,56,57,58,59,,,52,211,213',
'212,214,,38,203,204,206,205,208,209,,201,202,56,57,58,59,,200,,207,217',
'216,210,,203,204,206,205,208,209,211,201,202,228,-38,-38,-38,-38,200',
',207,,215,210,-44,-44,-44,-44,,,,211,213,212,214,,,203,204,206,205,208',
'209,,201,202,-40,-40,-40,-40,,200,,207,217,216,,,203,204,206,205,208',
'209,,201,202,229,,,,,200,,207,,215,210,206,205,,,,201,202,211,213,212',
'214,,200,,207,,,,35,36,,,103,,104,,,,,,217,216,,,203,204,206,205,208',
'209,102,201,202,35,36,,,103,200,104,207,80,,,,37,77,,,46,,,,32,101,102',
',31,35,36,100,,103,,104,,80,,52,,37,77,,,46,,,,32,101,102,,31,35,36',
'100,,103,,104,,80,,52,,37,77,,,46,,,,32,101,102,,31,35,36,100,,103,',
'104,,80,,52,,37,77,,,46,,,,32,101,102,,31,35,36,100,,103,,104,,80,,52',
',37,77,,,46,,,,32,101,102,,31,35,36,100,,103,,104,,80,,52,,37,77,,,46',
',,,32,101,102,,31,35,36,100,,103,,104,,80,,52,,37,77,,,46,,,,32,101',
'102,,31,35,36,100,,103,,104,,80,,52,,37,77,,,46,,,,32,101,102,,31,35',
'36,100,,103,,104,,80,,52,,37,77,,,46,,,,32,101,102,,31,35,36,100,,103',
',104,,80,,52,,37,77,,,46,,,,32,101,102,,31,35,36,100,,103,,104,,80,',
'52,,37,77,,,46,,,,32,101,102,,31,35,36,100,,103,,104,,80,,52,,37,77',
',,46,35,36,,32,101,102,155,31,,,100,,,35,36,,80,103,52,,37,77,,,46,',
',,32,101,35,36,31,80,103,100,104,37,231,,,46,,52,,32,80,,,31,37,77,102',
',46,35,36,,32,103,52,104,31,80,,35,36,37,77,,,46,358,52,,32,101,102',
',31,,,100,35,36,,,,80,,52,,37,77,,,46,,80,,32,101,37,231,31,,46,100',
',,32,,,,31,52,80,,,,37,231,,,46,52,,,32,35,36,,31,103,,104,,,,,,,,52',
',,35,36,,,103,102,104,,,,,,,,,,80,,,,37,77,102,,46,,,,32,101,,,31,80',
',100,,37,77,,,46,,52,,32,101,35,36,31,,103,100,104,,,,35,36,,52,103',
',104,,,,35,36,102,,103,161,104,,,,,,102,80,,,,37,77,,,46,102,80,,32',
'101,37,77,31,,46,100,80,,32,101,37,77,31,52,46,100,,,32,101,35,36,31',
'52,103,100,104,,,,35,36,,52,103,,104,,,,35,36,102,,103,,104,,,,,,102',
'80,,,,37,77,,,46,102,80,,32,101,37,77,31,,46,100,80,,32,101,37,77,31',
'52,46,100,,,32,101,35,36,31,52,103,100,104,,,,35,36,,52,78,,-197,,,',
'35,36,102,,103,,104,,,,,,63,80,,,,37,77,,,46,102,80,,32,101,37,77,31',
',46,100,80,,32,,37,77,31,52,46,,,,32,101,35,36,31,52,103,100,104,,,',
'35,36,,52,103,,104,,,,35,36,102,,103,,104,,,,,,102,80,,,,37,77,,,46',
'102,80,,32,101,37,77,31,,46,100,80,,32,101,37,77,31,52,46,100,,,32,101',
'35,36,31,52,103,100,104,,,,35,36,,52,103,,104,,,,35,36,102,,103,,104',
',,,,,102,80,,,,37,77,,,46,102,80,,32,101,37,77,31,,46,100,80,,32,101',
'37,77,31,52,46,100,,,32,101,35,36,31,52,103,100,104,,,,35,36,,52,103',
',104,,,,35,36,102,,103,,104,,,,,,102,80,,,,37,77,,,46,102,80,,32,101',
'37,77,31,,46,100,80,,32,101,37,77,31,52,46,100,,,32,101,35,36,31,52',
'103,100,104,,,,35,36,,52,103,161,104,,,,35,36,102,,103,,104,,,,,,102',
'80,,,,37,77,,,46,102,80,,32,101,37,77,31,,46,100,80,,32,101,37,77,31',
'52,46,100,,,32,101,35,36,31,52,103,100,104,,,,35,36,,52,,,,,,,35,36',
'102,,,,234,,,,35,36,,80,103,,,37,77,,,46,,80,,32,101,37,231,31,,46,100',
'80,,32,,37,231,31,52,46,,80,,32,,37,231,31,52,46,35,36,,32,103,,104',
'31,52,,35,36,,,103,,104,,52,,35,36,102,,,,,,,,,,102,80,,,,37,77,,,46',
',80,,32,101,37,77,31,,46,100,80,,32,101,37,231,31,52,46,100,,,32,,35',
'36,31,52,103,,104,,,,,,,52,,,,35,36,,,103,102,104,,,,,,,,,,80,,,,37',
'77,102,260,46,,35,36,32,101,103,,31,80,,100,,37,77,,,46,,52,,32,101',
'35,36,31,,103,100,104,,,,,,80,52,,,37,77,,,46,,102,,32,,210,,31,,,,',
'80,211,,,37,77,52,,46,,,,32,101,215,210,31,,,100,,,,211,213,212,214',
'52,203,204,206,205,208,209,,201,202,210,,,,,200,,207,211,217,216,,,203',
'204,206,205,208,209,,201,202,215,210,,,,200,,207,,211,213,212,214,203',
'204,206,205,208,209,,201,202,,210,,,,200,,207,,211,217,216,,,203,204',
'206,205,208,209,,201,202,215,210,,,,200,,207,,211,213,212,214,203,204',
'206,205,208,209,,201,202,,,,,,200,,207,,,217,216,,,203,204,206,205,208',
'209,,201,202,215,210,,,,200,,207,,211,213,212,214,203,204,206,205,208',
'209,,201,202,,,,,,200,,207,,,217,216,,,203,204,206,205,208,209,,201',
'202,215,210,,,,200,,207,301,211,213,212,214,,,,,215,210,,,,,,,,211,213',
'212,214,,,217,216,,,203,204,206,205,208,209,,201,202,,,,,,200,,207,203',
'204,206,205,208,209,,201,202,215,210,,,,200,,207,,211,213,212,214,,',
',,,,,,215,210,,,,,,,,211,213,212,214,,,203,204,206,205,208,209,,201',
'202,,,,,,200,,207,217,216,,,203,204,206,205,208,209,,201,202,215,210',
',,,200,,207,,211,213,212,214,,,,,,,,,,,,,,,,,,,,,216,,,203,204,206,205',
'208,209,,201,202,215,210,,,,200,,207,,211,213,212,214,,,,,,,,,,,,,,',
',,,,,217,216,,,203,204,206,205,208,209,,201,202,215,210,,,,200,,207',
',211,213,212,214,,,,,215,210,,,,,,,,211,213,212,214,,,217,216,,,203',
'204,206,205,208,209,,201,202,,,,,,200,,207,203,204,206,205,208,209,',
'201,202,215,210,,,,200,,207,,211,213,212,214,,,,,,,,,,,,,,,,,,,,217',
'216,,,203,204,206,205,208,209,,201,202,215,210,,,,200,,207,,211,213',
'212,214,,,,,,,,,,,,,,,,,,,,217,216,,,203,204,206,205,208,209,,201,202',
',,,,,200,,207,137,135,139,134,136,,132,140,141,148,179,,131,133,,,,',
',142,130,143,137,135,139,134,136,,132,140,141,148,146,,131,133,,138',
'144,,,142,130,143,137,135,139,134,136,,132,140,141,148,146,,131,133',
',138,144,,,142,130,143,137,135,139,134,136,,132,140,141,148,179,,131',
'133,,138,144,,,142,130,143,137,135,139,134,136,,132,140,141,148,179',
',131,133,,138,144,,,142,130,143,137,135,139,134,136,,132,140,141,148',
'146,,131,133,,138,144,,,142,130,143,,,,,,,,,,,,,,,,138,144' ]
racc_action_table = arr = ::Array.new(2588, nil)
idx = 0
clist.each do |str|
str.split(',', -1).each do |i|
arr[idx] = i.to_i unless i.empty?
idx += 1
end
end
clist = [
'0,0,97,97,115,77,262,28,168,28,186,354,313,345,354,97,97,128,186,128',
'313,168,28,77,97,97,97,97,0,115,0,0,178,0,0,128,0,0,0,177,0,0,0,391',
'391,0,28,97,97,0,391,97,97,97,97,97,97,254,97,97,0,262,262,262,262,97',
'0,97,304,304,191,391,68,391,391,304,391,391,49,391,391,391,49,391,391',
'391,2,2,391,68,33,153,391,259,259,315,315,191,191,33,33,153,153,391',
'154,154,304,259,175,391,153,153,153,153,2,263,2,2,154,2,2,264,2,2,2',
'275,2,2,2,229,229,2,173,153,153,2,229,153,153,153,153,153,153,266,153',
'153,2,104,104,275,275,153,2,153,104,222,171,229,222,229,229,170,229',
'229,84,229,229,229,34,229,229,229,383,383,229,84,34,195,229,383,84,269',
'29,288,288,104,29,195,195,229,288,310,270,29,310,229,195,195,195,195',
'383,169,383,383,166,383,383,50,383,383,383,50,383,383,383,382,382,383',
'185,195,195,383,382,195,195,195,195,195,195,185,195,195,383,51,185,290',
'290,195,383,195,289,289,290,382,248,382,382,289,382,382,272,382,382',
'382,165,382,382,382,372,372,382,272,85,326,382,372,272,326,246,164,326',
'51,261,85,51,382,261,261,85,221,163,382,248,221,221,248,372,162,372',
'372,201,372,372,51,372,372,372,155,372,372,372,81,81,372,248,81,246',
'372,149,246,192,287,287,146,192,192,287,287,372,145,114,113,160,287',
'372,287,160,160,246,81,81,81,81,81,81,81,81,81,81,81,112,81,81,306,306',
'87,81,83,81,81,81,181,80,79,181,181,292,292,292,292,76,81,75,292,292',
'73,81,81,71,307,292,306,292,306,306,309,306,306,249,306,306,306,69,306',
'306,306,319,319,306,67,202,66,306,319,291,291,291,291,64,320,321,291',
'291,306,55,55,323,324,291,306,291,53,245,244,319,48,319,319,41,319,319',
'343,319,319,319,255,319,319,319,327,327,319,30,27,25,319,327,55,55,243',
'23,55,55,357,55,55,319,60,60,55,360,22,319,55,362,21,364,327,366,327',
'327,369,327,327,55,327,327,327,370,327,327,327,228,228,327,241,240,376',
'327,228,60,60,235,225,60,60,231,60,60,327,1,394,60,396,,327,60,,,,228',
',228,228,,228,228,60,228,228,228,,228,228,228,108,,228,,,,228,,,108',
'108,20,20,20,20,,,228,108,108,108,108,,228,294,294,294,294,294,294,',
'294,294,19,19,19,19,,294,,294,108,108,296,,108,108,108,108,108,108,296',
'108,108,105,5,5,5,5,108,,108,,105,105,9,9,9,9,,,,105,105,105,105,,,296',
'296,296,296,296,296,,296,296,7,7,7,7,,296,,296,105,105,,,105,105,105',
'105,105,105,,105,105,107,,,,,105,,105,,107,107,286,286,,,,286,286,107',
'107,107,107,,286,,286,,,,204,204,,,204,,204,,,,,,107,107,,,107,107,107',
'107,107,107,204,107,107,36,36,,,36,107,36,107,204,,,,204,204,,,204,',
',,204,204,36,,204,38,38,204,,38,,38,,36,,204,,36,36,,,36,,,,36,36,38',
',36,39,39,36,,39,,39,,38,,36,,38,38,,,38,,,,38,38,39,,38,40,40,38,,40',
',40,,39,,38,,39,39,,,39,,,,39,39,40,,39,214,214,39,,214,,214,,40,,39',
',40,40,,,40,,,,40,40,214,,40,213,213,40,,213,,213,,214,,40,,214,214',
',,214,,,,214,214,213,,214,212,212,214,,212,,212,,213,,214,,213,213,',
',213,,,,213,213,212,,213,211,211,213,,211,,211,,212,,213,,212,212,,',
'212,,,,212,212,211,,212,210,210,212,,210,,210,,211,,212,,211,211,,,211',
',,,211,211,210,,211,209,209,211,,209,,209,,210,,211,,210,210,,,210,',
',,210,210,209,,210,356,356,210,,356,,356,,209,,210,,209,209,,,209,,',
',209,209,356,,209,63,63,209,,63,,63,,356,,209,,356,356,,,356,361,361',
',356,356,63,63,356,,,356,,,317,317,,63,317,356,,63,63,,,63,,,,63,63',
'208,208,63,361,208,63,208,361,361,,,361,,63,,361,317,,,361,317,317,208',
',317,207,207,,317,207,361,207,317,208,,311,311,208,208,,,208,311,317',
',208,208,207,,208,,,208,363,363,,,,207,,208,,207,207,,,207,,311,,207',
'207,311,311,207,,311,207,,,311,,,,311,207,363,,,,363,363,,,363,311,',
',363,305,305,,363,305,,305,,,,,,,,363,,,206,206,,,206,305,206,,,,,,',
',,,305,,,,305,305,206,,305,,,,305,305,,,305,206,,305,,206,206,,,206',
',305,,206,206,205,205,206,,205,206,205,,,,215,215,,206,215,,215,,,,78',
'78,205,,78,78,78,,,,,,215,205,,,,205,205,,,205,78,215,,205,205,215,215',
'205,,215,205,78,,215,215,78,78,215,205,78,215,,,78,78,203,203,78,215',
'203,78,203,,,,371,371,,78,371,,371,,,,200,200,203,,200,,200,,,,,,371',
'203,,,,203,203,,,203,200,371,,203,203,371,371,203,,371,203,200,,371',
'371,200,200,371,203,200,371,,,200,200,199,199,200,371,199,200,199,,',
',26,26,,200,26,,26,,,,251,251,199,,251,,251,,,,,,26,199,,,,199,199,',
',199,251,26,,199,199,26,26,199,,26,199,251,,26,,251,251,26,199,251,',
',,251,251,86,86,251,26,86,251,86,,,,252,252,,251,252,,252,,,,92,92,86',
',92,,92,,,,,,252,86,,,,86,86,,,86,92,252,,86,86,252,252,86,,252,86,92',
',252,252,92,92,252,86,92,252,,,92,92,93,93,92,252,93,92,93,,,,94,94',
',92,94,,94,,,,95,95,93,,95,,95,,,,,,94,93,,,,93,93,,,93,95,94,,93,93',
'94,94,93,,94,93,95,,94,94,95,95,94,93,95,94,,,95,95,216,216,95,94,216',
'95,216,,,,100,100,,95,100,,100,,,,101,101,216,,101,,101,,,,,,100,216',
',,,216,216,,,216,101,100,,216,216,100,100,216,,100,216,101,,100,100',
'101,101,100,216,101,100,,,101,101,102,102,101,100,102,101,102,,,,103',
'103,,101,103,103,103,,,,256,256,102,,256,,256,,,,,,103,102,,,,102,102',
',,102,256,103,,102,102,103,103,102,,103,102,256,,103,103,256,256,103',
'102,256,103,,,256,256,217,217,256,103,217,256,217,,,,234,234,,256,,',
',,,,109,109,217,,,,109,,,,265,265,,217,265,,,217,217,,,217,,234,,217',
'217,234,234,217,,234,217,109,,234,,109,109,234,217,109,,265,,109,,265',
'265,109,234,265,276,276,,265,276,,276,265,109,,277,277,,,277,,277,,265',
',230,230,276,,,,,,,,,,277,276,,,,276,276,,,276,,277,,276,276,277,277',
'276,,277,276,230,,277,277,230,230,277,276,230,277,,,230,,159,159,230',
'277,159,,159,,,,,,,230,,,,158,158,,,158,159,158,,,,,,,,,,159,,,,159',
'159,158,158,159,,157,157,159,159,157,,159,158,,159,,158,158,,,158,,159',
',158,158,62,62,158,,62,158,62,,,,,,157,158,,,157,157,,,157,,62,,157',
',298,,157,,,,,62,298,,,62,62,157,,62,,,,62,62,335,335,62,,,62,,,,335',
'335,335,335,62,298,298,298,298,298,298,,298,298,295,,,,,298,,298,295',
'335,335,,,335,335,335,335,335,335,,335,335,390,390,,,,335,,335,,390',
'390,390,390,295,295,295,295,295,295,,295,295,,297,,,,295,,295,,297,390',
'390,,,390,390,390,390,390,390,,390,390,156,156,,,,390,,390,,156,156',
'156,156,297,297,297,297,297,297,,297,297,,,,,,297,,297,,,156,156,,,156',
'156,156,156,156,156,,156,156,194,194,,,,156,,156,,194,194,194,194,293',
'293,293,293,293,293,,293,293,,,,,,293,,293,,,194,194,,,194,194,194,194',
'194,194,,194,194,220,220,,,,194,,194,220,220,220,220,220,,,,,348,348',
',,,,,,,348,348,348,348,,,220,220,,,220,220,220,220,220,220,,220,220',
',,,,,220,,220,348,348,348,348,348,348,,348,348,349,349,,,,348,,348,',
'349,349,349,349,,,,,,,,,333,333,,,,,,,,333,333,333,333,,,349,349,349',
'349,349,349,,349,349,,,,,,349,,349,333,333,,,333,333,333,333,333,333',
',333,333,300,300,,,,333,,333,,300,300,300,300,,,,,,,,,,,,,,,,,,,,,300',
',,300,300,300,300,300,300,,300,300,352,352,,,,300,,300,,352,352,352',
'352,,,,,,,,,,,,,,,,,,,,352,352,,,352,352,352,352,352,352,,352,352,196',
'196,,,,352,,352,,196,196,196,196,,,,,299,299,,,,,,,,299,299,299,299',
',,196,196,,,196,196,196,196,196,196,,196,196,,,,,,196,,196,299,299,299',
'299,299,299,,299,299,193,193,,,,299,,299,,193,193,193,193,,,,,,,,,,',
',,,,,,,,,193,193,,,193,193,193,193,193,193,,193,193,332,332,,,,193,',
'193,,332,332,332,332,,,,,,,,,,,,,,,,,,,,332,332,,,332,332,332,332,332',
'332,,332,332,,,,,,332,,332,268,268,268,268,268,,268,268,268,268,268',
',268,268,,,,,,268,268,268,180,180,180,180,180,,180,180,180,180,180,',
'180,180,,268,268,,,180,180,180,61,61,61,61,61,,61,61,61,61,61,,61,61',
',180,180,,,61,61,61,267,267,267,267,267,,267,267,267,267,267,,267,267',
',61,61,,,267,267,267,82,82,82,82,82,,82,82,82,82,82,,82,82,,267,267',
',,82,82,82,250,250,250,250,250,,250,250,250,250,250,,250,250,,82,82',
',,250,250,250,,,,,,,,,,,,,,,,250,250' ]
racc_action_check = arr = ::Array.new(2588, nil)
idx = 0
clist.each do |str|
str.split(',', -1).each do |i|
arr[idx] = i.to_i unless i.empty?
idx += 1
end
end
racc_action_pointer = [
-2, 490, 84, nil, nil, 507, nil, 539, nil, 517,
nil, nil, nil, nil, nil, nil, nil, nil, nil, 485,
463, 447, 428, 417, nil, 428, 1304, 425, 1, 146,
388, nil, nil, 84, 153, nil, 675, nil, 700, 725,
750, 395, nil, nil, nil, nil, nil, nil, 413, 42,
171, 231, nil, 411, nil, 402, nil, nil, nil, nil,
445, 2453, 1832, 950, 386, nil, 368, 366, 66, 359,
nil, 345, nil, 359, nil, 339, 337, -1, 1180, 330,
346, 299, 2497, 339, 140, 238, 1361, 337, nil, nil,
nil, nil, 1381, 1428, 1438, 1448, nil, -2, nil, nil,
1505, 1515, 1562, 1572, 145, 561, nil, 615, 507, 1649,
nil, nil, 328, 297, 296, -8, nil, nil, nil, nil,
nil, nil, nil, nil, nil, nil, nil, nil, 11, nil,
nil, nil, nil, nil, nil, nil, nil, nil, nil, nil,
nil, nil, nil, nil, nil, 307, 261, nil, nil, 297,
nil, nil, nil, 84, 93, 274, 1954, 1811, 1785, 1768,
315, nil, 264, 257, 228, 242, 184, nil, -3, 178,
138, 133, nil, 109, nil, 102, nil, 16, 20, nil,
2431, 305, nil, nil, nil, 194, -8, nil, nil, nil,
nil, 48, 303, 2324, 1999, 170, 2262, nil, nil, 1294,
1247, 228, 328, 1227, 650, 1160, 1113, 1022, 997, 900,
875, 850, 825, 800, 775, 1170, 1495, 1629, nil, nil,
2044, 272, 146, nil, nil, 472, nil, nil, 470, 127,
1721, 462, nil, nil, 1639, 471, nil, nil, nil, nil,
474, 467, nil, 404, 376, 404, 266, nil, 242, 369,
2519, 1314, 1371, nil, 34, 400, 1582, nil, nil, 82,
nil, 265, -2, 108, 112, 1659, 134, 2475, 2409, 172,
180, nil, 226, nil, nil, 100, 1701, 1711, nil, nil,
nil, nil, nil, nil, nil, nil, 579, 256, 123, 180,
175, 341, 303, 1976, 486, 1886, 540, 1931, 1842, 2279,
2172, nil, nil, nil, 66, 1096, 341, 360, nil, 366,
160, 1032, nil, 0, nil, 84, nil, 980, nil, 384,
374, 388, nil, 391, 399, nil, 227, 427, nil, nil,
nil, nil, 2369, 2127, nil, 1864, nil, nil, nil, nil,
nil, nil, nil, 409, nil, 4, nil, nil, 2061, 2106,
nil, nil, 2217, nil, -20, nil, 925, 435, nil, nil,
442, 967, 445, 1050, 447, nil, 449, nil, nil, 436,
433, 1237, 256, nil, nil, nil, 468, nil, nil, nil,
nil, nil, 213, 170, nil, nil, nil, nil, nil, nil,
1909, 41, nil, nil, 482, nil, 484, nil, nil, nil,
nil ]
racc_action_default = [
-206, -241, -1, -2, -3, -6, -7, -8, -9, -10,
-11, -12, -13, -14, -15, -16, -17, -18, -19, -20,
-241, -39, -41, -42, -43, -45, -97, -241, -178, -241,
-74, -96, -98, -221, -239, -124, -241, -129, -241, -241,
-241, -241, -177, -181, -182, -183, -185, -186, -241, -241,
-198, -241, -229, -241, -4, -241, -46, -47, -48, -49,
-241, -119, -241, -241, -53, -54, -58, -59, -60, -61,
-62, -63, -64, -65, -66, -67, -68, -97, -241, -239,
-221, -109, -109, -77, -206, -206, -241, -241, -73, -197,
-198, -75, -241, -241, -241, -241, -125, -241, -141, -142,
-241, -241, -241, -241, -241, -241, -134, -241, -241, -241,
-187, -188, -190, -206, -206, -206, -199, -201, -202, -203,
-204, -205, 401, -37, -38, -39, -40, -44, -97, -36,
-21, -22, -23, -24, -25, -26, -27, -28, -29, -30,
-31, -32, -33, -34, -35, -227, -112, -113, -114, -241,
-117, -118, -120, -241, -241, -52, -56, -241, -241, -241,
-241, -224, -24, -34, -94, -227, -241, -92, -97, -99,
-100, -101, -102, -103, -104, -105, -110, -114, -227, -112,
-119, -241, -80, -81, -83, -206, -241, -89, -90, -97,
-221, -241, -241, -106, -108, -241, -107, -126, -127, -241,
-241, -241, -241, -241, -241, -241, -241, -241, -241, -241,
-241, -241, -241, -241, -241, -241, -241, -241, -153, -160,
-241, -241, -241, -232, -233, -241, -236, -237, -241, -241,
-241, -97, -171, -172, -241, -241, -178, -179, -180, -184,
-241, -241, -208, -207, -206, -241, -241, -215, -241, -241,
-228, -241, -241, -240, -50, -226, -241, -225, -55, -241,
-123, -241, -222, -226, -241, -95, -241, -228, -109, -241,
-227, -78, -241, -85, -86, -241, -241, -241, -79, -131,
-226, -238, -128, -143, -144, -145, -146, -147, -148, -149,
-150, -151, -152, -154, -155, -156, -157, -158, -159, -161,
-162, -163, -222, -230, -241, -241, -5, -241, -133, -241,
-137, -241, -165, -241, -169, -227, -174, -241, -189, -241,
-241, -227, -211, -214, -241, -217, -241, -241, -200, -216,
-72, -121, -116, -115, -51, -57, -122, -130, -223, -69,
-93, -70, -111, -227, -71, -241, -82, -84, -87, -88,
-231, -234, -235, -132, -137, -136, -241, -241, -164, -166,
-241, -241, -241, -241, -226, -176, -241, -192, -209, -241,
-228, -241, -241, -218, -219, -220, -241, -196, -91, -76,
-135, -138, -241, -241, -170, -173, -175, -191, -210, -212,
-213, -241, -194, -195, -241, -140, -241, -168, -193, -139,
-167 ]
racc_goto_table = [
28, 2, 28, 42, 224, 42, 54, 65, 106, 233,
113, 114, 235, 121, 116, 44, 174, 44, 43, 111,
43, 249, 167, 96, 307, 309, 3, 322, 145, 87,
25, 88, 25, 165, 178, 176, 176, 83, 312, 355,
123, 266, 181, 191, 311, 129, 127, 331, 126, 154,
24, 127, 24, 126, 269, 28, 346, 124, 42, 232,
28, 197, 124, 42, 160, 55, 60, 241, 244, 315,
44, 264, 192, 43, 112, 44, 164, 324, 43, 115,
245, 171, 363, 380, 170, 25, 329, 188, 188, 221,
25, 320, 321, 64, 373, 222, 44, 1, nil, 43,
nil, nil, nil, nil, nil, 24, nil, nil, nil, 236,
24, 175, 42, nil, nil, 366, nil, nil, 34, 359,
34, nil, nil, 376, 44, nil, nil, 43, nil, nil,
nil, 172, nil, 314, 316, nil, nil, 235, 258, 242,
242, 247, nil, 275, 259, 261, 345, 270, nil, nil,
nil, nil, nil, nil, 389, nil, nil, nil, nil, nil,
nil, nil, nil, 284, 285, nil, nil, nil, nil, nil,
nil, nil, nil, nil, nil, nil, nil, nil, 394, 396,
nil, nil, nil, nil, nil, nil, 282, nil, 188, nil,
nil, 362, nil, nil, nil, nil, nil, 369, nil, nil,
174, nil, nil, nil, 351, nil, 340, nil, 121, 328,
121, 318, nil, nil, 314, nil, nil, nil, nil, 378,
343, 342, 176, nil, nil, nil, nil, 239, 28, 28,
236, 42, 42, 42, 236, nil, nil, 42, nil, nil,
nil, nil, nil, 44, 44, 44, 43, 43, 43, 44,
nil, nil, 43, nil, nil, nil, nil, nil, 25, 25,
nil, nil, nil, 386, 384, 171, 235, nil, 170, nil,
325, nil, nil, nil, nil, 188, nil, nil, 24, 24,
44, nil, nil, 43, 23, nil, 23, 374, nil, nil,
nil, nil, nil, nil, nil, 175, nil, nil, 365, 45,
nil, 45, nil, nil, nil, nil, 28, nil, nil, 42,
54, 236, nil, nil, 42, 172, nil, nil, nil, 28,
nil, 44, 42, nil, 43, 381, 44, 28, nil, 43,
42, nil, nil, nil, 44, nil, 25, 43, nil, 23,
nil, nil, 44, nil, 23, 43, 34, 34, 239, 25,
nil, nil, 239, nil, 45, nil, 24, 25, nil, 45,
nil, 236, nil, 236, 42, 169, 42, nil, 22, 24,
22, nil, 28, 391, nil, 42, 44, 24, 44, 43,
45, 43, 28, 28, nil, 42, 42, 44, 227, nil,
43, 28, nil, 237, 42, 54, nil, 44, 44, nil,
43, 43, 25, 21, nil, 21, 44, nil, 45, 43,
nil, nil, 25, 25, nil, nil, nil, nil, nil, nil,
nil, 25, 24, 22, 34, nil, nil, nil, 22, 239,
nil, nil, 24, 24, nil, nil, nil, 34, nil, nil,
nil, 24, nil, nil, nil, 34, nil, nil, nil, 173,
nil, nil, 187, 187, nil, nil, nil, nil, 125, nil,
nil, nil, nil, 125, nil, nil, nil, nil, nil, nil,
nil, nil, nil, nil, nil, nil, nil, 238, nil, 239,
nil, 239, nil, nil, nil, nil, nil, nil, nil, nil,
34, nil, nil, nil, nil, nil, nil, nil, nil, nil,
34, 34, nil, nil, nil, nil, nil, nil, nil, 34,
nil, nil, 23, 23, 237, nil, nil, nil, 237, nil,
nil, nil, nil, nil, nil, nil, nil, 45, 45, 45,
nil, nil, nil, 45, nil, nil, nil, nil, nil, nil,
nil, nil, nil, nil, nil, nil, nil, nil, nil, 169,
nil, nil, nil, 187, nil, nil, nil, nil, nil, nil,
nil, nil, nil, nil, 45, nil, nil, nil, nil, nil,
nil, nil, nil, nil, nil, nil, nil, nil, nil, nil,
nil, nil, nil, nil, nil, nil, nil, nil, 227, nil,
23, nil, nil, nil, nil, 237, 22, 22, 238, nil,
nil, nil, 238, 23, nil, 45, nil, nil, nil, nil,
45, 23, nil, nil, nil, nil, nil, nil, 45, nil,
nil, nil, nil, nil, nil, nil, 45, nil, nil, nil,
nil, 21, 21, 173, nil, 97, nil, 105, 107, 108,
187, nil, nil, nil, nil, 237, nil, 237, nil, nil,
nil, nil, nil, nil, nil, nil, 23, nil, nil, nil,
45, 153, 45, nil, nil, nil, 23, 23, nil, nil,
nil, 45, nil, nil, 22, 23, nil, nil, nil, 238,
nil, 45, 45, nil, nil, nil, nil, 22, nil, nil,
45, 193, 194, 195, 196, 22, nil, nil, nil, 218,
219, 220, nil, nil, nil, nil, nil, nil, nil, 21,
nil, nil, nil, nil, nil, nil, nil, nil, nil, nil,
nil, nil, 21, nil, nil, nil, nil, nil, nil, 238,
21, 238, nil, nil, nil, nil, nil, nil, nil, nil,
22, nil, nil, nil, nil, nil, nil, nil, nil, nil,
22, 22, nil, nil, nil, nil, nil, nil, nil, 22,
nil, nil, nil, nil, nil, nil, nil, nil, nil, nil,
nil, nil, nil, nil, nil, 21, nil, nil, nil, nil,
nil, nil, nil, nil, nil, 21, 21, nil, nil, nil,
nil, nil, nil, nil, 21, nil, nil, nil, 97, 283,
nil, nil, 286, 287, 288, 289, 290, 291, 292, 293,
294, 295, 296, 297, 298, 299, 300, nil, nil, nil,
nil, nil, nil, nil, nil, nil, nil, nil, nil, nil,
nil, nil, nil, nil, nil, nil, nil, nil, nil, nil,
nil, nil, nil, nil, nil, nil, nil, nil, nil, nil,
332, 333, nil, nil, nil, 335, nil, nil, nil, nil,
nil, nil, nil, nil, nil, nil, nil, nil, nil, nil,
nil, nil, nil, nil, nil, 348, 349, nil, nil, nil,
nil, nil, nil, nil, nil, nil, nil, nil, nil, nil,
nil, nil, nil, nil, nil, nil, nil, nil, nil, nil,
nil, nil, nil, nil, 352, nil, nil, nil, nil, nil,
nil, nil, nil, nil, nil, nil, nil, nil, nil, nil,
nil, nil, nil, nil, nil, nil, nil, nil, nil, nil,
nil, 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,
nil, nil, nil, nil, nil, nil, nil, nil, nil, nil,
390 ]
racc_goto_check = [
35, 2, 35, 34, 86, 34, 4, 31, 62, 71,
40, 40, 69, 65, 79, 36, 37, 36, 38, 73,
38, 44, 53, 60, 5, 5, 3, 83, 45, 35,
28, 6, 28, 43, 43, 56, 56, 47, 67, 63,
22, 44, 48, 48, 66, 22, 10, 59, 8, 29,
27, 10, 27, 8, 44, 35, 49, 6, 34, 70,
35, 61, 6, 34, 29, 23, 23, 75, 75, 72,
36, 42, 29, 38, 74, 36, 41, 76, 38, 77,
78, 35, 33, 63, 34, 28, 80, 34, 34, 29,
28, 81, 82, 30, 84, 85, 36, 1, nil, 38,
nil, nil, nil, nil, nil, 27, nil, nil, nil, 35,
27, 28, 34, nil, nil, 5, nil, nil, 55, 67,
55, nil, nil, 5, 36, nil, nil, 38, nil, nil,
nil, 27, nil, 69, 71, nil, nil, 69, 31, 3,
3, 3, nil, 48, 29, 29, 44, 45, nil, nil,
nil, nil, nil, nil, 83, nil, nil, nil, nil, nil,
nil, nil, nil, 65, 65, nil, nil, nil, nil, nil,
nil, nil, nil, nil, nil, nil, nil, nil, 5, 5,
nil, nil, nil, nil, nil, nil, 60, nil, 34, nil,
nil, 44, nil, nil, nil, nil, nil, 44, nil, nil,
37, nil, nil, nil, 86, nil, 53, nil, 65, 79,
65, 73, nil, nil, 69, nil, nil, nil, nil, 44,
43, 56, 56, nil, nil, nil, nil, 55, 35, 35,
35, 34, 34, 34, 35, nil, nil, 34, nil, nil,
nil, nil, nil, 36, 36, 36, 38, 38, 38, 36,
nil, nil, 38, nil, nil, nil, nil, nil, 28, 28,
nil, nil, nil, 71, 69, 35, 69, nil, 34, nil,
3, nil, nil, nil, nil, 34, nil, nil, 27, 27,
36, nil, nil, 38, 26, nil, 26, 40, nil, nil,
nil, nil, nil, nil, nil, 28, nil, nil, 31, 39,
nil, 39, nil, nil, nil, nil, 35, nil, nil, 34,
4, 35, nil, nil, 34, 27, nil, nil, nil, 35,
nil, 36, 34, nil, 38, 62, 36, 35, nil, 38,
34, nil, nil, nil, 36, nil, 28, 38, nil, 26,
nil, nil, 36, nil, 26, 38, 55, 55, 55, 28,
nil, nil, 55, nil, 39, nil, 27, 28, nil, 39,
nil, 35, nil, 35, 34, 26, 34, nil, 25, 27,
25, nil, 35, 2, nil, 34, 36, 27, 36, 38,
39, 38, 35, 35, nil, 34, 34, 36, 26, nil,
38, 35, nil, 26, 34, 4, nil, 36, 36, nil,
38, 38, 28, 24, nil, 24, 36, nil, 39, 38,
nil, nil, 28, 28, nil, nil, nil, nil, nil, nil,
nil, 28, 27, 25, 55, nil, nil, nil, 25, 55,
nil, nil, 27, 27, nil, nil, nil, 55, nil, nil,
nil, 27, nil, nil, nil, 55, nil, nil, nil, 25,
nil, nil, 25, 25, nil, nil, nil, nil, 24, nil,
nil, nil, nil, 24, nil, nil, nil, nil, nil, nil,
nil, nil, nil, nil, nil, nil, nil, 25, nil, 55,
nil, 55, nil, nil, nil, nil, nil, nil, nil, nil,
55, nil, nil, nil, nil, nil, nil, nil, nil, nil,
55, 55, nil, nil, nil, nil, nil, nil, nil, 55,
nil, nil, 26, 26, 26, nil, nil, nil, 26, nil,
nil, nil, nil, nil, nil, nil, nil, 39, 39, 39,
nil, nil, nil, 39, nil, nil, nil, nil, nil, nil,
nil, nil, nil, nil, nil, nil, nil, nil, nil, 26,
nil, nil, nil, 25, nil, nil, nil, nil, nil, nil,
nil, nil, nil, nil, 39, nil, nil, nil, nil, nil,
nil, nil, nil, nil, nil, nil, nil, nil, nil, nil,
nil, nil, nil, nil, nil, nil, nil, nil, 26, nil,
26, nil, nil, nil, nil, 26, 25, 25, 25, nil,
nil, nil, 25, 26, nil, 39, nil, nil, nil, nil,
39, 26, nil, nil, nil, nil, nil, nil, 39, nil,
nil, nil, nil, nil, nil, nil, 39, nil, nil, nil,
nil, 24, 24, 25, nil, 32, nil, 32, 32, 32,
25, nil, nil, nil, nil, 26, nil, 26, nil, nil,
nil, nil, nil, nil, nil, nil, 26, nil, nil, nil,
39, 32, 39, nil, nil, nil, 26, 26, nil, nil,
nil, 39, nil, nil, 25, 26, nil, nil, nil, 25,
nil, 39, 39, nil, nil, nil, nil, 25, nil, nil,
39, 32, 32, 32, 32, 25, nil, nil, nil, 32,
32, 32, nil, nil, nil, nil, nil, nil, nil, 24,
nil, nil, nil, nil, nil, nil, nil, nil, nil, nil,
nil, nil, 24, nil, nil, nil, nil, nil, nil, 25,
24, 25, nil, nil, nil, nil, nil, nil, nil, nil,
25, nil, nil, nil, nil, nil, nil, nil, nil, nil,
25, 25, nil, nil, nil, nil, nil, nil, nil, 25,
nil, nil, nil, nil, nil, nil, nil, nil, nil, nil,
nil, nil, nil, nil, nil, 24, nil, nil, nil, nil,
nil, nil, nil, nil, nil, 24, 24, nil, nil, nil,
nil, nil, nil, nil, 24, nil, nil, nil, 32, 32,
nil, nil, 32, 32, 32, 32, 32, 32, 32, 32,
32, 32, 32, 32, 32, 32, 32, nil, nil, nil,
nil, nil, nil, nil, nil, nil, nil, nil, nil, nil,
nil, nil, nil, nil, nil, nil, nil, nil, nil, nil,
nil, nil, nil, nil, nil, nil, nil, nil, nil, nil,
32, 32, nil, nil, nil, 32, nil, nil, nil, nil,
nil, nil, nil, nil, nil, nil, nil, nil, nil, nil,
nil, nil, nil, nil, nil, 32, 32, nil, nil, nil,
nil, nil, nil, nil, nil, nil, nil, nil, nil, nil,
nil, nil, nil, nil, nil, nil, nil, nil, nil, nil,
nil, nil, nil, nil, 32, nil, nil, nil, nil, nil,
nil, nil, nil, nil, nil, nil, nil, nil, nil, nil,
nil, nil, nil, nil, nil, nil, nil, nil, nil, nil,
nil, nil, nil, nil, nil, nil, nil, nil, nil, nil,
nil, nil, nil, nil, nil, nil, nil, nil, nil, nil,
nil, nil, nil, nil, nil, 32, nil, nil, nil, nil,
nil, nil, nil, nil, nil, nil, nil, nil, nil, nil,
32 ]
racc_goto_pointer = [
nil, 97, 1, 26, 4, -204, 2, nil, -7, nil,
-9, nil, nil, nil, nil, nil, nil, nil, nil, nil,
nil, nil, -15, 46, 403, 368, 284, 50, 30, -14,
67, -19, 599, -233, 3, 0, 15, -65, 18, 299,
-39, -5, -93, -48, -124, -33, nil, 9, -42, -216,
nil, nil, nil, -59, nil, 118, -46, nil, nil, -203,
-13, -36, -31, -271, nil, -38, -186, -192, nil, -97,
-50, -100, -165, -29, 26, -46, -167, 28, -35, -37,
-162, -152, -151, -216, -232, -9, -100, nil ]
racc_goto_default = [
nil, nil, 306, 182, 4, nil, 5, 6, 7, 8,
9, 10, 11, 12, 13, 14, 15, 16, 17, 18,
19, 147, 20, nil, 74, 71, 66, 70, 73, nil,
nil, 98, 156, 256, 67, 68, 69, 72, 75, 76,
27, nil, nil, nil, nil, nil, 29, nil, nil, 183,
272, 184, 186, nil, 166, 79, 150, 149, 151, 152,
nil, nil, nil, nil, 99, 47, nil, nil, 313, 41,
nil, nil, nil, nil, nil, nil, nil, nil, nil, nil,
117, nil, nil, nil, nil, nil, nil, 225 ]
racc_reduce_table = [
0, 0, :racc_error,
1, 71, :_reduce_none,
1, 71, :_reduce_none,
1, 72, :_reduce_3,
2, 72, :_reduce_4,
1, 75, :_reduce_5,
1, 74, :_reduce_none,
1, 74, :_reduce_none,
1, 74, :_reduce_none,
1, 74, :_reduce_none,
1, 74, :_reduce_none,
1, 74, :_reduce_none,
1, 74, :_reduce_none,
1, 74, :_reduce_none,
1, 74, :_reduce_none,
1, 74, :_reduce_none,
1, 74, :_reduce_none,
1, 74, :_reduce_none,
1, 74, :_reduce_none,
1, 74, :_reduce_none,
1, 74, :_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, 91, :_reduce_none,
1, 91, :_reduce_none,
1, 91, :_reduce_none,
1, 91, :_reduce_none,
1, 91, :_reduce_none,
3, 90, :_reduce_36,
3, 90, :_reduce_37,
1, 92, :_reduce_none,
1, 92, :_reduce_none,
1, 92, :_reduce_none,
1, 92, :_reduce_none,
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,
4, 84, :_reduce_50,
5, 84, :_reduce_51,
3, 84, :_reduce_52,
2, 84, :_reduce_53,
1, 100, :_reduce_54,
3, 100, :_reduce_55,
1, 99, :_reduce_56,
3, 99, :_reduce_57,
1, 101, :_reduce_none,
1, 101, :_reduce_none,
1, 101, :_reduce_none,
1, 101, :_reduce_none,
1, 101, :_reduce_none,
1, 101, :_reduce_none,
1, 101, :_reduce_none,
1, 101, :_reduce_none,
1, 101, :_reduce_none,
1, 101, :_reduce_none,
1, 101, :_reduce_none,
5, 76, :_reduce_69,
5, 76, :_reduce_70,
5, 76, :_reduce_71,
5, 88, :_reduce_72,
2, 77, :_reduce_73,
1, 116, :_reduce_74,
2, 116, :_reduce_75,
6, 78, :_reduce_76,
2, 78, :_reduce_77,
3, 117, :_reduce_78,
3, 117, :_reduce_79,
1, 118, :_reduce_none,
1, 118, :_reduce_none,
3, 118, :_reduce_82,
1, 119, :_reduce_none,
3, 119, :_reduce_84,
1, 120, :_reduce_85,
1, 120, :_reduce_86,
3, 121, :_reduce_87,
3, 121, :_reduce_88,
1, 122, :_reduce_none,
1, 122, :_reduce_none,
4, 123, :_reduce_91,
1, 111, :_reduce_92,
3, 111, :_reduce_93,
0, 112, :_reduce_none,
1, 112, :_reduce_none,
1, 109, :_reduce_96,
1, 104, :_reduce_97,
1, 105, :_reduce_98,
1, 124, :_reduce_none,
1, 124, :_reduce_none,
1, 124, :_reduce_none,
1, 124, :_reduce_none,
1, 124, :_reduce_none,
1, 124, :_reduce_none,
1, 124, :_reduce_none,
3, 79, :_reduce_106,
3, 79, :_reduce_107,
3, 89, :_reduce_108,
0, 113, :_reduce_109,
1, 113, :_reduce_110,
3, 113, :_reduce_111,
1, 127, :_reduce_none,
1, 127, :_reduce_none,
1, 127, :_reduce_none,
3, 126, :_reduce_115,
3, 128, :_reduce_116,
1, 129, :_reduce_none,
1, 129, :_reduce_none,
0, 115, :_reduce_119,
1, 115, :_reduce_120,
3, 115, :_reduce_121,
4, 108, :_reduce_122,
3, 108, :_reduce_123,
1, 96, :_reduce_124,
2, 96, :_reduce_125,
2, 130, :_reduce_126,
1, 131, :_reduce_127,
2, 131, :_reduce_128,
1, 106, :_reduce_129,
4, 94, :_reduce_130,
4, 94, :_reduce_131,
5, 82, :_reduce_132,
4, 82, :_reduce_133,
2, 81, :_reduce_134,
5, 132, :_reduce_135,
4, 132, :_reduce_136,
0, 133, :_reduce_none,
2, 133, :_reduce_138,
4, 133, :_reduce_139,
3, 133, :_reduce_140,
1, 102, :_reduce_none,
1, 102, :_reduce_none,
3, 102, :_reduce_143,
3, 102, :_reduce_144,
3, 102, :_reduce_145,
3, 102, :_reduce_146,
3, 102, :_reduce_147,
3, 102, :_reduce_148,
3, 102, :_reduce_149,
3, 102, :_reduce_150,
3, 102, :_reduce_151,
3, 102, :_reduce_152,
2, 102, :_reduce_153,
3, 102, :_reduce_154,
3, 102, :_reduce_155,
3, 102, :_reduce_156,
3, 102, :_reduce_157,
3, 102, :_reduce_158,
3, 102, :_reduce_159,
2, 102, :_reduce_160,
3, 102, :_reduce_161,
3, 102, :_reduce_162,
3, 102, :_reduce_163,
5, 80, :_reduce_164,
1, 136, :_reduce_165,
2, 136, :_reduce_166,
5, 137, :_reduce_167,
4, 137, :_reduce_168,
1, 138, :_reduce_169,
3, 138, :_reduce_170,
3, 97, :_reduce_171,
1, 140, :_reduce_none,
4, 140, :_reduce_173,
1, 142, :_reduce_none,
3, 142, :_reduce_175,
3, 141, :_reduce_176,
1, 139, :_reduce_none,
1, 139, :_reduce_none,
1, 139, :_reduce_none,
1, 139, :_reduce_none,
1, 139, :_reduce_none,
1, 139, :_reduce_none,
1, 139, :_reduce_none,
1, 139, :_reduce_none,
1, 139, :_reduce_185,
1, 139, :_reduce_none,
1, 143, :_reduce_187,
1, 144, :_reduce_none,
3, 144, :_reduce_189,
2, 83, :_reduce_190,
6, 85, :_reduce_191,
5, 85, :_reduce_192,
7, 86, :_reduce_193,
6, 86, :_reduce_194,
6, 87, :_reduce_195,
5, 87, :_reduce_196,
1, 110, :_reduce_197,
1, 110, :_reduce_198,
1, 147, :_reduce_199,
3, 147, :_reduce_200,
1, 149, :_reduce_201,
1, 150, :_reduce_202,
1, 150, :_reduce_203,
1, 150, :_reduce_204,
1, 150, :_reduce_none,
0, 73, :_reduce_206,
0, 151, :_reduce_207,
1, 145, :_reduce_none,
3, 145, :_reduce_209,
4, 145, :_reduce_210,
1, 152, :_reduce_none,
3, 152, :_reduce_212,
3, 153, :_reduce_213,
1, 153, :_reduce_214,
1, 148, :_reduce_none,
2, 148, :_reduce_216,
1, 146, :_reduce_none,
2, 146, :_reduce_218,
1, 154, :_reduce_none,
1, 154, :_reduce_none,
1, 95, :_reduce_221,
3, 107, :_reduce_222,
4, 107, :_reduce_223,
2, 107, :_reduce_224,
1, 103, :_reduce_none,
1, 103, :_reduce_none,
0, 114, :_reduce_none,
1, 114, :_reduce_228,
1, 135, :_reduce_229,
3, 134, :_reduce_230,
4, 134, :_reduce_231,
2, 134, :_reduce_232,
1, 155, :_reduce_none,
3, 155, :_reduce_234,
3, 156, :_reduce_235,
1, 157, :_reduce_236,
1, 157, :_reduce_237,
4, 125, :_reduce_238,
1, 98, :_reduce_none,
4, 98, :_reduce_240 ]
racc_reduce_n = 241
racc_shift_n = 401
racc_token_table = {
false => 0,
:error => 1,
:STRING => 2,
:DQPRE => 3,
:DQMID => 4,
:DQPOST => 5,
:LBRACK => 6,
:RBRACK => 7,
:LBRACE => 8,
:RBRACE => 9,
:SYMBOL => 10,
:FARROW => 11,
:COMMA => 12,
:TRUE => 13,
:FALSE => 14,
:EQUALS => 15,
:APPENDS => 16,
:LESSEQUAL => 17,
:NOTEQUAL => 18,
:DOT => 19,
:COLON => 20,
:LLCOLLECT => 21,
:RRCOLLECT => 22,
:QMARK => 23,
:LPAREN => 24,
:RPAREN => 25,
:ISEQUAL => 26,
:GREATEREQUAL => 27,
:GREATERTHAN => 28,
:LESSTHAN => 29,
:IF => 30,
:ELSE => 31,
:IMPORT => 32,
:DEFINE => 33,
:ELSIF => 34,
:VARIABLE => 35,
:CLASS => 36,
:INHERITS => 37,
:NODE => 38,
:BOOLEAN => 39,
:NAME => 40,
:SEMIC => 41,
:CASE => 42,
:DEFAULT => 43,
:AT => 44,
:LCOLLECT => 45,
:RCOLLECT => 46,
:CLASSREF => 47,
:NOT => 48,
:OR => 49,
:AND => 50,
:UNDEF => 51,
:PARROW => 52,
:PLUS => 53,
:MINUS => 54,
:TIMES => 55,
:DIV => 56,
:LSHIFT => 57,
:RSHIFT => 58,
:UMINUS => 59,
:MATCH => 60,
:NOMATCH => 61,
:REGEX => 62,
:IN_EDGE => 63,
:OUT_EDGE => 64,
:IN_EDGE_SUB => 65,
:OUT_EDGE_SUB => 66,
:IN => 67,
:UNLESS => 68,
:MODULO => 69 }
racc_nt_base = 70
racc_use_result_var = true
Racc_arg = [
racc_action_table,
racc_action_check,
racc_action_default,
racc_action_pointer,
racc_goto_table,
racc_goto_check,
racc_goto_default,
racc_goto_pointer,
racc_nt_base,
racc_reduce_table,
racc_token_table,
racc_shift_n,
racc_reduce_n,
racc_use_result_var ]
Racc_token_to_s_table = [
"$end",
"error",
"STRING",
"DQPRE",
"DQMID",
"DQPOST",
"LBRACK",
"RBRACK",
"LBRACE",
"RBRACE",
"SYMBOL",
"FARROW",
"COMMA",
"TRUE",
"FALSE",
"EQUALS",
"APPENDS",
"LESSEQUAL",
"NOTEQUAL",
"DOT",
"COLON",
"LLCOLLECT",
"RRCOLLECT",
"QMARK",
"LPAREN",
"RPAREN",
"ISEQUAL",
"GREATEREQUAL",
"GREATERTHAN",
"LESSTHAN",
"IF",
"ELSE",
"IMPORT",
"DEFINE",
"ELSIF",
"VARIABLE",
"CLASS",
"INHERITS",
"NODE",
"BOOLEAN",
"NAME",
"SEMIC",
"CASE",
"DEFAULT",
"AT",
"LCOLLECT",
"RCOLLECT",
"CLASSREF",
"NOT",
"OR",
"AND",
"UNDEF",
"PARROW",
"PLUS",
"MINUS",
"TIMES",
"DIV",
"LSHIFT",
"RSHIFT",
"UMINUS",
"MATCH",
"NOMATCH",
"REGEX",
"IN_EDGE",
"OUT_EDGE",
"IN_EDGE_SUB",
"OUT_EDGE_SUB",
"IN",
"UNLESS",
"MODULO",
"$start",
"program",
"statements_and_declarations",
"nil",
"statement_or_declaration",
"statements",
"resource",
"virtualresource",
"collection",
"assignment",
"casestatement",
"ifstatement_begin",
"unlessstatement",
"import",
"fstatement",
"definition",
"hostclass",
"nodedef",
"resourceoverride",
"append",
"relationship",
"keyword",
"relationship_side",
"edge",
"resourceref",
"variable",
"quotedtext",
"selector",
"hasharrayaccesses",
"expressions",
"funcvalues",
"rvalue",
"expression",
"comma",
"name",
"type",
"boolean",
"array",
"funcrvalue",
"undef",
"classname",
"resourceinstances",
"endsemi",
"params",
"endcomma",
"anyparams",
"at",
"collectrhand",
"collstatements",
"collstatement",
"colljoin",
"collexpr",
"colllval",
"resourceinst",
"resourcename",
"hasharrayaccess",
"param",
"param_name",
"addparam",
"anyparam",
"dqrval",
"dqtail",
"ifstatement",
"else",
"hash",
"regex",
"caseopts",
"caseopt",
"casevalues",
"selectlhand",
"svalues",
"selectval",
"sintvalues",
"string",
"strings",
"argumentlist",
"classparent",
"hostnames",
"nodeparent",
"nodename",
"hostname",
"nothing",
"arguments",
"argument",
"classnameordefault",
"hashpairs",
"hashpair",
"key" ]
Racc_debug_parser = false
##### State transition tables end #####
# reduce 0 omitted
# reduce 1 omitted
# reduce 2 omitted
module_eval(<<'.,.,', 'grammar.ra', 34)
def _reduce_3(val, _values, result)
result = ast AST::BlockExpression, :children => (val[0] ? [val[0]] : [])
result
end
.,.,
module_eval(<<'.,.,', 'grammar.ra', 37)
def _reduce_4(val, _values, result)
if val[1]
val[0].push(val[1])
end
result = val[0]
result
end
.,.,
module_eval(<<'.,.,', 'grammar.ra', 46)
def _reduce_5(val, _values, result)
val[0].each do |stmt|
if stmt.is_a?(AST::TopLevelConstruct)
error "Classes, definitions, and nodes may only appear at toplevel or inside other classes", \
:line => stmt.context[:line], :file => stmt.context[:file]
end
end
result = val[0]
result
end
.,.,
# reduce 6 omitted
# reduce 7 omitted
# reduce 8 omitted
# reduce 9 omitted
# reduce 10 omitted
# reduce 11 omitted
# reduce 12 omitted
# reduce 13 omitted
# reduce 14 omitted
# reduce 15 omitted
# reduce 16 omitted
# reduce 17 omitted
# reduce 18 omitted
# reduce 19 omitted
# reduce 20 omitted
# reduce 21 omitted
# reduce 22 omitted
# reduce 23 omitted
# reduce 24 omitted
# reduce 25 omitted
# reduce 26 omitted
# reduce 27 omitted
# reduce 28 omitted
# reduce 29 omitted
# reduce 30 omitted
# reduce 31 omitted
# reduce 32 omitted
# reduce 33 omitted
# reduce 34 omitted
# reduce 35 omitted
module_eval(<<'.,.,', 'grammar.ra', 89)
def _reduce_36(val, _values, result)
result = AST::Relationship.new(val[0], val[2], val[1][:value], ast_context)
result
end
.,.,
module_eval(<<'.,.,', 'grammar.ra', 92)
def _reduce_37(val, _values, result)
result = AST::Relationship.new(val[0], val[2], val[1][:value], ast_context)
result
end
.,.,
# reduce 38 omitted
# reduce 39 omitted
# reduce 40 omitted
# reduce 41 omitted
# reduce 42 omitted
# reduce 43 omitted
# reduce 44 omitted
# reduce 45 omitted
# reduce 46 omitted
# reduce 47 omitted
# reduce 48 omitted
# reduce 49 omitted
module_eval(<<'.,.,', 'grammar.ra', 107)
def _reduce_50(val, _values, result)
result = ast AST::Function,
:name => val[0][:value],
:line => val[0][:line],
:arguments => val[2],
:ftype => :statement
result
end
.,.,
module_eval(<<'.,.,', 'grammar.ra', 114)
def _reduce_51(val, _values, result)
result = ast AST::Function,
:name => val[0][:value],
:line => val[0][:line],
:arguments => val[2],
:ftype => :statement
result
end
.,.,
module_eval(<<'.,.,', 'grammar.ra', 120)
def _reduce_52(val, _values, result)
result = ast AST::Function,
:name => val[0][:value],
:line => val[0][:line],
:arguments => AST::ASTArray.new({}),
:ftype => :statement
result
end
.,.,
module_eval(<<'.,.,', 'grammar.ra', 127)
def _reduce_53(val, _values, result)
result = ast AST::Function,
:name => val[0][:value],
:line => val[0][:line],
:arguments => val[1],
:ftype => :statement
result
end
.,.,
module_eval(<<'.,.,', 'grammar.ra', 134)
def _reduce_54(val, _values, result)
result = aryfy(val[0])
result
end
.,.,
module_eval(<<'.,.,', 'grammar.ra', 137)
def _reduce_55(val, _values, result)
val[0].push(val[2])
result = val[0]
result
end
.,.,
module_eval(<<'.,.,', 'grammar.ra', 141)
def _reduce_56(val, _values, result)
result = aryfy(val[0])
result
end
.,.,
module_eval(<<'.,.,', 'grammar.ra', 142)
def _reduce_57(val, _values, result)
result = val[0].push(val[2])
result
end
.,.,
# reduce 58 omitted
# reduce 59 omitted
# reduce 60 omitted
# reduce 61 omitted
# reduce 62 omitted
# reduce 63 omitted
# reduce 64 omitted
# reduce 65 omitted
# reduce 66 omitted
# reduce 67 omitted
# reduce 68 omitted
module_eval(<<'.,.,', 'grammar.ra', 157)
def _reduce_69(val, _values, result)
@lexer.commentpop
result = ast(AST::Resource, :type => val[0], :instances => val[2])
result
end
.,.,
module_eval(<<'.,.,', 'grammar.ra', 160)
def _reduce_70(val, _values, result)
# This is a deprecated syntax.
error "All resource specifications require names"
result
end
.,.,
module_eval(<<'.,.,', 'grammar.ra', 163)
def _reduce_71(val, _values, result)
# a defaults setting for a type
@lexer.commentpop
result = ast(AST::ResourceDefaults, :type => val[0].value, :parameters => val[2])
result
end
.,.,
module_eval(<<'.,.,', 'grammar.ra', 170)
def _reduce_72(val, _values, result)
@lexer.commentpop
result = ast AST::ResourceOverride, :object => val[0], :parameters => val[2]
result
end
.,.,
module_eval(<<'.,.,', 'grammar.ra', 177)
def _reduce_73(val, _values, result)
type = val[0]
if (type == :exported and ! Puppet[:storeconfigs])
Puppet.warning addcontext("You cannot collect without storeconfigs being set")
end
error "Defaults are not virtualizable" if val[1].is_a? AST::ResourceDefaults
method = type.to_s + "="
# Just mark our resource as exported and pass it through.
val[1].send(method, true)
result = val[1]
result
end
.,.,
module_eval(<<'.,.,', 'grammar.ra', 193)
def _reduce_74(val, _values, result)
result = :virtual
result
end
.,.,
module_eval(<<'.,.,', 'grammar.ra', 194)
def _reduce_75(val, _values, result)
result = :exported
result
end
.,.,
module_eval(<<'.,.,', 'grammar.ra', 199)
def _reduce_76(val, _values, result)
@lexer.commentpop
type = val[0].value.downcase
args = {:type => type}
if val[1].is_a?(AST::CollExpr)
args[:query] = val[1]
args[:query].type = type
args[:form] = args[:query].form
else
args[:form] = val[1]
end
if args[:form] == :exported and ! Puppet[:storeconfigs]
Puppet.warning addcontext("You cannot collect exported resources without storeconfigs being set; the collection will be ignored")
end
args[:override] = val[3]
result = ast AST::Collection, args
result
end
.,.,
module_eval(<<'.,.,', 'grammar.ra', 217)
def _reduce_77(val, _values, result)
type = val[0].value.downcase
args = {:type => type }
if val[1].is_a?(AST::CollExpr)
args[:query] = val[1]
args[:query].type = type
args[:form] = args[:query].form
else
args[:form] = val[1]
end
if args[:form] == :exported and ! Puppet[:storeconfigs]
Puppet.warning addcontext("You cannot collect exported resources without storeconfigs being set; the collection will be ignored")
end
result = ast AST::Collection, args
result
end
.,.,
module_eval(<<'.,.,', 'grammar.ra', 235)
def _reduce_78(val, _values, result)
if val[1]
result = val[1]
result.form = :virtual
else
result = :virtual
end
result
end
.,.,
module_eval(<<'.,.,', 'grammar.ra', 243)
def _reduce_79(val, _values, result)
if val[1]
result = val[1]
result.form = :exported
else
result = :exported
end
result
end
.,.,
# reduce 80 omitted
# reduce 81 omitted
module_eval(<<'.,.,', 'grammar.ra', 256)
def _reduce_82(val, _values, result)
result = ast AST::CollExpr, :test1 => val[0], :oper => val[1], :test2 => val[2]
result
end
.,.,
# reduce 83 omitted
module_eval(<<'.,.,', 'grammar.ra', 261)
def _reduce_84(val, _values, result)
result = val[1]
result.parens = true
result
end
.,.,
module_eval(<<'.,.,', 'grammar.ra', 265)
def _reduce_85(val, _values, result)
result=val[0][:value]
result
end
.,.,
module_eval(<<'.,.,', 'grammar.ra', 266)
def _reduce_86(val, _values, result)
result=val[0][:value]
result
end
.,.,
module_eval(<<'.,.,', 'grammar.ra', 269)
def _reduce_87(val, _values, result)
result = ast AST::CollExpr, :test1 => val[0], :oper => val[1][:value], :test2 => val[2]
#result = ast AST::CollExpr
#result.push *val
result
end
.,.,
module_eval(<<'.,.,', 'grammar.ra', 274)
def _reduce_88(val, _values, result)
result = ast AST::CollExpr, :test1 => val[0], :oper => val[1][:value], :test2 => val[2]
#result = ast AST::CollExpr
#result.push *val
result
end
.,.,
# reduce 89 omitted
# reduce 90 omitted
module_eval(<<'.,.,', 'grammar.ra', 283)
def _reduce_91(val, _values, result)
result = ast AST::ResourceInstance, :title => val[0], :parameters => val[2]
result
end
.,.,
module_eval(<<'.,.,', 'grammar.ra', 286)
def _reduce_92(val, _values, result)
result = aryfy(val[0])
result
end
.,.,
module_eval(<<'.,.,', 'grammar.ra', 288)
def _reduce_93(val, _values, result)
val[0].push val[2]
result = val[0]
result
end
.,.,
# reduce 94 omitted
# reduce 95 omitted
module_eval(<<'.,.,', 'grammar.ra', 296)
def _reduce_96(val, _values, result)
result = ast AST::Undef, :value => :undef
result
end
.,.,
module_eval(<<'.,.,', 'grammar.ra', 300)
def _reduce_97(val, _values, result)
result = ast AST::Name, :value => val[0][:value], :line => val[0][:line]
result
end
.,.,
module_eval(<<'.,.,', 'grammar.ra', 304)
def _reduce_98(val, _values, result)
result = ast AST::Type, :value => val[0][:value], :line => val[0][:line]
result
end
.,.,
# reduce 99 omitted
# reduce 100 omitted
# reduce 101 omitted
# reduce 102 omitted
# reduce 103 omitted
# reduce 104 omitted
# reduce 105 omitted
module_eval(<<'.,.,', 'grammar.ra', 316)
def _reduce_106(val, _values, result)
raise Puppet::ParseError, "Cannot assign to variables in other namespaces" if val[0][:value] =~ /::/
# this is distinct from referencing a variable
variable = ast AST::Name, :value => val[0][:value], :line => val[0][:line]
result = ast AST::VarDef, :name => variable, :value => val[2], :line => val[0][:line]
result
end
.,.,
module_eval(<<'.,.,', 'grammar.ra', 322)
def _reduce_107(val, _values, result)
result = ast AST::VarDef, :name => val[0], :value => val[2]
result
end
.,.,
module_eval(<<'.,.,', 'grammar.ra', 326)
def _reduce_108(val, _values, result)
variable = ast AST::Name, :value => val[0][:value], :line => val[0][:line]
result = ast AST::VarDef, :name => variable, :value => val[2], :append => true, :line => val[0][:line]
result
end
.,.,
module_eval(<<'.,.,', 'grammar.ra', 332)
def _reduce_109(val, _values, result)
result = ast AST::ASTArray
result
end
.,.,
module_eval(<<'.,.,', 'grammar.ra', 334)
def _reduce_110(val, _values, result)
result = aryfy(val[0])
result
end
.,.,
module_eval(<<'.,.,', 'grammar.ra', 336)
def _reduce_111(val, _values, result)
val[0].push(val[2])
result = val[0]
result
end
.,.,
# reduce 112 omitted
# reduce 113 omitted
# reduce 114 omitted
module_eval(<<'.,.,', 'grammar.ra', 345)
def _reduce_115(val, _values, result)
result = ast AST::ResourceParam, :param => val[0][:value], :line => val[0][:line], :value => val[2]
result
end
.,.,
module_eval(<<'.,.,', 'grammar.ra', 349)
def _reduce_116(val, _values, result)
result = ast AST::ResourceParam, :param => val[0][:value], :line => val[0][:line], :value => val[2],
:add => true
result
end
.,.,
# reduce 117 omitted
# reduce 118 omitted
module_eval(<<'.,.,', 'grammar.ra', 358)
def _reduce_119(val, _values, result)
result = ast AST::ASTArray
result
end
.,.,
module_eval(<<'.,.,', 'grammar.ra', 360)
def _reduce_120(val, _values, result)
result = aryfy(val[0])
result
end
.,.,
module_eval(<<'.,.,', 'grammar.ra', 362)
def _reduce_121(val, _values, result)
val[0].push(val[2])
result = val[0]
result
end
.,.,
module_eval(<<'.,.,', 'grammar.ra', 368)
def _reduce_122(val, _values, result)
result = ast AST::Function,
:name => val[0][:value], :line => val[0][:line],
:arguments => val[2],
:ftype => :rvalue
result
end
.,.,
module_eval(<<'.,.,', 'grammar.ra', 373)
def _reduce_123(val, _values, result)
result = ast AST::Function,
:name => val[0][:value], :line => val[0][:line],
:arguments => AST::ASTArray.new({}),
:ftype => :rvalue
result
end
.,.,
module_eval(<<'.,.,', 'grammar.ra', 379)
def _reduce_124(val, _values, result)
result = ast AST::String, :value => val[0][:value], :line => val[0][:line]
result
end
.,.,
module_eval(<<'.,.,', 'grammar.ra', 380)
def _reduce_125(val, _values, result)
result = ast AST::Concat, :value => [ast(AST::String,val[0])]+val[1], :line => val[0][:line]
result
end
.,.,
module_eval(<<'.,.,', 'grammar.ra', 382)
def _reduce_126(val, _values, result)
result = [val[0]] + val[1]
result
end
.,.,
module_eval(<<'.,.,', 'grammar.ra', 384)
def _reduce_127(val, _values, result)
result = [ast(AST::String,val[0])]
result
end
.,.,
module_eval(<<'.,.,', 'grammar.ra', 385)
def _reduce_128(val, _values, result)
result = [ast(AST::String,val[0])] + val[1]
result
end
.,.,
module_eval(<<'.,.,', 'grammar.ra', 388)
def _reduce_129(val, _values, result)
result = ast AST::Boolean, :value => val[0][:value], :line => val[0][:line]
result
end
.,.,
module_eval(<<'.,.,', 'grammar.ra', 392)
def _reduce_130(val, _values, result)
Puppet.warning addcontext("Deprecation notice: Resource references should now be capitalized")
result = ast AST::ResourceReference, :type => val[0][:value], :line => val[0][:line], :title => val[2]
result
end
.,.,
module_eval(<<'.,.,', 'grammar.ra', 395)
def _reduce_131(val, _values, result)
result = ast AST::ResourceReference, :type => val[0].value, :title => val[2]
result
end
.,.,
module_eval(<<'.,.,', 'grammar.ra', 399)
def _reduce_132(val, _values, result)
@lexer.commentpop
args = {
:test => ast(AST::Not, :value => val[1]),
:statements => val[3]
}
result = ast AST::IfStatement, args
result
end
.,.,
module_eval(<<'.,.,', 'grammar.ra', 408)
def _reduce_133(val, _values, result)
@lexer.commentpop
args = {
:test => ast(AST::Not, :value => val[1]),
:statements => ast(AST::Nop)
}
result = ast AST::IfStatement, args
result
end
.,.,
module_eval(<<'.,.,', 'grammar.ra', 417)
def _reduce_134(val, _values, result)
result = val[1]
result
end
.,.,
module_eval(<<'.,.,', 'grammar.ra', 421)
def _reduce_135(val, _values, result)
@lexer.commentpop
args = {
:test => val[0],
:statements => val[2]
}
args[:else] = val[4] if val[4]
result = ast AST::IfStatement, args
result
end
.,.,
module_eval(<<'.,.,', 'grammar.ra', 432)
def _reduce_136(val, _values, result)
@lexer.commentpop
args = {
:test => val[0],
:statements => ast(AST::Nop)
}
args[:else] = val[3] if val[3]
result = ast AST::IfStatement, args
result
end
.,.,
# reduce 137 omitted
module_eval(<<'.,.,', 'grammar.ra', 445)
def _reduce_138(val, _values, result)
result = ast AST::Else, :statements => val[1]
result
end
.,.,
module_eval(<<'.,.,', 'grammar.ra', 448)
def _reduce_139(val, _values, result)
@lexer.commentpop
result = ast AST::Else, :statements => val[2]
result
end
.,.,
module_eval(<<'.,.,', 'grammar.ra', 452)
def _reduce_140(val, _values, result)
@lexer.commentpop
result = ast AST::Else, :statements => ast(AST::Nop)
result
end
.,.,
# reduce 141 omitted
# reduce 142 omitted
module_eval(<<'.,.,', 'grammar.ra', 471)
def _reduce_143(val, _values, result)
result = ast AST::InOperator, :lval => val[0], :rval => val[2]
result
end
.,.,
module_eval(<<'.,.,', 'grammar.ra', 474)
def _reduce_144(val, _values, result)
result = ast AST::MatchOperator, :operator => val[1][:value], :lval => val[0], :rval => val[2]
result
end
.,.,
module_eval(<<'.,.,', 'grammar.ra', 477)
def _reduce_145(val, _values, result)
result = ast AST::MatchOperator, :operator => val[1][:value], :lval => val[0], :rval => val[2]
result
end
.,.,
module_eval(<<'.,.,', 'grammar.ra', 480)
def _reduce_146(val, _values, result)
result = ast AST::ArithmeticOperator, :operator => val[1][:value], :lval => val[0], :rval => val[2]
result
end
.,.,
module_eval(<<'.,.,', 'grammar.ra', 483)
def _reduce_147(val, _values, result)
result = ast AST::ArithmeticOperator, :operator => val[1][:value], :lval => val[0], :rval => val[2]
result
end
.,.,
module_eval(<<'.,.,', 'grammar.ra', 486)
def _reduce_148(val, _values, result)
result = ast AST::ArithmeticOperator, :operator => val[1][:value], :lval => val[0], :rval => val[2]
result
end
.,.,
module_eval(<<'.,.,', 'grammar.ra', 489)
def _reduce_149(val, _values, result)
result = ast AST::ArithmeticOperator, :operator => val[1][:value], :lval => val[0], :rval => val[2]
result
end
.,.,
module_eval(<<'.,.,', 'grammar.ra', 492)
def _reduce_150(val, _values, result)
result = ast AST::ArithmeticOperator, :operator => val[1][:value], :lval => val[0], :rval => val[2]
result
end
.,.,
module_eval(<<'.,.,', 'grammar.ra', 495)
def _reduce_151(val, _values, result)
result = ast AST::ArithmeticOperator, :operator => val[1][:value], :lval => val[0], :rval => val[2]
result
end
.,.,
module_eval(<<'.,.,', 'grammar.ra', 498)
def _reduce_152(val, _values, result)
result = ast AST::ArithmeticOperator, :operator => val[1][:value], :lval => val[0], :rval => val[2]
result
end
.,.,
module_eval(<<'.,.,', 'grammar.ra', 501)
def _reduce_153(val, _values, result)
result = ast AST::Minus, :value => val[1]
result
end
.,.,
module_eval(<<'.,.,', 'grammar.ra', 504)
def _reduce_154(val, _values, result)
result = ast AST::ComparisonOperator, :operator => val[1][:value], :lval => val[0], :rval => val[2]
result
end
.,.,
module_eval(<<'.,.,', 'grammar.ra', 507)
def _reduce_155(val, _values, result)
result = ast AST::ComparisonOperator, :operator => val[1][:value], :lval => val[0], :rval => val[2]
result
end
.,.,
module_eval(<<'.,.,', 'grammar.ra', 510)
def _reduce_156(val, _values, result)
result = ast AST::ComparisonOperator, :operator => val[1][:value], :lval => val[0], :rval => val[2]
result
end
.,.,
module_eval(<<'.,.,', 'grammar.ra', 513)
def _reduce_157(val, _values, result)
result = ast AST::ComparisonOperator, :operator => val[1][:value], :lval => val[0], :rval => val[2]
result
end
.,.,
module_eval(<<'.,.,', 'grammar.ra', 516)
def _reduce_158(val, _values, result)
result = ast AST::ComparisonOperator, :operator => val[1][:value], :lval => val[0], :rval => val[2]
result
end
.,.,
module_eval(<<'.,.,', 'grammar.ra', 519)
def _reduce_159(val, _values, result)
result = ast AST::ComparisonOperator, :operator => val[1][:value], :lval => val[0], :rval => val[2]
result
end
.,.,
module_eval(<<'.,.,', 'grammar.ra', 522)
def _reduce_160(val, _values, result)
result = ast AST::Not, :value => val[1]
result
end
.,.,
module_eval(<<'.,.,', 'grammar.ra', 525)
def _reduce_161(val, _values, result)
result = ast AST::BooleanOperator, :operator => val[1][:value], :lval => val[0], :rval => val[2]
result
end
.,.,
module_eval(<<'.,.,', 'grammar.ra', 528)
def _reduce_162(val, _values, result)
result = ast AST::BooleanOperator, :operator => val[1][:value], :lval => val[0], :rval => val[2]
result
end
.,.,
module_eval(<<'.,.,', 'grammar.ra', 531)
def _reduce_163(val, _values, result)
result = val[1]
result
end
.,.,
module_eval(<<'.,.,', 'grammar.ra', 535)
def _reduce_164(val, _values, result)
@lexer.commentpop
result = ast AST::CaseStatement, :test => val[1], :options => val[3]
result
end
.,.,
module_eval(<<'.,.,', 'grammar.ra', 539)
def _reduce_165(val, _values, result)
result = aryfy(val[0])
result
end
.,.,
module_eval(<<'.,.,', 'grammar.ra', 541)
def _reduce_166(val, _values, result)
val[0].push val[1]
result = val[0]
result
end
.,.,
module_eval(<<'.,.,', 'grammar.ra', 546)
def _reduce_167(val, _values, result)
@lexer.commentpop
result = ast AST::CaseOpt, :value => val[0], :statements => val[3]
result
end
.,.,
module_eval(<<'.,.,', 'grammar.ra', 549)
def _reduce_168(val, _values, result)
@lexer.commentpop
result = ast(
AST::CaseOpt,
:value => val[0],
:statements => ast(AST::BlockExpression)
)
result
end
.,.,
module_eval(<<'.,.,', 'grammar.ra', 559)
def _reduce_169(val, _values, result)
result = aryfy(val[0])
result
end
.,.,
module_eval(<<'.,.,', 'grammar.ra', 561)
def _reduce_170(val, _values, result)
val[0].push(val[2])
result = val[0]
result
end
.,.,
module_eval(<<'.,.,', 'grammar.ra', 566)
def _reduce_171(val, _values, result)
result = ast AST::Selector, :param => val[0], :values => val[2]
result
end
.,.,
# reduce 172 omitted
module_eval(<<'.,.,', 'grammar.ra', 571)
def _reduce_173(val, _values, result)
@lexer.commentpop
result = val[1]
result
end
.,.,
# reduce 174 omitted
module_eval(<<'.,.,', 'grammar.ra', 577)
def _reduce_175(val, _values, result)
if val[0].instance_of?(AST::ASTArray)
val[0].push(val[2])
result = val[0]
else
result = ast AST::ASTArray, :children => [val[0],val[2]]
end
result
end
.,.,
module_eval(<<'.,.,', 'grammar.ra', 586)
def _reduce_176(val, _values, result)
result = ast AST::ResourceParam, :param => val[0], :value => val[2]
result
end
.,.,
# reduce 177 omitted
# reduce 178 omitted
# reduce 179 omitted
# reduce 180 omitted
# reduce 181 omitted
# reduce 182 omitted
# reduce 183 omitted
# reduce 184 omitted
module_eval(<<'.,.,', 'grammar.ra', 598)
def _reduce_185(val, _values, result)
result = ast AST::Default, :value => val[0][:value], :line => val[0][:line]
result
end
.,.,
# reduce 186 omitted
module_eval(<<'.,.,', 'grammar.ra', 603)
def _reduce_187(val, _values, result)
result = [val[0][:value]]
result
end
.,.,
# reduce 188 omitted
module_eval(<<'.,.,', 'grammar.ra', 605)
def _reduce_189(val, _values, result)
result = val[0] += val[2]
result
end
.,.,
module_eval(<<'.,.,', 'grammar.ra', 608)
def _reduce_190(val, _values, result)
val[1].each do |file|
import(file)
end
result = nil
result
end
.,.,
module_eval(<<'.,.,', 'grammar.ra', 618)
def _reduce_191(val, _values, result)
@lexer.commentpop
result = Puppet::Parser::AST::Definition.new(classname(val[1]),
ast_context(true).merge(:arguments => val[2], :code => val[4],
:line => val[0][:line]))
@lexer.indefine = false
#} | DEFINE NAME argumentlist parent LBRACE RBRACE {
result
end
.,.,
module_eval(<<'.,.,', 'grammar.ra', 626)
def _reduce_192(val, _values, result)
@lexer.commentpop
result = Puppet::Parser::AST::Definition.new(classname(val[1]),
ast_context(true).merge(:arguments => val[2], :line => val[0][:line]))
@lexer.indefine = false
result
end
.,.,
module_eval(<<'.,.,', 'grammar.ra', 634)
def _reduce_193(val, _values, result)
@lexer.commentpop
# Our class gets defined in the parent namespace, not our own.
@lexer.namepop
result = Puppet::Parser::AST::Hostclass.new(classname(val[1]),
ast_context(true).merge(:arguments => val[2], :parent => val[3],
:code => val[5], :line => val[0][:line]))
result
end
.,.,
module_eval(<<'.,.,', 'grammar.ra', 641)
def _reduce_194(val, _values, result)
@lexer.commentpop
# Our class gets defined in the parent namespace, not our own.
@lexer.namepop
result = Puppet::Parser::AST::Hostclass.new(classname(val[1]),
ast_context(true).merge(:arguments => val[2], :parent => val[3],
:line => val[0][:line]))
result
end
.,.,
module_eval(<<'.,.,', 'grammar.ra', 650)
def _reduce_195(val, _values, result)
@lexer.commentpop
result = Puppet::Parser::AST::Node.new(val[1],
ast_context(true).merge(:parent => val[2], :code => val[4],
:line => val[0][:line]))
result
end
.,.,
module_eval(<<'.,.,', 'grammar.ra', 655)
def _reduce_196(val, _values, result)
@lexer.commentpop
result = Puppet::Parser::AST::Node.new(val[1], ast_context(true).merge(:parent => val[2], :line => val[0][:line]))
result
end
.,.,
module_eval(<<'.,.,', 'grammar.ra', 659)
def _reduce_197(val, _values, result)
result = val[0][:value]
result
end
.,.,
module_eval(<<'.,.,', 'grammar.ra', 660)
def _reduce_198(val, _values, result)
result = "class"
result
end
.,.,
module_eval(<<'.,.,', 'grammar.ra', 665)
def _reduce_199(val, _values, result)
result = [result]
result
end
.,.,
module_eval(<<'.,.,', 'grammar.ra', 668)
def _reduce_200(val, _values, result)
result = val[0]
result << val[2]
result
end
.,.,
module_eval(<<'.,.,', 'grammar.ra', 673)
def _reduce_201(val, _values, result)
result = ast AST::HostName, :value => val[0]
result
end
.,.,
module_eval(<<'.,.,', 'grammar.ra', 676)
def _reduce_202(val, _values, result)
result = val[0][:value]
result
end
.,.,
module_eval(<<'.,.,', 'grammar.ra', 677)
def _reduce_203(val, _values, result)
result = val[0][:value]
result
end
.,.,
module_eval(<<'.,.,', 'grammar.ra', 678)
def _reduce_204(val, _values, result)
result = val[0][:value]
result
end
.,.,
# reduce 205 omitted
module_eval(<<'.,.,', 'grammar.ra', 682)
def _reduce_206(val, _values, result)
result = nil
result
end
.,.,
module_eval(<<'.,.,', 'grammar.ra', 686)
def _reduce_207(val, _values, result)
result = ast AST::ASTArray, :children => []
result
end
.,.,
# reduce 208 omitted
module_eval(<<'.,.,', 'grammar.ra', 691)
def _reduce_209(val, _values, result)
result = nil
result
end
.,.,
module_eval(<<'.,.,', 'grammar.ra', 694)
def _reduce_210(val, _values, result)
result = val[1]
result = [result] unless result[0].is_a?(Array)
result
end
.,.,
# reduce 211 omitted
module_eval(<<'.,.,', 'grammar.ra', 700)
def _reduce_212(val, _values, result)
result = val[0]
result = [result] unless result[0].is_a?(Array)
result << val[2]
result
end
.,.,
module_eval(<<'.,.,', 'grammar.ra', 706)
def _reduce_213(val, _values, result)
result = [val[0][:value], val[2]]
result
end
.,.,
module_eval(<<'.,.,', 'grammar.ra', 707)
def _reduce_214(val, _values, result)
result = [val[0][:value]]
result
end
.,.,
# reduce 215 omitted
module_eval(<<'.,.,', 'grammar.ra', 711)
def _reduce_216(val, _values, result)
result = val[1]
result
end
.,.,
# reduce 217 omitted
module_eval(<<'.,.,', 'grammar.ra', 716)
def _reduce_218(val, _values, result)
result = val[1]
result
end
.,.,
# reduce 219 omitted
# reduce 220 omitted
module_eval(<<'.,.,', 'grammar.ra', 722)
def _reduce_221(val, _values, result)
result = ast AST::Variable, :value => val[0][:value], :line => val[0][:line]
result
end
.,.,
module_eval(<<'.,.,', 'grammar.ra', 725)
def _reduce_222(val, _values, result)
result = val[1]
result
end
.,.,
module_eval(<<'.,.,', 'grammar.ra', 726)
def _reduce_223(val, _values, result)
result = val[1]
result
end
.,.,
module_eval(<<'.,.,', 'grammar.ra', 727)
def _reduce_224(val, _values, result)
result = ast AST::ASTArray
result
end
.,.,
# reduce 225 omitted
# reduce 226 omitted
# reduce 227 omitted
module_eval(<<'.,.,', 'grammar.ra', 733)
def _reduce_228(val, _values, result)
result = nil
result
end
.,.,
module_eval(<<'.,.,', 'grammar.ra', 736)
def _reduce_229(val, _values, result)
result = ast AST::Regex, :value => val[0][:value]
result
end
.,.,
module_eval(<<'.,.,', 'grammar.ra', 740)
def _reduce_230(val, _values, result)
- if val[1].instance_of?(AST::ASTHash)
+ @lexer.commentpop
+ if val[1].instance_of?(AST::ASTHash)
result = val[1]
else
result = ast AST::ASTHash, { :value => val[1] }
end
result
end
.,.,
-module_eval(<<'.,.,', 'grammar.ra', 747)
+module_eval(<<'.,.,', 'grammar.ra', 748)
def _reduce_231(val, _values, result)
- if val[1].instance_of?(AST::ASTHash)
+ @lexer.commentpop
+ if val[1].instance_of?(AST::ASTHash)
result = val[1]
else
result = ast AST::ASTHash, { :value => val[1] }
end
result
end
.,.,
-module_eval(<<'.,.,', 'grammar.ra', 753)
+module_eval(<<'.,.,', 'grammar.ra', 755)
def _reduce_232(val, _values, result)
- result = ast AST::ASTHash
+ @lexer.commentpop
+ result = ast AST::ASTHash
result
end
.,.,
# reduce 233 omitted
-module_eval(<<'.,.,', 'grammar.ra', 758)
+module_eval(<<'.,.,', 'grammar.ra', 761)
def _reduce_234(val, _values, result)
if val[0].instance_of?(AST::ASTHash)
result = val[0].merge(val[2])
else
result = ast AST::ASTHash, :value => val[0]
result.merge(val[2])
end
result
end
.,.,
-module_eval(<<'.,.,', 'grammar.ra', 767)
+module_eval(<<'.,.,', 'grammar.ra', 770)
def _reduce_235(val, _values, result)
result = ast AST::ASTHash, { :value => { val[0] => val[2] } }
result
end
.,.,
-module_eval(<<'.,.,', 'grammar.ra', 770)
+module_eval(<<'.,.,', 'grammar.ra', 773)
def _reduce_236(val, _values, result)
result = val[0][:value]
result
end
.,.,
-module_eval(<<'.,.,', 'grammar.ra', 771)
+module_eval(<<'.,.,', 'grammar.ra', 774)
def _reduce_237(val, _values, result)
result = val[0]
result
end
.,.,
-module_eval(<<'.,.,', 'grammar.ra', 774)
+module_eval(<<'.,.,', 'grammar.ra', 777)
def _reduce_238(val, _values, result)
result = ast AST::HashOrArrayAccess, :variable => val[0][:value], :key => val[2]
result
end
.,.,
# reduce 239 omitted
-module_eval(<<'.,.,', 'grammar.ra', 779)
+module_eval(<<'.,.,', 'grammar.ra', 782)
def _reduce_240(val, _values, result)
result = ast AST::HashOrArrayAccess, :variable => val[0], :key => val[2]
result
end
.,.,
def _reduce_none(val, _values, result)
val[0]
end
end # class Parser
end # module Parser
end # module Puppet
diff --git a/lib/puppet/parser/parser_factory.rb b/lib/puppet/parser/parser_factory.rb
index 06c612f39..576bc8755 100644
--- a/lib/puppet/parser/parser_factory.rb
+++ b/lib/puppet/parser/parser_factory.rb
@@ -1,63 +1,88 @@
module Puppet; end
module Puppet::Parser
# The ParserFactory makes selection of parser possible.
# Currently, it is possible to switch between two different parsers:
# * classic_parser, the parser in 3.1
# * eparser, the Expression Based Parser
#
class ParserFactory
# Produces a parser instance for the given environment
def self.parser(environment)
case Puppet[:parser]
when 'future'
- eparser(environment)
+ if Puppet[:evaluator] == 'future'
+ evaluating_parser(environment)
+ else
+ eparser(environment)
+ end
else
classic_parser(environment)
end
end
# Creates an instance of the classic parser.
#
def self.classic_parser(environment)
require 'puppet/parser'
+
Puppet::Parser::Parser.new(environment)
end
+ # Returns an instance of an EvaluatingParser
+ def self.evaluating_parser(file_watcher)
+ # Since RGen is optional, test that it is installed
+ @@asserted ||= false
+ assert_rgen_installed() unless @@asserted
+ @@asserted = true
+ require 'puppet/parser/e4_parser_adapter'
+ require 'puppet/pops/parser/code_merger'
+ E4ParserAdapter.new(file_watcher)
+ end
+
# Creates an instance of the expression based parser 'eparser'
#
def self.eparser(environment)
# Since RGen is optional, test that it is installed
@@asserted ||= false
assert_rgen_installed() unless @@asserted
@@asserted = true
require 'puppet/parser'
require 'puppet/parser/e_parser_adapter'
EParserAdapter.new(Puppet::Parser::Parser.new(environment))
end
private
def self.assert_rgen_installed
begin
require 'rgen/metamodel_builder'
rescue LoadError
raise Puppet::DevError.new("The gem 'rgen' version >= 0.6.1 is required when using the setting '--parser future'. Please install 'rgen'.")
end
# Since RGen is optional, there is nothing specifying its version.
# It is not installed in any controlled way, so not possible to use gems to check (it may be installed some other way).
# Instead check that "eContainer, and eContainingFeature" has been installed.
require 'puppet/pops'
begin
litstring = Puppet::Pops::Model::LiteralString.new();
container = Puppet::Pops::Model::ArithmeticExpression.new();
container.left_expr = litstring
raise "no eContainer" if litstring.eContainer() != container
raise "no eContainingFeature" if litstring.eContainingFeature() != :left_expr
rescue
raise Puppet::DevError.new("The gem 'rgen' version >= 0.6.1 is required when using '--parser future'. An older version is installed, please update.")
end
end
+
+ def self.code_merger
+ if Puppet[:parser] == 'future' && Puppet[:evaluator] == 'future'
+ Puppet::Pops::Parser::CodeMerger.new
+ else
+ Puppet::Parser::CodeMerger.new
+ end
+ end
+
end
end
diff --git a/lib/puppet/parser/parser_support.rb b/lib/puppet/parser/parser_support.rb
index b67f3c752..963e4d106 100644
--- a/lib/puppet/parser/parser_support.rb
+++ b/lib/puppet/parser/parser_support.rb
@@ -1,186 +1,195 @@
# I pulled this into a separate file, because I got
# tired of rebuilding the parser.rb file all the time.
require 'forwardable'
class Puppet::Parser::Parser
extend Forwardable
require 'puppet/parser/functions'
require 'puppet/parser/files'
require 'puppet/resource/type_collection'
require 'puppet/resource/type_collection_helper'
require 'puppet/resource/type'
require 'monitor'
AST = Puppet::Parser::AST
include Puppet::Resource::TypeCollectionHelper
attr_reader :version, :environment
attr_accessor :files
attr_accessor :lexer
# Add context to a message; useful for error messages and such.
def addcontext(message, obj = nil)
obj ||= @lexer
message += " on line #{obj.line}"
if file = obj.file
message += " in file #{file}"
end
message
end
# Create an AST array containing a single element
def aryfy(arg)
ast AST::ASTArray, :children => [arg]
end
# Create an AST block containing a single element
def block(arg)
ast AST::BlockExpression, :children => [arg]
end
# Create an AST object, and automatically add the file and line information if
# available.
def ast(klass, hash = {})
klass.new ast_context(klass.use_docs, hash[:line]).merge(hash)
end
def ast_context(include_docs = false, ast_line = nil)
result = {
:line => ast_line || lexer.line,
:file => lexer.file
}
result[:doc] = lexer.getcomment(result[:line]) if include_docs
result
end
# The fully qualifed name, with the full namespace.
def classname(name)
[@lexer.namespace, name].join("::").sub(/^::/, '')
end
def clear
initvars
end
# Raise a Parse error.
def error(message, options = {})
if @lexer.expected
message += "; expected '%s'"
end
except = Puppet::ParseError.new(message)
except.line = options[:line] || @lexer.line
except.file = options[:file] || @lexer.file
raise except
end
def_delegators :@lexer, :file, :string=
def file=(file)
- unless Puppet::FileSystem::File.exist?(file)
+ unless Puppet::FileSystem.exist?(file)
unless file =~ /\.pp$/
file = file + ".pp"
end
end
- raise Puppet::AlreadyImportedError, "Import loop detected" if known_resource_types.watching_file?(file)
+ raise Puppet::AlreadyImportedError, "Import loop detected for #{file}" if known_resource_types.watching_file?(file)
watch_file(file)
@lexer.file = file
end
def_delegators :known_resource_types, :hostclass, :definition, :node, :nodes?
def_delegators :known_resource_types, :find_hostclass, :find_definition
def_delegators :known_resource_types, :watch_file, :version
def import(file)
+ deprecation_location_text =
+ if @lexer.file && @lexer.line
+ " at #{@lexer.file}:#{@lexer.line}"
+ elsif @lexer.file
+ " in file #{@lexer.file}"
+ elsif @lexer.line
+ " at #{@lexer.line}"
+ end
+
+ Puppet.deprecation_warning("The use of 'import' is deprecated#{deprecation_location_text}. See http://links.puppetlabs.com/puppet-import-deprecation")
if @lexer.file
# use a path relative to the file doing the importing
dir = File.dirname(@lexer.file)
else
# otherwise assume that everything needs to be from where the user is
# executing this command. Normally, this would be in a "puppet apply -e"
dir = Dir.pwd
end
known_resource_types.loader.import(file, dir)
end
def initialize(env)
- # The environment is needed to know how to find the resource type collection.
- @environment = env.is_a?(String) ? Puppet::Node::Environment.new(env) : env
+ @environment = env
initvars
end
# Initialize or reset all of our variables.
def initvars
@lexer = Puppet::Parser::Lexer.new
end
# Split an fq name into a namespace and name
def namesplit(fullname)
ary = fullname.split("::")
n = ary.pop || ""
ns = ary.join("::")
return ns, n
end
def on_error(token,value,stack)
if token == 0 # denotes end of file
value = 'end of file'
else
value = "'#{value[:value]}'"
end
error = "Syntax error at #{value}"
if brace = @lexer.expected
error += "; expected '#{brace}'"
end
except = Puppet::ParseError.new(error)
except.line = @lexer.line
except.file = @lexer.file if @lexer.file
raise except
end
# how should I do error handling here?
def parse(string = nil)
if self.file =~ /\.rb$/
main = parse_ruby_file
else
self.string = string if string
begin
@yydebug = false
main = yyparse(@lexer,:scan)
rescue Puppet::ParseError => except
except.line ||= @lexer.line
except.file ||= @lexer.file
except.pos ||= @lexer.pos
raise except
rescue => except
raise Puppet::ParseError.new(except.message, @lexer.file, @lexer.line, nil, except)
end
end
# Store the results as the top-level class.
return Puppet::Parser::AST::Hostclass.new('', :code => main)
ensure
@lexer.clear
end
def parse_ruby_file
Puppet.deprecation_warning("Use of the Ruby DSL is deprecated.")
# Execute the contents of the file inside its own "main" object so
# that it can call methods in the resource type API.
main_object = Puppet::DSL::ResourceTypeAPI.new
main_object.instance_eval(File.read(self.file))
# Then extract any types that were created.
Puppet::Parser::AST::BlockExpression.new :children => main_object.instance_eval { @__created_ast_objects__ }
end
end
diff --git a/lib/puppet/parser/resource.rb b/lib/puppet/parser/resource.rb
index 0b4c6677b..58e806c6e 100644
--- a/lib/puppet/parser/resource.rb
+++ b/lib/puppet/parser/resource.rb
@@ -1,267 +1,267 @@
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
require 'puppet/parser/resource/param'
require 'puppet/util/tagging'
require 'puppet/parser/yaml_trimmer'
require 'puppet/resource/type_collection_helper'
include Puppet::Resource::TypeCollectionHelper
include Puppet::Util
include Puppet::Util::MethodHelper
include Puppet::Util::Errors
include Puppet::Util::Logging
include Puppet::Parser::YamlTrimmer
attr_accessor :source, :scope, :collector_id
attr_accessor :virtual, :override, :translated, :catalog, :evaluated
attr_accessor :file, :line
attr_reader :exported, :parameters
# Determine whether the provided parameter name is a relationship parameter.
def self.relationship_parameter?(name)
@relationship_names ||= Puppet::Type.relationship_params.collect { |p| p.name }
@relationship_names.include?(name)
end
# Set up some boolean test methods
def translated?; !!@translated; end
def override?; !!@override; end
def evaluated?; !!@evaluated; end
def [](param)
param = param.intern
if param == :title
return self.title
end
if @parameters.has_key?(param)
@parameters[param].value
else
nil
end
end
def eachparam
@parameters.each do |name, param|
yield param
end
end
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
# defaults to main, if none is specified.
def add_edge_to_stage
return unless self.class?
unless stage = catalog.resource(:stage, self[:stage] || (scope && scope.resource && scope.resource[:stage]) || :main)
raise ArgumentError, "Could not find stage #{self[:stage] || :main} specified by #{self}"
end
self[:stage] ||= stage.title unless stage.title == :main
catalog.add_edge(stage, self)
end
# Retrieve the associated definition and evaluate it.
def evaluate
return if evaluated?
@evaluated = true
if klass = resource_type and ! builtin_type?
finish
evaluated_code = klass.evaluate_code(self)
return evaluated_code
elsif builtin?
devfail "Cannot evaluate a builtin type (#{type})"
else
self.fail "Cannot find definition #{type}"
end
end
# Mark this resource as both exported and virtual,
# or remove the exported mark.
def exported=(value)
if value
@virtual = true
@exported = value
else
@exported = value
end
end
# Do any finishing work on this object, called before evaluation or
# before storage/translation.
def finish
return if finished?
@finished = true
add_defaults
add_scope_tags
validate
end
# Has this resource already been finished?
def finished?
@finished
end
def initialize(*args)
raise ArgumentError, "Resources require a hash as last argument" unless args.last.is_a? Hash
raise ArgumentError, "Resources require a scope" unless args.last[:scope]
super
@source ||= scope.source
end
# Is this resource modeling an isomorphic resource type?
def isomorphic?
if builtin_type?
return resource_type.isomorphic?
else
return true
end
end
# Merge an override resource in. This will throw exceptions if
# any overrides aren't allowed.
def merge(resource)
# Test the resource scope, to make sure the resource is even allowed
# to override.
unless self.source.object_id == resource.source.object_id || resource.source.child_of?(self.source)
raise Puppet::ParseError.new("Only subclasses can override parameters", resource.line, resource.file)
end
# Some of these might fail, but they'll fail in the way we want.
resource.parameters.each do |name, param|
override_parameter(param)
end
end
# This only mattered for clients < 0.25, which we don't support any longer.
# ...but, since this hasn't been deprecated, and at least some functions
# used it, deprecate now rather than just eliminate. --daniel 2012-07-15
def metaparam_compatibility_mode?
Puppet.deprecation_warning "metaparam_compatibility_mode? is obsolete since < 0.25 clients are really, really not supported any more"
false
end
def name
self[:name] || self.title
end
# A temporary occasion, until I get paths in the scopes figured out.
alias path to_s
# Define a parameter in our resource.
# if we ever receive a parameter named 'tag', set
# the resource tags with its value.
def set_parameter(param, value = nil)
if ! value.nil?
param = Puppet::Parser::Resource::Param.new(
:name => param, :value => value, :source => self.source
)
elsif ! param.is_a?(Puppet::Parser::Resource::Param)
raise ArgumentError, "Received incomplete information - no value provided for parameter #{param}"
end
tag(*param.value) if param.name == :tag
# And store it in our parameter hash.
@parameters[param.name] = param
end
alias []= set_parameter
def to_hash
@parameters.inject({}) do |hash, ary|
param = ary[1]
# Skip "undef" values.
hash[param.name] = param.value if param.value != :undef
hash
end
end
# Convert this resource to a RAL resource.
def to_ral
copy_as_resource.to_ral
end
private
# Add default values from our definition.
def add_defaults
scope.lookupdefaults(self.type).each do |name, param|
unless @parameters.include?(name)
self.debug "Adding default for #{name}"
@parameters[name] = param.dup
end
end
end
def add_scope_tags
if scope_resource = scope.resource
tag(*scope_resource.tags)
end
end
# Accept a parameter from an override.
def override_parameter(param)
# This can happen if the override is defining a new parameter, rather
# than replacing an existing one.
(set_parameter(param) and return) unless current = @parameters[param.name]
# The parameter is already set. Fail if they're not allowed to override it.
unless param.source.child_of?(current.source)
msg = "Parameter '#{param.name}' is already set on #{self}"
msg += " by #{current.source}" if current.source.to_s != ""
if current.file or current.line
fields = []
fields << current.file if current.file
fields << current.line.to_s if current.line
msg += " at #{fields.join(":")}"
end
msg += "; cannot redefine"
Puppet.log_exception(ArgumentError.new(), msg)
raise Puppet::ParseError.new(msg, param.line, param.file)
end
# If we've gotten this far, we're allowed to override.
# Merge with previous value, if the parameter was generated with the +>
# syntax. It's important that we use a copy of the new param instance
# here, not the old one, and not the original new one, so that the source
# is registered correctly for later overrides but the values aren't
# implcitly shared when multiple resources are overrriden at once (see
# ticket #3556).
if param.add
param = param.dup
param.value = [current.value, param.value].flatten
end
set_parameter(param)
end
# Make sure the resource's parameters are all valid for the type.
def validate
@parameters.each do |name, param|
validate_parameter(name)
end
rescue => detail
- fail Puppet::ParseError, detail.to_s
+ self.fail Puppet::ParseError, detail.to_s, detail
end
def extract_parameters(params)
params.each do |param|
# Don't set the same parameter twice
self.fail Puppet::ParseError, "Duplicate parameter '#{param.name}' for on #{self}" if @parameters[param.name]
set_parameter(param)
end
end
end
diff --git a/lib/puppet/parser/scope.rb b/lib/puppet/parser/scope.rb
index 2cad0c1dd..11bb0fde6 100644
--- a/lib/puppet/parser/scope.rb
+++ b/lib/puppet/parser/scope.rb
@@ -1,684 +1,833 @@
# The scope class, which handles storing and retrieving variables and types and
# such.
require 'forwardable'
require 'puppet/parser'
require 'puppet/parser/templatewrapper'
require 'puppet/resource/type_collection_helper'
require 'puppet/util/methodhelper'
# This class is part of the internal parser/evaluator/compiler functionality of Puppet.
# It is passed between the various classes that participate in evaluation.
# None of its methods are API except those that are clearly marked as such.
#
# @api public
class Puppet::Parser::Scope
extend Forwardable
include Puppet::Util::MethodHelper
include Puppet::Resource::TypeCollectionHelper
require 'puppet/parser/resource'
AST = Puppet::Parser::AST
Puppet::Util.logmethods(self)
- include Enumerable
include Puppet::Util::Errors
attr_accessor :source, :resource
- attr_accessor :base, :keyword
- attr_accessor :top, :translated, :compiler
+ attr_accessor :compiler
attr_accessor :parent
attr_reader :namespaces
# Add some alias methods that forward to the compiler, since we reference
# them frequently enough to justify the extra method call.
def_delegators :compiler, :catalog, :environment
- # thin wrapper around an ephemeral
- # symbol table.
- # when a symbol
+
+ # Abstract base class for LocalScope and MatchScope
+ #
class Ephemeral
- extend Forwardable
- def initialize(parent=nil, local=false)
- @symbols = {}
+ attr_reader :parent
+
+ def initialize(parent = nil)
@parent = parent
- @local_scope = local
end
- def_delegators :@symbols, :delete, :[]=, :each
+ def is_local_scope?
+ false
+ end
def [](name)
- if @symbols.include?(name) or @parent.nil?
- @symbols[name]
- else
+ if @parent
@parent[name]
end
end
def include?(name)
- bound?(name) or (@parent and @parent.include?(name))
+ (@parent and @parent.include?(name))
end
def bound?(name)
- @symbols.include?(name)
+ false
+ end
+
+ def add_entries_to(target = {})
+ @parent.add_entries_to(target) unless @parent.nil?
+ # do not include match data ($0-$n)
+ target
+ end
+ end
+
+ class LocalScope < Ephemeral
+
+ def initialize(parent=nil)
+ super parent
+ @symbols = {}
+ end
+
+ def [](name)
+ if @symbols.include?(name)
+ @symbols[name]
+ else
+ super
+ end
end
def is_local_scope?
- @local_scope
+ true
+ end
+
+ def []=(name, value)
+ @symbols[name] = value
+ end
+
+ def include?(name)
+ bound?(name) || super
+ end
+
+ def delete(name)
+ @symbols.delete(name)
end
- # @return [Ephemeral, Hash, nil]
- def parent
- @parent
+ def bound?(name)
+ @symbols.include?(name)
end
def add_entries_to(target = {})
- @parent.add_entries_to(target) unless @parent.nil?
- # do not return pure ephemeral ($0-$n)
- if is_local_scope?
- @symbols.each do |k, v|
- if v == :undef
- target.delete(k)
- else
- target[ k ] = v
- end
+ super
+ @symbols.each do |k, v|
+ if v == :undef
+ target.delete(k)
+ else
+ target[ k ] = v
end
end
target
end
-
end
- # Initialize a new scope suitable for parser function testing. This method
- # should be considered a public API for external modules. A shared spec
- # helper should consume this API method.
- #
- # @api protected
- #
- def self.new_for_test_harness(node_name)
- node = Puppet::Node.new(node_name)
- compiler = Puppet::Parser::Compiler.new(node)
- scope = new(compiler)
- scope.source = Puppet::Resource::Type.new(:node, node_name)
- scope.parent = compiler.topscope
- scope
- end
+ class MatchScope < Ephemeral
+
+ attr_accessor :match_data
+
+ def initialize(parent = nil, match_data = nil)
+ super parent
+ @match_data = match_data
+ end
+
+ def is_local_scope?
+ false
+ end
+
+ def [](name)
+ if bound?(name)
+ @match_data[name.to_i]
+ else
+ super
+ end
+ end
+
+ def include?(name)
+ bound?(name) or super
+ end
+
+ def bound?(name)
+ # A "match variables" scope reports all numeric variables to be bound if the scope has
+ # match_data. Without match data the scope is transparent.
+ #
+ @match_data && name =~ /^\d+$/
+ end
+
+ def []=(name, value)
+ # TODO: Bad choice of exception
+ raise Puppet::ParseError, "Numerical variables cannot be changed. Attempt to set $#{name}"
+ end
+
+ def delete(name)
+ # TODO: Bad choice of exception
+ raise Puppet::ParseError, "Numerical variables cannot be deleted: Attempt to delete: $#{name}"
+ end
+
+ def add_entries_to(target = {})
+ # do not include match data ($0-$n)
+ super
+ end
- def each
- to_hash.each { |name, value| yield(name, value) }
end
- # Proxy accessors
- def host
- compiler.node.name
+ # Returns true if the variable of the given name has a non nil value.
+ # TODO: This has vague semantics - does the variable exist or not?
+ # use ['name'] to get nil or value, and if nil check with exist?('name')
+ # this include? is only useful because of checking against the boolean value false.
+ #
+ def include?(name)
+ ! self[name].nil?
end
- # TODO: 19514 - this is smelly; who uses this? functions? templates?
- # What about trusted facts ? Should untrusted facts be removed from facts?
+ # Returns true if the variable of the given name is set to any value (including nil)
#
- def facts
- compiler.node.facts
+ def exist?(name)
+ next_scope = inherited_scope || enclosing_scope
+ effective_symtable(true).include?(name) || next_scope && next_scope.exist?(name)
end
- def include?(name)
- ! self[name].nil?
+ # Returns true if the given name is bound in the current (most nested) scope for assignments.
+ #
+ def bound?(name)
+ # Do not look in ephemeral (match scope), the semantics is to answer if an assignable variable is bound
+ effective_symtable(false).bound?(name)
end
# Is the value true? This allows us to control the definition of truth
# in one place.
def self.true?(value)
case value
when ''
false
when :undef
false
else
!!value
end
end
# Coerce value to a number, or return `nil` if it isn't one.
def self.number?(value)
case value
when Numeric
value
when /^-?\d+(:?\.\d+|(:?\.\d+)?e\d+)$/
value.to_f
when /^0x[0-9a-f]+$/i
value.to_i(16)
when /^0[0-7]+$/
value.to_i(8)
when /^-?\d+$/
value.to_i
else
nil
end
end
# Add to our list of namespaces.
def add_namespace(ns)
return false if @namespaces.include?(ns)
if @namespaces == [""]
@namespaces = [ns]
else
@namespaces << ns
end
end
def find_hostclass(name, options = {})
known_resource_types.find_hostclass(namespaces, name, options)
end
def find_definition(name)
known_resource_types.find_definition(namespaces, name)
end
+ def find_global_scope()
+ # walk upwards until first found node_scope or top_scope
+ if is_nodescope? || is_topscope?
+ self
+ else
+ next_scope = inherited_scope || enclosing_scope
+ if next_scope.nil?
+ # this happens when testing, and there is only a single test scope and no link to any
+ # other scopes
+ self
+ else
+ next_scope.find_global_scope()
+ end
+ end
+ end
+
# This just delegates directly.
def_delegator :compiler, :findresource
# Initialize our new scope. Defaults to having no parent.
def initialize(compiler, options = {})
if compiler.is_a? Puppet::Parser::Compiler
self.compiler = compiler
else
raise Puppet::DevError, "you must pass a compiler instance to a new scope object"
end
if n = options.delete(:namespace)
@namespaces = [n]
else
@namespaces = [""]
end
raise Puppet::DevError, "compiler passed in options" if options.include? :compiler
set_options(options)
extend_with_functions_module
# The symbol table for this scope. This is where we store variables.
- @symtable = Ephemeral.new(nil, true)
+ # @symtable = Ephemeral.new(nil, true)
+ @symtable = LocalScope.new(nil)
- @ephemeral = [ Ephemeral.new(@symtable) ]
+ @ephemeral = [ MatchScope.new(@symtable, nil) ]
# All of the defaults set for types. It's a hash of hashes,
# with the first key being the type, then the second key being
# the parameter.
@defaults = Hash.new { |dhash,type|
dhash[type] = {}
}
# The table for storing class singletons. This will only actually
# be used by top scopes and node scopes.
@class_scopes = {}
+
+ @enable_immutable_data = Puppet[:immutable_node_data]
end
# Store the fact that we've evaluated a class, and store a reference to
# the scope in which it was evaluated, so that we can look it up later.
def class_set(name, scope)
if parent
parent.class_set(name, scope)
else
@class_scopes[name] = scope
end
end
# Return the scope associated with a class. This is just here so
# that subclasses can set their parent scopes to be the scope of
# their parent class, and it's also used when looking up qualified
# variables.
def class_scope(klass)
# They might pass in either the class or class name
k = klass.respond_to?(:name) ? klass.name : klass
@class_scopes[k] || (parent && parent.class_scope(k))
end
# Collect all of the defaults set at any higher scopes.
# This is a different type of lookup because it's additive --
# it collects all of the defaults, with defaults in closer scopes
# overriding those in later scopes.
def lookupdefaults(type)
values = {}
# first collect the values from the parents
if parent
parent.lookupdefaults(type).each { |var,value|
values[var] = value
}
end
# then override them with any current values
# this should probably be done differently
if @defaults.include?(type)
@defaults[type].each { |var,value|
values[var] = value
}
end
values
end
# Look up a defined type.
def lookuptype(name)
find_definition(name) || find_hostclass(name)
end
def undef_as(x,v)
if v.nil? or v == :undef
x
else
v
end
end
# Lookup a variable within this scope using the Puppet language's
# scoping rules. Variables can be qualified using just as in a
# manifest.
#
# @param [String] name the variable name to lookup
#
# @return Object the value of the variable, or nil if it's not found
#
# @api public
def lookupvar(name, options = {})
unless name.is_a? String
raise Puppet::ParseError, "Scope variable name #{name.inspect} is a #{name.class}, not a string"
end
table = @ephemeral.last
if name =~ /^(.*)::(.+)$/
class_name = $1
variable_name = $2
lookup_qualified_variable(class_name, variable_name, options)
+
+ # TODO: optimize with an assoc instead, this searches through scopes twice for a hit
elsif table.include?(name)
table[name]
else
next_scope = inherited_scope || enclosing_scope
if next_scope
next_scope.lookupvar(name, options)
else
- nil
+ variable_not_found(name)
end
end
end
+ def variable_not_found(name, reason=nil)
+ if Puppet[:strict_variables]
+ if Puppet[:evaluator] == 'future' && Puppet[:parser] == 'future'
+ throw :undefined_variable
+ else
+ reason_msg = reason.nil? ? '' : "; #{reason}"
+ raise Puppet::ParseError, "Undefined variable #{name.inspect}#{reason_msg}"
+ end
+ else
+ nil
+ end
+ end
# Retrieves the variable value assigned to the name given as an argument. The name must be a String,
# and namespace can be qualified with '::'. The value is looked up in this scope, its parent scopes,
# or in a specific visible named scope.
#
# @param varname [String] the name of the variable (may be a qualified name using `(ns'::')*varname`
# @param options [Hash] Additional options, not part of api.
# @return [Object] the value assigned to the given varname
# @see #[]=
# @api public
#
def [](varname, options={})
lookupvar(varname, options)
end
# The scope of the inherited thing of this scope's resource. This could
# either be a node that was inherited or the class.
#
# @return [Puppet::Parser::Scope] The scope or nil if there is not an inherited scope
def inherited_scope
if has_inherited_class?
qualified_scope(resource.resource_type.parent)
else
nil
end
end
# The enclosing scope (topscope or nodescope) of this scope.
# The enclosing scopes are produced when a class or define is included at
# some point. The parent scope of the included class or define becomes the
# scope in which it was included. The chain of parent scopes is followed
# until a node scope or the topscope is found
#
# @return [Puppet::Parser::Scope] The scope or nil if there is no enclosing scope
def enclosing_scope
if has_enclosing_scope?
if parent.is_topscope? or parent.is_nodescope?
parent
else
parent.enclosing_scope
end
else
nil
end
end
def is_classscope?
resource and resource.type == "Class"
end
def is_nodescope?
resource and resource.type == "Node"
end
def is_topscope?
compiler and self == compiler.topscope
end
def lookup_qualified_variable(class_name, variable_name, position)
begin
if lookup_as_local_name?(class_name, variable_name)
self[variable_name]
else
qualified_scope(class_name).lookupvar(variable_name, position)
end
rescue RuntimeError => e
- location = if position[:lineproc]
- " at #{position[:lineproc].call}"
- elsif position[:file] && position[:line]
- " at #{position[:file]}:#{position[:line]}"
- else
- ""
- end
- warning "Could not look up qualified variable '#{class_name}::#{variable_name}'; #{e.message}#{location}"
- nil
+ unless Puppet[:strict_variables]
+ # Do not issue warning if strict variables are on, as an error will be raised by variable_not_found
+ location = if position[:lineproc]
+ " at #{position[:lineproc].call}"
+ elsif position[:file] && position[:line]
+ " at #{position[:file]}:#{position[:line]}"
+ else
+ ""
+ end
+ warning "Could not look up qualified variable '#{class_name}::#{variable_name}'; #{e.message}#{location}"
+ end
+ variable_not_found("#{class_name}::#{variable_name}", e.message)
end
end
# Handles the special case of looking up fully qualified variable in not yet evaluated top scope
# This is ok if the lookup request originated in topscope (this happens when evaluating
# bindings; using the top scope to provide the values for facts.
# @param class_name [String] the classname part of a variable name, may be special ""
# @param variable_name [String] the variable name without the absolute leading '::'
# @return [Boolean] true if the given variable name should be looked up directly in this scope
#
def lookup_as_local_name?(class_name, variable_name)
# not a local if name has more than one segment
return nil if variable_name =~ /::/
# partial only if the class for "" cannot be found
return nil unless class_name == "" && klass = find_hostclass(class_name) && class_scope(klass).nil?
is_topscope?
end
def has_inherited_class?
is_classscope? and resource.resource_type.parent
end
private :has_inherited_class?
def has_enclosing_scope?
not parent.nil?
end
private :has_enclosing_scope?
def qualified_scope(classname)
raise "class #{classname} could not be found" unless klass = find_hostclass(classname)
raise "class #{classname} has not been evaluated" unless kscope = class_scope(klass)
kscope
end
private :qualified_scope
# Returns a Hash containing all variables and their values, optionally (and
# by default) including the values defined in parent. Local values
# shadow parent values. Ephemeral scopes for match results ($0 - $n) are not included.
#
+ # This is currently a wrapper for to_hash_legacy or to_hash_future.
+ #
+ # @see to_hash_future
+ #
+ # @see to_hash_legacy
def to_hash(recursive = true)
+ @parser ||= Puppet[:parser]
+ if @parser == 'future'
+ to_hash_future(recursive)
+ else
+ to_hash_legacy(recursive)
+ end
+ end
+
+ # Fixed version of to_hash that implements scoping correctly (i.e., with
+ # dynamic scoping disabled #28200 / PUP-1220
+ #
+ # @see to_hash
+ def to_hash_future(recursive)
+ if recursive and has_enclosing_scope?
+ target = enclosing_scope.to_hash_future(recursive)
+ else
+ target = Hash.new
+ end
+
+ # add all local scopes
+ @ephemeral.last.add_entries_to(target)
+ target
+ end
+
+ # The old broken implementation of to_hash that retains the dynamic scoping
+ # semantics
+ #
+ # @see to_hash
+ def to_hash_legacy(recursive = true)
if recursive and parent
- target = parent.to_hash(recursive)
+ target = parent.to_hash_legacy(recursive)
else
target = Hash.new
end
# add all local scopes
@ephemeral.last.add_entries_to(target)
target
end
def namespaces
@namespaces.dup
end
# Create a new scope and set these options.
def newscope(options = {})
compiler.newscope(self, options)
end
def parent_module_name
return nil unless @parent
return nil unless @parent.source
@parent.source.module_name
end
# Set defaults for a type. The typename should already be downcased,
# so that the syntax is isolated. We don't do any kind of type-checking
# here; instead we let the resource do it when the defaults are used.
def define_settings(type, params)
table = @defaults[type]
# if we got a single param, it'll be in its own array
params = [params] unless params.is_a?(Array)
params.each { |param|
if table.include?(param.name)
raise Puppet::ParseError.new("Default already defined for #{type} { #{param.name} }; cannot redefine", param.line, param.file)
end
table[param.name] = param
}
end
- RESERVED_VARIABLE_NAMES = ['trusted'].freeze
+ RESERVED_VARIABLE_NAMES = ['trusted', 'facts'].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.
# It's preferred that you use self[]= instead of this; only use this
# when you need to set options.
def setvar(name, value, options = {})
if name =~ /^[0-9]+$/
- raise Puppet::ParseError.new("Cannot assign to a numeric match result variable '$#{name}'") unless options[:ephemeral]
+ raise Puppet::ParseError.new("Cannot assign to a numeric match result variable '$#{name}'") # unless options[:ephemeral]
end
unless name.is_a? String
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)
+ if @enable_immutable_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]
error = Puppet::ParseError.new("Cannot append, variable #{name} is defined in this scope")
else
error = Puppet::ParseError.new("Cannot reassign variable #{name}")
end
error.file = options[:file] if options[:file]
error.line = options[:line] if options[:line]
raise error
end
if options[:append]
table[name] = append_value(undef_as('', self[name]), value)
else
table[name] = value
end
table[name]
end
def set_trusted(hash)
setvar('trusted', deep_freeze(hash), :privileged => true)
end
+ def set_facts(hash)
+ setvar('facts', 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 Array
+ object.each {|v| deep_freeze(v) }
+ object.freeze
when Hash
object.each {|k, v| deep_freeze(k); deep_freeze(v) }
- when NilClass
+ object.freeze
+ when NilClass, Numeric, TrueClass, FalseClass
# do nothing
when String
object.freeze
else
- raise Puppet::Error, "Unsupported data type: '#{object.class}"
+ 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"
# will be returned (irrespective of it being a match scope or a local scope).
#
# @param use_ephemeral [Boolean] whether the top most ephemeral (of any kind) should be used or not
def effective_symtable use_ephemeral
s = @ephemeral.last
- return s if use_ephemeral
+ return s || @symtable if use_ephemeral
+ # Why check if ephemeral is a Hash ??? Not needed, a hash cannot be a parent scope ???
while s && !(s.is_a?(Hash) || s.is_local_scope?())
s = s.parent
end
s ? s : @symtable
end
# Sets the variable value of the name given as an argument to the given value. The value is
# set in the current scope and may shadow a variable with the same name in a visible outer scope.
# It is illegal to re-assign a variable in the same scope. It is illegal to set a variable in some other
# scope/namespace than the scope passed to a method.
#
# @param varname [String] The variable name to which the value is assigned. Must not contain `::`
# @param value [String] The value to assign to the given variable name.
# @param options [Hash] Additional options, not part of api.
#
# @api public
#
def []=(varname, value, options = {})
setvar(varname, value, options = {})
end
def append_value(bound_value, new_value)
case new_value
when Array
bound_value + new_value
when Hash
bound_value.merge(new_value)
else
if bound_value.is_a?(Hash)
raise ArgumentError, "Trying to append to a hash with something which is not a hash is unsupported"
end
bound_value + new_value
end
end
private :append_value
# Return the tags associated with this scope.
def_delegator :resource, :tags
# Used mainly for logging
def to_s
"Scope(#{@resource})"
end
# remove ephemeral scope up to level
+ # TODO: Who uses :all ? Remove ??
+ #
def unset_ephemeral_var(level=:all)
if level == :all
- @ephemeral = [ Ephemeral.new(@symtable)]
+ @ephemeral = [ MatchScope.new(@symtable, nil)]
else
@ephemeral.pop(@ephemeral.size - level)
end
end
- # check if name exists in one of the ephemeral scopes.
- def ephemeral_include?(name)
- @ephemeral.any? {|eph| eph.include?(name) }
+ def ephemeral_level
+ @ephemeral.size
end
- # Checks whether the variable should be processed in the ephemeral scope or not.
- # All numerical variables are processed in ephemeral scope at all times, and all other
- # variables when the ephemeral scope is a local scope.
- #
- def ephemeral?(name)
- @ephemeral.last.is_local_scope? || name =~ /^\d+$/
+ # TODO: Who calls this?
+ def new_ephemeral(local_scope = false)
+ if local_scope
+ @ephemeral.push(LocalScope.new(@ephemeral.last))
+ else
+ @ephemeral.push(MatchScope.new(@ephemeral.last, nil))
+ end
end
- def ephemeral_level
- @ephemeral.size
+ # Sets match data in the most nested scope (which always is a MatchScope), it clobbers match data already set there
+ #
+ def set_match_data(match_data)
+ @ephemeral.last.match_data = match_data
end
- def new_ephemeral(local_scope = false)
- @ephemeral.push(Ephemeral.new(@ephemeral.last, local_scope))
+ # Nests a match data scope
+ def new_match_scope(match_data)
+ @ephemeral.push(MatchScope.new(@ephemeral.last, match_data))
end
def ephemeral_from(match, file = nil, line = nil)
case match
when Hash
# Create local scope ephemeral and set all values from hash
- new_ephemeral true
+ new_ephemeral(true)
match.each {|k,v| setvar(k, v, :file => file, :line => line, :ephemeral => true) }
+ # Must always have an inner match data scope (that starts out as transparent)
+ # In 3x slightly wasteful, since a new nested scope is created for a match
+ # (TODO: Fix that problem)
+ new_ephemeral(false)
else
raise(ArgumentError,"Invalid regex match data. Got a #{match.class}") unless match.is_a?(MatchData)
# Create a match ephemeral and set values from match data
- new_ephemeral false
- setvar("0", match[0], :file => file, :line => line, :ephemeral => true)
- match.captures.each_with_index do |m,i|
- setvar("#{i+1}", m, :file => file, :line => line, :ephemeral => true)
- end
+ new_match_scope(match)
end
end
def find_resource_type(type)
# It still works fine without the type == 'class' short-cut, but it is a lot slower.
return nil if ["class", "node"].include? type.to_s.downcase
find_builtin_resource_type(type) || find_defined_resource_type(type)
end
def find_builtin_resource_type(type)
Puppet::Type.type(type.to_s.downcase.to_sym)
end
def find_defined_resource_type(type)
- environment.known_resource_types.find_definition(namespaces, type.to_s.downcase)
+ known_resource_types.find_definition(namespaces, type.to_s.downcase)
end
+
def method_missing(method, *args, &block)
method.to_s =~ /^function_(.*)$/
name = $1
super unless name
super unless Puppet::Parser::Functions.function(name)
# In odd circumstances, this might not end up defined by the previous
# method, so we might as well be certain.
if respond_to? method
send(method, *args)
else
raise Puppet::DevError, "Function #{name} not defined despite being loaded!"
end
end
def resolve_type_and_titles(type, titles)
raise ArgumentError, "titles must be an array" unless titles.is_a?(Array)
case type.downcase
when "class"
# resolve the titles
titles = titles.collect do |a_title|
hostclass = find_hostclass(a_title)
hostclass ? hostclass.name : a_title
end
when "node"
# no-op
else
# resolve the type
resource_type = find_resource_type(type)
type = resource_type.name if resource_type
end
return [type, titles]
end
private
def extend_with_functions_module
- extend Puppet::Parser::Functions.environment_module(Puppet::Node::Environment.root)
- extend Puppet::Parser::Functions.environment_module(environment) if environment != Puppet::Node::Environment.root
+ root = Puppet.lookup(:root_environment)
+ extend Puppet::Parser::Functions.environment_module(root)
+ extend Puppet::Parser::Functions.environment_module(environment) if environment != root
end
end
diff --git a/lib/puppet/parser/templatewrapper.rb b/lib/puppet/parser/templatewrapper.rb
index fc5721626..e4426cdf9 100644
--- a/lib/puppet/parser/templatewrapper.rb
+++ b/lib/puppet/parser/templatewrapper.rb
@@ -1,127 +1,127 @@
require 'puppet/parser/files'
require 'erb'
# A simple wrapper for templates, so they don't have full access to
# the scope objects.
#
# @api private
class Puppet::Parser::TemplateWrapper
include Puppet::Util
Puppet::Util.logmethods(self)
def initialize(scope)
@__scope__ = scope
end
# @return [String] The full path name of the template that is being executed
# @api public
def file
@__file__
end
# @return [Puppet::Parser::Scope] The scope in which the template is evaluated
# @api public
def scope
@__scope__
end
# Find which line in the template (if any) we were called from.
# @return [String] the line number
# @api private
def script_line
identifier = Regexp.escape(@__file__ || "(erb)")
(caller.find { |l| l =~ /#{identifier}:/ }||"")[/:(\d+):/,1]
end
private :script_line
# Should return true if a variable is defined, false if it is not
# @api public
def has_variable?(name)
scope.include?(name.to_s)
end
# @return [Array<String>] The list of defined classes
# @api public
def classes
scope.catalog.classes
end
# @return [Array<String>] The tags defined in the current scope
# @api public
def tags
scope.tags
end
# @return [Array<String>] All the defined tags
# @api public
def all_tags
scope.catalog.tags
end
# Ruby treats variables like methods, so we used to expose variables
# within scope to the ERB code via method_missing. As per RedMine #1427,
# though, this means that conflicts between methods in our inheritance
# tree (Kernel#fork) and variable names (fork => "yes/no") could arise.
#
# Worse, /new/ conflicts could pop up when a new kernel or object method
# was added to Ruby, causing templates to suddenly fail mysteriously when
# Ruby was upgraded.
#
# To ensure that legacy templates using unqualified names work we retain
# the missing_method definition here until we declare the syntax finally
# dead.
def method_missing(name, *args)
line_number = script_line
if scope.include?(name.to_s)
Puppet.deprecation_warning("Variable access via '#{name}' is deprecated. Use '@#{name}' instead. #{to_s}:#{line_number}")
return scope[name.to_s, { :file => @__file__, :line => line_number }]
else
# Just throw an error immediately, instead of searching for
# other missingmethod things or whatever.
raise Puppet::ParseError.new("Could not find value for '#{name}'", @__file__, line_number)
end
end
# @api private
def file=(filename)
- unless @__file__ = Puppet::Parser::Files.find_template(filename, scope.compiler.environment.to_s)
+ unless @__file__ = Puppet::Parser::Files.find_template(filename, scope.compiler.environment)
raise Puppet::ParseError, "Could not find template '#{filename}'"
end
# We'll only ever not have a parser in testing, but, eh.
scope.known_resource_types.watch_file(@__file__)
end
# @api private
def result(string = nil)
if string
template_source = "inline template"
else
string = File.read(@__file__)
template_source = @__file__
end
# Expose all the variables in our scope as instance variables of the
# current object, making it possible to access them without conflict
# to the regular methods.
benchmark(:debug, "Bound template variables for #{template_source}") do
scope.to_hash.each do |name, value|
realname = name.gsub(/[^\w]/, "_")
instance_variable_set("@#{realname}", value)
end
end
result = nil
benchmark(:debug, "Interpolated template #{template_source}") do
template = ERB.new(string, 0, "-")
template.filename = @__file__
result = template.result(binding)
end
result
end
def to_s
"template[#{(@__file__ ? @__file__ : "inline")}]"
end
end
diff --git a/lib/puppet/parser/type_loader.rb b/lib/puppet/parser/type_loader.rb
index 4ce9dc24f..1357f948d 100644
--- a/lib/puppet/parser/type_loader.rb
+++ b/lib/puppet/parser/type_loader.rb
@@ -1,141 +1,152 @@
require 'find'
require 'forwardable'
require 'puppet/node/environment'
require 'puppet/parser/parser_factory'
class Puppet::Parser::TypeLoader
extend Forwardable
- include Puppet::Node::Environment::Helper
# Import manifest files that match a given file glob pattern.
#
# @param pattern [String] the file glob to apply when determining which files
# to load
# @param dir [String] base directory to use when the file is not
# found in a module
# @api private
def import(pattern, dir)
return if Puppet[:ignoreimport]
modname, files = Puppet::Parser::Files.find_manifests_in_modules(pattern, environment)
if files.empty?
abspat = File.expand_path(pattern, dir)
file_pattern = abspat + (File.extname(abspat).empty? ? '{.pp,.rb}' : '' )
files = Dir.glob(file_pattern).uniq.reject { |f| FileTest.directory?(f) }
modname = nil
if files.empty?
raise_no_files_found(pattern)
end
end
load_files(modname, files)
end
# Load all of the manifest files in all known modules.
# @api private
def import_all
# And then load all files from each module, but (relying on system
# behavior) only load files from the first module of a given name. E.g.,
# given first/foo and second/foo, only files from first/foo will be loaded.
environment.modules.each do |mod|
load_files(mod.name, mod.all_manifests)
end
end
def_delegator :environment, :known_resource_types
def initialize(env)
self.environment = env
end
+ def environment
+ @environment
+ end
+
+ def environment=(env)
+ if env.is_a?(String) or env.is_a?(Symbol)
+ @environment = Puppet.lookup(:environments).get(env)
+ else
+ @environment = env
+ end
+ end
+
# Try to load the object with the given fully qualified name.
def try_load_fqname(type, fqname)
return nil if fqname == "" # special-case main.
files_to_try_for(fqname).each do |filename|
begin
imported_types = import_from_modules(filename)
if result = imported_types.find { |t| t.type == type and t.name == fqname }
Puppet.debug "Automatically imported #{fqname} from #{filename} into #{environment}"
return result
end
rescue Puppet::ImportError => detail
# I'm not convienced we should just drop these errors, but this
# preserves existing behaviours.
end
end
# Nothing found.
return nil
end
def parse_file(file)
Puppet.debug("importing '#{file}' in environment #{environment}")
parser = Puppet::Parser::ParserFactory.parser(environment)
parser.file = file
return parser.parse
end
private
def import_from_modules(pattern)
modname, files = Puppet::Parser::Files.find_manifests_in_modules(pattern, environment)
if files.empty?
raise_no_files_found(pattern)
end
load_files(modname, files)
end
def raise_no_files_found(pattern)
raise Puppet::ImportError, "No file(s) found for import of '#{pattern}'"
end
def load_files(modname, files)
@loaded ||= {}
loaded_asts = []
files.reject { |file| @loaded[file] }.each do |file|
# NOTE: This ugly implementation will be replaced in Puppet 3.5.
# The implementation now makes use of a global variable because the context support is
# not available until Puppet 3.5.
# The use case is that parsing for the purpose of searching for information
# should not abort. There is currently one such use case in indirector/resourcetype/parser
#
- if $squelsh_parse_errors
+ if Puppet.lookup(:squelch_parse_errors) {|| false }
begin
loaded_asts << parse_file(file)
rescue => e
# Resume from errors so that all parseable files would
# still be parsed. Mark this file as loaded so that
# it would not be parsed next time (handle it as if
# it was successfully parsed).
Puppet.debug("Unable to parse '#{file}': #{e.message}")
end
else
loaded_asts << parse_file(file)
end
@loaded[file] = true
end
loaded_asts.collect do |ast|
known_resource_types.import_ast(ast, modname)
end.flatten
end
# Return a list of all file basenames that should be tried in order
# to load the object with the given fully qualified name.
def files_to_try_for(qualified_name)
qualified_name.split('::').inject([]) do |paths, name|
add_path_for_name(paths, name)
end
end
def add_path_for_name(paths, name)
if paths.empty?
[name]
else
paths.unshift(File.join(paths.first, name))
end
end
end
diff --git a/lib/puppet/pops.rb b/lib/puppet/pops.rb
index be30a59ee..7aa812b59 100644
--- a/lib/puppet/pops.rb
+++ b/lib/puppet/pops.rb
@@ -1,85 +1,99 @@
module Puppet
module Pops
require 'puppet/pops/patterns'
require 'puppet/pops/utils'
require 'puppet/pops/adaptable'
require 'puppet/pops/adapters'
require 'puppet/pops/visitable'
require 'puppet/pops/visitor'
require 'puppet/pops/containment'
require 'puppet/pops/issues'
require 'puppet/pops/label_provider'
require 'puppet/pops/validation'
require 'puppet/pops/issue_reporter'
require 'puppet/pops/model/model'
module Types
require 'puppet/pops/types/types'
require 'puppet/pops/types/type_calculator'
require 'puppet/pops/types/type_factory'
require 'puppet/pops/types/type_parser'
require 'puppet/pops/types/class_loader'
+ require 'puppet/pops/types/enumeration'
end
module Model
require 'puppet/pops/model/tree_dumper'
require 'puppet/pops/model/ast_transformer'
require 'puppet/pops/model/ast_tree_dumper'
require 'puppet/pops/model/factory'
require 'puppet/pops/model/model_tree_dumper'
require 'puppet/pops/model/model_label_provider'
end
module Binder
module SchemeHandler
# the handlers are auto loaded via bindings
end
module Producers
require 'puppet/pops/binder/producers'
end
require 'puppet/pops/binder/binder'
require 'puppet/pops/binder/bindings_model'
require 'puppet/pops/binder/binder_issues'
require 'puppet/pops/binder/bindings_checker'
require 'puppet/pops/binder/bindings_factory'
require 'puppet/pops/binder/bindings_label_provider'
require 'puppet/pops/binder/bindings_validator_factory'
require 'puppet/pops/binder/injector_entry'
require 'puppet/pops/binder/key_factory'
require 'puppet/pops/binder/injector'
- require 'puppet/pops/binder/hiera2'
require 'puppet/pops/binder/bindings_composer'
require 'puppet/pops/binder/bindings_model_dumper'
require 'puppet/pops/binder/system_bindings'
require 'puppet/pops/binder/bindings_loader'
+ require 'puppet/pops/binder/lookup'
module Config
require 'puppet/pops/binder/config/binder_config'
require 'puppet/pops/binder/config/binder_config_checker'
require 'puppet/pops/binder/config/issues'
require 'puppet/pops/binder/config/diagnostic_producer'
end
end
module Parser
require 'puppet/pops/parser/eparser'
require 'puppet/pops/parser/parser_support'
+ require 'puppet/pops/parser/locator'
+ require 'puppet/pops/parser/locatable'
require 'puppet/pops/parser/lexer'
+ require 'puppet/pops/parser/lexer2'
require 'puppet/pops/parser/evaluating_parser'
+ require 'puppet/pops/parser/epp_parser'
end
module Validation
require 'puppet/pops/validation/checker3_1'
require 'puppet/pops/validation/validator_factory_3_1'
+ require 'puppet/pops/validation/checker4_0'
+ require 'puppet/pops/validation/validator_factory_4_0'
+ end
+
+ module Evaluator
+ require 'puppet/pops/evaluator/runtime3_support'
+ require 'puppet/pops/evaluator/evaluator_impl'
+ require 'puppet/pops/evaluator/epp_evaluator'
end
end
+ require 'puppet/parser/ast/pops_bridge'
require 'puppet/bindings'
end
diff --git a/lib/puppet/pops/adapters.rb b/lib/puppet/pops/adapters.rb
index 13e53845c..bea64041d 100644
--- a/lib/puppet/pops/adapters.rb
+++ b/lib/puppet/pops/adapters.rb
@@ -1,69 +1,101 @@
# The Adapters module contains adapters for Documentation, Origin, SourcePosition, and Loader.
#
module Puppet::Pops::Adapters
# A documentation adapter adapts an object with a documentation string.
# (The intended use is for a source text parser to extract documentation and store this
# in DocumentationAdapter instances).
#
class DocumentationAdapter < Puppet::Pops::Adaptable::Adapter
# @return [String] The documentation associated with an object
attr_accessor :documentation
end
- # An origin adapter adapts an object with where it came from. This origin
- # describes the resource (a file, etc.) where source text originates.
- # Instances of SourcePosAdapter is then used on other objects in a model to
- # describe their relative position versus the origin.
- #
- # @see Puppet::Pops::Utils#find_adapter
- #
- class OriginAdapter < Puppet::Pops::Adaptable::Adapter
- # @return [String] the origin of the adapted (usually a filename)
- attr_accessor :origin
- end
-
- # A SourcePosAdapter describes a position relative to an origin. (Typically an {OriginAdapter} is
- # associated with the root of a model. This origin has a URI to the resource, and a line number.
- # The offset in the SourcePosAdapter is then relative to this origin.
- # (This somewhat complex structure makes it possible to correctly refer to a source position
+ # A SourcePosAdapter holds a reference to a *Positioned* object (object that has offset and length).
+ # This somewhat complex structure makes it possible to correctly refer to a source position
# in source that is embedded in some resource; a parser only sees the embedded snippet of source text
- # and does not know where it was embedded).
+ # and does not know where it was embedded. It also enables lazy evaluation of source positions (they are
+ # rarely needed - typically just when there is an error to report.
#
- # @see Puppet::Pops::Utils#find_adapter
+ # @note It is relatively expensive to compute line and position on line - it is not something that
+ # should be done for every token or model object.
+ #
+ # @see Puppet::Pops::Utils#find_adapter, Puppet::Pops::Utils#find_closest_positioned
#
class SourcePosAdapter < Puppet::Pops::Adaptable::Adapter
- # @return [Fixnum] The start line in source starting from 1
- attr_accessor :line
+ attr_accessor :locator
+
+ def self.create_adapter(o)
+ new(o)
+ end
+
+ def initialize(o)
+ @adapted = o
+ end
+
+ def locator
+ # The locator is always the parent locator, all positioned objects are positioned within their
+ # parent. If a positioned object also has a locator that locator is for its children!
+ #
+ @locator ||= find_locator(@adapted.eContainer)
+ end
+
+ def find_locator(o)
+ if o.nil?
+ raise ArgumentError, "InternalError: SourcePosAdapter for something that has no locator among parents"
+ end
+ case
+ when o.is_a?(Puppet::Pops::Model::Program)
+ return o.locator
+ # TODO_HEREDOC use case of SubLocator instead
+ when o.is_a?(Puppet::Pops::Model::SubLocatedExpression) && !(found_locator = o.locator).nil?
+ return found_locator
+ when adapter = self.class.get(o)
+ return adapter.locator
+ else
+ find_locator(o.eContainer)
+ end
+ end
+ private :find_locator
- # @return [Fixnum] The position on the start_line (in characters) starting from 0
- attr_accessor :pos
+ def offset
+ @adapted.offset
+ end
- # @return [Fixnum] The (start) offset of source text characters
- # (starting from 0) representing the adapted object.
- # Value may be nil
- attr_accessor :offset
+ def length
+ @adapted.length
+ end
- # @return [Fixnum] The length (count) of characters of source text
- # representing the adapted object from the origin. Not including any
- # trailing whitespace.
- attr_accessor :length
+ # Produces the line number for the given offset.
+ # @note This is an expensive operation
+ #
+ def line
+ locator.line_for_offset(offset)
+ end
+
+ # Produces the position on the line of the given offset.
+ # @note This is an expensive operation
+ #
+ def pos
+ locator.pos_on_line(offset)
+ end
- def extract_text_from_string(string)
- string.slice(offset, length)
+ # Extracts the text represented by this source position (the string is obtained from the locator)
+ def extract_text
+ locator.string.slice(offset, length)
end
end
# A LoaderAdapter adapts an object with a {Puppet::Pops::Loader}. This is used to make further loading from the
# perspective of the adapted object take place in the perspective of this Loader.
#
# It is typically enough to adapt the root of a model as a search is made towards the root of the model
# until a loader is found, but there is no harm in duplicating this information provided a contained
# object is adapted with the correct loader.
#
# @see Puppet::Pops::Utils#find_adapter
#
class LoaderAdapter < Puppet::Pops::Adaptable::Adapter
# @return [Puppet::Pops::Loader] the loader
attr_accessor :loader
end
end
diff --git a/lib/puppet/pops/binder/binder.rb b/lib/puppet/pops/binder/binder.rb
index a21098ebc..54620db46 100644
--- a/lib/puppet/pops/binder/binder.rb
+++ b/lib/puppet/pops/binder/binder.rb
@@ -1,421 +1,393 @@
# The Binder is responsible for processing layered bindings that can be used to setup an Injector.
#
-# An instance should be created, and calls should then be made to {#define_categories} to define the available categories, and
-# their precedence. This should be followed by a call to {#define_layers} which will match the layered bindings against the
-# effective categories (filtering out everything that does not apply, handle overrides, abstract entries etc.).
+# An instance should be created and a call to {#define_layers} should be made which will process the layered bindings
+# (handle overrides, abstract entries etc.).
# The constructed hash with `key => InjectorEntry` mappings is obtained as {#injector_entries}, and is used to initialize an
# {Puppet::Pops::Binder::Injector Injector}.
#
# @api public
#
class Puppet::Pops::Binder::Binder
- # This limits the number of available categorizations, including "common".
+
# @api private
- PRECEDENCE_MAX = 1000
+ attr_reader :injector_entries
# @api private
- attr_reader :category_precedences
+ attr :id_index
# @api private
- attr_reader :category_values
+ attr_reader :key_factory
+ # A parent Binder or nil
# @api private
- attr_reader :injector_entries
+ attr_reader :parent
+ # The next anonymous key to use
# @api private
- attr_reader :key_factory
+ attr_reader :anonymous_key
- # Whether the binder is fully configured or not
- # @api public
- #
- attr_reader :configured
+ # This binder's precedence
+ # @api private
+ attr_reader :binder_precedence
# @api public
- def initialize
- @category_precedences = {}
- @category_values = {}
+ def initialize(layered_bindings, parent_binder=nil)
+ @parent = parent_binder
+ @id_index = Hash.new() { |k, v| [] }
+
@key_factory = Puppet::Pops::Binder::KeyFactory.new()
# Resulting hash of all key -> binding
@injector_entries = {}
- # Not configured until the fat lady sings
- @configured = false
-
- @next_anonymous_key = 0
- end
-
- # Answers the question 'is this binder configured?' to the point it can be used to instantiate an Injector
- # @api public
- def configured?()
- configured()
- end
-
- # Defines the effective categories in precedence order (highest precedence first).
- # The 'common' (lowest precedence) category should not be included in the list.
- # A sanity check is made that there are no more than 1000 categorizations (which is pretty wild).
- #
- # The term 'effective categories' refers to the evaluated list of tuples (categorization, category-value) represented with
- # an instance of Puppet::Pops::Binder::Bindings::EffectiveCategories.
- #
- # @param effective_categories [Puppet::Pops::Binder::Bindings::EffectiveCategories] effective categories (i.e. with evaluated values)
- # @raise ArgumentError if this binder is already configured
- # @raise ArgumentError if the argument is not an EffectiveCategories
- # @raise ArgumentError if there is an attempt to redefine a category (non unique, or 'common').
- # @return [Puppet::Pops::Binder::Binder] self
- # @api public
- #
- def define_categories(effective_categories)
- raise ArgumentError, "This categories are already defined. Cannot redefine." unless @category_precedences.empty?
-
- # Note: a model instance is used since a Hash does not have a defined order in all Rubies.
- unless effective_categories.is_a?(Puppet::Pops::Binder::Bindings::EffectiveCategories)
- raise ArgumentError, "Expected Puppet::Pops::Binder::Bindings::EffectiveCategories, but got a: #{effective_categories.class}"
- end
- categories = effective_categories.categories
- raise ArgumentError, "Category limit (#{PRECEDENCE_MAX}) exceeded" unless categories.size <= PRECEDENCE_MAX
-
- # Automatically add the 'common' category with lowest precedence
- @category_precedences['common'] = 0
-
- # if categories contains "common", it should be last - simply drop it if present
- if last = categories[-1]
- if last.categorization == 'common'
- categories.delete_at(-1)
- end
- end
- # Process the given categories (highest precedence is first in the list)
- categories.each_with_index do |c, index|
- cname = c.categorization
- raise ArgumentError, "Attempt to redefine categorization: #{cname}" if @category_precedences[cname]
- @category_precedences[cname] = PRECEDENCE_MAX - index
- @category_values[cname] = c.value
+ if @parent.nil?
+ @anonymous_key = 0
+ @binder_precedence = 0
+ else
+ # First anonymous key is the parent's next (non incremented key). (The parent can not change, it is
+ # the final, free key).
+ @anonymous_key = @parent.anonymous_key
+ @binder_precedence = @parent.binder_precedence + 1
end
- self
+ define_layers(layered_bindings)
end
# Binds layers from highest to lowest as defined by the given LayeredBindings.
# @note
- # Categories must be set with #define_categories before calling this method. The model should have been
+ # The model should have been
# validated to get better error messages if the model is invalid. This implementation expects the model
# to be valid, and any errors raised will be more technical runtime errors.
#
# @param layered_bindings [Puppet::Pops::Binder::Bindings::LayeredBindings] the named and ordered layers
- # @raise ArgumentError if categories have not been defined
# @raise ArgumentError if this binder is already configured
# @raise ArgumentError if bindings with unresolved 'override' surfaces as an effective binding
# @raise ArgumentError if the given argument has the wrong type, or if model is invalid in some way
# @return [Puppet::Pops::Binder::Binder] self
# @api public
#
def define_layers(layered_bindings)
- raise ArgumentError, "This binder is already configured. Cannot redefine its content." if configured?()
- raise ArgumentError, "Categories must be defined first" if @category_precedences.empty?
LayerProcessor.new(self, key_factory).bind(layered_bindings)
- injector_entries.each do |k,v|
- unless key_factory.is_contributions_key?(k) || v.is_resolved?()
+ contribution_keys = []
+ # make one pass over entries to collect contributions, and check overrides
+ injector_entries.each do |k,v|
+ if key_factory.is_contributions_key?(k)
+ contribution_keys << [k,v]
+ elsif !v.is_resolved?()
raise ArgumentError, "Binding with unresolved 'override' detected: #{self.class.format_binding(v.binding)}}"
+ else
+ # if binding has an id, add it to the index
+ add_id_to_index(v.binding)
end
end
- # and the fat lady has sung
- @configured = true
- self
+
+ # If a lower level binder has contributions for a key also contributed to in this binder
+ # they must included in the higher shadowing contribution.
+ # If a contribution is made to an id that is defined in a parent
+ # contribute to an id that is defined in a lower binder, it must be promoted to this binder (copied) or
+ # there is risk of making the lower level injector dirty.
+ #
+ contribution_keys.each do |kv|
+ parent_contribution = lookup_in_parent(kv[0])
+ next unless parent_contribution
+ injector_entries[kv[0]] = kv[1] + parent_contributions
+
+ # key the multibind_id from the contribution key
+ multibind_id = key_factory.multibind_contribution_key_to_id(kv[0])
+ promote_matching_bindings(self, @parent, multibind_id)
+ end
end
+ private :define_layers
# @api private
def next_anonymous_key
- tmp = @next_anonymous_key
- @next_anonymous_key += 1
+ tmp = @anonymous_key
+ @anonymous_key += 1
tmp
end
+ def add_id_to_index(binding)
+ return unless binding.is_a?(Puppet::Pops::Binder::Bindings::Multibinding) && !(id = binding.id).nil?
+ @id_index[id] = @id_index[id] << binding
+ end
+
+ def promote_matching_bindings(to_binder, from_binder, multibind_id)
+ return if from_binder.nil?
+ from_binder.id_index[ multibind_id ].each do |binding|
+ key = key_factory.binding_key(binding)
+ entry = lookup(key)
+ unless entry.precedence == @binder_precedence
+ # it is from a lower layer it must be promoted
+ injector_entries[ key ] = Puppet::Pops::Binder::InjectorEntry.new(binding, binder_precedence)
+ end
+ end
+ # recursive "up the parent chain" to promote all
+ promote_matching_bindings(to_binder, from_binder.parent, multibind_id)
+ end
+
+ def lookup_in_parent(key)
+ @parent.nil? ? nil : @parent.lookup(key)
+ end
+
+ def lookup(key)
+ if x = injector_entries[key]
+ return x
+ end
+ @parent ? @parent.lookup(key) : nil
+ end
+
# @api private
def self.format_binding(b)
type_name = Puppet::Pops::Types::TypeCalculator.new().string(b.type)
layer_name, bindings_name = get_named_binding_layer_and_name(b)
"binding: '#{type_name}/#{b.name}' in: '#{bindings_name}' in layer: '#{layer_name}'"
end
# @api private
def self.format_contribution_source(b)
layer_name, bindings_name = get_named_binding_layer_and_name(b)
"(layer: #{layer_name}, bindings: #{bindings_name})"
end
# @api private
def self.get_named_binding_layer_and_name(b)
return ['<unknown>', '<unknown>'] if b.nil?
return [get_named_layer(b), b.name] if b.is_a?(Puppet::Pops::Binder::Bindings::NamedBindings)
get_named_binding_layer_and_name(b.eContainer)
end
# @api private
def self.get_named_layer(b)
return '<unknown>' if b.nil?
return b.name if b.is_a?(Puppet::Pops::Binder::Bindings::NamedLayer)
get_named_layer(b.eContainer)
end
# Processes the information in a layer, aggregating it to the injector_entries hash in its parent binder.
# A LayerProcessor holds the intermediate state required while processing one layer.
#
# @api private
#
class LayerProcessor
- attr :effective_prec
- attr :prec_stack
attr :bindings
attr :binder
attr :key_factory
attr :contributions
+ attr :binder_precedence
def initialize(binder, key_factory)
@binder = binder
+ @binder_precedence = binder.binder_precedence
@key_factory = key_factory
- @prec_stack = []
- @effective_prec = nil
@bindings = []
@contributions = []
@@bind_visitor ||= Puppet::Pops::Visitor.new(nil,"bind",0,0)
end
# Add the binding to the list of potentially effective bindings from this layer
# @api private
#
def add(b)
- bindings << Puppet::Pops::Binder::InjectorEntry.new(effective_prec, b)
+ bindings << Puppet::Pops::Binder::InjectorEntry.new(b, binder_precedence)
end
# Add a multibind contribution
# @api private
#
def add_contribution(b)
- contributions << Puppet::Pops::Binder::InjectorEntry.new(effective_prec, b)
+ contributions << Puppet::Pops::Binder::InjectorEntry.new(b, binder_precedence)
end
# Bind given abstract binding
# @api private
#
def bind(binding)
@@bind_visitor.visit_this(self, binding)
end
- # @return [Puppet::Pops::Binder::InjectorEntry] the entry with the highest (category) precedence
+ # @return [Puppet::Pops::Binder::InjectorEntry] the entry with the highest precedence
# @api private
def highest(b1, b2)
- case b1.precedence <=> b2.precedence
- when 1
- b1
- when -1
- b2
- when 0
- raise_conflicting_binding(b1, b2)
+ if b1.is_abstract? != b2.is_abstract?
+ # if one is abstract and the other is not, the non abstract wins
+ b1.is_abstract? ? b2 : b1
+ else
+ case b1.precedence <=> b2.precedence
+ when 1
+ b1
+ when -1
+ b2
+ when 0
+ raise_conflicting_binding(b1, b2)
+ end
end
end
# Raises a conflicting bindings error given two InjectorEntry's with same precedence in the same layer
# (if they are in different layers, something is seriously wrong)
def raise_conflicting_binding(b1, b2)
- b1_layer_name, b1_bindings_name = Puppet::Pops::Binder::Binder.get_named_binding_layer_and_name(b1.binding)
- b2_layer_name, b2_bindings_name = Puppet::Pops::Binder::Binder.get_named_binding_layer_and_name(b2.binding)
+ b1_layer_name, b1_bindings_name = binder.class.get_named_binding_layer_and_name(b1.binding)
+ b2_layer_name, b2_bindings_name = binder.class.get_named_binding_layer_and_name(b2.binding)
- # The resolution is per layer, and if they differ something is serious wrong as a higher layer
- # overrides a lower; so no such conflict should be possible:
+ finality_msg = (b1.is_final? || b2.is_final?) ? ". Override of final binding not allowed" : ''
+
+ # TODO: Use of layer_name is not very good, it is not guaranteed to be unique
unless b1_layer_name == b2_layer_name
raise ArgumentError, [
- 'Internal Error: Conflicting binding for',
+ 'Conflicting binding for',
"'#{b1.binding.name}'",
'being resolved across layers',
"'#{b1_layer_name}' and",
"'#{b2_layer_name}'"
- ].join(' ')
+ ].join(' ')+finality_msg
end
# Conflicting bindings made from the same source
if b1_bindings_name == b2_bindings_name
raise ArgumentError, [
'Conflicting binding for name:',
"'#{b1.binding.name}'",
'in layer:',
"'#{b1_layer_name}', ",
'both from:',
"'#{b1_bindings_name}'"
- ].join(' ')
+ ].join(' ')+finality_msg
end
# Conflicting bindings from different sources
raise ArgumentError, [
'Conflicting binding for name:',
"'#{b1.binding.name}'",
'in layer:',
"'#{b1_layer_name}',",
'from:',
"'#{b1_bindings_name}', and",
"'#{b2_bindings_name}'"
- ].join(' ')
+ ].join(' ')+finality_msg
end
# Produces the key for the given Binding.
# @param binding [Puppet::Pops::Binder::Bindings::Binding] the binding to get a key for
# @return [Object] an opaque key
# @api private
#
def key(binding)
k = if is_contribution?(binding)
# contributions get a unique (sequential) key
binder.next_anonymous_key()
else
key_factory.binding_key(binding)
end
end
# @api private
def is_contribution?(binding)
! binding.multibind_id.nil?
end
- # @api private
- def push_precedences(precedences)
- prec_stack.push(precedences)
- @effective_prec = nil # clear cache
- end
-
- # @api private
- def pop_precedences()
- prec_stack.pop()
- @effective_prec = nil # clear cache
- end
-
- # Returns the effective precedence as an array with highest precedence first.
- # Internally the precedence is an array with the highest precedence first.
- #
- # @api private
- #
- def effective_prec()
- unless @effective_prec
- @effective_prec = prec_stack.flatten.uniq.sort.reverse
- if @effective_prec.size == 0
- @effective_prec = [ 0 ] # i.e. "common"
- end
- end
- @effective_prec
- end
-
# @api private
def bind_Binding(o)
if is_contribution?(o)
add_contribution(o)
else
add(o)
end
end
# @api private
def bind_Bindings(o)
o.bindings.each {|b| bind(b) }
end
# @api private
def bind_NamedBindings(o)
# Name is ignored here, it should be introspected when needed (in case of errors)
o.bindings.each {|b| bind(b) }
end
- # Process CategorizedBindings by calculating precedence, and then if satisfying the predicates, process the contained
- # bindings.
- # @api private
- #
- def bind_CategorizedBindings(o)
- precedences = o.predicates.collect do |p|
- prec = binder.category_precedences[p.categorization]
-
- # Skip bindings if the categorization is not present, or
- # if the category value is not the effective value for the categorization
- # Ignore the value for the common category (it is not possible to state common 'false' etc.)
- #
- return unless prec
- return unless binder.category_values[p.categorization] == p.value.downcase || p.categorization == 'common'
- prec
- end
- push_precedences(precedences)
- o.bindings.each {|b| bind(b) }
- pop_precedences()
- end
-
# Process layered bindings from highest to lowest layer
# @api private
#
def bind_LayeredBindings(o)
o.layers.each do |layer|
processor = LayerProcessor.new(binder, key_factory)
# All except abstract (==error) are transfered to injector_entries
processor.bind(layer).each do |k, v|
entry = binder.injector_entries[k]
unless key_factory.is_contributions_key?(k)
if v.is_abstract?()
layer_name, bindings_name = Puppet::Pops::Binder::Binder.get_named_binding_layer_and_name(v.binding)
type_name = key_factory.type_calculator.string(v.binding.type)
raise ArgumentError, "The abstract binding '#{type_name}/#{v.binding.name}' in '#{bindings_name}' in layer '#{layer_name}' was not overridden"
end
raise ArgumentError, "Internal Error - redefinition of key: #{k}, (should never happen)" if entry
binder.injector_entries[k] = v
else
entry ? entry << v : binder.injector_entries[k] = v
end
end
end
end
# Processes one named ("top level") layer consisting of a list of NamedBindings
# @api private
#
def bind_NamedLayer(o)
o.bindings.each {|b| bind(b) }
this_layer = {}
# process regular bindings
bindings.each do |b|
bkey = key(b.binding)
- # ignore if a higher layer defined it, but ensure override gets resolved
+ # ignore if a higher layer defined it (unless the lower is final), but ensure override gets resolved
+ # (override is not resolved across binders)
if x = binder.injector_entries[bkey]
+ if b.is_final?
+ raise_conflicting_binding(x, b)
+ end
x.mark_override_resolved()
next
end
+ # If a lower (parent) binder exposes a final binding it may not be overridden
+ #
+ if (x = binder.lookup_in_parent(bkey)) && x.is_final?
+ raise_conflicting_binding(x, b)
+ end
+
# if already found in this layer, one wins (and resolves override), or it is an error
existing = this_layer[bkey]
winner = existing ? highest(existing, b) : b
this_layer[bkey] = winner
if existing
winner.mark_override_resolved()
end
end
# Process contributions
# - organize map multibind_id to bindings with this id
# - for each id, create an array with the unique anonymous keys to the contributed bindings
# - bind the index to a special multibind contributions key (these are aggregated)
#
c_hash = Hash.new {|hash, key| hash[ key ] = [] }
contributions.each {|b| c_hash[ b.binding.multibind_id ] << b }
# - for each id
c_hash.each do |k, v|
index = v.collect do |b|
bkey = key(b.binding)
this_layer[bkey] = b
bkey
end
contributions_key = key_factory.multibind_contributions(k)
unless this_layer[contributions_key]
this_layer[contributions_key] = []
end
this_layer[contributions_key] += index
end
this_layer
end
end
end
diff --git a/lib/puppet/pops/binder/binder_issues.rb b/lib/puppet/pops/binder/binder_issues.rb
index 1258b1e78..98171add5 100644
--- a/lib/puppet/pops/binder/binder_issues.rb
+++ b/lib/puppet/pops/binder/binder_issues.rb
@@ -1,142 +1,122 @@
# @api public
module Puppet::Pops::Binder::BinderIssues
# NOTE: The methods #issue and #hard_issue are done in a somewhat funny way
# since the Puppet::Pops::Issues is a module with these methods defined on the module-class
# This makes it hard to inherit them in this module. (Likewise if Issues was a class, and they
# need to be defined for the class, and such methods are also not inherited, it becomes more
# difficult to reuse these. It did not seem as a good idea to refactor Issues at this point
# in time - they should both probably be refactored once bindings support is finished.
# Meanwhile, they delegate to Issues.
# (see Puppet::Pops::Issues#issue)
def self.issue (issue_code, *args, &block)
Puppet::Pops::Issues.issue(issue_code, *args, &block)
end
# (see Puppet::Pops::Issues#hard_issue)
def self.hard_issue(issue_code, *args, &block)
Puppet::Pops::Issues.hard_issue(issue_code, *args, &block)
end
# Producer issues (binding identified using :binding argument)
# @api public
MISSING_NAME = issue :MISSING_NAME, :binding do
"#{label.a_an_uc(binding)} with #{label.a_an(semantic)} has no name"
end
# @api public
MISSING_KEY = issue :MISSING_KEY, :binding do
"#{label.a_an_uc(binding)} with #{label.a_an(semantic)} has no key"
end
# @api public
MISSING_VALUE = issue :MISSING_VALUE, :binding do
"#{label.a_an_uc(binding)} with #{label.a_an(semantic)} has no value"
end
# @api public
MISSING_EXPRESSION = issue :MISSING_EXPRESSION, :binding do
"#{label.a_an_uc(binding)} with #{label.a_an(semantic)} has no expression"
end
# @api public
MISSING_CLASS_NAME = issue :MISSING_CLASS_NAME, :binding do
"#{label.a_an_uc(binding)} with #{label.a_an(semantic)} has no class name"
end
# @api public
CACHED_PRODUCER_MISSING_PRODUCER = issue :PRODUCER_MISSING_PRODUCER, :binding do
"#{label.a_an_uc(binding)} with #{label.a_an(semantic)} has no producer"
end
# @api public
INCOMPATIBLE_TYPE = issue :INCOMPATIBLE_TYPE, :binding, :expected_type, :actual_type do
"#{label.a_an_uc(binding)} with #{label.a_an(semantic)} has an incompatible type: expected #{label.a_an(expected_type)}, but got #{label.a_an(actual_type)}."
end
# @api public
MULTIBIND_INCOMPATIBLE_TYPE = issue :MULTIBIND_INCOMPATIBLE_TYPE, :binding, :actual_type do
"#{label.a_an_uc(binding)} with #{label.a_an(semantic)} cannot bind #{label.a_an(actual_type)} value"
end
# @api public
MODEL_OBJECT_IS_UNBOUND = issue :MODEL_OBJECT_IS_UNBOUND do
"#{label.a_an_uc(semantic)} is not contained in a binding"
end
# Binding issues (binding identified using semantic)
# @api public
MISSING_PRODUCER = issue :MISSING_PRODUCER do
"#{label.a_an_uc(semantic)} has no producer"
end
# @api public
MISSING_TYPE = issue :MISSING_TYPE do
"#{label.a_an_uc(semantic)} has no type"
end
# @api public
MULTIBIND_NOT_COLLECTION_PRODUCER = issue :MULTIBIND_NOT_COLLECTION_PRODUCER, :actual_producer do
"#{label.a_an_uc(semantic)} must have a MultibindProducerDescriptor, but got: #{label.a_an(actual_producer)}"
end
# @api public
MULTIBIND_TYPE_ERROR = issue :MULTIBIND_TYPE_ERROR, :actual_type do
"#{label.a_an_uc(semantic)} is expected to bind a collection type, but got: #{label.a_an(actual_type)}."
end
# @api public
MISSING_BINDINGS = issue :MISSING_BINDINGS do
"#{label.a_an_uc(semantic)} has zero bindings"
end
# @api public
MISSING_BINDINGS_NAME = issue :MISSING_BINDINGS_NAME do
"#{label.a_an_uc(semantic)} has no name"
end
# @api public
MISSING_PREDICATES = issue :MISSING_PREDICATES do
"#{label.a_an_uc(semantic)} has zero predicates"
end
- # @api public
- MISSING_CATEGORIZATION = issue :MISSING_CATEGORIZATION do
- "#{label.a_an_uc(semantic)} has a category without categorization"
- end
-
- # @api public
- MISSING_CATEGORY_VALUE = issue :MISSING_CATEGORY_VALUE do
- "#{label.a_an_uc(semantic)} has a category without value"
- end
-
# @api public
MISSING_LAYERS = issue :MISSING_LAYERS do
"#{label.a_an_uc(semantic)} has zero layers"
end
# @api public
MISSING_LAYER_NAME = issue :MISSING_LAYER_NAME do
"#{label.a_an_uc(semantic)} has a layer without name"
end
# @api public
MISSING_BINDINGS_IN_LAYER = issue :MISSING_BINDINGS_IN_LAYER, :layer do
"#{label.a_an_uc(semantic)} has zero bindings in #{label.label(layer)}"
end
- # @api public
- PRECEDENCE_MISMATCH_IN_CONTRIBUTION = issue :PRECEDENCE_MISMATCH_IN_CONTRIBUTION, :categorization do
- "Precedence mismatch: binding contribution '#{semantic.name}', category: '#{categorization}' is not in correct order"
- end
-
- # @api public
- MISSING_CATEGORY_PRECEDENCE = issue :MISSING_CATEGORY_PRECEDENCE, :categorization do
- "Missing category precedence: binding contribution '#{semantic.name}', category: '#{categorization}' not found in overall config"
- end
-
-end
\ No newline at end of file
+end
diff --git a/lib/puppet/pops/binder/bindings_checker.rb b/lib/puppet/pops/binder/bindings_checker.rb
index dad4bd7c2..6e436db72 100644
--- a/lib/puppet/pops/binder/bindings_checker.rb
+++ b/lib/puppet/pops/binder/bindings_checker.rb
@@ -1,217 +1,197 @@
# A validator/checker of a bindings model
# @api public
#
class Puppet::Pops::Binder::BindingsChecker
Bindings = Puppet::Pops::Binder::Bindings
Issues = Puppet::Pops::Binder::BinderIssues
Types = Puppet::Pops::Types
attr_reader :type_calculator
attr_reader :acceptor
# @api public
def initialize(diagnostics_producer)
@@check_visitor ||= Puppet::Pops::Visitor.new(nil, "check", 0, 0)
@type_calculator = Puppet::Pops::Types::TypeCalculator.new()
@expression_validator = Puppet::Pops::Validation::ValidatorFactory_3_1.new().checker(diagnostics_producer)
@acceptor = diagnostics_producer
end
# Validates the entire model by visiting each model element and calling `check`.
# The result is collected (or acted on immediately) by the configured diagnostic provider/acceptor
# given when creating this Checker.
#
# @api public
#
def validate(b)
check(b)
b.eAllContents.each {|c| check(c) }
end
# Performs binding validity check
# @api private
def check(b)
@@check_visitor.visit_this(self, b)
end
# Checks that a binding has a producer and a type
# @api private
def check_Binding(b)
# Must have a type
acceptor.accept(Issues::MISSING_TYPE, b) unless b.type.is_a?(Types::PObjectType)
# Must have a producer
acceptor.accept(Issues::MISSING_PRODUCER, b) unless b.producer.is_a?(Bindings::ProducerDescriptor)
end
# Checks that the producer is a Multibind producer and that the type is a PCollectionType
# @api private
def check_Multibinding(b)
# id is optional (empty id blocks contributions)
# A multibinding must have PCollectionType
acceptor.accept(Issues::MULTIBIND_TYPE_ERROR, b, {:actual_type => b.type}) unless b.type.is_a?(Types::PCollectionType)
# if the producer is nil, a suitable producer will be picked automatically
unless b.producer.nil? || b.producer.is_a?(Bindings::MultibindProducerDescriptor)
acceptor.accept(Issues::MULTIBIND_NOT_COLLECTION_PRODUCER, b, {:actual_producer => b.producer})
end
end
# Checks that the bindings object contains at least one binding. Then checks each binding in turn
# @api private
def check_Bindings(b)
acceptor.accept(Issues::MISSING_BINDINGS, b) unless has_entries?(b.bindings)
end
# Checks that a name has been associated with the bindings
# @api private
def check_NamedBindings(b)
acceptor.accept(Issues::MISSING_BINDINGS_NAME, b) unless has_chars?(b.name)
check_Bindings(b)
end
- # Check that the category has a categorization and a value
- # @api private
- def check_Category(c)
- acceptor.accept(Issues::MISSING_CATEGORIZATION, binding_parent(c)) unless has_chars?(c.categorization)
- acceptor.accept(Issues::MISSING_CATEGORY_VALUE, binding_parent(c)) unless has_chars?(c.value)
- end
-
- # Check that the binding contains at least one predicate and that all predicates are categorized and has a value
- # @api private
- def check_CategorizedBindings(b)
- acceptor.accept(Issues::MISSING_PREDICATES, b) unless has_entries?(b.predicates)
- check_Bindings(b)
- end
-
- # @api private
- def check_EffectiveCategories(ec)
- end
-
# Check layer has a name
# @api private
def check_NamedLayer(l)
acceptor.accept(Issues::MISSING_LAYER_NAME, binding_parent(l)) unless has_chars?(l.name)
-# It is ok to have an empty layer
-# acceptor.accept(Issues::MISSING_BINDINGS_IN_LAYER, binding_parent(l), { :layer => l.name }) unless has_entries?(l.bindings)
end
# Checks that the binding has layers and that each layer has a name and at least one binding
# @api private
def check_LayeredBindings(b)
acceptor.accept(Issues::MISSING_LAYERS, b) unless has_entries?(b.layers)
end
# Checks that the non caching producer has a producer to delegate to
# @api private
def check_NonCachingProducerDescriptor(p)
acceptor.accept(Issues::PRODUCER_MISSING_PRODUCER, p) unless p.producer.is_a?(Bindings::ProducerDescriptor)
end
# Checks that a constant value has been declared in the producer and that the type
# of the value is compatible with the type declared in the binding
# @api private
def check_ConstantProducerDescriptor(p)
# the product must be of compatible type
# TODO: Likely to change when value becomes a typed Puppet Object
b = binding_parent(p)
if p.value.nil?
acceptor.accept(Issues::MISSING_VALUE, p, {:binding => b})
else
infered = type_calculator.infer(p.value)
unless type_calculator.assignable?(b.type, infered)
acceptor.accept(Issues::INCOMPATIBLE_TYPE, p, {:binding => b, :expected_type => b.type, :actual_type => infered})
end
end
end
# Checks that an expression has been declared in the producer
# @api private
def check_EvaluatingProducerDescriptor(p)
unless p.expression.is_a?(Puppet::Pops::Model::Expression)
acceptor.accept(Issues::MISSING_EXPRESSION, p, {:binding => binding_parent(p)})
end
end
# Checks that a class name has been declared in the producer
# @api private
def check_InstanceProducerDescriptor(p)
acceptor.accept(Issues::MISSING_CLASS_NAME, p, {:binding => binding_parent(p)}) unless has_chars?(p.class_name)
end
# Checks that a type and a name has been declared. The type must be assignable to the type
# declared in the binding. The name can be an empty string to denote 'no name'
# @api private
def check_LookupProducerDescriptor(p)
b = binding_parent(p)
unless type_calculator.assignable(b.type, p.type)
acceptor.accept(Issues::INCOMPATIBLE_TYPE, p, {:binding => b, :expected_type => b.type, :actual_type => p.type })
end
acceptor.accept(Issues::MISSING_NAME, p, {:binding => b}) if p.name.nil? # empty string is OK
end
# Checks that a key has been declared, then calls producer_LookupProducerDescriptor to perform
# checks associated with the super class
# @api private
def check_HashLookupProducerDescriptor(p)
acceptor.accept(Issues::MISSING_KEY, p, {:binding => binding_parent(p)}) unless has_chars?(p.key)
check_LookupProducerDescriptor(p)
end
# Checks that the type declared in the binder is a PArrayType
# @api private
def check_ArrayMultibindProducerDescriptor(p)
b = binding_parent(p)
acceptor.accept(Issues::MULTIBIND_INCOMPATIBLE_TYPE, p, {:binding => b, :actual_type => b.type}) unless b.type.is_a?(Types::PArrayType)
end
# Checks that the type declared in the binder is a PHashType
# @api private
def check_HashMultibindProducerDescriptor(p)
b = binding_parent(p)
acceptor.accept(Issues::MULTIBIND_INCOMPATIBLE_TYPE, p, {:binding => b, :actual_type => b.type}) unless b.type.is_a?(Types::PHashType)
end
# Checks that the producer that this producer delegates to is declared
# @api private
def check_ProducerProducerDescriptor(p)
unless p.producer.is_a?(Bindings::ProducerDescriptor)
acceptor.accept(Issues::PRODUCER_MISSING_PRODUCER, p, {:binding => binding_parent(p)})
end
end
# @api private
def check_Expression(t)
@expression_validator.validate(t)
end
# @api private
def check_PObjectType(t)
# Do nothing
end
# Returns true if the argument is a non empty string
# @api private
def has_chars?(s)
s.is_a?(String) && !s.empty?
end
# @api private
def has_entries?(s)
!(s.nil? || s.empty?)
end
# @api private
def binding_parent(p)
begin
x = p.eContainer
if x.nil?
acceptor.accept(Issues::MODEL_OBJECT_IS_UNBOUND, p)
return nil
end
p = x
end while !p.is_a?(Bindings::AbstractBinding)
p
end
end
diff --git a/lib/puppet/pops/binder/bindings_composer.rb b/lib/puppet/pops/binder/bindings_composer.rb
index 7284f90c8..86be89ef0 100644
--- a/lib/puppet/pops/binder/bindings_composer.rb
+++ b/lib/puppet/pops/binder/bindings_composer.rb
@@ -1,241 +1,175 @@
# The BindingsComposer handles composition of multiple bindings sources
# It is directed by a {Puppet::Pops::Binder::Config::BinderConfig BinderConfig} that indicates how
# the final composition should be layered, and what should be included/excluded in each layer
#
# The bindings composer is intended to be used once per environment as the compiler starts its work.
#
# TODO: Possibly support envdir: scheme / relative to environment root (== same as confdir if there is only one environment).
# This is probably easier to do after ENC changes described in ARM-8 have been implemented.
# TODO: If same config is loaded in a higher layer, skip it in the lower (since it is meaningless to load it again with lower
# precedence. (Optimization, or possibly an error, should produce a warning).
#
class Puppet::Pops::Binder::BindingsComposer
# The BindingsConfig instance holding the read and parsed, but not evaluated configuration
# @api public
#
attr_reader :config
# map of scheme name to handler
# @api private
attr_reader :scheme_handlers
# @return Hash<String, Puppet::Module> map of module name to module instance
# @api private
attr_reader :name_to_module
# @api private
attr_reader :confdir
# @api private
attr_reader :diagnostics
# Container of all warnings and errors produced while initializing and loading bindings
#
# @api public
attr_reader :acceptor
# @api public
def initialize()
@acceptor = Puppet::Pops::Validation::Acceptor.new()
@diagnostics = Puppet::Pops::Binder::Config::DiagnosticProducer.new(acceptor)
@config = Puppet::Pops::Binder::Config::BinderConfig.new(@diagnostics)
if acceptor.errors?
Puppet::Pops::IssueReporter.assert_and_report(acceptor, :message => 'Binding Composer: error while reading config.')
raise Puppet::DevError.new("Internal Error: IssueReporter did not raise exception for errors in bindings config.")
end
end
# Configures and creates the boot injector.
# The read config may optionally contain mapping of bindings scheme handler name to handler class, and
# mapping of biera2 backend symbolic name to backend class.
# If present, these are turned into bindings in the category 'extension' (which is only used in the boot injector) which
# has higher precedence than 'default'. This is done to allow users to override the default bindings for
# schemes and backends.
# @param scope [Puppet::Parser:Scope] the scope (used to find compiler and injector for the environment)
# @api private
#
def configure_and_create_injector(scope)
# create the injector (which will pick up the bindings registered above)
@scheme_handlers = SchemeHandlerHelper.new(scope)
# get extensions from the config
# ------------------------------
scheme_extensions = @config.scheme_extensions
- hiera_backends = @config.hiera_backends
# Define a named bindings that are known by the SystemBindings
boot_bindings = Puppet::Pops::Binder::BindingsFactory.named_bindings(Puppet::Pops::Binder::SystemBindings::ENVIRONMENT_BOOT_BINDINGS_NAME) do
scheme_extensions.each_pair do |scheme, class_name|
# turn each scheme => class_name into a binding (contribute to the buildings-schemes multibind).
# do this in category 'extensions' to allow them to override the 'default'
- when_in_category('extension', 'true').bind do
+ bind do
name(scheme)
- instance_of(Puppetx::BINDINGS_SCHEMES_TYPE)
- in_multibind(Puppetx::BINDINGS_SCHEMES)
+ instance_of(::Puppetx::BINDINGS_SCHEMES_TYPE)
+ in_multibind(::Puppetx::BINDINGS_SCHEMES)
to_instance(class_name)
end
end
- hiera_backends.each_pair do |symbolic, class_name|
- # turn each symbolic => class_name into a binding (contribute to the hiera backends multibind).
- # do this in category 'extensions' to allow them to override the 'default'
- when_in_category('extension', 'true').bind do
- name(symbolic)
- instance_of(Puppetx::HIERA2_BACKENDS_TYPE)
- in_multibind(Puppetx::HIERA2_BACKENDS)
- to_instance(class_name)
- end
- end
end
@injector = scope.compiler.create_boot_injector(boot_bindings.model)
end
# @return [Puppet::Pops::Binder::Bindings::LayeredBindings]
def compose(scope)
# The boot injector is used to lookup scheme-handlers
configure_and_create_injector(scope)
# get all existing modules and their root path
@name_to_module = {}
scope.environment.modules.each {|mod| name_to_module[mod.name] = mod }
# setup the confdir
@confdir = Puppet.settings[:confdir]
factory = Puppet::Pops::Binder::BindingsFactory
contributions = []
configured_layers = @config.layering_config.collect do | layer_config |
- # get contributions with effective categories
+ # get contributions
contribs = configure_layer(layer_config, scope, diagnostics)
# collect the contributions separately for later checking of category precedence
contributions.concat(contribs)
# create a named layer with all the bindings for this layer
factory.named_layer(layer_config['name'], *contribs.collect {|c| c.bindings }.flatten)
end
- # must check all contributions are based on compatible category precedence
- # (Note that contributions no longer contains the bindings as a side effect of setting them in the collected
- # layer. The effective categories and the name remains in the contributed model; this is enough for checking
- # and error reporting).
- check_contribution_precedence(contributions)
-
# Add the two system layers; the final - highest ("can not be overridden" layer), and the lowest
# Everything here can be overridden 'default' layer.
#
configured_layers.insert(0, Puppet::Pops::Binder::SystemBindings.final_contribution)
configured_layers.insert(-1, Puppet::Pops::Binder::SystemBindings.default_contribution)
# and finally... create the resulting structure
factory.layered_bindings(*configured_layers)
end
- # Evaluates configured categorization and returns the result.
- # The result is not cached.
- # @api public
- #
- def effective_categories(scope)
- unevaluated_categories = @config.categorization
- parser = Puppet::Pops::Parser::EvaluatingParser.new()
- file_source = @config.config_file or "defaults in: #{__FILE__}"
- evaluated_categories = unevaluated_categories.collect do |category_tuple|
- evaluated_categories = [ category_tuple[0], parser.evaluate_string( scope, parser.quote( category_tuple[1] ), file_source ) ]
- if evaluated_categories[1].is_a?(String)
- # category values are always in lower case
- evaluated_categories[1] = evaluated_categories[1].downcase
- else
- raise ArgumentError, "Categorization value must be a string, category #{evaluated_categories[0]} evaluation resulted in a: '#{result[1].class}'"
- end
- evaluated_categories
- end
- Puppet::Pops::Binder::BindingsFactory::categories(evaluated_categories)
- end
-
private
- # Checks that contribution's effective categorization is in the same relative order as in the overall
- # categorization precedence.
- #
- def check_contribution_precedence(contributions)
- cat_prec = { }
- @config.categorization.each_with_index {|c, i| cat_prec[ c[0] ] = i }
- contributions.each() do |contrib|
- # Contributions that do not specify their opinion about categorization silently accepts the precedence
- # set in the root configuration - and may thus produce an unexpected result
- #
- next unless ec = contrib.effective_categories
- next unless categories = ec.categories
- prev_prec = -1
- categories.each do |c|
- prec = cat_prec[c.categorization]
- issues = Puppet::Pops::Binder::BinderIssues
- unless prec
- diagnostics.accept(issues::MISSING_CATEGORY_PRECEDENCE, c, :categorization => c.categorization)
- next
- end
- unless prec > prev_prec
- diagnostics.accept(issues::PRECEDENCE_MISMATCH_IN_CONTRIBUTION, c, :categorization => c.categorization)
- end
- prev_prec = prec
- end
- end
- end
-
def configure_layer(layer_description, scope, diagnostics)
name = layer_description['name']
# compute effective set of uris to load (and get rid of any duplicates in the process
included_uris = array_of_uris(layer_description['include'])
excluded_uris = array_of_uris(layer_description['exclude'])
effective_uris = Set.new(expand_included_uris(included_uris)).subtract(Set.new(expand_excluded_uris(excluded_uris)))
# Each URI should result in a ContributedBindings
effective_uris.collect { |uri| scheme_handlers[uri.scheme].contributed_bindings(uri, scope, self) }
end
def array_of_uris(descriptions)
return [] unless descriptions
descriptions = [descriptions] unless descriptions.is_a?(Array)
descriptions.collect {|d| URI.parse(d) }
end
def expand_included_uris(uris)
result = []
uris.each do |uri|
unless handler = scheme_handlers[uri.scheme]
raise ArgumentError, "Unknown bindings provider scheme: '#{uri.scheme}'"
end
result.concat(handler.expand_included(uri, self))
end
result
end
def expand_excluded_uris(uris)
result = []
uris.each do |uri|
unless handler = scheme_handlers[uri.scheme]
raise ArgumentError, "Unknown bindings provider scheme: '#{uri.scheme}'"
end
result.concat(handler.expand_excluded(uri, self))
end
result
end
class SchemeHandlerHelper
T = Puppet::Pops::Types::TypeFactory
HASH_OF_HANDLER = T.hash_of(T.type_of('Puppetx::Puppet::BindingsSchemeHandler'))
def initialize(scope)
@scope = scope
@cache = nil
end
def [] (scheme)
load_schemes unless @cache
@cache[scheme]
end
def load_schemes
@cache = @scope.compiler.boot_injector.lookup(@scope, HASH_OF_HANDLER, Puppetx::BINDINGS_SCHEMES) || {}
end
end
end
diff --git a/lib/puppet/pops/binder/bindings_factory.rb b/lib/puppet/pops/binder/bindings_factory.rb
index a5d3ca46a..c7716c52b 100644
--- a/lib/puppet/pops/binder/bindings_factory.rb
+++ b/lib/puppet/pops/binder/bindings_factory.rb
@@ -1,847 +1,805 @@
# A helper class that makes it easier to construct a Bindings model.
#
# The Bindings Model
# ------------------
# The BindingsModel (defined in {Puppet::Pops::Binder::Bindings} is a model that is intended to be generally free from Ruby concerns.
# This means that it is possible for system integrators to create and serialize such models using other technologies than
# Ruby. This manifests itself in the model in that producers are described using instances of a `ProducerDescriptor` rather than
# describing Ruby classes directly. This is also true of the type system where type is expressed using the {Puppet::Pops::Types} model
# to describe all types.
#
# This class, the `BindingsFactory` is a concrete Ruby API for constructing instances of classes in the model.
#
# Named Bindings
# --------------
# The typical usage of the factory is to call {named_bindings} which creates a container of bindings wrapped in a *build object*
# equipped with convenience methods to define the details of the just created named bindings.
# The returned builder is an instance of {Puppet::Pops::Binder::BindingsFactory::BindingsContainerBuilder BindingsContainerBuilder}.
#
# Binding
# -------
# A Binding binds a type/name key to a producer of a value. A binding is conveniently created by calling `bind` on a
# `BindingsContainerBuilder`. The call to bind, produces a binding wrapped in a build object equipped with convenience methods
# to define the details of the just created binding. The returned builder is an instance of
# {Puppet::Pops::Binder::BindingsFactory::BindingsBuilder BindingsBuilder}.
#
# Multibinding
# ------------
# A multibinding works like a binding, but it requires an additional ID. It also places constraints on the type of the binding;
# it must be a collection type (Hash or Array).
#
# Constructing and Contributing Bindings from Ruby
# ------------------------------------------------
# The bindings system is used by referencing bindings symbolically; these are then specified in a Ruby file which is autoloaded
# by Puppet. The entry point for user code that creates bindings is described in {Puppet::Bindings Bindings}.
# That class makes use of a BindingsFactory, and the builder objects to make it easy to construct bindings.
#
# It is intended that a user defining bindings in Ruby should be able to use the builder object methods for the majority of tasks.
# If something advanced is wanted, use of one of the helper class methods on the BuildingsFactory, and/or the
# {Puppet::Pops::Types::TypeCalculator TypeCalculator} will be required to create and configure objects that are not handled by
# the methods in the builder objects.
#
# Chaining of calls
# ------------------
# Since all the build methods return the build object it is easy to stack on additional calls. The intention is to
# do this in an order that is readable from left to right: `bind.string.name('thename').to(42)`, but there is nothing preventing
# making the calls in some other order e.g. `bind.to(42).name('thename').string`, the second is quite unreadable but produces
# the same result.
#
# For sake of human readability, the method `name` is alsp available as `named`, with the intention that it is used after a type,
# e.g. `bind.integer.named('the meaning of life').to(42)`
#
# Methods taking blocks
# ----------------------
# Several methods take an optional block. The block evaluates with the builder object as `self`. This means that there is no
# need to chain the methods calls, they can instead be made in sequence - e.g.
#
# bind do
# integer
# named 'the meaning of life'
# to 42
# end
#
# or mix the two styles
#
# bind do
# integer.named 'the meaning of life'
# to 42
# end
#
# Unwrapping the result
# ---------------------
# The result from all methods is a builder object. Call the method `model` to unwrap the constructed bindings model object.
#
# bindings = BindingsFactory.named_bindings('my named bindings') do
# # bind things
# end.model
#
# @example Create a NamedBinding with content
# result = Puppet::Pops::Binder::BindingsFactory.named_bindings("mymodule::mybindings") do
# bind.name("foo").to(42)
-# when_in_category("node", "kermit.example.com").bind.name("foo").to(43)
# bind.string.name("site url").to("http://www.example.com")
# end
# result.model()
#
# @api public
#
module Puppet::Pops::Binder::BindingsFactory
# Alias for the {Puppet::Pops::Types::TypeFactory TypeFactory}. This is also available as the method
# `type_factory`.
#
T = Puppet::Pops::Types::TypeFactory
# Abstract base class for bindings object builders.
# Supports delegation of method calls to the BindingsFactory class methods for all methods not implemented
# by a concrete builder.
#
# @abstract
#
class AbstractBuilder
# The built model object.
attr_reader :model
# @param binding [Puppet::Pops::Binder::Bindings::AbstractBinding] The binding to build.
# @api public
def initialize(binding)
@model = binding
end
# Provides convenient access to the Bindings Factory class methods. The intent is to provide access to the
# methods that return producers for the purpose of composing more elaborate things than the builder convenience
# methods support directly.
# @api private
#
def method_missing(meth, *args, &block)
factory = Puppet::Pops::Binder::BindingsFactory
if factory.respond_to?(meth)
factory.send(meth, *args, &block)
else
super
end
end
end
# A bindings builder for an AbstractBinding containing other AbstractBinding instances.
# @api public
class BindingsContainerBuilder < AbstractBuilder
# Adds an empty binding to the container, and returns a builder for it for further detailing.
# An optional block may be given which is evaluated using `instance_eval`.
# @return [BindingsBuilder] the builder for the created binding
# @api public
#
def bind(&block)
binding = Puppet::Pops::Binder::Bindings::Binding.new()
model.addBindings(binding)
builder = BindingsBuilder.new(binding)
builder.instance_eval(&block) if block_given?
builder
end
# Binds a multibind with the given identity where later, the looked up result contains all
# contributions to this key. An optional block may be given which is evaluated using `instance_eval`.
# @param id [String] the multibind's id used when adding contributions
# @return [MultibindingsBuilder] the builder for the created multibinding
# @api public
#
def multibind(id, &block)
binding = Puppet::Pops::Binder::Bindings::Multibinding.new()
binding.id = id
model.addBindings(binding)
builder = MultibindingsBuilder.new(binding)
builder.instance_eval(&block) if block_given?
builder
end
-
- # Adds a categorized bindings to this container. Returns a BindingsContainerBuilder to allow adding
- # bindings in the newly created container. An optional block may be given which is evaluated using `instance_eval`.
- # @param categorization [String] the name of the categorization e.g. 'node'
- # @param category_value [String] the value in that category e.g. 'kermit.example.com'
- # @return [BindingsContainerBuilder] the builder for the created categorized bindings container
- # @api public
- #
- def when_in_category(categorization, category_value, &block)
- when_in_categories({categorization => category_value}, &block)
- end
-
- # Adds a categorized bindings to this container. Returns a BindingsContainerBuilder to allow adding
- # bindings in the newly created container.
- # The result is that a processed request must match all the given categorizations
- # with the given values. An optional block may be given which is evaluated using `instance_eval`.
- # @param categories_hash Hash[String, String] a hash with categorization and categorization value entries
- # @return [BindingsContainerBuilder] the builder for the created categorized bindings container
- # @api public
- #
- def when_in_categories(categories_hash, &block)
- binding = Puppet::Pops::Binder::Bindings::CategorizedBindings.new()
- categories_hash.each do |k,v|
- pred = Puppet::Pops::Binder::Bindings::Category.new()
- pred.categorization = k
- pred.value = v
- binding.addPredicates(pred)
- end
- model.addBindings(binding)
- builder = BindingsContainerBuilder.new(binding)
- builder.instance_eval(&block) if block_given?
- builder
- end
end
# Builds a Binding via convenience methods.
#
# @api public
#
class BindingsBuilder < AbstractBuilder
# @param binding [Puppet::Pops::Binder::Bindings::AbstractBinding] the binding to build.
# @api public
def initialize(binding)
super binding
data()
end
# Sets the name of the binding.
# @param name [String] the name to bind.
# @api public
def name(name)
model.name = name
self
end
# Same as {#name}, but reads better in certain combinations.
# @api public
alias_method :named, :name
# Sets the binding to be abstract (it must be overridden)
# @api public
def abstract
model.abstract = true
self
end
# Sets the binding to be override (it must override something)
# @api public
def override
model.override = true
self
end
+ # Sets the binding to be final (it may not be overridden)
+ # @api public
+ def final
+ model.final = true
+ self
+ end
+
# Makes the binding a multibind contribution to the given multibind id
# @param id [String] the multibind id to contribute this binding to
# @api public
def in_multibind(id)
model.multibind_id = id
self
end
# Sets the type of the binding to the given type.
# @note
# This is only needed if something other than the default type `Data` is wanted, or if the wanted type is
# not provided by one of the convenience methods {#array_of_data}, {#boolean}, {#float}, {#hash_of_data},
# {#integer}, {#literal}, {#pattern}, {#string}, or one of the collection methods {#array_of}, or {#hash_of}.
#
# To create a type, use the method {#type_factory}, to obtain the type.
# @example creating a Hash with Integer key and Array[Integer] element type
# tc = type_factory
# type(tc.hash(tc.array_of(tc.integer), tc.integer)
# @param type [Puppet::Pops::Types::PObjectType] the type to set for the binding
# @api public
#
def type(type)
model.type = type
self
end
# Sets the type of the binding to Integer.
# @return [Puppet::Pops::Types::PIntegerType] the type
# @api public
def integer()
type(T.integer())
end
# Sets the type of the binding to Float.
# @return [Puppet::Pops::Types::PFloatType] the type
# @api public
def float()
type(T.float())
end
# Sets the type of the binding to Boolean.
# @return [Puppet::Pops::Types::PBooleanType] the type
# @api public
def boolean()
type(T.boolean())
end
# Sets the type of the binding to String.
# @return [Puppet::Pops::Types::PStringType] the type
# @api public
def string()
type(T.string())
end
# Sets the type of the binding to Pattern.
- # @return [Puppet::Pops::Types::PPatternType] the type
+ # @return [Puppet::Pops::Types::PRegexpType] the type
# @api public
def pattern()
type(T.pattern())
end
- # Sets the type of the binding to the abstract type Literal.
- # @return [Puppet::Pops::Types::PLiteralType] the type
+ # Sets the type of the binding to the abstract type Scalar.
+ # @return [Puppet::Pops::Types::PScalarType] the type
# @api public
- def literal()
- type(T.literal())
+ def scalar()
+ type(T.scalar())
end
# Sets the type of the binding to the abstract type Data.
# @return [Puppet::Pops::Types::PDataType] the type
# @api public
def data()
type(T.data())
end
# Sets the type of the binding to Array[Data].
# @return [Puppet::Pops::Types::PArrayType] the type
# @api public
def array_of_data()
type(T.array_of_data())
end
# Sets the type of the binding to Array[T], where T is given.
# @param t [Puppet::Pops::Types::PObjectType] the type of the elements of the array
# @return [Puppet::Pops::Types::PArrayType] the type
# @api public
def array_of(t)
type(T.array_of(t))
end
# Sets the type of the binding to Hash[Literal, Data].
# @return [Puppet::Pops::Types::PHashType] the type
# @api public
def hash_of_data()
type(T.hash_of_data())
end
# Sets type of the binding to `Hash[Literal, t]`.
# To also limit the key type, use {#type} and give it a fully specified
# hash using {#type_factory} and then `hash_of(value_type, key_type)`.
# @return [Puppet::Pops::Types::PHashType] the type
# @api public
def hash_of(t)
type(T.hash_of(t))
end
# Sets the type of the binding based on the given argument.
# @overload instance_of(t)
# The same as calling {#type} with `t`.
# @param t [Puppet::Pops::Types::PObjectType] the type
# @overload instance_of(o)
# Infers the type from the given Ruby object and sets that as the type - i.e. "set the type
# of the binding to be that of the given data object".
# @param o [Object] the object to infer the type from
# @overload instance_of(c)
# @param c [Class] the Class to base the type on.
# Sets the type based on the given ruby class. The result is one of the specific puppet types
# if the class can be represented by a specific type, or the open ended PRubyType otherwise.
# @overload instance_of(s)
# The same as using a class, but instead of giving a class instance, the class is expressed using its fully
# qualified name. This method of specifying the type allows late binding (the class does not have to be loaded
# before it can be used in a binding).
# @param s [String] the fully qualified classname to base the type on.
# @return the resulting type
# @api public
#
def instance_of(t)
type(T.type_of(t))
end
# Provides convenient access to the type factory.
# This is intended to be used when methods taking a type as argument i.e. {#type}, {#array_of}, {#hash_of}, and {#instance_of}.
# @note
# The type factory is also available via the constant {T}.
# @api public
def type_factory
Puppet::Pops::Types::TypeFactory
end
# Sets the binding's producer to a singleton producer, if given argument is a value, a literal producer is created for it.
# To create a producer producing an instance of a class with lazy loading of the class, use {#to_instance}.
#
# @overload to(a_literal)
# Sets a constant producer in the binding.
# @overload to(a_class, *args)
# Sets an Instantiating producer (producing an instance of the given class)
# @overload to(a_producer_descriptor)
# Sets the producer from the given producer descriptor
# @return [BindingsBuilder] self
# @api public
#
def to(producer, *args)
case producer
when Class
producer = Puppet::Pops::Binder::BindingsFactory.instance_producer(producer.name, *args)
+ when Puppet::Pops::Model::Program
+ # program is not an expression
+ producer = Puppet::Pops::Binder::BindingsFactory.evaluating_producer(producer.body)
when Puppet::Pops::Model::Expression
producer = Puppet::Pops::Binder::BindingsFactory.evaluating_producer(producer)
when Puppet::Pops::Binder::Bindings::ProducerDescriptor
else
# If given producer is not a producer, create a literal producer
producer = Puppet::Pops::Binder::BindingsFactory.literal_producer(producer)
end
model.producer = producer
self
end
# Sets the binding's producer to a producer of an instance of given class (a String class name, or a Class instance).
# Use a string class name when lazy loading of the class is wanted.
#
# @overload to_instance(class_name, *args)
# @param class_name [String] the name of the class to instantiate
# @param args [Object] optional arguments to the constructor
# @overload to_instance(a_class)
# @param a_class [Class] the class to instantiate
# @param args [Object] optional arguments to the constructor
#
def to_instance(type, *args)
class_name = case type
when Class
type.name
when String
type
else
raise ArgumentError, "to_instance accepts String (a class name), or a Class.*args got: #{type.class}."
end
model.producer = Puppet::Pops::Binder::BindingsFactory.instance_producer(class_name, *args)
end
# Sets the binding's producer to a singleton producer
# @overload to_producer(a_producer)
# Sets the producer to an instantiated producer. The resulting model can not be serialized as a consequence as there
# is no meta-model describing the specialized producer. Use this only in exceptional cases, or where there is never the
# need to serialize the model.
# @param a_producer [Puppet::Pops::Binder::Producers::Producer] an instantiated producer, not serializeable !
#
# @overload to_producer(a_class, *args)
# @param a_class [Class] the class to create an instance of
# @param args [Object] the arguments to the given class' new
#
# @overload to_producer(a_producer_descriptor)
# @param a_producer_descriptor [Puppet::Pops::Binder::Bindings::ProducerDescriptor] a descriptor
# producing Puppet::Pops::Binder::Producers::Producer
#
# @api public
#
def to_producer(producer, *args)
case producer
when Class
producer = Puppet::Pops::Binder::BindingsFactory.instance_producer(producer.name, *args)
when Puppet::Pops::Binder::Bindings::ProducerDescriptor
when Puppet::Pops::Binder::Producers::Producer
# a custom producer instance
producer = Puppet::Pops::Binder::BindingsFactory.literal_producer(producer)
else
raise ArgumentError, "Given producer argument is none of a producer descriptor, a class, or a producer"
end
metaproducer = Puppet::Pops::Binder::BindingsFactory.producer_producer(producer)
model.producer = metaproducer
self
end
# Sets the binding's producer to a series of producers.
# Use this when you want to produce a different producer on each request for a producer
#
# @overload to_producer(a_producer)
# Sets the producer to an instantiated producer. The resulting model can not be serialized as a consequence as there
# is no meta-model describing the specialized producer. Use this only in exceptional cases, or where there is never the
# need to serialize the model.
# @param a_producer [Puppet::Pops::Binder::Producers::Producer] an instantiated producer, not serializeable !
#
# @overload to_producer(a_class, *args)
# @param a_class [Class] the class to create an instance of
# @param args [Object] the arguments to the given class' new
#
# @overload to_producer(a_producer_descriptor)
# @param a_producer_descriptor [Puppet::Pops::Binder::Bindings::ProducerDescriptor] a descriptor
# producing Puppet::Pops::Binder::Producers::Producer
#
# @api public
#
def to_producer_series(producer, *args)
case producer
when Class
producer = Puppet::Pops::Binder::BindingsFactory.instance_producer(producer.name, *args)
when Puppet::Pops::Binder::Bindings::ProducerDescriptor
when Puppet::Pops::Binder::Producers::Producer
# a custom producer instance
producer = Puppet::Pops::Binder::BindingsFactory.literal_producer(producer)
else
raise ArgumentError, "Given producer argument is none of a producer descriptor, a class, or a producer"
end
non_caching = Puppet::Pops::Binder::Bindings::NonCachingProducerDescriptor.new()
non_caching.producer = producer
metaproducer = Puppet::Pops::Binder::BindingsFactory.producer_producer(non_caching)
non_caching = Puppet::Pops::Binder::Bindings::NonCachingProducerDescriptor.new()
non_caching.producer = metaproducer
model.producer = non_caching
self
end
# Sets the binding's producer to a "non singleton" producer (each call to produce produces a new instance/copy).
# @overload to_series_of(a_literal)
# a constant producer
# @overload to_series_of(a_class, *args)
# Instantiating producer
# @overload to_series_of(a_producer_descriptor)
# a given producer
#
# @api public
#
def to_series_of(producer, *args)
case producer
when Class
producer = Puppet::Pops::Binder::BindingsFactory.instance_producer(producer.name, *args)
when Puppet::Pops::Binder::Bindings::ProducerDescriptor
else
# If given producer is not a producer, create a literal producer
producer = Puppet::Pops::Binder::BindingsFactory.literal_producer(producer)
end
non_caching = Puppet::Pops::Binder::Bindings::NonCachingProducerDescriptor.new()
non_caching.producer = producer
model.producer = non_caching
self
end
# Sets the binding's producer to one that performs a lookup of another key
# @overload to_lookup_of(type, name)
# @overload to_lookup_of(name)
# @api public
#
def to_lookup_of(type, name=nil)
unless name
name = type
type = Puppet::Pops::Types::TypeFactory.data()
end
model.producer = Puppet::Pops::Binder::BindingsFactory.lookup_producer(type, name)
self
end
# Sets the binding's producer to a one that performs a lookup of another key and they applies hash lookup on
# the result.
#
# @overload to_lookup_of(type, name)
# @overload to_lookup_of(name)
# @api public
#
def to_hash_lookup_of(type, name, key)
model.producer = Puppet::Pops::Binder::BindingsFactory.hash_lookup_producer(type, name, key)
self
end
# Sets the binding's producer to one that produces the first found lookup of another key
# @param list_of_lookups [Array] array of arrays [type name], or just name (implies data)
# @example
# binder.bind().name('foo').to_first_found('fee', 'fum', 'extended-bar')
# binder.bind().name('foo').to_first_found(
# [T.ruby(ThisClass), 'fee'],
# [T.ruby(ThatClass), 'fum'],
# 'extended-bar')
# @api public
#
def to_first_found(*list_of_lookups)
producers = list_of_lookups.collect do |entry|
if entry.is_a?(Array)
case entry.size
when 2
Puppet::Pops::Binder::BindingsFactory.lookup_producer(entry[0], entry[1])
when 1
Puppet::Pops::Binder::BindingsFactory.lookup_producer(Puppet::Pops::Types::TypeFactory.data(), entry[0])
else
raise ArgumentError, "Not an array of [type, name], name, or [name]"
end
else
Puppet::Pops::Binder::BindingsFactory.lookup_producer(T.data(), entry)
end
end
model.producer = Puppet::Pops::Binder::BindingsFactory.first_found_producer(*producers)
self
end
# Sets options to the producer.
# See the respective producer for the options it supports. All producers supports the option `:transformer`, a
# puppet or ruby lambda that is evaluated with the produced result as an argument. The ruby lambda gets scope and
# value as arguments.
# @note
# A Ruby lambda is not cross platform safe. Use a puppet lambda if you want a bindings model that is.
#
# @api public
def producer_options(options)
options.each do |k, v|
arg = Puppet::Pops::Binder::Bindings::NamedArgument.new()
arg.name = k.to_s
arg.value = v
model.addProducer_args(arg)
end
self
end
end
# A builder specialized for multibind - checks that type is Array or Hash based. A new builder sets the
# multibinding to be of type Hash[Data].
#
# @api public
class MultibindingsBuilder < BindingsBuilder
# Constraints type to be one of {Puppet::Pops::Types::PArrayType PArrayType}, or {Puppet::Pops::Types::PHashType PHashType}.
# @raise [ArgumentError] if type constraint is not met.
# @api public
def type(type)
unless type.class == Puppet::Pops::Types::PArrayType || type.class == Puppet::Pops::Types::PHashType
raise ArgumentError, "Wrong type; only PArrayType, or PHashType allowed, got '#{type.to_s}'"
end
model.type = type
self
end
# Overrides the default implementation that will raise an exception as a multibind requires a hash type.
# Thus, if nothing else is requested, a multibind will be configured as Hash[Data].
#
def data()
hash_of_data()
end
end
# Produces a ContributedBindings.
# A ContributedBindings is used by bindings providers to return a set of named bindings.
#
# @param name [String] the name of the contributed bindings (for human use in messages/logs only)
# @param named_bindings [Puppet::Pops::Binder::Bindings::NamedBindings, Array<Puppet::Pops::Binder::Bindings::NamedBindings>] the
# named bindings to include
- # @param effective_categories [Puppet::Pops::Binder::Bindings::EffectiveCategories] the contributors opinion about categorization
- # this is used to ensure consistent use of categories.
#
- def self.contributed_bindings(name, named_bindings, effective_categories)
+ def self.contributed_bindings(name, named_bindings)
cb = Puppet::Pops::Binder::Bindings::ContributedBindings.new()
cb.name = name
named_bindings = [named_bindings] unless named_bindings.is_a?(Array)
named_bindings.each {|b| cb.addBindings(b) }
- cb.effective_categories = effective_categories
cb
end
# Creates a named binding container, the top bindings model object.
# A NamedBindings is typically produced by a bindings provider.
#
# The created container is wrapped in a BindingsContainerBuilder for further detailing.
# Unwrap the built result when done.
# @api public
#
def self.named_bindings(name, &block)
binding = Puppet::Pops::Binder::Bindings::NamedBindings.new()
binding.name = name
builder = BindingsContainerBuilder.new(binding)
builder.instance_eval(&block) if block_given?
builder
end
# This variant of {named_bindings} evaluates the given block as a method on an anonymous class,
# thus, if the block defines methods or do something with the class itself, this does not pollute
# the base class (BindingsContainerBuilder).
# @api private
#
def self.safe_named_bindings(name, scope, &block)
binding = Puppet::Pops::Binder::Bindings::NamedBindings.new()
binding.name = name
anon = Class.new(BindingsContainerBuilder) do
def initialize(b)
super b
end
end
anon.send(:define_method, :_produce, block)
builder = anon.new(binding)
case block.arity
when 0
builder._produce()
when 1
builder._produce(scope)
end
builder
end
# Creates a literal/constant producer
# @param value [Object] the value to produce
# @return [Puppet::Pops::Binder::Bindings::ProducerDescriptor] a producer description
# @api public
#
def self.literal_producer(value)
producer = Puppet::Pops::Binder::Bindings::ConstantProducerDescriptor.new()
producer.value = value
producer
end
# Creates a non caching producer
# @param producer [Puppet::Pops::Binder::Bindings::Producer] the producer to make non caching
# @return [Puppet::Pops::Binder::Bindings::ProducerDescriptor] a producer description
# @api public
#
def self.non_caching_producer(producer)
p = Puppet::Pops::Binder::Bindings::NonCachingProducerDescriptor.new()
p.producer = producer
p
end
# Creates a producer producer
# @param producer [Puppet::Pops::Binder::Bindings::Producer] a producer producing a Producer.
# @return [Puppet::Pops::Binder::Bindings::ProducerDescriptor] a producer description
# @api public
#
def self.producer_producer(producer)
p = Puppet::Pops::Binder::Bindings::ProducerProducerDescriptor.new()
p.producer = producer
p
end
# Creates an instance producer
# An instance producer creates a new instance of a class.
# If the class implements the class method `inject` this method is called instead of `new` to allow further lookups
# to take place. This is referred to as *assisted inject*. If the class method `inject` is missing, the regular `new` method
# is called.
#
# @param class_name [String] the name of the class
# @param args[Object] arguments to the class' `new` method.
# @return [Puppet::Pops::Binder::Bindings::ProducerDescriptor] a producer description
# @api public
#
def self.instance_producer(class_name, *args)
p = Puppet::Pops::Binder::Bindings::InstanceProducerDescriptor.new()
p.class_name = class_name
args.each {|a| p.addArguments(a) }
p
end
# Creates a Producer that looks up a value.
# @param type [Puppet::Pops::Types::PObjectType] the type to lookup
# @param name [String] the name to lookup
# @return [Puppet::Pops::Binder::Bindings::ProducerDescriptor] a producer description
# @api public
def self.lookup_producer(type, name)
p = Puppet::Pops::Binder::Bindings::LookupProducerDescriptor.new()
p.type = type
p.name = name
p
end
# Creates a Hash lookup producer that looks up a hash value, and then a key in the hash.
#
# @return [Puppet::Pops::Binder::Bindings::ProducerDescriptor] a producer description
# @param type [Puppet::Pops::Types::PObjectType] the type to lookup (i.e. a Hash of some key/value type).
# @param name [String] the name to lookup
# @param key [Object] the key to lookup in the looked up hash (type should comply with given key type).
# @api public
#
def self.hash_lookup_producer(type, name, key)
p = Puppet::Pops::Binder::Bindings::HashLookupProducerDescriptor.new()
p.type = type
p.name = name
p.key = key
p
end
# Creates a first-found producer that looks up from a given series of keys. The first found looked up
# value will be produced.
# @param producers [Array<Puppet::Pops::Binder::Bindings::ProducerDescriptor>] the producers to consult in given order
# @return [Puppet::Pops::Binder::Bindings::ProducerDescriptor] a producer descriptor
# @api public
def self.first_found_producer(*producers)
p = Puppet::Pops::Binder::Bindings::FirstFoundProducerDescriptor.new()
producers.each {|p2| p.addProducers(p2) }
p
end
# Creates an evaluating producer that evaluates a puppet expression.
# A puppet expression is most conveniently created by using the {Puppet::Pops::Parser::EvaluatingParser EvaluatingParser} as it performs
# all set up and validation of the parsed source. Two convenience methods are used to parse an expression, or parse a ruby string
# as a puppet string. See methods {puppet_expression}, {puppet_string} and {parser} for more information.
#
# @example producing a puppet expression
# expr = puppet_string("Interpolated $fqdn", __FILE__)
#
# @param expression [Puppet::Pops::Model::Expression] a puppet DSL expression as producer by the eparser.
# @return [Puppet::Pops::Binder::Bindings::ProducerDescriptor] a producer descriptor
# @api public
#
def self.evaluating_producer(expression)
p = Puppet::Pops::Binder::Bindings::EvaluatingProducerDescriptor.new()
p.expression = expression
p
end
- # Creates an EffectiveCategories from a list of tuples `[categorization category ...]`, or `[[categorization category] ...]`
- # This method is used by backends to create a model of the effective categories.
- # @api public
- #
- def self.categories(tuple_array)
- result = Puppet::Pops::Binder::Bindings::EffectiveCategories.new()
- tuple_array.flatten.each_slice(2) do |c|
- cat = Puppet::Pops::Binder::Bindings::Category.new()
- cat.categorization = c[0]
- cat.value = c[1]
- result.addCategories(cat)
- end
- result
- end
-
# Creates a NamedLayer. This is used by the bindings system to create a model of the layers.
#
# @api public
#
def self.named_layer(name, *bindings)
result = Puppet::Pops::Binder::Bindings::NamedLayer.new()
result.name = name
bindings.each { |b| result.addBindings(b) }
result
end
# Create a LayeredBindings. This is used by the bindings system to create a model of all given layers.
# @param named_layers [Puppet::Pops::Binder::Bindings::NamedLayer] one or more named layers
# @return [Puppet::Pops::Binder::Bindings::LayeredBindings] the constructed layered bindings.
# @api public
#
def self.layered_bindings(*named_layers)
result = Puppet::Pops::Binder::Bindings::LayeredBindings.new()
named_layers.each {|b| result.addLayers(b) }
result
end
# @return [Puppet::Pops::Parser::EvaluatingParser] a parser for puppet expressions
def self.parser
@parser ||= Puppet::Pops::Parser::EvaluatingParser.new()
end
# Parses and produces a puppet expression from the given string.
# @param string [String] puppet source e.g. "1 + 2"
# @param source_file [String] the source location, typically `__File__`
# @return [Puppet::Pops::Model::Expression] an expression (that can be bound)
# @api public
#
def self.puppet_expression(string, source_file)
parser.parse_string(string, source_file).current
end
# Parses and produces a puppet string expression from the given string.
# The string will automatically be quoted and special characters escaped.
# As an example if given the (ruby) string "Hi\nMary" it is transformed to
# the puppet string (illustrated with a ruby string) "\"Hi\\nMary\”" before being
# parsed.
#
# @param string [String] puppet source e.g. "On node $!{fqdn}"
# @param source_file [String] the source location, typically `__File__`
# @return [Puppet::Pops::Model::Expression] an expression (that can be bound)
# @api public
#
def self.puppet_string(string, source_file)
parser.parse_string(parser.quote(string), source_file).current
end
end
diff --git a/lib/puppet/pops/binder/bindings_label_provider.rb b/lib/puppet/pops/binder/bindings_label_provider.rb
index f7555468a..ac10f7677 100644
--- a/lib/puppet/pops/binder/bindings_label_provider.rb
+++ b/lib/puppet/pops/binder/bindings_label_provider.rb
@@ -1,46 +1,43 @@
# A provider of labels for bindings model object, producing a human name for the model object.
# @api private
#
class Puppet::Pops::Binder::BindingsLabelProvider < Puppet::Pops::LabelProvider
def initialize
@@label_visitor ||= Puppet::Pops::Visitor.new(self,"label",0,0)
end
# Produces a label for the given object without article.
# @return [String] a human readable label
#
def label o
@@label_visitor.visit(o)
end
def label_PObjectType o ; "#{Puppet::Pops::Types::TypeFactory.label(o)}" end
def label_ProducerDescriptor o ; "Producer" end
def label_NonCachingProducerDescriptor o ; "Non Caching Producer" end
def label_ConstantProducerDescriptor o ; "Producer['#{o.value}']" end
def label_EvaluatingProducerDescriptor o ; "Evaluating Producer" end
def label_InstanceProducerDescriptor o ; "Producer[#{o.class_name}]" end
def label_LookupProducerDescriptor o ; "Lookup Producer[#{o.name}]" end
def label_HashLookupProducerDescriptor o ; "Hash Lookup Producer[#{o.name}][#{o.key}]" end
def label_FirstFoundProducerDescriptor o ; "First Found Producer" end
def label_ProducerProducerDescriptor o ; "Producer[Producer]" end
def label_MultibindProducerDescriptor o ; "Multibind Producer" end
def label_ArrayMultibindProducerDescriptor o ; "Array Multibind Producer" end
def label_HashMultibindProducerDescriptor o ; "Hash Multibind Producer" end
def label_Bindings o ; "Bindings" end
def label_NamedBindings o ; "Named Bindings" end
- def label_Category o ; "Category '#{o.categorization}/#{o.value}'" end
- def label_CategorizedBindings o ; "Categorized Bindings" end
def label_LayeredBindings o ; "Layered Bindings" end
def label_NamedLayer o ; "Layer '#{o.name}'" end
- def label_EffectiveCategories o ; "Effective Categories" end
def label_ContributedBindings o ; "Contributed Bindings" end
def label_NamedArgument o ; "Named Argument" end
def label_Binding(o)
'Binding' + (o.multibind_id.nil? ? '' : ' In Multibind')
end
def label_Multibinding(o)
'Multibinding' + (o.multibind_id.nil? ? '' : ' In Multibind')
end
end
diff --git a/lib/puppet/pops/binder/bindings_loader.rb b/lib/puppet/pops/binder/bindings_loader.rb
index eca05c1e9..84a4051b8 100644
--- a/lib/puppet/pops/binder/bindings_loader.rb
+++ b/lib/puppet/pops/binder/bindings_loader.rb
@@ -1,79 +1,88 @@
require 'rgen/metamodel_builder'
# The ClassLoader provides a Class instance given a class name or a meta-type.
# If the class is not already loaded, it is loaded using the Puppet Autoloader.
# This means it can load a class from a gem, or from puppet modules.
#
class Puppet::Pops::Binder::BindingsLoader
- @autoloader = Puppet::Util::Autoload.new("BindingsLoader", "puppet/bindings", :wrap => false)
+ @confdir = Puppet.settings[:confdir]
+# @autoloader = Puppet::Util::Autoload.new("BindingsLoader", "puppet/bindings", :wrap => false)
# Returns a XXXXX given a fully qualified class name.
# Lookup of class is never relative to the calling namespace.
# @param name [String, Array<String>, Array<Symbol>, Puppet::Pops::Types::PObjectType] A fully qualified
# class name String (e.g. '::Foo::Bar', 'Foo::Bar'), a PObjectType, or a fully qualified name in Array form where each part
# is either a String or a Symbol, e.g. `%w{Puppetx Puppetlabs SomeExtension}`.
# @return [Class, nil] the looked up class or nil if no such class is loaded
# @raise ArgumentError If the given argument has the wrong type
# @api public
#
def self.provide(scope, name)
case name
when String
provide_from_string(scope, name)
when Array
provide_from_name_path(scope, name.join('::'), name)
else
raise ArgumentError, "Cannot provide a bindings from a '#{name.class.name}'"
end
end
# If loadable name exists relative to a a basedir or not. Returns the loadable path as a side effect.
# @return [String, nil] a loadable path for the given name, or nil
#
def self.loadable?(basedir, name)
# note, "lib" is added by the autoloader
#
- paths_for_name(name).find {|p| Puppet::FileSystem::File.exist?(File.join(basedir, "lib/puppet/bindings", p)+'.rb') }
+ paths_for_name(name).find {|p| Puppet::FileSystem.exist?(File.join(basedir, "lib/puppet/bindings", p)+'.rb') }
end
private
+ def self.loader()
+ unless Puppet.settings[:confdir] == @confdir
+ @confdir = Puppet.settings[:confdir] == @confdir
+ @autoloader = Puppet::Util::Autoload.new("BindingsLoader", "puppet/bindings", :wrap => false)
+ end
+ @autoloader
+ end
+
def self.provide_from_string(scope, name)
name_path = name.split('::')
# always from the root, so remove an empty first segment
if name_path[0].empty?
name_path = name_path[1..-1]
end
provide_from_name_path(scope, name, name_path)
end
def self.provide_from_name_path(scope, name, name_path)
# If bindings is already loaded, try this first
result = Puppet::Bindings.resolve(scope, name)
unless result
# Attempt to load it using the auto loader
- paths_for_name(name).find {|path| @autoloader.load(path) }
+ paths_for_name(name).find {|path| loader.load(path) }
result = Puppet::Bindings.resolve(scope, name)
end
result
end
def self.paths_for_name(fq_name)
[de_camel(fq_name), downcased_path(fq_name)]
end
def self.downcased_path(fq_name)
fq_name.to_s.gsub(/::/, '/').downcase
end
def self.de_camel(fq_name)
fq_name.to_s.gsub(/::/, '/').
gsub(/([A-Z]+)([A-Z][a-z])/,'\1_\2').
gsub(/([a-z\d])([A-Z])/,'\1_\2').
tr("-", "_").
downcase
end
-end
\ No newline at end of file
+end
diff --git a/lib/puppet/pops/binder/bindings_model.rb b/lib/puppet/pops/binder/bindings_model.rb
index aa01e1366..4d041fca1 100644
--- a/lib/puppet/pops/binder/bindings_model.rb
+++ b/lib/puppet/pops/binder/bindings_model.rb
@@ -1,215 +1,201 @@
require 'rgen/metamodel_builder'
# The Bindings model is a model of Key to Producer mappings (bindings).
# The central concept is that a Bindings is a nested structure of bindings.
# A top level Bindings should be a NamedBindings (the name is used primarily
# in error messages). A Key is a Type/Name combination.
#
# TODO: In this version, references to "any object" uses the class Object,
# but this is only temporary. The intent is to use specific Puppet Objects
# that are typed using the Puppet Type System (to enable serialization).
#
# @see Puppet::Pops::Binder::BindingsFactory The BindingsFactory for more details on how to create model instances.
# @api public
module Puppet::Pops::Binder::Bindings
# @abstract
# @api public
#
class AbstractBinding < Puppet::Pops::Model::PopsObject
abstract
end
# An abstract producer
# @abstract
# @api public
#
class ProducerDescriptor < Puppet::Pops::Model::PopsObject
abstract
contains_one_uni 'transformer', Puppet::Pops::Model::LambdaExpression
end
# All producers are singleton producers unless wrapped in a non caching producer
# where each lookup produces a new instance. It is an error to have a nesting level > 1
# and to nest a NonCachingProducerDescriptor.
#
# @api public
#
class NonCachingProducerDescriptor < ProducerDescriptor
contains_one_uni 'producer', ProducerDescriptor
end
# Produces a constant value (i.e. something of {Puppet::Pops::Types::PDataType PDataType})
# @api public
#
class ConstantProducerDescriptor < ProducerDescriptor
# TODO: This should be a typed Puppet Object
has_attr 'value', Object
end
- # Produces a value by evaluating a Puppet DSL expression
+ # Produces a value by evaluating a Puppet DSL expression.
+ # Note that the expression is not contained as it is part of a Puppet::Pops::Model::Program.
+ # To include the expression in the serialization, the Program it is contained in must be
+ # contained in the same serialization. This can be achieved by containing it in the
+ # ContributedBindings that is the top of a BindingsModel produced and given to the Injector.
+ #
# @api public
#
class EvaluatingProducerDescriptor < ProducerDescriptor
- contains_one_uni 'expression', Puppet::Pops::Model::Expression
+ has_one 'expression', Puppet::Pops::Model::Expression
end
# An InstanceProducer creates an instance of the given class
# Arguments are passed to the class' `new` operator in the order they are given.
# @api public
#
class InstanceProducerDescriptor < ProducerDescriptor
# TODO: This should be a typed Puppet Object ??
has_many_attr 'arguments', Object, :upperBound => -1
has_attr 'class_name', String
end
# A ProducerProducerDescriptor, describes that the produced instance is itself a Producer
# that should be used to produce the value.
# @api public
#
class ProducerProducerDescriptor < ProducerDescriptor
contains_one_uni 'producer', ProducerDescriptor, :lowerBound => 1
end
# Produces a value by looking up another key (type/name)
# @api public
#
class LookupProducerDescriptor < ProducerDescriptor
contains_one_uni 'type', Puppet::Pops::Types::PObjectType
has_attr 'name', String
end
# Produces a value by looking up another multibound key, and then looking up
# the detail using a detail_key.
# This is used to produce a specific service of a given type (such as a SyntaxChecker for the syntax "json").
# @api public
#
class HashLookupProducerDescriptor < LookupProducerDescriptor
has_attr 'key', String
end
# Produces a value by looking up each producer in turn. The first existing producer wins.
# @api public
#
class FirstFoundProducerDescriptor < ProducerDescriptor
contains_many_uni 'producers', LookupProducerDescriptor
end
# @api public
# @abstract
class MultibindProducerDescriptor < ProducerDescriptor
abstract
end
# Used in a Multibind of Array type unless it has a producer. May explicitly be used as well.
# @api public
#
class ArrayMultibindProducerDescriptor < MultibindProducerDescriptor
end
# Used in a Multibind of Hash type unless it has a producer. May explicitly be used as well.
# @api public
#
class HashMultibindProducerDescriptor < MultibindProducerDescriptor
end
# Plays the role of "Hash[String, Object] entry" but with keys in defined order.
#
# @api public
#
class NamedArgument < Puppet::Pops::Model::PopsObject
has_attr 'name', String, :lowerBound => 1
has_attr 'value', Object, :lowerBound => 1
end
# Binds a type/name combination to a producer. Optionally marking the bindidng as being abstract, or being an
# override of another binding. Optionally, the binding defines producer arguments passed to the producer when
# it is created.
#
# @api public
class Binding < AbstractBinding
contains_one_uni 'type', Puppet::Pops::Types::PObjectType
has_attr 'name', String
has_attr 'override', Boolean
has_attr 'abstract', Boolean
+ has_attr 'final', Boolean
# If set is a contribution in a multibind
has_attr 'multibind_id', String, :lowerBound => 0
# Invariant: Only multibinds may have lowerBound 0, all regular Binding must have a producer.
contains_one_uni 'producer', ProducerDescriptor, :lowerBound => 0
contains_many_uni 'producer_args', NamedArgument, :lowerBound => 0
end
# A multibinding is a binding other bindings can contribute to.
#
# @api public
class Multibinding < Binding
has_attr 'id', String
end
# A container of Binding instances
# @api public
#
class Bindings < AbstractBinding
contains_many_uni 'bindings', AbstractBinding
end
# The top level container of bindings can have a name (for error messages, logging, tracing).
# May be nested.
# @api public
#
class NamedBindings < Bindings
has_attr 'name', String
end
- # A category predicate (the request has to be in this category).
- # @api public
- #
- class Category < Puppet::Pops::Model::PopsObject
- has_attr 'categorization', String, :lowerBound => 1
- has_attr 'value', String, :lowerBound => 1
- end
-
- # A container of Binding instances that are in effect when the
- # predicates (min one) evaluates to true. Multiple predicates are handles as an 'and'.
- # Note that 'or' semantics are handled by repeating the same rules.
- # @api public
- #
- class CategorizedBindings < Bindings
- contains_many_uni 'predicates', Category, :lowerBound => 1
- end
-
# A named layer of bindings having the same priority.
# @api public
class NamedLayer < Puppet::Pops::Model::PopsObject
has_attr 'name', String, :lowerBound => 1
contains_many_uni 'bindings', NamedBindings
end
# A list of layers with bindings in descending priority order.
# @api public
#
class LayeredBindings < Puppet::Pops::Model::PopsObject
contains_many_uni 'layers', NamedLayer
end
- # A list of categories consisting of categroization name and category value (i.e. the *state of the request*)
- # @api public
- #
- class EffectiveCategories < Puppet::Pops::Model::PopsObject
- # The order is from highest precedence to lowest
- contains_many_uni 'categories', Category
- end
-
# ContributedBindings is a named container of one or more NamedBindings.
- # The intent is that a bindings producer returns a ContributedBindings which in addition to the bindings
- # may optionally contain provider's opinion about the precedence of categories, and their category values.
- # This enables merging of bindings, and validation of consistency.
+ # The intent is that a bindings producer returns a ContributedBindings that identifies the contributor
+ # as opposed to the names of the different set of bindings. The ContributorBindings name is typically
+ # a technical name that indicates their source (a service).
+ #
+ # When EvaluatingProducerDescriptor is used, it holds a reference to an Expression. That expression
+ # should be contained in the programs referenced in the ContributedBindings that contains that producer.
+ # While the bindings model will still work if this is not the case, it will not serialize and deserialize
+ # correctly.
#
# @api public
#
class ContributedBindings < NamedLayer
- contains_one_uni 'effective_categories', EffectiveCategories
+ contains_many_uni 'programs', Puppet::Pops::Model::Program
end
end
diff --git a/lib/puppet/pops/binder/bindings_model_dumper.rb b/lib/puppet/pops/binder/bindings_model_dumper.rb
index de62e525f..1abfd0675 100644
--- a/lib/puppet/pops/binder/bindings_model_dumper.rb
+++ b/lib/puppet/pops/binder/bindings_model_dumper.rb
@@ -1,205 +1,187 @@
# Dumps a Pops::Binder::Bindings model in reverse polish notation; i.e. LISP style
# The intention is to use this for debugging output
# TODO: BAD NAME - A DUMP is a Ruby Serialization
# NOTE: use :break, :indent, :dedent in lists to do just that
#
class Puppet::Pops::Binder::BindingsModelDumper < Puppet::Pops::Model::TreeDumper
Bindings = Puppet::Pops::Binder::Bindings
attr_reader :type_calculator
attr_reader :expression_dumper
def initialize
super
@type_calculator = Puppet::Pops::Types::TypeCalculator.new()
@expression_dumper = Puppet::Pops::Model::ModelTreeDumper.new()
end
def dump_BindingsFactory o
do_dump(o.model)
end
def dump_BindingsBuilder o
do_dump(o.model)
end
def dump_BindingsContainerBuilder o
do_dump(o.model)
end
def dump_NamedLayer o
result = ['named-layer', (o.name.nil? ? '<no-name>': o.name), :indent]
if o.bindings
o.bindings.each do |b|
result << :break
result << do_dump(b)
end
end
result << :dedent
result
end
def dump_Array o
o.collect {|e| do_dump(e) }
end
def dump_ASTArray o
["[]"] + o.children.collect {|x| do_dump(x)}
end
def dump_ASTHash o
["{}"] + o.value.sort_by{|k,v| k.to_s}.collect {|x| [do_dump(x[0]), do_dump(x[1])]}
end
def dump_Integer o
o.to_s
end
# Dump a Ruby String in single quotes unless it is a number.
def dump_String o
"'#{o}'"
end
def dump_NilClass o
"()"
end
def dump_Object o
['dev-error-no-polymorph-dump-for:', o.class.to_s, o.to_s]
end
def is_nop? o
o.nil? || o.is_a?(Model::Nop) || o.is_a?(AST::Nop)
end
def dump_ProducerDescriptor o
result = [o.class.name]
result << expression_dumper.dump(o.transformer) if o.transformer
result
end
def dump_NonCachingProducerDescriptor o
dump_ProducerDescriptor(o) + do_dump(o.producer)
end
def dump_ConstantProducerDescriptor o
['constant', do_dump(o.value)]
end
def dump_EvaluatingProducerDescriptor o
result = dump_ProducerDescriptor(o)
result << expression_dumper.dump(o.expression)
end
- def dump_InstanceProducerDescriptor
+ def dump_InstanceProducerDescriptor o
# TODO: o.arguments, o. transformer
['instance', o.class_name]
end
def dump_ProducerProducerDescriptor o
# skip the transformer lambda...
result = ['producer-producer', do_dump(o.producer)]
result << expression_dumper.dump(o.transformer) if o.transformer
result
end
def dump_LookupProducerDescriptor o
['lookup', do_dump(o.type), o.name]
end
def dump_PObjectType o
type_calculator.string(o)
end
def dump_HashLookupProducerDescriptor o
# TODO: transformer lambda
result = ['hash-lookup', do_dump(o.type), o.name, "[#{do_dump(o.key)}]"]
result << expression_dumper.dump(o.transformer) if o.transformer
result
end
def dump_FirstFoundProducerDescriptor o
# TODO: transformer lambda
['first-found', do_dump(o.producers)]
end
def dump_ArrayMultibindProducerDescriptor o
['multibind-array']
end
def dump_HashMultibindProducerDescriptor o
['multibind-hash']
end
def dump_NamedArgument o
"#{o.name} => #{do_dump(o.value)}"
end
def dump_Binding o
result = ['bind']
result << 'override' if o.override
result << 'abstract' if o.abstract
result.concat([do_dump(o.type), o.name])
result << "(in #{o.multibind_id})" if o.multibind_id
result << ['to', do_dump(o.producer)] + do_dump(o.producer_args)
result
end
def dump_Multibinding o
result = ['multibind', o.id]
result << 'override' if o.override
result << 'abstract' if o.abstract
result.concat([do_dump(o.type), o.name])
result << "(in #{o.multibind_id})" if o.multibind_id
result << ['to', do_dump(o.producer)] + do_dump(o.producer_args)
result
end
def dump_Bindings o
do_dump(o.bindings)
end
def dump_NamedBindings o
result = ['named-bindings', o.name, :indent]
o.bindings.each do |b|
result << :break
result << do_dump(b)
end
result << :dedent
result
end
- def dump_Category o
- ['category', o.categorization, do_dump(o.value)]
- end
-
- def dump_CategorizedBindings o
- result = ['when', do_dump(o.predicates), :indent]
- o.bindings.each do |b|
- result << :break
- result << do_dump(b)
- end
- result << :dedent
- result
- end
-
def dump_LayeredBindings o
result = ['layers', :indent]
o.layers.each do |layer|
result << :break
result << do_dump(layer)
end
result << :dedent
result
end
- def dump_EffectiveCategories o
- ['categories', do_dump(o.categories)]
- end
-
def dump_ContributedBindings o
- ['contributed', o.name, do_dump(o.effective_categories), do_dump(o.bindings)]
+ ['contributed', o.name, do_dump(o.bindings)]
end
end
diff --git a/lib/puppet/pops/binder/config/binder_config.rb b/lib/puppet/pops/binder/config/binder_config.rb
index a4fb7796a..3e83e4afb 100644
--- a/lib/puppet/pops/binder/config/binder_config.rb
+++ b/lib/puppet/pops/binder/config/binder_config.rb
@@ -1,139 +1,107 @@
module Puppet::Pops::Binder::Config
# Class holding the Binder Configuration
# The configuration is obtained from the file 'binder_config.yaml'
# that must reside in the root directory of the site
# @api public
#
class BinderConfig
- # The bindings hierarchy is an array of categorizations where the
- # array for each category has exactly three elements - the categorization name,
- # category value, and the path that is later used by the backend to read
- # the bindings for that category
+ # The layering configuration is an array of layers from most to least significant.
+ # Each layer is represented by a Hash containing :name and :include and optionally :exclude
#
# @return [Array<Hash<String, String>, Hash<String, Array<String>>]
# @api public
#
attr_reader :layering_config
- # @return [Array<Array(String, String)>] Array of Category tuples where Strings are not evaluated.
- # @api public
- #
- attr_reader :categorization
-
# @return <Hash<String, String>] ({}) optional mapping of bindings-scheme to handler class name
attr_reader :scheme_extensions
- # @return <Hash<String, String>] ({}) optional mapping of hiera backend name to backend class name
- attr_reader :hiera_backends
# @return [String] the loaded config file
attr_accessor :config_file
DEFAULT_LAYERS = [
- { 'name' => 'site', 'include' => ['confdir-hiera:/', 'confdir:/default?optional'] },
- { 'name' => 'modules', 'include' => ['module-hiera:/*/', 'module:/*::default'] },
- ]
-
- DEFAULT_CATEGORIES = [
- ['node', "${fqdn}"],
- ['osfamily', "${osfamily}"],
- ['environment', "${environment}"],
- ['common', "true"]
+ { 'name' => 'site', 'include' => [ 'confdir:/default?optional'] },
+ { 'name' => 'modules', 'include' => [ 'module:/*::default'] },
]
DEFAULT_SCHEME_EXTENSIONS = {}
- DEFAULT_HIERA_BACKENDS_EXTENSIONS = {}
-
def default_config()
# This is hardcoded now, but may be a user supplied default configuration later
- {'version' => 1, 'layers' => default_layers, 'categories' => default_categories}
+ {'version' => 1, 'layers' => default_layers }
end
def confdir()
Puppet.settings[:confdir]
end
# Creates a new Config. The configuration is loaded from the file 'binder_config.yaml' which
# is expected to be found in confdir.
#
# @param diagnostics [DiagnosticProducer] collector of diagnostics
# @api public
#
def initialize(diagnostics)
@config_file = Puppet.settings[:binder_config]
# if file is stated, it must exist
# otherwise it is optional $confdir/binder_conf.yaml
# and if that fails, the default
case @config_file
when NilClass
# use the config file if it exists
rootdir = confdir
if rootdir.is_a?(String)
expanded_config_file = File.expand_path(File.join(rootdir, '/binder_config.yaml'))
- if Puppet::FileSystem::File.exist?(expanded_config_file)
+ if Puppet::FileSystem.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 Puppet::FileSystem::File.exist?(@config_file)
+ unless Puppet::FileSystem.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) && Puppet::FileSystem::File.exist?(@config_file)
+ unless @config_file.is_a?(String) && Puppet::FileSystem.exist?(@config_file)
@config_file = nil # use defaults
end
validator = BinderConfigChecker.new(diagnostics)
begin
data = @config_file ? YAML.load_file(@config_file) : default_config()
validator.validate(data, @config_file)
rescue Errno::ENOENT
diagnostics.accept(Issues::CONFIG_FILE_NOT_FOUND, @config_file)
rescue Errno::ENOTDIR
diagnostics.accept(Issues::CONFIG_FILE_NOT_FOUND, @config_file)
rescue ::SyntaxError => e
diagnostics.accept(Issues::CONFIG_FILE_SYNTAX_ERROR, @config_file, :detail => e.message)
end
unless diagnostics.errors?
@layering_config = data['layers'] or default_layers
- @categorization = data['categories'] or default_categories
@scheme_extensions = (data['extensions'] and data['extensions']['scheme_handlers'] or default_scheme_extensions)
- @hiera_backends = (data['extensions'] and data['extensions']['hiera_backends'] or default_hiera_backends_extensions)
else
@layering_config = []
- @categorization = {}
@scheme_extensions = {}
- @hiera_backends = {}
end
end
# The default_xxx methods exists to make it easier to do mocking in tests.
# @api private
def default_layers
DEFAULT_LAYERS
end
- # @api private
- def default_categories
- DEFAULT_CATEGORIES
- end
-
# @api private
def default_scheme_extensions
DEFAULT_SCHEME_EXTENSIONS
end
-
- # @api private
- def default_hiera_backends_extensions
- DEFAULT_HIERA_BACKENDS_EXTENSIONS
- end
end
end
diff --git a/lib/puppet/pops/binder/config/binder_config_checker.rb b/lib/puppet/pops/binder/config/binder_config_checker.rb
index 24270ae14..a65623dca 100644
--- a/lib/puppet/pops/binder/config/binder_config_checker.rb
+++ b/lib/puppet/pops/binder/config/binder_config_checker.rb
@@ -1,183 +1,142 @@
module Puppet::Pops::Binder::Config
# Validates the consistency of a Binder::BinderConfig
class BinderConfigChecker
# Create an instance with a diagnostic producer that will receive the result during validation
# @param diangostics [DiagnosticProducer] The producer that will receive the diagnostic
# @api public
#
def initialize(diagnostics)
@diagnostics = diagnostics
t = Puppet::Pops::Types
@type_calculator = t::TypeCalculator.new()
@array_of_string_type = t::TypeFactory.array_of(t::TypeFactory.string())
end
# Validate the consistency of the given data. Diagnostics will be emitted to the DiagnosticProducer
# that was set when this checker was created
#
# @param data [Object] The data read from the config file
# @param config_file [String] The full path of the file. Used in error messages
# @api public
#
def validate(data, config_file)
@unique_layer_names = Set.new()
if data.is_a?(Hash)
check_top_level(data, config_file)
else
accept(Issues::CONFIG_IS_NOT_HASH, config_file)
end
end
private
def accept(issue, semantic, options = {})
@diagnostics.accept(issue, semantic, options)
end
def check_top_level(data, config_file)
if layers = (data['layers'] || data[:layers])
check_layers(layers, config_file)
else
accept(Issues::CONFIG_LAYERS_MISSING, config_file)
end
- if categories = (data['categories'] || data[:categories])
- check_categories(categories, config_file)
- else
- accept(Issues::CONFIG_CATEGORIES_MISSING, config_file)
- end
-
if version = (data['version'] or data[:version])
accept(Issues::CONFIG_WRONG_VERSION, config_file, {:expected => 1, :actual => version}) unless version == 1
else
accept(Issues::CONFIG_VERSION_MISSING, config_file)
end
if extensions = data['extensions']
check_extensions(extensions, config_file)
end
end
def check_layers(layers, config_file)
unless layers.is_a?(Array)
accept(Issues::LAYERS_IS_NOT_ARRAY, config_file, :klass => data.class)
else
layers.each {|layer| check_layer(layer, config_file) }
end
end
def check_layer(layer, config_file)
unless layer.is_a?(Hash)
accept(Issues::LAYER_IS_NOT_HASH, config_file, :klass => layer.class)
return
end
layer.each_pair do |k, v|
case k
when 'name'
unless v.is_a?(String)
accept(Issues::LAYER_NAME_NOT_STRING, config_file, :class_name => v.class.name)
end
unless @unique_layer_names.add?(v)
accept(Issues::DUPLICATE_LAYER_NAME, config_file, :name => v.to_s )
end
when 'include'
check_bindings_references('include', v, config_file)
when 'exclude'
check_bindings_references('exclude', v, config_file)
when Symbol
accept(Issues::LAYER_ATTRIBUTE_IS_SYMBOL, config_file, :name => k.to_s)
else
accept(Issues::UNKNOWN_LAYER_ATTRIBUTE, config_file, :name => k.to_s )
end
end
end
- def check_categories(categories, config_file)
- unless categories.is_a?(Array)
- accept(Issues::CATEGORIES_IS_NOT_ARRAY, config_file, :klass => categories.class)
- else
- categories.each { |entry| check_category(entry, config_file) }
- end
- end
-
- def check_category(category, config_file)
- type = @type_calculator.infer(category)
- unless @type_calculator.assignable?(@array_of_string_type, type)
- accept(Issues::CATEGORY_IS_NOT_ARRAY, config_file, :type => @type_calculator.string(type))
- return
- end
- unless category.size == 2
- accept(Issues::CATEGORY_NOT_TWO_STRINGS, config_file, :count => category.size)
- return
- end
- unless category[0] =~ /[a-z][a-zA-Z0-9_]*/
- accept(Issues::INVALID_CATEGORY_NAME, config_file, :name => category[0])
- end
- end
-
# references to bindings is a single String URI, or an array of String URI
# @param kind [String] 'include' or 'exclude' (used in issue messages)
# @param value [String, Array<String>] one or more String URI binding references
# @param config_file [String] reference to the loaded config file
#
def check_bindings_references(kind, value, config_file)
return check_reference(value, kind, config_file) if value.is_a?(String)
accept(Issues::BINDINGS_REF_NOT_STRING_OR_ARRAY, config_file, :kind => kind ) unless value.is_a?(Array)
value.each {|ref| check_reference(ref, kind, config_file) }
end
- # A reference is a URI in string form having one of the schemes:
- # - module-hiera
- # - confdir-hiera
- # - enc
- #
- # and with a path (at least '/')
+ # A reference is a URI in string form having a scheme and a path (at least '/')
#
def check_reference(value, kind, config_file)
begin
uri = URI.parse(value)
unless uri.scheme
accept(Issues::MISSING_SCHEME, config_file, :uri => uri)
end
unless uri.path
accept(Issues::REF_WITHOUT_PATH, config_file, :uri => uri, :kind => kind)
end
rescue InvalidURIError => e
accept(Issues::BINDINGS_REF_INVALID_URI, config_file, :msg => e.message)
end
end
def check_extensions(extensions, config_file)
unless extensions.is_a?(Hash)
accept(Issues::EXTENSIONS_NOT_HASH, config_file, :actual => extensions.class.name)
return
end
# check known extensions
extensions.each_key do |key|
- unless ['scheme_handlers', 'hiera_backends'].include? key
+ unless ['scheme_handlers'].include? key
accept(Issues::UNKNOWN_EXTENSION, config_file, :extension => key)
end
end
if binding_schemes = extensions['scheme_handlers']
unless binding_schemes.is_a?(Hash)
accept(Issues::EXTENSION_BINDING_NOT_HASH, config_file, :extension => 'scheme_handlers', :actual => binding_schemes.class.name)
end
end
-
- if hiera_backends = extensions['hiera_backends']
- unless hiera_backends.is_a?(Hash)
- accept(Issues::EXTENSION_BINDING_NOT_HASH, config_file, :extension => 'hiera_backends', :actual => hiera_backends.class.name)
- end
- end
-
end
end
end
diff --git a/lib/puppet/pops/binder/config/issues.rb b/lib/puppet/pops/binder/config/issues.rb
index d67df45ee..11e37fb32 100644
--- a/lib/puppet/pops/binder/config/issues.rb
+++ b/lib/puppet/pops/binder/config/issues.rb
@@ -1,106 +1,86 @@
module Puppet::Pops::Binder::Config::Issues
# (see Puppet::Pops::Issues#issue)
def self.issue (issue_code, *args, &block)
Puppet::Pops::Issues.issue(issue_code, *args, &block)
end
CONFIG_FILE_NOT_FOUND = issue :CONFIG_FILE_NOT_FOUND do
"The binder configuration file: #{semantic} can not be found."
end
CONFIG_FILE_SYNTAX_ERROR = issue :CONFIG_FILE_SYNTAX_ERROR, :detail do
"Syntax error in configuration file: #{detail}"
end
CONFIG_IS_NOT_HASH = issue :CONFIG_IS_NOT_HASH do
"The configuration file '#{semantic}' has no hash at the top level"
end
CONFIG_LAYERS_MISSING = issue :CONFIG_LAYERS_MISSING do
"The configuration file '#{semantic}' has no 'layers' entry in the top level hash"
end
CONFIG_VERSION_MISSING = issue :CONFIG_VERSION_MISSING do
"The configuration file '#{semantic}' has no 'version' entry in the top level hash"
end
- CONFIG_CATEGORIES_MISSING = issue :CONFIG_CATEGORIES_MISSING do
- "The configuration file '#{semantic}' has no 'categories' entry in the top level hash"
- end
-
LAYERS_IS_NOT_ARRAY = issue :LAYERS_IS_NOT_ARRAY, :klass do
"The configuration file '#{semantic}' should contain a 'layers' key with an Array value, got: #{klass.name}"
end
- CATEGORIES_IS_NOT_ARRAY = issue :CATEGORIES_IS_NOT_ARRAY, :klass do
- "The configuration file '#{semantic}' should contain a 'categories' key with an Array value, got: #{klass.name}"
- end
-
- CATEGORY_IS_NOT_ARRAY = issue :CATEGORY_IS_NOT_ARRAY, :klass do
- "The configuration file '#{semantic}' each entry in 'categories' should be an Array(String, String), got: #{klass.name}"
- end
-
- CATEGORY_NOT_TWO_STRINGS = issue :CATEGORY_NOT_TWO_STRINGS, :count do
- "The configuration file '#{semantic}' each entry in 'categories' should be an array with 2 strings, got: #{count}"
- end
-
- INVALID_CATEGORY_NAME = issue :INVALID_CATEGORY_NAME, :name do
- "The configuration file '#{semantic}' contains the invalid category: '#{name}', must match /[a-z][a-zA-Z0-9_]*/"
- end
-
LAYER_IS_NOT_HASH = issue :LAYER_IS_NOT_HASH, :klass do
"The configuration file '#{semantic}' should contain one hash per layer, got #{klass.name} instead of Hash"
end
DUPLICATE_LAYER_NAME = issue :DUPLICATE_LAYER_NAME, :name do
"Duplicate layer '#{name}' in configuration file #{semantic}"
end
UNKNOWN_LAYER_ATTRIBUTE = issue :UNKNOWN_LAYER_ATTRIBUTE, :name do
"Unknown layer attribute '#{name}' in configuration file #{semantic}"
end
BINDINGS_REF_NOT_STRING_OR_ARRAY = issue :BINDINGS_REF_NOT_STRING_OR_ARRAY, :kind do
"Configuration file #{semantic} has bindings reference in '#{kind}' that is neither a String nor an Array."
end
MISSING_SCHEME = issue :MISSING_SCHEME, :uri do
"Configuration file #{semantic} contains a bindings reference: '#{uri}' without scheme."
end
UNKNOWN_REF_SCHEME = issue :UNKNOWN_REF_SCHEME, :uri, :kind do
"Configuration file #{semantic} contains a bindings reference: '#{kind}' => '#{uri}' with unknown scheme"
end
REF_WITHOUT_PATH = issue :REF_WITHOUT_PATH, :uri, :kind do
"Configuration file #{semantic} contains a bindings reference: '#{kind}' => '#{uri}' without path"
end
BINDINGS_REF_INVALID_URI = issue :BINDINGS_REF_INVALID_URI, :msg do
"Configuration file #{semantic} contains a bindings reference: '#{kind}' => invalid uri, msg: '#{msg}'"
end
LAYER_ATTRIBUTE_IS_SYMBOL = issue :LAYER_ATTRIBUTE_IS_SYMBOL, :name do
"Configuration file #{semantic} contains a layer attribute '#{name}' that is a Symbol (should be String)"
end
LAYER_NAME_NOT_STRING = issue :LAYER_NAME_NOT_STRING, :class_name do
"Configuration file #{semantic} contains a layer name that is not a String, got a: '#{class_name}'"
end
CONFIG_WRONG_VERSION = issue :CONFIG_WRONG_VERSION, :expected, :actual do
"The configuration file '#{semantic}' has unsupported 'version', expected: #{expected}, but got: #{actual}."
end
EXTENSIONS_NOT_HASH = issue :EXTENSIONS_NOT_HASH, :actual do
"The configuration file '#{semantic}' contains 'extensions', expected: Hash, but got: #{actual}."
end
EXTENSION_BINDING_NOT_HASH = issue :EXTENSION_BINDING_NOT_HASH, :extension, :actual do
"The configuration file '#{semantic}' contains '#{extension}', expected: Hash, but got: #{actual}."
end
- UNKNOWN_EXTENSION = issue :UNKNOWN_EXTENSION, :actual do
+ UNKNOWN_EXTENSION = issue :UNKNOWN_EXTENSION, :extension do
"The configuration file '#{semantic}' contains the unknown extension: #{extension}."
end
end
diff --git a/lib/puppet/pops/binder/hiera2.rb b/lib/puppet/pops/binder/hiera2.rb
deleted file mode 100644
index e86299120..000000000
--- a/lib/puppet/pops/binder/hiera2.rb
+++ /dev/null
@@ -1,10 +0,0 @@
-# The Hiera2 Module contains the classes needed to configure a bindings producer
-# to read module specific data. The configuration is expected to be found in
-# a hiera.yaml file in the root of each module
-module Puppet::Pops::Binder::Hiera2
- require 'puppet/pops/binder/hiera2/config_checker'
- require 'puppet/pops/binder/hiera2/config'
- require 'puppet/pops/binder/hiera2/diagnostic_producer'
- require 'puppet/pops/binder/hiera2/bindings_provider'
- require 'puppet/pops/binder/hiera2/issues'
-end
diff --git a/lib/puppet/pops/binder/hiera2/bindings_provider.rb b/lib/puppet/pops/binder/hiera2/bindings_provider.rb
deleted file mode 100644
index d0e5d9a5e..000000000
--- a/lib/puppet/pops/binder/hiera2/bindings_provider.rb
+++ /dev/null
@@ -1,148 +0,0 @@
-module Puppet::Pops::Binder::Hiera2
- Model = Puppet::Pops::Model
-
- # A BindingsProvider instance is used for creating a bindings model from a module directory
- # @api public
- #
- class BindingsProvider
- # The resulting name of loaded bindings (given when initializing)
- attr_reader :name
-
- # Creates a new BindingsProvider by reading the hiera_conf.yaml configuration file. Problems
- # with the configuration are reported propagated to the acceptor
- #
- # @param name [String] the name to assign to the result (and in error messages if there is no result)
- # @param hiera_config_dir [String] Path to the directory containing a hiera_config
- # @param acceptor [Puppet::Pops::Validation::Acceptor] Acceptor that will receive diagnostics
- def initialize(name, hiera_config_dir, acceptor)
- @name = name
- @parser = Puppet::Pops::Parser::EvaluatingParser.new()
- @diagnostics = DiagnosticProducer.new(acceptor)
- @type_calculator = Puppet::Pops::Types::TypeCalculator.new()
- @config = Config.new(hiera_config_dir, @diagnostics)
- end
-
- # Loads a bindings model using the hierarchy and backends configured for this instance.
- #
- # @param scope [Puppet::Parser::Scope] The hash used when expanding
- # @return [Puppet::Pops::Binder::Bindings::ContributedBindings] A bindings model with effective categories
- def load_bindings(scope)
- backends = BackendHelper.new(scope)
- factory = Puppet::Pops::Binder::BindingsFactory
- result = factory.named_bindings(name)
-
- hierarchy = {}
- precedence = []
-
- @config.hierarchy.each do |key, value, path|
- source_file = File.join(@config.module_dir, 'hiera.yaml')
- category_value = @parser.evaluate_string(scope, @parser.quote(value), source_file)
-
- hierarchy[key] = {
- :bindings => result.when_in_category(key, category_value),
- :path => @parser.evaluate_string(scope, @parser.quote(path)),
- :unique_keys =>Set.new()}
-
- precedence << [key, category_value]
- end
-
- @config.backends.each do |backend_key|
- backend = backends[backend_key]
-
- hierarchy.each_pair do |hier_key, hier_val|
- bindings = hier_val[:bindings]
- unique_keys = hier_val[:unique_keys]
-
- hiera_data_file_path = hier_val[:path]
- backend.read_data(@config.module_dir, hiera_data_file_path).each_pair do |key, value|
- if unique_keys.add?(key)
- b = bindings.bind().name(key)
- # Transform value into a Model::Expression
- expr = build_expr(value, hiera_data_file_path)
- if is_constant?(expr)
- # The value is constant so toss the expression
- b.type(@type_calculator.infer(value)).to(value)
- else
- # Use an evaluating producer for the binding
- b.to(expr)
- end
- end
- end
- end
- end
-
- factory.contributed_bindings(name, result.model, factory.categories(precedence))
- end
-
- private
-
- # @return true unless the expression is a Model::ConcatenatedString or
- # somehow contains one
- def is_constant?(expr)
- if expr.is_a?(Model::ConcatenatedString)
- false
- else
- !expr.eAllContents.any? { |v| v.is_a?(Model::ConcatenatedString) }
- end
- end
-
- # Transform the value into a Model::Expression. Strings are parsed using
- # the Pops::Parser::Parser to produce either Model::LiteralString or Model::ConcatenatedString
- #
- # @param value [Object] May be an String, Number, TrueClass, FalseClass, or NilClass nested to any depth using Hash or Array.
- # @param hiera_data_file_path [String] The source_file used when reporting errors
- # @return [Model::Expression] The expression that corresponds to the value
- def build_expr(value, hiera_data_file_path)
- case value
- when Symbol
- value.to_s
- when String
- @parser.parse_string(@parser.quote(value)).current
- when Hash
- value.inject(Model::LiteralHash.new) do |h,(k,v)|
- e = Model::KeyedEntry.new
- e.key = build_expr(k, hiera_data_file_path)
- e.value = build_expr(v, hiera_data_file_path)
- h.addEntries(e)
- h
- end
- when Enumerable
- value.inject(Model::LiteralList.new) {|a,v| a.addValues(build_expr(v, hiera_data_file_path)); a }
- when Numeric
- expr = Model::LiteralNumber.new
- expr.value = value;
- expr
- when TrueClass, FalseClass
- expr = Model::LiteralBoolean.new
- expr.value = value;
- expr
- when NilClass
- Model::Nop.new
- else
- @diagnostics.accept(Issues::UNABLE_TO_PARSE_INSTANCE, value.class.name)
- nil
- end
- end
- end
-
- # @api private
- class BackendHelper
- T = Puppet::Pops::Types::TypeFactory
- HASH_OF_BACKENDS = T.hash_of(T.type_of('Puppetx::Puppet::Hiera2Backend'))
- def initialize(scope)
- @scope = scope
- @cache = nil
- end
-
- def [] (backend_key)
- load_backends unless @cache
- @cache[backend_key]
- end
-
- def load_backends
- @cache = @scope.compiler.boot_injector.lookup(@scope, HASH_OF_BACKENDS, Puppetx::HIERA2_BACKENDS) || {}
- end
- end
-
-end
-
diff --git a/lib/puppet/pops/binder/hiera2/config.rb b/lib/puppet/pops/binder/hiera2/config.rb
deleted file mode 100644
index 104cb5531..000000000
--- a/lib/puppet/pops/binder/hiera2/config.rb
+++ /dev/null
@@ -1,69 +0,0 @@
-module Puppet::Pops::Binder::Hiera2
-
- # Class holding the Hiera2 Configuration
- # The configuration is obtained from the file 'hiera.yaml'
- # that must reside in the root directory of the module
- # @api public
- #
- class Puppet::Pops::Binder::Hiera2::Config
- DEFAULT_HIERARCHY = [ ['osfamily', '${osfamily}', 'data/osfamily/${osfamily}'], ['common', 'true', 'data/common']]
- DEFAULT_BACKENDS = ['yaml', 'json']
-
- if defined?(::Psych::SyntaxError)
- YamlLoadExceptions = [::StandardError, ::ArgumentError, ::Psych::SyntaxError]
- else
- YamlLoadExceptions = [::StandardError, ::ArgumentError]
- end
-
- # Returns a list of configured backends.
- #
- # @return [Array<String>] backend names
- attr_reader :backends
-
- # Root directory of the module holding the configuration
- #
- # @return [String] An absolute path
- attr_reader :module_dir
-
- # The bindings hierarchy is an array of categorizations where the
- # array for each category has exactly three elements - the categorization name,
- # category value, and the path that is later used by the backend to read
- # the bindings for that category
- #
- # @return [Array<Array(String, String, String)>]
- # @api public
- attr_reader :hierarchy
-
- # Creates a new Config. The configuration is loaded from the file 'hiera.yaml' which
- # is expected to be found in the given module_dir.
- #
- # @param module_dir [String] The module directory
- # @param diagnostics [DiagnosticProducer] collector of diagnostics
- # @api public
- #
- def initialize(module_dir, diagnostics)
- @module_dir = module_dir
- config_file = File.join(module_dir, 'hiera.yaml')
- validator = ConfigChecker.new(diagnostics)
- begin
- data = YAML.load_file(config_file)
- validator.validate(data, config_file)
- unless diagnostics.errors?
- # if these are missing the result is nil, and they get default values later
- @hierarchy = data['hierarchy']
- @backends = data['backends']
- end
- rescue Errno::ENOENT
- diagnostics.accept(Issues::CONFIG_FILE_NOT_FOUND, config_file)
- rescue Errno::ENOTDIR
- diagnostics.accept(Issues::CONFIG_FILE_NOT_FOUND, config_file)
- rescue ::SyntaxError => e
- diagnostics.accept(Issues::CONFIG_FILE_SYNTAX_ERROR, e)
- rescue *YamlLoadExceptions => e
- diagnostics.accept(Issues::CONFIG_FILE_SYNTAX_ERROR, e)
- end
- @hierarchy ||= DEFAULT_HIERARCHY
- @backends ||= DEFAULT_BACKENDS
- end
- end
-end
diff --git a/lib/puppet/pops/binder/hiera2/config_checker.rb b/lib/puppet/pops/binder/hiera2/config_checker.rb
deleted file mode 100644
index 53b32ed81..000000000
--- a/lib/puppet/pops/binder/hiera2/config_checker.rb
+++ /dev/null
@@ -1,68 +0,0 @@
-module Puppet::Pops::Binder::Hiera2
-
- # Validates the consistency of a Hiera2::Config
- class ConfigChecker
-
- # Create an instance with a diagnostic producer that will receive the result during validation
- # @param diangostics [DiagnosticProducer] The producer that will receive the diagnostic
- def initialize(diagnostics)
- @diagnostics = diagnostics
- end
-
- # Validate the consistency of the given data. Diagnostics will be emitted to the DiagnosticProducer
- # that was set when this checker was created
- #
- # @param data [Object] The data read from the config file
- # @param config_file [String] The full path of the file. Used in error messages
- def validate(data, config_file)
- if data.is_a?(Hash)
- # If the version is missing, it is not meaningful to continue
- return unless check_version(data['version'], config_file)
- check_hierarchy(data['hierarchy'], config_file)
- check_backends(data['backends'], config_file)
- else
- @diagnostics.accept(Issues::CONFIG_IS_NOT_HASH, config_file)
- end
- end
-
- private
-
- # Version is required and must be >= 2. A warning is issued if version > 2 as this checker is
- # for version 2 only.
- # @return [Boolean] false if it is meaningless to continue checking
- def check_version(version, config_file)
- if version.nil?
- # This is not hiera2 compatible
- @diagnostics.accept(Issues::MISSING_VERSION, config_file)
- return false
- end
- unless version >= 2
- @diagnostics.accept(Issues::WRONG_VERSION, config_file, :expected => 2, :actual => version)
- return false
- end
- unless version == 2
- # it may have a sane subset, hence a different error (configured as warning)
- @diagnostics.accept(Issues::LATER_VERSION, config_file, :expected => 2, :actual => version)
- end
- return true
- end
-
- def check_hierarchy(hierarchy, config_file)
- if !hierarchy.is_a?(Array) || hierarchy.empty?
- @diagnostics.accept(Issues::MISSING_HIERARCHY, config_file)
- else
- hierarchy.each do |value|
- unless value.is_a?(Array) && value.length() == 3
- @diagnostics.accept(Issues::CATEGORY_MUST_BE_THREE_ELEMENT_ARRAY, config_file)
- end
- end
- end
- end
-
- def check_backends(backends, config_file)
- if !backends.is_a?(Array) || backends.empty?
- @diagnostics.accept(Issues::MISSING_BACKENDS, config_file)
- end
- end
- end
-end
diff --git a/lib/puppet/pops/binder/hiera2/diagnostic_producer.rb b/lib/puppet/pops/binder/hiera2/diagnostic_producer.rb
deleted file mode 100644
index 0ae2d4611..000000000
--- a/lib/puppet/pops/binder/hiera2/diagnostic_producer.rb
+++ /dev/null
@@ -1,36 +0,0 @@
-module Puppet::Pops::Binder::Hiera2
- # Generates validation diagnostics
- class Puppet::Pops::Binder::Hiera2::DiagnosticProducer
- attr_reader :acceptor
- def initialize(an_acceptor)
- raise ArgumentError, "Not an acceptor" unless an_acceptor.is_a?(Puppet::Pops::Validation::Acceptor)
- @acceptor = an_acceptor
- @severity_producer = Puppet::Pops::Validation::SeverityProducer.new
- end
-
- def accept(issue, semantic, arguments={})
- arguments[:semantic] ||= semantic
- severity = severity_producer.severity(issue)
- acceptor.accept(Puppet::Pops::Validation::Diagnostic.new(severity, issue, nil, nil, arguments))
- end
-
- def errors?()
- acceptor.errors?
- end
-
- def severity_producer
- p = @severity_producer
- p[Issues::UNRESOLVED_STRING_VARIABLE] = :warning
-
- # Warning since if it does not blow up on anything else, a sane subset of later version was used
- p[Issues::LATER_VERSION] = :warning
-
- # Ignore MISSING_BACKENDS because a default will be provided
- p[Issues::MISSING_BACKENDS] = :ignore
-
- # Ignore MISSING_HIERARCHY because a default will be provided
- p[Issues::MISSING_HIERARCHY] = :ignore
- p
- end
- end
-end
diff --git a/lib/puppet/pops/binder/hiera2/issues.rb b/lib/puppet/pops/binder/hiera2/issues.rb
deleted file mode 100644
index b3b0a10d0..000000000
--- a/lib/puppet/pops/binder/hiera2/issues.rb
+++ /dev/null
@@ -1,67 +0,0 @@
-module Puppet::Pops::Binder::Hiera2::Issues
- # (see Puppet::Pops::Issues#issue)
- def self.issue (issue_code, *args, &block)
- Puppet::Pops::Issues.issue(issue_code, *args, &block)
- end
-
- CONFIG_IS_NOT_HASH = issue :CONFIG_IS_NOT_HASH do
- "The configuration file '#{semantic}' has no hash at the top level"
- end
-
- MISSING_HIERARCHY = issue :MISSING_HIERARCHY do
- "The configuration file '#{semantic}' contains no hierarchy"
- end
-
- MISSING_BACKENDS = issue :MISSING_BACKENDS do
- "The configuration file '#{semantic}' contains no backends"
- end
-
- CATEGORY_MUST_BE_THREE_ELEMENT_ARRAY = issue :CATEGORY_MUST_BE_THREE_ELEMENT_ARRAY do
- "The configuration file '#{semantic}' has a malformed hierarchy (should consist of arrays with three string entries)"
- end
-
- CONFIG_FILE_NOT_FOUND = issue :CONFIG_FILE_NOT_FOUND do
- "The configuration file '#{semantic}' does not exist"
- end
-
- CONFIG_FILE_SYNTAX_ERROR = issue :CONFIG_FILE_SYNTAX_ERROR do
- "Unable to parse: #{semantic}"
- end
-
- CANNOT_LOAD_BACKEND = issue :CANNOT_LOAD_BACKEND, :key, :error do
- "Backend '#{key}' in configuration file '#{semantic}' cannot be loaded: #{error}"
- end
-
- BACKEND_FILE_DOES_NOT_DEFINE_CLASS = issue :BACKEND_FILE_DOES_NOT_DEFINE_CLASS, :class_name do
- "The file '#{semantic}' does not define class #{class_name}"
- end
-
- NOT_A_BACKEND_CLASS = issue :NOT_A_BACKEND_CLASS, :key, :class_name do
- "Class #{class_name}, loaded using key #{key} in file '#{semantic}' is not a subclass of Backend"
- end
-
- METADATA_JSON_NOT_FOUND = issue :METADATA_JSON_NOT_FOUND do
- "The metadata file '#{semantic}' does not exist"
- end
-
- UNSUPPORTED_STRING_EXPRESSION = issue :UNSUPPORTED_STRING_EXPRESSION, :expr do
- "String '#{semantic}' contains an unsupported expression (type was #{expr.class.name})"
- end
-
- UNRESOLVED_STRING_VARIABLE = issue :UNRESOLVED_STRING_VARIABLE, :key do
- "Variable '#{key}' found in string '#{semantic}' cannot be resolved"
- end
-
- MISSING_VERSION = issue :MISSING_VERSION do
- "The configuration file '#{semantic}' does not have a version."
- end
-
- WRONG_VERSION = issue :WRONG_VERSION do
- "The configuration file '#{semantic}' has the wrong version, expected: #{expected}, actual: #{actual}"
- end
-
- LATER_VERSION = issue :LATER_VERSION do
- "The configuration file '#{semantic}' has a version that is newer (features may not work), expected: #{expected}, actual: #{actual}"
- end
-
-end
diff --git a/lib/puppet/pops/binder/hiera2/json_backend.rb b/lib/puppet/pops/binder/hiera2/json_backend.rb
deleted file mode 100644
index dd4c0f220..000000000
--- a/lib/puppet/pops/binder/hiera2/json_backend.rb
+++ /dev/null
@@ -1,18 +0,0 @@
-require 'json'
-
-# A Backend implementation capable of reading JSON syntax
-class Puppet::Pops::Binder::Hiera2::JsonBackend < Puppetx::Puppet::Hiera2Backend
- def read_data(module_dir, source)
- begin
- source_file = File.join(module_dir, "#{source}.json")
- JSON.parse(File.read(source_file))
- rescue Errno::ENOTDIR
- # This is OK, the file doesn't need to be present. Return an empty hash
- {}
- rescue Errno::ENOENT
- # This is OK, the file doesn't need to be present. Return an empty hash
- {}
- end
- end
-end
-
diff --git a/lib/puppet/pops/binder/hiera2/yaml_backend.rb b/lib/puppet/pops/binder/hiera2/yaml_backend.rb
deleted file mode 100644
index d24a0dc5b..000000000
--- a/lib/puppet/pops/binder/hiera2/yaml_backend.rb
+++ /dev/null
@@ -1,21 +0,0 @@
-# A Backend implementation capable of reading YAML syntax
-class Puppet::Pops::Binder::Hiera2::YamlBackend < Puppetx::Puppet::Hiera2Backend
- def read_data(module_dir, source)
- begin
- source_file = File.join(module_dir, "#{source}.yaml")
- # if file is present but empty or has only "---", YAML.load_file returns false,
- # in which case fall back to returning an empty hash
- YAML.load_file(source_file) || {}
- rescue TypeError => e
- # SafeYaml chokes when trying to load using utf-8 and the file is empty
- raise e if File.size?(source_file)
- {}
- rescue Errno::ENOTDIR
- # This is OK, the file doesn't need to be present. Return an empty hash
- {}
- rescue Errno::ENOENT
- # This is OK, the file doesn't need to be present. Return an empty hash
- {}
- end
- end
-end
diff --git a/lib/puppet/pops/binder/injector.rb b/lib/puppet/pops/binder/injector.rb
index 722e8abe5..994394975 100644
--- a/lib/puppet/pops/binder/injector.rb
+++ b/lib/puppet/pops/binder/injector.rb
@@ -1,688 +1,767 @@
# The injector is the "lookup service" class
#
# Initialization
# --------------
# The injector is initialized with a configured {Puppet::Pops::Binder::Binder Binder}. The Binder instance contains a resolved set of
# `key => "binding information"` that is used to setup the injector.
#
# Lookup
# ------
# It is possible to lookup either the value, or a producer of the value. The {#lookup} method looks up a value, and the
# {#lookup_producer} looks up a producer.
# Both of these methods can be called with three different signatures; `lookup(key)`, `lookup(type, name)`, and `lookup(name)`,
# with the corresponding calls to obtain a producer; `lookup_producer(key)`, `lookup_producer(type, name)`, and `lookup_producer(name)`.
#
# It is possible to pass a block to {#lookup} and {#lookup_producer}, the block is passed the result of the lookup
# and the result of the block is returned as the value of the lookup. This is useful in order to provide a default value.
#
# @example Lookup with default value
# injector.lookup('favourite_food') {|x| x.nil? ? 'bacon' : x }
#
# Singleton or Not
# ----------------
# The lookup of a value is always based on the lookup of a producer. For *singleton producers* this means that the value is
# determined by the first value lookup. Subsequent lookups via `lookup` or `lookup_producer` will produce the same instance.
#
# *Non singleton producers* will produce a new instance on each request for a value. For constant value producers this
# means that a new deep-clone is produced for mutable objects (but not for immutable objects as this is not needed).
# Custom producers should have non singleton behavior, or if this is not possible ensure that the produced result is
# immutable. (The behavior if a custom producer hands out a mutable value and this is mutated is undefined).
#
# Custom bound producers capable of producing a series of objects when bound as a singleton means that the producer
# is a singleton, not the value it produces. If such a producer is bound as non singleton, each `lookup` will get a new
# producer (hence, typically, restarting the series). However, the producer returned from `lookup_producer` will not
# recreate the producer on each call to `produce`; i.e. each `lookup_producer` returns a producer capable of returning
# a series of objects.
#
# @see Puppet::Pops::Binder::Binder Binder, for details about how to bind keys to producers
# @see Puppet::Pops::Binder::BindingsFactory BindingsFactory, for a convenient way to create a Binder and bindings
#
# Assisted Inject
# ---------------
# The injector supports lookup of instances of classes *even if the requested class is not explicitly bound*.
# This is possible for classes that have a zero argument `initialize` method, or that has a class method called
# `inject` that takes two arguments; `injector`, and `scope`.
# This is useful in ruby logic as a class can then use the given injector to inject details.
# An `inject` class method wins over a zero argument `initialize` in all cases.
#
# @example Using assisted inject
# # Class with assisted inject support
# class Duck
# attr_reader :name, :year_of_birth
#
# def self.inject(injector, scope, binding, *args)
# # lookup default name and year of birth, and use defaults if not present
# name = injector.lookup(scope,'default-duck-name') {|x| x ? x : 'Donald Duck' }
# year_of_birth = injector.lookup(scope,'default-duck-year_of_birth') {|x| x ? x : 1934 }
# self.new(name, year_of_birth)
# end
#
# def initialize(name, year_of_birth)
# @name = name
# @year_of_birth = year_of_birth
# end
# end
#
# injector.lookup(scope, Duck)
# # Produces a Duck named 'Donald Duck' or named after the binding 'default-duck-name' (and with similar treatment of
# # year_of_birth).
# @see Puppet::Pops::Binder::Producers::AssistedInjectProducer AssistedInjectProducer, for more details on assisted injection
#
# Access to key factory and type calculator
# -----------------------------------------
-# It is important to use the same key factory, and type calculator as the binder. It is therefor possible to obtaint
+# It is important to use the same key factory, and type calculator as the binder. It is therefor possible to obtain
# these with the methods {#key_factory}, and {#type_calculator}.
#
# Special support for producers
# -----------------------------
# There is one method specially designed for producers. The {#get_contributions} method returns an array of all contributions
# to a given *contributions key*. This key is obtained from the {#key_factory} for a given multibinding. The returned set of
# contributed bindings is sorted in descending precedence order. Any conflicts, merges, etc. is performed by the multibinding
# producer configured for a multibinding.
#
# @api public
#
class Puppet::Pops::Binder::Injector
Producers = Puppet::Pops::Binder::Producers
+ def self.create_from_model(layered_bindings_model)
+ self.new(Puppet::Pops::Binder::Binder.new(layered_bindings_model))
+ end
+
+ def self.create_from_hash(name, key_value_hash)
+ factory = Puppet::Pops::Binder::BindingsFactory
+ named_bindings = factory.named_bindings(name) { key_value_hash.each {|k,v| bind.name(k).to(v) }}
+ layered_bindings = factory.layered_bindings(factory.named_layer(name+'-layer',named_bindings.model))
+ self.new(Puppet::Pops::Binder::Binder.new(layered_bindings))
+ end
+
+ # Creates an injector with a single bindings layer created with the given name, and the bindings
+ # produced by the given block. The block is evaluated with self bound to a BindingsContainerBuilder.
+ #
+ # @example
+ # Injector.create('mysettings') do
+ # bind('name').to(42)
+ # end
+ #
+ # @api public
+ #
+ def self.create(name, &block)
+ factory = Puppet::Pops::Binder::BindingsFactory
+ layered_bindings = factory.layered_bindings(factory.named_layer(name+'-layer',factory.named_bindings(name, &block).model))
+ self.new(Puppet::Pops::Binder::Binder.new(layered_bindings))
+ end
+
+ # Creates an overriding injector with a single bindings layer
+ # created with the given name, and the bindings produced by the given block.
+ # The block is evaluated with self bound to a BindingsContainerBuilder.
+ #
+ # @example
+ # an_injector.override('myoverrides') do
+ # bind('name').to(43)
+ # end
+ #
+ # @api public
+ #
+ def override(name, &block)
+ factory = Puppet::Pops::Binder::BindingsFactory
+ layered_bindings = factory.layered_bindings(factory.named_layer(name+'-layer',factory.named_bindings(name, &block).model))
+ self.class.new(Puppet::Pops::Binder::Binder.new(layered_bindings, @impl.binder))
+ end
+
+ # Creates an overriding injector with bindings from a bindings model (a LayeredBindings) which
+ # may consists of multiple layers of bindings.
+ #
+ # @api public
+ #
+ def override_with_model(layered_bindings)
+ unless layered_bindings.is_a?(Puppet::Pops::Binder::Bindings::LayeredBindings)
+ raise ArgumentError, "Expected a LayeredBindings model, got '#{bindings_model.class}'"
+ end
+ self.class.new(Puppet::Pops::Binder::Binder.new(layered_bindings, @impl.binder))
+ end
+
+ # Creates an overriding injector with a single bindings layer
+ # created with the given name, and the bindings given in the key_value_hash
+ # @api public
+ #
+ def override_with_hash(name, key_value_hash)
+ factory = Puppet::Pops::Binder::BindingsFactory
+ named_bindings = factory.named_bindings(name) { key_value_hash.each {|k,v| bind.name(k).to(v) }}
+ layered_bindings = factory.layered_bindings(factory.named_layer(name+'-layer',named_bindings.model))
+ self.class.new(Puppet::Pops::Binder::Binder.new(layered_bindings, @impl.binder))
+ end
+
# An Injector is initialized with a configured {Puppet::Pops::Binder::Binder Binder}.
#
# @param configured_binder [Puppet::Pops::Binder::Binder,nil] The configured binder containing effective bindings. A given value
# of nil creates an injector that returns or yields nil on all lookup requests.
# @raise ArgumentError if the given binder is not fully configured
#
# @api public
#
- def initialize(configured_binder)
+ def initialize(configured_binder, parent_injector = nil)
if configured_binder.nil?
@impl = Private::NullInjectorImpl.new()
else
- @impl = Private::InjectorImpl.new(configured_binder)
+ @impl = Private::InjectorImpl.new(configured_binder, parent_injector)
end
end
# The KeyFactory used to produce keys in this injector.
# The factory is shared with the Binder to ensure consistent translation to keys.
# A compatible type calculator can also be obtained from the key factory.
# @return [Puppet::Pops::Binder::KeyFactory] the key factory in use
#
# @api public
#
def key_factory()
@impl.key_factory
end
# Returns the TypeCalculator in use for keys. The same calculator (as used for keys) should be used if there is a need
# to check type conformance, or infer the type of Ruby objects.
#
# @return [Puppet::Pops::Types::TypeCalculator] the type calculator that is in use for keys
# @api public
#
def type_calculator()
@impl.type_calculator()
end
# Lookup (a.k.a "inject") of a value given a key.
# The lookup may be called with different parameters. This method is a convenience method that
# dispatches to one of #lookup_key or #lookup_type depending on the arguments. It also provides
# the ability to use an optional block that is called with the looked up value, or scope and value if the
# block takes two parameters. This is useful to provide a default value or other transformations, calculations
# based on the result of the lookup.
#
# @overload lookup(scope, key)
# (see #lookup_key)
# @param scope [Puppet::Parser::Scope] the scope to use for evaluation
# @param key [Object] an opaque object being the full key
#
# @overload lookup(scope, type, name = '')
# (see #lookup_type)
# @param scope [Puppet::Parser::Scope] the scope to use for evaluation
# @param type [Puppet::Pops::Types::PObjectType] the type of what to lookup
# @param name [String] the name to use, defaults to empty string (for unnamed)
#
# @overload lookup(scope, name)
# Lookup of Data type with given name.
# @see #lookup_type
# @param scope [Puppet::Parser::Scope] the scope to use for evaluation
# @param name [String] the Data/name to lookup
#
# @yield [value] passes the looked up value to an optional block and returns what this block returns
# @yield [scope, value] passes scope and value to the block and returns what this block returns
# @yieldparam scope [Puppet::Parser::Scope] the scope given to lookup
# @yieldparam value [Object, nil] the looked up value or nil if nothing was found
#
# @raise [ArgumentError] if the block has an arity that is not 1 or 2
#
# @api public
#
def lookup(scope, *args, &block)
@impl.lookup(scope, *args, &block)
end
# Looks up a (typesafe) value based on a type/name combination.
# Creates a key for the type/name combination using a KeyFactory. Specialization of the Data type are transformed
# to a Data key, and the result is type checked to conform with the given key.
#
# @param type [Puppet::Pops::Types::PObjectType] the type to lookup as defined by Puppet::Pops::Types::TypeFactory
# @param name [String] the (optional for non `Data` types) name of the entry to lookup.
# The name may be an empty String (the default), but not nil. The name is required for lookup for subtypes of
# `Data`.
# @return [Object, nil] the looked up bound object, or nil if not found (type conformance with given type is guaranteed)
# @raise [ArgumentError] if the produced value does not conform with the given type
#
# @api public
#
def lookup_type(scope, type, name='')
@impl.lookup_type(scope, type, name)
end
# Looks up the key and returns the entry, or nil if no entry is found.
# Produced type is checked for type conformance with its binding, but not with the lookup key.
# (This since all subtypes of PDataType are looked up using a key based on PDataType).
# Use the Puppet::Pops::Types::TypeCalculator#instance? method to check for conformance of the result
# if this is wanted, or use #lookup_type.
#
# @param key [Object] lookup of key as produced by the key factory
# @return [Object, nil] produced value of type that conforms with bound type (type conformance with key not guaranteed).
# @raise [ArgumentError] if the produced value does not conform with the bound type
#
# @api public
#
def lookup_key(scope, key)
@impl.lookup_key(scope, key)
end
# Lookup (a.k.a "inject") producer of a value given a key.
# The producer lookup may be called with different parameters. This method is a convenience method that
# dispatches to one of #lookup_producer_key or #lookup_producer_type depending on the arguments. It also provides
# the ability to use an optional block that is called with the looked up producer, or scope and producer if the
# block takes two parameters. This is useful to provide a default value, call a custom producer method,
# or other transformations, calculations based on the result of the lookup.
#
# @overload lookup_producer(scope, key)
# (see #lookup_proudcer_key)
# @param scope [Puppet::Parser::Scope] the scope to use for evaluation
# @param key [Object] an opaque object being the full key
#
# @overload lookup_producer(scope, type, name = '')
# (see #lookup_type)
# @param scope [Puppet::Parser::Scope] the scope to use for evaluation
# @param type [Puppet::Pops::Types::PObjectType], the type of what to lookup
# @param name [String], the name to use, defaults to empty string (for unnamed)
#
# @overload lookup_producer(scope, name)
# Lookup of Data type with given name.
# @see #lookup_type
# @param scope [Puppet::Parser::Scope] the scope to use for evaluation
# @param name [String], the Data/name to lookup
#
# @return [Puppet::Pops::Binder::Producers::Producer, Object, nil] a producer, or what the optional block returns
#
# @yield [producer] passes the looked up producer to an optional block and returns what this block returns
# @yield [scope, producer] passes scope and producer to the block and returns what this block returns
# @yieldparam producer [Puppet::Pops::Binder::Producers::Producer, nil] the looked up producer or nil if nothing was bound
# @yieldparam scope [Puppet::Parser::Scope] the scope given to lookup
#
# @raise [ArgumentError] if the block has an arity that is not 1 or 2
#
# @api public
#
def lookup_producer(scope, *args, &block)
@impl.lookup_producer(scope, *args, &block)
end
# Looks up a Producer given an opaque binder key.
# @return [Puppet::Pops::Binder::Producers::Producer, nil] the bound producer, or nil if no such producer was found.
#
# @api public
#
def lookup_producer_key(scope, key)
@impl.lookup_producer_key(scope, key)
end
# Looks up a Producer given a type/name key.
# @note The result is not type checked (it cannot be until the producer has produced an instance).
# @return [Puppet::Pops::Binder::Producers::Producer, nil] the bound producer, or nil if no such producer was found
#
# @api public
#
def lookup_producer_type(scope, type, name='')
@impl.lookup_producer_type(scope, type, name)
end
# Returns the contributions to a multibind given its contribution key (as produced by the KeyFactory).
# This method is typically used by multibind value producers, but may be used for introspection of the injector's state.
#
# @param scope [Puppet::Parser::Scope] the scope to use
# @param contributions_key [Object] Opaque key as produced by KeyFactory as the contributions key for a multibinding
# @return [Array<Puppet::Pops::Binder::InjectorEntry>] the contributions sorted in deecending order of precedence
#
# @api public
#
def get_contributions(scope, contributions_key)
@impl.get_contributions(scope, contributions_key)
end
# Returns an Injector that returns (or yields) nil on all lookups, and produces an empty structure for contributions
# This method is intended for testing purposes.
#
def self.null_injector
self.new(nil)
end
# The implementation of the Injector is private.
# @see Puppet::Pops::Binder::Injector The public API this module implements.
# @api private
#
module Private
# This is a mocking "Null" implementation of Injector. It never finds anything
# @api private
class NullInjectorImpl
attr_reader :entries
attr_reader :key_factory
attr_reader :type_calculator
def initialize
@entries = []
@key_factory = Puppet::Pops::Binder::KeyFactory.new()
@type_calculator = @key_factory.type_calculator
end
def lookup(scope, *args, &block)
raise ArgumentError, "lookup should be called with two or three arguments, got: #{args.size()+1}" unless args.size.between?(1,2)
# call block with result if given
if block
case block.arity
when 1
block.call(:undef)
when 2
block.call(scope, :undef)
else
raise ArgumentError, "The block should have arity 1 or 2"
end
else
val
end
+ end
+ # @api private
+ def binder
+ nil
end
# @api private
def lookup_key(scope, key)
nil
end
# @api private
def lookup_producer(scope, *args, &block)
lookup(scope, *args, &block)
end
# @api private
def lookup_producer_key(scope, key)
nil
end
# @api private
def lookup_producer_type(scope, type, name='')
nil
end
def get_contributions()
[]
end
end
# @api private
#
class InjectorImpl
# Hash of key => InjectorEntry
# @api private
#
attr_reader :entries
attr_reader :key_factory
attr_reader :type_calculator
- def initialize(configured_binder)
- raise ArgumentError, "Given Binder is not configured" unless configured_binder && configured_binder.configured?()
+ attr_reader :binder
+
+ def initialize(configured_binder, parent_injector = nil)
+ @binder = configured_binder
+ @parent = parent_injector
+
+ # TODO: Different error message
+ raise ArgumentError, "Given Binder is not configured" unless configured_binder #&& configured_binder.configured?()
@entries = configured_binder.injector_entries()
# It is essential that the injector uses the same key factory as the binder since keys must be
# represented the same (but still opaque) way.
#
@key_factory = configured_binder.key_factory()
@type_calculator = key_factory.type_calculator()
@@transform_visitor ||= Puppet::Pops::Visitor.new(nil,"transform", 2, 2)
@recursion_lock = [ ]
end
# @api private
def lookup(scope, *args, &block)
raise ArgumentError, "lookup should be called with two or three arguments, got: #{args.size()+1}" unless args.size.between?(1,2)
val = case args[ 0 ]
when Puppet::Pops::Types::PObjectType
lookup_type(scope, *args)
when String
raise ArgumentError, "lookup of name should only pass the name" unless args.size == 1
lookup_key(scope, key_factory.data_key(args[ 0 ]))
else
raise ArgumentError, 'lookup using a key should only pass a single key' unless args.size == 1
lookup_key(scope, args[ 0 ])
end
# call block with result if given
if block
case block.arity
when 1
block.call(val)
when 2
block.call(scope, val)
else
raise ArgumentError, "The block should have arity 1 or 2"
end
else
val
end
end
# Produces a key for a type/name combination.
# @api private
def named_key(type, name)
key_factory.named_key(type, name)
end
# Produces a key for a PDataType/name combination
# @api private
def data_key(name)
key_factory.data_key(name)
end
# @api private
def lookup_type(scope, type, name='')
val = lookup_key(scope, named_key(type, name))
+ return nil if val.nil?
unless key_factory.type_calculator.instance?(type, val)
raise ArgumentError, "Type error: incompatible type, #{type_error_detail(type, val)}"
end
val
end
# @api private
def type_error_detail(expected, actual)
actual_t = type_calculator.infer(actual)
"expected: #{type_calculator.string(expected)}, got: #{type_calculator.string(actual_t)}"
end
# @api private
def lookup_key(scope, key)
if @recursion_lock.include?(key)
raise ArgumentError, "Lookup loop detected for key: #{key}"
end
begin
@recursion_lock.push(key)
case entry = get_entry(key)
when NilClass
- nil
+ @parent ? @parent.lookup_key(scope, key) : nil
+
when Puppet::Pops::Binder::InjectorEntry
val = produce(scope, entry)
return nil if val.nil?
unless key_factory.type_calculator.instance?(entry.binding.type, val)
raise "Type error: incompatible type returned by producer, #{type_error_detail(entry.binding.type, val)}"
end
val
when Producers::AssistedInjectProducer
entry.produce(scope)
else
# internal, direct entries
entry
end
ensure
@recursion_lock.pop()
end
end
# Should be used to get entries as it converts missing entries to NotFound entries or AssistedInject entries
#
# @api private
def get_entry(key)
case entry = entries[ key ]
when NilClass
# not found, is this an assisted inject?
if clazz = assistable_injected_class(key)
entry = Producers::AssistedInjectProducer.new(self, clazz)
entries[ key ] = entry
else
entries[ key ] = NotFound.new()
entry = nil
end
when NotFound
entry = nil
end
entry
end
# Returns contributions to a multibind in precedence order; highest first.
# Returns an Array on the form [ [key, entry], [key, entry]] where the key is intended to be used to lookup the value
# (or a producer) for that entry.
# @api private
def get_contributions(scope, contributions_key)
result = {}
return [] unless contributions = lookup_key(scope, contributions_key)
contributions.each { |k| result[k] = get_entry(k) }
result.sort {|a, b| a[0] <=> b[0] }
#result.sort_by {|key, entry| entry }
end
# Produces an injectable class given a key, or nil if key does not represent an injectable class
# @api private
#
def assistable_injected_class(key)
kt = key_factory.get_type(key)
return nil unless kt.is_a?(Puppet::Pops::Types::PRubyType) && !key_factory.is_named?(key)
type_calculator.injectable_class(kt)
end
def lookup_producer(scope, *args, &block)
raise ArgumentError, "lookup_producer should be called with two or three arguments, got: #{args.size()+1}" unless args.size <= 2
p = case args[ 0 ]
when Puppet::Pops::Types::PObjectType
lookup_producer_type(scope, *args)
when String
raise ArgumentError, "lookup_producer of name should only pass the name" unless args.size == 1
lookup_producer_key(scope, key_factory.data_key(args[ 0 ]))
else
raise ArgumentError, "lookup_producer using a key should only pass a single key" unless args.size == 1
lookup_producer_key(scope, args[ 0 ])
end
# call block with result if given
if block
case block.arity
when 1
block.call(p)
when 2
block.call(scope, p)
else
raise ArgumentError, "The block should have arity 1 or 2"
end
else
p
end
end
# @api private
def lookup_producer_key(scope, key)
if @recursion_lock.include?(key)
raise ArgumentError, "Lookup loop detected for key: #{key}"
end
begin
@recursion_lock.push(key)
producer(scope, get_entry(key), :multiple_use)
ensure
@recursion_lock.pop()
end
end
# @api private
def lookup_producer_type(scope, type, name='')
lookup_producer_key(scope, named_key(type, name))
end
# Returns the producer for the entry
# @return [Puppet::Pops::Binder::Producers::Producer] the entry's producer.
#
# @api private
#
def producer(scope, entry, use)
return nil unless entry # not found
return entry.producer(scope) if entry.is_a?(Producers::AssistedInjectProducer)
unless entry.cached_producer
entry.cached_producer = transform(entry.binding.producer, scope, entry)
end
unless entry.cached_producer
raise ArgumentError, "Injector entry without a producer #{format_binding(entry.binding)}"
end
entry.cached_producer.producer(scope)
end
# @api private
def transform(producer_descriptor, scope, entry)
@@transform_visitor.visit_this(self, producer_descriptor, scope, entry)
end
# Returns the produced instance
# @return [Object] the produced instance
# @api private
#
def produce(scope, entry)
return nil unless entry # not found
producer(scope, entry, :single_use).produce(scope)
end
# @api private
def named_arguments_to_hash(named_args)
nb = named_args.nil? ? [] : named_args
result = {}
nb.each {|arg| result[ :"#{arg.name}" ] = arg.value }
result
end
# @api private
def merge_producer_options(binding, options)
named_arguments_to_hash(binding.producer_args).merge(options)
end
# @api private
def format_binding(b)
Puppet::Pops::Binder::Binder.format_binding(b)
end
# Handles a missing producer (which is valid for a Multibinding where one is selected automatically)
# @api private
#
def transform_NilClass(descriptor, scope, entry)
unless entry.binding.is_a?(Puppet::Pops::Binder::Bindings::Multibinding)
raise ArgumentError, "Binding without producer detected, #{format_binding(entry.binding)}"
end
case entry.binding.type
when Puppet::Pops::Types::PArrayType
transform(Puppet::Pops::Binder::Bindings::ArrayMultibindProducerDescriptor.new(), scope, entry)
when Puppet::Pops::Types::PHashType
transform(Puppet::Pops::Binder::Bindings::HashMultibindProducerDescriptor.new(), scope, entry)
else
raise ArgumentError, "Unsupported multibind type, must be an array or hash type, #{format_binding(entry.binding)}"
end
end
# @api private
def transform_ArrayMultibindProducerDescriptor(descriptor, scope, entry)
make_producer(Producers::ArrayMultibindProducer, descriptor, scope, entry, named_arguments_to_hash(entry.binding.producer_args))
end
# @api private
def transform_HashMultibindProducerDescriptor(descriptor, scope, entry)
make_producer(Producers::HashMultibindProducer, descriptor, scope, entry, named_arguments_to_hash(entry.binding.producer_args))
end
# @api private
def transform_ConstantProducerDescriptor(descriptor, scope, entry)
producer_class = singleton?(descriptor) ? Producers::SingletonProducer : Producers::DeepCloningProducer
producer_class.new(self, entry.binding, scope, merge_producer_options(entry.binding, {:value => descriptor.value}))
end
# @api private
def transform_InstanceProducerDescriptor(descriptor, scope, entry)
make_producer(Producers::InstantiatingProducer, descriptor, scope, entry,
merge_producer_options(entry.binding, {:class_name => descriptor.class_name, :init_args => descriptor.arguments}))
end
# @api private
def transform_EvaluatingProducerDescriptor(descriptor, scope, entry)
make_producer(Producers::EvaluatingProducer, descriptor, scope, entry,
merge_producer_options(entry.binding, {:expression => descriptor.expression}))
end
# @api private
def make_producer(clazz, descriptor, scope, entry, options)
singleton_wrapped(descriptor, scope, entry, clazz.new(self, entry.binding, scope, options))
end
# @api private
def singleton_wrapped(descriptor, scope, entry, producer)
return producer unless singleton?(descriptor)
Producers::SingletonProducer.new(self, entry.binding, scope,
merge_producer_options(entry.binding, {:value => producer.produce(scope)}))
end
# @api private
def transform_ProducerProducerDescriptor(descriptor, scope, entry)
p = transform(descriptor.producer, scope, entry)
clazz = singleton?(descriptor) ? Producers::SingletonProducerProducer : Producers::ProducerProducer
clazz.new(self, entry.binding, scope, merge_producer_options(entry.binding,
merge_producer_options(entry.binding, { :producer_producer => p })))
end
# @api private
def transform_LookupProducerDescriptor(descriptor, scope, entry)
make_producer(Producers::LookupProducer, descriptor, scope, entry,
merge_producer_options(entry.binding, {:type => descriptor.type, :name => descriptor.name}))
end
# @api private
def transform_HashLookupProducerDescriptor(descriptor, scope, entry)
make_producer(Producers::LookupKeyProducer, descriptor, scope, entry,
merge_producer_options(entry.binding, {:type => descriptor.type, :name => descriptor.name, :key => descriptor.key}))
end
# @api private
def transform_NonCachingProducerDescriptor(descriptor, scope, entry)
# simply delegates to the wrapped producer
transform(descriptor.producer, scope, entry)
end
# @api private
def transform_FirstFoundProducerDescriptor(descriptor, scope, entry)
make_producer(Producers::FirstFoundProducer, descriptor, scope, entry,
merge_producer_options(entry.binding, {:producers => descriptor.producers.collect {|p| transform(p, scope, entry) }}))
end
# @api private
def singleton?(descriptor)
! descriptor.eContainer().is_a?(Puppet::Pops::Binder::Bindings::NonCachingProducerDescriptor)
end
# Special marker class used in entries
# @api private
class NotFound
end
end
end
-end
\ No newline at end of file
+end
diff --git a/lib/puppet/pops/binder/injector_entry.rb b/lib/puppet/pops/binder/injector_entry.rb
index f61773706..629bc5e74 100644
--- a/lib/puppet/pops/binder/injector_entry.rb
+++ b/lib/puppet/pops/binder/injector_entry.rb
@@ -1,53 +1,57 @@
# Represents an entry in the injectors internal data.
#
# @api public
#
class Puppet::Pops::Binder::InjectorEntry
- # @return [Object] An opaque object representing the precedence
+ # @return [Object] An opaque (comparable) object representing the precedence
# @api public
attr_reader :precedence
# @return [Puppet::Pops::Binder::Bindings::Binding] The binding for this entry
# @api public
attr_reader :binding
# @api private
attr_accessor :resolved
# @api private
attr_accessor :cached_producer
# @api private
- def initialize(precedence, binding)
+ def initialize(binding, precedence = 0)
@precedence = precedence.freeze
@binding = binding
@cached_producer = nil
end
# Marks an overriding entry as resolved (if not an overriding entry, the marking has no effect).
# @api private
#
def mark_override_resolved()
@resolved = true
end
# The binding is resolved if it is non-override, or if the override has been resolved
# @api private
#
def is_resolved?()
!binding.override || resolved
end
def is_abstract?
binding.abstract
end
+ def is_final?
+ binding.final
+ end
+
# Compares against another InjectorEntry by comparing precedence.
# @param injector_entry [InjectorEntry] entry to compare against.
# @return [Integer] 1, if this entry has higher precedence, 0 if equal, and -1 if given entry has higher precedence.
# @api public
#
def <=> (injector_entry)
precedence <=> injector_entry.precedence
end
end
diff --git a/lib/puppet/pops/binder/key_factory.rb b/lib/puppet/pops/binder/key_factory.rb
index e5d890c63..0b45d4f02 100644
--- a/lib/puppet/pops/binder/key_factory.rb
+++ b/lib/puppet/pops/binder/key_factory.rb
@@ -1,61 +1,67 @@
# The KeyFactory is responsible for creating keys used for lookup of bindings.
# @api public
#
class Puppet::Pops::Binder::KeyFactory
attr_reader :type_calculator
# @api public
def initialize(type_calculator = Puppet::Pops::Types::TypeCalculator.new())
@type_calculator = type_calculator
end
# @api public
def binding_key(binding)
named_key(binding.type, binding.name)
end
# @api public
def named_key(type, name)
[(@type_calculator.assignable?(@type_calculator.data, type) ? @type_calculator.data : type), name]
end
# @api public
def data_key(name)
[@type_calculator.data, name]
end
# @api public
def is_contributions_key?(s)
return false unless s.is_a?(String)
s.start_with?('mc_')
end
# @api public
def multibind_contributions(multibind_id)
"mc_#{multibind_id}"
end
+ # @api public
+ def multibind_contribution_key_to_id(contributions_key)
+ # removes the leading "mc_" from the key to get the multibind_id
+ contributions_key[3..-1]
+ end
+
# @api public
def is_named?(key)
key.is_a?(Array) && key[1] && !key[1].empty?
end
# @api public
def is_data?(key)
return false unless key.is_a?(Array) && key[0].is_a?(Puppet::Pops::Types::PObjectType)
type_calculator.assignable?(type_calculator.data(), key[0])
end
# @api public
def is_ruby?(key)
return key.is_a?(Array) && key[0].is_a?(Puppet::Pops::Types::PRubyType)
end
# Returns the type of the key
# @api public
#
def get_type(key)
return nil unless key.is_a?(Array)
key[0]
end
-end
\ No newline at end of file
+end
diff --git a/lib/puppet/pops/binder/lookup.rb b/lib/puppet/pops/binder/lookup.rb
new file mode 100644
index 000000000..d44d5269c
--- /dev/null
+++ b/lib/puppet/pops/binder/lookup.rb
@@ -0,0 +1,191 @@
+# This class is the backing implementation of the Puppet function 'lookup'.
+# See puppet/parser/functions/lookup.rb for documentation.
+#
+class Puppet::Pops::Binder::Lookup
+
+ def self.parse_lookup_args(args)
+ options = {}
+ pblock = if args[-1].respond_to?(:puppet_lambda)
+ args.pop
+ end
+
+ case args.size
+ when 1
+ # name, or all options
+ if args[ 0 ].is_a?(Hash)
+ options = to_symbolic_hash(args[ 0 ])
+ else
+ options[ :name ] = args[ 0 ]
+ end
+
+ when 2
+ # name and type, or name and options
+ if args[ 1 ].is_a?(Hash)
+ options = to_symbolic_hash(args[ 1 ])
+ options[:name] = args[ 0 ] # silently overwrite option with given name
+ else
+ options[:name] = args[ 0 ]
+ options[:type] = args[ 1 ]
+ end
+
+ when 3
+ # name, type, default (no options)
+ options[ :name ] = args[ 0 ]
+ options[ :type ] = args[ 1 ]
+ options[ :default ] = args[ 2 ]
+ else
+ raise Puppet::ParseError, "The lookup function accepts 1-3 arguments, got #{args.size}"
+ end
+ options[:pblock] = pblock
+ options
+ end
+
+ def self.to_symbolic_hash(input)
+ names = [:name, :type, :default, :accept_undef, :extra, :override]
+ options = {}
+ names.each {|n| options[n] = undef_as_nil(input[n.to_s] || input[n]) }
+ options
+ end
+
+ def self.type_mismatch(type_calculator, expected, got)
+ "has wrong type, expected #{type_calculator.string(expected)}, got #{type_calculator.string(got)}"
+ end
+
+ def self.fail(msg)
+ raise Puppet::ParseError, "Function lookup() " + msg
+ end
+
+ def self.fail_lookup(names)
+ name_part = if names.size == 1
+ "the name '#{names[0]}'"
+ else
+ "any of the names ['" + names.join(', ') + "']"
+ end
+ fail("did not find a value for #{name_part}")
+ end
+
+ def self.validate_options(options, type_calculator)
+ type_parser = Puppet::Pops::Types::TypeParser.new
+ name_type = type_parser.parse('Variant[Array[String], String]')
+
+ if is_nil_or_undef?(options[:name]) || options[:name].is_a?(Array) && options[:name].empty?
+ fail ("requires a name, or array of names. Got nothing to lookup.")
+ end
+
+ t = type_calculator.infer(options[:name])
+ if ! type_calculator.assignable?(name_type, t)
+ fail("given 'name' argument, #{type_mismatch(type_calculator, options[:name], t)}")
+ end
+
+ # unless a type is already given (future case), parse the type (or default 'Data'), fails if invalid type is given
+ unless options[:type].is_a?(Puppet::Pops::Types::PAbstractType)
+ options[:type] = type_parser.parse(options[:type] || 'Data')
+ end
+
+ # default value must comply with the given type
+ if options[:default]
+ t = type_calculator.infer(options[:default])
+ if ! type_calculator.assignable?(options[:type], t)
+ fail("'default' value #{type_mismatch(type_calculator, options[:type], t)}")
+ end
+ end
+
+ if options[:extra] && !options[:extra].is_a?(Hash)
+ # do not perform inference here, it is enough to know that it is not a hash
+ fail("'extra' value must be a Hash, got #{options[:extra].class}")
+ end
+ options[:extra] = {} unless options[:extra]
+
+ if options[:override] && !options[:override].is_a?(Hash)
+ # do not perform inference here, it is enough to know that it is not a hash
+ fail("'override' value must be a Hash, got #{options[:extra].class}")
+ end
+ options[:override] = {} unless options[:override]
+
+ end
+
+ def self.nil_as_undef(x)
+ x.nil? ? :undef : x
+ end
+
+ def self.undef_as_nil(x)
+ is_nil_or_undef?(x) ? nil : x
+ end
+
+ def self.is_nil_or_undef?(x)
+ x.nil? || x == :undef
+ end
+
+ # This is used as a marker - a value that cannot (at least not easily) by mistake be found in
+ # hiera data.
+ #
+ class PrivateNotFoundMarker; end
+
+ def self.search_for(scope, type, name, options)
+ # search in order, override, injector, hiera, then extra
+ if !(result = options[:override][name]).nil?
+ result
+ elsif !(result = scope.compiler.injector.lookup(scope, type, name)).nil?
+ result
+ else
+ result = scope.function_hiera([name, PrivateNotFoundMarker])
+ if !result.nil? && result != PrivateNotFoundMarker
+ result
+ else
+ options[:extra][name]
+ end
+ end
+ end
+
+ # This is the method called from the puppet/parser/functions/lookup.rb
+ # @param args [Array] array following the puppet function call conventions
+ def self.lookup(scope, args)
+ type_calculator = Puppet::Pops::Types::TypeCalculator.new
+ options = parse_lookup_args(args)
+ validate_options(options, type_calculator)
+ names = [options[:name]].flatten
+ type = options[:type]
+
+ result_with_name = names.reduce([]) do |memo, name|
+ break memo if !memo[1].nil?
+ [name, search_for(scope, type, name, options)]
+ end
+
+ result = if result_with_name[1].nil?
+ # not found, use default (which may be nil), the default is already type checked
+ options[:default]
+ else
+ # injector.lookup is type-safe already do no need to type check the result
+ result_with_name[1]
+ end
+
+ result = if pblock = options[:pblock]
+ result2 = case pblock.parameter_count
+ when 1
+ pblock.call(scope, nil_as_undef(result))
+ when 2
+ pblock.call(scope, result_with_name[ 0 ], nil_as_undef(result))
+ else
+ pblock.call(scope, result_with_name[ 0 ], nil_as_undef(result), nil_as_undef(options[ :default ]))
+ end
+
+ # if the given result was returned, there is not need to type-check it again
+ if !result2.equal?(result)
+ t = type_calculator.infer(undef_as_nil(result2))
+ if !type_calculator.assignable?(type, t)
+ fail "the value produced by the given code block #{type_mismatch(type_calculator, type, t)}"
+ end
+ end
+ result2
+ else
+ result
+ end
+
+ # Finally, the result if nil must be acceptable or an error is raised
+ if is_nil_or_undef?(result) && !options[:accept_undef]
+ fail_lookup(names)
+ else
+ nil_as_undef(result)
+ end
+ end
+end
diff --git a/lib/puppet/pops/binder/producers.rb b/lib/puppet/pops/binder/producers.rb
index 2cd8fb6db..e5f32cd00 100644
--- a/lib/puppet/pops/binder/producers.rb
+++ b/lib/puppet/pops/binder/producers.rb
@@ -1,829 +1,829 @@
# This module contains the various producers used by Puppet Bindings.
# The main (abstract) class is {Puppet::Pops::Binder::Producers::Producer} which documents the
# Producer API and serves as a base class for all other producers.
# It is required that custom producers inherit from this producer (directly or indirectly).
#
# The selection of a Producer is typically performed by the Innjector when it configures itself
-# from a Bindings model where a {Puppet::Pops::Binder::Bindings::ProducerDescriptor} describes
+# from a Bindings model where a {Puppet::Pops::Binder::Bindings::ProducerDescriptor} describes
# which producer to use. The configuration uses this to create the concrete producer.
# It is possible to describe that a particular producer class is to be used, and also to describe that
# a custom producer (derived from Producer) should be used. This is available for both regular
# bindings as well as multi-bindings.
#
#
# @api public
#
module Puppet::Pops::Binder::Producers
# Producer is an abstract base class representing the base contract for a bound producer.
# Typically, when a lookup is performed it is the value that is returned (via a producer), but
# it is also possible to lookup the producer, and ask it to produce the value (the producer may
# return a series of values, which makes this especially useful).
#
# When looking up a producer, it is of importance to only use the API of the Producer class
# unless it is known that a particular custom producer class has been bound.
#
# Custom Producers
# ----------------
# The intent is that this class is derived for custom producers that require additional
# options/arguments when producing an instance. Such a custom producer may raise an error if called
# with too few arguments, or may implement specific `produce` methods and always raise an
# error on #produce indicating that this producer requires custom calls and that it can not
# be used as an implicit producer.
#
# Features of Producer
# --------------------
# The Producer class is abstract, but offers the ability to transform the produced result
# by passing the option `:transformer` which should be a Puppet Lambda Expression taking one argument
# and producing the transformed (wanted) result.
#
# @abstract
# @api public
#
class Producer
# A Puppet 3 AST Lambda Expression
# @api public
#
attr_reader :transformer
# Creates a Producer.
# Derived classes should call this constructor to get support for transformer lambda.
#
# @param injector [Puppet::Pops::Binder::Injector] The injector where the lookup originates
# @param binding [Puppet::Pops::Binder::Bindings::Binding, nil] The binding using this producer
# @param scope [Puppet::Parser::Scope] The scope to use for evaluation
# @option options [Puppet::Pops::Model::LambdaExpression] :transformer (nil) a transformer of produced value
# @api public
#
def initialize(injector, binding, scope, options)
if transformer_lambda = options[:transformer]
if transformer_lambda.is_a?(Proc)
raise ArgumentError, "Transformer Proc must take two arguments; scope, value." unless transformer_lambda.arity == 2
@transformer = transformer_lambda
else
raise ArgumentError, "Transformer must be a LambdaExpression" unless transformer_lambda.is_a?(Puppet::Pops::Model::LambdaExpression)
raise ArgumentError, "Transformer lambda must take one argument; value." unless transformer_lambda.parameters.size() == 1
# NOTE: This depends on Puppet 3 AST Lambda
@transformer = Puppet::Pops::Model::AstTransformer.new().transform(transformer_lambda)
end
end
end
# Produces an instance.
# @param scope [Puppet::Parser:Scope] the scope to use for evaluation
# @param args [Object] arguments to custom producers, always empty for implicit productions
# @return [Object] the produced instance (should never be nil).
# @api public
#
def produce(scope, *args)
do_transformation(scope, internal_produce(scope))
end
# Returns the producer after possibly having recreated an internal/wrapped producer.
# This implementation returns `self`. A derived class may want to override this method
# to perform initialization/refresh of its internal state. This method is called when
# a producer is requested.
# @see Puppet::Pops::Binder::ProducerProducer for an example of implementation.
# @param scope [Puppet::Parser:Scope] the scope to use for evaluation
# @return [Puppet::Pops::Binder::Producer] the producer to use
# @api public
#
def producer(scope)
self
end
protected
# Derived classes should implement this method to do the production of a value
# @param scope [Puppet::Parser::Scope] the scope to use when performing lookup and evaluation
# @raise [NotImplementedError] this implementation always raises an error
# @abstract
# @api private
#
def internal_produce(scope)
raise NotImplementedError, "Producer-class '#{self.class.name}' should implement #internal_produce(scope)"
end
# Transforms the produced value if a transformer has been defined.
# @param scope [Puppet::Parser::Scope] the scope used for evaluation
# @param produced_value [Object, nil] the produced value (possibly nil)
# @return [Object] the transformed value if a transformer is defined, else the given `produced_value`
# @api private
#
def do_transformation(scope, produced_value)
return produced_value unless transformer
produced_value = :undef if produced_value.nil?
transformer.call(scope, produced_value)
end
end
# Abstract Producer holding a value
# @abstract
# @api public
#
class AbstractValueProducer < Producer
# @api public
attr_reader :value
# @param injector [Puppet::Pops::Binder::Injector] The injector where the lookup originates
# @param binding [Puppet::Pops::Binder::Bindings::Binding, nil] The binding using this producer
# @param scope [Puppet::Parser::Scope] The scope to use for evaluation
# @option options [Puppet::Pops::Model::LambdaExpression] :transformer (nil) a transformer of produced value
# @option options [Puppet::Pops::Model::LambdaExpression, nil] :value (nil) the value to produce
# @api public
#
def initialize(injector, binding, scope, options)
super
# nil is ok here, as an abstract value producer may be used to signal "not found"
@value = options[:value]
end
end
# Produces the same/singleton value on each production
# @api public
#
class SingletonProducer < AbstractValueProducer
protected
# @api private
def internal_produce(scope)
value()
end
end
# Produces a deep clone of its value on each production.
# @api public
#
class DeepCloningProducer < AbstractValueProducer
protected
# @api private
def internal_produce(scope)
case value
when Integer, Float, TrueClass, FalseClass, Symbol
# These are immutable
return value
when String
# ok if frozen, else fall through to default
return value() if value.frozen?
end
# The default: serialize/deserialize to get a deep copy
Marshal.load(Marshal.dump(value()))
end
end
# This abstract producer class remembers the injector and binding.
# @abstract
# @api public
#
class AbstractArgumentedProducer < Producer
# @api public
attr_reader :injector
# @api public
attr_reader :binding
# @param injector [Puppet::Pops::Binder::Injector] The injector where the lookup originates
# @param binding [Puppet::Pops::Binder::Bindings::Binding, nil] The binding using this producer
# @param scope [Puppet::Parser::Scope] The scope to use for evaluation
# @option options [Puppet::Pops::Model::LambdaExpression] :transformer (nil) a transformer of produced value
# @api public
#
def initialize(injector, binding, scope, options)
super
@injector = injector
@binding = binding
end
end
# @api public
class InstantiatingProducer < AbstractArgumentedProducer
# @api public
attr_reader :the_class
# @api public
attr_reader :init_args
# @param injector [Puppet::Pops::Binder::Injector] The injector where the lookup originates
# @param binding [Puppet::Pops::Binder::Bindings::Binding, nil] The binding using this producer
# @param scope [Puppet::Parser::Scope] The scope to use for evaluation
# @option options [Puppet::Pops::Model::LambdaExpression] :transformer (nil) a transformer of produced value
# @option options [String] :class_name The name of the class to create instance of
# @option options [Array<Object>] :init_args ([]) Optional arguments to class constructor
# @api public
#
def initialize(injector, binding, scope, options)
# Better do this, even if a transformation of a created instance is kind of an odd thing to do, one can imagine
# sending it to a function for further detailing.
#
super
class_name = options[:class_name]
raise ArgumentError, "Option 'class_name' must be given for an InstantiatingProducer" unless class_name
# get class by name
@the_class = Puppet::Pops::Types::ClassLoader.provide(class_name)
@init_args = options[:init_args] || []
raise ArgumentError, "Can not load the class #{class_name} specified in binding named: '#{binding.name}'" unless @the_class
end
protected
# Performs initialization the same way as Assisted Inject does (but handle arguments to
# constructor)
# @api private
#
def internal_produce(scope)
result = nil
# A class :inject method wins over an instance :initialize if it is present, unless a more specific
# constructor exists. (i.e do not pick :inject from superclass if class has a constructor).
#
if the_class.respond_to?(:inject)
inject_method = the_class.method(:inject)
initialize_method = the_class.instance_method(:initialize)
if inject_method.owner <= initialize_method.owner
result = the_class.inject(injector, scope, binding, *init_args)
end
end
if result.nil?
result = the_class.new(*init_args)
end
result
end
end
# @api public
class FirstFoundProducer < Producer
# @api public
attr_reader :producers
# @param injector [Puppet::Pops::Binder::Injector] The injector where the lookup originates
# @param binding [Puppet::Pops::Binder::Bindings::Binding, nil] The binding using this producer
# @param scope [Puppet::Parser::Scope] The scope to use for evaluation
# @option options [Puppet::Pops::Model::LambdaExpression] :transformer (nil) a transformer of produced value
# @option options [Array<Puppet::Pops::Binder::Producers::Producer>] :producers list of producers to consult. Required.
# @api public
#
def initialize(injector, binding, scope, options)
super
@producers = options[:producers]
raise ArgumentError, "Option :producers' must be set to a list of producers." if @producers.nil?
raise ArgumentError, "Given 'producers' option is not an Array" unless @producers.is_a?(Array)
end
protected
# @api private
def internal_produce(scope)
# return the first produced value that is non-nil (unfortunately there is no such enumerable method)
producers.reduce(nil) {|memo, p| break memo unless memo.nil?; p.produce(scope)}
end
end
# Evaluates a Puppet Expression and returns the result.
# This is typically used for strings with interpolated expressions.
# @api public
#
class EvaluatingProducer < Producer
# A Puppet 3 AST Expression
# @api public
#
attr_reader :expression
# @param injector [Puppet::Pops::Binder::Injector] The injector where the lookup originates
# @param binding [Puppet::Pops::Binder::Bindings::Binding, nil] The binding using this producer
# @param scope [Puppet::Parser::Scope] The scope to use for evaluation
# @option options [Puppet::Pops::Model::LambdaExpression] :transformer (nil) a transformer of produced value
# @option options [Array<Puppet::Pops::Model::Expression>] :expression The expression to evaluate
# @api public
#
def initialize(injector, binding, scope, options)
super
expr = options[:expression]
raise ArgumentError, "Option 'expression' must be given to an EvaluatingProducer." unless expr
@expression = Puppet::Pops::Model::AstTransformer.new().transform(expr)
end
# @api private
def internal_produce(scope)
expression.evaluate(scope)
end
end
# @api public
class LookupProducer < AbstractArgumentedProducer
# @api public
attr_reader :type
# @api public
attr_reader :name
# @param injector [Puppet::Pops::Binder::Injector] The injector where the lookup originates
# @param binding [Puppet::Pops::Binder::Bindings::Binding, nil] The binding using this producer
# @param scope [Puppet::Parser::Scope] The scope to use for evaluation
# @option options [Puppet::Pops::Model::LambdaExpression] :transformer (nil) a transformer of produced value
# @option options [Puppet::Pops::Types::PObjectType] :type The type to lookup
# @option options [String] :name ('') The name to lookup
# @api public
#
def initialize(injector, binder, scope, options)
super
@type = options[:type]
@name = options[:name] || ''
raise ArgumentError, "Option 'type' must be given in a LookupProducer." unless @type
end
protected
# @api private
def internal_produce(scope)
injector.lookup_type(scope, type, name)
end
end
# @api public
class LookupKeyProducer < LookupProducer
# @api public
attr_reader :key
# @param injector [Puppet::Pops::Binder::Injector] The injector where the lookup originates
# @param binding [Puppet::Pops::Binder::Bindings::Binding, nil] The binding using this producer
# @param scope [Puppet::Parser::Scope] The scope to use for evaluation
# @option options [Puppet::Pops::Model::LambdaExpression] :transformer (nil) a transformer of produced value
# @option options [Puppet::Pops::Types::PObjectType] :type The type to lookup
# @option options [String] :name ('') The name to lookup
# @option options [Puppet::Pops::Types::PObjectType] :key The key to lookup in the hash
# @api public
#
def initialize(injector, binder, scope, options)
super
@key = options[:key]
raise ArgumentError, "Option 'key' must be given in a LookupKeyProducer." if key.nil?
end
protected
# @api private
def internal_produce(scope)
result = super
result.is_a?(Hash) ? result[key] : nil
end
end
# Produces the given producer, then uses that producer.
# @see ProducerProducer for the non singleton version
# @api public
#
class SingletonProducerProducer < Producer
# @api public
attr_reader :value_producer
# @param injector [Puppet::Pops::Binder::Injector] The injector where the lookup originates
# @param binding [Puppet::Pops::Binder::Bindings::Binding, nil] The binding using this producer
# @param scope [Puppet::Parser::Scope] The scope to use for evaluation
# @option options [Puppet::Pops::Model::LambdaExpression] :transformer (nil) a transformer of produced value
# @option options [Puppet::Pops::Model::LambdaExpression] :producer_producer a producer of a value producer (required)
# @api public
#
def initialize(injector, binding, scope, options)
super
p = options[:producer_producer]
raise ArgumentError, "Option :producer_producer must be given in a SingletonProducerProducer" unless p
@value_producer = p.produce(scope)
end
protected
# @api private
def internal_produce(scope)
value_producer.produce(scope)
end
end
# A ProducerProducer creates a producer via another producer, and then uses this created producer
# to produce values. This is useful for custom production of series of values.
# On each request for a producer, this producer will reset its internal producer (i.e. restarting
# the series).
#
# @param producer_producer [#produce(scope)] the producer of the producer
#
# @api public
#
class ProducerProducer < Producer
# @api public
attr_reader :producer_producer
# @api public
attr_reader :value_producer
# Creates new ProducerProducer given a producer.
#
# @param injector [Puppet::Pops::Binder::Injector] The injector where the lookup originates
# @param binding [Puppet::Pops::Binder::Bindings::Binding, nil] The binding using this producer
# @param scope [Puppet::Parser::Scope] The scope to use for evaluation
# @option options [Puppet::Pops::Model::LambdaExpression] :transformer (nil) a transformer of produced value
# @option options [Puppet::Pops::Binder::Producer] :producer_producer a producer of a value producer (required)
#
# @api public
#
def initialize(injector, binding, scope, options)
super
unless producer_producer = options[:producer_producer]
raise ArgumentError, "The option :producer_producer must be set in a ProducerProducer"
end
raise ArgumentError, "Argument must be a Producer" unless producer_producer.is_a?(Producer)
@producer_producer = producer_producer
@value_producer = nil
end
# Updates the internal state to use a new instance of the wrapped producer.
# @api public
#
def producer(scope)
@value_producer = @producer_producer.produce(scope)
self
end
protected
# Produces a value after having created an instance of the wrapped producer (if not already created).
# @api private
#
def internal_produce(scope, *args)
producer() unless value_producer
value_producer.produce(scope)
end
end
# This type of producer should only be created by the Injector.
- #
+ #
# @api private
#
class AssistedInjectProducer < Producer
# An Assisted Inject Producer is created when a lookup is made of a type that is
- # not bound. It does not support a transformer lambda.
+ # not bound. It does not support a transformer lambda.
# @note This initializer has a different signature than all others. Do not use in regular logic.
# @api private
#
def initialize(injector, clazz)
raise ArgumentError, "class must be given" unless clazz.is_a?(Class)
@injector = injector
@clazz = clazz
@inst = nil
end
def produce(scope, *args)
producer(scope, *args) unless @inst
@inst
end
# @api private
def producer(scope, *args)
@inst = nil
# A class :inject method wins over an instance :initialize if it is present, unless a more specific zero args
# constructor exists. (i.e do not pick :inject from superclass if class has a zero args constructor).
#
if @clazz.respond_to?(:inject)
inject_method = @clazz.method(:inject)
initialize_method = @clazz.instance_method(:initialize)
if inject_method.owner <= initialize_method.owner || initialize_method.arity != 0
@inst = @clazz.inject(@injector, scope, nil, *args)
end
end
if @inst.nil?
unless args.empty?
raise ArgumentError, "Assisted Inject can not pass arguments to no-args constructor when there is no class inject method."
end
@inst = @clazz.new()
end
self
end
end
# Abstract base class for multibind producers.
# Is suitable as base class for custom implementations of multibind producers.
# @abstract
# @api public
#
class MultibindProducer < AbstractArgumentedProducer
attr_reader :contributions_key
# @param injector [Puppet::Pops::Binder::Injector] The injector where the lookup originates
# @param binding [Puppet::Pops::Binder::Bindings::Binding, nil] The binding using this producer
# @param scope [Puppet::Parser::Scope] The scope to use for evaluation
# @option options [Puppet::Pops::Model::LambdaExpression] :transformer (nil) a transformer of produced value
#
# @api public
#
def initialize(injector, binding, scope, options)
super
@contributions_key = injector.key_factory.multibind_contributions(binding.id)
end
# @param expected [Array<Puppet::Pops::Types::PObjectType>, Puppet::Pops::Types::PObjectType] expected type or types
# @param actual [Object, Puppet::Pops::Types::PObjectType> the actual value (or its type)
# @return [String] a formatted string for inclusion as detail in an error message
# @api private
#
def type_error_detail(expected, actual)
tc = injector.type_calculator
expected = [expected] unless expected.is_a?(Array)
actual_t = tc.is_ptype?(actual) ? actual : tc.infer(actual)
expstrs = expected.collect {|t| tc.string(t) }
"expected: #{expstrs.join(', or ')}, got: #{tc.string(actual_t)}"
end
end
# A configurable multibind producer for Array type multibindings.
#
# This implementation collects all contributions to the multibind and then combines them using the following rules:
#
# - all *unnamed* entries are added unless the option `:priority_on_unnamed` is set to true, in which case the unnamed
# contribution with the highest priority is added, and the rest are ignored (unless they have the same priority in which
# case an error is raised).
# - all *named* entries are handled the same way as *unnamed* but the option `:priority_on_named` controls their handling.
# - the option `:uniq` post processes the result to only contain unique entries
# - the option `:flatten` post processes the result by flattening all nested arrays.
# - If both `:flatten` and `:uniq` are true, flattening is done first.
#
# @note
# Collection accepts elements that comply with the array's element type, or the entire type (i.e. Array[element_type]).
# If the type is restrictive - e.g. Array[String] and an Array[String] is contributed, the result will not be type
# compliant without also using the `:flatten` option, and a type error will be raised. For an array with relaxed typing
- # i.e. Array[Data], it it valid to produce a result such as `['a', ['b', 'c'], 'd']` and no flattening is required
+ # i.e. Array[Data], it is valid to produce a result such as `['a', ['b', 'c'], 'd']` and no flattening is required
# and no error is raised (but using the array needs to be aware of potential array, non-array entries.
# The use of the option `:flatten` controls how the result is flattened.
#
# @api public
#
class ArrayMultibindProducer < MultibindProducer
# @return [Boolean] whether the result should be made contain unique (non-equal) entries or not
# @api public
attr_reader :uniq
# @return [Boolean, Integer] If result should be flattened (true), or not (false), or flattened to given level (0 = none, -1 = all)
# @api public
attr_reader :flatten
# @return [Boolean] whether priority should be considered for named contributions
# @api public
attr_reader :priority_on_named
# @return [Boolean] whether priority should be considered for unnamed contributions
# @api public
attr_reader :priority_on_unnamed
# @param injector [Puppet::Pops::Binder::Injector] The injector where the lookup originates
# @param binding [Puppet::Pops::Binder::Bindings::Binding, nil] The binding using this producer
# @param scope [Puppet::Parser::Scope] The scope to use for evaluation
# @option options [Puppet::Pops::Model::LambdaExpression] :transformer (nil) a transformer of produced value
# @option options [Boolean] :uniq (false) if collected result should be post-processed to contain only unique entries
# @option options [Boolean, Integer] :flatten (false) if collected result should be post-processed so all contained arrays
# are flattened. May be set to an Integer value to indicate the level of recursion (-1 is endless, 0 is none).
# @option options [Boolean] :priority_on_named (true) if highest precedented named element should win or if all should be included
# @option options [Boolean] :priority_on_unnamed (false) if highest precedented unnamed element should win or if all should be included
# @api public
#
def initialize(injector, binding, scope, options)
super
@uniq = !!options[:uniq]
@flatten = options[:flatten]
@priority_on_named = options[:priority_on_named].nil? ? true : options[:priority_on_name]
@priority_on_unnamed = !!options[:priority_on_unnamed]
case @flatten
when Integer
when true
@flatten = -1
when false
@flatten = nil
when NilClass
@flatten = nil
else
raise ArgumentError, "Option :flatten must be nil, Boolean, or an integer value" unless @flatten.is_a?(Integer)
end
end
protected
# @api private
def internal_produce(scope)
seen = {}
included_keys = []
injector.get_contributions(scope, contributions_key).each do |element|
key = element[0]
entry = element[1]
name = entry.binding.name
existing = seen[name]
empty_name = name.nil? || name.empty?
if existing
if empty_name && priority_on_unnamed
if (seen[name] <=> entry) >= 0
raise ArgumentError, "Duplicate key (same priority) contributed to Array Multibinding '#{binding.name}' with unnamed entry."
end
next
elsif !empty_name && priority_on_named
if (seen[name] <=> entry) >= 0
raise ArgumentError, "Duplicate key (same priority) contributed to Array Multibinding '#{binding.name}', key: '#{name}'."
end
next
end
else
seen[name] = entry
end
included_keys << key
end
result = included_keys.collect do |k|
x = injector.lookup_key(scope, k)
assert_type(binding(), injector.type_calculator(), x)
x
end
result.flatten!(flatten) if flatten
result.uniq! if uniq
result
end
# @api private
def assert_type(binding, tc, value)
infered = tc.infer(value)
unless tc.assignable?(binding.type.element_type, infered) || tc.assignable?(binding.type, infered)
raise ArgumentError, ["Type Error: contribution to '#{binding.name}' does not match type of multibind, ",
"#{type_error_detail([binding.type.element_type, binding.type], value)}"].join()
end
end
end
# @api public
class HashMultibindProducer < MultibindProducer
# @return [Symbol] One of `:error`, `:merge`, `:append`, `:priority`, `:ignore`
# @api public
attr_reader :conflict_resolution
# @return [Boolean]
# @api public
attr_reader :uniq
# @return [Boolean, Integer] Flatten all if true, or none if false, or to given level (0 = none, -1 = all)
# @api public
attr_reader :flatten
# The hash multibind producer provides options to control conflict resolution.
# By default, the hash is produced using `:priority` resolution - the highest entry is selected, the rest are
# ignored unless they have the same priority which is an error.
#
# @param injector [Puppet::Pops::Binder::Injector] The injector where the lookup originates
# @param binding [Puppet::Pops::Binder::Bindings::Binding, nil] The binding using this producer
# @param scope [Puppet::Parser::Scope] The scope to use for evaluation
# @option options [Puppet::Pops::Model::LambdaExpression] :transformer (nil) a transformer of produced value
# @option options [Symbol, String] :conflict_resolution (:priority) One of `:error`, `:merge`, `:append`, `:priority`, `:ignore`
# <ul><li> `ignore` the first found highest priority contribution is used, the rest are ignored</li>
# <li>`error` any duplicate key is an error</li>
# <li>`append` element type must be compatible with Array, makes elements be arrays and appends all found</li>
# <li>`merge` element type must be compatible with hash, merges hashes with retention of highest priority hash content</li>
# <li>`priority` the first found highest priority contribution is used, duplicates with same priority raises and error, the rest are
# ignored.</li></ul>
# @option options [Boolean, Integer] :flatten (false) If appended should be flattened. Also see {#flatten}.
# @option options [Boolean] :uniq (false) If appended result should be made unique.
#
# @api public
#
def initialize(injector, binding, scope, options)
super
@conflict_resolution = options[:conflict_resolution].nil? ? :priority : options[:conflict_resolution]
@uniq = !!options[:uniq]
@flatten = options[:flatten]
unless [:error, :merge, :append, :priority, :ignore].include?(@conflict_resolution)
raise ArgumentError, "Unknown conflict_resolution for Multibind Hash: '#{@conflict_resolution}."
end
case @flatten
when Integer
when true
@flatten = -1
when false
@flatten = nil
when NilClass
@flatten = nil
else
raise ArgumentError, "Option :flatten must be nil, Boolean, or an integer value" unless @flatten.is_a?(Integer)
end
if uniq || flatten || conflict_resolution.to_s == 'append'
etype = binding.type.element_type
unless etype.class == Puppet::Pops::Types::PDataType || etype.is_a?(Puppet::Pops::Types::PArrayType)
detail = []
detail << ":uniq" if uniq
detail << ":flatten" if flatten
detail << ":conflict_resolution => :append" if conflict_resolution.to_s == 'append'
raise ArgumentError, ["Options #{detail.join(', and ')} cannot be used with a Multibind ",
"of type #{injector.type_calculator.string(binding.type)}"].join()
end
end
end
protected
# @api private
def internal_produce(scope)
seen = {}
included_entries = []
injector.get_contributions(scope, contributions_key).each do |element|
key = element[0]
entry = element[1]
name = entry.binding.name
raise ArgumentError, "A Hash Multibind contribution to '#{binding.name}' must have a name." if name.nil? || name.empty?
existing = seen[name]
if existing
case conflict_resolution.to_s
when 'priority'
# skip if duplicate has lower prio
if (comparison = (seen[name] <=> entry)) <= 0
raise ArgumentError, "Internal Error: contributions not given in decreasing precedence order" unless comparison == 0
raise ArgumentError, "Duplicate key (same priority) contributed to Hash Multibinding '#{binding.name}', key: '#{name}'."
end
next
when 'ignore'
# skip, ignore conflict if prio is the same
next
when 'error'
raise ArgumentError, "Duplicate key contributed to Hash Multibinding '#{binding.name}', key: '#{name}'."
end
else
seen[name] = entry
end
included_entries << [key, entry]
end
result = {}
included_entries.each do |element|
k = element[ 0 ]
entry = element[ 1 ]
x = injector.lookup_key(scope, k)
name = entry.binding.name
assert_type(binding(), injector.type_calculator(), name, x)
if result[ name ]
merge(result, name, result[ name ], x)
else
result[ name ] = conflict_resolution().to_s == 'append' ? [x] : x
end
end
result
end
# @api private
def merge(result, name, higher, lower)
case conflict_resolution.to_s
when 'append'
unless higher.is_a?(Array)
higher = [higher]
end
tmp = higher + [lower]
tmp.flatten!(flatten) if flatten
tmp.uniq! if uniq
result[name] = tmp
when 'merge'
result[name] = lower.merge(higher)
end
end
# @api private
def assert_type(binding, tc, key, value)
unless tc.instance?(binding.type.key_type, key)
raise ArgumentError, ["Type Error: key contribution to #{binding.name}['#{key}'] ",
"is incompatible with key type: #{tc.label(binding.type)}, ",
type_error_detail(binding.type.key_type, key)].join()
end
if key.nil? || !key.is_a?(String) || key.empty?
raise ArgumentError, "Entry contributing to multibind hash with id '#{binding.id}' must have a name."
end
unless tc.instance?(binding.type.element_type, value)
raise ArgumentError, ["Type Error: value contribution to #{binding.name}['#{key}'] ",
"is incompatible, ",
type_error_detail(binding.type.element_type, value)].join()
end
end
end
-end
\ No newline at end of file
+end
diff --git a/lib/puppet/pops/binder/scheme_handler/confdir_hiera_scheme.rb b/lib/puppet/pops/binder/scheme_handler/confdir_hiera_scheme.rb
deleted file mode 100644
index d2ae152f7..000000000
--- a/lib/puppet/pops/binder/scheme_handler/confdir_hiera_scheme.rb
+++ /dev/null
@@ -1,67 +0,0 @@
-# Similar to {Puppet::Pops::Binder::SchemeHandler::ModuleHieraScheme ModuleHieraScheme} but path is
-# relative to the `$confdir` instead of relative to a module root.
-#
-# Does not handle wild-cards.
-# @api public
-class Puppet::Pops::Binder::SchemeHandler::ConfdirHieraScheme < Puppetx::Puppet::BindingsSchemeHandler
-
- # (Puppetx::Puppet::BindingsSchemeHandler.contributed_bindings)
- #
- def contributed_bindings(uri, scope, composer)
- split_path = uri.path.split('/')
- name = split_path[1]
- confdir = composer.confdir
- provider = Puppet::Pops::Binder::Hiera2::BindingsProvider.new(uri.to_s, File.join(confdir, uri.path), composer.acceptor)
- provider.load_bindings(scope)
- end
-
- # This handler does not support wildcards.
- # The given uri is simply returned in an array.
- # @param uri [URI] the uri to expand
- # @return [Array<URI>] the uri wrapped in an array
- # @todo Handle optional and possibly hiera-1 hiera.yaml config file in the expected location (the same as missing)
- # @api public
- #
- def expand_included(uri, composer)
- result = []
- if config_exist?(uri, composer)
- result << uri unless is_ignored_hiera_version?(uri, composer)
- else
- result << uri unless is_optional?(uri)
- end
- result
- end
-
- # This handler does not support wildcards.
- # The given uri is simply returned in an array.
- # @param uri [URI] the uri to expand
- # @return [Array<URI>] the uri wrapped in an array
- # @api public
- #
- def expand_excluded(uri, composer)
- [uri]
- end
-
- def config_exist?(uri, composer)
- 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.
- # All other conditions are reported as 'not ignored' even if there are errors; these will be handled later
- # as if the hiera.yaml is a hiera-2 file.
- # @api private
- def is_ignored_hiera_version?(uri, composer)
- config_file = File.join(composer.confdir, uri.path, 'hiera.yaml')
- begin
- data = YAML.load_file(config_file)
- if data.is_a?(Hash)
- ver = data[:version] || data['version']
- return ver.nil? || ver < 2
- end
- rescue Errno::ENOENT
- rescue Errno::ENOTDIR
- rescue ::SyntaxError => e
- end
- return false
- end
-end
diff --git a/lib/puppet/pops/binder/scheme_handler/module_hiera_scheme.rb b/lib/puppet/pops/binder/scheme_handler/module_hiera_scheme.rb
deleted file mode 100644
index 0663ee2cb..000000000
--- a/lib/puppet/pops/binder/scheme_handler/module_hiera_scheme.rb
+++ /dev/null
@@ -1,92 +0,0 @@
-# The `module-hiera:` scheme uses the path to denote a directory relative to a module root
-# The path starts with the name of the module, or '*' to denote *any module*.
-#
-# @example All root hiera.yaml from all modules.
-# module-hiera:/*
-#
-# @example The hiera.yaml from the module `foo`'s relative path `<foo root>/bar`.
-# module-hiera:/foo/bar
-#
-class Puppet::Pops::Binder::SchemeHandler::ModuleHieraScheme < Puppetx::Puppet::BindingsSchemeHandler
- # (Puppetx::Puppet::BindingsSchemeHandler.contributed_bindings)
- # @api public
- def contributed_bindings(uri, scope, composer)
- split_path = uri.path.split('/')
- name = split_path[1]
- mod = composer.name_to_module[name]
- provider = Puppet::Pops::Binder::Hiera2::BindingsProvider.new(uri.to_s, File.join(mod.path, split_path[ 2..-1 ]), composer.acceptor)
- provider.load_bindings(scope)
- end
-
- # Expands URIs with wildcards and checks optionality.
- # @param uri [URI] the uri to possibly expand
- # @return [Array<URI>] the URIs to include
- # @api public
- #
- def expand_included(uri, composer)
- result = []
- split_path = uri.path.split('/')
- if split_path.size > 1 && split_path[-1].empty?
- split_path.delete_at(-1)
- end
-
- # 0 = "", since a URI with a path must start with '/'
- # 1 = '*' or the module name
- case split_path[ 1 ]
- 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 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
- end
- when nil
- raise ArgumentError, "Bad bindings uri, the #{uri} has neither module name or wildcard '*' in its first path position"
- else
- # 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 Puppet::FileSystem::File.exist?(File.join(mod.path, split_path[ 2..-1 ], 'hiera.yaml' ))
- result << URI.parse('module-hiera:' + uri.path)
- end
- end
- else
- # assume it exists (do not give error since it may be excluded later)
- result << URI.parse('module-hiera:' + File.join(split_path))
- end
- end
- result
- end
-
- # Expands URIs with wildcards and checks optionality.
- # @param uri [URI] the uri to possibly expand
- # @return [Array<URI>] the URIs to exclude
- # @api public
- #
- def expand_excluded(uri, composer)
- result = []
- split_path = uri.path.split('/')
- if split_path.size > 1 && split_path[-1].empty?
- split_path.delete_at(-1)
- end
-
- # 0 = "", since a URI with a path must start with '/'
- # 1 = '*' or the module name
- case split_path[ 1 ]
- 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 |
- path_parts =["", mod.name] + split_path[2..-1]
- result << URI.parse('module-hiera:'+File.join(path_parts))
- end
-
- when nil
- raise ArgumentError, "Bad bindings uri, the #{uri} has neither module name or wildcard '*' in its first path position"
- else
- # create a clean copy (get rid of optional, fragments etc. and a trailing "/")
- result << URI.parse('module-hiera:' + File.join(split_path))
- end
- result
- end
-end
diff --git a/lib/puppet/pops/binder/scheme_handler/symbolic_scheme.rb b/lib/puppet/pops/binder/scheme_handler/symbolic_scheme.rb
index af8c9c786..dc258112d 100644
--- a/lib/puppet/pops/binder/scheme_handler/symbolic_scheme.rb
+++ b/lib/puppet/pops/binder/scheme_handler/symbolic_scheme.rb
@@ -1,54 +1,53 @@
# Abstract base class for schemes based on symbolic names of bindings.
# This class helps resolve symbolic names by computing a path from a fully qualified name (fqn).
# There are also helper methods do determine if the symbolic name contains a wild-card ('*') in the first
# portion of the fqn (with the convention that '*' means 'any module').
#
# @abstract
# @api public
#
class Puppet::Pops::Binder::SchemeHandler::SymbolicScheme < Puppetx::Puppet::BindingsSchemeHandler
# Shared implementation for module: and confdir: since the distinction is only in checks if a symbolic name
# exists as a loadable file or not. Once this method is called it is assumed that the name is relative
# and that it should exist relative to some loadable ruby location.
#
# TODO: this needs to be changed once ARM-8 Puppet DSL concrete syntax is also supported.
# @api public
#
def contributed_bindings(uri, scope, composer)
fqn = fqn_from_path(uri)[1]
bindings = Puppet::Pops::Binder::BindingsLoader.provide(scope, fqn)
raise ArgumentError, "Cannot load bindings '#{uri}' - no bindings found." unless bindings
- # Must clone as the the rest mutates the model
+ # Must clone as the rest mutates the model
cloned_bindings = Marshal.load(Marshal.dump(bindings))
- # Give no effective categories (i.e. ok with whatever categories there is)
- Puppet::Pops::Binder::BindingsFactory.contributed_bindings(fqn, cloned_bindings, nil)
+ Puppet::Pops::Binder::BindingsFactory.contributed_bindings(fqn, cloned_bindings)
end
# @api private
def fqn_from_path(uri)
split_path = uri.path.split('/')
if split_path.size > 1 && split_path[-1].empty?
split_path.delete_at(-1)
end
fqn = split_path[ 1 ]
raise ArgumentError, "Module scheme binding reference has no name." unless fqn
split_name = fqn.split('::')
# drop leading '::'
split_name.shift if split_name[0] && split_name[0].empty?
[split_name, split_name.join('::')]
end
# True if super thinks it is optional or if it contains a wildcard.
# @return [Boolean] true if the uri represents an optional set of bindings.
# @api public
def is_optional?(uri)
super(uri) || has_wildcard?(uri)
end
# @api private
def has_wildcard?(uri)
(path = uri.path) && path.split('/')[1].start_with?('*::')
end
end
diff --git a/lib/puppet/pops/binder/system_bindings.rb b/lib/puppet/pops/binder/system_bindings.rb
index 557074825..33bbd71f9 100644
--- a/lib/puppet/pops/binder/system_bindings.rb
+++ b/lib/puppet/pops/binder/system_bindings.rb
@@ -1,72 +1,60 @@
class Puppet::Pops::Binder::SystemBindings
# Constant with name used for bindings used during initialization of injector
ENVIRONMENT_BOOT_BINDINGS_NAME = 'puppet::env::injector::boot'
Factory = Puppet::Pops::Binder::BindingsFactory
@extension_bindings = Factory.named_bindings("puppet::extensions")
@default_bindings = Factory.named_bindings("puppet::default")
# Bindings in effect when real injector is created
@injector_boot_bindings = Factory.named_bindings("puppet::injector_boot")
def self.extensions()
@extension_bindings
end
def self.default_bindings()
@default_bindings
end
def self.injector_boot_bindings()
@injector_boot_bindings
end
-# def self.env_boot_bindings()
-# Puppet::Bindings[Puppet::Pops::Binder::SystemBindings::ENVIRONMENT_BOOT_BINDINGS_NAME]
-# end
-
def self.final_contribution
- effective_categories = Factory.categories([['common', 'true']])
- Factory.contributed_bindings("puppet-final", [deep_clone(@extension_bindings.model)], effective_categories)
+ Factory.contributed_bindings("puppet-final", [deep_clone(@extension_bindings.model)])
end
def self.default_contribution
- effective_categories = Factory.categories([['common', 'true']])
- Factory.contributed_bindings("puppet-default", [deep_clone(@default_bindings.model)], effective_categories)
+ Factory.contributed_bindings("puppet-default", [deep_clone(@default_bindings.model)])
end
def self.injector_boot_contribution(env_boot_bindings)
# Compose the injector_boot_bindings contributed from the puppet runtime book (i.e. defaults for
# extensions that should be active in the boot injector - see Puppetx initialization.
#
bindings = [deep_clone(@injector_boot_bindings.model), deep_clone(@injector_default_bindings)]
# Add the bindings that come from the bindings_composer as it may define custom extensions added in the bindings
# configuration. (i.e. bindings required to be able to lookup using bindings schemes and backends when
# configuring the real injector).
#
bindings << env_boot_bindings unless env_boot_bindings.nil?
- # Use an 'extension' category for extension bindings to allow them to override the default
- # bindings since they are placed in the same layer (this is done to avoid having a separate layer).
- # The purpose for allowing overrides is that someone may want to replace say 'yaml' with a different version,
- # (say one that uses a YAML implementation that actually works ok in ruby 1.8.7 ;-)), an encrypted parser, etc.
- effective_categories = Factory.categories([['extension', 'true'],['common', 'true']])
-
- # return the composition and the cateogires.
- Factory.contributed_bindings("puppet-injector-boot", bindings, effective_categories)
+ # return the composition
+ Factory.contributed_bindings("puppet-injector-boot", bindings)
end
def self.factory()
Puppet::Pops::Binder::BindingsFactory
end
def self.type_factory()
Puppet::Pops::Types::TypeFactory
end
private
def self.deep_clone(o)
Marshal.load(Marshal.dump(o))
end
end
diff --git a/lib/puppet/pops/containment.rb b/lib/puppet/pops/containment.rb
index a019044b0..6f38f0c1b 100644
--- a/lib/puppet/pops/containment.rb
+++ b/lib/puppet/pops/containment.rb
@@ -1,37 +1,104 @@
# FIXME: This module should be updated when a newer version of RGen (>0.6.2) adds required meta model "e-method" supports.
#
+require 'rgen/ecore/ecore'
module Puppet::Pops::Containment
# Returns Enumerable, thus allowing
# some_element.eAllContents each {|contained| }
# This is a depth first enumeration where parent appears before children.
# @note the top-most object itself is not included in the enumeration, only what it contains.
def eAllContents
EAllContentsEnumerator.new(self)
end
+ def eAllContainers
+ EAllContainersEnumerator.new(self)
+ end
+
+ class EAllContainersEnumerator
+ include Enumerable
+
+ def initialize o
+ @element = o
+ end
+
+ def each &block
+ if block_given?
+ eAllContainers(@element, &block)
+ else
+ self
+ end
+ end
+
+ def eAllContainers(element, &block)
+ x = element.eContainer
+ while !x.nil? do
+ yield x
+ x = x.eContainer
+ end
+ end
+
+ end
+
class EAllContentsEnumerator
include Enumerable
def initialize o
@element = o
+ @@cache ||= {}
end
def each &block
if block_given?
eAllContents(@element, &block)
@element
else
self
end
end
def eAllContents(element, &block)
- element.class.ecore.eAllReferences.select{|r| r.containment}.each do |r|
- children = element.getGenericAsArray(r.name)
- children.each do |c|
- block.call(c)
- eAllContents(c, &block)
+ # This method is performance critical and code has been manually in-lined.
+ # Resist the urge to make this pretty.
+ # The slow way is element.eAllContainments.each {|c| element.getGenericsAsArray(c.name) }
+ #
+ (@@cache[element.class] || all_containment_getters(element)).each do |r|
+ children = element.send(r)
+ if children.is_a?(Array)
+ children.each do |c|
+ yield c
+ eAllContents(c, &block)
+ end
+ elsif !children.nil?
+ yield children
+ eAllContents(children, &block)
+ end
+ end
+ end
+
+ private
+
+ def all_containment_getters(element)
+ elem_class = element.class
+ containments = []
+ collect_getters(elem_class.ecore, containments)
+ @@cache[elem_class] = containments
+ end
+
+ def collect_getters(eclass, containments)
+ eclass.eStructuralFeatures.select {|r| r.is_a?(RGen::ECore::EReference) && r.containment}.each do |r|
+ n = r.name
+ containments << :"get#{n[0..0].upcase + ( n[1..-1] || "" )}"
end
+ eclass.eSuperTypes.each do |t|
+ if cached = @@cache[ t.instanceClass ]
+ containments.concat(cached)
+ else
+ super_containments = []
+ collect_getters(t, super_containments)
+ @@cache[ t.instanceClass ] = super_containments
+ containments.concat(super_containments)
+ end
end
end
+
end
end
diff --git a/lib/puppet/pops/evaluator/access_operator.rb b/lib/puppet/pops/evaluator/access_operator.rb
new file mode 100644
index 000000000..cfc586706
--- /dev/null
+++ b/lib/puppet/pops/evaluator/access_operator.rb
@@ -0,0 +1,548 @@
+# AccessOperator handles operator []
+# This operator is part of evaluation.
+#
+class Puppet::Pops::Evaluator::AccessOperator
+ # Provides access to the Puppet 3.x runtime (scope, etc.)
+ # This separation has been made to make it easier to later migrate the evaluator to an improved runtime.
+ #
+ include Puppet::Pops::Evaluator::Runtime3Support
+
+ Issues = Puppet::Pops::Issues
+ TYPEFACTORY = Puppet::Pops::Types::TypeFactory
+
+ attr_reader :semantic
+
+ # Initialize with AccessExpression to enable reporting issues
+ # @param access_expression [Puppet::Pops::Model::AccessExpression] the semantic object being evaluated
+ # @return [void]
+ #
+ def initialize(access_expression)
+ @@access_visitor ||= Puppet::Pops::Visitor.new(self, "access", 2, nil)
+ @semantic = access_expression
+ end
+
+ def access (o, scope, *keys)
+ @@access_visitor.visit_this_2(self, o, scope, keys)
+ end
+
+ protected
+
+ def access_Object(o, scope, keys)
+ fail(Issues::OPERATOR_NOT_APPLICABLE, @semantic.left_expr, :operator=>'[]', :left_value => o)
+ end
+
+ def access_String(o, scope, keys)
+ keys.flatten!
+ result = case keys.size
+ when 0
+ fail(Puppet::Pops::Issues::BAD_STRING_SLICE_ARITY, @semantic.left_expr, {:actual => keys.size})
+ when 1
+ # Note that Ruby 1.8.7 requires a length of 1 to produce a String
+ k1 = coerce_numeric(keys[0], @semantic.keys, scope)
+ bad_access_key_type(o, 0, k1, Integer) unless k1.is_a?(Integer)
+ k2 = 1
+ k1 = k1 < 0 ? o.length + k1 : k1 # abs pos
+ # if k1 is outside, a length of 1 always produces an empty string
+ if k1 < 0
+ ''
+ else
+ o[ k1, k2 ]
+ end
+ when 2
+ k1 = coerce_numeric(keys[0], @semantic.keys, scope)
+ k2 = coerce_numeric(keys[1], @semantic.keys, scope)
+ [k1, k2].each_with_index { |k,i| bad_access_key_type(o, i, k, Integer) unless k.is_a?(Integer) }
+
+ k1 = k1 < 0 ? o.length + k1 : k1 # abs pos (negative is count from end)
+ k2 = k2 < 0 ? o.length - k1 + k2 + 1 : k2 # abs length (negative k2 is length from pos to end count)
+ # if k1 is outside, adjust to first position, and adjust length
+ if k1 < 0
+ k2 = k2 + k1
+ k1 = 0
+ end
+ o[ k1, k2 ]
+ else
+ fail(Puppet::Pops::Issues::BAD_STRING_SLICE_ARITY, @semantic.left_expr, {:actual => keys.size})
+ end
+ # Specified as: an index outside of range, or empty result == empty string
+ (result.nil? || result.empty?) ? '' : result
+ end
+
+ # Parameterizes a PRegexp Type with a pattern string or r ruby egexp
+ #
+ def access_PRegexpType(o, scope, keys)
+ keys.flatten!
+ unless keys.size == 1
+ blamed = keys.size == 0 ? @semantic : @semantic.keys[2]
+ fail(Puppet::Pops::Issues::BAD_TYPE_SLICE_ARITY, blamed, :base_type => o, :min=>1, :actual => keys.size)
+ end
+ assert_keys(keys, o, 1, 1, String, Regexp)
+ Puppet::Pops::Types::TypeFactory.regexp(*keys)
+ end
+
+ # Evaluates <ary>[] with 1 or 2 arguments. One argument is an index lookup, two arguments is a slice from/to.
+ #
+ def access_Array(o, scope, keys)
+ keys.flatten!
+ case keys.size
+ when 0
+ fail(Puppet::Pops::Issues::BAD_ARRAY_SLICE_ARITY, @semantic.left_expr, {:actual => keys.size})
+ when 1
+ k = coerce_numeric(keys[0], @semantic.keys[0], scope)
+ unless k.is_a?(Integer)
+ bad_access_key_type(o, 0, k, Integer)
+ end
+ o[k]
+ when 2
+ # A slice [from, to] with support for -1 to mean start, or end respectively.
+ k1 = coerce_numeric(keys[0], @semantic.keys[0], scope)
+ k2 = coerce_numeric(keys[1], @semantic.keys[1], scope)
+
+ [k1, k2].each_with_index { |k,i| bad_access_key_type(o, i, k, Integer) unless k.is_a?(Integer) }
+
+ # Help confused Ruby do the right thing (it truncates to the right, but negative index + length can never overlap
+ # the available range.
+ k1 = k1 < 0 ? o.length + k1 : k1 # abs pos (negative is count from end)
+ k2 = k2 < 0 ? o.length - k1 + k2 + 1 : k2 # abs length (negative k2 is length from pos to end count)
+ # if k1 is outside, adjust to first position, and adjust length
+ if k1 < 0
+ k2 = k2 + k1
+ k1 = 0
+ end
+ # Help ruby always return empty array when asking for a sub array
+ result = o[ k1, k2 ]
+ result.nil? ? [] : result
+ else
+ fail(Puppet::Pops::Issues::BAD_ARRAY_SLICE_ARITY, @semantic.left_expr, {:actual => keys.size})
+ end
+ end
+
+
+ # Evaluates <hsh>[] with support for one or more arguments. If more than one argument is used, the result
+ # is an array with each lookup.
+ # @note
+ # Does not flatten its keys to enable looking up with a structure
+ #
+ def access_Hash(o, scope, keys)
+ # Look up key in hash, if key is nil or :undef, try alternate form before giving up.
+ # This makes :undef and nil "be the same key". (The alternative is to always only write one or the other
+ # in all hashes - that is much harder to guarantee since the Hash is a regular Ruby hash.
+ #
+ result = keys.collect do |k|
+ o.fetch(k) do |key|
+ case key
+ when nil
+ o[:undef]
+ when :undef
+ o[:nil]
+ else
+ nil
+ end
+ end
+ end
+ case result.size
+ when 0
+ fail(Puppet::Pops::Issues::BAD_HASH_SLICE_ARITY, @semantic.left_expr, {:actual => keys.size})
+ when 1
+ result.pop
+ else
+ # remove nil elements and return
+ result.compact!
+ result
+ end
+ end
+
+ # Ruby does not have an infinity constant. TODO: Consider having one constant in Puppet. Now it is in several places.
+ INFINITY = 1.0 / 0.0
+
+ def access_PEnumType(o, scope, keys)
+ keys.flatten!
+ assert_keys(keys, o, 1, INFINITY, String)
+ Puppet::Pops::Types::TypeFactory.enum(*keys)
+ end
+
+ def access_PVariantType(o, scope, keys)
+ keys.flatten!
+ assert_keys(keys, o, 1, INFINITY, Puppet::Pops::Types::PAbstractType)
+ Puppet::Pops::Types::TypeFactory.variant(*keys)
+ end
+
+ def access_PTupleType(o, scope, keys)
+ keys.flatten!
+ if TYPEFACTORY.is_range_parameter?(keys[-2]) && TYPEFACTORY.is_range_parameter?(keys[-1])
+ size_type = TYPEFACTORY.range(keys[-2], keys[-1])
+ keys = keys[0, keys.size - 2]
+ elsif TYPEFACTORY.is_range_parameter?(keys[-1])
+ size_type = TYPEFACTORY.range(keys[-1], :default)
+ keys = keys[0, keys.size - 1]
+ end
+ assert_keys(keys, o, 1, INFINITY, Puppet::Pops::Types::PAbstractType)
+ t = Puppet::Pops::Types::TypeFactory.tuple(*keys)
+ # set size type, or nil for default (exactly 1)
+ t.size_type = size_type
+ t
+ end
+
+ def access_PStructType(o, scope, keys)
+ assert_keys(keys, o, 1, 1, Hash)
+ TYPEFACTORY.struct(keys[0])
+ end
+
+ def access_PStringType(o, scope, keys)
+ keys.flatten!
+ case keys.size
+ when 1
+ size_t = collection_size_t(0, keys[0])
+ when 2
+ size_t = collection_size_t(0, keys[0], keys[1])
+ else
+ fail(Puppet::Pops::Issues::BAD_STRING_SLICE_ARITY, @semantic, {:actual => keys.size})
+ end
+ string_t = Puppet::Pops::Types::TypeFactory.string()
+ string_t.size_type = size_t
+ string_t
+ end
+
+ # Asserts type of each key and calls fail with BAD_TYPE_SPECIFICATION
+ # @param keys [Array<Object>] the evaluated keys
+ # @param o [Object] evaluated LHS reported as :base_type
+ # @param min [Integer] the minimum number of keys (typically 1)
+ # @param max [Numeric] the maximum number of keys (use same as min, specific number, or INFINITY)
+ # @param allowed_classes [Class] a variable number of classes that each key must be an instance of (any)
+ # @api private
+ #
+ def assert_keys(keys, o, min, max, *allowed_classes)
+ size = keys.size
+ unless size.between?(min, max || INFINITY)
+ fail(Puppet::Pops::Issues::BAD_TYPE_SLICE_ARITY, blamed, :base_type => o, :min=>1, :max => max, :actual => keys.size)
+ end
+ keys.each_with_index do |k, i|
+ unless allowed_classes.any? {|clazz| k.is_a?(clazz) }
+ bad_type_specialization_key_type(o, i, k, *allowed_classes)
+ end
+ end
+ end
+
+ def bad_access_key_type(lhs, key_index, actual, *expected_classes)
+ fail(Puppet::Pops::Issues::BAD_SLICE_KEY_TYPE, @semantic.keys[key_index], {
+ :left_value => lhs,
+ :actual => bad_key_type_name(actual),
+ :expected_classes => expected_classes
+ })
+ end
+
+ def bad_key_type_name(actual)
+ case actual
+ when nil, :undef
+ 'Undef'
+ when :default
+ 'Default'
+ else
+ actual.class.name
+ end
+ end
+
+ def bad_type_specialization_key_type(type, key_index, actual, *expected_classes)
+ label_provider = Puppet::Pops::Model::ModelLabelProvider.new()
+ expected = expected_classes.map {|c| label_provider.label(c) }.join(' or ')
+ fail(Puppet::Pops::Issues::BAD_TYPE_SPECIALIZATION, @semantic.keys[key_index], {
+ :type => type,
+ :message => "Cannot use #{bad_key_type_name(actual)} where #{expected} is expected"
+ })
+ end
+
+ def access_PPatternType(o, scope, keys)
+ keys.flatten!
+ assert_keys(keys, o, 1, INFINITY, String, Regexp, Puppet::Pops::Types::PPatternType, Puppet::Pops::Types::PRegexpType)
+ Puppet::Pops::Types::TypeFactory.pattern(*keys)
+ end
+
+ def access_POptionalType(o, scope, keys)
+ keys.flatten!
+ if keys.size == 1
+ unless keys[0].is_a?(Puppet::Pops::Types::PAbstractType)
+ fail(Puppet::Pops::Issues::BAD_TYPE_SLICE_TYPE, @semantic.keys[0], {:base_type => 'Optional-Type', :actual => keys[0].class})
+ end
+ result = Puppet::Pops::Types::POptionalType.new()
+ result.optional_type = keys[0]
+ result
+ else
+ fail(Puppet::Pops::Issues::BAD_TYPE_SLICE_ARITY, @semantic, {:base_type => 'Optional-Type', :min => 1, :actual => keys.size})
+ end
+ end
+
+ def access_PType(o, scope, keys)
+ keys.flatten!
+ if keys.size == 1
+ unless keys[0].is_a?(Puppet::Pops::Types::PAbstractType)
+ fail(Puppet::Pops::Issues::BAD_TYPE_SLICE_TYPE, @semantic.keys[0], {:base_type => 'Type-Type', :actual => keys[0].class})
+ end
+ result = Puppet::Pops::Types::PType.new()
+ result.type = keys[0]
+ result
+ else
+ fail(Puppet::Pops::Issues::BAD_TYPE_SLICE_ARITY, @semantic, {:base_type => 'Type-Type', :min => 1, :actual => keys.size})
+ end
+ end
+
+ def access_PRubyType(o, scope, keys)
+ keys.flatten!
+ assert_keys(keys, o, 1, 1, String)
+ # create ruby type based on name of class, not inference of key's type
+ Puppet::Pops::Types::TypeFactory.ruby_type(keys[0])
+ end
+
+ def access_PIntegerType(o, scope, keys)
+ keys.flatten!
+ unless keys.size.between?(1, 2)
+ fail(Puppet::Pops::Issues::BAD_INTEGER_SLICE_ARITY, @semantic, {:actual => keys.size})
+ end
+ keys.each_with_index do |x, index|
+ fail(Puppet::Pops::Issues::BAD_INTEGER_SLICE_TYPE, @semantic.keys[index],
+ {:actual => x.class}) unless (x.is_a?(Integer) || x == :default)
+ end
+ ranged_integer = Puppet::Pops::Types::PIntegerType.new()
+ from, to = keys
+ ranged_integer.from = from == :default ? nil : from
+ ranged_integer.to = to == :default ? nil : to
+ ranged_integer
+ end
+
+ def access_PFloatType(o, scope, keys)
+ keys.flatten!
+ unless keys.size.between?(1, 2)
+ fail(Puppet::Pops::Issues::BAD_FLOAT_SLICE_ARITY, @semantic, {:actual => keys.size})
+ end
+ keys.each_with_index do |x, index|
+ fail(Puppet::Pops::Issues::BAD_FLOAT_SLICE_TYPE, @semantic.keys[index],
+ {:actual => x.class}) unless (x.is_a?(Float) || x.is_a?(Integer) || x == :default)
+ end
+ ranged_float = Puppet::Pops::Types::PFloatType.new()
+ from, to = keys
+ ranged_float.from = from == :default || from.nil? ? nil : Float(from)
+ ranged_float.to = to == :default || to.nil? ? nil : Float(to)
+ ranged_float
+ end
+
+ # A Hash can create a new Hash type, one arg sets value type, two args sets key and value type in new type.
+ # With 3 or 4 arguments, these are used to create a size constraint.
+ # It is not possible to create a collection of Hash types directly.
+ #
+ def access_PHashType(o, scope, keys)
+ keys.flatten!
+ keys[0,2].each_with_index do |k, index|
+ unless k.is_a?(Puppet::Pops::Types::PAbstractType)
+ fail(Puppet::Pops::Issues::BAD_TYPE_SLICE_TYPE, @semantic.keys[index], {:base_type => 'Hash-Type', :actual => k.class})
+ end
+ end
+ case keys.size
+ when 1
+ result = Puppet::Pops::Types::PHashType.new()
+ result.key_type = o.key_type.copy
+ result.element_type = keys[0]
+ result
+ when 2
+ result = Puppet::Pops::Types::PHashType.new()
+ result.key_type = keys[0]
+ result.element_type = keys[1]
+ result
+ when 3
+ result = Puppet::Pops::Types::PHashType.new()
+ result.key_type = keys[0]
+ result.element_type = keys[1]
+ size_t = collection_size_t(1, keys[2])
+ result
+ when 4
+ result = Puppet::Pops::Types::PHashType.new()
+ result.key_type = keys[0]
+ result.element_type = keys[1]
+ size_t = collection_size_t(1, keys[2], keys[3])
+ result
+ else
+ fail(Puppet::Pops::Issues::BAD_TYPE_SLICE_ARITY, @semantic, {
+ :base_type => 'Hash-Type', :min => 1, :max => 4, :actual => keys.size
+ })
+ end
+ result.size_type = size_t if size_t
+ result
+ end
+
+ # CollectionType is parameterized with a range
+ def access_PCollectionType(o, scope, keys)
+ keys.flatten!
+ case keys.size
+ when 1
+ size_t = collection_size_t(1, keys[0])
+ when 2
+ size_t = collection_size_t(1, keys[0], keys[1])
+ else
+ fail(Puppet::Pops::Issues::BAD_TYPE_SLICE_ARITY, @semantic,
+ {:base_type => 'Collection-Type', :min => 1, :max => 2, :actual => keys.size})
+ end
+ result = Puppet::Pops::Types::PCollectionType.new()
+ result.size_type = size_t
+ result
+ end
+
+ # An Array can create a new Array type. It is not possible to create a collection of Array types.
+ #
+ def access_PArrayType(o, scope, keys)
+ keys.flatten!
+ case keys.size
+ when 1
+ size_t = nil
+ when 2
+ size_t = collection_size_t(1, keys[1])
+ when 3
+ size_t = collection_size_t(1, keys[1], keys[2])
+ else
+ fail(Puppet::Pops::Issues::BAD_TYPE_SLICE_ARITY, @semantic,
+ {:base_type => 'Array-Type', :min => 1, :max => 3, :actual => keys.size})
+ end
+ unless keys[0].is_a?(Puppet::Pops::Types::PAbstractType)
+ fail(Puppet::Pops::Issues::BAD_TYPE_SLICE_TYPE, @semantic.keys[0], {:base_type => 'Array-Type', :actual => keys[0].class})
+ end
+ result = Puppet::Pops::Types::PArrayType.new()
+ result.element_type = keys[0]
+ result.size_type = size_t
+ result
+ end
+
+ # Produces an PIntegerType (range) given one or two keys.
+ def collection_size_t(start_index, *keys)
+ if keys.size == 1 && keys[0].is_a?(Puppet::Pops::Types::PIntegerType)
+ keys[0].copy
+ else
+ keys.each_with_index do |x, index|
+ fail(Puppet::Pops::Issues::BAD_COLLECTION_SLICE_TYPE, @semantic.keys[start_index + index],
+ {:actual => x.class}) unless (x.is_a?(Integer) || x == :default)
+ end
+ ranged_integer = Puppet::Pops::Types::PIntegerType.new()
+ from, to = keys
+ ranged_integer.from = from == :default ? nil : from
+ ranged_integer.to = to == :default ? nil : to
+ ranged_integer
+ end
+ end
+
+ # A Resource can create a new more specific Resource type, and/or an array of resource types
+ # If the given type has title set, it can not be specified further.
+ # @example
+ # Resource[File] # => File
+ # Resource[File, 'foo'] # => File[foo]
+ # Resource[File. 'foo', 'bar'] # => [File[foo], File[bar]]
+ # File['foo', 'bar'] # => [File[foo], File[bar]]
+ # File['foo']['bar'] # => Value of the 'bar' parameter in the File['foo'] resource
+ # Resource[File]['foo', 'bar'] # => [File[Foo], File[bar]]
+ # Resource[File, 'foo', 'bar'] # => [File[foo], File[bar]]
+ # Resource[File, 'foo']['bar'] # => Value of the 'bar' parameter in the File['foo'] resource
+ #
+ def access_PResourceType(o, scope, keys)
+ keys.flatten!
+ if keys.size == 0
+ fail(Puppet::Pops::Issues::BAD_TYPE_SLICE_ARITY, o,
+ :base_type => Puppet::Pops::Types::TypeCalculator.new().string(o), :min => 1, :actual => 0)
+ end
+ if !o.title.nil?
+ # lookup resource and return one or more parameter values
+ resource = find_resource(scope, o.type_name, o.title)
+ unless resource
+ fail(Puppet::Pops::Issues::UNKNOWN_RESOURCE, @semantic, {:type_name => o.type_name, :title => o.title})
+ end
+ result = keys.map do |k|
+ unless is_parameter_of_resource?(scope, resource, k)
+ fail(Puppet::Pops::Issues::UNKNOWN_RESOURCE_PARAMETER, @semantic,
+ {:type_name => o.type_name, :title => o.title, :param_name=>k})
+ end
+ get_resource_parameter_value(scope, resource, k)
+ end
+ return result.size <= 1 ? result.pop : result
+ end
+
+ # type_name is LHS type_name if set, else the first given arg
+ keys_orig_size = keys.size
+ type_name = o.type_name || keys.shift
+ type_name = case type_name
+ when Puppet::Pops::Types::PResourceType
+ type_name.type_name
+ when String
+ type_name.downcase
+ else
+ blame = keys_orig_size != keys.size ? @semantic.keys[0] : @semantic.left_expr
+ fail(Puppet::Pops::Issues::ILLEGAL_RESOURCE_SPECIALIZATION, blame, {:actual => type_name.class})
+ end
+
+ keys = [:no_title] if keys.size < 1 # if there was only a type_name and it was consumed
+ result = keys.each_with_index.map do |t, i|
+ unless t.is_a?(String) || t == :no_title
+ type_to_report = case t
+ when nil, :undef
+ 'Undef'
+ when :default
+ 'Default'
+ else
+ t.class.name
+ end
+ index = keys_orig_size != keys.size ? i+1 : i
+ fail(Puppet::Pops::Issues::BAD_TYPE_SPECIALIZATION, @semantic.keys[index], {
+ :type => o,
+ :message => "Cannot use #{type_to_report} where String is expected"
+ })
+ end
+
+ rtype = Puppet::Pops::Types::PResourceType.new()
+ rtype.type_name = type_name
+ rtype.title = (t == :no_title ? nil : t)
+ rtype
+ end
+ # returns single type as type, else an array of types
+ result.size == 1 ? result.pop : result
+ end
+
+ def access_PHostClassType(o, scope, keys)
+ keys.flatten!
+ if keys.size == 0
+ fail(Puppet::Pops::Issues::BAD_TYPE_SLICE_ARITY, o,
+ :base_type => Puppet::Pops::Types::TypeCalculator.new().string(o), :min => 1, :actual => 0)
+ end
+ if ! o.class_name.nil?
+ # lookup class resource and return one or more parameter values
+ resource = find_resource(scope, 'class', o.class_name)
+ unless resource
+ fail(Puppet::Pops::Issues::UNKNOWN_RESOURCE, @semantic, {:type_name => 'Class', :title => o.class_name})
+ end
+ result = keys.map do |k|
+ unless is_parameter_of_resource?(scope, resource, k)
+ fail(Puppet::Pops::Issues::UNKNOWN_RESOURCE_PARAMETER, @semantic,
+ {:type_name => 'Class', :title => o.class_name, :param_name=>k})
+ end
+ get_resource_parameter_value(scope, resource, k)
+ end
+ return result.size <= 1 ? result.pop : result
+ # TODO: if [] is applied to specific class, it should be treated the same as getting
+ # a resource parameter. Now it fails the operation
+ #
+ fail(Puppet::Pops::Issues::ILLEGAL_TYPE_SPECIALIZATION, semantic.left_expr, {:kind => 'Class'})
+ end
+ # The type argument may be a Resource Type - the Puppet Language allows a reference such as
+ # Class[Foo], and this is interpreted as Class[Resource[Foo]] - which is ok as long as the resource
+ # does not have a title. This should probably be deprecated.
+ #
+ result = keys.each_with_index.map do |c, i|
+ ctype = Puppet::Pops::Types::PHostClassType.new()
+ if c.is_a?(Puppet::Pops::Types::PResourceType) && !c.type_name.nil? && c.title.nil?
+ c = c.type_name.downcase
+ end
+ unless c.is_a?(String)
+ fail(Puppet::Pops::Issues::ILLEGAL_HOSTCLASS_NAME, @semantic.keys[i], {:name => c})
+ end
+ if c !~ Puppet::Pops::Patterns::NAME
+ fail(Issues::ILLEGAL_NAME, @semantic.keys[i], {:name=>c})
+ end
+ ctype.class_name = c
+ ctype
+ end
+ # returns single type as type, else an array of types
+ result.size == 1 ? result.pop : result
+ end
+end
diff --git a/lib/puppet/pops/evaluator/closure.rb b/lib/puppet/pops/evaluator/closure.rb
new file mode 100644
index 000000000..547e15181
--- /dev/null
+++ b/lib/puppet/pops/evaluator/closure.rb
@@ -0,0 +1,57 @@
+
+# A Closure represents logic bound to a particular scope.
+# As long as the runtime (basically the scope implementation) has the behaviour of Puppet 3x it is not
+# safe to use this closure when the scope given to it when initialized goes "out of scope".
+#
+# Note that the implementation is backwards compatible in that the call method accepts a scope, but this
+# scope is not used.
+#
+class Puppet::Pops::Evaluator::Closure
+ attr_reader :evaluator
+ attr_reader :model
+ attr_reader :enclosing_scope
+
+ def initialize(evaluator, model, scope)
+ @evaluator = evaluator
+ @model = model
+ @enclosing_scope = scope
+ end
+
+ # marker method checked with respond_to :puppet_lambda
+ def puppet_lambda()
+ true
+ end
+
+ # compatible with 3x AST::Lambda
+ def call(scope, *args)
+ @evaluator.call(self, args, @enclosing_scope)
+ end
+
+ # Call closure with argument assignment by name
+ def call_by_name(scope, args_hash, spill_over = false)
+ @evaluator.call_by_name(self, args_hash, @enclosing_scope, spill_over)
+ end
+
+ # incompatible with 3x except that it is an array of the same size
+ def parameters()
+ @model.parameters || []
+ end
+
+ # Returns the number of parameters (required and optional)
+ # @return [Integer] the total number of accepted parameters
+ def parameter_count
+ # yes, this is duplication of code, but it saves a method call
+ (@model.parameters || []).size
+ end
+
+ # Returns the number of optional parameters.
+ # @return [Integer] the number of optional accepted parameters
+ def optional_parameter_count
+ @model.parameters.count { |p| !p.value.nil? }
+ end
+
+ def parameter_names
+ @model.parameters.collect {|p| p.name }
+ end
+
+end
diff --git a/lib/puppet/pops/evaluator/compare_operator.rb b/lib/puppet/pops/evaluator/compare_operator.rb
new file mode 100644
index 000000000..df5730d17
--- /dev/null
+++ b/lib/puppet/pops/evaluator/compare_operator.rb
@@ -0,0 +1,168 @@
+# Compares the puppet DSL way
+#
+# ==Equality
+# All string vs. numeric equalities check for numeric equality first, then string equality
+# Arrays are equal to arrays if they have the same length, and each element #equals
+# Hashes are equal to hashes if they have the same size and keys and values #equals.
+# All other objects are equal if they are ruby #== equal
+#
+class Puppet::Pops::Evaluator::CompareOperator
+ include Puppet::Pops::Utils
+
+ def initialize
+ @@equals_visitor ||= Puppet::Pops::Visitor.new(self, "equals", 1, 1)
+ @@compare_visitor ||= Puppet::Pops::Visitor.new(self, "cmp", 1, 1)
+ @@include_visitor ||= Puppet::Pops::Visitor.new(self, "include", 1, 1)
+ @type_calculator = Puppet::Pops::Types::TypeCalculator.new()
+ end
+
+ def equals (a, b)
+ @@equals_visitor.visit_this_1(self, a, b)
+ end
+
+ # Performs a comparison of a and b, and return > 0 if a is bigger, 0 if equal, and < 0 if b is bigger.
+ # Comparison of String vs. Numeric always compares using numeric.
+ def compare(a, b)
+ @@compare_visitor.visit_this_1(self, a, b)
+ end
+
+ # Answers is b included in a
+ def include?(a, b)
+ @@include_visitor.visit_this_1(self, a, b)
+ end
+
+ protected
+
+ def cmp_String(a, b)
+ # if both are numerics in string form, compare as number
+ n1 = Puppet::Pops::Utils.to_n(a)
+ n2 = Puppet::Pops::Utils.to_n(b)
+
+ # Numeric is always lexically smaller than a string, even if the string is empty.
+ return n1 <=> n2 if n1 && n2
+ return -1 if n1 && b.is_a?(String)
+ return 1 if n2
+ return a.casecmp(b) if b.is_a?(String)
+
+ raise ArgumentError.new("A String is not comparable to a non String or Number")
+ end
+
+ # Equality is case independent.
+ def equals_String(a, b)
+ if n1 = Puppet::Pops::Utils.to_n(a)
+ if n2 = Puppet::Pops::Utils.to_n(b)
+ n1 == n2
+ else
+ false
+ end
+ else
+ return false unless b.is_a?(String)
+ a.casecmp(b) == 0
+ end
+ end
+
+ def cmp_Numeric(a, b)
+ if n2 = Puppet::Pops::Utils.to_n(b)
+ a <=> n2
+ elsif b.kind_of(String)
+ # Numeric is always lexiographically smaller than a string, even if the string is empty.
+ -1
+ else
+ raise ArgumentError.new("A Numeric is not comparable to non Numeric or String")
+ end
+ end
+
+ def equals_Numeric(a, b)
+ if n2 = Puppet::Pops::Utils.to_n(b)
+ a == n2
+ else
+ false
+ end
+ end
+
+ def equals_Array(a, b)
+ return false unless b.is_a?(Array) && a.size == b.size
+ a.each_index {|i| return false unless equals(a.slice(i), b.slice(i)) }
+ true
+ end
+
+ def equals_Hash(a, b)
+ return false unless b.is_a?(Hash) && a.size == b.size
+ a.each {|ak, av| return false unless equals(b[ak], av)}
+ true
+ end
+
+ def cmp_Symbol(a, b)
+ if b.is_a?(Symbol)
+ a <=> b
+ else
+ raise ArgumentError.new("Symbol not comparable to non Symbol")
+ end
+ end
+
+ def cmp_Object(a, b)
+ raise ArgumentError.new("Only Strings and Numbers are comparable")
+ end
+
+
+ def equals_Object(a, b)
+ a == b
+ end
+
+ def equals_NilClass(a, b)
+ b.nil? || b == :undef
+ end
+
+ def equals_Symbol(a, b)
+ a == b || a == :undef && b.nil?
+ end
+
+ def include_Object(a, b)
+ false
+ end
+
+ def include_String(a, b)
+ case b
+ when String
+ # subsstring search downcased
+ a.downcase.include?(b.downcase)
+ when Regexp
+ # match (convert to boolean)
+ !!(a =~ b)
+ when Numeric
+ # convert string to number, true if ==
+ equals(a, b)
+ when Puppet::Pops::Types::PStringType
+ # is there a string in a string? (yes, each char is a string, and an empty string contains an empty string)
+ true
+ else
+ if b == Puppet::Pops::Types::PDataType || b == Puppet::Pops::Types::PObjectType
+ # A String is Data and Object (but not of all subtypes of those types).
+ true
+ else
+ false
+ end
+ end
+ end
+
+ def include_Array(a, b)
+ case b
+ when Regexp
+ a.each do |element|
+ next unless element.is_a? String
+ return true if element =~ b
+ end
+ return false
+ when Puppet::Pops::Types::PAbstractType
+ a.each {|element| return true if @type_calculator.instance?(b, element) }
+ return false
+ else
+ a.each {|element| return true if equals(element, b) }
+ return false
+ end
+ end
+
+ def include_Hash(a, b)
+ include?(a.keys, b)
+ end
+end
diff --git a/lib/puppet/pops/evaluator/epp_evaluator.rb b/lib/puppet/pops/evaluator/epp_evaluator.rb
new file mode 100644
index 000000000..31e59aea7
--- /dev/null
+++ b/lib/puppet/pops/evaluator/epp_evaluator.rb
@@ -0,0 +1,87 @@
+# Handler of Epp call/evaluation from the epp and inline_epp functions
+#
+class Puppet::Pops::Evaluator::EppEvaluator
+
+ def self.inline_epp(scope, epp_source, template_args = nil)
+ unless epp_source.is_a? String
+ raise ArgumentError, "inline_epp(): the first argument must be a String with the epp source text, got a #{epp_source.class}"
+ end
+
+ # Parse and validate the source
+ parser = Puppet::Pops::Parser::EvaluatingParser::EvaluatingEppParser.new
+ begin
+ result = parser.parse_string(epp_source, 'inlined-epp-text')
+ rescue Puppet::ParseError => e
+ raise ArgumentError, "inline_epp(): Invalid EPP: #{e.message}"
+ end
+
+ # Evaluate (and check template_args)
+ evaluate(parser, 'inline_epp', scope, false, result, template_args)
+ end
+
+ def self.epp(scope, file, env_name, template_args = nil)
+ unless file.is_a? String
+ raise ArgumentError, "epp(): the first argument must be a String with the filename, got a #{file.class}"
+ end
+
+ file = file + ".epp" unless file =~ /\.epp$/
+ scope.debug "Retrieving epp template #{file}"
+ template_file = Puppet::Parser::Files.find_template(file, env_name)
+ unless template_file
+ raise Puppet::ParseError, "Could not find template '#{file}'"
+ end
+
+ # Parse and validate the source
+ parser = Puppet::Pops::Parser::EvaluatingParser::EvaluatingEppParser.new
+ begin
+ result = parser.parse_file(template_file)
+ rescue Puppet::ParseError => e
+ raise ArgumentError, "epp(): Invalid EPP: #{e.message}"
+ end
+
+ # Evaluate (and check template_args)
+ evaluate(parser, 'epp', scope, true, result, template_args)
+ end
+
+ private
+
+ def self.evaluate(parser, func_name, scope, use_global_scope_only, parse_result, template_args)
+ template_args, template_args_set = handle_template_args(func_name, template_args)
+
+ body = parse_result.body
+ unless body.is_a?(Puppet::Pops::Model::LambdaExpression)
+ raise ArgumentError, "#{func_name}(): the parser did not produce a LambdaExpression, got '#{body.class}'"
+ end
+ unless body.body.is_a?(Puppet::Pops::Model::EppExpression)
+ raise ArgumentError, "#{func_name}(): the parser did not produce an EppExpression, got '#{body.body.class}'"
+ end
+ unless parse_result.definitions.empty?
+ raise ArgumentError, "#{func_name}(): The EPP template contains illegal expressions (definitions)"
+ end
+
+ see_scope = body.body.see_scope
+ if see_scope && !template_args_set
+ # no epp params and no arguments were given => inline_epp logic sees all local variables, epp all global
+ closure_scope = use_global_scope_only ? scope.find_global_scope : scope
+ spill_over = false
+ else
+ # no epp params or user provided arguments in a hash, epp logic only sees global + what was given
+ closure_scope = scope.find_global_scope
+ # given spill over if there are no params (e.g. replace closure scope by a new scope with the given args)
+ spill_over = see_scope
+ end
+ evaluated_result = parser.closure(body, closure_scope).call_by_name(scope, template_args, spill_over)
+ evaluated_result
+ end
+
+ def self.handle_template_args(func_name, template_args)
+ if template_args.nil?
+ [{}, false]
+ else
+ unless template_args.is_a?(Hash)
+ raise ArgumentError, "#{func_name}(): the template_args must be a Hash, got a #{template_args.class}"
+ end
+ [template_args, true]
+ end
+ end
+end
\ No newline at end of file
diff --git a/lib/puppet/pops/evaluator/evaluator_impl.rb b/lib/puppet/pops/evaluator/evaluator_impl.rb
new file mode 100644
index 000000000..07be2a25a
--- /dev/null
+++ b/lib/puppet/pops/evaluator/evaluator_impl.rb
@@ -0,0 +1,1069 @@
+require 'rgen/ecore/ecore'
+require 'puppet/pops/evaluator/compare_operator'
+require 'puppet/pops/evaluator/relationship_operator'
+require 'puppet/pops/evaluator/access_operator'
+require 'puppet/pops/evaluator/closure'
+require 'puppet/pops/evaluator/external_syntax_support'
+
+# This implementation of {Puppet::Pops::Evaluator} performs evaluation using the puppet 3.x runtime system
+# in a manner largely compatible with Puppet 3.x, but adds new features and introduces constraints.
+#
+# The evaluation uses _polymorphic dispatch_ which works by dispatching to the first found method named after
+# the class or one of its super-classes. The EvaluatorImpl itself mainly deals with evaluation (it currently
+# also handles assignment), and it uses a delegation pattern to more specialized handlers of some operators
+# that in turn use polymorphic dispatch; this to not clutter EvaluatorImpl with too much responsibility).
+#
+# Since a pattern is used, only the main entry points are fully documented. The parameters _o_ and _scope_ are
+# the same in all the polymorphic methods, (the type of the parameter _o_ is reflected in the method's name;
+# either the actual class, or one of its super classes). The _scope_ parameter is always the scope in which
+# the evaluation takes place. If nothing else is mentioned, the return is always the result of evaluation.
+#
+# See {Puppet::Pops::Visitable} and {Puppet::Pops::Visitor} for more information about
+# polymorphic calling.
+#
+class Puppet::Pops::Evaluator::EvaluatorImpl
+ include Puppet::Pops::Utils
+
+ # Provides access to the Puppet 3.x runtime (scope, etc.)
+ # This separation has been made to make it easier to later migrate the evaluator to an improved runtime.
+ #
+ include Puppet::Pops::Evaluator::Runtime3Support
+ include Puppet::Pops::Evaluator::ExternalSyntaxSupport
+
+ # This constant is not defined as Float::INFINITY in Ruby 1.8.7 (but is available in later version
+ # Refactor when support is dropped for Ruby 1.8.7.
+ #
+ INFINITY = 1.0 / 0.0
+
+ # Reference to Issues name space makes it easier to refer to issues
+ # (Issues are shared with the validator).
+ #
+ Issues = Puppet::Pops::Issues
+
+ def initialize
+ @@eval_visitor ||= Puppet::Pops::Visitor.new(self, "eval", 1, 1)
+ @@lvalue_visitor ||= Puppet::Pops::Visitor.new(self, "lvalue", 1, 1)
+ @@assign_visitor ||= Puppet::Pops::Visitor.new(self, "assign", 3, 3)
+ @@string_visitor ||= Puppet::Pops::Visitor.new(self, "string", 1, 1)
+
+ @@type_calculator ||= Puppet::Pops::Types::TypeCalculator.new()
+ @@type_parser ||= Puppet::Pops::Types::TypeParser.new()
+
+ @@compare_operator ||= Puppet::Pops::Evaluator::CompareOperator.new()
+ @@relationship_operator ||= Puppet::Pops::Evaluator::RelationshipOperator.new()
+
+ # Initialize the runtime module
+ Puppet::Pops::Evaluator::Runtime3Support.instance_method(:initialize).bind(self).call()
+ end
+
+ # @api private
+ def type_calculator
+ @@type_calculator
+ end
+
+ # Polymorphic evaluate - calls eval_TYPE
+ #
+ # ## Polymorphic evaluate
+ # Polymorphic evaluate calls a method on the format eval_TYPE where classname is the last
+ # part of the class of the given _target_. A search is performed starting with the actual class, continuing
+ # with each of the _target_ class's super classes until a matching method is found.
+ #
+ # # Description
+ # Evaluates the given _target_ object in the given scope, optionally passing a block which will be
+ # called with the result of the evaluation.
+ #
+ # @overload evaluate(target, scope, {|result| block})
+ # @param target [Object] evaluation target - see methods on the pattern assign_TYPE for actual supported types.
+ # @param scope [Object] the runtime specific scope class where evaluation should take place
+ # @return [Object] the result of the evaluation
+ #
+ # @api
+ #
+ def evaluate(target, scope)
+ begin
+ @@eval_visitor.visit_this_1(self, target, scope)
+ rescue StandardError => e
+ if e.is_a? Puppet::ParseError
+ raise e
+ end
+ fail(Issues::RUNTIME_ERROR, target, {:detail => e.message}, e)
+ end
+ end
+
+ # Polymorphic assign - calls assign_TYPE
+ #
+ # ## Polymorphic assign
+ # Polymorphic assign calls a method on the format assign_TYPE where TYPE is the last
+ # part of the class of the given _target_. A search is performed starting with the actual class, continuing
+ # with each of the _target_ class's super classes until a matching method is found.
+ #
+ # # Description
+ # Assigns the given _value_ to the given _target_. The additional argument _o_ is the instruction that
+ # produced the target/value tuple and it is used to set the origin of the result.
+ # @param target [Object] assignment target - see methods on the pattern assign_TYPE for actual supported types.
+ # @param value [Object] the value to assign to `target`
+ # @param o [Puppet::Pops::Model::PopsObject] originating instruction
+ # @param scope [Object] the runtime specific scope where evaluation should take place
+ #
+ # @api
+ #
+ def assign(target, value, o, scope)
+ @@assign_visitor.visit_this_3(self, target, value, o, scope)
+ end
+
+ def lvalue(o, scope)
+ @@lvalue_visitor.visit_this_1(self, o, scope)
+ end
+
+ def string(o, scope)
+ @@string_visitor.visit_this_1(self, o, scope)
+ end
+
+ # Call a closure matching arguments by name - Can only be called with a Closure (for now), may be refactored later
+ # to also handle other types of calls (function calls are also handled by CallNamedFunction and CallMethod, they
+ # could create similar objects to Closure, wait until other types of defines are instantiated - they may behave
+ # as special cases of calls - i.e. 'new').
+ #
+ # Call by name supports a "spill_over" mode where extra arguments in the given args_hash are introduced
+ # as variables in the resulting scope.
+ #
+ # @raise ArgumentError, if there are to many or too few arguments
+ # @raise ArgumentError, if given closure is not a Puppet::Pops::Evaluator::Closure
+ #
+ def call_by_name(closure, args_hash, scope, spill_over = false)
+ raise ArgumentError, "Can only call a Lambda" unless closure.is_a?(Puppet::Pops::Evaluator::Closure)
+ pblock = closure.model
+ parameters = pblock.parameters || []
+
+ if !spill_over && args_hash.size > parameters.size
+ raise ArgumentError, "Too many arguments: #{args_hash.size} for #{parameters.size}"
+ end
+
+ # associate values with parameters
+ scope_hash = {}
+ parameters.each do |p|
+ scope_hash[p.name] = args_hash[p.name] || evaluate(p.value, scope)
+ end
+ missing = scope_hash.reduce([]) {|memo, entry| memo << entry[0] if entry[1].nil?; memo }
+ unless missing.empty?
+ optional = parameters.count { |p| !p.value.nil? }
+ raise ArgumentError, "Too few arguments; no value given for required parameters #{missing.join(" ,")}"
+ end
+ if spill_over
+ # all args from given hash should be used, nil entries replaced by default values should win
+ scope_hash = args_hash.merge(scope_hash)
+ end
+
+ # Store the evaluated name => value associations in a new inner/local/ephemeral scope
+ # (This is made complicated due to the fact that the implementation of scope is overloaded with
+ # functionality and an inner ephemeral scope must be used (as opposed to just pushing a local scope
+ # on a scope "stack").
+
+ # Ensure variable exists with nil value if error occurs.
+ # Some ruby implementations does not like creating variable on return
+ result = nil
+ begin
+ scope_memo = get_scope_nesting_level(scope)
+ # change to create local scope_from - cannot give it file and line - that is the place of the call, not
+ # "here"
+ create_local_scope_from(scope_hash, scope)
+ result = evaluate(pblock.body, scope)
+ ensure
+ set_scope_nesting_level(scope, scope_memo)
+ end
+ result
+ end
+
+ # Call a closure - Can only be called with a Closure (for now), may be refactored later
+ # to also handle other types of calls (function calls are also handled by CallNamedFunction and CallMethod, they
+ # could create similar objects to Closure, wait until other types of defines are instantiated - they may behave
+ # as special cases of calls - i.e. 'new')
+ #
+ # @raise ArgumentError, if there are to many or too few arguments
+ # @raise ArgumentError, if given closure is not a Puppet::Pops::Evaluator::Closure
+ #
+ def call(closure, args, scope)
+ raise ArgumentError, "Can only call a Lambda" unless closure.is_a?(Puppet::Pops::Evaluator::Closure)
+ pblock = closure.model
+ parameters = pblock.parameters || []
+
+ raise ArgumentError, "Too many arguments: #{args.size} for #{parameters.size}" unless args.size <= parameters.size
+
+ # associate values with parameters
+ merged = parameters.zip(args)
+ # calculate missing arguments
+ missing = parameters.slice(args.size, parameters.size - args.size).select {|p| p.value.nil? }
+ unless missing.empty?
+ optional = parameters.count { |p| !p.value.nil? }
+ raise ArgumentError, "Too few arguments; #{args.size} for #{optional > 0 ? ' min ' : ''}#{parameters.size - optional}"
+ end
+
+ evaluated = merged.collect do |m|
+ # m can be one of
+ # m = [Parameter{name => "name", value => nil], "given"]
+ # | [Parameter{name => "name", value => Expression}, "given"]
+ #
+ # "given" is always an optional entry. If a parameter was provided then
+ # the entry will be in the array, otherwise the m array will be a
+ # single element.a = []
+ given_argument = m[1]
+ argument_name = m[0].name
+ default_expression = m[0].value
+
+ value = if default_expression
+ evaluate(default_expression, scope)
+ else
+ given_argument
+ end
+ [argument_name, value]
+ end
+
+ # Store the evaluated name => value associations in a new inner/local/ephemeral scope
+ # (This is made complicated due to the fact that the implementation of scope is overloaded with
+ # functionality and an inner ephemeral scope must be used (as opposed to just pushing a local scope
+ # on a scope "stack").
+
+ # Ensure variable exists with nil value if error occurs.
+ # Some ruby implementations does not like creating variable on return
+ result = nil
+ begin
+ scope_memo = get_scope_nesting_level(scope)
+ # change to create local scope_from - cannot give it file and line - that is the place of the call, not
+ # "here"
+ create_local_scope_from(Hash[evaluated], scope)
+ result = evaluate(pblock.body, scope)
+ ensure
+ set_scope_nesting_level(scope, scope_memo)
+ end
+ result
+ end
+
+ protected
+
+ def lvalue_VariableExpression(o, scope)
+ # evaluate the name
+ evaluate(o.expr, scope)
+ end
+
+ # Catches all illegal lvalues
+ #
+ def lvalue_Object(o, scope)
+ fail(Issues::ILLEGAL_ASSIGNMENT, o)
+ end
+
+ # Assign value to named variable.
+ # The '$' sign is never part of the name.
+ # @example In Puppet DSL
+ # $name = value
+ # @param name [String] name of variable without $
+ # @param value [Object] value to assign to the variable
+ # @param o [Puppet::Pops::Model::PopsObject] originating instruction
+ # @param scope [Object] the runtime specific scope where evaluation should take place
+ # @return [value<Object>]
+ #
+ def assign_String(name, value, o, scope)
+ if name =~ /::/
+ fail(Issues::CROSS_SCOPE_ASSIGNMENT, o.left_expr, {:name => name})
+ end
+ set_variable(name, value, o, scope)
+ value
+ end
+
+ def assign_Numeric(n, value, o, scope)
+ fail(Issues::ILLEGAL_NUMERIC_ASSIGNMENT, o.left_expr, {:varname => n.to_s})
+ end
+
+ # Catches all illegal assignment (e.g. 1 = 2, {'a'=>1} = 2, etc)
+ #
+ def assign_Object(name, value, o, scope)
+ fail(Issues::ILLEGAL_ASSIGNMENT, o)
+ end
+
+ def eval_Factory(o, scope)
+ evaluate(o.current, scope)
+ end
+
+ # Evaluates any object not evaluated to something else to itself.
+ def eval_Object o, scope
+ o
+ end
+
+ # Allows nil to be used as a Nop.
+ # Evaluates to nil
+ # TODO: What is the difference between literal undef, nil, and nop?
+ #
+ def eval_NilClass(o, scope)
+ nil
+ end
+
+ # Evaluates Nop to nil.
+ # TODO: or is this the same as :undef
+ # TODO: is this even needed as a separate instruction when there is a literal undef?
+ def eval_Nop(o, scope)
+ nil
+ end
+
+ # Captures all LiteralValues not handled elsewhere.
+ #
+ def eval_LiteralValue(o, scope)
+ o.value
+ end
+
+ def eval_LiteralDefault(o, scope)
+ :default
+ end
+
+ def eval_LiteralUndef(o, scope)
+ :undef # TODO: or just use nil for this?
+ end
+
+ # A QualifiedReference (i.e. a capitalized qualified name such as Foo, or Foo::Bar) evaluates to a PType
+ #
+ def eval_QualifiedReference(o, scope)
+ @@type_parser.interpret(o)
+ end
+
+ def eval_NotExpression(o, scope)
+ ! is_true?(evaluate(o.expr, scope))
+ end
+
+ def eval_UnaryMinusExpression(o, scope)
+ - coerce_numeric(evaluate(o.expr, scope), o, scope)
+ end
+
+ # Abstract evaluation, returns array [left, right] with the evaluated result of left_expr and
+ # right_expr
+ # @return <Array<Object, Object>> array with result of evaluating left and right expressions
+ #
+ def eval_BinaryExpression o, scope
+ [ evaluate(o.left_expr, scope), evaluate(o.right_expr, scope) ]
+ end
+
+ # Evaluates assignment with operators =, +=, -= and
+ #
+ # @example Puppet DSL
+ # $a = 1
+ # $a += 1
+ # $a -= 1
+ #
+ def eval_AssignmentExpression(o, scope)
+ name = lvalue(o.left_expr, scope)
+ value = evaluate(o.right_expr, scope)
+
+ case o.operator
+ when :'=' # regular assignment
+ assign(name, value, o, scope)
+
+ when :'+='
+ # if value does not exist and strict is on, looking it up fails, else it is nil or :undef
+ existing_value = get_variable_value(name, o, scope)
+ begin
+ if existing_value.nil? || existing_value == :undef
+ assign(name, value, o, scope)
+ else
+ # Delegate to calculate function to deal with check of LHS, and perform ´+´ as arithmetic or concatenation the
+ # same way as ArithmeticExpression performs `+`.
+ assign(name, calculate(existing_value, value, :'+', o.left_expr, o.right_expr, scope), o, scope)
+ end
+ rescue ArgumentError => e
+ fail(Issues::APPEND_FAILED, o, {:message => e.message})
+ end
+
+ when :'-='
+ # If an attempt is made to delete values from something that does not exists, the value is :undef (it is guaranteed to not
+ # include any values the user wants deleted anyway :-)
+ #
+ # if value does not exist and strict is on, looking it up fails, else it is nil or :undef
+ existing_value = get_variable_value(name, o, scope)
+ begin
+ if existing_value.nil? || existing_value == :undef
+ assign(name, :undef, o, scope)
+ else
+ # Delegate to delete function to deal with check of LHS, and perform deletion
+ assign(name, delete(get_variable_value(name, o, scope), value), o, scope)
+ end
+ rescue ArgumentError => e
+ fail(Issues::APPEND_FAILED, o, {:message => e.message}, e)
+ end
+ else
+ fail(Issues::UNSUPPORTED_OPERATOR, o, {:operator => o.operator})
+ end
+ value
+ end
+
+ ARITHMETIC_OPERATORS = [:'+', :'-', :'*', :'/', :'%', :'<<', :'>>']
+ COLLECTION_OPERATORS = [:'+', :'-', :'<<']
+
+ # Handles binary expression where lhs and rhs are array/hash or numeric and operator is +, - , *, % / << >>
+ #
+ def eval_ArithmeticExpression(o, scope)
+ left, right = eval_BinaryExpression(o, scope)
+ begin
+ result = calculate(left, right, o.operator, o.left_expr, o.right_expr, scope)
+ rescue ArgumentError => e
+ fail(Issues::RUNTIME_ERROR, o, {:detail => e.message}, e)
+ end
+ result
+ end
+
+
+ # Handles binary expression where lhs and rhs are array/hash or numeric and operator is +, - , *, % / << >>
+ #
+ def calculate(left, right, operator, left_o, right_o, scope)
+ unless ARITHMETIC_OPERATORS.include?(operator)
+ fail(Issues::UNSUPPORTED_OPERATOR, left_o.eContainer, {:operator => o.operator})
+ end
+
+ if (left.is_a?(Array) || left.is_a?(Hash)) && COLLECTION_OPERATORS.include?(operator)
+ # Handle operation on collections
+ case operator
+ when :'+'
+ concatenate(left, right)
+ when :'-'
+ delete(left, right)
+ when :'<<'
+ unless left.is_a?(Array)
+ fail(Issues::OPERATOR_NOT_APPLICABLE, left_o, {:operator => operator, :left_value => left})
+ end
+ left + [right]
+ end
+ else
+ # Handle operation on numeric
+ left = coerce_numeric(left, left_o, scope)
+ right = coerce_numeric(right, right_o, scope)
+ begin
+ if operator == :'%' && (left.is_a?(Float) || right.is_a?(Float))
+ # Deny users the fun of seeing severe rounding errors and confusing results
+ fail(Issues::OPERATOR_NOT_APPLICABLE, left_o, {:operator => operator, :left_value => left})
+ end
+ result = left.send(operator, right)
+ rescue NoMethodError => e
+ fail(Issues::OPERATOR_NOT_APPLICABLE, left_o, {:operator => operator, :left_value => left})
+ rescue ZeroDivisionError => e
+ fail(Issues::DIV_BY_ZERO, right_o)
+ end
+ if result == INFINITY || result == -INFINITY
+ fail(Issues::RESULT_IS_INFINITY, left_o, {:operator => operator})
+ end
+ result
+ end
+ end
+
+ def eval_EppExpression(o, scope)
+ scope["@epp"] = []
+ evaluate(o.body, scope)
+ result = scope["@epp"].join('')
+ result
+ end
+
+ def eval_RenderStringExpression(o, scope)
+ scope["@epp"] << o.value.dup
+ nil
+ end
+
+ def eval_RenderExpression(o, scope)
+ scope["@epp"] << string(evaluate(o.expr, scope), scope)
+ nil
+ end
+
+ # Evaluates Puppet DSL ->, ~>, <-, and <~
+ def eval_RelationshipExpression(o, scope)
+ # First level evaluation, reduction to basic data types or puppet types, the relationship operator then translates this
+ # to the final set of references (turning strings into references, which can not naturally be done by the main evaluator since
+ # all strings should not be turned into references.
+ #
+ real = eval_BinaryExpression(o, scope)
+ @@relationship_operator.evaluate(real, o, scope)
+ end
+
+ # Evaluates x[key, key, ...]
+ #
+ def eval_AccessExpression(o, scope)
+ left = evaluate(o.left_expr, scope)
+ keys = o.keys.nil? ? [] : o.keys.collect {|key| evaluate(key, scope) }
+ Puppet::Pops::Evaluator::AccessOperator.new(o).access(left, scope, *keys)
+ end
+
+ # Evaluates <, <=, >, >=, and ==
+ #
+ def eval_ComparisonExpression o, scope
+ left, right = eval_BinaryExpression o, scope
+
+ begin
+ # Left is a type
+ if left.is_a?(Puppet::Pops::Types::PAbstractType)
+ case o.operator
+ when :'=='
+ @@type_calculator.equals(left,right)
+
+ when :'!='
+ !@@type_calculator.equals(left,right)
+
+ when :'<'
+ # left can be assigned to right, but they are not equal
+ @@type_calculator.assignable?(right, left) && ! @@type_calculator.equals(left,right)
+ when :'<='
+ # left can be assigned to right
+ @@type_calculator.assignable?(right, left)
+ when :'>'
+ # right can be assigned to left, but they are not equal
+ @@type_calculator.assignable?(left,right) && ! @@type_calculator.equals(left,right)
+ when :'>='
+ # right can be assigned to left
+ @@type_calculator.assignable?(left, right)
+ else
+ fail(Issues::UNSUPPORTED_OPERATOR, o, {:operator => o.operator})
+ end
+ else
+ case o.operator
+ when :'=='
+ @@compare_operator.equals(left,right)
+ when :'!='
+ ! @@compare_operator.equals(left,right)
+ when :'<'
+ @@compare_operator.compare(left,right) < 0
+ when :'<='
+ @@compare_operator.compare(left,right) <= 0
+ when :'>'
+ @@compare_operator.compare(left,right) > 0
+ when :'>='
+ @@compare_operator.compare(left,right) >= 0
+ else
+ fail(Issues::UNSUPPORTED_OPERATOR, o, {:operator => o.operator})
+ end
+ end
+ rescue ArgumentError => e
+ fail(Issues::COMPARISON_NOT_POSSIBLE, o, {
+ :operator => o.operator,
+ :left_value => left,
+ :right_value => right,
+ :detail => e.message}, e)
+ end
+ end
+
+ # Evaluates matching expressions with type, string or regexp rhs expression.
+ # If RHS is a type, the =~ matches compatible (assignable?) type.
+ #
+ # @example
+ # x =~ /abc.*/
+ # @example
+ # x =~ "abc.*/"
+ # @example
+ # y = "abc"
+ # x =~ "${y}.*"
+ # @example
+ # [1,2,3] =~ Array[Integer[1,10]]
+ # @return [Boolean] if a match was made or not. Also sets $0..$n to matchdata in current scope.
+ #
+ def eval_MatchExpression o, scope
+ left, pattern = eval_BinaryExpression o, scope
+ # matches RHS types as instance of for all types except a parameterized Regexp[R]
+ if pattern.is_a?(Puppet::Pops::Types::PAbstractType)
+ if pattern.is_a?(Puppet::Pops::Types::PRegexpType) && pattern.pattern
+ # A qualified PRegexpType, get its ruby regexp
+ pattern = pattern.regexp
+ else
+ # evaluate as instance?
+ matched = @@type_calculator.instance?(pattern, left)
+ # convert match result to Boolean true, or false
+ return o.operator == :'=~' ? !!matched : !matched
+ end
+ end
+
+ begin
+ pattern = Regexp.new(pattern) unless pattern.is_a?(Regexp)
+ rescue StandardError => e
+ fail(Issues::MATCH_NOT_REGEXP, o.right_expr, {:detail => e.message}, e)
+ end
+ unless left.is_a?(String)
+ fail(Issues::MATCH_NOT_STRING, o.left_expr, {:left_value => left})
+ end
+
+ matched = pattern.match(left) # nil, or MatchData
+ set_match_data(matched, o, scope) # creates ephemeral
+
+ # convert match result to Boolean true, or false
+ o.operator == :'=~' ? !!matched : !matched
+ end
+
+ # Evaluates Puppet DSL `in` expression
+ #
+ def eval_InExpression o, scope
+ left, right = eval_BinaryExpression o, scope
+ @@compare_operator.include?(right, left)
+ end
+
+ # @example
+ # $a and $b
+ # b is only evaluated if a is true
+ #
+ def eval_AndExpression o, scope
+ is_true?(evaluate(o.left_expr, scope)) ? is_true?(evaluate(o.right_expr, scope)) : false
+ end
+
+ # @example
+ # a or b
+ # b is only evaluated if a is false
+ #
+ def eval_OrExpression o, scope
+ is_true?(evaluate(o.left_expr, scope)) ? true : is_true?(evaluate(o.right_expr, scope))
+ end
+
+ # Evaluates each entry of the literal list and creates a new Array
+ # @return [Array] with the evaluated content
+ #
+ def eval_LiteralList o, scope
+ o.values.collect {|expr| evaluate(expr, scope)}
+ end
+
+ # Evaluates each entry of the literal hash and creates a new Hash.
+ # @return [Hash] with the evaluated content
+ #
+ def eval_LiteralHash o, scope
+ h = Hash.new
+ o.entries.each {|entry| h[ evaluate(entry.key, scope)]= evaluate(entry.value, scope)}
+ h
+ end
+
+ # Evaluates all statements and produces the last evaluated value
+ #
+ def eval_BlockExpression o, scope
+ r = nil
+ o.statements.each {|s| r = evaluate(s, scope)}
+ r
+ end
+
+ # Performs optimized search over case option values, lazily evaluating each
+ # until there is a match. If no match is found, the case expression's default expression
+ # is evaluated (it may be nil or Nop if there is no default, thus producing nil).
+ # If an option matches, the result of evaluating that option is returned.
+ # @return [Object, nil] what a matched option returns, or nil if nothing matched.
+ #
+ def eval_CaseExpression(o, scope)
+ # memo scope level before evaluating test - don't want a match in the case test to leak $n match vars
+ # to expressions after the case expression.
+ #
+ with_guarded_scope(scope) do
+ test = evaluate(o.test, scope)
+ result = nil
+ the_default = nil
+ if o.options.find do |co|
+ # the first case option that matches
+ if co.values.find do |c|
+ the_default = co.then_expr if c.is_a? Puppet::Pops::Model::LiteralDefault
+ is_match?(test, evaluate(c, scope), c, scope)
+ end
+ result = evaluate(co.then_expr, scope)
+ true # the option was picked
+ end
+ end
+ result # an option was picked, and produced a result
+ else
+ evaluate(the_default, scope) # evaluate the default (should be a nop/nil) if there is no default).
+ end
+ end
+ end
+
+ # Evaluates a CollectExpression by transforming it into a 3x AST::Collection and then evaluating that.
+ # This is done because of the complex API between compiler, indirector, backends, and difference between
+ # collecting virtual resources and exported resources.
+ #
+ def eval_CollectExpression o, scope
+ # The Collect Expression and its contained query expressions are implemented in such a way in
+ # 3x that it is almost impossible to do anything about them (the AST objects are lazily evaluated,
+ # and the built structure consists of both higher order functions and arrays with query expressions
+ # that are either used as a predicate filter, or given to an indirection terminus (such as the Puppet DB
+ # resource terminus). Unfortunately, the 3x implementation has many inconsistencies that the implementation
+ # below carries forward.
+ #
+ collect_3x = Puppet::Pops::Model::AstTransformer.new().transform(o)
+ collected = collect_3x.evaluate(scope)
+ # the 3x returns an instance of Parser::Collector (but it is only registered with the compiler at this
+ # point and does not contain any valuable information (like the result)
+ # Dilemma: If this object is returned, it is a first class value in the Puppet Language and we
+ # need to be able to perform operations on it. We can forbid it from leaking by making CollectExpression
+ # a non R-value. This makes it possible for the evaluator logic to make use of the Collector.
+ collected
+ end
+
+ def eval_ParenthesizedExpression(o, scope)
+ evaluate(o.expr, scope)
+ end
+
+ # This evaluates classes, nodes and resource type definitions to nil, since 3x:
+ # instantiates them, and evaluates their parameters and body. This is achieved by
+ # providing bridge AST classes in Puppet::Parser::AST::PopsBridge that bridges a
+ # Pops Program and a Pops Expression.
+ #
+ # Since all Definitions are handled "out of band", they are treated as a no-op when
+ # evaluated.
+ #
+ def eval_Definition(o, scope)
+ nil
+ end
+
+ def eval_Program(o, scope)
+ evaluate(o.body, scope)
+ end
+
+ # Produces Array[PObjectType], an array of resource references
+ #
+ def eval_ResourceExpression(o, scope)
+ exported = o.exported
+ virtual = o.virtual
+ type_name = evaluate(o.type_name, scope)
+ o.bodies.map do |body|
+ titles = [evaluate(body.title, scope)].flatten
+ evaluated_parameters = body.operations.map {|op| evaluate(op, scope) }
+ create_resources(o, scope, virtual, exported, type_name, titles, evaluated_parameters)
+ end.flatten.compact
+ end
+
+ def eval_ResourceOverrideExpression(o, scope)
+ evaluated_resources = evaluate(o.resources, scope)
+ evaluated_parameters = o.operations.map { |op| evaluate(op, scope) }
+ create_resource_overrides(o, scope, [evaluated_resources].flatten, evaluated_parameters)
+ evaluated_resources
+ end
+
+ # Produces 3x array of parameters
+ def eval_AttributeOperation(o, scope)
+ create_resource_parameter(o, scope, o.attribute_name, evaluate(o.value_expr, scope), o.operator)
+ end
+
+ # Sets default parameter values for a type, produces the type
+ #
+ def eval_ResourceDefaultsExpression(o, scope)
+ type_name = o.type_ref.value # a QualifiedName's string value
+ evaluated_parameters = o.operations.map {|op| evaluate(op, scope) }
+ create_resource_defaults(o, scope, type_name, evaluated_parameters)
+ # Produce the type
+ evaluate(o.type_ref, scope)
+ end
+
+ # Evaluates function call by name.
+ #
+ def eval_CallNamedFunctionExpression(o, scope)
+ # The functor expression is not evaluated, it is not possible to select the function to call
+ # via an expression like $a()
+ case o.functor_expr
+ when Puppet::Pops::Model::QualifiedName
+ # ok
+ when Puppet::Pops::Model::RenderStringExpression
+ # helpful to point out this easy to make Epp error
+ fail(Issues::ILLEGAL_EPP_PARAMETERS, o)
+ else
+ fail(Issues::ILLEGAL_EXPRESSION, o.functor_expr, {:feature=>'function name', :container => o})
+ end
+ name = o.functor_expr.value
+ assert_function_available(name, o, scope)
+ evaluated_arguments = o.arguments.collect {|arg| evaluate(arg, scope) }
+ # wrap lambda in a callable block if it is present
+ evaluated_arguments << Puppet::Pops::Evaluator::Closure.new(self, o.lambda, scope) if o.lambda
+ call_function(name, evaluated_arguments, o, scope) do |result|
+ # prevent functions that are not r-value from leaking its return value
+ rvalue_function?(name, o, scope) ? result : nil
+ end
+ end
+
+ # Evaluation of CallMethodExpression handles a NamedAccessExpression functor (receiver.function_name)
+ #
+ def eval_CallMethodExpression(o, scope)
+ unless o.functor_expr.is_a? Puppet::Pops::Model::NamedAccessExpression
+ fail(Issues::ILLEGAL_EXPRESSION, o.functor_expr, {:feature=>'function accessor', :container => o})
+ end
+ receiver = evaluate(o.functor_expr.left_expr, scope)
+ name = o.functor_expr.right_expr
+ unless name.is_a? Puppet::Pops::Model::QualifiedName
+ fail(Issues::ILLEGAL_EXPRESSION, o.functor_expr, {:feature=>'function name', :container => o})
+ end
+ name = name.value # the string function name
+ assert_function_available(name, o, scope)
+ evaluated_arguments = [receiver] + (o.arguments || []).collect {|arg| evaluate(arg, scope) }
+ evaluated_arguments << Puppet::Pops::Evaluator::Closure.new(self, o.lambda, scope) if o.lambda
+ call_function(name, evaluated_arguments, o, scope) do |result|
+ # prevent functions that are not r-value from leaking its return value
+ rvalue_function?(name, o, scope) ? result : nil
+ end
+ end
+
+ # @example
+ # $x ? { 10 => true, 20 => false, default => 0 }
+ #
+ def eval_SelectorExpression o, scope
+ # memo scope level before evaluating test - don't want a match in the case test to leak $n match vars
+ # to expressions after the selector expression.
+ #
+ with_guarded_scope(scope) do
+ test = evaluate(o.left_expr, scope)
+ selected = o.selectors.find do |s|
+ candidate = evaluate(s.matching_expr, scope)
+ candidate == :default || is_match?(test, candidate, s.matching_expr, scope)
+ end
+ if selected
+ evaluate(selected.value_expr, scope)
+ else
+ nil
+ end
+ end
+ end
+
+ # SubLocatable is simply an expression that holds location information
+ def eval_SubLocatedExpression o, scope
+ evaluate(o.expr, scope)
+ end
+
+ # Evaluates Puppet DSL Heredoc
+ def eval_HeredocExpression o, scope
+ result = evaluate(o.text_expr, scope)
+ assert_external_syntax(scope, result, o.syntax, o.text_expr)
+ result
+ end
+
+ # Evaluates Puppet DSL `if`
+ def eval_IfExpression o, scope
+ with_guarded_scope(scope) do
+ if is_true?(evaluate(o.test, scope))
+ evaluate(o.then_expr, scope)
+ else
+ evaluate(o.else_expr, scope)
+ end
+ end
+ end
+
+ # Evaluates Puppet DSL `unless`
+ def eval_UnlessExpression o, scope
+ with_guarded_scope(scope) do
+ unless is_true?(evaluate(o.test, scope))
+ evaluate(o.then_expr, scope)
+ else
+ evaluate(o.else_expr, scope)
+ end
+ end
+ end
+
+ # Evaluates a variable (getting its value)
+ # The evaluator is lenient; any expression producing a String is used as a name
+ # of a variable.
+ #
+ def eval_VariableExpression o, scope
+ # Evaluator is not too fussy about what constitutes a name as long as the result
+ # is a String and a valid variable name
+ #
+ name = evaluate(o.expr, scope)
+
+ # Should be caught by validation, but make this explicit here as well, or mysterious evaluation issues
+ # may occur.
+ case name
+ when String
+ when Numeric
+ else
+ fail(Issues::ILLEGAL_VARIABLE_EXPRESSION, o.expr)
+ end
+ # TODO: Check for valid variable name (Task for validator)
+ # TODO: semantics of undefined variable in scope, this just returns what scope does == value or nil
+ get_variable_value(name, o, scope)
+ end
+
+ # Evaluates double quoted strings that may contain interpolation
+ #
+ def eval_ConcatenatedString o, scope
+ o.segments.collect {|expr| string(evaluate(expr, scope), scope)}.join
+ end
+
+
+ # If the wrapped expression is a QualifiedName, it is taken as the name of a variable in scope.
+ # Note that this is different from the 3.x implementation, where an initial qualified name
+ # is accepted. (e.g. `"---${var + 1}---"` is legal. This implementation requires such concrete
+ # syntax to be expressed in a model as `(TextExpression (+ (Variable var) 1)` - i.e. moving the decision to
+ # the parser.
+ #
+ # Semantics; the result of an expression is turned into a string, nil is silently transformed to empty
+ # string.
+ # @return [String] the interpolated result
+ #
+ def eval_TextExpression o, scope
+ if o.expr.is_a?(Puppet::Pops::Model::QualifiedName)
+ # TODO: formalize, when scope returns nil, vs error
+ string(get_variable_value(o.expr.value, o, scope), scope)
+ else
+ string(evaluate(o.expr, scope), scope)
+ end
+ end
+
+ def string_Object(o, scope)
+ o.to_s
+ end
+
+ def string_Symbol(o, scope)
+ case o
+ when :undef
+ ''
+ else
+ o.to_s
+ end
+ end
+
+ def string_Array(o, scope)
+ ['[', o.map {|e| string(e, scope)}.join(', '), ']'].join()
+ end
+
+ def string_Hash(o, scope)
+ ['{', o.map {|k,v| string(k, scope) + " => " + string(v, scope)}.join(', '), '}'].join()
+ end
+
+ def string_Regexp(o, scope)
+ ['/', o.source, '/'].join()
+ end
+
+ def string_PAbstractType(o, scope)
+ @@type_calculator.string(o)
+ end
+
+ # Produces concatenation / merge of x and y.
+ #
+ # When x is an Array, y of type produces:
+ #
+ # * Array => concatenation `[1,2], [3,4] => [1,2,3,4]`
+ # * Hash => concatenation of hash as array `[key, value, key, value, ...]`
+ # * any other => concatenation of single value
+ #
+ # When x is a Hash, y of type produces:
+ #
+ # * Array => merge of array interpreted as `[key, value, key, value,...]`
+ # * Hash => a merge, where entries in `y` overrides
+ # * any other => error
+ #
+ # When x is something else, wrap it in an array first.
+ #
+ # When x is nil, an empty array is used instead.
+ #
+ # @note to concatenate an Array, nest the array - i.e. `[1,2], [[2,3]]`
+ #
+ # @overload concatenate(obj_x, obj_y)
+ # @param obj_x [Object] object to wrap in an array and concatenate to; see other overloaded methods for return type
+ # @param ary_y [Object] array to concatenate at end of `ary_x`
+ # @return [Object] wraps obj_x in array before using other overloaded option based on type of obj_y
+ # @overload concatenate(ary_x, ary_y)
+ # @param ary_x [Array] array to concatenate to
+ # @param ary_y [Array] array to concatenate at end of `ary_x`
+ # @return [Array] new array with `ary_x` + `ary_y`
+ # @overload concatenate(ary_x, hsh_y)
+ # @param ary_x [Array] array to concatenate to
+ # @param hsh_y [Hash] converted to array form, and concatenated to array
+ # @return [Array] new array with `ary_x` + `hsh_y` converted to array
+ # @overload concatenate (ary_x, obj_y)
+ # @param ary_x [Array] array to concatenate to
+ # @param obj_y [Object] non array or hash object to add to array
+ # @return [Array] new array with `ary_x` + `obj_y` added as last entry
+ # @overload concatenate(hsh_x, ary_y)
+ # @param hsh_x [Hash] the hash to merge with
+ # @param ary_y [Array] array interpreted as even numbered sequence of key, value merged with `hsh_x`
+ # @return [Hash] new hash with `hsh_x` merged with `ary_y` interpreted as hash in array form
+ # @overload concatenate(hsh_x, hsh_y)
+ # @param hsh_x [Hash] the hash to merge to
+ # @param hsh_y [Hash] hash merged with `hsh_x`
+ # @return [Hash] new hash with `hsh_x` merged with `hsh_y`
+ # @raise [ArgumentError] when `xxx_x` is neither an Array nor a Hash
+ # @raise [ArgumentError] when `xxx_x` is a Hash, and `xxx_y` is neither Array nor Hash.
+ #
+ def concatenate(x, y)
+ x = [x] unless x.is_a?(Array) || x.is_a?(Hash)
+ case x
+ when Array
+ y = case y
+ when Array then y
+ when Hash then y.to_a
+ else
+ [y]
+ end
+ x + y # new array with concatenation
+ when Hash
+ y = case y
+ when Hash then y
+ when Array
+ # Hash[[a, 1, b, 2]] => {}
+ # Hash[a,1,b,2] => {a => 1, b => 2}
+ # Hash[[a,1], [b,2]] => {[a,1] => [b,2]}
+ # Hash[[[a,1], [b,2]]] => {a => 1, b => 2}
+ # Use type calcultor to determine if array is Array[Array[?]], and if so use second form
+ # of call
+ t = @@type_calculator.infer(y)
+ if t.element_type.is_a? Puppet::Pops::Types::PArrayType
+ Hash[y]
+ else
+ Hash[*y]
+ end
+ else
+ raise ArgumentError.new("Can only append Array or Hash to a Hash")
+ end
+ x.merge y # new hash with overwrite
+ else
+ raise ArgumentError.new("Can only append to an Array or a Hash.")
+ end
+ end
+
+ # Produces the result x \ y (set difference)
+ # When `x` is an Array, `y` is transformed to an array and then all matching elements removed from x.
+ # When `x` is a Hash, all contained keys are removed from x as listed in `y` if it is an Array, or all its keys if it is a Hash.
+ # The difference is returned. The given `x` and `y` are not modified by this operation.
+ # @raise [ArgumentError] when `x` is neither an Array nor a Hash
+ #
+ def delete(x, y)
+ result = x.dup
+ case x
+ when Array
+ y = case y
+ when Array then y
+ when Hash then y.to_a
+ else
+ [y]
+ end
+ y.each {|e| result.delete(e) }
+ when Hash
+ y = case y
+ when Array then y
+ when Hash then y.keys
+ else
+ [y]
+ end
+ y.each {|e| result.delete(e) }
+ else
+ raise ArgumentError.new("Can only delete from an Array or Hash.")
+ end
+ result
+ end
+
+ # Implementation of case option matching.
+ #
+ # This is the type of matching performed in a case option, using == for every type
+ # of value except regular expression where a match is performed.
+ #
+ def is_match? left, right, o, scope
+ if right.is_a?(Regexp)
+ return false unless left.is_a? String
+ matched = right.match(left)
+ set_match_data(matched, o, scope) # creates or clears ephemeral
+ !!matched # convert to boolean
+ elsif right.is_a?(Puppet::Pops::Types::PAbstractType)
+ # right is a type and left is not - check if left is an instance of the given type
+ # (The reverse is not terribly meaningful - computing which of the case options that first produces
+ # an instance of a given type).
+ #
+ @@type_calculator.instance?(right, left)
+ else
+ # Handle equality the same way as the language '==' operator (case insensitive etc.)
+ @@compare_operator.equals(left,right)
+ end
+ end
+
+ def with_guarded_scope(scope)
+ scope_memo = get_scope_nesting_level(scope)
+ begin
+ yield
+ ensure
+ set_scope_nesting_level(scope, scope_memo)
+ end
+ end
+
+end
diff --git a/lib/puppet/pops/evaluator/external_syntax_support.rb b/lib/puppet/pops/evaluator/external_syntax_support.rb
new file mode 100644
index 000000000..9ee96a863
--- /dev/null
+++ b/lib/puppet/pops/evaluator/external_syntax_support.rb
@@ -0,0 +1,49 @@
+# This module is an integral part of the evaluator. It deals with the concern of validating
+# external syntax in text produced by heredoc and templates.
+#
+module Puppet::Pops::Evaluator::ExternalSyntaxSupport
+ # TODO: This can be simplified if the Factory directly supporteded hash_of/type_of
+ TYPES = Puppet::Pops::Types::TypeFactory
+
+ def assert_external_syntax(scope, result, syntax, reference_expr)
+ @@HASH_OF_SYNTAX_CHECKERS ||= TYPES.hash_of(TYPES.type_of(::Puppetx::SYNTAX_CHECKERS_TYPE))
+ # ignore 'unspecified syntax'
+ return if syntax.nil? || syntax == ''
+
+ checker = checker_for_syntax(scope, syntax)
+ # ignore syntax with no matching checker
+ return unless checker
+
+ # Call checker and give it the location information from the expression
+ # (as opposed to where the heredoc tag is (somewhere on the line above)).
+ acceptor = Puppet::Pops::Validation::Acceptor.new()
+ source_pos = find_closest_positioned(reference_expr)
+ checker.check(result, syntax, acceptor, source_pos)
+
+ if acceptor.error_count > 0
+ checker_message = "Invalid produced text having syntax: '#{syntax}'."
+ Puppet::Pops::IssueReporter.assert_and_report(acceptor, :message => checker_message)
+ raise ArgumentError, "Internal Error: Configuration of runtime error handling wrong: should have raised exception"
+ end
+ end
+
+ # Finds the most significant checker for the given syntax (most significant is to the right).
+ # Returns nil if there is no registered checker.
+ #
+ def checker_for_syntax(scope, syntax)
+ checkers_hash = scope.compiler.injector.lookup(scope, @@HASH_OF_SYNTAX_CHECKERS, ::Puppetx::SYNTAX_CHECKERS) || {}
+ checkers_hash[lookup_keys_for_syntax(syntax).find {|x| checkers_hash[x] }]
+ end
+
+ # Returns an array of possible syntax names
+ def lookup_keys_for_syntax(syntax)
+ segments = syntax.split(/\+/)
+ result = []
+ begin
+ result << segments.join("+")
+ segments.shift
+ end until segments.empty?
+ result
+ end
+
+end
diff --git a/lib/puppet/pops/evaluator/relationship_operator.rb b/lib/puppet/pops/evaluator/relationship_operator.rb
new file mode 100644
index 000000000..7ffd20c8c
--- /dev/null
+++ b/lib/puppet/pops/evaluator/relationship_operator.rb
@@ -0,0 +1,156 @@
+# The RelationshipOperator implements the semantics of the -> <- ~> <~ operators creating relationships or notification
+# relationships between the left and right hand side's references to resources.
+#
+# This is separate class since a second level of evaluation is required that transforms string in left or right hand
+# to type references. The task of "making a relationship" is delegated to the "runtime support" class that is included.
+# This is done to separate the concerns of the new evaluator from the 3x runtime; messy logic goes into the runtime support
+# module. Later when more is cleaned up this can be simplified further.
+#
+class Puppet::Pops::Evaluator::RelationshipOperator
+
+ # Provides access to the Puppet 3.x runtime (scope, etc.)
+ # This separation has been made to make it easier to later migrate the evaluator to an improved runtime.
+ #
+ include Puppet::Pops::Evaluator::Runtime3Support
+
+ Issues = Puppet::Pops::Issues
+
+ class IllegalRelationshipOperandError < RuntimeError
+ attr_reader :operand
+ def initialize operand
+ @operand = operand
+ end
+ end
+
+ class NotCatalogTypeError < RuntimeError
+ attr_reader :type
+ def initialize type
+ @type = type
+ end
+ end
+
+ def initialize
+ @type_transformer_visitor = Puppet::Pops::Visitor.new(self, "transform", 1, 1)
+ @type_calculator = Puppet::Pops::Types::TypeCalculator.new()
+ @type_parser = Puppet::Pops::Types::TypeParser.new()
+
+ @catalog_type = Puppet::Pops::Types::TypeFactory.catalog_entry()
+ end
+
+ def transform(o, scope)
+ @type_transformer_visitor.visit_this_1(self, o, scope)
+ end
+
+ # Catch all non transformable objects
+ # @api private
+ def transform_Object(o, scope)
+ raise IllegalRelationshipOperandError.new(o)
+ end
+
+ # A string must be a type reference in string format
+ # @api private
+ def transform_String(o, scope)
+ assert_catalog_type(@type_parser.parse(o), scope)
+ end
+
+ # A qualified name is short hand for a class with this name
+ # @api private
+ def transform_QualifiedName(o, scope)
+ Puppet::Pops::Types::TypeFactory.host_class(o.value)
+ end
+
+ # Types are what they are, just check the type
+ # @api private
+ def transform_PAbstractType(o, scope)
+ assert_catalog_type(o, scope)
+ end
+
+ # This transforms a 3x Collector (the result of evaluating a 3x AST::Collection).
+ # It is passed through verbatim since it is evaluated late by the compiler. At the point
+ # where the relationship is evaluated, it is simply recorded with the compiler for later evaluation.
+ # If one of the sides of the relationship is a Collector it is evaluated before the actual
+ # relationship is formed. (All of this happens at a later point in time.
+ #
+ def transform_Collector(o, scope)
+ o
+ end
+
+ # Array content needs to be transformed
+ def transform_Array(o, scope)
+ o.map{|x| transform(x, scope) }
+ end
+
+ # Asserts (and returns) the type if it is a PCatalogEntryType
+ # (A PCatalogEntryType is the base class of PHostClassType, and PResourceType).
+ #
+ def assert_catalog_type(o, scope)
+ unless @type_calculator.assignable?(@catalog_type, o)
+ raise NotCatalogTypeError.new(o)
+ end
+ # TODO must check if this is an abstract PResourceType (i.e. without a type_name) - which should fail ?
+ # e.g. File -> File (and other similar constructs) - maybe the catalog protects against this since references
+ # may be to future objects...
+ o
+ end
+
+ RELATIONSHIP_OPERATORS = [:'->', :'~>', :'<-', :'<~']
+ REVERSE_OPERATORS = [:'<-', :'<~']
+ RELATION_TYPE = {
+ :'->' => :relationship,
+ :'<-' => :relationship,
+ :'~>' => :subscription,
+ :'<~' => :subscription
+ }
+
+ # Evaluate a relationship.
+ # TODO: The error reporting is not fine grained since evaluation has already taken place
+ # There is no references to the original source expressions at this point, only the overall
+ # relationship expression. (e.g.. the expression may be ['string', func_call(), etc.] -> func_call())
+ # To implement this, the general evaluator needs to be able to track each evaluation result and associate
+ # it with a corresponding expression. This structure should then be passed to the relationship operator.
+ #
+ def evaluate (left_right_evaluated, relationship_expression, scope)
+ # assert operator (should have been validated, but this logic makes assumptions which would
+ # screw things up royally). Better safe than sorry.
+ unless RELATIONSHIP_OPERATORS.include?(relationship_expression.operator)
+ fail(Issues::UNSUPPORTED_OPERATOR, relationship_expression, {:operator => relationship_expression.operator})
+ end
+
+ begin
+ # Turn each side into an array of types (this also asserts their type)
+ # (note wrap in array first if value is not already an array)
+ #
+ # TODO: Later when objects are Puppet Runtime Objects and know their type, it will be more efficient to check/infer
+ # the type first since a chained operation then does not have to visit each element again. This is not meaningful now
+ # since inference needs to visit each object each time, and this is what the transformation does anyway).
+ #
+ # real is [left, right], and both the left and right may be a single value or an array. In each case all content
+ # should be flattened, and then transformed to a type. left or right may also be a value that is transformed
+ # into an array, and thus the resulting left and right must be flattened individually
+ # Once flattened, the operands should be sets (to remove duplicate entries)
+ #
+ real = left_right_evaluated.collect {|x| [x].flatten.collect {|x| transform(x, scope) }}
+ real[0].flatten!
+ real[1].flatten!
+ real[0].uniq!
+ real[1].uniq!
+
+ # reverse order if operator is Right to Left
+ source, target = reverse_operator?(relationship_expression) ? real.reverse : real
+
+ # Add the relationships to the catalog
+ source.each {|s| target.each {|t| add_relationship(s, t, RELATION_TYPE[relationship_expression.operator], scope) }}
+
+ # Produce the transformed source RHS (if this is a chain, this does not need to be done again)
+ real.slice(1)
+ rescue NotCatalogTypeError => e
+ fail(Issues::ILLEGAL_RELATIONSHIP_OPERAND_TYPE, relationship_expression, {:type => @type_calculator.string(e.type)})
+ rescue IllegalRelationshipOperandError => e
+ fail(Issues::ILLEGAL_RELATIONSHIP_OPERAND_TYPE, relationship_expression, {:operand => e.operand})
+ end
+ end
+
+ def reverse_operator?(o)
+ REVERSE_OPERATORS.include?(o.operator)
+ end
+end
diff --git a/lib/puppet/pops/evaluator/runtime3_support.rb b/lib/puppet/pops/evaluator/runtime3_support.rb
new file mode 100644
index 000000000..8c92c315a
--- /dev/null
+++ b/lib/puppet/pops/evaluator/runtime3_support.rb
@@ -0,0 +1,489 @@
+# A module with bindings between the new evaluator and the 3x runtime.
+# The intention is to separate all calls into scope, compiler, resource, etc. in this module
+# to make it easier to later refactor the evaluator for better implementations of the 3x classes.
+#
+# @api private
+module Puppet::Pops::Evaluator::Runtime3Support
+ # Fails the evaluation of _semantic_ with a given issue.
+ #
+ # @param issue [Puppet::Pops::Issue] the issue to report
+ # @param semantic [Puppet::Pops::ModelPopsObject] the object for which evaluation failed in some way. Used to determine origin.
+ # @param options [Hash] hash of optional named data elements for the given issue
+ # @return [!] this method does not return
+ # @raise [Puppet::ParseError] an evaluation error initialized from the arguments (TODO: Change to EvaluationError?)
+ #
+ def fail(issue, semantic, options={}, except=nil)
+ if except.nil?
+ # Want a stacktrace, and it must be passed as an exception
+ begin
+ raise EvaluationError.new()
+ rescue EvaluationError => e
+ except = e
+ end
+ end
+ diagnostic_producer.accept(issue, semantic, options, except)
+ end
+
+ # Binds the given variable name to the given value in the given scope.
+ # The reference object `o` is intended to be used for origin information - the 3x scope implementation
+ # only makes use of location when there is an error. This is now handled by other mechanisms; first a check
+ # is made if a variable exists and an error is raised if attempting to change an immutable value. Errors
+ # in name, numeric variable assignment etc. have also been validated prior to this call. In the event the
+ # scope.setvar still raises an error, the general exception handling for evaluation of the assignment
+ # expression knows about its location. Because of this, there is no need to extract the location for each
+ # setting (extraction is somewhat expensive since 3x requires line instead of offset).
+ #
+ def set_variable(name, value, o, scope)
+ # Scope also checks this but requires that location information are passed as options.
+ # Those are expensive to calculate and a test is instead made here to enable failing with better information.
+ # The error is not specific enough to allow catching it - need to check the actual message text.
+ # TODO: Improve the messy implementation in Scope.
+ #
+ if scope.bound?(name)
+ if Puppet::Parser::Scope::RESERVED_VARIABLE_NAMES.include?(name)
+ fail(Puppet::Pops::Issues::ILLEGAL_RESERVED_ASSIGNMENT, o, {:name => name} )
+ else
+ fail(Puppet::Pops::Issues::ILLEGAL_REASSIGNMENT, o, {:name => name} )
+ end
+ end
+ scope.setvar(name, value)
+ end
+
+ # Returns the value of the variable (nil is returned if variable has no value, or if variable does not exist)
+ #
+ def get_variable_value(name, o, scope)
+ # Puppet 3x stores all variables as strings (then converts them back to numeric with a regexp... to see if it is a match variable)
+ # Not ideal, scope should support numeric lookup directly instead.
+ # TODO: consider fixing scope
+ catch(:undefined_variable) {
+ return scope.lookupvar(name.to_s)
+ }
+ # It is always ok to reference numeric variables even if they are not assigned. They are always undef
+ # if not set by a match expression.
+ #
+ unless name =~ Puppet::Pops::Patterns::NUMERIC_VAR_NAME
+ fail(Puppet::Pops::Issues::UNKNOWN_VARIABLE, o, {:name => name})
+ end
+ end
+
+ # Returns true if the variable of the given name is set in the given most nested scope. True is returned even if
+ # variable is bound to nil.
+ #
+ def variable_bound?(name, scope)
+ scope.bound?(name.to_s)
+ end
+
+ # Returns true if the variable is bound to a value or nil, in the scope or it's parent scopes.
+ #
+ def variable_exists?(name, scope)
+ scope.exist?(name.to_s)
+ end
+
+ def set_match_data(match_data, o, scope)
+ # See set_variable for rationale for not passing file and line to ephemeral_from.
+ # NOTE: The 3x scope adds one ephemeral(match) to its internal stack per match that succeeds ! It never
+ # clears anything. Thus a context that performs many matches will get very deep (there simply is no way to
+ # clear the match variables without rolling back the ephemeral stack.)
+ # This implementation does not attempt to fix this, it behaves the same bad way.
+ unless match_data.nil?
+ scope.ephemeral_from(match_data)
+ end
+ end
+
+ # Creates a local scope with vairalbes set from a hash of variable name to value
+ #
+ def create_local_scope_from(hash, scope)
+ # two dummy values are needed since the scope tries to give an error message (can not happen in this
+ # case - it is just wrong, the error should be reported by the caller who knows in more detail where it
+ # is in the source.
+ #
+ raise ArgumentError, "Internal error - attempt to create a local scope without a hash" unless hash.is_a?(Hash)
+ scope.ephemeral_from(hash)
+ end
+
+ # Creates a nested match scope
+ def create_match_scope_from(scope)
+ # Create a transparent match scope (for future matches)
+ scope.new_match_scope(nil)
+ end
+
+ def get_scope_nesting_level(scope)
+ scope.ephemeral_level
+ end
+
+ def set_scope_nesting_level(scope, level)
+ # Yup, 3x uses this method to reset the level, it also supports passing :all to destroy all
+ # ephemeral/local scopes - which is a sure way to create havoc.
+ #
+ scope.unset_ephemeral_var(level)
+ end
+
+ # Adds a relationship between the given `source` and `target` of the given `relationship_type`
+ # @param source [Puppet:Pops::Types::PCatalogEntryType] the source end of the relationship (from)
+ # @param target [Puppet:Pops::Types::PCatalogEntryType] the target end of the relationship (to)
+ # @param relationship_type [:relationship, :subscription] the type of the relationship
+ #
+ def add_relationship(source, target, relationship_type, scope)
+ # The 3x way is to record a Puppet::Parser::Relationship that is evaluated at the end of the compilation.
+ # This means it is not possible to detect any duplicates at this point (and signal where an attempt is made to
+ # add a duplicate. There is also no location information to signal the original place in the logic. The user will have
+ # to go fish.
+ # The 3.x implementation is based on Strings :-o, so the source and target must be transformed. The resolution is
+ # done by Catalog#resource(type, title). To do that, it creates a Puppet::Resource since it is responsible for
+ # translating the name/type/title and create index-keys used by the catalog. The Puppet::Resource has bizarre parsing of
+ # the type and title (scan for [] that is interpreted as type/title (but it gets it wrong).
+ # Moreover if the type is "" or "component", the type is Class, and if the type is :main, it is :main, all other cases
+ # undergo capitalization of name-segments (foo::bar becomes Foo::Bar). (This was earlier done in the reverse by the parser).
+ # Further, the title undergoes the same munging !!!
+ #
+ # That bug infested nest of messy logic needs serious Exorcism!
+ #
+ # Unfortunately it is not easy to simply call more intelligent methods at a lower level as the compiler evaluates the recorded
+ # Relationship object at a much later point, and it is responsible for invoking all the messy logic.
+ #
+ # TODO: Revisit the below logic when there is a sane implementation of the catalog, compiler and resource. For now
+ # concentrate on transforming the type references to what is expected by the wacky logic.
+ #
+ # HOWEVER, the Compiler only records the Relationships, and the only method it calls is @relationships.each{|x| x.evaluate(catalog) }
+ # Which means a smarter Relationship class could do this right. Instead of obtaining the resource from the catalog using
+ # the borked resource(type, title) which creates a resource for the purpose of looking it up, it needs to instead
+ # scan the catalog's resources
+ #
+ # GAAAH, it is even worse!
+ # It starts in the parser, which parses "File['foo']" into an AST::ResourceReference with type = File, and title = foo
+ # This AST is evaluated by looking up the type/title in the scope - causing it to be loaded if it exists, and if not, the given
+ # type name/title is used. It does not search for resource instances, only classes and types. It returns symbolic information
+ # [type, [title, title]]. From this, instances of Puppet::Resource are created and returned. These only have type/title information
+ # filled out. One or an array of resources are returned.
+ # This set of evaluated (empty reference) Resource instances are then passed to the relationship operator. It creates a
+ # Puppet::Parser::Relationship giving it a source and a target that are (empty reference) Resource instances. These are then remembered
+ # until the relationship is evaluated by the compiler (at the end). When evaluation takes place, the (empty reference) Resource instances
+ # are converted to String (!?! WTF) on the simple format "#{type}[#{title}]", and the catalog is told to find a resource, by giving
+ # it this string. If it cannot find the resource it fails, else the before/notify parameter is appended with the target.
+ # The search for the resource begin with (you guessed it) again creating an (empty reference) resource from type and title (WTF?!?!).
+ # The catalog now uses the reference resource to compute a key [r.type, r.title.to_s] and also gets a uniqueness key from the
+ # resource (This is only a reference type created from title and type). If it cannot find it with the first key, it uses the
+ # uniqueness key to lookup.
+ #
+ # This is probably done to allow a resource type to munge/translate the title in some way (but it is quite unclear from the long
+ # and convoluted path of evaluation.
+ # In order to do this in a way that is similar to 3.x two resources are created to be used as keys.
+ #
+ #
+ # TODO: logic that creates a PCatalogEntryType should resolve it to ensure it is loaded (to the best of known_resource_types knowledge).
+ # If this is not done, the order in which things are done may be different? OTOH, it probably works anyway :-)
+ # TODO: Not sure if references needs to be resolved via the scope?
+ #
+ # And if that is not enough, a source/target may be a Collector (a baked query that will be evaluated by the
+ # compiler - it is simply passed through here for processing by the compiler at the right time).
+ #
+ if source.is_a?(Puppet::Parser::Collector)
+ # use verbatim - behavior defined by 3x
+ source_resource = source
+ else
+ # transform into the wonderful String representation in 3x
+ type, title = catalog_type_to_split_type_title(source)
+ source_resource = Puppet::Resource.new(type, title)
+ end
+ if target.is_a?(Puppet::Parser::Collector)
+ # use verbatim - behavior defined by 3x
+ target_resource = target
+ else
+ # transform into the wonderful String representation in 3x
+ type, title = catalog_type_to_split_type_title(target)
+ target_resource = Puppet::Resource.new(type, title)
+ end
+ # Add the relationship to the compiler for later evaluation.
+ scope.compiler.add_relationship(Puppet::Parser::Relationship.new(source_resource, target_resource, relationship_type))
+ end
+
+ # Coerce value `v` to numeric or fails.
+ # The given value `v` is coerced to Numeric, and if that fails the operation
+ # calls {#fail}.
+ # @param v [Object] the value to convert
+ # @param o [Object] originating instruction
+ # @param scope [Object] the (runtime specific) scope where evaluation of o takes place
+ # @return [Numeric] value `v` converted to Numeric.
+ #
+ def coerce_numeric(v, o, scope)
+ unless n = Puppet::Pops::Utils.to_n(v)
+ fail(Puppet::Pops::Issues::NOT_NUMERIC, o, {:value => v})
+ end
+ n
+ end
+
+ # Asserts that the given function name resolves to an available function. The function is loaded
+ # as a side effect. Fails if the function does not exist.
+ #
+ def assert_function_available(name, o, scope)
+ fail(Puppet::Pops::Issues::UNKNOWN_FUNCTION, o, {:name => name}) unless Puppet::Parser::Functions.function(name)
+ end
+
+ def call_function(name, args, o, scope)
+ # Arguments must be mapped since functions are unaware of the new and magical creatures in 4x.
+ # NOTE: Passing an empty string last converts :undef to empty string
+ mapped_args = args.map {|a| convert(a, scope, '') }
+ scope.send("function_#{name}", mapped_args)
+ end
+
+ # Returns true if the function produces a value
+ def rvalue_function?(name, o, scope)
+ Puppet::Parser::Functions.rvalue?(name)
+ end
+
+ # The o is used for source reference
+ def create_resource_parameter(o, scope, name, value, operator)
+ file, line = extract_file_line(o)
+ Puppet::Parser::Resource::Param.new(
+ :name => name,
+ :value => convert(value, scope, :undef), # converted to 3x since 4x supports additional objects / types
+ :source => scope.source, :line => line, :file => file,
+ :add => operator == :'+>'
+ )
+ end
+
+ def create_resources(o, scope, virtual, exported, type_name, resource_titles, evaluated_parameters)
+
+ # TODO: Unknown resource causes creation of Resource to fail with ArgumentError, should give
+ # a proper Issue. Now the result is "Error while evaluating a Resource Statement" with the message
+ # from the raised exception. (It may be good enough).
+
+ # resolve in scope.
+ fully_qualified_type, resource_titles = scope.resolve_type_and_titles(type_name, resource_titles)
+
+ # Not 100% accurate as this is the resource expression location and each title is processed separately
+ # The titles are however the result of evaluation and they have no location at this point (an array
+ # of positions for the source expressions are required for this to work).
+ # TODO: Revisit and possible improve the accuracy.
+ #
+ file, line = extract_file_line(o)
+
+ # Build a resource for each title
+ resource_titles.map do |resource_title|
+ resource = Puppet::Parser::Resource.new(
+ fully_qualified_type, resource_title,
+ :parameters => evaluated_parameters,
+ :file => file,
+ :line => line,
+ :exported => exported,
+ :virtual => virtual,
+ # WTF is this? Which source is this? The file? The name of the context ?
+ :source => scope.source,
+ :scope => scope,
+ :strict => true
+ )
+
+ if resource.resource_type.is_a? Puppet::Resource::Type
+ resource.resource_type.instantiate_resource(scope, resource)
+ end
+ scope.compiler.add_resource(scope, resource)
+ scope.compiler.evaluate_classes([resource_title], scope, false, true) if fully_qualified_type == 'class'
+ # Turn the resource into a PType (a reference to a resource type)
+ # weed out nil's
+ resource_to_ptype(resource)
+ end
+ end
+
+ # Defines default parameters for a type with the given name.
+ #
+ def create_resource_defaults(o, scope, type_name, evaluated_parameters)
+ # Note that name must be capitalized in this 3x call
+ # The 3x impl creates a Resource instance with a bogus title and then asks the created resource
+ # for the type of the name.
+ # Note, locations are available per parameter.
+ #
+ scope.define_settings(type_name.capitalize, evaluated_parameters)
+ end
+
+ # Creates resource overrides for all resource type objects in evaluated_resources. The same set of
+ # evaluated parameters are applied to all.
+ #
+ def create_resource_overrides(o, scope, evaluated_resources, evaluated_parameters)
+ # Not 100% accurate as this is the resource expression location and each title is processed separately
+ # The titles are however the result of evaluation and they have no location at this point (an array
+ # of positions for the source expressions are required for this to work.
+ # TODO: Revisit and possible improve the accuracy.
+ #
+ file, line = extract_file_line(o)
+
+ evaluated_resources.each do |r|
+ resource = Puppet::Parser::Resource.new(
+ r.type_name, r.title,
+ :parameters => evaluated_parameters,
+ :file => file,
+ :line => line,
+ # WTF is this? Which source is this? The file? The name of the context ?
+ :source => scope.source,
+ :scope => scope
+ )
+
+ scope.compiler.add_override(resource)
+ end
+ end
+
+ # Finds a resource given a type and a title.
+ #
+ def find_resource(scope, type_name, title)
+ scope.compiler.findresource(type_name, title)
+ end
+
+ # Returns the value of a resource's parameter by first looking up the parameter in the resource
+ # and then in the defaults for the resource. Since the resource exists (it must in order to look up its
+ # parameters, any overrides have already been applied). Defaults are not applied to a resource until it
+ # has been finished (which typically has not taked place when this is evaluated; hence the dual lookup).
+ #
+ def get_resource_parameter_value(scope, resource, parameter_name)
+ val = resource[parameter_name]
+ if val.nil? && defaults = scope.lookupdefaults(resource.type)
+ # NOTE: 3x resource keeps defaults as hash using symbol for name as key to Parameter which (again) holds
+ # name and value.
+ param = defaults[parameter_name.to_sym]
+ val = param.value
+ end
+ val
+ end
+
+ # Returns true, if the given name is the name of a resource parameter.
+ #
+ def is_parameter_of_resource?(scope, resource, name)
+ resource.valid_parameter?(name)
+ end
+
+ def resource_to_ptype(resource)
+ nil if resource.nil?
+ type_calculator.infer(resource)
+ end
+
+ # This is the same type of "truth" as used in the current Puppet DSL.
+ #
+ def is_true? o
+ # Is the value true? This allows us to control the definition of truth
+ # in one place.
+ case o
+ when ''
+ false
+ when :undef
+ false
+ else
+ !!o
+ end
+ end
+
+ # Utility method for TrueClass || FalseClass
+ # @param x [Object] the object to test if it is instance of TrueClass or FalseClass
+ def is_boolean? x
+ x.is_a?(TrueClass) || x.is_a?(FalseClass)
+ end
+
+ def initialize
+ @@convert_visitor ||= Puppet::Pops::Visitor.new(self, "convert", 2, 2)
+ end
+
+ # Converts 4x supported values to 3x values. This is required because
+ # resources and other objects do not know about the new type system, and does not support
+ # regular expressions. Unfortunately this has to be done for array and hash as well.
+ # A complication is that catalog types needs to be resolved against the scope.
+ #
+ def convert(o, scope, undef_value)
+ @@convert_visitor.visit_this_2(self, o, scope, undef_value)
+ end
+
+
+ def convert_NilClass(o, scope, undef_value)
+ undef_value
+ end
+
+ def convert_Object(o, scope, undef_value)
+ o
+ end
+
+ def convert_Array(o, scope, undef_value)
+ o.map {|x| convert(x, scope, undef_value) }
+ end
+
+ def convert_Hash(o, scope, undef_value)
+ result = {}
+ o.each {|k,v| result[convert(k, scope, undef_value)] = convert(v, scope, undef_value) }
+ result
+ end
+
+ def convert_Regexp(o, scope, undef_value)
+ # Puppet 3x cannot handle parameter values that are reqular expressions. Turn into regexp string in
+ # source form
+ o.inspect
+ end
+
+ def convert_Symbol(o, scope, undef_value)
+ case o
+ when :undef
+ undef_value # 3x wants :undef as empty string in function
+ else
+ o # :default, and all others are verbatim since they are new in future evaluator
+ end
+ end
+
+ def convert_PAbstractType(o, scope, undef_value)
+ o
+ end
+
+ def convert_PResourceType(o,scope, undef_value)
+ # Needs conversion by calling scope to resolve the name and possibly return a different name
+ # Resolution can only be called with an array, and returns an array. Here there is only one name
+ type, titles = scope.resolve_type_and_titles(o.type_name, [o.title])
+ Puppet::Resource.new(type, titles[0])
+ end
+
+ def convert_PHostClassType(o, scope, undef_value)
+ # Needs conversion by calling scope to resolve the name and possibly return a different name
+ # Resolution can only be called with an array, and returns an array. Here there is only one name
+ type, titles = scope.resolve_type_and_titles('class', [o.class_name])
+ Puppet::Resource.new(type, titles[0])
+ end
+
+ private
+
+ # Produces an array with [type, title] from a PCatalogEntryType
+ # Used to produce reference resource instances (used when 3x is operating on a resource).
+ #
+ def catalog_type_to_split_type_title(catalog_type)
+ case catalog_type
+ when Puppet::Pops::Types::PHostClassType
+ return ['Class', catalog_type.class_name]
+ when Puppet::Pops::Types::PResourceType
+ return [catalog_type.type_name, catalog_type.title]
+ else
+ raise ArgumentError, "Cannot split the type #{catalog_type.class}, it is neither a PHostClassType, nor a PResourceClass."
+ end
+ end
+
+ def extract_file_line(o)
+ source_pos = Puppet::Pops::Utils.find_closest_positioned(o)
+ return [nil, -1] unless source_pos
+ [source_pos.locator.file, source_pos.line]
+ end
+
+ def find_closest_positioned(o)
+ return nil if o.nil? || o.is_a?(Puppet::Pops::Model::Program)
+ o.offset.nil? ? find_closest_positioned(o.eContainer) : Puppet::Pops::Adapters::SourcePosAdapter.adapt(o)
+ end
+
+ # Creates a diagnostic producer
+ def diagnostic_producer
+ Puppet::Pops::Validation::DiagnosticProducer.new(
+ ExceptionRaisingAcceptor.new(), # Raises exception on all issues
+ Puppet::Pops::Validation::SeverityProducer.new(), # All issues are errors
+ Puppet::Pops::Model::ModelLabelProvider.new())
+ end
+
+ # An acceptor of diagnostics that immediately raises an exception.
+ class ExceptionRaisingAcceptor < Puppet::Pops::Validation::Acceptor
+ def accept(diagnostic)
+ super
+ Puppet::Pops::IssueReporter.assert_and_report(self, {:message => "Evaluation Error:" })
+ raise ArgumentError, "Internal Error: Configuration of runtime error handling wrong: should have raised exception"
+ end
+ end
+
+ class EvaluationError < StandardError
+ end
+end
diff --git a/lib/puppet/pops/issue_reporter.rb b/lib/puppet/pops/issue_reporter.rb
index 675b03992..cac2efcbc 100644
--- a/lib/puppet/pops/issue_reporter.rb
+++ b/lib/puppet/pops/issue_reporter.rb
@@ -1,75 +1,79 @@
class Puppet::Pops::IssueReporter
# @param acceptor [Puppet::Pops::Validation::Acceptor] the acceptor containing reported issues
# @option options [String] :message (nil) A message text to use as prefix in a single Error message
# @option options [Boolean] :emit_warnings (false) A message text to use as prefix in a single Error message
# @option options [Boolean] :emit_errors (true) whether errors should be emitted or only given message
# @option options [Exception] :exception_class (Puppet::ParseError) The exception to raise
#
def self.assert_and_report(acceptor, options)
return unless acceptor
max_errors = Puppet[:max_errors]
max_warnings = Puppet[:max_warnings] + 1
max_deprecations = Puppet[:max_deprecations] + 1
emit_warnings = options[:emit_warnings] || false
- emit_errors = options[:emit_errors] || true
+ emit_errors = options[:emit_errors].nil? ? true : !!options[:emit_errors]
emit_message = options[:message]
emit_exception = options[:exception_class] || Puppet::ParseError
# If there are warnings output them
warnings = acceptor.warnings
if emit_warnings && warnings.size > 0
formatter = Puppet::Pops::Validation::DiagnosticFormatterPuppetStyle.new
emitted_w = 0
emitted_dw = 0
acceptor.warnings.each do |w|
if w.severity == :deprecation
# Do *not* call Puppet.deprecation_warning it is for internal deprecation, not
# deprecation of constructs in manifests! (It is not designed for that purpose even if
# used throughout the code base).
#
Puppet.warning(formatter.format(w)) if emitted_dw < max_deprecations
emitted_dw += 1
else
Puppet.warning(formatter.format(w)) if emitted_w < max_warnings
emitted_w += 1
end
break if emitted_w > max_warnings && emitted_dw > max_deprecations # but only then
end
end
# If there were errors, report the first found. Use a puppet style formatter.
errors = acceptor.errors
if errors.size > 0
unless emit_errors
raise emit_exception.new(emit_message)
end
formatter = Puppet::Pops::Validation::DiagnosticFormatterPuppetStyle.new
if errors.size == 1 || max_errors <= 1
# raise immediately
- raise emit_exception.new(format_with_prefix(emit_message, formatter.format(errors[0])))
+ exception = emit_exception.new(format_with_prefix(emit_message, formatter.format(errors[0])))
+ # if an exception was given as cause, use it's backtrace instead of the one indicating "here"
+ if errors[0].exception
+ exception.set_backtrace(errors[0].exception.backtrace)
+ end
+ raise exception
end
emitted = 0
if emit_message
Puppet.err(emit_message)
end
errors.each do |e|
Puppet.err(formatter.format(e))
emitted += 1
break if emitted >= max_errors
end
warnings_message = (emit_warnings && warnings.size > 0) ? ", and #{warnings.size} warnings" : ""
giving_up_message = "Found #{errors.size} errors#{warnings_message}. Giving up"
exception = emit_exception.new(giving_up_message)
exception.file = errors[0].file
raise exception
end
- parse_result
end
def self.format_with_prefix(prefix, message)
return message unless prefix
[prefix, message].join(' ')
end
-end
\ No newline at end of file
+end
diff --git a/lib/puppet/pops/issues.rb b/lib/puppet/pops/issues.rb
index 61663b167..8dfe5d6c0 100644
--- a/lib/puppet/pops/issues.rb
+++ b/lib/puppet/pops/issues.rb
@@ -1,266 +1,461 @@
# Defines classes to deal with issues, and message formatting and defines constants with Issues.
# @api public
#
module Puppet::Pops::Issues
# Describes an issue, and can produce a message for an occurrence of the issue.
#
class Issue
# The issue code
# @return [Symbol]
attr_reader :issue_code
# A block producing the message
# @return [Proc]
attr_reader :message_block
# Names that must be bound in an occurrence of the issue to be able to produce a message.
# These are the names in addition to requirements stipulated by the Issue formatter contract; i.e. :label`,
# and `:semantic`.
#
attr_reader :arg_names
# If this issue can have its severity lowered to :warning, :deprecation, or :ignored
attr_writer :demotable
# Configures the Issue with required arguments (bound by occurrence), and a block producing a message.
def initialize issue_code, *args, &block
@issue_code = issue_code
@message_block = block
@arg_names = args
@demotable = true
end
# Returns true if it is allowed to demote this issue
def demotable?
@demotable
end
# Formats a message for an occurrence of the issue with argument bindings passed in a hash.
# The hash must contain a LabelProvider bound to the key `label` and the semantic model element
# bound to the key `semantic`. All required arguments as specified by `arg_names` must be bound
# in the given `hash`.
# @api public
#
def format(hash ={})
# Create a Message Data where all hash keys become methods for convenient interpolation
# in issue text.
msgdata = MessageData.new(*arg_names)
begin
# Evaluate the message block in the msg data's binding
msgdata.format(hash, &message_block)
rescue StandardError => e
+ Puppet::Pops::Issues::MessageData
raise RuntimeError, "Error while reporting issue: #{issue_code}. #{e.message}", caller
end
end
end
# Provides a binding of arguments passed to Issue.format to method names available
# in the issue's message producing block.
# @api private
#
class MessageData
def initialize *argnames
singleton = class << self; self end
argnames.each do |name|
singleton.send(:define_method, name) do
@data[name]
end
end
end
def format(hash, &block)
@data = hash
instance_eval &block
end
# Returns the label provider given as a key in the hash passed to #format.
+ # If given an argument, calls #label on the label provider (caller would otherwise have to
+ # call label.label(it)
#
- def label
+ def label(it = nil)
raise "Label provider key :label must be set to produce the text of the message!" unless @data[:label]
- @data[:label]
+ it.nil? ? @data[:label] : @data[:label].label(it)
end
# Returns the label provider given as a key in the hash passed to #format.
#
def semantic
raise "Label provider key :semantic must be set to produce the text of the message!" unless @data[:semantic]
@data[:semantic]
end
end
# Defines an issue with the given `issue_code`, additional required parameters, and a block producing a message.
# The block is evaluated in the context of a MessageData which provides convenient access to all required arguments
# via accessor methods. In addition to accessors for specified arguments, these are also available:
# * `label` - a `LabelProvider` that provides human understandable names for model elements and production of article (a/an/the).
# * `semantic` - the model element for which the issue is reported
#
# @param issue_code [Symbol] the issue code for the issue used as an identifier, should be the same as the constant
# the issue is bound to.
# @param args [Symbol] required arguments that must be passed when formatting the message, may be empty
# @param block [Proc] a block producing the message string, evaluated in a MessageData scope. The produced string
# should not end with a period as additional information may be appended.
#
# @see MessageData
# @api public
#
def self.issue (issue_code, *args, &block)
Issue.new(issue_code, *args, &block)
end
# Creates a non demotable issue.
# @see Issue.issue
#
def self.hard_issue(issue_code, *args, &block)
result = Issue.new(issue_code, *args, &block)
result.demotable = false
result
end
# @comment Here follows definitions of issues. The intent is to provide a list from which yardoc can be generated
# containing more detailed information / explanation of the issue.
# These issues are set as constants, but it is unfortunately not possible for the created object to easily know which
# name it is bound to. Instead the constant has to be repeated. (Alternatively, it could be done by instead calling
# #const_set on the module, but the extra work required to get yardoc output vs. the extra effort to repeat the name
# twice makes it not worth it (if doable at all, since there is no tag to artificially construct a constant, and
# the parse tag does not produce any result for a constant assignment).
# This is allowed (3.1) and has not yet been deprecated.
# @todo configuration
#
NAME_WITH_HYPHEN = issue :NAME_WITH_HYPHEN, :name do
"#{label.a_an_uc(semantic)} may not have a name containing a hyphen. The name '#{name}' is not legal"
end
# When a variable name contains a hyphen and these are illegal.
# It is possible to control if a hyphen is legal in a name or not using the setting TODO
# @todo describe the setting
# @api public
# @todo configuration if this is error or warning
#
VAR_WITH_HYPHEN = issue :VAR_WITH_HYPHEN, :name do
"A variable name may not contain a hyphen. The name '#{name}' is not legal"
end
# A class, definition, or node may only appear at top level or inside other classes
# @todo Is this really true for nodes? Can they be inside classes? Isn't that too late?
# @api public
#
NOT_TOP_LEVEL = hard_issue :NOT_TOP_LEVEL do
"Classes, definitions, and nodes may only appear at toplevel or inside other classes"
end
CROSS_SCOPE_ASSIGNMENT = hard_issue :CROSS_SCOPE_ASSIGNMENT, :name do
"Illegal attempt to assign to '#{name}'. Cannot assign to variables in other namespaces"
end
# Assignment can only be made to certain types of left hand expressions such as variables.
ILLEGAL_ASSIGNMENT = hard_issue :ILLEGAL_ASSIGNMENT do
"Illegal attempt to assign to '#{label.a_an(semantic)}'. Not an assignable reference"
end
+ # Variables are immutable, cannot reassign in the same assignment scope
+ ILLEGAL_REASSIGNMENT = hard_issue :ILLEGAL_REASSIGNMENT, :name do
+ "Cannot reassign variable #{name}"
+ end
+
+ ILLEGAL_RESERVED_ASSIGNMENT = hard_issue :ILLEGAL_RESERVED_ASSIGNMENT, :name do
+ "Attempt to assign to a reserved variable name: '#{name}'"
+ end
+
# Assignment cannot be made to numeric match result variables
ILLEGAL_NUMERIC_ASSIGNMENT = issue :ILLEGAL_NUMERIC_ASSIGNMENT, :varname do
"Illegal attempt to assign to the numeric match result variable '$#{varname}'. Numeric variables are not assignable"
end
+ APPEND_FAILED = issue :APPEND_FAILED, :message do
+ "Append assignment += failed with error: #{message}"
+ end
+
+ DELETE_FAILED = issue :DELETE_FAILED, :message do
+ "'Delete' assignment -= failed with error: #{message}"
+ end
+
# parameters cannot have numeric names, clashes with match result variables
ILLEGAL_NUMERIC_PARAMETER = issue :ILLEGAL_NUMERIC_PARAMETER, :name do
"The numeric parameter name '$#{varname}' cannot be used (clashes with numeric match result variables)"
end
# In certain versions of Puppet it may be allowed to assign to a not already assigned key
# in an array or a hash. This is an optional validation that may be turned on to prevent accidental
# mutation.
#
ILLEGAL_INDEXED_ASSIGNMENT = issue :ILLEGAL_INDEXED_ASSIGNMENT do
"Illegal attempt to assign via [index/key]. Not an assignable reference"
end
# When indexed assignment ($x[]=) is allowed, the leftmost expression must be
# a variable expression.
#
ILLEGAL_ASSIGNMENT_VIA_INDEX = hard_issue :ILLEGAL_ASSIGNMENT_VIA_INDEX do
"Illegal attempt to assign to #{label.a_an(semantic)} via [index/key]. Not an assignable reference"
end
+ # For unsupported operators (e.g. -= in puppet 3).
+ #
+ UNSUPPORTED_OPERATOR = hard_issue :UNSUPPORTED_OPERATOR, :operator do
+ "The operator '#{operator}' in #{label.a_an(semantic)} is not supported."
+ end
+
+ # For non applicable operators (e.g. << on Hash).
+ #
+ OPERATOR_NOT_APPLICABLE = hard_issue :OPERATOR_NOT_APPLICABLE, :operator, :left_value do
+ "Operator '#{operator}' is not applicable to #{label.a_an(left_value)}."
+ end
+
+ COMPARISON_NOT_POSSIBLE = hard_issue :COMPARISON_NOT_POSSIBLE, :operator, :left_value, :right_value, :detail do
+ "Comparison of: #{label(left_value)} #{operator} #{label(right_value)}, is not possible. Caused by '#{detail}'."
+ end
+
+ MATCH_NOT_REGEXP = hard_issue :MATCH_NOT_REGEXP, :detail do
+ "Can not convert right match operand to a regular expression. Caused by '#{detail}'."
+ end
+
+ MATCH_NOT_STRING = hard_issue :MATCH_NOT_STRING, :left_value do
+ "Left match operand must result in a String value. Got #{label.a_an(left_value)}."
+ end
+
# Some expressions/statements may not produce a value (known as right-value, or rvalue).
# This may vary between puppet versions.
#
NOT_RVALUE = issue :NOT_RVALUE do
"Invalid use of expression. #{label.a_an_uc(semantic)} does not produce a value"
end
# Appending to attributes is only allowed in certain types of resource expressions.
#
ILLEGAL_ATTRIBUTE_APPEND = hard_issue :ILLEGAL_ATTRIBUTE_APPEND, :name, :parent do
"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"
+ "Illegal name. The given name #{name} does not conform to the naming rule /^((::)?[a-z_]\w*)(::[a-z]\w*)*$/"
+ end
+
+ ILLEGAL_VAR_NAME = hard_issue :ILLEGAL_VAR_NAME, :name do
+ "Illegal variable name, The given name '#{name}' does not conform to the naming rule /^((::)?[a-z]\w*)*((::)?[a-z_]\w*)$/"
+ end
+
+ ILLEGAL_NUMERIC_VAR_NAME = hard_issue :ILLEGAL_NUMERIC_VAR_NAME, :name do
+ "Illegal numeric variable name, The given name '#{name}' must be a decimal value if it starts with a digit 0-9"
end
# In case a model is constructed programmatically, it must create valid type references.
#
ILLEGAL_CLASSREF = hard_issue :ILLEGAL_CLASSREF, :name do
"Illegal type reference. The given name '#{name}' does not conform to the naming rule"
end
# This is a runtime issue - storeconfigs must be on in order to collect exported. This issue should be
# set to :ignore when just checking syntax.
# @todo should be a :warning by default
#
RT_NO_STORECONFIGS = issue :RT_NO_STORECONFIGS do
"You cannot collect exported resources without storeconfigs being set; the collection will be ignored"
end
# This is a runtime issue - storeconfigs must be on in order to export a resource. This issue should be
# set to :ignore when just checking syntax.
# @todo should be a :warning by default
#
RT_NO_STORECONFIGS_EXPORT = issue :RT_NO_STORECONFIGS_EXPORT do
"You cannot collect exported resources without storeconfigs being set; the export is ignored"
end
# A hostname may only contain letters, digits, '_', '-', and '.'.
#
ILLEGAL_HOSTNAME_CHARS = hard_issue :ILLEGAL_HOSTNAME_CHARS, :hostname do
"The hostname '#{hostname}' contains illegal characters (only letters, digits, '_', '-', and '.' are allowed)"
end
# A hostname may only contain letters, digits, '_', '-', and '.'.
#
ILLEGAL_HOSTNAME_INTERPOLATION = hard_issue :ILLEGAL_HOSTNAME_INTERPOLATION do
"An interpolated expression is not allowed in a hostname of a node"
end
# Issues when an expression is used where it is not legal.
# E.g. an arithmetic expression where a hostname is expected.
#
ILLEGAL_EXPRESSION = hard_issue :ILLEGAL_EXPRESSION, :feature, :container do
"Illegal expression. #{label.a_an_uc(semantic)} is unacceptable as #{feature} in #{label.a_an(container)}"
end
+ # Issues when an expression is used where it is not legal.
+ # E.g. an arithmetic expression where a hostname is expected.
+ #
+ ILLEGAL_VARIABLE_EXPRESSION = hard_issue :ILLEGAL_VARIABLE_EXPRESSION do
+ "Illegal variable expression. #{label.a_an_uc(semantic)} did not produce a variable name (String or Numeric)."
+ end
+
# Issues when an expression is used illegaly in a query.
# query only supports == and !=, and not <, > etc.
#
ILLEGAL_QUERY_EXPRESSION = hard_issue :ILLEGAL_QUERY_EXPRESSION do
"Illegal query expression. #{label.a_an_uc(semantic)} cannot be used in a query"
end
# If an attempt is made to make a resource default virtual or exported.
#
NOT_VIRTUALIZEABLE = hard_issue :NOT_VIRTUALIZEABLE do
"Resource Defaults are not virtualizable"
end
# When an attempt is made to use multiple keys (to produce a range in Ruby - e.g. $arr[2,-1]).
- # This is currently not supported, but may be in future versions
+ # This is not supported in 3x, but it allowed in 4x.
#
UNSUPPORTED_RANGE = issue :UNSUPPORTED_RANGE, :count do
"Attempt to use unsupported range in #{label.a_an(semantic)}, #{count} values given for max 1"
end
DEPRECATED_NAME_AS_TYPE = issue :DEPRECATED_NAME_AS_TYPE, :name do
"Resource references should now be capitalized. The given '#{name}' does not have the correct form"
end
+
+ ILLEGAL_RELATIONSHIP_OPERAND_TYPE = issue :ILLEGAL_RELATIONSHIP_OPERAND_TYPE, :operand do
+ "Illegal relationship operand, can not form a relationship with #{label.a_an(operand)}. A Catalog type is required."
+ end
+
+ NOT_CATALOG_TYPE = issue :NOT_CATALOG_TYPE, :type do
+ "Illegal relationship operand, can not form a relationship with something of type #{type}. A Catalog type is required."
+ end
+
+ BAD_STRING_SLICE_ARITY = issue :BAD_STRING_SLICE_ARITY, :actual do
+ "String supports [] with one or two arguments. Got #{actual}"
+ end
+
+ BAD_STRING_SLICE_TYPE = issue :BAD_STRING_SLICE_TYPE, :actual do
+ "String-Type [] requires all arguments to be integers (or default). Got #{actual}"
+ end
+
+ BAD_ARRAY_SLICE_ARITY = issue :BAD_ARRAY_SLICE_ARITY, :actual do
+ "Array supports [] with one or two arguments. Got #{actual}"
+ end
+
+ BAD_HASH_SLICE_ARITY = issue :BAD_HASH_SLICE_ARITY, :actual do
+ "Hash supports [] with one or more arguments. Got #{actual}"
+ end
+
+ BAD_INTEGER_SLICE_ARITY = issue :BAD_INTEGER_SLICE_ARITY, :actual do
+ "Integer-Type supports [] with one or two arguments (from, to). Got #{actual}"
+ end
+
+ BAD_INTEGER_SLICE_TYPE = issue :BAD_INTEGER_SLICE_TYPE, :actual do
+ "Integer-Type [] requires all arguments to be integers (or default). Got #{actual}"
+ end
+
+ BAD_COLLECTION_SLICE_TYPE = issue :BAD_COLLECTION_SLICE_TYPE, :actual do
+ "A Type's size constraint arguments must be a single Integer type, or 1-2 integers (or default). Got #{label.a_an(actual)}"
+ end
+
+ BAD_FLOAT_SLICE_ARITY = issue :BAD_INTEGER_SLICE_ARITY, :actual do
+ "Float-Type supports [] with one or two arguments (from, to). Got #{actual}"
+ end
+
+ BAD_FLOAT_SLICE_TYPE = issue :BAD_INTEGER_SLICE_TYPE, :actual do
+ "Float-Type [] requires all arguments to be floats, or integers (or default). Got #{actual}"
+ end
+
+ BAD_SLICE_KEY_TYPE = issue :BAD_SLICE_KEY_TYPE, :left_value, :expected_classes, :actual do
+ expected_text = if expected_classes.size > 1
+ "one of #{expected_classes.join(', ')} are"
+ else
+ "#{expected_classes[0]} is"
+ end
+ "#{label.a_an_uc(left_value)}[] cannot use #{actual} where #{expected_text} expected"
+ end
+
+ BAD_TYPE_SLICE_TYPE = issue :BAD_TYPE_SLICE_TYPE, :base_type, :actual do
+ "#{base_type}[] arguments must be types. Got #{actual}"
+ end
+
+ BAD_TYPE_SLICE_ARITY = issue :BAD_TYPE_SLICE_ARITY, :base_type, :min, :max, :actual do
+ base_type_label = base_type.is_a?(String) ? base_type : label.a_an_uc(base_type)
+ if max == -1 || max == 1.0 / 0.0 # Infinity
+ "#{base_type_label}[] accepts #{min} or more arguments. Got #{actual}"
+ elsif max
+ "#{base_type_label}[] accepts #{min} to #{max} arguments. Got #{actual}"
+ else
+ "#{base_type_label}[] accepts #{min} #{label.plural_s(min, 'argument')}. Got #{actual}"
+ end
+ end
+
+ BAD_TYPE_SPECIALIZATION = hard_issue :BAD_TYPE_SPECIALIZATION, :type, :message do
+ "Error creating type specialization of #{label.a_an(type)}, #{message}"
+ end
+
+ ILLEGAL_TYPE_SPECIALIZATION = issue :ILLEGAL_TYPE_SPECIALIZATION, :kind do
+ "Cannot specialize an already specialized #{kind} type"
+ end
+
+ ILLEGAL_RESOURCE_SPECIALIZATION = issue :ILLEGAL_RESOURCE_SPECIALIZATION, :actual do
+ "First argument to Resource[] must be a resource type or a String. Got #{actual}."
+ end
+
+ ILLEGAL_HOSTCLASS_NAME = hard_issue :ILLEGAL_HOSTCLASS_NAME, :name do
+ "Illegal Class name in class reference. #{label.a_an_uc(name)} cannot be used where a String is expected"
+ end
+
+ # Issues when an expression is used where it is not legal.
+ # E.g. an arithmetic expression where a hostname is expected.
+ #
+ ILLEGAL_DEFINITION_NAME = hard_issue :ILLEGAL_DEFINTION_NAME, :name do
+ "Unacceptable name. The name '#{name}' is unacceptable as the name of #{label.a_an(semantic)}"
+ end
+
+ NOT_NUMERIC = issue :NOT_NUMERIC, :value do
+ "The value '#{value}' cannot be converted to Numeric."
+ end
+
+ UNKNOWN_FUNCTION = issue :UNKNOWN_FUNCTION, :name do
+ "Unknown function: '#{name}'."
+ end
+
+ UNKNOWN_VARIABLE = issue :UNKNOWN_VARIABLE, :name do
+ "Unknown variable: '#{name}'."
+ end
+
+ RUNTIME_ERROR = issue :RUNTIME_ERROR, :detail do
+ "Error while evaluating #{label.a_an(semantic)}, #{detail}"
+ end
+
+ UNKNOWN_RESOURCE_TYPE = issue :UNKNOWN_RESOURCE_TYPE, :type_name do
+ "Resource type not found: #{type_name.capitalize}"
+ end
+
+ UNKNOWN_RESOURCE = issue :UNKNOWN_RESOURCE, :type_name, :title do
+ "Resource not found: #{type_name.capitalize}['#{title}']"
+ end
+
+ UNKNOWN_RESOURCE_PARAMETER = issue :UNKNOWN_RESOURCE_PARAMETER, :type_name, :title, :param_name do
+ "The resource #{type_name.capitalize}['#{title}'] does not have a parameter called '#{param_name}'"
+ end
+
+ DIV_BY_ZERO = hard_issue :DIV_BY_ZERO do
+ "Division by 0"
+ end
+
+ RESULT_IS_INFINITY = hard_issue :RESULT_IS_INFINITY, :operator do
+ "The result of the #{operator} expression is Infinity"
+ end
+
+ # TODO_HEREDOC
+ EMPTY_HEREDOC_SYNTAX_SEGMENT = issue :EMPTY_HEREDOC_SYNTAX_SEGMENT, :syntax do
+ "Heredoc syntax specification has empty segment between '+' : '#{syntax}'"
+ end
+
+ ILLEGAL_EPP_PARAMETERS = issue :ILLEGAL_EPP_PARAMETERS do
+ "Ambiguous EPP parameter expression. Probably missing '<%-' before parameters to remove leading whitespace"
+ end
end
diff --git a/lib/puppet/pops/label_provider.rb b/lib/puppet/pops/label_provider.rb
index e8a75a784..3581c6183 100644
--- a/lib/puppet/pops/label_provider.rb
+++ b/lib/puppet/pops/label_provider.rb
@@ -1,71 +1,76 @@
# Provides a label for an object.
# This simple implementation calls #to_s on the given object, and handles articles 'a/an/the'.
#
class Puppet::Pops::LabelProvider
VOWELS = %w{a e i o u y}
SKIPPED_CHARACTERS = %w{" '}
A = "a"
AN = "an"
# Provides a label for the given object by calling `to_s` on the object.
# The intent is for this method to be overridden in concrete label providers.
def label o
o.to_s
end
# Produces a label for the given text with indefinite article (a/an)
def a_an o
text = label(o)
"#{article(text)} #{text}"
end
# Produces a label for the given text with indefinite article (A/An)
def a_an_uc o
text = label(o)
"#{article(text).capitalize} #{text}"
end
# Produces a label for the given text with *definitie article* (the).
def the o
"the #{label(o)}"
end
# Produces a label for the given text with *definitie article* (The).
def the_uc o
"The #{label(o)}"
end
+ # Appends 's' to (optional) text if count != 1 else an empty string
+ def plural_s(count, text = '')
+ count == 1 ? text : "#{text}s"
+ end
+
private
# Produces an *indefinite article* (a/an) for the given text ('a' if
# it starts with a vowel) This is obviously flawed in the general
# sense as may labels have punctuation at the start and this method
# does not translate punctuation to English words. Also, if a vowel is
# pronounced as a consonant, the article should not be "an".
#
def article s
article_for_letter(first_letter_of(s))
end
def first_letter_of(string)
char = string[0,1]
if SKIPPED_CHARACTERS.include? char
char = string[1,1]
end
if char == ""
raise Puppet::DevError, "<#{string}> does not appear to contain a word"
end
char
end
def article_for_letter(letter)
downcased = letter.downcase
if VOWELS.include? downcased
AN
else
A
end
end
end
diff --git a/lib/puppet/pops/model/ast_transformer.rb b/lib/puppet/pops/model/ast_transformer.rb
index 2d4504050..4016cc266 100644
--- a/lib/puppet/pops/model/ast_transformer.rb
+++ b/lib/puppet/pops/model/ast_transformer.rb
@@ -1,639 +1,663 @@
require 'puppet/parser/ast'
# The receiver of `import(file)` calls; once per imported file, or nil if imports are ignored
#
# Transforms a Pops::Model to classic Puppet AST.
# TODO: Documentation is currently skipped completely (it is only used for Rdoc)
#
class Puppet::Pops::Model::AstTransformer
AST = Puppet::Parser::AST
Model = Puppet::Pops::Model
attr_reader :importer
def initialize(source_file = "unknown-file", importer=nil)
@@transform_visitor ||= Puppet::Pops::Visitor.new(nil,"transform",0,0)
@@query_transform_visitor ||= Puppet::Pops::Visitor.new(nil,"query",0,0)
@@hostname_transform_visitor ||= Puppet::Pops::Visitor.new(nil,"hostname",0,0)
@importer = importer
@source_file = source_file
end
# Initialize klass from o (location) and hash (options to created instance).
# The object o is used to compute a source location. It may be nil. Source position is merged into
# the given options (non surgically). If o is non-nil, the first found source position going up
# the containment hierarchy is set. I.e. callers should pass nil if a source position is not wanted
# or known to be unobtainable for the object.
#
# @param o [Object, nil] object from which source position / location is obtained, may be nil
# @param klass [Class<Puppet::Parser::AST>] the ast class to create an instance of
# @param hash [Hash] hash with options for the class to create
#
def ast(o, klass, hash={})
# create and pass hash with file and line information
klass.new(merge_location(hash, o))
end
+ # THIS IS AN EXPENSIVE OPERATION
+ # The 3x AST requires line, pos etc. to be recorded directly in the AST nodes and this information
+ # must be computed.
+ # (Newer implementation only computes the information that is actually needed; typically when raising an
+ # exception).
+ #
def merge_location(hash, o)
if o
pos = {}
- source_pos = Puppet::Pops::Utils.find_adapter(o, Puppet::Pops::Adapters::SourcePosAdapter)
+ source_pos = Puppet::Pops::Utils.find_closest_positioned(o)
if source_pos
pos[:line] = source_pos.line
pos[:pos] = source_pos.pos
end
pos[:file] = @source_file if @source_file
hash = hash.merge(pos)
end
hash
end
# Transforms pops expressions into AST 3.1 statements/expressions
def transform(o)
+ begin
@@transform_visitor.visit_this(self,o)
+ rescue StandardError => e
+ loc_data = {}
+ merge_location(loc_data, o)
+ raise Puppet::ParseError.new("Error while transforming to Puppet 3 AST: #{e.message}",
+ loc_data[:file], loc_data[:line], loc_data[:pos], e)
+ end
end
# Transforms pops expressions into AST 3.1 query expressions
def query(o)
@@query_transform_visitor.visit_this(self, o)
end
# Transforms pops expressions into AST 3.1 hostnames
def hostname(o)
@@hostname_transform_visitor.visit_this(self, o)
end
- def transform_LiteralNumber(o)
+ def transform_LiteralFloat(o)
+ # Numbers are Names in the AST !! (Name a.k.a BareWord)
+ ast o, AST::Name, :value => o.value.to_s
+ end
+
+ def transform_LiteralInteger(o)
s = case o.radix
when 10
o.value.to_s
when 8
"0%o" % o.value
when 16
"0x%X" % o.value
else
"bad radix:" + o.value.to_s
end
# Numbers are Names in the AST !! (Name a.k.a BareWord)
ast o, AST::Name, :value => s
end
# Transforms all literal values to string (override for those that should not be AST::String)
#
def transform_LiteralValue(o)
ast o, AST::String, :value => o.value.to_s
end
def transform_LiteralBoolean(o)
ast o, AST::Boolean, :value => o.value
end
def transform_Factory(o)
transform(o.current)
end
def transform_ArithmeticExpression(o)
ast o, AST::ArithmeticOperator2, :lval => transform(o.left_expr), :rval=>transform(o.right_expr),
:operator => o.operator.to_s
end
def transform_Array(o)
ast nil, AST::ASTArray, :children => o.collect {|x| transform(x) }
end
# Puppet AST only allows:
# * variable[expression] => Hasharray Access
# * NAME [expressions] => Resource Reference(s)
# * type [epxressions] => Resource Reference(s)
# * HashArrayAccesses[expression] => HasharrayAccesses
#
# i.e. it is not possible to do `func()[3]`, `[1,2,3][$x]`, `{foo=>10, bar=>20}[$x]` etc. since
# LHS is not an expression
#
# Validation for 3.x semantics should validate the illegal cases. This transformation may fail,
# or ignore excess information if the expressions are not correct.
# This means that the transformation does not have to evaluate the lhs to detect the target expression.
#
# Hm, this seems to have changed, the LHS (variable) is evaluated if evaluateable, else it is used as is.
#
def transform_AccessExpression(o)
case o.left_expr
when Model::QualifiedName
ast o, AST::ResourceReference, :type => o.left_expr.value, :title => transform(o.keys)
when Model::QualifiedReference
ast o, AST::ResourceReference, :type => o.left_expr.value, :title => transform(o.keys)
when Model::VariableExpression
ast o, AST::HashOrArrayAccess, :variable => transform(o.left_expr), :key => transform(o.keys()[0])
else
ast o, AST::HashOrArrayAccess, :variable => transform(o.left_expr), :key => transform(o.keys()[0])
end
end
# Puppet AST has a complicated structure
# LHS can not be an expression, it must be a type (which is downcased).
# type = a downcased QualifiedName
#
def transform_CollectExpression(o)
raise "LHS is not a type" unless o.type_expr.is_a? Model::QualifiedReference
type = o.type_expr.value().downcase()
args = { :type => type }
# This somewhat peculiar encoding is used by the 3.1 AST.
query = transform(o.query)
if query.is_a? Symbol
args[:form] = query
else
args[:form] = query.form
args[:query] = query
query.type = type
end
if o.operations.size > 0
args[:override] = transform(o.operations)
end
ast o, AST::Collection, args
end
+ def transform_EppExpression(o)
+ # TODO: Not supported in 3x TODO_EPP
+ parameters = o.parameters.collect {|p| transform(p) }
+ args = { :parameters => parameters }
+ args[:children] = transform(o.body) unless is_nop?(o.body)
+ Puppet::Parser::AST::Epp.new(merge_location(args, o))
+ end
+
def transform_ExportedQuery(o)
if is_nop?(o.expr)
result = :exported
else
result = query(o.expr)
result.form = :exported
end
result
end
def transform_VirtualQuery(o)
if is_nop?(o.expr)
result = :virtual
else
result = query(o.expr)
result.form = :virtual
end
result
end
# Ensures transformation fails if a 3.1 non supported object is encountered in a query expression
#
def query_Object(o)
raise "Not a valid expression in a collection query: "+o.class.name
end
# Puppet AST only allows == and !=, and left expr is restricted, but right value is an expression
#
def query_ComparisonExpression(o)
if [:'==', :'!='].include? o.operator
ast o, AST::CollExpr, :test1 => query(o.left_expr), :oper => o.operator.to_s, :test2 => transform(o.right_expr)
else
raise "Not a valid comparison operator in a collection query: " + o.operator.to_s
end
end
def query_AndExpression(o)
ast o, AST::CollExpr, :test1 => query(o.left_expr), :oper => 'and', :test2 => query(o.right_expr)
end
def query_OrExpression(o)
ast o, AST::CollExpr, :test1 => query(o.left_expr), :oper => 'or', :test2 => query(o.right_expr)
end
def query_ParenthesizedExpression(o)
result = query(o.expr) # produces CollExpr
result.parens = true
result
end
def query_VariableExpression(o)
transform(o)
end
def query_QualifiedName(o)
transform(o)
end
def query_LiteralNumber(o)
transform(o) # number to string in correct radix
end
def query_LiteralString(o)
transform(o)
end
def query_LiteralBoolean(o)
transform(o)
end
def transform_QualifiedName(o)
ast o, AST::Name, :value => o.value
end
def transform_QualifiedReference(o)
ast o, AST::Type, :value => o.value
end
def transform_ComparisonExpression(o)
ast o, AST::ComparisonOperator, :operator => o.operator.to_s, :lval => transform(o.left_expr), :rval => transform(o.right_expr)
end
def transform_AndExpression(o)
ast o, AST::BooleanOperator, :operator => 'and', :lval => transform(o.left_expr), :rval => transform(o.right_expr)
end
def transform_OrExpression(o)
ast o, AST::BooleanOperator, :operator => 'or', :lval => transform(o.left_expr), :rval => transform(o.right_expr)
end
def transform_InExpression(o)
ast o, AST::InOperator, :lval => transform(o.left_expr), :rval => transform(o.right_expr)
end
- # This is a complex transformation from a modeled import to a Nop result (where the import took place),
- # and calls to perform import/parsing etc. during the transformation.
- # When testing syntax, the @importer does not have to be set, but it is not possible to check
- # the actual import without inventing a new AST::ImportExpression with nop effect when evaluating.
- def transform_ImportExpression(o)
- if importer
- o.files.each {|f|
- unless f.is_a? Model::LiteralString
- raise "Illegal import file expression. Must be a single quoted string"
- end
- importer.import(f.value)
- }
- end
- # Crazy stuff
- # Transformation of "import" needs to parse the other files at the time of transformation.
- # Then produce a :nop, since nothing should be evaluated.
- ast o, AST::Nop, {}
- end
-
- def transform_InstanceReferences(o)
- ast o, AST::ResourceReference, :type => o.type_name.value, :title => transform(o.names)
- end
-
# Assignment in AST 3.1 is to variable or hasharray accesses !!! See Bug #16116
def transform_AssignmentExpression(o)
args = {:value => transform(o.right_expr) }
- args[:append] = true if o.operator == :'+='
+ case o.operator
+ when :'+='
+ args[:append] = true
+ when :'='
+ else
+ raise "The operator #{o.operator} is not supported by Puppet 3."
+ end
args[:name] = case o.left_expr
when Model::VariableExpression
ast o, AST::Name, {:value => o.left_expr.expr.value }
when Model::AccessExpression
transform(o.left_expr)
else
raise "LHS is not an expression that can be assigned to"
end
ast o, AST::VarDef, args
end
# Produces (name => expr) or (name +> expr)
def transform_AttributeOperation(o)
args = { :value => transform(o.value_expr) }
args[:add] = true if o.operator == :'+>'
args[:param] = o.attribute_name
ast o, AST::ResourceParam, args
end
def transform_LiteralList(o)
# Uses default transform of Ruby Array to ASTArray
transform(o.values)
end
# Literal hash has strange behavior in Puppet 3.1. See Bug #19426, and this implementation is bug
# compatible
def transform_LiteralHash(o)
if o.entries.size == 0
ast o, AST::ASTHash, {:value=> {}}
else
value = {}
o.entries.each {|x| value.merge! transform(x) }
ast o, AST::ASTHash, {:value=> value}
end
end
# Transforms entry into a hash (they are later merged with strange effects: Bug #19426).
# Puppet 3.x only allows:
# * NAME
# * quotedtext
# As keys (quoted text can be an interpolated string which is compared as a key in a less than satisfactory way).
#
def transform_KeyedEntry(o)
value = transform(o.value)
key = case o.key
when Model::QualifiedName
o.key.value
when Model::LiteralString
transform o.key
when Model::LiteralNumber
transform o.key
when Model::ConcatenatedString
transform o.key
else
raise "Illegal hash key expression of type (#{o.key.class})"
end
{key => value}
end
def transform_MatchExpression(o)
ast o, AST::MatchOperator, :operator => o.operator.to_s, :lval => transform(o.left_expr), :rval => transform(o.right_expr)
end
def transform_LiteralString(o)
ast o, AST::String, :value => o.value
end
- # Literal text in a concatenated string
- def transform_LiteralText(o)
- ast o, AST::String, :value => o.value
- end
-
def transform_LambdaExpression(o)
astargs = { :parameters => o.parameters.collect {|p| transform(p) } }
astargs.merge!({ :children => transform(o.body) }) if o.body # do not want children if it is nil/nop
ast o, AST::Lambda, astargs
end
def transform_LiteralDefault(o)
ast o, AST::Default, :value => :default
end
def transform_LiteralUndef(o)
ast o, AST::Undef, :value => :undef
end
def transform_LiteralRegularExpression(o)
ast o, AST::Regex, :value => o.value
end
def transform_Nop(o)
ast o, AST::Nop
end
# In the 3.1. grammar this is a hash that is merged with other elements to form a method call
# Also in 3.1. grammar there are restrictions on the LHS (that are only there for grammar issues).
#
def transform_NamedAccessExpression(o)
receiver = transform(o.left_expr)
name = o.right_expr
raise "Unacceptable function/method name" unless name.is_a? Model::QualifiedName
{:receiver => receiver, :name => name.value}
end
def transform_NilClass(o)
ast o, AST::Nop, {}
end
def transform_NotExpression(o)
ast o, AST::Not, :value => transform(o.expr)
end
def transform_VariableExpression(o)
# assumes the expression is a QualifiedName
ast o, AST::Variable, :value => o.expr.value
end
# In Puppet 3.1, the ConcatenatedString is responsible for the evaluation and stringification of
# expression segments. Expressions and Strings are kept in an array.
def transform_TextExpression(o)
transform(o.expr)
end
def transform_UnaryMinusExpression(o)
ast o, AST::Minus, :value => transform(o.expr)
end
# Puppet 3.1 representation of a BlockExpression is an AST::Array - this makes it impossible to differentiate
# between a LiteralArray and a Sequence. (Should it return the collected array, or the last expression?)
# (A BlockExpression has now been introduced in the AST to solve this).
#
def transform_BlockExpression(o)
children = []
# remove nops resulting from import
o.statements.each {|s| r = transform(s); children << r unless is_nop?(r) }
ast o, AST::BlockExpression, :children => children # o.statements.collect {|s| transform(s) }
end
# Interpolated strings are kept in an array of AST (string or other expression).
def transform_ConcatenatedString(o)
ast o, AST::Concat, :value => o.segments.collect {|x| transform(x)}
end
def transform_HostClassDefinition(o)
parameters = o.parameters.collect {|p| transform(p) }
args = {
:arguments => parameters,
:parent => o.parent_class,
}
args[:code] = transform(o.body) unless is_nop?(o.body)
Puppet::Parser::AST::Hostclass.new(o.name, merge_location(args, o))
end
+ def transform_HeredocExpression(o)
+ # TODO_HEREDOC Not supported in 3x
+ args = {:syntax=> o.syntax(), :expr => transform(o.text_expr()) }
+ Puppet::Parser::AST::Heredoc.new(merge_location(args, o))
+ end
+
def transform_NodeDefinition(o)
# o.host_matches are expressions, and 3.1 AST requires special object AST::HostName
# where a HostName is one of NAME, STRING, DEFAULT or Regexp - all of these are strings except regexp
#
args = {
:code => transform(o.body)
}
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
# Transforms Array of host matching expressions into a (Ruby) array of AST::HostName
def hostname_Array(o)
o.collect {|x| ast x, AST::HostName, :value => hostname(x) }
end
def hostname_LiteralValue(o)
return o.value
end
def hostname_QualifiedName(o)
return o.value
end
def hostname_LiteralNumber(o)
transform(o) # Number to string with correct radix
end
def hostname_LiteralDefault(o)
return 'default'
end
def hostname_LiteralRegularExpression(o)
ast o, AST::Regex, :value => o.value
end
def hostname_Object(o)
raise "Illegal expression - unacceptable as a node name"
end
def transform_RelationshipExpression(o)
Puppet::Parser::AST::Relationship.new(transform(o.left_expr), transform(o.right_expr), o.operator.to_s, merge_location({}, o))
end
+ def transform_RenderStringExpression(o)
+ # TODO_EPP Not supported in 3x
+ ast o, AST::RenderString, :value => o.value
+ end
+
+ def transform_RenderExpression(o)
+ # TODO_EPP Not supported in 3x
+ ast o, AST::RenderExpression, :value => transform(o.expr)
+ end
+
def transform_ResourceTypeDefinition(o)
parameters = o.parameters.collect {|p| transform(p) }
args = { :arguments => parameters }
args[:code] = transform(o.body) unless is_nop?(o.body)
Puppet::Parser::AST::Definition.new(o.name, merge_location(args, o))
end
# Transformation of ResourceOverrideExpression is slightly more involved than a straight forward
# transformation.
# A ResourceOverrideExppression has "resources" which should be an AccessExpression
# on the form QualifiedName[expressions], or QualifiedReference[expressions] to be valid.
# It also has a set of attribute operations.
#
# The AST equivalence is an AST::ResourceOverride with a ResourceReference as its LHS, and
# a set of Parameters.
# ResourceReference has type as a string, and the expressions representing
# the "titles" to be an ASTArray.
#
def transform_ResourceOverrideExpression(o)
resource_ref = o.resources
raise "Unacceptable expression for resource override" unless resource_ref.is_a? Model::AccessExpression
type = case resource_ref.left_expr
when Model::QualifiedName
# This is deprecated "Resource references should now be capitalized" - this is caught elsewhere
resource_ref.left_expr.value
when Model::QualifiedReference
resource_ref.left_expr.value
else
raise "Unacceptable expression for resource override; need NAME or CLASSREF"
end
result_ref = ast o, AST::ResourceReference, :type => type, :title => transform(resource_ref.keys)
# title is one or more expressions, if more than one it should be an ASTArray
ast o, AST::ResourceOverride, :object => result_ref, :parameters => transform(o.operations)
end
# Parameter is a parameter in a definition of some kind.
# It is transformed to an array on the form `[name]´, or `[name, value]´.
def transform_Parameter(o)
if o.value
[o.name, transform(o.value)]
else
[o.name]
end
end
# For non query expressions, parentheses can be dropped in the resulting AST.
def transform_ParenthesizedExpression(o)
transform(o.expr)
end
+ def transform_Program(o)
+ transform(o.body)
+ end
+
def transform_IfExpression(o)
args = { :test => transform(o.test), :statements => transform(o.then_expr) }
args[:else] = transform(o.else_expr) # Tests say Nop should be there (unless is_nop? o.else_expr), probably not needed
ast o, AST::IfStatement, args
end
# Unless is not an AST object, instead an AST::IfStatement is used with an AST::Not around the test
#
def transform_UnlessExpression(o)
args = { :test => ast(o, AST::Not, :value => transform(o.test)),
:statements => transform(o.then_expr) }
# AST 3.1 does not allow else on unless in the grammar, but it is ok since unless is encoded as an if !x
args.merge!({:else => transform(o.else_expr)}) unless is_nop?(o.else_expr)
ast o, AST::IfStatement, args
end
# Puppet 3.1 AST only supports calling a function by name (it is not possible to produce a function
# that is then called).
# rval_required (for an expression)
# functor_expr (lhs - the "name" expression)
# arguments - list of arguments
#
def transform_CallNamedFunctionExpression(o)
name = o.functor_expr
raise "Unacceptable expression for name of function" unless name.is_a? Model::QualifiedName
args = {
:name => name.value,
:arguments => transform(o.arguments),
:ftype => o.rval_required ? :rvalue : :statement
}
args[:pblock] = transform(o.lambda) if o.lambda
ast o, AST::Function, args
end
# Transformation of CallMethodExpression handles a NamedAccessExpression functor and
# turns this into a 3.1 AST::MethodCall.
#
def transform_CallMethodExpression(o)
name = o.functor_expr
raise "Unacceptable expression for name of function" unless name.is_a? Model::NamedAccessExpression
# transform of NamedAccess produces a hash, add arguments to it
astargs = transform(name).merge(:arguments => transform(o.arguments))
astargs.merge!(:lambda => transform(o.lambda)) if o.lambda # do not want a Nop as the lambda
ast o, AST::MethodCall, astargs
end
def transform_CaseExpression(o)
# Expects expression, AST::ASTArray of AST
ast o, AST::CaseStatement, :test => transform(o.test), :options => transform(o.options)
end
def transform_CaseOption(o)
ast o, AST::CaseOpt, :value => transform(o.values), :statements => transform(o.then_expr)
end
def transform_ResourceBody(o)
# expects AST, AST::ASTArray of AST
ast o, AST::ResourceInstance, :title => transform(o.title), :parameters => transform(o.operations)
end
def transform_ResourceDefaultsExpression(o)
ast o, AST::ResourceDefaults, :type => o.type_ref.value, :parameters => transform(o.operations)
end
# Transformation of ResourceExpression requires calling a method on the resulting
# AST::Resource if it is virtual or exported
#
def transform_ResourceExpression(o)
raise "Unacceptable type name expression" unless o.type_name.is_a? Model::QualifiedName
resource = ast o, AST::Resource, :type => o.type_name.value, :instances => transform(o.bodies)
resource.send("#{o.form}=", true) unless o.form == :regular
resource
end
# Transformation of SelectorExpression is limited to certain types of expressions.
# This is probably due to constraints in the old grammar rather than any real concerns.
def transform_SelectorExpression(o)
case o.left_expr
when Model::CallNamedFunctionExpression
when Model::AccessExpression
when Model::VariableExpression
when Model::ConcatenatedString
else
raise "Unacceptable select expression" unless o.left_expr.kind_of? Model::Literal
end
ast o, AST::Selector, :param => transform(o.left_expr), :values => transform(o.selectors)
end
def transform_SelectorEntry(o)
ast o, AST::ResourceParam, :param => transform(o.matching_expr), :value => transform(o.value_expr)
end
def transform_Object(o)
raise "Unacceptable transform - found an Object without a rule: #{o.class}"
end
# Nil, nop
# Bee bopp a luh-lah, a bop bop boom.
#
def is_nop?(o)
o.nil? || o.is_a?(Model::Nop)
end
end
diff --git a/lib/puppet/pops/model/ast_tree_dumper.rb b/lib/puppet/pops/model/ast_tree_dumper.rb
index 624b12267..00c4ae40a 100644
--- a/lib/puppet/pops/model/ast_tree_dumper.rb
+++ b/lib/puppet/pops/model/ast_tree_dumper.rb
@@ -1,378 +1,386 @@
require 'puppet/parser/ast'
# Dumps a Pops::Model in reverse polish notation; i.e. LISP style
# The intention is to use this for debugging output
# TODO: BAD NAME - A DUMP is a Ruby Serialization
#
class Puppet::Pops::Model::AstTreeDumper < Puppet::Pops::Model::TreeDumper
AST = Puppet::Parser::AST
Model = Puppet::Pops::Model
- def dump_LiteralNumber o
+ def dump_LiteralFloat o
+ o.value.to_s
+ end
+
+ def dump_LiteralInteger o
case o.radix
when 10
o.value.to_s
when 8
"0%o" % o.value
when 16
"0x%X" % o.value
else
"bad radix:" + o.value.to_s
end
end
+ def dump_Expression(o)
+ "(pops-expression #{Puppet::Pops::Model::ModelTreeDumper.new().dump(o.value)})"
+ end
+
def dump_Factory o
do_dump(o.current)
end
def dump_ArithmeticOperator o
[o.operator.to_s, do_dump(o.lval), do_dump(o.rval)]
end
def dump_Relationship o
[o.arrow.to_s, do_dump(o.left), do_dump(o.right)]
end
# Hostname is tricky, it is either a bare word, a string, or default, or regular expression
# Least evil, all strings except default are quoted
def dump_HostName o
result = do_dump o.value
unless o.value.is_a? AST::Regex
result = result == "default" ? ":default" : "'#{result}'"
end
result
end
# x[y] prints as (slice x y)
def dump_HashOrArrayAccess o
var = o.variable.is_a?(String) ? "$#{o.variable}" : do_dump(o.variable)
["slice", var, do_dump(o.key)]
end
# The AST Collection knows about exported or virtual query, not the query.
def dump_Collection o
result = ["collect", do_dump(o.type), :indent, :break]
if o.form == :virtual
q = ["<| |>"]
else
q = ["<<| |>>"]
end
q << do_dump(o.query) unless is_nop?(o.query)
q << :indent
result << q
o.override do |ao|
result << :break << do_dump(ao)
end
result += [:dedent, :dedent ]
result
end
def dump_CollExpr o
operator = case o.oper
when 'and'
'&&'
when 'or'
'||'
else
o.oper
end
[operator, do_dump(o.test1), do_dump(o.test2)]
end
def dump_ComparisonOperator o
[o.operator.to_s, do_dump(o.lval), do_dump(o.rval)]
end
def dump_Boolean o
o.to_s
end
def dump_BooleanOperator o
operator = o.operator == 'and' ? '&&' : '||'
[operator, do_dump(o.lval), do_dump(o.rval)]
end
def dump_InOperator o
["in", do_dump(o.lval), do_dump(o.rval)]
end
# $x = ...
# $x += ...
#
def dump_VarDef o
operator = o.append ? "+=" : "="
[operator, '$' + do_dump(o.name), do_dump(o.value)]
end
# Produces (name => expr) or (name +> expr)
def dump_ResourceParam o
operator = o.add ? "+>" : "=>"
[do_dump(o.param), operator, do_dump(o.value)]
end
def dump_Array o
o.collect {|e| do_dump(e) }
end
def dump_ASTArray o
["[]"] + o.children.collect {|x| do_dump(x)}
end
def dump_ASTHash o
["{}"] + o.value.sort_by{|k,v| k.to_s}.collect {|x| [do_dump(x[0]), do_dump(x[1])]}
# ["{}"] + o.value.collect {|x| [do_dump(x[0]), do_dump(x[1])]}
end
def dump_MatchOperator o
[o.operator.to_s, do_dump(o.lval), do_dump(o.rval)]
end
# Dump a Ruby String in single quotes unless it is a number.
def dump_String o
if o.is_a? String
o # A Ruby String, not quoted
elsif Puppet::Pops::Utils.to_n(o.value)
o.value # AST::String that is a number without quotes
else
"'#{o.value}'" # AST::String that is not a number
end
end
def dump_Lambda o
result = ["lambda"]
result << ["parameters"] + o.parameters.collect {|p| _dump_ParameterArray(p) } if o.parameters.size() > 0
if o.children == []
result << [] # does not have a lambda body
else
result << do_dump(o.children)
end
result
end
def dump_Default o
":default"
end
def dump_Undef o
":undef"
end
# Note this is Regex (the AST kind), not Ruby Regexp
def dump_Regex o
"/#{o.value.source}/"
end
def dump_Nop o
":nop"
end
def dump_NilClass o
"()"
end
def dump_Not o
['!', dump(o.value)]
end
def dump_Variable o
"$#{dump(o.value)}"
end
def dump_Minus o
['-', do_dump(o.value)]
end
def dump_BlockExpression o
["block"] + o.children.collect {|x| do_dump(x) }
end
# Interpolated strings are shown as (cat seg0 seg1 ... segN)
def dump_Concat o
["cat"] + o.value.collect {|x| x.is_a?(AST::String) ? " "+do_dump(x) : ["str", do_dump(x)]}
end
def dump_Hostclass o
# ok, this is kind of crazy stuff in the AST, information in a context instead of in AST, and
# parameters are in a Ruby Array with each parameter being an Array...
#
context = o.context
args = context[:arguments]
parent = context[:parent]
result = ["class", o.name]
result << ["inherits", parent] if parent
result << ["parameters"] + args.collect {|p| _dump_ParameterArray(p) } if args && args.size() > 0
if is_nop?(o.code)
result << []
else
result << do_dump(o.code)
end
result
end
def dump_Name o
o.value
end
def dump_Node o
context = o.context
parent = context[:parent]
code = context[:code]
result = ["node"]
result << ["matches"] + o.names.collect {|m| do_dump(m) }
result << ["parent", do_dump(parent)] if !is_nop?(parent)
if is_nop?(code)
result << []
else
result << do_dump(code)
end
result
end
def dump_Definition o
# ok, this is even crazier that Hostclass. The name of the define does not have an accessor
# and some things are in the context (but not the name). Parameters are called arguments and they
# are in a Ruby Array where each parameter is an array of 1 or 2 elements.
#
context = o.context
name = o.instance_variable_get("@name")
args = context[:arguments]
code = context[:code]
result = ["define", name]
result << ["parameters"] + args.collect {|p| _dump_ParameterArray(p) } if args && args.size() > 0
if is_nop?(code)
result << []
else
result << do_dump(code)
end
result
end
def dump_ResourceReference o
result = ["slice", do_dump(o.type)]
if o.title.children.size == 1
result << do_dump(o.title[0])
else
result << do_dump(o.title.children)
end
result
end
def dump_ResourceOverride o
result = ["override", do_dump(o.object), :indent]
o.parameters.each do |p|
result << :break << do_dump(p)
end
result << :dedent
result
end
# Puppet AST encodes a parameter as a one or two slot Array.
# This is not a polymorph dump method.
#
def _dump_ParameterArray o
if o.size == 2
["=", o[0], do_dump(o[1])]
else
o[0]
end
end
def dump_IfStatement o
result = ["if", do_dump(o.test), :indent, :break,
["then", :indent, do_dump(o.statements), :dedent]]
result +=
[:break,
["else", :indent, do_dump(o.else), :dedent],
:dedent] unless is_nop? o.else
result
end
# Produces (invoke name args...) when not required to produce an rvalue, and
# (call name args ... ) otherwise.
#
def dump_Function o
# somewhat ugly as Function hides its "ftype" instance variable
result = [o.instance_variable_get("@ftype") == :rvalue ? "call" : "invoke", do_dump(o.name)]
o.arguments.collect {|a| result << do_dump(a) }
result << do_dump(o.pblock) if o.pblock
result
end
def dump_MethodCall o
# somewhat ugly as Method call (does the same as function) and hides its "ftype" instance variable
result = [o.instance_variable_get("@ftype") == :rvalue ? "call-method" : "invoke-method",
[".", do_dump(o.receiver), do_dump(o.name)]]
o.arguments.collect {|a| result << do_dump(a) }
result << do_dump(o.lambda) if o.lambda
result
end
def dump_CaseStatement o
result = ["case", do_dump(o.test), :indent]
o.options.each do |s|
result << :break << do_dump(s)
end
result << :dedent
end
def dump_CaseOpt o
result = ["when"]
result << o.value.collect {|x| do_dump(x) }
# A bit of trickery to get it into the same shape as Pops output
if is_nop?(o.statements)
result << ["then", []] # Puppet AST has a nop if there is no body
else
result << ["then", do_dump(o.statements) ]
end
result
end
def dump_ResourceInstance o
result = [do_dump(o.title), :indent]
o.parameters.each do |p|
result << :break << do_dump(p)
end
result << :dedent
result
end
def dump_ResourceDefaults o
result = ["resource-defaults", do_dump(o.type), :indent]
o.parameters.each do |p|
result << :break << do_dump(p)
end
result << :dedent
result
end
def dump_Resource o
if o.exported
form = 'exported-'
elsif o.virtual
form = 'virtual-'
else
form = ''
end
result = [form+"resource", do_dump(o.type), :indent]
o.instances.each do |b|
result << :break << do_dump(b)
end
result << :dedent
result
end
def dump_Selector o
values = o.values
values = [values] unless values.instance_of? AST::ASTArray or values.instance_of? Array
["?", do_dump(o.param)] + values.collect {|x| do_dump(x) }
end
def dump_Object o
['dev-error-no-polymorph-dump-for:', o.class.to_s, o.to_s]
end
def is_nop? o
o.nil? || o.is_a?(Model::Nop) || o.is_a?(AST::Nop)
end
end
diff --git a/lib/puppet/pops/model/factory.rb b/lib/puppet/pops/model/factory.rb
index 3cfa15fca..83af10242 100644
--- a/lib/puppet/pops/model/factory.rb
+++ b/lib/puppet/pops/model/factory.rb
@@ -1,816 +1,970 @@
# Factory is a helper class that makes construction of a Pops Model
# much more convenient. It can be viewed as a small internal DSL for model
# constructions.
# For usage see tests using the factory.
#
# @todo All those uppercase methods ... they look bad in one way, but stand out nicely in the grammar...
# decide if they should change into lower case names (some of the are lower case)...
#
class Puppet::Pops::Model::Factory
Model = Puppet::Pops::Model
attr_accessor :current
alias_method :model, :current
# Shared build_visitor, since there are many instances of Factory being used
@@build_visitor = Puppet::Pops::Visitor.new(self, "build")
+ @@interpolation_visitor = Puppet::Pops::Visitor.new(self, "interpolate")
+
# Initialize a factory with a single object, or a class with arguments applied to build of
# created instance
#
- def initialize popsobj, *args
- @current = to_ops(popsobj, *args)
+ def initialize o, *args
+ @current = case o
+ when Model::PopsObject
+ o
+ when Puppet::Pops::Model::Factory
+ o.current
+ else
+ build(o, *args)
+ end
end
# Polymorphic build
def build(o, *args)
begin
@@build_visitor.visit_this(self, o, *args)
rescue =>e
- # require 'debugger'; debugger # enable this when in trouble...
+ # debug here when in trouble...
+ raise e
+ end
+ end
+
+ # Polymorphic interpolate
+ def interpolate()
+ begin
+ @@interpolation_visitor.visit_this_0(self, current)
+ rescue =>e
+ # debug here when in trouble...
raise e
end
end
# Building of Model classes
def build_ArithmeticExpression(o, op, a, b)
o.operator = op
build_BinaryExpression(o, a, b)
end
def build_AssignmentExpression(o, op, a, b)
o.operator = op
build_BinaryExpression(o, a, b)
end
def build_AttributeOperation(o, name, op, value)
o.operator = op
o.attribute_name = name.to_s # BOOLEAN is allowed in the grammar
o.value_expr = build(value)
o
end
def build_AccessExpression(o, left, *keys)
o.left_expr = to_ops(left)
keys.each {|expr| o.addKeys(to_ops(expr)) }
o
end
def build_BinaryExpression(o, left, right)
o.left_expr = to_ops(left)
o.right_expr = to_ops(right)
o
end
def build_BlockExpression(o, *args)
args.each {|expr| o.addStatements(to_ops(expr)) }
o
end
def build_CollectExpression(o, type_expr, query_expr, attribute_operations)
o.type_expr = to_ops(type_expr)
o.query = build(query_expr)
attribute_operations.each {|op| o.addOperations(build(op)) }
o
end
def build_ComparisonExpression(o, op, a, b)
o.operator = op
build_BinaryExpression(o, a, b)
end
def build_ConcatenatedString(o, *args)
args.each {|expr| o.addSegments(build(expr)) }
o
end
def build_CreateTypeExpression(o, name, super_name = nil)
o.name = name
o.super_name = super_name
o
end
def build_CreateEnumExpression(o, *args)
o.name = args.slice(0) if args.size == 2
o.values = build(args.last)
o
end
def build_CreateAttributeExpression(o, name, datatype_expr)
o.name = name
o.type = to_ops(datatype_expr)
o
end
+ def build_HeredocExpression(o, name, expr)
+ o.syntax = name
+ o.text_expr = build(expr)
+ o
+ end
+
# @param name [String] a valid classname
# @param parameters [Array<Model::Parameter>] may be empty
# @param parent_class_name [String, nil] a valid classname referencing a parent class, optional.
# @param body [Array<Expression>, Expression, nil] expression that constitute the body
# @return [Model::HostClassDefinition] configured from the parameters
#
def build_HostClassDefinition(o, name, parameters, parent_class_name, body)
build_NamedDefinition(o, name, parameters, body)
o.parent_class = parent_class_name if parent_class_name
o
end
- # # @param name [String] a valid classname
- # # @param parameters [Array<Model::Parameter>] may be empty
- # # @param body [Array<Expression>, Expression, nil] expression that constitute the body
- # # @return [Model::HostClassDefinition] configured from the parameters
- # #
- # def build_ResourceTypeDefinition(o, name, parameters, body)
- # build_NamedDefinition(o, name, parameters, body)
- # o.name = name
- # parameters.each {|p| o.addParameters(build(p)) }
- # b = f_build_body(body)
- # o.body = b.current if b
- # o
- # end
-
def build_ResourceOverrideExpression(o, resources, attribute_operations)
o.resources = build(resources)
attribute_operations.each {|ao| o.addOperations(build(ao)) }
o
end
def build_KeyedEntry(o, k, v)
- o.key = build(k)
- o.value = build(v)
+ o.key = to_ops(k)
+ o.value = to_ops(v)
o
end
def build_LiteralHash(o, *keyed_entries)
keyed_entries.each {|entry| o.addEntries build(entry) }
o
end
def build_LiteralList(o, *values)
values.each {|v| o.addValues build(v) }
o
end
- def build_LiteralNumber(o, val, radix)
+ def build_LiteralFloat(o, val)
o.value = val
- o.radix = radix
o
end
- def build_InstanceReferences(o, type_name, name_expressions)
- o.type_name = build(type_name)
- name_expressions.each {|n| o.addNames(build(n)) }
- o
- end
-
- def build_ImportExpression(o, files)
- # The argument files has already been built
- files.each {|f| o.addFiles(to_ops(f)) }
+ def build_LiteralInteger(o, val, radix)
+ o.value = val
+ o.radix = radix
o
end
def build_IfExpression(o, t, ift, els)
o.test = build(t)
o.then_expr = build(ift)
o.else_expr= build(els)
o
end
def build_MatchExpression(o, op, a, b)
o.operator = op
build_BinaryExpression(o, a, b)
end
# Builds body :) from different kinds of input
# @overload f_build_body(nothing)
# @param nothing [nil] unchanged, produces nil
# @overload f_build_body(array)
# @param array [Array<Expression>] turns into a BlockExpression
# @overload f_build_body(expr)
# @param expr [Expression] produces the given expression
# @overload f_build_body(obj)
# @param obj [Object] produces the result of calling #build with body as argument
def f_build_body(body)
case body
when NilClass
nil
when Array
Puppet::Pops::Model::Factory.new(Model::BlockExpression, *body)
else
build(body)
end
end
- def build_Definition(o, parameters, body)
+ def build_LambdaExpression(o, parameters, body)
parameters.each {|p| o.addParameters(build(p)) }
b = f_build_body(body)
- o.body = b.current if b
+ o.body = to_ops(b) if b
o
end
def build_NamedDefinition(o, name, parameters, body)
- build_Definition(o, parameters, body)
+ parameters.each {|p| o.addParameters(build(p)) }
+ b = f_build_body(body)
+ o.body = b.current if b
o.name = name
o
end
# @param o [Model::NodeDefinition]
# @param hosts [Array<Expression>] host matches
# @param parent [Expression] parent node matcher
# @param body [Object] see {#f_build_body}
def build_NodeDefinition(o, hosts, parent, body)
hosts.each {|h| o.addHost_matches(build(h)) }
o.parent = build(parent) if parent # no nop here
b = f_build_body(body)
o.body = b.current if b
o
end
def build_Parameter(o, name, expr)
o.name = name
o.value = build(expr) if expr # don't build a nil/nop
o
end
def build_QualifiedReference(o, name)
o.value = name.to_s.downcase
o
end
def build_RelationshipExpression(o, op, a, b)
o.operator = op
build_BinaryExpression(o, a, b)
end
def build_ResourceExpression(o, type_name, bodies)
o.type_name = build(type_name)
bodies.each {|b| o.addBodies(build(b)) }
o
end
+ def build_RenderStringExpression(o, string)
+ o.value = string;
+ o
+ end
+
def build_ResourceBody(o, title_expression, attribute_operations)
o.title = build(title_expression)
attribute_operations.each {|ao| o.addOperations(build(ao)) }
o
end
def build_ResourceDefaultsExpression(o, type_ref, attribute_operations)
o.type_ref = build(type_ref)
attribute_operations.each {|ao| o.addOperations(build(ao)) }
o
end
def build_SelectorExpression(o, left, *selectors)
o.left_expr = to_ops(left)
selectors.each {|s| o.addSelectors(build(s)) }
o
end
+ # Builds a SubLocatedExpression - this wraps the expression in a sublocation configured
+ # from the given token
+ # A SubLocated holds its own locator that is used for subexpressions holding positions relative
+ # to what it describes.
+ #
+ def build_SubLocatedExpression(o, token, expression)
+ o.expr = build(expression)
+ o.offset = token.offset
+ o.length = token.length
+ locator = token.locator
+ o.locator = locator
+ o.leading_line_count = locator.leading_line_count
+ o.leading_line_offset = locator.leading_line_offset
+ # Index is held in sublocator's parent locator - needed to be able to reconstruct
+ o.line_offsets = locator.locator.line_index
+ o
+ end
+
def build_SelectorEntry(o, matching, value)
o.matching_expr = build(matching)
o.value_expr = build(value)
o
end
def build_QueryExpression(o, expr)
ops = to_ops(expr)
o.expr = ops unless Puppet::Pops::Model::Factory.nop? ops
o
end
def build_UnaryExpression(o, expr)
ops = to_ops(expr)
o.expr = ops unless Puppet::Pops::Model::Factory.nop? ops
o
end
+ def build_Program(o, body, definitions, locator)
+ o.body = to_ops(body)
+ # non containment
+ definitions.each { |d| o.addDefinitions(d) }
+ o.source_ref = locator.file
+ o.source_text = locator.string
+ o.line_offsets = locator.line_index
+ o.locator = locator
+ o
+ end
+
def build_QualifiedName(o, name)
o.value = name.to_s
o
end
# Puppet::Pops::Model::Factory helpers
def f_build_unary(klazz, expr)
Puppet::Pops::Model::Factory.new(build(klazz.new, expr))
end
def f_build_binary_op(klazz, op, left, right)
Puppet::Pops::Model::Factory.new(build(klazz.new, op, left, right))
end
def f_build_binary(klazz, left, right)
Puppet::Pops::Model::Factory.new(build(klazz.new, left, right))
end
def f_build_vararg(klazz, left, *arg)
Puppet::Pops::Model::Factory.new(build(klazz.new, left, *arg))
end
def f_arithmetic(op, r)
f_build_binary_op(Model::ArithmeticExpression, op, current, r)
end
def f_comparison(op, r)
f_build_binary_op(Model::ComparisonExpression, op, current, r)
end
def f_match(op, r)
f_build_binary_op(Model::MatchExpression, op, current, r)
end
# Operator helpers
def in(r) f_build_binary(Model::InExpression, current, r); end
def or(r) f_build_binary(Model::OrExpression, current, r); end
def and(r) f_build_binary(Model::AndExpression, current, r); end
def not(); f_build_unary(Model::NotExpression, self); end
def minus(); f_build_unary(Model::UnaryMinusExpression, self); end
def text(); f_build_unary(Model::TextExpression, self); end
def var(); f_build_unary(Model::VariableExpression, self); end
def [](*r); f_build_vararg(Model::AccessExpression, current, *r); end
def dot r; f_build_binary(Model::NamedAccessExpression, current, r); end
def + r; f_arithmetic(:+, r); end
def - r; f_arithmetic(:-, r); end
def / r; f_arithmetic(:/, r); end
def * r; f_arithmetic(:*, r); end
def % r; f_arithmetic(:%, r); end
def << r; f_arithmetic(:<<, r); end
def >> r; f_arithmetic(:>>, r); end
def < r; f_comparison(:<, r); end
def <= r; f_comparison(:<=, r); end
def > r; f_comparison(:>, r); end
def >= r; f_comparison(:>=, r); end
def == r; f_comparison(:==, r); end
def ne r; f_comparison(:'!=', r); end
def =~ r; f_match(:'=~', r); end
def mne r; f_match(:'!~', r); end
def paren(); f_build_unary(Model::ParenthesizedExpression, current); end
def relop op, r
f_build_binary_op(Model::RelationshipExpression, op.to_sym, current, r)
end
def select *args
Puppet::Pops::Model::Factory.new(build(Model::SelectorExpression, current, *args))
end
# For CaseExpression, setting the default for an already build CaseExpression
def default r
current.addOptions(Puppet::Pops::Model::Factory.WHEN(:default, r).current)
self
end
def lambda=(lambda)
current.lambda = lambda.current
self
end
# Assignment =
def set(r)
f_build_binary_op(Model::AssignmentExpression, :'=', current, r)
end
# Assignment +=
def plus_set(r)
f_build_binary_op(Model::AssignmentExpression, :'+=', current, r)
end
+ # Assignment -=
+ def minus_set(r)
+ f_build_binary_op(Model::AssignmentExpression, :'-=', current, r)
+ end
+
def attributes(*args)
args.each {|a| current.addAttributes(build(a)) }
self
end
# Catch all delegation to current
def method_missing(meth, *args, &block)
if current.respond_to?(meth)
current.send(meth, *args, &block)
else
super
end
end
- def respond_to?(meth)
- current.respond_to?(meth) || super
+ def respond_to?(meth, include_all=false)
+ current.respond_to?(meth, include_all) || super
end
# Records the position (start -> end) and computes the resulting length.
#
- def record_position(start_pos, end_pos)
- Puppet::Pops::Adapters::SourcePosAdapter.adapt(current) do |a|
- a.line = start_pos.line
- a.offset = start_pos.offset
- a.pos = start_pos.pos
- a.length = start_pos.length
- if(end_pos.offset && end_pos.length)
- a.length = end_pos.offset + end_pos.length - start_pos.offset
- end
- end
- self
- end
-
- # Records the origin file of an element
- # Does nothing if file is nil.
- #
- # @param file [String,nil] the file/path to the origin, may contain URI scheme of file: or some other URI scheme
- # @return [Factory] returns self
- #
- def record_origin(file)
- return self unless file
- Puppet::Pops::Adapters::OriginAdapter.adapt(current) do |a|
- a.origin = file
- end
+ def record_position(start_locatable, end_locatable)
+ from = start_locatable.is_a?(Puppet::Pops::Model::Factory) ? start_locatable.current : start_locatable
+ to = end_locatable.is_a?(Puppet::Pops::Model::Factory) ? end_locatable.current : end_locatable
+ to = from if to.nil?
+ o = current
+ # record information directly in the Model::Positioned object
+ o.offset = from.offset
+ o.length ||= to.offset - from.offset + to.length
self
end
# @return [Puppet::Pops::Adapters::SourcePosAdapter] with location information
def loc()
Puppet::Pops::Adapters::SourcePosAdapter.adapt(current)
end
- # Returns documentation string, or nil if not available
- # @return [String, nil] associated documentation if available
- def doc()
- a = Puppet::Pops::Adapters::SourcePosAdapter.adapt(current)
- return a.documentation if a
- nil
- end
-
- def doc=(doc_string)
- a = Puppet::Pops::Adapters::SourcePosAdapter.adapt(current)
- a.documentation = doc_string
- end
-
# Returns symbolic information about an expected share of a resource expression given the LHS of a resource expr.
#
# * `name { }` => `:resource`, create a resource of the given type
# * `Name { }` => ':defaults`, set defaults for the referenced type
# * `Name[] { }` => `:override`, overrides instances referenced by LHS
# * _any other_ => ':error', all other are considered illegal
#
def self.resource_shape(expr)
expr = expr.current if expr.is_a?(Puppet::Pops::Model::Factory)
case expr
when Model::QualifiedName
:resource
when Model::QualifiedReference
:defaults
when Model::AccessExpression
:override
when 'class'
:class
else
:error
end
end
# Factory starting points
def self.literal(o); new(o); end
def self.minus(o); new(o).minus; end
def self.var(o); new(o).var; end
def self.block(*args); new(Model::BlockExpression, *args); end
def self.string(*args); new(Model::ConcatenatedString, *args); end
def self.text(o); new(o).text; end
def self.IF(test_e,then_e,else_e); new(Model::IfExpression, test_e, then_e, else_e); end
def self.UNLESS(test_e,then_e,else_e); new(Model::UnlessExpression, test_e, then_e, else_e); end
def self.CASE(test_e,*options); new(Model::CaseExpression, test_e, *options); end
def self.WHEN(values_list, block); new(Model::CaseOption, values_list, block); end
def self.MAP(match, value); new(Model::SelectorEntry, match, value); end
def self.TYPE(name, super_name=nil); new(Model::CreateTypeExpression, name, super_name); end
def self.ATTR(name, type_expr=nil); new(Model::CreateAttributeExpression, name, type_expr); end
def self.ENUM(*args); new(Model::CreateEnumExpression, *args); end
def self.KEY_ENTRY(key, val); new(Model::KeyedEntry, key, val); end
def self.HASH(entries); new(Model::LiteralHash, *entries); end
+ # TODO_HEREDOC
+ def self.HEREDOC(name, expr); new(Model::HeredocExpression, name, expr); end
+
+ def self.SUBLOCATE(token, expr) new(Model::SubLocatedExpression, token, expr); end
+
def self.LIST(entries); new(Model::LiteralList, *entries); end
def self.PARAM(name, expr=nil); new(Model::Parameter, name, expr); end
def self.NODE(hosts, parent, body); new(Model::NodeDefinition, hosts, parent, body); end
# Creates a QualifiedName representation of o, unless o already represents a QualifiedName in which
# case it is returned.
#
def self.fqn(o)
o = o.current if o.is_a?(Puppet::Pops::Model::Factory)
o = new(Model::QualifiedName, o) unless o.is_a? Model::QualifiedName
o
end
# Creates a QualifiedName representation of o, unless o already represents a QualifiedName in which
# case it is returned.
#
def self.fqr(o)
o = o.current if o.is_a?(Puppet::Pops::Model::Factory)
o = new(Model::QualifiedReference, o) unless o.is_a? Model::QualifiedReference
o
end
def self.TEXT(expr)
- new(Model::TextExpression, expr)
+ new(Model::TextExpression, new(expr).interpolate)
+ end
+
+ # TODO_EPP
+ def self.RENDER_STRING(o)
+ new(Model::RenderStringExpression, o)
+ end
+
+ def self.RENDER_EXPR(expr)
+ new(Model::RenderExpression, expr)
+ end
+
+ def self.EPP(parameters, body)
+ see_scope = false
+ params = parameters
+ if parameters.nil?
+ params = []
+ see_scope = true
+ end
+ LAMBDA(params, new(Model::EppExpression, see_scope, body))
end
# TODO: This is the same a fqn factory method, don't know if callers to fqn and QNAME can live with the
# same result or not yet - refactor into one method when decided.
#
def self.QNAME(name)
new(Model::QualifiedName, name)
end
- # Convert input string to either a qualified name, or a LiteralNumber with radix
+ def self.NUMBER(name_or_numeric)
+ if n_radix = Puppet::Pops::Utils.to_n_with_radix(name_or_numeric)
+ val, radix = n_radix
+ if val.is_a?(Float)
+ new(Model::LiteralFloat, val)
+ else
+ new(Model::LiteralInteger, val, radix)
+ end
+ else
+ # Bad number should already have been caught by lexer - this should never happen
+ raise ArgumentError, "Internal Error, NUMBER token does not contain a valid number, #{name_or_numeric}"
+ end
+ end
+
+ # Convert input string to either a qualified name, a LiteralInteger with radix, or a LiteralFloat
#
def self.QNAME_OR_NUMBER(name)
if n_radix = Puppet::Pops::Utils.to_n_with_radix(name)
- new(Model::LiteralNumber, *n_radix)
+ val, radix = n_radix
+ if val.is_a?(Float)
+ new(Model::LiteralFloat, val)
+ else
+ new(Model::LiteralInteger, val, radix)
+ end
else
new(Model::QualifiedName, name)
end
end
def self.QREF(name)
new(Model::QualifiedReference, name)
end
def self.VIRTUAL_QUERY(query_expr)
new(Model::VirtualQuery, query_expr)
end
def self.EXPORTED_QUERY(query_expr)
new(Model::ExportedQuery, query_expr)
end
- # Used by regular grammar, egrammar creates an AccessExpression instead, and evaluation determines
- # if access is to instances or something else.
- #
- def self.INSTANCE(type_name, name_expressions)
- new(Model::InstanceReferences, type_name, name_expressions)
- end
-
def self.ATTRIBUTE_OP(name, op, expr)
new(Model::AttributeOperation, name, op, expr)
end
def self.CALL_NAMED(name, rval_required, argument_list)
unless name.kind_of?(Model::PopsObject)
name = Puppet::Pops::Model::Factory.fqn(name) unless name.is_a?(Puppet::Pops::Model::Factory)
end
new(Model::CallNamedFunctionExpression, name, rval_required, *argument_list)
end
def self.CALL_METHOD(functor, argument_list)
new(Model::CallMethodExpression, functor, true, nil, *argument_list)
end
def self.COLLECT(type_expr, query_expr, attribute_operations)
- new(Model::CollectExpression, Puppet::Pops::Model::Factory.fqr(type_expr), query_expr, attribute_operations)
- end
-
- def self.IMPORT(files)
- new(Model::ImportExpression, files)
+ new(Model::CollectExpression, type_expr, query_expr, attribute_operations)
end
def self.NAMED_ACCESS(type_name, bodies)
new(Model::NamedAccessExpression, type_name, bodies)
end
def self.RESOURCE(type_name, bodies)
new(Model::ResourceExpression, type_name, bodies)
end
def self.RESOURCE_DEFAULTS(type_name, attribute_operations)
new(Model::ResourceDefaultsExpression, type_name, attribute_operations)
end
def self.RESOURCE_OVERRIDE(resource_ref, attribute_operations)
new(Model::ResourceOverrideExpression, resource_ref, attribute_operations)
end
def self.RESOURCE_BODY(resource_title, attribute_operations)
new(Model::ResourceBody, resource_title, attribute_operations)
end
+ def self.PROGRAM(body, definitions, locator)
+ new(Model::Program, body, definitions, locator)
+ end
+
# Builds a BlockExpression if args size > 1, else the single expression/value in args
def self.block_or_expression(*args)
if args.size > 1
new(Model::BlockExpression, *args)
else
new(args[0])
end
end
def self.HOSTCLASS(name, parameters, parent, body)
new(Model::HostClassDefinition, name, parameters, parent, body)
end
def self.DEFINITION(name, parameters, body)
new(Model::ResourceTypeDefinition, name, parameters, body)
end
def self.LAMBDA(parameters, body)
new(Model::LambdaExpression, parameters, body)
end
def self.nop? o
o.nil? || o.is_a?(Puppet::Pops::Model::Nop)
end
+ STATEMENT_CALLS = {
+ 'require' => true,
+ 'realize' => true,
+ 'include' => true,
+ 'contain' => true,
+
+ 'debug' => true,
+ 'info' => true,
+ 'notice' => true,
+ 'warning' => true,
+ 'error' => true,
+
+ 'fail' => true,
+ }
+ # Returns true if the given name is a "statement keyword" (require, include, contain,
+ # error, notice, info, debug
+ #
+ def name_is_statement(name)
+ STATEMENT_CALLS[name]
+ end
+
# Transforms an array of expressions containing literal name expressions to calls if followed by an
- # expression, or expression list. Also transforms a "call" to `import` into an ImportExpression.
+ # expression, or expression list.
#
def self.transform_calls(expressions)
expressions.reduce([]) do |memo, expr|
expr = expr.current if expr.is_a?(Puppet::Pops::Model::Factory)
name = memo[-1]
- if name.is_a? Model::QualifiedName
- if name.value() == 'import'
- memo[-1] = Puppet::Pops::Model::Factory.IMPORT(expr.is_a?(Array) ? expr : [expr])
- else
- memo[-1] = Puppet::Pops::Model::Factory.CALL_NAMED(name, false, expr.is_a?(Array) ? expr : [expr])
- if expr.is_a?(Model::CallNamedFunctionExpression)
- # Patch statement function call to expression style
- # This is needed because it is first parsed as a "statement" and the requirement changes as it becomes
- # an argument to the name to call transform above.
- expr.rval_required = true
- end
+ if name.is_a?(Model::QualifiedName) && STATEMENT_CALLS[name.value]
+ memo[-1] = Puppet::Pops::Model::Factory.CALL_NAMED(name, false, expr.is_a?(Array) ? expr : [expr])
+ if expr.is_a?(Model::CallNamedFunctionExpression)
+ # Patch statement function call to expression style
+ # This is needed because it is first parsed as a "statement" and the requirement changes as it becomes
+ # an argument to the name to call transform above.
+ expr.rval_required = true
end
else
memo << expr
if expr.is_a?(Model::CallNamedFunctionExpression)
# Patch rvalue expression function call to statement style.
# This is not really required but done to be AST model compliant
expr.rval_required = false
end
end
memo
end
end
+ # Transforms a left expression followed by an untitled resource (in the form of attribute_operations)
+ # @param left [Factory, Expression] the lhs followed what may be a hash
+ def self.transform_resource_wo_title(left, attribute_ops)
+ return nil unless attribute_ops.is_a? Array
+# return nil if attribute_ops.find { |ao| ao.operator == :'+>' }
+ keyed_entries = attribute_ops.map do |ao|
+ return nil if ao.operator == :'+>'
+ KEY_ENTRY(ao.attribute_name, ao.value_expr)
+ end
+ result = block_or_expression(*transform_calls([left, HASH(keyed_entries)]))
+ result
+ end
+
+
# Building model equivalences of Ruby objects
# Allows passing regular ruby objects to the factory to produce instructions
# that when evaluated produce the same thing.
def build_String(o)
x = Model::LiteralString.new
x.value = o;
x
end
def build_NilClass(o)
x = Model::Nop.new
x
end
def build_TrueClass(o)
x = Model::LiteralBoolean.new
x.value = o
x
end
def build_FalseClass(o)
x = Model::LiteralBoolean.new
x.value = o
x
end
def build_Fixnum(o)
- x = Model::LiteralNumber.new
+ x = Model::LiteralInteger.new
x.value = o;
x
end
def build_Float(o)
- x = Model::LiteralNumber.new
+ x = Model::LiteralFloat.new
x.value = o;
x
end
def build_Regexp(o)
x = Model::LiteralRegularExpression.new
x.value = o;
x
end
+ def build_EppExpression(o, see_scope, body)
+ o.see_scope = see_scope
+ b = f_build_body(body)
+ o.body = b.current if b
+ o
+ end
+
# If building a factory, simply unwrap the model oject contained in the factory.
def build_Factory(o)
o.current
end
# Creates a String literal, unless the symbol is one of the special :undef, or :default
# which instead creates a LiterlUndef, or a LiteralDefault.
def build_Symbol(o)
case o
when :undef
Model::LiteralUndef.new
when :default
Model::LiteralDefault.new
else
build_String(o.to_s)
end
end
# Creates a LiteralList instruction from an Array, where the entries are built.
def build_Array(o)
x = Model::LiteralList.new
o.each { |v| x.addValues(build(v)) }
x
end
# Create a LiteralHash instruction from a hash, where keys and values are built
# The hash entries are added in sorted order based on key.to_s
#
def build_Hash(o)
x = Model::LiteralHash.new
(o.sort_by {|k,v| k.to_s}).each {|k,v| x.addEntries(build(Model::KeyedEntry.new, k, v)) }
x
end
# @param rval_required [Boolean] if the call must produce a value
def build_CallExpression(o, functor, rval_required, *args)
o.functor_expr = to_ops(functor)
o.rval_required = rval_required
args.each {|x| o.addArguments(to_ops(x)) }
o
end
- # # @param rval_required [Boolean] if the call must produce a value
- # def build_CallNamedFunctionExpression(o, name, rval_required, *args)
- # build_CallExpression(o, name, rval_required, *args)
- ## o.functor_expr = build(name)
- ## o.rval_required = rval_required
- ## args.each {|x| o.addArguments(build(x)) }
- # o
- # end
-
def build_CallMethodExpression(o, functor, rval_required, lambda, *args)
build_CallExpression(o, functor, rval_required, *args)
o.lambda = lambda
o
end
def build_CaseExpression(o, test, *args)
o.test = build(test)
args.each {|opt| o.addOptions(build(opt)) }
o
end
def build_CaseOption(o, value_list, then_expr)
value_list = [value_list] unless value_list.is_a? Array
value_list.each { |v| o.addValues(build(v)) }
b = f_build_body(then_expr)
o.then_expr = to_ops(b) if b
o
end
# Build a Class by creating an instance of it, and then calling build on the created instance
# with the given arguments
def build_Class(o, *args)
build(o.new(), *args)
end
+ def interpolate_Factory(o)
+ interpolate(o.current)
+ end
+
+ def interpolate_LiteralInteger(o)
+ # convert number to a variable
+ self.class.new(o).var
+ end
+
+ def interpolate_Object(o)
+ o
+ end
+
+ def interpolate_QualifiedName(o)
+ self.class.new(o).var
+ end
+
+ # rewrite left expression to variable if it is name, number, and recurse if it is an access expression
+ # this is for interpolation support in new lexer (${NAME}, ${NAME[}}, ${NUMBER}, ${NUMBER[]} - all
+ # other expressions requires variables to be preceded with $
+ #
+ def interpolate_AccessExpression(o)
+ if is_interop_rewriteable?(o.left_expr)
+ o.left_expr = to_ops(self.class.new(o.left_expr).interpolate)
+ end
+ o
+ end
+
+ def interpolate_NamedAccessExpression(o)
+ if is_interop_rewriteable?(o.left_expr)
+ o.left_expr = to_ops(self.class.new(o.left_expr).interpolate)
+ end
+ o
+ end
+
+ # Rewrite method calls on the form ${x.each ...} to ${$x.each}
+ def interpolate_CallMethodExpression(o)
+ if is_interop_rewriteable?(o.functor_expr)
+ o.functor_expr = to_ops(self.class.new(o.functor_expr).interpolate)
+ end
+ o
+ end
+
+ def is_interop_rewriteable?(o)
+ case o
+ when Model::AccessExpression, Model::QualifiedName,
+ Model::NamedAccessExpression, Model::CallMethodExpression
+ true
+ when Model::LiteralInteger
+ # Only decimal integers can represent variables, else it is a number
+ o.radix == 10
+ else
+ false
+ end
+ end
+
# Checks if the object is already a model object, or build it
def to_ops(o, *args)
- if o.kind_of?(Model::PopsObject)
+ case o
+ when Model::PopsObject
o
+ when Puppet::Pops::Model::Factory
+ o.current
else
build(o, *args)
end
end
+
+ def self.concat(*args)
+ new(args.map do |e|
+ e = e.current if e.is_a?(self)
+ case e
+ when Model::LiteralString
+ e.value
+ when String
+ e
+ else
+ raise ArgumentError, "can only concatenate strings, got #{e.class}"
+ end
+ end.join(''))
+ end
end
diff --git a/lib/puppet/pops/model/model.rb b/lib/puppet/pops/model/model.rb
index 5f5302be3..b186aca3f 100644
--- a/lib/puppet/pops/model/model.rb
+++ b/lib/puppet/pops/model/model.rb
@@ -1,567 +1,606 @@
#
# The Puppet Pops Metamodel
#
# This module contains a formal description of the Puppet Pops (*P*uppet *OP*eration instruction*S*).
# It describes a Metamodel containing DSL instructions, a description of PuppetType and related
# classes needed to evaluate puppet logic.
# The metamodel resembles the existing AST model, but it is a semantic model of instructions and
# the types that they operate on rather than an Abstract Syntax Tree, although closely related.
#
# The metamodel is anemic (has no behavior) except basic datatype and type
# assertions and reference/containment assertions.
# The metamodel is also a generalized description of the Puppet DSL to enable the
# same metamodel to be used to express Puppet DSL models (instances) with different semantics as
# the language evolves.
#
# The metamodel is concretized by a validator for a particular version of
# the Puppet DSL language.
#
# This metamodel is expressed using RGen.
#
-# TODO: Anonymous Enums - probably ok, but they can be named (don't know if that is meaningsful)
require 'rgen/metamodel_builder'
module Puppet::Pops::Model
+ extend RGen::MetamodelBuilder::ModuleExtension
+
# A base class for modeled objects that makes them Visitable, and Adaptable.
- # @todo currently includes Containment which will not be needed when the corresponding methods
- # are added to RGen (in some version after 0.6.2).
#
class PopsObject < RGen::MetamodelBuilder::MMBase
include Puppet::Pops::Visitable
include Puppet::Pops::Adaptable
include Puppet::Pops::Containment
abstract
end
+ # A Positioned object has an offset measured in an opaque unit (representing characters) from the start
+ # of a source text (starting
+ # from 0), and a length measured in the same opaque unit. The resolution of the opaque unit requires the
+ # aid of a Locator instance that knows about the measure. This information is stored in the model's
+ # root node - a Program.
+ #
+ # The offset and length are optional if the source of the model is not from parsed text.
+ #
+ class Positioned < PopsObject
+ abstract
+ has_attr 'offset', Integer
+ has_attr 'length', Integer
+ end
+
# @abstract base class for expressions
- class Expression < PopsObject
+ class Expression < Positioned
abstract
end
# A Nop - the "no op" expression.
# @note not really needed since the evaluator can evaluate nil with the meaning of NoOp
# @todo deprecate? May be useful if there is the need to differentiate between nil and Nop when transforming model.
#
class Nop < Expression
end
# A binary expression is abstract and has a left and a right expression. The order of evaluation
# and semantics are determined by the concrete subclass.
#
class BinaryExpression < Expression
abstract
#
# @!attribute [rw] left_expr
# @return [Expression]
contains_one_uni 'left_expr', Expression, :lowerBound => 1
contains_one_uni 'right_expr', Expression, :lowerBound => 1
end
# An unary expression is abstract and contains one expression. The semantics are determined by
# a concrete subclass.
#
class UnaryExpression < Expression
abstract
contains_one_uni 'expr', Expression, :lowerBound => 1
end
# A class that simply evaluates to the contained expression.
# It is of value in order to preserve user entered parentheses in transformations, and
# transformations from model to source.
#
class ParenthesizedExpression < UnaryExpression; end
- # An import of one or several files.
- #
- class ImportExpression < Expression
- contains_many_uni 'files', Expression, :lowerBound => 1
- end
-
# A boolean not expression, reversing the truth of the unary expr.
#
class NotExpression < UnaryExpression; end
# An arithmetic expression reversing the polarity of the numeric unary expr.
#
class UnaryMinusExpression < UnaryExpression; end
# An assignment expression assigns a value to the lval() of the left_expr.
#
class AssignmentExpression < BinaryExpression
- has_attr 'operator', RGen::MetamodelBuilder::DataTypes::Enum.new([:'=', :'+=']), :lowerBound => 1
+ has_attr 'operator', RGen::MetamodelBuilder::DataTypes::Enum.new([:'=', :'+=', :'-=']), :lowerBound => 1
end
# An arithmetic expression applies an arithmetic operator on left and right expressions.
#
class ArithmeticExpression < BinaryExpression
has_attr 'operator', RGen::MetamodelBuilder::DataTypes::Enum.new([:'+', :'-', :'*', :'%', :'/', :'<<', :'>>' ]), :lowerBound => 1
end
# A relationship expression associates the left and right expressions
#
class RelationshipExpression < BinaryExpression
has_attr 'operator', RGen::MetamodelBuilder::DataTypes::Enum.new([:'->', :'<-', :'~>', :'<~']), :lowerBound => 1
end
# A binary expression, that accesses the value denoted by right in left. i.e. typically
# expressed concretely in a language as left[right].
#
class AccessExpression < Expression
contains_one_uni 'left_expr', Expression, :lowerBound => 1
contains_many_uni 'keys', Expression, :lowerBound => 1
end
# A comparison expression compares left and right using a comparison operator.
#
class ComparisonExpression < BinaryExpression
has_attr 'operator', RGen::MetamodelBuilder::DataTypes::Enum.new([:'==', :'!=', :'<', :'>', :'<=', :'>=' ]), :lowerBound => 1
end
# A match expression matches left and right using a matching operator.
#
class MatchExpression < BinaryExpression
has_attr 'operator', RGen::MetamodelBuilder::DataTypes::Enum.new([:'!~', :'=~']), :lowerBound => 1
end
# An 'in' expression checks if left is 'in' right
#
class InExpression < BinaryExpression; end
# A boolean expression applies a logical connective operator (and, or) to left and right expressions.
#
class BooleanExpression < BinaryExpression
abstract
end
# An and expression applies the logical connective operator and to left and right expression
# and does not evaluate the right expression if the left expression is false.
#
class AndExpression < BooleanExpression; end
# An or expression applies the logical connective operator or to the left and right expression
# and does not evaluate the right expression if the left expression is true
#
class OrExpression < BooleanExpression; end
# A literal list / array containing 0:M expressions.
#
class LiteralList < Expression
contains_many_uni 'values', Expression
end
- # A Keyed entry has a key and a value expression. It it typically used as an entry in a Hash.
+ # A Keyed entry has a key and a value expression. It is typically used as an entry in a Hash.
#
- class KeyedEntry < PopsObject
+ class KeyedEntry < Positioned
contains_one_uni 'key', Expression, :lowerBound => 1
contains_one_uni 'value', Expression, :lowerBound => 1
end
# A literal hash is a collection of KeyedEntry objects
#
class LiteralHash < Expression
contains_many_uni 'entries', KeyedEntry
end
# A block contains a list of expressions
#
class BlockExpression < Expression
contains_many_uni 'statements', Expression
end
# A case option entry in a CaseStatement
#
class CaseOption < Expression
contains_many_uni 'values', Expression, :lowerBound => 1
contains_one_uni 'then_expr', Expression, :lowerBound => 1
end
# A case expression has a test, a list of options (multi values => block map).
# One CaseOption may contain a LiteralDefault as value. This option will be picked if nothing
# else matched.
#
class CaseExpression < Expression
contains_one_uni 'test', Expression, :lowerBound => 1
contains_many_uni 'options', CaseOption
end
# A query expression is an expression that is applied to some collection.
# The contained optional expression may contain different types of relational expressions depending
# on what the query is applied to.
#
class QueryExpression < Expression
abstract
contains_one_uni 'expr', Expression, :lowerBound => 0
end
# An exported query is a special form of query that searches for exported objects.
#
class ExportedQuery < QueryExpression
end
# A virtual query is a special form of query that searches for virtual objects.
#
class VirtualQuery < QueryExpression
end
# An attribute operation sets or appends a value to a named attribute.
#
- class AttributeOperation < PopsObject
+ class AttributeOperation < Positioned
has_attr 'attribute_name', String, :lowerBound => 1
has_attr 'operator', RGen::MetamodelBuilder::DataTypes::Enum.new([:'=>', :'+>', ]), :lowerBound => 1
contains_one_uni 'value_expr', Expression, :lowerBound => 1
end
- # An optional attribute operation sets or appends a value to a named attribute unless
- # the value is undef/nil in which case the opereration is a Nop.
- #
- # This is a new feature proposed to solve the undef as antimatter problem
- # @note Currently Unused
- #
- class OptionalAttributeOperation < AttributeOperation
- end
-
# An object that collects stored objects from the central cache and returns
# them to the current host. Operations may optionally be applied.
#
class CollectExpression < Expression
contains_one_uni 'type_expr', Expression, :lowerBound => 1
contains_one_uni 'query', QueryExpression, :lowerBound => 1
contains_many_uni 'operations', AttributeOperation
end
- class Parameter < PopsObject
+ class Parameter < Positioned
has_attr 'name', String, :lowerBound => 1
contains_one_uni 'value', Expression
end
# Abstract base class for definitions.
#
class Definition < Expression
abstract
- contains_many_uni 'parameters', Parameter
- contains_one_uni 'body', Expression
end
- # Abstract base class for named definitions.
+ # Abstract base class for named and parameterized definitions.
class NamedDefinition < Definition
abstract
has_attr 'name', String, :lowerBound => 1
+ contains_many_uni 'parameters', Parameter
+ contains_one_uni 'body', Expression
end
# A resource type definition (a 'define' in the DSL).
#
class ResourceTypeDefinition < NamedDefinition
- # FUTURE
- # contains_one_uni 'producer', Producer
end
# A node definition matches hosts using Strings, or Regular expressions. It may inherit from
# a parent node (also using a String or Regular expression).
#
- class NodeDefinition < Expression
+ class NodeDefinition < Definition
contains_one_uni 'parent', Expression
contains_many_uni 'host_matches', Expression, :lowerBound => 1
contains_one_uni 'body', Expression
end
+ class LocatableExpression < Expression
+ has_many_attr 'line_offsets', Integer
+ has_attr 'locator', Object, :lowerBound => 1, :transient => true
+
+ module ClassModule
+ # Go through the gymnastics of making either value or pattern settable
+ # with synchronization to the other form. A derived value cannot be serialized
+ # and we want to serialize the pattern. When recreating the object we need to
+ # recreate it from the pattern string.
+ # The below sets both values if one is changed.
+ #
+ def locator
+ unless result = getLocator
+ setLocator(result = Puppet::Pops::Parser::Locator.locator(source_text, source_ref(), line_offsets))
+ end
+ result
+ end
+ end
+ end
+
+ # Contains one expression which has offsets reported virtually (offset against the Program's
+ # overall locator).
+ #
+ class SubLocatedExpression < Expression
+ contains_one_uni 'expr', Expression, :lowerBound => 1
+
+ # line offset index for contained expressions
+ has_many_attr 'line_offsets', Integer
+
+ # Number of preceding lines (before the line_offsets)
+ has_attr 'leading_line_count', Integer
+
+ # The offset of the leading source line (i.e. size of "left margin").
+ has_attr 'leading_line_offset', Integer
+
+ # The locator for the sub-locatable's children (not for the sublocator itself)
+ # The locator is not serialized and is recreated on demand from the indexing information
+ # in self.
+ #
+ has_attr 'locator', Object, :lowerBound => 1, :transient => true
+
+ module ClassModule
+ def locator
+ unless result = getLocator
+ # Adapt myself to get the Locator for me
+ adapter = Puppet::Pops::Adapters::SourcePosAdapter.adapt(self)
+ # Get the program (root), and deal with case when not contained in a program
+ program = eAllContainers.find {|c| c.is_a?(Program) }
+ source_ref = program.nil? ? '' : program.source_ref
+
+ # An outer locator is needed since SubLocator only deals with offsets. This outer locator
+ # has 0,0 as origin.
+ outer_locator = Puppet::Pops::Parser::Locator.locator(adpater.extract_text, source_ref, line_offsets)
+
+ # Create a sublocator that describes an offset from the outer
+ # NOTE: the offset of self is the same as the sublocator's leading_offset
+ result = Puppet::Pops::Parser::Locator::SubLocator.new(outer_locator,
+ leading_line_count, offset, leading_line_offset)
+ setLocator(result)
+ end
+ result
+ end
+ end
+ end
+
+ # A heredoc is a wrapper around a LiteralString or a ConcatenatedStringExpression with a specification
+ # of syntax. The expectation is that "syntax" has meaning to a validator. A syntax of nil or '' means
+ # "unspecified syntax".
+ #
+ class HeredocExpression < Expression
+ has_attr 'syntax', String
+ contains_one_uni 'text_expr', Expression, :lowerBound => 1
+ end
+
# A class definition
#
class HostClassDefinition < NamedDefinition
has_attr 'parent_class', String
end
# i.e {|parameters| body }
- class LambdaExpression < Definition; end
+ class LambdaExpression < Expression
+ contains_many_uni 'parameters', Parameter
+ contains_one_uni 'body', Expression
+ end
# If expression. If test is true, the then_expr part should be evaluated, else the (optional)
# else_expr. An 'elsif' is simply an else_expr = IfExpression, and 'else' is simply else == Block.
# a 'then' is typically a Block.
#
class IfExpression < Expression
contains_one_uni 'test', Expression, :lowerBound => 1
contains_one_uni 'then_expr', Expression, :lowerBound => 1
contains_one_uni 'else_expr', Expression
end
# An if expression with boolean reversed test.
#
class UnlessExpression < IfExpression
end
# An abstract call.
#
class CallExpression < Expression
abstract
# A bit of a crutch; functions are either procedures (void return) or has an rvalue
# this flag tells the evaluator that it is a failure to call a function that is void/procedure
# where a value is expected.
#
has_attr 'rval_required', Boolean, :defaultValueLiteral => "false"
contains_one_uni 'functor_expr', Expression, :lowerBound => 1
contains_many_uni 'arguments', Expression
contains_one_uni 'lambda', Expression
end
# A function call where the functor_expr should evaluate to something callable.
#
class CallFunctionExpression < CallExpression; end
# A function call where the given functor_expr should evaluate to the name
# of a function.
#
class CallNamedFunctionExpression < CallExpression; end
# A method/function call where the function expr is a NamedAccess and with support for
# an optional lambda block
#
class CallMethodExpression < CallExpression
end
# Abstract base class for literals.
#
class Literal < Expression
abstract
end
# A literal value is an abstract value holder. The type of the contained value is
# determined by the concrete subclass.
#
class LiteralValue < Literal
abstract
- has_attr 'value', Object, :lowerBound => 1
end
# A Regular Expression Literal.
#
- class LiteralRegularExpression < LiteralValue; end
+ class LiteralRegularExpression < LiteralValue
+ has_attr 'value', Object, :lowerBound => 1, :transient => true
+ has_attr 'pattern', String, :lowerBound => 1
+
+ module ClassModule
+ # Go through the gymnastics of making either value or pattern settable
+ # with synchronization to the other form. A derived value cannot be serialized
+ # and we want to serialize the pattern. When recreating the object we need to
+ # recreate it from the pattern string.
+ # The below sets both values if one is changed.
+ #
+ def value= regexp
+ setValue regexp
+ setPattern regexp.to_s
+ end
+
+ def pattern= regexp_string
+ setPattern regexp_string
+ setValue Regexp.new(regexp_string)
+ end
+ end
+
+ end
# A Literal String
#
- class LiteralString < LiteralValue; end
+ class LiteralString < LiteralValue
+ has_attr 'value', String, :lowerBound => 1
+ end
- # A literal text is like a literal string, but has other rules for escaped characters. It
- # is used as part of a ConcatenatedString
- #
- class LiteralText < LiteralValue; end
+ class LiteralNumber < LiteralValue
+ abstract
+ end
# A literal number has a radix of decimal (10), octal (8), or hex (16) to enable string conversion with the input radix.
# By default, a radix of 10 is used.
#
- class LiteralNumber < LiteralValue
+ class LiteralInteger < LiteralNumber
has_attr 'radix', Integer, :lowerBound => 1, :defaultValueLiteral => "10"
+ has_attr 'value', Integer, :lowerBound => 1
+ end
+
+ class LiteralFloat < LiteralNumber
+ has_attr 'value', Float, :lowerBound => 1
end
# The DSL `undef`.
#
class LiteralUndef < Literal; end
# The DSL `default`
class LiteralDefault < Literal; end
# DSL `true` or `false`
- class LiteralBoolean < LiteralValue; end
+ class LiteralBoolean < LiteralValue
+ has_attr 'value', Boolean, :lowerBound => 1
+ end
# A text expression is an interpolation of an expression. If the embedded expression is
- # a QualifiedName, it it taken as a variable name and resolved. All other expressions are evaluated.
+ # a QualifiedName, it is taken as a variable name and resolved. All other expressions are evaluated.
# The result is transformed to a string.
#
class TextExpression < UnaryExpression; end
# An interpolated/concatenated string. The contained segments are expressions. Verbatim sections
# should be LiteralString instances, and interpolated expressions should either be
# TextExpression instances (if QualifiedNames should be turned into variables), or any other expression
# if such treatment is not needed.
#
class ConcatenatedString < Expression
contains_many_uni 'segments', Expression
end
# A DSL NAME (one or multiple parts separated by '::').
#
- class QualifiedName < LiteralValue; end
+ class QualifiedName < LiteralValue
+ has_attr 'value', String, :lowerBound => 1
+ end
# A DSL CLASSREF (one or multiple parts separated by '::' where (at least) the first part starts with an upper case letter).
#
- class QualifiedReference < LiteralValue; end
+ class QualifiedReference < LiteralValue
+ has_attr 'value', String, :lowerBound => 1
+ end
# A Variable expression looks up value of expr (some kind of name) in scope.
# The expression is typically a QualifiedName, or QualifiedReference.
#
class VariableExpression < UnaryExpression; end
- # A type reference is a reference to a type.
- #
- class TypeReference < Expression
- contains_one_uni 'type_name', QualifiedReference, :lowerBound => 1
+ # Epp start
+ class EppExpression < Expression
+ has_attr 'see_scope', Boolean
+ contains_one_uni 'body', Expression
end
- # An instance reference is a reference to one or many named instances of a particular type
- #
- class InstanceReferences < TypeReference
- contains_many_uni 'names', Expression, :lowerBound => 1
+ # A string to render
+ class RenderStringExpression < LiteralString
+ end
+
+ # An expression to evluate and render
+ class RenderExpression < UnaryExpression
end
# A resource body describes one resource instance
#
- class ResourceBody < PopsObject
+ class ResourceBody < Positioned
contains_one_uni 'title', Expression
contains_many_uni 'operations', AttributeOperation
end
# An abstract resource describes the form of the resource (regular, virtual or exported)
# and adds convenience methods to ask if it is virtual or exported.
# All derived classes may not support all forms, and these needs to be validated
#
class AbstractResource < Expression
+ abstract
has_attr 'form', RGen::MetamodelBuilder::DataTypes::Enum.new([:regular, :virtual, :exported ]), :lowerBound => 1, :defaultValueLiteral => "regular"
has_attr 'virtual', Boolean, :derived => true
has_attr 'exported', Boolean, :derived => true
module ClassModule
def virtual_derived
form == :virtual || form == :exported
end
def exported_derived
form == :exported
end
end
end
# A resource expression is used to instantiate one or many resource. Resources may optionally
# be virtual or exported, an exported resource is always virtual.
#
class ResourceExpression < AbstractResource
contains_one_uni 'type_name', Expression, :lowerBound => 1
contains_many_uni 'bodies', ResourceBody
end
# A resource defaults sets defaults for a resource type. This class inherits from AbstractResource
# but does only support the :regular form (this is intentional to be able to produce better error messages
# when illegal forms are applied to a model.
#
class ResourceDefaultsExpression < AbstractResource
contains_one_uni 'type_ref', QualifiedReference
contains_many_uni 'operations', AttributeOperation
end
# A resource override overrides already set values.
#
class ResourceOverrideExpression < Expression
contains_one_uni 'resources', Expression, :lowerBound => 1
contains_many_uni 'operations', AttributeOperation
end
# A selector entry describes a map from matching_expr to value_expr.
#
- class SelectorEntry < PopsObject
+ class SelectorEntry < Positioned
contains_one_uni 'matching_expr', Expression, :lowerBound => 1
contains_one_uni 'value_expr', Expression, :lowerBound => 1
end
# A selector expression represents a mapping from a left_expr to a matching SelectorEntry.
#
class SelectorExpression < Expression
contains_one_uni 'left_expr', Expression, :lowerBound => 1
contains_many_uni 'selectors', SelectorEntry
end
- # Create Invariant. Future suggested enhancement Puppet Types.
- #
- class CreateInvariantExpression < Expression
- has_attr 'name', String
- contains_one_uni 'message_expr', Expression, :lowerBound => 1
- contains_one_uni 'constraint_expr', Expression, :lowerBound => 1
- end
-
- # Create Attribute. Future suggested enhancement Puppet Types.
- #
- class CreateAttributeExpression < Expression
- has_attr 'name', String, :lowerBound => 1
-
- # Should evaluate to name of datatype (String, Integer, Float, Boolean) or an EEnum metadata
- # (created by CreateEnumExpression). If omitted, the type is a String.
- #
- contains_one_uni 'type', Expression
- contains_one_uni 'min_expr', Expression
- contains_one_uni 'max_expr', Expression
- contains_one_uni 'default_value', Expression
- contains_one_uni 'input_transformer', Expression
- contains_one_uni 'derived_expr', Expression
- end
-
- # Create Attribute. Future suggested enhancement Puppet Types.
- #
- class CreateEnumExpression < Expression
- has_attr 'name', String
- contains_one_uni 'values', Expression
- end
-
- # Create Type. Future suggested enhancement Puppet Types.
- #
- class CreateTypeExpression < Expression
- has_attr 'name', String, :lowerBound => 1
- has_attr 'super_name', String
- contains_many_uni 'attributes', CreateAttributeExpression
- contains_many_uni 'invariants', CreateInvariantExpression
- end
-
- # Create ResourceType. Future suggested enhancement Puppet Types.
- # @todo UNFINISHED
- #
- class CreateResourceType < CreateTypeExpression
- # TODO CreateResourceType
- # - has features required by the provider - provider invariant?
- # - super type must be a ResourceType
- end
-
# A named access expression looks up a named part. (e.g. $a.b)
#
class NamedAccessExpression < BinaryExpression; end
- # A named function definition declares and defines a new function
- # Future enhancement.
- #
- class NamedFunctionDefinition < NamedDefinition; end
-
- # Future enhancements - Injection - Unfinished
+ # A Program is the top level construct returned by the parser
+ # it contains the parsed result in the body, and has a reference to the full source text,
+ # and its origin. The line_offset's is an array with the start offset of each line.
#
- module Injection
- # A producer expression produces an instance of a type. The instance is initialized
- # from an expression (or from the current scope if this expression is missing).
- #--
- # new. to handle production of injections
- #
- class Producer < Expression
- contains_one_uni 'type_name', TypeReference, :lowerBound => 1
- contains_one_uni 'instantiation_expr', Expression
- end
-
- # A binding entry binds one capability generically or named, specifies default bindings or
- # composition of other bindings.
- #
- class BindingEntry < PopsObject
- contains_one_uni 'key', Expression
- contains_one_uni 'value', Expression
- end
+ class Program < PopsObject
+ contains_one_uni 'body', Expression
+ has_many 'definitions', Definition
+ has_attr 'source_text', String
+ has_attr 'source_ref', String
+ has_many_attr 'line_offsets', Integer
+ has_attr 'locator', Object, :lowerBound => 1, :transient => true
- # Defines an optionally named binding.
- #
- class Binding < Expression
- contains_one_uni 'title_expr', Expression
- contains_many_uni 'bindings', BindingEntry
+ module ClassModule
+ def locator
+ unless result = getLocator
+ setLocator(result = Puppet::Pops::Parser::Locator.locator(source_text, source_ref(), line_offsets))
+ end
+ result
+ end
end
- # An injection provides a value bound in the effective binding scope. The injection
- # is based on a type (a capability) and an optional list of instance names (i.e. an InstanceReference).
- # Invariants: optional and instantiation are mutually exclusive
- #
- class InjectExpression < Expression
- has_attr 'optional', Boolean
- contains_one_uni 'binding', Expression, :lowerBound => 1
- contains_one_uni 'instantiation', Expression
- end
end
end
diff --git a/lib/puppet/pops/model/model_label_provider.rb b/lib/puppet/pops/model/model_label_provider.rb
index 469130de4..979aa4bc5 100644
--- a/lib/puppet/pops/model/model_label_provider.rb
+++ b/lib/puppet/pops/model/model_label_provider.rb
@@ -1,75 +1,104 @@
# A provider of labels for model object, producing a human name for the model object.
# As an example, if object is an ArithmeticExpression with operator +, `#a_an(o)` produces "a '+' Expression",
# #the(o) produces "the + Expression", and #label produces "+ Expression".
#
class Puppet::Pops::Model::ModelLabelProvider < Puppet::Pops::LabelProvider
def initialize
@@label_visitor ||= Puppet::Pops::Visitor.new(self,"label",0,0)
end
# Produces a label for the given objects type/operator without article.
+ # If a Class is given, its name is used as label
+ #
def label o
- @@label_visitor.visit(o)
+ @@label_visitor.visit(o)
end
def label_Factory o ; label(o.current) end
- def label_Array o ; "Array Object" end
- def label_LiteralNumber o ; "Literal Number" end
+ def label_Array o ; "Array" end
+ def label_LiteralInteger o ; "Literal Integer" end
+ def label_LiteralFloat o ; "Literal Float" 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
+ def label_EppExpression o ; "Epp Template" end
def label_ExportedQuery o ; "Exported Query" end
def label_VirtualQuery o ; "Virtual Query" end
def label_QueryExpression o ; "Collect Query" end
def label_ComparisonExpression o ; "'#{o.operator}' expression" end
def label_AndExpression o ; "'and' expression" end
def label_OrExpression o ; "'or' expression" end
def label_InExpression o ; "'in' expression" end
- def label_ImportExpression o ; "'import' expression" end
- def label_InstanceReferences o ; "Resource Reference" end
def label_AssignmentExpression o ; "'#{o.operator}' expression" end
def label_AttributeOperation o ; "'#{o.operator}' expression" end
def label_LiteralList o ; "Array Expression" end
def label_LiteralHash o ; "Hash Expression" end
def label_KeyedEntry o ; "Hash Entry" end
def label_LiteralBoolean o ; "Boolean" end
+ def label_TrueClass o ; "Boolean" end
+ def label_FalseClass o ; "Boolean" end
def label_LiteralString o ; "String" end
- def label_LiteralText o ; "Text in Interpolated String" end
def label_LambdaExpression o ; "Lambda" end
def label_LiteralDefault o ; "'default' expression" end
def label_LiteralUndef o ; "'undef' expression" end
def label_LiteralRegularExpression o ; "Regular Expression" end
def label_Nop o ; "Nop Expression" end
def label_NamedAccessExpression o ; "'.' expression" end
def label_NilClass o ; "Nil Object" end
def label_NotExpression o ; "'not' expression" end
def label_VariableExpression o ; "Variable" end
def label_TextExpression o ; "Expression in Interpolated String" end
def label_UnaryMinusExpression o ; "Unary Minus" end
def label_BlockExpression o ; "Block Expression" end
def label_ConcatenatedString o ; "Double Quoted String" end
+ def label_HeredocExpression o ; "'@(#{o.syntax})' expression" end
def label_HostClassDefinition o ; "Host Class Definition" end
def label_NodeDefinition o ; "Node Definition" end
def label_ResourceTypeDefinition o ; "'define' expression" end
def label_ResourceOverrideExpression o ; "Resource Override" end
def label_Parameter o ; "Parameter Definition" end
def label_ParenthesizedExpression o ; "Parenthesized Expression" end
def label_IfExpression o ; "'if' statement" end
def label_UnlessExpression o ; "'unless' Statement" end
def label_CallNamedFunctionExpression o ; "Function Call" end
def label_CallMethodExpression o ; "Method call" end
def label_CaseExpression o ; "'case' statement" end
def label_CaseOption o ; "Case Option" end
+ def label_RenderStringExpression o ; "Epp Text" end
+ def label_RenderExpression o ; "Epp Interpolated Expression" end
def label_RelationshipExpression o ; "'#{o.operator}' expression" end
def label_ResourceBody o ; "Resource Instance Definition" end
def label_ResourceDefaultsExpression o ; "Resource Defaults Expression" end
def label_ResourceExpression o ; "Resource Statement" end
def label_SelectorExpression o ; "Selector Expression" end
def label_SelectorEntry o ; "Selector Option" end
- def label_String o ; "Ruby String" end
- def label_Object o ; "Ruby Object" end
+ def label_Integer o ; "Integer" end
+ def label_Fixnum o ; "Integer" end
+ def label_Bignum o ; "Integer" end
+ def label_Float o ; "Float" end
+ def label_String o ; "String" end
+ def label_Regexp o ; "Regexp" end
+ def label_Object o ; "Object" end
+ def label_Hash o ; "Hash" end
def label_QualifiedName o ; "Name" end
- def label_QualifiedReference o ; "Type Name" end
+ def label_QualifiedReference o ; "Type-Name" end
+ def label_PAbstractType o ; "#{Puppet::Pops::Types::TypeCalculator.string(o)}-Type" end
+ def label_PResourceType o
+ if o.title
+ "#{Puppet::Pops::Types::TypeCalculator.string(o)} Resource-Reference"
+ else
+ "#{Puppet::Pops::Types::TypeCalculator.string(o)}-Type"
+ end
+ end
+
+ def label_Class o
+ if o <= Puppet::Pops::Types::PAbstractType
+ simple_name = o.name.split('::').last
+ simple_name[1..-5] + "-Type"
+ else
+ o.name
+ end
+ end
end
diff --git a/lib/puppet/pops/model/model_tree_dumper.rb b/lib/puppet/pops/model/model_tree_dumper.rb
index 24d341cda..1c4bf19cd 100644
--- a/lib/puppet/pops/model/model_tree_dumper.rb
+++ b/lib/puppet/pops/model/model_tree_dumper.rb
@@ -1,352 +1,377 @@
# Dumps a Pops::Model in reverse polish notation; i.e. LISP style
# The intention is to use this for debugging output
# TODO: BAD NAME - A DUMP is a Ruby Serialization
#
class Puppet::Pops::Model::ModelTreeDumper < Puppet::Pops::Model::TreeDumper
def dump_Array o
o.collect {|e| do_dump(e) }
end
- def dump_LiteralNumber o
+ def dump_LiteralFloat o
+ o.value.to_s
+ end
+
+ def dump_LiteralInteger o
case o.radix
when 10
o.value.to_s
when 8
"0%o" % o.value
when 16
"0x%X" % o.value
else
"bad radix:" + o.value.to_s
end
end
def dump_LiteralValue o
o.value.to_s
end
def dump_Factory o
do_dump(o.current)
end
def dump_ArithmeticExpression o
[o.operator.to_s, do_dump(o.left_expr), do_dump(o.right_expr)]
end
# x[y] prints as (slice x y)
def dump_AccessExpression o
if o.keys.size <= 1
["slice", do_dump(o.left_expr), do_dump(o.keys[0])]
else
["slice", do_dump(o.left_expr), do_dump(o.keys)]
end
end
def dump_MatchesExpression o
[o.operator.to_s, do_dump(o.left_expr), do_dump(o.right_expr)]
end
def dump_CollectExpression o
result = ["collect", do_dump(o.type_expr), :indent, :break, do_dump(o.query), :indent]
o.operations do |ao|
result << :break << do_dump(ao)
end
result += [:dedent, :dedent ]
result
end
+ def dump_EppExpression o
+ result = ["epp"]
+# result << ["parameters"] + o.parameters.collect {|p| do_dump(p) } if o.parameters.size() > 0
+ if o.body
+ result << do_dump(o.body)
+ else
+ result << []
+ end
+ result
+ end
+
def dump_ExportedQuery o
result = ["<<| |>>"]
result += dump_QueryExpression(o) unless is_nop?(o.expr)
result
end
def dump_VirtualQuery o
result = ["<| |>"]
result += dump_QueryExpression(o) unless is_nop?(o.expr)
result
end
def dump_QueryExpression o
[do_dump(o.expr)]
end
def dump_ComparisonExpression o
[o.operator.to_s, do_dump(o.left_expr), do_dump(o.right_expr)]
end
def dump_AndExpression o
["&&", do_dump(o.left_expr), do_dump(o.right_expr)]
end
def dump_OrExpression o
["||", do_dump(o.left_expr), do_dump(o.right_expr)]
end
def dump_InExpression o
["in", do_dump(o.left_expr), do_dump(o.right_expr)]
end
- def dump_ImportExpression o
- ["import"] + o.files.collect {|f| do_dump(f) }
- end
-
- def dump_InstanceReferences o
- ["instances", do_dump(o.type_name)] + o.names.collect {|n| do_dump(n) }
- end
-
def dump_AssignmentExpression o
[o.operator.to_s, do_dump(o.left_expr), do_dump(o.right_expr)]
end
# Produces (name => expr) or (name +> expr)
def dump_AttributeOperation o
[o.attribute_name, o.operator, do_dump(o.value_expr)]
end
def dump_LiteralList o
["[]"] + o.values.collect {|x| do_dump(x)}
end
def dump_LiteralHash o
["{}"] + o.entries.collect {|x| do_dump(x)}
end
def dump_KeyedEntry o
[do_dump(o.key), do_dump(o.value)]
end
def dump_MatchExpression o
[o.operator.to_s, do_dump(o.left_expr), do_dump(o.right_expr)]
end
def dump_LiteralString o
"'#{o.value}'"
end
- def dump_LiteralText o
- o.value
- end
-
def dump_LambdaExpression o
result = ["lambda"]
result << ["parameters"] + o.parameters.collect {|p| do_dump(p) } if o.parameters.size() > 0
if o.body
result << do_dump(o.body)
else
result << []
end
result
end
def dump_LiteralDefault o
":default"
end
def dump_LiteralUndef o
":undef"
end
def dump_LiteralRegularExpression o
"/#{o.value.source}/"
end
def dump_Nop o
":nop"
end
def dump_NamedAccessExpression o
[".", do_dump(o.left_expr), do_dump(o.right_expr)]
end
def dump_NilClass o
"()"
end
def dump_NotExpression o
['!', dump(o.expr)]
end
def dump_VariableExpression o
"$#{dump(o.expr)}"
end
# Interpolation (to string) shown as (str expr)
def dump_TextExpression o
["str", do_dump(o.expr)]
end
def dump_UnaryMinusExpression o
['-', do_dump(o.expr)]
end
def dump_BlockExpression o
["block"] + o.statements.collect {|x| do_dump(x) }
end
# Interpolated strings are shown as (cat seg0 seg1 ... segN)
def dump_ConcatenatedString o
["cat"] + o.segments.collect {|x| do_dump(x)}
end
+ def dump_HeredocExpression(o)
+ result = ["@(#{o.syntax})", :indent, :break, do_dump(o.text_expr), :dedent, :break]
+ end
+
def dump_HostClassDefinition o
result = ["class", o.name]
result << ["inherits", o.parent_class] if o.parent_class
result << ["parameters"] + o.parameters.collect {|p| do_dump(p) } if o.parameters.size() > 0
if o.body
result << do_dump(o.body)
else
result << []
end
result
end
def dump_NodeDefinition o
result = ["node"]
result << ["matches"] + o.host_matches.collect {|m| do_dump(m) }
result << ["parent", do_dump(o.parent)] if o.parent
if o.body
result << do_dump(o.body)
else
result << []
end
result
end
def dump_ResourceTypeDefinition o
result = ["define", o.name]
result << ["parameters"] + o.parameters.collect {|p| do_dump(p) } if o.parameters.size() > 0
if o.body
result << do_dump(o.body)
else
result << []
end
result
end
def dump_ResourceOverrideExpression o
result = ["override", do_dump(o.resources), :indent]
o.operations.each do |p|
result << :break << do_dump(p)
end
result << :dedent
result
end
# Produces parameters as name, or (= name value)
def dump_Parameter o
if o.value
["=", o.name, do_dump(o.value)]
else
o.name
end
end
def dump_ParenthesizedExpression o
do_dump(o.expr)
end
+ # Hides that Program exists in the output (only its body is shown), the definitions are just
+ # references to contained classes, resource types, and nodes
+ def dump_Program(o)
+ dump(o.body)
+ end
+
def dump_IfExpression o
result = ["if", do_dump(o.test), :indent, :break,
["then", :indent, do_dump(o.then_expr), :dedent]]
result +=
[:break,
["else", :indent, do_dump(o.else_expr), :dedent],
:dedent] unless is_nop? o.else_expr
result
end
def dump_UnlessExpression o
result = ["unless", do_dump(o.test), :indent, :break,
["then", :indent, do_dump(o.then_expr), :dedent]]
result +=
[:break,
["else", :indent, do_dump(o.else_expr), :dedent],
:dedent] unless is_nop? o.else_expr
result
end
# Produces (invoke name args...) when not required to produce an rvalue, and
# (call name args ... ) otherwise.
#
def dump_CallNamedFunctionExpression o
result = [o.rval_required ? "call" : "invoke", do_dump(o.functor_expr)]
o.arguments.collect {|a| result << do_dump(a) }
result
end
# def dump_CallNamedFunctionExpression o
# result = [o.rval_required ? "call" : "invoke", do_dump(o.functor_expr)]
# o.arguments.collect {|a| result << do_dump(a) }
# result
# end
def dump_CallMethodExpression o
result = [o.rval_required ? "call-method" : "invoke-method", do_dump(o.functor_expr)]
o.arguments.collect {|a| result << do_dump(a) }
result << do_dump(o.lambda) if o.lambda
result
end
def dump_CaseExpression o
result = ["case", do_dump(o.test), :indent]
o.options.each do |s|
result << :break << do_dump(s)
end
result << :dedent
end
def dump_CaseOption o
result = ["when"]
result << o.values.collect {|x| do_dump(x) }
result << ["then", do_dump(o.then_expr) ]
result
end
def dump_RelationshipExpression o
[o.operator.to_s, do_dump(o.left_expr), do_dump(o.right_expr)]
end
+ def dump_RenderStringExpression o
+ ["render-s", " '#{o.value}'"]
+ end
+
+ def dump_RenderExpression o
+ ["render", do_dump(o.expr)]
+ end
+
def dump_ResourceBody o
result = [do_dump(o.title), :indent]
o.operations.each do |p|
result << :break << do_dump(p)
end
result << :dedent
result
end
def dump_ResourceDefaultsExpression o
result = ["resource-defaults", do_dump(o.type_ref), :indent]
o.operations.each do |p|
result << :break << do_dump(p)
end
result << :dedent
result
end
def dump_ResourceExpression o
form = o.form == :regular ? '' : o.form.to_s + "-"
result = [form+"resource", do_dump(o.type_name), :indent]
o.bodies.each do |b|
result << :break << do_dump(b)
end
result << :dedent
result
end
def dump_SelectorExpression o
["?", do_dump(o.left_expr)] + o.selectors.collect {|x| do_dump(x) }
end
def dump_SelectorEntry o
[do_dump(o.matching_expr), "=>", do_dump(o.value_expr)]
end
+ def dump_SubLocatedExpression o
+ ["sublocated", do_dump(o.expr)]
+ end
+
def dump_Object o
[o.class.to_s, o.to_s]
end
def is_nop? o
o.nil? || o.is_a?(Puppet::Pops::Model::Nop)
end
end
diff --git a/lib/puppet/pops/parser/code_merger.rb b/lib/puppet/pops/parser/code_merger.rb
new file mode 100644
index 000000000..79e1328cd
--- /dev/null
+++ b/lib/puppet/pops/parser/code_merger.rb
@@ -0,0 +1,17 @@
+
+class Puppet::Pops::Parser::CodeMerger
+
+ # Concatenates the logic in the array of parse results into one parse result.
+ # @return Puppet::Parser::AST::BlockExpression
+ #
+ def concatenate(parse_results)
+ # this is a bit brute force as the result is already 3x ast with wrapped 4x content
+ # this could be combined in a more elegant way, but it is only used to process a handful of files
+ # at the beginning of a puppet run. TODO: Revisit for Puppet 4x when there is no 3x ast at the top.
+ #
+ children = parse_results.select {|x| !x.nil? && x.code}.reduce([]) do |memo, parsed_class|
+ memo << parsed_class.code
+ end
+ Puppet::Parser::AST::BlockExpression.new(:children => children)
+ end
+end
diff --git a/lib/puppet/pops/parser/egrammar.ra b/lib/puppet/pops/parser/egrammar.ra
index b45cd2cc3..1660b47a1 100644
--- a/lib/puppet/pops/parser/egrammar.ra
+++ b/lib/puppet/pops/parser/egrammar.ra
@@ -1,703 +1,752 @@
# vim: syntax=ruby
# Parser using the Pops model, expression based
class Puppet::Pops::Parser::Parser
token STRING DQPRE DQMID DQPOST
token LBRACK RBRACK LBRACE RBRACE SYMBOL FARROW COMMA TRUE
-token FALSE EQUALS APPENDS LESSEQUAL NOTEQUAL DOT COLON LLCOLLECT RRCOLLECT
+token FALSE EQUALS APPENDS DELETES LESSEQUAL NOTEQUAL DOT COLON LLCOLLECT RRCOLLECT
token QMARK LPAREN RPAREN ISEQUAL GREATEREQUAL GREATERTHAN LESSTHAN
token IF ELSE
token DEFINE ELSIF VARIABLE CLASS INHERITS NODE BOOLEAN
-token NAME SEMIC CASE DEFAULT AT LCOLLECT RCOLLECT CLASSREF
+token NAME SEMIC CASE DEFAULT AT ATAT 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 SELBRACE
+token LAMBDA SELBRACE
+token NUMBER
+token HEREDOC SUBLOCATE
+token RENDER_STRING RENDER_EXPR EPP_START EPP_END EPP_END_TRIM
token LOW
prechigh
left HIGH
left SEMIC
left PIPE
left LPAREN
left RPAREN
- left AT
+ left AT ATAT
left DOT
left CALL
- left LBRACK
+ nonassoc EPP_START
+ left LBRACK LISTSTART
+ left RBRACK
left QMARK
left LCOLLECT LLCOLLECT
right NOT
nonassoc UMINUS
left IN
left MATCH NOMATCH
left TIMES DIV MODULO
left MINUS PLUS
left LSHIFT RSHIFT
left NOTEQUAL ISEQUAL
left GREATEREQUAL GREATERTHAN LESSTHAN LESSEQUAL
left AND
left OR
- right APPENDS EQUALS
+ right APPENDS DELETES EQUALS
left LBRACE
left SELBRACE
left RBRACE
left IN_EDGE OUT_EDGE IN_EDGE_SUB OUT_EDGE_SUB
left TITLE_COLON
left CASE_COLON
left FARROW
left COMMA
+ nonassoc RENDER_EXPR
+ nonassoc RENDER_STRING
left LOW
preclow
rule
# Produces [Model::BlockExpression, Model::Expression, nil] depending on multiple statements, single statement or empty
program
- : statements { result = Factory.block_or_expression(*val[0]) }
+ : statements { result = create_program(Factory.block_or_expression(*val[0])) }
+ | epp_expression { result = create_program(Factory.block_or_expression(*val[0])) }
| nil
# Produces a semantic model (non validated, but semantically adjusted).
statements
: syntactic_statements { result = transform_calls(val[0]) }
# Change may have issues with nil; i.e. program is a sequence of nils/nops
# Simplified from original which had validation for top level constructs - see statement rule
# Produces Array<Model::Expression>
syntactic_statements
: syntactic_statement { result = [val[0]]}
| syntactic_statements SEMIC syntactic_statement { result = val[0].push val[2] }
| syntactic_statements syntactic_statement { result = val[0].push val[1] }
# Produce a single expression or Array of expression
syntactic_statement
: any_expression { result = val[0] }
| syntactic_statement COMMA any_expression { result = aryfy(val[0]).push val[2] }
any_expression
: relationship_expression
relationship_expression
: resource_expression =LOW { result = val[0] }
| relationship_expression IN_EDGE relationship_expression { result = val[0].relop(val[1][:value], val[2]); loc result, val[1] }
| relationship_expression IN_EDGE_SUB relationship_expression { result = val[0].relop(val[1][:value], val[2]); loc result, val[1] }
| relationship_expression OUT_EDGE relationship_expression { result = val[0].relop(val[1][:value], val[2]); loc result, val[1] }
| relationship_expression OUT_EDGE_SUB relationship_expression { result = val[0].relop(val[1][:value], val[2]); loc result, val[1] }
#---EXPRESSION
#
# Produces Model::Expression
expression
: higher_precedence
| expression LBRACK expressions RBRACK =LBRACK { result = val[0][*val[2]] ; loc result, val[0], val[3] }
| expression IN expression { result = val[0].in val[2] ; loc result, val[1] }
- | expression MATCH match_rvalue { result = val[0] =~ val[2] ; loc result, val[1] }
- | expression NOMATCH match_rvalue { result = val[0].mne val[2] ; loc result, val[1] }
+ | expression MATCH expression { result = val[0] =~ val[2] ; loc result, val[1] }
+ | expression NOMATCH expression { result = val[0].mne val[2] ; loc result, val[1] }
| expression PLUS expression { result = val[0] + val[2] ; loc result, val[1] }
| expression MINUS expression { result = val[0] - val[2] ; loc result, val[1] }
| expression DIV expression { result = val[0] / val[2] ; loc result, val[1] }
| expression TIMES expression { result = val[0] * val[2] ; loc result, val[1] }
| expression MODULO expression { result = val[0] % val[2] ; loc result, val[1] }
| expression LSHIFT expression { result = val[0] << val[2] ; loc result, val[1] }
| expression RSHIFT expression { result = val[0] >> val[2] ; loc result, val[1] }
| MINUS expression =UMINUS { result = val[1].minus() ; loc result, val[0] }
| expression NOTEQUAL expression { result = val[0].ne val[2] ; loc result, val[1] }
| expression ISEQUAL expression { result = val[0] == val[2] ; loc result, val[1] }
| expression GREATERTHAN expression { result = val[0] > val[2] ; loc result, val[1] }
| expression GREATEREQUAL expression { result = val[0] >= val[2] ; loc result, val[1] }
| expression LESSTHAN expression { result = val[0] < val[2] ; loc result, val[1] }
| expression LESSEQUAL expression { result = val[0] <= val[2] ; loc result, val[1] }
| NOT expression { result = val[1].not ; loc result, val[0] }
| expression AND expression { result = val[0].and val[2] ; loc result, val[1] }
| expression OR expression { result = val[0].or val[2] ; loc result, val[1] }
| expression EQUALS expression { result = val[0].set(val[2]) ; loc result, val[1] }
| expression APPENDS expression { result = val[0].plus_set(val[2]) ; loc result, val[1] }
+ | expression DELETES expression { result = val[0].minus_set(val[2]); loc result, val[1] }
| expression QMARK selector_entries { result = val[0].select(*val[2]) ; loc result, val[0] }
| LPAREN expression RPAREN { result = val[1].paren() ; loc result, val[0] }
#---EXPRESSIONS
# (e.g. argument list)
#
# This expression list can not contain function calls without parentheses around arguments
# Produces Array<Model::Expression>
expressions
: expression { result = [val[0]] }
| expressions COMMA expression { result = val[0].push(val[2]) }
# These go through a chain of left recursion, ending with primary_expression
higher_precedence
: call_function_expression
primary_expression
: literal_expression
| variable
| call_method_with_lambda_expression
| collection_expression
| case_expression
| if_expression
| unless_expression
| definition_expression
| hostclass_expression
| node_definition_expression
+ | epp_render_expression
# Aleways have the same value
literal_expression
: array
| boolean
| default
| hash
| regex
- | text_or_name =LOW # resolves hash key ambiguity (racc W U require this?)
+ | text_or_name
+ | number
| type
| undef
text_or_name
: name { result = val[0] }
| quotedtext { result = val[0] }
#---CALL FUNCTION
#
# Produces Model::CallNamedFunction
call_function_expression
: primary_expression LPAREN expressions endcomma RPAREN {
result = Factory.CALL_NAMED(val[0], true, val[2])
loc result, val[0], val[4]
}
| primary_expression LPAREN RPAREN {
result = Factory.CALL_NAMED(val[0], true, [])
loc result, val[0], val[2]
}
| primary_expression LPAREN expressions endcomma RPAREN lambda {
result = Factory.CALL_NAMED(val[0], true, val[2])
loc result, val[0], val[4]
result.lambda = val[5]
}
| primary_expression LPAREN RPAREN lambda {
result = Factory.CALL_NAMED(val[0], true, [])
loc result, val[0], val[2]
result.lambda = val[3]
}
| primary_expression = LOW { result = val[0] }
#---CALL METHOD
#
call_method_with_lambda_expression
: call_method_expression =LOW { result = val[0] }
| call_method_expression lambda { result = val[0]; val[0].lambda = val[1] }
call_method_expression
: named_access LPAREN expressions RPAREN { result = Factory.CALL_METHOD(val[0], val[2]); loc result, val[1], val[3] }
| named_access LPAREN RPAREN { result = Factory.CALL_METHOD(val[0], []); loc result, val[1], val[3] }
| named_access =LOW { result = Factory.CALL_METHOD(val[0], []); loc result, val[0] }
# TODO: It may be of value to access named elements of types too
named_access
: expression DOT NAME {
result = val[0].dot(Factory.fqn(val[2][:value]))
loc result, val[1], val[2]
}
#---LAMBDA
#
# This is a temporary switch while experimenting with concrete syntax
# One should be picked for inclusion in puppet.
# 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
}
lambda_rest
: LBRACE statements RBRACE { result = val[1] }
| LBRACE RBRACE { result = nil }
# Produces Array<Model::Parameter>
lambda_parameter_list
: PIPE PIPE { result = [] }
| PIPE parameters endcomma PIPE { result = val[1] }
#---CONDITIONALS
#
#--IF
#
# Produces Model::IfExpression
if_expression
: IF if_part {
result = val[1]
loc(result, val[0], val[1])
}
# Produces Model::IfExpression
if_part
: expression LBRACE statements RBRACE else {
result = Factory.IF(val[0], Factory.block_or_expression(*val[2]), val[4])
loc(result, val[0], (val[4] ? val[4] : val[3]))
}
| expression LBRACE RBRACE else {
result = Factory.IF(val[0], nil, val[3])
loc(result, val[0], (val[3] ? val[3] : val[2]))
}
# Produces [Model::Expression, nil] - nil if there is no else or elsif part
else
: # nothing
| ELSIF if_part {
result = val[1]
loc(result, val[0], val[1])
}
| ELSE LBRACE statements RBRACE {
result = Factory.block_or_expression(*val[2])
loc result, val[0], val[3]
}
| ELSE LBRACE RBRACE {
result = nil # don't think a nop is needed here either
}
#--UNLESS
#
# Changed from Puppet 3x where there is no else part on unless
#
unless_expression
: UNLESS expression LBRACE statements RBRACE unless_else {
result = Factory.UNLESS(val[1], Factory.block_or_expression(*val[3]), val[5])
loc result, val[0], val[4]
}
| UNLESS expression LBRACE RBRACE unless_else {
result = Factory.UNLESS(val[1], nil, nil)
loc result, val[0], val[4]
}
# Different from else part of if, since "elsif" is not supported, but 'else' is
#
# Produces [Model::Expression, nil] - nil if there is no else or elsif part
unless_else
: # nothing
| ELSE LBRACE statements RBRACE {
result = Factory.block_or_expression(*val[2])
loc result, val[0], val[3]
}
| ELSE LBRACE RBRACE {
result = nil # don't think a nop is needed here either
}
#--- CASE EXPRESSION
#
# Produces Model::CaseExpression
case_expression
: CASE expression LBRACE case_options RBRACE {
result = Factory.CASE(val[1], *val[3])
loc result, val[0], val[4]
}
# Produces Array<Model::CaseOption>
case_options
: case_option { result = [val[0]] }
| case_options case_option { result = val[0].push val[1] }
# Produced Model::CaseOption (aka When)
case_option
: expressions case_colon LBRACE statements RBRACE {
result = Factory.WHEN(val[0], val[3])
loc result, val[1], val[4]
}
| expressions case_colon LBRACE RBRACE = LOW {
result = Factory.WHEN(val[0], nil)
loc result, val[1], val[3]
}
case_colon: COLON =CASE_COLON { result = val[0] }
# This special construct is required or racc will produce the wrong result when the selector entry
# LHS is generalized to any expression (LBRACE looks like a hash). Thus it is not possible to write
# a selector with a single entry where the entry LHS is a hash.
# The SELBRACE token is a LBRACE that follows a QMARK, and this is produced by the lexer with a lookback
# Produces Array<Model::SelectorEntry>
#
selector_entries
: selector_entry
| SELBRACE selector_entry_list endcomma RBRACE {
result = val[1]
}
# Produces Array<Model::SelectorEntry>
selector_entry_list
: selector_entry { result = [val[0]] }
| selector_entry_list COMMA selector_entry { result = val[0].push val[2] }
# Produces a Model::SelectorEntry
# This FARROW wins over FARROW in Hash
selector_entry
: expression FARROW expression { result = Factory.MAP(val[0], val[2]) ; loc result, val[1] }
-# --- IMPORT
-# IMPORT is handled as a non parenthesized call and is transformed to an ImportExpression.
-# i.e. there is no special grammar for it - it is just a "call statement".
-
#---RESOURCE
#
# Produces [Model::ResourceExpression, Model::ResourceDefaultsExpression]
# The resource expression parses a generalized syntax and then selects the correct
# resulting model based on the combinatoin of the LHS and what follows.
# It also handled exported and virtual resources, and the class case
#
resource_expression
: expression =LOW {
result = val[0]
}
| at expression LBRACE resourceinstances endsemi RBRACE {
result = case Factory.resource_shape(val[1])
when :resource, :class
tmp = Factory.RESOURCE(Factory.fqn(token_text(val[1])), val[3])
tmp.form = val[0]
tmp
when :defaults
- error "A resource default can not be virtual or exported"
+ error val[1], "A resource default can not be virtual or exported"
when :override
- error "A resource override can not be virtual or exported"
+ error val[1], "A resource override can not be virtual or exported"
else
- error "Expression is not valid as a resource, resource-default, or resource-override"
+ error val[1], "Expression is not valid as a resource, resource-default, or resource-override"
end
loc result, val[1], val[4]
}
| at expression LBRACE attribute_operations endcomma RBRACE {
result = case Factory.resource_shape(val[1])
- when :resource, :class
- error "Defaults are not virtualizable"
- when :defaults
- error "Defaults are not virtualizable"
- when :override
- error "Defaults are not virtualizable"
+ when :resource, :class, :defaults, :override
+ error val[1], "Defaults are not virtualizable"
else
- error "Expression is not valid as a resource, resource-default, or resource-override"
+ error val[1], "Expression is not valid as a resource, resource-default, or resource-override"
end
}
| expression LBRACE resourceinstances endsemi RBRACE {
result = case Factory.resource_shape(val[0])
when :resource, :class
Factory.RESOURCE(Factory.fqn(token_text(val[0])), val[2])
when :defaults
- error "A resource default can not specify a resource name"
+ error val[1], "A resource default can not specify a resource name"
when :override
- error "A resource override does not allow override of name of resource"
+ error val[1], "A resource override does not allow override of name of resource"
else
- error "Expression is not valid as a resource, resource-default, or resource-override"
+ error val[1], "Expression is not valid as a resource, resource-default, or resource-override"
end
loc result, val[0], val[4]
}
| expression LBRACE attribute_operations endcomma RBRACE {
result = case Factory.resource_shape(val[0])
when :resource, :class
# This catches deprecated syntax.
- error "All resource specifications require names"
+ # If the attribute operations does not include +>, then the found expression
+ # is actually a LEFT followed by LITERAL_HASH
+ #
+ unless tmp = transform_resource_wo_title(val[0], val[2])
+ error val[1], "Syntax error resource body without title or hash with +>"
+ end
+ tmp
when :defaults
Factory.RESOURCE_DEFAULTS(val[0], val[2])
when :override
# This was only done for override in original - TODO shuld it be here at all
Factory.RESOURCE_OVERRIDE(val[0], val[2])
else
- error "Expression is not valid as a resource, resource-default, or resource-override"
+ error val[0], "Expression is not valid as a resource, resource-default, or resource-override"
end
loc result, val[0], val[4]
}
+ | at CLASS LBRACE resourceinstances endsemi RBRACE {
+ result = Factory.RESOURCE(Factory.fqn(token_text(val[1])), val[3])
+ result.form = val[0]
+ loc result, val[1], val[5]
+ }
| CLASS LBRACE resourceinstances endsemi RBRACE {
result = Factory.RESOURCE(Factory.fqn(token_text(val[0])), val[2])
loc result, val[0], val[4]
}
resourceinst
: expression title_colon attribute_operations endcomma { result = Factory.RESOURCE_BODY(val[0], val[2]) }
title_colon : COLON =TITLE_COLON { result = val[0] }
resourceinstances
: resourceinst { result = [val[0]] }
| resourceinstances SEMIC resourceinst { result = val[0].push val[2] }
# Produces Symbol corresponding to resource form
#
at
: AT { result = :virtual }
| AT AT { result = :exported }
+ | ATAT { result = :exported }
#---COLLECTION
#
# A Collection is a predicate applied to a set of objects with an implied context (used variables are
# attributes of the object.
# i.e. this is equivalent for source.select(QUERY).apply(ATTRIBUTE_OPERATIONS)
#
# Produces Model::CollectExpression
#
collection_expression
: expression collect_query LBRACE attribute_operations endcomma RBRACE {
result = Factory.COLLECT(val[0], val[1], val[3])
loc result, val[0], val[5]
}
| expression collect_query =LOW {
result = Factory.COLLECT(val[0], val[1], [])
loc result, val[0], val[1]
}
collect_query
: LCOLLECT optional_query RCOLLECT { result = Factory.VIRTUAL_QUERY(val[1]) ; loc result, val[0], val[2] }
| LLCOLLECT optional_query RRCOLLECT { result = Factory.EXPORTED_QUERY(val[1]) ; loc result, val[0], val[2] }
optional_query
: nil
| expression
#---ATTRIBUTE OPERATIONS
#
# (Not an expression)
#
# Produces Array<Model::AttributeOperation>
#
attribute_operations
: { result = [] }
| attribute_operation { result = [val[0]] }
| attribute_operations COMMA attribute_operation { result = val[0].push(val[2]) }
# Produces String
# QUESTION: Why is BOOLEAN valid as an attribute name?
#
attribute_name
: NAME
| keyword
| BOOLEAN
# In this version, illegal combinations are validated instead of producing syntax errors
# (Can give nicer error message "+> is not applicable to...")
# Produces Model::AttributeOperation
#
attribute_operation
: attribute_name FARROW expression {
result = Factory.ATTRIBUTE_OP(val[0][:value], :'=>', val[2])
loc result, val[0], val[2]
}
| attribute_name PARROW expression {
result = Factory.ATTRIBUTE_OP(val[0][:value], :'+>', val[2])
loc result, val[0], val[2]
}
#---DEFINE
#
# Produces Model::Definition
#
definition_expression
- : DEFINE classname parameter_list LBRACE statements RBRACE {
- result = Factory.DEFINITION(classname(val[1][:value]), val[2], val[4])
+ : DEFINE classname parameter_list LBRACE opt_statements RBRACE {
+ result = add_definition(Factory.DEFINITION(classname(val[1][:value]), val[2], val[4]))
loc result, val[0], val[5]
- @lexer.indefine = false
- }
- | DEFINE classname parameter_list LBRACE RBRACE {
- result = Factory.DEFINITION(classname(val[1][:value]), val[2], nil)
- loc result, val[0], val[4]
- @lexer.indefine = false
+ # New lexer does not keep track of this, this is done in validation
+ if @lexer.respond_to?(:'indefine=')
+ @lexer.indefine = false
+ end
}
#---HOSTCLASS
-# ORIGINAL COMMENT: Our class gets defined in the parent namespace, not our own.
-# WAT ??! This is way odd; should get its complete name, classnames do not nest
-# Seems like the call to classname makes use of the name scope
-# (This is uneccesary, since the parent name is known when evaluating)
#
# Produces Model::HostClassDefinition
#
hostclass_expression
- : CLASS classname parameter_list classparent LBRACE statements RBRACE {
- @lexer.namepop
- result = Factory.HOSTCLASS(classname(val[1][:value]), val[2], token_text(val[3]), val[5])
+ : CLASS stacked_classname parameter_list classparent LBRACE opt_statements RBRACE {
+ # Remove this class' name from the namestack as all nested classes have been parsed
+ namepop
+ result = add_definition(Factory.HOSTCLASS(classname(val[1][:value]), val[2], token_text(val[3]), val[5]))
loc result, val[0], val[6]
}
- | CLASS classname parameter_list classparent LBRACE RBRACE {
- @lexer.namepop
- result = Factory.HOSTCLASS(classname(val[1][:value]), val[2], token_text(val[3]), nil)
- loc result, val[0], val[5]
- }
+
+ # Record the classname so nested classes gets a fully qualified name at parse-time
+ # This is a separate rule since racc does not support intermediate actions.
+ #
+ stacked_classname
+ : classname { namestack(val[0][:value]) ; result = val[0] }
+
+ opt_statements
+ : statements
+ | nil
# Produces String, name or nil result
classparent
: nil
| INHERITS classnameordefault { result = val[1] }
# Produces String (this construct allows a class to be named "default" and to be referenced as
# the parent class.
# TODO: Investigate the validity
# Produces a String (classname), or a token (DEFAULT).
#
classnameordefault
: classname
| DEFAULT
#---NODE
#
# Produces Model::NodeDefinition
#
node_definition_expression
: NODE hostnames nodeparent LBRACE statements RBRACE {
- result = Factory.NODE(val[1], val[2], val[4])
+ result = add_definition(Factory.NODE(val[1], val[2], val[4]))
loc result, val[0], val[5]
}
| NODE hostnames nodeparent LBRACE RBRACE {
- result = Factory.NODE(val[1], val[2], nil)
+ result = add_definition(Factory.NODE(val[1], val[2], nil))
loc result, val[0], val[4]
}
# Hostnames is not a list of names, it is a list of name matchers (including a Regexp).
# (The old implementation had a special "Hostname" object with some minimal validation)
#
# Produces Array<Model::LiteralExpression>
#
hostnames
: hostname { result = [result] }
| hostnames COMMA hostname { result = val[0].push(val[2]) }
# Produces a LiteralExpression (string, :default, or regexp)
# String with interpolation is validated for better error message
hostname
- : NAME { result = Factory.fqn(val[0][:value]); loc result, val[0] }
- | quotedtext { result = val[0] }
+ : dotted_name { result = val[0] }
+ | quotedtext { result = val[0] }
| DEFAULT { result = Factory.literal(:default); loc result, val[0] }
| regex
+ dotted_name
+ : NAME { result = Factory.literal(val[0][:value]); loc result, val[0] }
+ | dotted_name DOT NAME { result = Factory.concat(val[0], '.', val[2][:value]); loc result, val[0], val[2] }
+
# Produces Expression, since hostname is an Expression
nodeparent
: nil
| INHERITS hostname { result = val[1] }
-#---NAMES AND PARAMTERS COMMON TO SEVERAL RULES
-# String result
+#---NAMES AND PARAMETERS COMMON TO SEVERAL RULES
+# Produces String
+#
classname
: NAME { result = val[0] }
- | CLASS { result = val[0] }
+ | CLASS { error val[0], "'class' is not a valid classname" }
# Produces Array<Model::Parameter>
parameter_list
: nil { result = [] }
| LPAREN RPAREN { result = [] }
| LPAREN parameters endcomma RPAREN { result = val[1] }
# Produces Array<Model::Parameter>
parameters
: parameter { result = [val[0]] }
| parameters COMMA parameter { result = val[0].push(val[2]) }
# Produces Model::Parameter
parameter
: VARIABLE EQUALS expression { result = Factory.PARAM(val[0][:value], val[2]) ; loc result, val[0] }
| VARIABLE { result = Factory.PARAM(val[0][:value]); loc result, val[0] }
#--RESTRICTED EXPRESSIONS
# i.e. where one could have expected an expression, but the set is limited
-# What is allowed RHS of match operators (see expression)
-match_rvalue
- : regex
- | text_or_name
+## What is allowed RHS of match operators (see expression)
+#match_rvalue
+# : regex
+# | text_or_name
#--VARIABLE
#
variable
: VARIABLE { result = Factory.fqn(val[0][:value]).var ; loc result, val[0] }
#---LITERALS (dynamic and static)
#
array
: LBRACK expressions RBRACK { result = Factory.LIST(val[1]); loc result, val[0], val[2] }
| LBRACK expressions COMMA RBRACK { result = Factory.LIST(val[1]); loc result, val[0], val[3] }
| LBRACK RBRACK { result = Factory.literal([]) ; loc result, val[0] }
+ | LISTSTART expressions RBRACK { result = Factory.LIST(val[1]); loc result, val[0], val[2] }
+ | LISTSTART expressions COMMA RBRACK { result = Factory.LIST(val[1]); loc result, val[0], val[3] }
+ | LISTSTART RBRACK { result = Factory.literal([]) ; loc result, val[0] }
hash
: LBRACE hashpairs RBRACE { result = Factory.HASH(val[1]); loc result, val[0], val[2] }
| LBRACE hashpairs COMMA RBRACE { result = Factory.HASH(val[1]); loc result, val[0], val[3] }
| LBRACE RBRACE { result = Factory.literal({}) ; loc result, val[0], val[3] }
hashpairs
: hashpair { result = [val[0]] }
| hashpairs COMMA hashpair { result = val[0].push val[2] }
hashpair
- : text_or_name FARROW expression { result = Factory.KEY_ENTRY(val[0], val[2]); loc result, val[1] }
+ : expression FARROW expression { result = Factory.KEY_ENTRY(val[0], val[2]); loc result, val[1] }
quotedtext
: string
| dq_string
+ | heredoc
string : STRING { result = Factory.literal(val[0][:value]) ; loc result, val[0] }
dq_string : dqpre dqrval { result = Factory.string(val[0], *val[1]) ; loc result, val[0], val[1][-1] }
dqpre : DQPRE { result = Factory.literal(val[0][:value]); loc result, val[0] }
dqpost : DQPOST { result = Factory.literal(val[0][:value]); loc result, val[0] }
dqmid : DQMID { result = Factory.literal(val[0][:value]); loc result, val[0] }
dqrval : text_expression dqtail { result = [val[0]] + val[1] }
text_expression : expression { result = Factory.TEXT(val[0]) }
dqtail
: dqpost { result = [val[0]] }
| dqmid dqrval { result = [val[0]] + val[1] }
+heredoc
+ : HEREDOC sublocated_text { result = Factory.HEREDOC(val[0][:value], val[1]); loc result, val[0] }
+
+sublocated_text
+ : SUBLOCATE string { result = Factory.SUBLOCATE(val[0], val[1]); loc result, val[0] }
+ | SUBLOCATE dq_string { result = Factory.SUBLOCATE(val[0], val[1]); loc result, val[0] }
+
+epp_expression
+ : EPP_START epp_parameters_list statements { result = Factory.EPP(val[1], val[2]); loc result, val[0] }
+
+epp_parameters_list
+ : =LOW{ result = nil }
+ | PIPE PIPE { result = [] }
+ | PIPE parameters endcomma PIPE { result = val[1] }
+
+epp_render_expression
+ : RENDER_STRING { result = Factory.RENDER_STRING(val[0][:value]); loc result, val[0] }
+ | RENDER_EXPR expression epp_end { result = Factory.RENDER_EXPR(val[1]); loc result, val[0], val[2] }
+ | RENDER_EXPR LBRACE statements RBRACE epp_end { result = Factory.RENDER_EXPR(Factory.block_or_expression(*val[2])); loc result, val[0], val[4] }
+
+epp_end
+ : EPP_END
+ | EPP_END_TRIM
+
+number : NUMBER { result = Factory.NUMBER(val[0][:value]) ; loc result, val[0] }
name : NAME { result = Factory.QNAME_OR_NUMBER(val[0][:value]) ; loc result, val[0] }
type : CLASSREF { result = Factory.QREF(val[0][:value]) ; loc result, val[0] }
undef : UNDEF { result = Factory.literal(:undef); loc result, val[0] }
default : DEFAULT { result = Factory.literal(:default); loc result, val[0] }
# Assumes lexer produces a Boolean value for booleans, or this will go wrong and produce a literal string
# with the text 'true'.
#TODO: could be changed to a specific boolean literal factory method to prevent this possible glitch.
boolean : BOOLEAN { result = Factory.literal(val[0][:value]) ; loc result, val[0] }
regex
: REGEX { result = Factory.literal(val[0][:value]); loc result, val[0] }
#---MARKERS, SPECIAL TOKENS, SYNTACTIC SUGAR, etc.
endcomma
: #
| COMMA { result = nil }
endsemi
: #
| SEMIC
keyword
: AND
| CASE
| CLASS
| DEFAULT
| DEFINE
| ELSE
| ELSIF
| IF
| IN
| INHERITS
| NODE
| OR
| UNDEF
| UNLESS
nil
: { result = nil}
end
---- header ----
require 'puppet'
require 'puppet/pops'
module Puppet
class ParseError < Puppet::Error; end
class ImportError < Racc::ParseError; end
class AlreadyImportedError < ImportError; end
end
---- inner ----
# Make emacs happy
# Local Variables:
# mode: ruby
# End:
diff --git a/lib/puppet/pops/parser/eparser.rb b/lib/puppet/pops/parser/eparser.rb
index 6d71e7b7e..6d1a3668d 100644
--- a/lib/puppet/pops/parser/eparser.rb
+++ b/lib/puppet/pops/parser/eparser.rb
@@ -1,2256 +1,2579 @@
#
# DO NOT MODIFY!!!!
# This file is automatically generated by Racc 1.4.9
# from Racc grammer file "".
#
require 'racc/parser.rb'
require 'puppet'
require 'puppet/pops'
module Puppet
class ParseError < Puppet::Error; end
class ImportError < Racc::ParseError; end
class AlreadyImportedError < ImportError; end
end
module Puppet
module Pops
module Parser
class Parser < Racc::Parser
-module_eval(<<'...end egrammar.ra/module_eval...', 'egrammar.ra', 699)
+module_eval(<<'...end egrammar.ra/module_eval...', 'egrammar.ra', 748)
# Make emacs happy
# Local Variables:
# mode: ruby
# End:
...end egrammar.ra/module_eval...
##### State transition tables begin ###
clist = [
-'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,',
-',,,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',
-',,,,,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,,,,,,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,,,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,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)
+'57,59,275,-130,51,265,53,-216,79,-132,-225,126,313,126,79,125,265,125',
+'363,298,224,224,102,14,106,353,101,360,102,41,106,48,101,50,45,235,49',
+'69,65,238,43,68,46,47,276,-130,66,13,105,-216,67,-132,-225,12,105,258',
+'57,59,260,261,51,70,53,395,240,224,247,42,245,248,80,64,60,244,62,63',
+'61,328,243,14,231,264,52,242,126,41,265,48,125,50,45,126,49,69,65,125',
+'43,68,46,47,221,316,66,13,57,59,67,235,126,12,57,59,125,331,51,126,53',
+'70,79,125,348,272,347,42,348,333,347,64,60,220,62,63,102,14,106,335',
+'101,74,52,41,297,48,135,50,45,133,49,69,65,72,43,68,46,47,296,122,66',
+'13,105,274,67,57,59,12,114,70,57,59,250,249,51,70,53,393,340,341,60',
+'42,342,224,79,64,60,126,62,63,211,125,345,14,241,349,52,351,102,41,106',
+'48,101,50,45,290,49,69,65,187,43,68,46,47,272,274,66,13,272,359,67,296',
+'289,12,105,296,57,59,74,154,51,70,53,391,151,149,370,42,312,81,82,64',
+'60,288,62,63,80,372,274,14,274,127,52,272,375,41,114,48,115,50,45,315',
+'49,69,65,114,43,68,46,47,379,351,66,13,381,382,67,383,384,12,57,59,385',
+'111,51,387,53,70,75,77,76,78,388,42,389,319,74,64,60,71,62,63,396,14',
+'397,57,59,398,52,41,399,48,,50,108,,49,69,65,,43,68,,,,,66,13,,,67,',
+',12,57,59,,,51,,53,70,,135,,,133,42,,,,64,60,,62,63,,14,,57,59,,52,41',
+',48,70,50,108,,49,69,65,,43,68,,60,,,66,13,,,67,,,12,57,59,,,51,,53',
+'70,,135,,,133,42,,,,64,60,,62,63,,14,,57,59,,52,41,,48,70,50,108,,49',
+'69,65,,43,68,,60,,,66,13,,,67,,,12,57,59,,,51,,53,70,79,135,,,133,42',
+',,,64,60,,62,63,102,14,106,,101,,52,41,,48,70,50,108,,49,69,65,,43,68',
+',60,,,66,13,105,,67,,,12,57,59,,,51,,53,70,79,,,,,42,,,80,64,60,,62',
+'63,102,14,106,,101,,52,41,,48,,50,45,,49,69,65,,43,68,46,47,,,66,13',
+'105,,67,,,12,57,59,,,51,,53,70,79,81,82,,,42,,,80,64,60,,62,63,102,14',
+'106,,101,,52,41,,48,,50,108,,49,69,65,,43,68,,,,,66,13,105,,67,,,12',
+'57,59,,,51,,53,70,79,81,82,,,42,,,80,64,60,,62,63,102,14,106,,101,,52',
+'41,,48,,50,108,,49,69,65,,43,68,,,,,66,13,105,,67,,,12,57,59,,,51,,53',
+'70,,,,,,42,,,,64,60,,62,63,,14,,,,,52,41,,48,,50,108,,49,69,65,,43,68',
+',,,,66,13,,,67,,,12,57,59,,,51,,53,70,,,,,,42,,,,64,60,,62,63,,14,,',
+',,52,41,,48,,50,108,,49,69,65,,43,68,,,,,66,13,,,67,,,12,57,59,,,51',
+',53,70,,,,,,42,,,,64,60,,62,63,,14,,,,,52,41,,48,,50,108,,49,69,65,',
+'43,68,,,,,66,13,,,67,,,12,57,59,,,51,,53,70,,,,,,42,,,,64,60,,62,63',
+',14,,,,,52,41,,48,,50,121,,49,69,65,,43,68,,,,,66,13,,,67,,,12,57,59',
+',,51,,53,70,,,,,,42,,,,64,60,,62,63,,14,,,,,52,41,,48,,50,108,,49,69',
+'65,,43,68,,,,,66,13,,,67,,,12,57,59,,,51,,53,70,,,,,,42,,,,64,60,,62',
+'63,,14,,,,,52,41,,48,,50,108,,49,69,65,,43,68,,,,,66,13,,,67,,,12,57',
+'59,,,51,,53,70,,,,,,42,,,,64,60,,62,63,,14,,,,,52,41,,48,,50,108,,49',
+'69,65,,43,68,,,,,66,13,,,67,,,12,,,57,59,,,51,70,53,294,,,,42,,,,64',
+'60,,62,63,,,,14,,,52,,,41,,48,,50,45,,49,69,65,,43,68,46,47,,,66,13',
+',,67,,,12,57,59,,,51,138,53,70,,,,,,42,,,,64,60,,62,63,,14,,,,,52,41',
+',48,,50,108,,49,69,65,,43,68,,,,,66,13,,,67,,,12,57,59,,,51,140,53,70',
+',,,,,42,,,,64,60,,62,63,,14,,,,,52,41,,48,,50,108,,49,69,65,,43,68,',
+',,,66,13,,,67,,,12,,,57,59,,,51,70,53,143,,,,42,,,,64,60,,62,63,,,,14',
+',,52,,,41,,48,,50,108,,49,69,65,,43,68,,,,,66,13,,,67,,,12,57,59,,,51',
+',53,70,,,,,,42,,,,64,60,,62,63,,14,,,,,52,41,,48,,50,108,,49,69,65,',
+'43,68,,,,,66,13,,,67,,,12,,,57,59,,,51,70,53,300,,,,42,,,,64,60,,62',
+'63,,,,14,,,52,,,41,,48,,50,45,,49,69,65,,43,68,46,47,,,66,13,,,67,,',
+'12,,,57,59,,,51,70,53,143,,,,42,,,,64,60,,62,63,,,,14,,,52,,,41,,48',
+',50,45,,49,69,65,,43,68,46,47,,,66,13,,,67,,,12,57,59,,,51,,153,70,',
+',,,,42,,,,64,60,,62,63,,14,,,,,52,41,,48,,50,108,,49,69,65,,43,68,,',
+',,66,13,,,67,,,12,,,57,59,,,51,70,53,369,,,,42,,,,64,60,,62,63,,,,14',
+',,52,,,41,,48,,50,45,,49,69,65,,43,68,46,47,,,66,13,,,67,,,12,57,59',
+',,51,,53,70,,,,,,42,,,,64,60,,62,63,,14,,,,,52,41,,48,,50,45,,49,69',
+'65,,43,68,46,47,,,66,13,,,67,,,12,57,59,,,51,,53,70,,,,,,42,,,,64,60',
+',62,63,,14,,,,,52,41,,48,,50,45,,49,69,65,,43,68,46,47,,,66,13,,,67',
+',,12,57,59,,,51,,53,70,,,,,,42,,,,64,60,,62,63,,14,,,,,52,41,,48,,50',
+'45,,49,69,65,,43,68,46,47,,,66,13,,,67,,,12,57,59,,,51,,53,70,,,,,,42',
+',,,64,60,,62,63,,14,,,,,52,41,,48,,50,45,,49,69,65,,43,68,46,47,,,66',
+'13,,,67,,,12,57,59,,,51,,53,70,,,,,,42,,,,64,60,,62,63,,14,,,,,52,41',
+',48,,50,45,,49,69,65,,43,68,46,47,,,66,13,,,67,,,12,57,59,,,51,,53,70',
+',,,,,42,,,,64,60,,62,63,,14,,,,,52,41,,48,,50,45,,49,69,65,,43,68,46',
+'47,,,66,13,,,67,,,12,57,59,,,51,,53,70,,,,,,42,,,,64,60,,62,63,,14,',
+',,,52,41,,48,,50,45,,49,69,65,,43,68,46,47,,,66,13,,,67,,,12,57,59,',
+',51,,53,70,,,,,,42,,,,64,60,,62,63,,14,,,,,52,41,,48,,50,108,,49,69',
+'65,,43,68,,,,,66,13,,,67,,,12,57,59,,,51,,53,70,,,,,,42,,,,64,60,,62',
+'63,,14,,,,,52,41,,48,,50,108,,49,69,65,,43,68,,,,,66,13,,,67,,,12,57',
+'59,,,51,,53,70,,,,,,42,,,,64,60,,62,63,,14,,,,,52,41,,48,,50,108,,49',
+'69,65,,43,68,,,,,66,13,,,67,,,12,57,59,,,51,,53,70,,,,,,42,,,,64,60',
+',62,63,,14,,,,,52,41,,48,,50,108,,49,69,65,,43,68,,,,,66,13,,,67,,,12',
+'57,59,,,51,,53,70,,,,,,42,,,,64,60,,62,63,,14,,,,,52,41,,48,,50,108',
+',49,69,65,,43,68,,,,,66,13,,,67,,,12,57,59,,,51,,53,70,,,,,,42,,,,64',
+'60,,62,63,,14,,,,,52,41,,48,,50,108,,49,69,65,,43,68,,,,,66,13,,,67',
+',,12,57,59,,,51,,53,70,,,,,,42,,,,64,60,,62,63,,14,,,,,52,41,,48,,50',
+'108,,49,69,65,,43,68,,,,,66,13,,,67,,,12,57,59,,,51,,53,70,,,,,,42,',
+',,64,60,,62,63,,14,,,,,52,41,,48,,50,108,,49,69,65,,43,68,,,,,66,13',
+',,67,,,12,57,59,,,51,,53,70,,,,,,42,,,,64,60,,62,63,,14,,,,,52,41,,48',
+',50,108,,49,69,65,,43,68,,,,,66,13,,,67,,,12,57,59,,,51,,53,70,,,,,',
+'42,,,,64,60,,62,63,,14,,,,,52,41,,48,,50,108,,49,69,65,,43,68,,,,,66',
+'13,,,67,,,12,57,59,,,51,,53,70,,,,,,42,,,,64,60,,62,63,,14,,,,,52,41',
+',48,,50,108,,49,69,65,,43,68,,,,,66,13,,,67,,,12,57,59,,,51,,53,70,',
+',,,,42,,,,64,60,,62,63,,14,,,,,52,41,,48,,50,108,,49,69,65,,43,68,,',
+',,66,13,,,67,,,12,57,59,,,51,,53,70,,,,,,42,,,,64,60,,62,63,,14,,,,',
+'52,41,,48,,50,108,,49,69,65,,43,68,,,,,66,13,,,67,,,12,57,59,,,51,,53',
+'70,,,,,,42,,,,64,60,,62,63,,14,,,,,52,41,,48,,50,108,,49,69,65,,43,68',
+',,,,66,13,,,67,,,12,57,59,,,51,,53,70,,,,,,42,,,,64,60,,62,63,,14,,',
+',,52,41,,48,,50,108,,49,69,65,,43,68,,,,,66,13,,,67,,,12,57,59,,,51',
+',53,70,,,,,,42,,,,64,60,,62,63,,14,,,,,52,41,,48,,50,108,,49,69,65,',
+'43,68,,,,,66,13,,,67,,,12,57,59,,,51,,53,70,,,,,,42,,,,64,60,,62,63',
+',14,,,,,52,41,,48,,50,108,,49,69,65,,43,68,,,,,66,13,,,67,,,12,57,59',
+',,51,,53,70,,,,,,42,,,,64,60,,62,63,,14,,,,,52,41,,48,,50,108,,49,69',
+'65,,43,68,,,,,66,13,,,67,,,12,57,59,,,51,,53,70,,,,,,42,,,,64,60,,62',
+'63,,14,,,,,52,41,,48,,50,108,,49,69,65,,43,68,,,,,66,13,,,67,,,12,57',
+'59,,,51,,53,70,,,,,,42,,,,64,60,,62,63,,14,,,,,52,41,,48,,50,108,,49',
+'69,65,,43,68,,,,,66,13,,,67,,,12,57,59,,,51,,53,70,,,,,,42,,,,64,60',
+',62,63,,14,,,,,52,41,,48,,50,108,,49,69,65,,43,68,,,,,66,13,,,67,,,12',
+'57,59,,,51,,53,70,,,,,,42,,,,64,60,,62,63,,14,,,,,52,41,,48,,50,108',
+',49,69,65,,43,68,,,,,66,13,,,67,,,12,57,59,,,51,,53,70,,,,,,42,,,,64',
+'60,,62,63,,14,,,,,52,41,,48,,50,108,,49,69,65,,43,68,,,,,66,13,,,67',
+',,12,,,57,59,,,51,70,53,354,,,,42,,,186,64,60,,62,63,,,,14,,,52,,,41',
+',48,,50,108,,49,69,65,,43,68,,,,,66,13,,,67,,,12,57,59,,,51,,53,70,',
+',,,,42,,,,64,60,,62,63,,14,,,,,52,189,206,200,207,50,201,209,202,198',
+'196,,191,204,,,,,66,13,210,205,203,,,12,57,59,,,51,,53,70,,,,,208,190',
+',,,64,60,,62,63,,14,,,,,52,41,,48,,50,108,,49,69,65,,43,68,,,,,66,13',
+',,67,,,12,57,59,,,51,,53,70,,,,,,42,,,,64,60,,62,63,,14,,,,,52,41,,48',
+',50,108,,49,69,65,,43,68,,,,,66,13,,,67,,,12,57,59,,,51,,53,70,,,,,',
+'42,,,,64,60,,62,63,,14,,,,,52,41,,48,,50,108,,49,69,65,,43,68,,,,,66',
+'13,,,67,,,12,57,59,,,51,,53,70,,,,,,42,,,,64,60,,62,63,,14,,,,,52,41',
+',48,,50,108,,49,69,65,,43,68,,,,,66,13,,,67,,,12,57,59,,,51,,53,70,',
+',,,,42,,,,64,60,,62,63,,14,,,,,52,41,,48,,50,108,,49,69,65,,43,68,,',
+',,66,13,,,67,,,12,57,59,,,51,,53,70,,,,,,42,,,,64,60,,62,63,,14,,,,',
+'52,41,,48,,50,108,,49,69,65,,43,68,,,,,66,13,,,67,,,12,,,57,59,,,51',
+'70,53,302,,,,42,,,,64,60,,62,63,,,,14,,,52,,,41,,48,,50,45,,49,69,65',
+',43,68,46,47,,,66,13,,,67,,,12,57,59,,,51,,53,70,,,,,,42,,,,64,60,,62',
+'63,,14,218,,,,52,41,,48,,50,108,,49,69,65,,43,68,,,,,66,13,,,67,,,12',
+'57,59,,,51,,53,70,,,,,,42,,,,64,60,,62,63,,14,,,,,52,41,,48,,50,108',
+',49,69,65,,43,68,,,,,66,13,,,67,,,12,57,59,,,51,,53,70,,,,,,42,,,,64',
+'60,,62,63,,14,,,,,52,41,,48,,50,108,,49,69,65,,43,68,,,,,66,13,,,67',
+',,12,57,59,,,51,,53,70,,,,,,42,,,,64,60,,62,63,,14,226,,,,52,41,,48',
+',50,108,,49,69,65,,43,68,,,,,66,13,,,67,,,12,57,59,,,51,,53,70,,,,,',
+'42,,,,64,60,,62,63,,14,,,,,52,41,,48,,50,45,,49,69,65,,43,68,46,47,',
+',66,13,,,67,,,12,57,59,,,51,,53,70,,,,,,42,,,,64,60,,62,63,,14,,,,,52',
+'41,,48,,50,108,,49,69,65,,43,68,,,,,66,13,,,67,,,12,57,59,,,51,322,53',
+'70,,,,,,42,,,,64,60,,62,63,,14,,,,,52,41,,48,,50,108,,49,69,65,,43,68',
+',,,,66,13,,,67,,,12,57,59,,,51,,53,70,,,,,,42,,,,64,60,,62,63,,14,,',
+',,52,41,,48,,50,108,,49,69,65,,43,68,,,,,66,13,,,67,,,12,57,59,,,51',
+',53,70,,,,,,42,,,,64,60,,62,63,,14,,,,,52,41,,48,,50,108,,49,69,65,',
+'43,68,,,,,66,13,,,67,,,12,57,59,,,51,,53,70,,,,,,42,,,,64,60,,62,63',
+',14,,,,,52,41,,48,,50,108,,49,69,65,,43,68,,,,,66,13,,,67,,,12,57,59',
+',,51,321,53,70,,,,,,42,,,,64,60,,62,63,,14,,,,,52,41,,48,,50,108,,49',
+'69,65,,43,68,,,,,66,13,,,67,,,12,57,59,,,51,,53,70,,,,,,42,,,,64,60',
+',62,63,,14,,,,,52,41,,48,,50,108,,49,69,65,,43,68,,,,,66,13,,,67,,,12',
+',,57,59,,,51,70,53,324,,,,42,,,,64,60,,62,63,,,,14,,,52,,,41,,48,,50',
+'108,,49,69,65,,43,68,,,,,66,13,,,67,,,12,57,59,,,51,,53,70,,,,,,42,',
+',,64,60,,62,63,,14,,,,,52,41,,48,,50,108,,49,69,65,,43,68,,,,,66,13',
+',,67,,,12,57,59,,,51,,53,70,,,,,,42,,,,64,60,,62,63,,14,,,,,52,189,206',
+'200,207,50,201,209,202,198,196,,191,204,,,,,66,13,210,205,203,,,12,',
+',,,,,,70,,,,,208,190,,,,64,60,,62,63,79,,,,,,52,,,98,99,100,95,90,102',
+',106,,101,,,91,93,92,94,,,,,,,,,,,,,,,,105,,,,97,96,,,83,84,86,85,88',
+'89,,81,82,,,79,,103,80,,246,,,,98,99,100,95,90,102,,106,,101,,87,91',
+'93,92,94,,,,,,,,,,,,,,,,105,,,,97,96,,,83,84,86,85,88,89,79,81,82,,',
+'246,,,80,98,99,100,95,90,102,,106,,101,,,91,93,92,94,87,,,,,,,,,,,,',
+',,105,,,,97,96,,,83,84,86,85,88,89,,81,82,,,79,,230,80,,,,,,98,99,100',
+'95,90,102,,106,,101,,87,91,93,92,94,,,,,,,,,,,,,,,,105,,,,97,96,,,83',
+'84,86,85,88,89,79,81,82,,,,,,80,98,99,100,95,90,102,,106,,101,,,91,93',
+'92,94,87,,,,,,,,,,,,,,,105,,,,97,96,,,83,84,86,85,88,89,,81,82,,,79',
+',229,80,,,,,,98,99,100,95,90,102,,106,,101,,87,91,93,92,94,,,,,,,,,',
+',,,,,,105,,,,97,96,,,83,84,86,85,88,89,,81,82,,,79,,228,80,,,,,,98,99',
+'100,95,90,102,,106,,101,,87,91,93,92,94,,,,,,,,,,,,,,,,105,,,,97,96',
+',,83,84,86,85,88,89,,81,82,,,79,,227,80,,,,,,98,99,100,95,90,102,,106',
+',101,,87,91,93,92,94,,,,,,,,,,,,,,,,105,,,,97,96,,,83,84,86,85,88,89',
+'79,81,82,,,,,,80,98,99,100,95,90,102,,106,,101,,216,91,93,92,94,87,',
+',,,,,,,,,,,,,105,,,,97,96,,,83,84,86,85,88,89,79,81,82,,,,,,80,98,99',
+'100,95,90,102,,106,,101,,,91,93,92,94,87,,,,,,,,,,,,,,,105,,,,97,96',
+',,83,84,86,85,88,89,79,81,82,,,,,,80,98,99,100,95,90,102,,106,,101,260',
+'261,91,93,92,94,87,,,,,,,,,,,,,,,105,,,,97,96,,,83,84,86,85,88,89,79',
+'81,82,,,,,,80,98,99,100,95,90,102,,106,,101,,,91,93,92,94,87,,,,,,,',
+',,,,,,,105,,,,97,96,,,83,84,86,85,88,89,79,81,82,,,,,,80,98,99,100,95',
+'90,102,,106,,101,,,91,93,92,94,87,,,,,,,,,,,,,,,105,,,,97,96,,,83,84',
+'86,85,88,89,79,81,82,,,,,,80,98,99,100,95,90,102,,106,,101,,,91,93,92',
+'94,87,,,,,,,,,,,,,,,105,,,,97,96,,,83,84,86,85,88,89,79,81,82,,,,,,80',
+'98,99,100,95,90,102,,106,,101,,,91,93,92,94,87,,,,,,,,,,,,,,,105,,,',
+'97,96,,,83,84,86,85,88,89,79,81,82,,,,,,80,98,99,100,95,90,102,,106',
+',101,79,,91,93,92,94,87,,,,,,,,102,,106,,101,,,105,,,,97,96,,,83,84',
+'86,85,88,89,,81,82,,,105,,,80,,79,,,,,86,85,,,,81,82,,,102,87,106,80',
+'101,,,,,,,,,,,,,,,,87,,,,,,105,,,,,,,,,,86,85,,,79,81,82,,,,,,80,98',
+'99,100,95,90,102,,106,,101,,,91,93,92,94,87,,,,,,,,,,,,,,,105,,,,97',
+'96,,,83,84,86,85,88,89,79,81,82,,,,,,80,98,99,100,95,90,102,270,106',
+',101,,,91,93,92,94,87,,,,,,,,,,,,,,,105,,,,97,96,,,83,84,86,85,88,89',
+',81,82,,,79,,103,80,,,,,,98,99,100,95,90,102,,106,,101,79,87,91,93,92',
+'94,,,,,,,,,102,,106,,101,,,105,,,,97,96,,,83,84,86,85,88,89,,81,82,',
+',105,,,80,,79,,,83,84,86,85,,,,81,82,,,102,87,106,80,101,79,,,,,,,,',
+',,,,,102,87,106,,101,,,105,,,,,,,,83,84,86,85,,,,81,82,,,105,,,80,,79',
+',,83,84,86,85,88,89,,81,82,,,102,87,106,80,101,,,,,,,79,,,,,,,,,87,',
+',,90,102,105,106,,101,,79,91,,83,84,86,85,88,89,,81,82,,90,102,,106',
+'80,101,,105,91,,,,79,,,83,84,86,85,88,89,87,81,82,,90,102,105,106,80',
+'101,,,91,,83,84,86,85,88,89,,81,82,,,87,,,80,,,105,,,,,79,,,83,84,86',
+'85,88,89,87,81,82,,90,102,,106,80,101,,,91,,,,,,,,,,,,,87,,,,,,105,',
+',,,79,,,83,84,86,85,88,89,,81,82,95,90,102,,106,80,101,,,91,93,92,94',
+',,,,,,,,,87,,,,,,105,,,,,79,,,83,84,86,85,88,89,,81,82,95,90,102,,106',
+'80,101,,,91,93,92,94,,,,,,,,,,87,,,,,,105,,,,,96,,,83,84,86,85,88,89',
+'79,81,82,,,,,,80,98,99,100,95,90,102,,106,,101,,,91,93,92,94,87,,,,',
+',,,,,,,,,,105,,,,97,96,,,83,84,86,85,88,89,79,81,82,,,266,,,80,98,99',
+'100,95,90,102,,106,,101,,,91,93,92,94,87,,,,,,,,,,,,,,,105,,,,97,96',
+',,83,84,86,85,88,89,79,81,82,,,,,,80,98,99,100,95,90,102,,106,,101,',
+',91,93,92,94,87,,,,,,,,,,,,,,,105,,,,97,96,,,83,84,86,85,88,89,79,81',
+'82,,,,,,80,98,99,100,95,90,102,,106,,101,,,91,93,92,94,87,,,,,,,,,,',
+',,,,105,,,,97,96,,,83,84,86,85,88,89,,81,82,,,,,,80,284,206,283,207',
+',281,209,285,279,278,,280,282,,,,87,,,210,205,286,284,206,283,207,,281',
+'209,285,279,278,,280,282,,,208,287,,,210,205,286,284,206,283,207,,281',
+'209,285,279,278,,280,282,,,208,287,,,210,205,286,,,,,,,,,,,,,,,,208',
+'287' ]
+ racc_action_table = arr = ::Array.new(6233, nil)
idx = 0
clist.each do |str|
str.split(',', -1).each do |i|
arr[idx] = i.to_i unless i.empty?
idx += 1
end
end
clist = [
-'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)
+'0,0,199,196,0,225,0,204,163,198,203,312,235,201,162,312,303,201,312',
+'225,151,235,163,0,163,303,163,310,162,0,162,0,162,0,0,123,0,0,0,129',
+'0,0,0,0,199,196,0,0,163,204,0,198,203,0,162,151,382,382,328,328,382',
+'0,382,382,129,114,142,0,139,142,163,0,0,139,0,0,0,263,137,382,121,161',
+'0,137,48,382,161,382,48,382,382,200,382,382,382,200,382,382,382,382',
+'114,237,382,382,240,240,382,128,121,382,5,5,121,267,5,108,5,382,109',
+'108,345,232,345,382,300,271,300,382,382,113,382,382,109,5,109,273,109',
+'155,382,5,224,5,240,5,5,240,5,5,5,5,5,5,5,5,222,45,5,5,109,277,5,149',
+'149,5,218,240,381,381,147,147,381,5,381,381,291,293,240,5,295,296,168',
+'5,5,45,5,5,104,45,299,381,131,301,5,302,168,381,168,381,168,381,381',
+'217,381,381,381,102,381,381,381,381,306,307,381,381,308,309,381,257',
+'215,381,168,314,379,379,73,71,379,381,379,379,61,60,327,381,233,168',
+'168,381,381,213,381,381,168,330,193,379,332,46,381,192,339,379,340,379',
+'40,379,379,236,379,379,379,39,379,379,379,379,348,349,379,379,351,352',
+'379,356,357,379,186,186,358,38,186,364,186,379,8,8,8,8,365,379,368,241',
+'6,379,379,1,379,379,386,186,390,238,238,392,379,186,394,186,,186,186',
+',186,186,186,,186,186,,,,,186,186,,,186,,,186,12,12,,,12,,12,186,,238',
+',,238,186,,,,186,186,,186,186,,12,,202,202,,186,12,,12,238,12,12,,12',
+'12,12,,12,12,,238,,,12,12,,,12,,,12,13,13,,,13,,13,12,,202,,,202,12',
+',,,12,12,,12,12,,13,,49,49,,12,13,,13,202,13,13,,13,13,13,,13,13,,202',
+',,13,13,,,13,,,13,14,14,,,14,,14,13,164,49,,,49,13,,,,13,13,,13,13,164',
+'14,164,,164,,13,14,,14,49,14,14,,14,14,14,,14,14,,49,,,14,14,164,,14',
+',,14,360,360,,,360,,360,14,167,,,,,14,,,164,14,14,,14,14,167,360,167',
+',167,,14,360,,360,,360,360,,360,360,360,,360,360,360,360,,,360,360,167',
+',360,,,360,347,347,,,347,,347,360,169,167,167,,,360,,,167,360,360,,360',
+'360,169,347,169,,169,,360,347,,347,,347,347,,347,347,347,,347,347,,',
+',,347,347,169,,347,,,347,189,189,,,189,,189,347,107,169,169,,,347,,',
+'169,347,347,,347,347,107,189,107,,107,,347,189,,189,,189,189,,189,189',
+'189,,189,189,,,,,189,189,107,,189,,,189,41,41,,,41,,41,189,,,,,,189',
+',,,189,189,,189,189,,41,,,,,189,41,,41,,41,41,,41,41,41,,41,41,,,,,41',
+'41,,,41,,,41,42,42,,,42,,42,41,,,,,,41,,,,41,41,,41,41,,42,,,,,41,42',
+',42,,42,42,,42,42,42,,42,42,,,,,42,42,,,42,,,42,43,43,,,43,,43,42,,',
+',,,42,,,,42,42,,42,42,,43,,,,,42,43,,43,,43,43,,43,43,43,,43,43,,,,',
+'43,43,,,43,,,43,44,44,,,44,,44,43,,,,,,43,,,,43,43,,43,43,,44,,,,,43',
+'44,,44,,44,44,,44,44,44,,44,44,,,,,44,44,,,44,,,44,190,190,,,190,,190',
+'44,,,,,,44,,,,44,44,,44,44,,190,,,,,44,190,,190,,190,190,,190,190,190',
+',190,190,,,,,190,190,,,190,,,190,191,191,,,191,,191,190,,,,,,190,,,',
+'190,190,,190,190,,191,,,,,190,191,,191,,191,191,,191,191,191,,191,191',
+',,,,191,191,,,191,,,191,331,331,,,331,,331,191,,,,,,191,,,,191,191,',
+'191,191,,331,,,,,191,331,,331,,331,331,,331,331,331,,331,331,,,,,331',
+'331,,,331,,,331,,,220,220,,,220,331,220,220,,,,331,,,,331,331,,331,331',
+',,,220,,,331,,,220,,220,,220,220,,220,220,220,,220,220,220,220,,,220',
+'220,,,220,,,220,51,51,,,51,51,51,220,,,,,,220,,,,220,220,,220,220,,51',
+',,,,220,51,,51,,51,51,,51,51,51,,51,51,,,,,51,51,,,51,,,51,52,52,,,52',
+'52,52,51,,,,,,51,,,,51,51,,51,51,,52,,,,,51,52,,52,,52,52,,52,52,52',
+',52,52,,,,,52,52,,,52,,,52,,,53,53,,,53,52,53,53,,,,52,,,,52,52,,52',
+'52,,,,53,,,52,,,53,,53,,53,53,,53,53,53,,53,53,,,,,53,53,,,53,,,53,58',
+'58,,,58,,58,53,,,,,,53,,,,53,53,,53,53,,58,,,,,53,58,,58,,58,58,,58',
+'58,58,,58,58,,,,,58,58,,,58,,,58,,,227,227,,,227,58,227,227,,,,58,,',
+',58,58,,58,58,,,,227,,,58,,,227,,227,,227,227,,227,227,227,,227,227',
+'227,227,,,227,227,,,227,,,227,,,153,153,,,153,227,153,153,,,,227,,,',
+'227,227,,227,227,,,,153,,,227,,,153,,153,,153,153,,153,153,153,,153',
+'153,153,153,,,153,153,,,153,,,153,63,63,,,63,,63,153,,,,,,153,,,,153',
+'153,,153,153,,63,,,,,153,63,,63,,63,63,,63,63,63,,63,63,,,,,63,63,,',
+'63,,,63,,,316,316,,,316,63,316,316,,,,63,,,,63,63,,63,63,,,,316,,,63',
+',,316,,316,,316,316,,316,316,316,,316,316,316,316,,,316,316,,,316,,',
+'316,72,72,,,72,,72,316,,,,,,316,,,,316,316,,316,316,,72,,,,,316,72,',
+'72,,72,72,,72,72,72,,72,72,72,72,,,72,72,,,72,,,72,315,315,,,315,,315',
+'72,,,,,,72,,,,72,72,,72,72,,315,,,,,72,315,,315,,315,315,,315,315,315',
+',315,315,315,315,,,315,315,,,315,,,315,74,74,,,74,,74,315,,,,,,315,',
+',,315,315,,315,315,,74,,,,,315,74,,74,,74,74,,74,74,74,,74,74,74,74',
+',,74,74,,,74,,,74,75,75,,,75,,75,74,,,,,,74,,,,74,74,,74,74,,75,,,,',
+'74,75,,75,,75,75,,75,75,75,,75,75,75,75,,,75,75,,,75,,,75,76,76,,,76',
+',76,75,,,,,,75,,,,75,75,,75,75,,76,,,,,75,76,,76,,76,76,,76,76,76,,76',
+'76,76,76,,,76,76,,,76,,,76,77,77,,,77,,77,76,,,,,,76,,,,76,76,,76,76',
+',77,,,,,76,77,,77,,77,77,,77,77,77,,77,77,77,77,,,77,77,,,77,,,77,78',
+'78,,,78,,78,77,,,,,,77,,,,77,77,,77,77,,78,,,,,77,78,,78,,78,78,,78',
+'78,78,,78,78,78,78,,,78,78,,,78,,,78,79,79,,,79,,79,78,,,,,,78,,,,78',
+'78,,78,78,,79,,,,,78,79,,79,,79,79,,79,79,79,,79,79,,,,,79,79,,,79,',
+',79,80,80,,,80,,80,79,,,,,,79,,,,79,79,,79,79,,80,,,,,79,80,,80,,80',
+'80,,80,80,80,,80,80,,,,,80,80,,,80,,,80,81,81,,,81,,81,80,,,,,,80,,',
+',80,80,,80,80,,81,,,,,80,81,,81,,81,81,,81,81,81,,81,81,,,,,81,81,,',
+'81,,,81,82,82,,,82,,82,81,,,,,,81,,,,81,81,,81,81,,82,,,,,81,82,,82',
+',82,82,,82,82,82,,82,82,,,,,82,82,,,82,,,82,83,83,,,83,,83,82,,,,,,82',
+',,,82,82,,82,82,,83,,,,,82,83,,83,,83,83,,83,83,83,,83,83,,,,,83,83',
+',,83,,,83,84,84,,,84,,84,83,,,,,,83,,,,83,83,,83,83,,84,,,,,83,84,,84',
+',84,84,,84,84,84,,84,84,,,,,84,84,,,84,,,84,85,85,,,85,,85,84,,,,,,84',
+',,,84,84,,84,84,,85,,,,,84,85,,85,,85,85,,85,85,85,,85,85,,,,,85,85',
+',,85,,,85,86,86,,,86,,86,85,,,,,,85,,,,85,85,,85,85,,86,,,,,85,86,,86',
+',86,86,,86,86,86,,86,86,,,,,86,86,,,86,,,86,87,87,,,87,,87,86,,,,,,86',
+',,,86,86,,86,86,,87,,,,,86,87,,87,,87,87,,87,87,87,,87,87,,,,,87,87',
+',,87,,,87,88,88,,,88,,88,87,,,,,,87,,,,87,87,,87,87,,88,,,,,87,88,,88',
+',88,88,,88,88,88,,88,88,,,,,88,88,,,88,,,88,89,89,,,89,,89,88,,,,,,88',
+',,,88,88,,88,88,,89,,,,,88,89,,89,,89,89,,89,89,89,,89,89,,,,,89,89',
+',,89,,,89,90,90,,,90,,90,89,,,,,,89,,,,89,89,,89,89,,90,,,,,89,90,,90',
+',90,90,,90,90,90,,90,90,,,,,90,90,,,90,,,90,91,91,,,91,,91,90,,,,,,90',
+',,,90,90,,90,90,,91,,,,,90,91,,91,,91,91,,91,91,91,,91,91,,,,,91,91',
+',,91,,,91,92,92,,,92,,92,91,,,,,,91,,,,91,91,,91,91,,92,,,,,91,92,,92',
+',92,92,,92,92,92,,92,92,,,,,92,92,,,92,,,92,93,93,,,93,,93,92,,,,,,92',
+',,,92,92,,92,92,,93,,,,,92,93,,93,,93,93,,93,93,93,,93,93,,,,,93,93',
+',,93,,,93,94,94,,,94,,94,93,,,,,,93,,,,93,93,,93,93,,94,,,,,93,94,,94',
+',94,94,,94,94,94,,94,94,,,,,94,94,,,94,,,94,95,95,,,95,,95,94,,,,,,94',
+',,,94,94,,94,94,,95,,,,,94,95,,95,,95,95,,95,95,95,,95,95,,,,,95,95',
+',,95,,,95,96,96,,,96,,96,95,,,,,,95,,,,95,95,,95,95,,96,,,,,95,96,,96',
+',96,96,,96,96,96,,96,96,,,,,96,96,,,96,,,96,97,97,,,97,,97,96,,,,,,96',
+',,,96,96,,96,96,,97,,,,,96,97,,97,,97,97,,97,97,97,,97,97,,,,,97,97',
+',,97,,,97,98,98,,,98,,98,97,,,,,,97,,,,97,97,,97,97,,98,,,,,97,98,,98',
+',98,98,,98,98,98,,98,98,,,,,98,98,,,98,,,98,99,99,,,99,,99,98,,,,,,98',
+',,,98,98,,98,98,,99,,,,,98,99,,99,,99,99,,99,99,99,,99,99,,,,,99,99',
+',,99,,,99,100,100,,,100,,100,99,,,,,,99,,,,99,99,,99,99,,100,,,,,99',
+'100,,100,,100,100,,100,100,100,,100,100,,,,,100,100,,,100,,,100,101',
+'101,,,101,,101,100,,,,,,100,,,,100,100,,100,100,,101,,,,,100,101,,101',
+',101,101,,101,101,101,,101,101,,,,,101,101,,,101,,,101,,,304,304,,,304',
+'101,304,304,,,,101,,,101,101,101,,101,101,,,,304,,,101,,,304,,304,,304',
+'304,,304,304,304,,304,304,,,,,304,304,,,304,,,304,103,103,,,103,,103',
+'304,,,,,,304,,,,304,304,,304,304,,103,,,,,304,103,103,103,103,103,103',
+'103,103,103,103,,103,103,,,,,103,103,103,103,103,,,103,297,297,,,297',
+',297,103,,,,,103,103,,,,103,103,,103,103,,297,,,,,103,297,,297,,297',
+'297,,297,297,297,,297,297,,,,,297,297,,,297,,,297,105,105,,,105,,105',
+'297,,,,,,297,,,,297,297,,297,297,,105,,,,,297,105,,105,,105,105,,105',
+'105,105,,105,105,,,,,105,105,,,105,,,105,106,106,,,106,,106,105,,,,',
+',105,,,,105,105,,105,105,,106,,,,,105,106,,106,,106,106,,106,106,106',
+',106,106,,,,,106,106,,,106,,,106,290,290,,,290,,290,106,,,,,,106,,,',
+'106,106,,106,106,,290,,,,,106,290,,290,,290,290,,290,290,290,,290,290',
+',,,,290,290,,,290,,,290,276,276,,,276,,276,290,,,,,,290,,,,290,290,',
+'290,290,,276,,,,,290,276,,276,,276,276,,276,276,276,,276,276,,,,,276',
+'276,,,276,,,276,275,275,,,275,,275,276,,,,,,276,,,,276,276,,276,276',
+',275,,,,,276,275,,275,,275,275,,275,275,275,,275,275,,,,,275,275,,,275',
+',,275,,,228,228,,,228,275,228,228,,,,275,,,,275,275,,275,275,,,,228',
+',,275,,,228,,228,,228,228,,228,228,228,,228,228,228,228,,,228,228,,',
+'228,,,228,111,111,,,111,,111,228,,,,,,228,,,,228,228,,228,228,,111,111',
+',,,228,111,,111,,111,111,,111,111,111,,111,111,,,,,111,111,,,111,,,111',
+'272,272,,,272,,272,111,,,,,,111,,,,111,111,,111,111,,272,,,,,111,272',
+',272,,272,272,,272,272,272,,272,272,,,,,272,272,,,272,,,272,266,266',
+',,266,,266,272,,,,,,272,,,,272,272,,272,272,,266,,,,,272,266,,266,,266',
+'266,,266,266,266,,266,266,,,,,266,266,,,266,,,266,115,115,,,115,,115',
+'266,,,,,,266,,,,266,266,,266,266,,115,115,,,,266,115,,115,,115,115,',
+'115,115,115,,115,115,,,,,115,115,,,115,,,115,150,150,,,150,,150,115',
+',,,,,115,,,,115,115,,115,115,,150,,,,,115,150,,150,,150,150,,150,150',
+'150,,150,150,150,150,,,150,150,,,150,,,150,229,229,,,229,,229,150,,',
+',,,150,,,,150,150,,150,150,,229,,,,,150,229,,229,,229,229,,229,229,229',
+',229,229,,,,,229,229,,,229,,,229,244,244,,,244,244,244,229,,,,,,229',
+',,,229,229,,229,229,,244,,,,,229,244,,244,,244,244,,244,244,244,,244',
+'244,,,,,244,244,,,244,,,244,231,231,,,231,,231,244,,,,,,244,,,,244,244',
+',244,244,,231,,,,,244,231,,231,,231,231,,231,231,231,,231,231,,,,,231',
+'231,,,231,,,231,265,265,,,265,,265,231,,,,,,231,,,,231,231,,231,231',
+',265,,,,,231,265,,265,,265,265,,265,265,265,,265,265,,,,,265,265,,,265',
+',,265,122,122,,,122,,122,265,,,,,,265,,,,265,265,,265,265,,122,,,,,265',
+'122,,122,,122,122,,122,122,122,,122,122,,,,,122,122,,,122,,,122,242',
+'242,,,242,242,242,122,,,,,,122,,,,122,122,,122,122,,242,,,,,122,242',
+',242,,242,242,,242,242,242,,242,242,,,,,242,242,,,242,,,242,253,253',
+',,253,,253,242,,,,,,242,,,,242,242,,242,242,,253,,,,,242,253,,253,,253',
+'253,,253,253,253,,253,253,,,,,253,253,,,253,,,253,,,248,248,,,248,253',
+'248,248,,,,253,,,,253,253,,253,253,,,,248,,,253,,,248,,248,,248,248',
+',248,248,248,,248,248,,,,,248,248,,,248,,,248,246,246,,,246,,246,248',
+',,,,,248,,,,248,248,,248,248,,246,,,,,248,246,,246,,246,246,,246,246',
+'246,,246,246,,,,,246,246,,,246,,,246,230,230,,,230,,230,246,,,,,,246',
+',,,246,246,,246,246,,230,,,,,246,230,230,230,230,230,230,230,230,230',
+'230,,230,230,,,,,230,230,230,230,230,,,230,,,,,,,,230,,,,,230,230,,',
+',230,230,,230,230,136,,,,,,230,,,136,136,136,136,136,136,,136,,136,',
+',136,136,136,136,,,,,,,,,,,,,,,,136,,,,136,136,,,136,136,136,136,136',
+'136,,136,136,,,262,,262,136,,262,,,,262,262,262,262,262,262,,262,,262',
+',136,262,262,262,262,,,,,,,,,,,,,,,,262,,,,262,262,,,262,262,262,262',
+'262,262,141,262,262,,,141,,,262,141,141,141,141,141,141,,141,,141,,',
+'141,141,141,141,262,,,,,,,,,,,,,,,141,,,,141,141,,,141,141,141,141,141',
+'141,,141,141,,,120,,120,141,,,,,,120,120,120,120,120,120,,120,,120,',
+'141,120,120,120,120,,,,,,,,,,,,,,,,120,,,,120,120,,,120,120,120,120',
+'120,120,145,120,120,,,,,,120,145,145,145,145,145,145,,145,,145,,,145',
+'145,145,145,120,,,,,,,,,,,,,,,145,,,,145,145,,,145,145,145,145,145,145',
+',145,145,,,119,,119,145,,,,,,119,119,119,119,119,119,,119,,119,,145',
+'119,119,119,119,,,,,,,,,,,,,,,,119,,,,119,119,,,119,119,119,119,119',
+'119,,119,119,,,118,,118,119,,,,,,118,118,118,118,118,118,,118,,118,',
+'119,118,118,118,118,,,,,,,,,,,,,,,,118,,,,118,118,,,118,118,118,118',
+'118,118,,118,118,,,116,,116,118,,,,,,116,116,116,116,116,116,,116,,116',
+',118,116,116,116,116,,,,,,,,,,,,,,,,116,,,,116,116,,,116,116,116,116',
+'116,116,110,116,116,,,,,,116,110,110,110,110,110,110,,110,,110,,110',
+'110,110,110,110,116,,,,,,,,,,,,,,,110,,,,110,110,,,110,110,110,110,110',
+'110,152,110,110,,,,,,110,152,152,152,152,152,152,,152,,152,,,152,152',
+'152,152,110,,,,,,,,,,,,,,,152,,,,152,152,,,152,152,152,152,152,152,320',
+'152,152,,,,,,152,320,320,320,320,320,320,,320,,320,152,152,320,320,320',
+'320,152,,,,,,,,,,,,,,,320,,,,320,320,,,320,320,320,320,320,320,323,320',
+'320,,,,,,320,323,323,323,323,323,323,,323,,323,,,323,323,323,323,320',
+',,,,,,,,,,,,,,323,,,,323,323,,,323,323,323,323,323,323,329,323,323,',
+',,,,323,329,329,329,329,329,329,,329,,329,,,329,329,329,329,323,,,,',
+',,,,,,,,,,329,,,,329,329,,,329,329,329,329,329,329,212,329,329,,,,,',
+'329,212,212,212,212,212,212,,212,,212,,,212,212,212,212,329,,,,,,,,',
+',,,,,,212,,,,212,212,,,212,212,212,212,212,212,337,212,212,,,,,,212',
+'337,337,337,337,337,337,,337,,337,,,337,337,337,337,212,,,,,,,,,,,,',
+',,337,,,,337,337,,,337,337,337,337,337,337,338,337,337,,,,,,337,338',
+'338,338,338,338,338,,338,,338,165,,338,338,338,338,337,,,,,,,,165,,165',
+',165,,,338,,,,338,338,,,338,338,338,338,338,338,,338,338,,,165,,,338',
+',166,,,,,165,165,,,,165,165,,,166,338,166,165,166,,,,,,,,,,,,,,,,165',
+',,,,,166,,,,,,,,,,166,166,,,344,166,166,,,,,,166,344,344,344,344,344',
+'344,,344,,344,,,344,344,344,344,166,,,,,,,,,,,,,,,344,,,,344,344,,,344',
+'344,344,344,344,344,188,344,344,,,,,,344,188,188,188,188,188,188,188',
+'188,,188,,,188,188,188,188,344,,,,,,,,,,,,,,,188,,,,188,188,,,188,188',
+'188,188,188,188,,188,188,,,11,,11,188,,,,,,11,11,11,11,11,11,,11,,11',
+'170,188,11,11,11,11,,,,,,,,,170,,170,,170,,,11,,,,11,11,,,11,11,11,11',
+'11,11,,11,11,,,170,,,11,,171,,,170,170,170,170,,,,170,170,,,171,11,171',
+'170,171,172,,,,,,,,,,,,,,172,170,172,,172,,,171,,,,,,,,171,171,171,171',
+',,,171,171,,,172,,,171,,173,,,172,172,172,172,172,172,,172,172,,,173',
+'171,173,172,173,,,,,,,174,,,,,,,,,172,,,,174,174,173,174,,174,,175,174',
+',173,173,173,173,173,173,,173,173,,175,175,,175,173,175,,174,175,,,',
+'176,,,174,174,174,174,174,174,173,174,174,,176,176,175,176,174,176,',
+',176,,175,175,175,175,175,175,,175,175,,,174,,,175,,,176,,,,,177,,,176',
+'176,176,176,176,176,175,176,176,,177,177,,177,176,177,,,177,,,,,,,,',
+',,,,176,,,,,,177,,,,,178,,,177,177,177,177,177,177,,177,177,178,178',
+'178,,178,177,178,,,178,178,178,178,,,,,,,,,,177,,,,,,178,,,,,179,,,178',
+'178,178,178,178,178,,178,178,179,179,179,,179,178,179,,,179,179,179',
+'179,,,,,,,,,,178,,,,,,179,,,,,179,,,179,179,179,179,179,179,180,179',
+'179,,,,,,179,180,180,180,180,180,180,,180,,180,,,180,180,180,180,179',
+',,,,,,,,,,,,,,180,,,,180,180,,,180,180,180,180,180,180,183,180,180,',
+',183,,,180,183,183,183,183,183,183,,183,,183,,,183,183,183,183,180,',
+',,,,,,,,,,,,,183,,,,183,183,,,183,183,183,183,183,183,182,183,183,,',
+',,,183,182,182,182,182,182,182,,182,,182,,,182,182,182,182,183,,,,,',
+',,,,,,,,,182,,,,182,182,,,182,182,182,182,182,182,181,182,182,,,,,,182',
+'181,181,181,181,181,181,,181,,181,,,181,181,181,181,182,,,,,,,,,,,,',
+',,181,,,,181,181,,,181,181,181,181,181,181,,181,181,,,,,,181,274,274',
+'274,274,,274,274,274,274,274,,274,274,,,,181,,,274,274,274,269,269,269',
+'269,,269,269,269,269,269,,269,269,,,274,274,,,269,269,269,211,211,211',
+'211,,211,211,211,211,211,,211,211,,,269,269,,,211,211,211,,,,,,,,,,',
+',,,,,211,211' ]
+ racc_action_check = arr = ::Array.new(6233, nil)
idx = 0
clist.each do |str|
str.split(',', -1).each do |i|
arr[idx] = i.to_i unless i.empty?
idx += 1
end
end
racc_action_pointer = [
- 4691, 297, nil, nil, 4599, 284, nil, 145, nil, nil,
- 285, 4461, 4415, 4369, nil, nil, nil, nil, nil, nil,
+ -2, 295, nil, nil, nil, 108, 280, nil, 220, nil,
+ nil, 5532, 328, 382, 436, nil, nil, nil, nil, nil,
nil, nil, nil, nil, nil, nil, nil, nil, nil, nil,
- 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 ]
+ nil, nil, nil, nil, nil, nil, nil, nil, 254, 191,
+ 229, 652, 706, 760, 814, 147, 203, nil, 48, 407,
+ nil, 1086, 1140, 1196, nil, nil, nil, nil, 1250, nil,
+ 156, 160, nil, 1416, nil, nil, nil, nil, nil, nil,
+ nil, 225, 1526, 212, 1634, 1688, 1742, 1796, 1850, 1904,
+ 1958, 2012, 2066, 2120, 2174, 2228, 2282, 2336, 2390, 2444,
+ 2498, 2552, 2606, 2660, 2714, 2768, 2822, 2876, 2930, 2984,
+ 3038, 3092, 165, 3202, 178, 3310, 3364, 602, 79, 112,
+ 4923, 3636, nil, 121, 30, 3798, 4869, nil, 4810, 4751,
+ 4638, 72, 4122, 10, nil, nil, nil, nil, 82, 27,
+ nil, 170, nil, nil, nil, nil, 4466, 71, nil, 61,
+ nil, 4579, 57, nil, nil, 4692, nil, 164, nil, 159,
+ 3852, -15, 4977, 1362, nil, 125, nil, nil, nil, nil,
+ nil, 74, 8, 2, 440, 5320, 5365, 494, 174, 548,
+ 5551, 5596, 5615, 5660, 5685, 5705, 5730, 5775, 5820, 5865,
+ 5919, 6081, 6027, 5973, nil, nil, 274, nil, 5473, 598,
+ 868, 922, 208, 232, nil, nil, -8, nil, -2, -9,
+ 55, -23, 353, -1, -4, nil, nil, nil, nil, nil,
+ nil, 6163, 5193, 192, nil, 195, nil, 189, 94, nil,
+ 1032, nil, 142, nil, 125, -7, nil, 1306, 3582, 3906,
+ 4394, 4014, 80, 197, nil, -14, 249, 93, 299, nil,
+ 102, 251, 4176, nil, 3960, nil, 4340, nil, 4286, nil,
+ nil, nil, nil, 4230, nil, nil, nil, 205, nil, nil,
+ nil, nil, 4525, 68, nil, 4068, 3744, 101, nil, 6141,
+ nil, 116, 3690, 126, 6119, 3526, 3472, 147, nil, nil,
+ nil, nil, nil, nil, nil, nil, nil, nil, nil, nil,
+ 3418, 148, nil, 166, nil, 108, 144, 3256, nil, 179,
+ 92, 182, 161, 4, 3148, nil, 169, 199, 173, 206,
+ 19, nil, -25, nil, 209, 1580, 1472, nil, nil, nil,
+ 5031, nil, nil, 5085, nil, nil, nil, 162, -21, 5139,
+ 234, 976, 234, nil, nil, nil, nil, 5247, 5301, 241,
+ 182, nil, nil, nil, 5419, 88, nil, 544, 258, 235,
+ nil, 262, 263, nil, nil, nil, 264, 265, 269, nil,
+ 490, nil, nil, nil, 255, 279, nil, nil, 281, nil,
+ nil, nil, nil, nil, nil, nil, nil, nil, nil, 220,
+ nil, 164, 54, nil, nil, nil, 289, nil, nil, nil,
+ 291, nil, 294, nil, 297, nil, nil, nil, nil, nil ]
racc_action_default = [
- -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, -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 ]
+ -227, -228, -1, -2, -3, -4, -5, -8, -10, -11,
+ -16, -107, -228, -228, -228, -45, -46, -47, -48, -49,
+ -50, -51, -52, -53, -54, -55, -56, -57, -58, -59,
+ -60, -61, -62, -63, -64, -65, -66, -67, -72, -73,
+ -77, -228, -228, -228, -228, -228, -118, -120, -228, -228,
+ -165, -228, -228, -228, -178, -179, -180, -181, -228, -183,
+ -228, -194, -197, -228, -202, -203, -204, -205, -206, -207,
+ -208, -228, -228, -7, -228, -228, -228, -228, -228, -228,
+ -228, -228, -228, -228, -228, -228, -228, -228, -228, -228,
+ -228, -228, -228, -228, -228, -228, -228, -228, -228, -228,
+ -228, -228, -228, -127, -122, -227, -227, -28, -228, -35,
+ -228, -228, -74, -228, -228, -228, -228, -84, -228, -228,
+ -228, -228, -228, -227, -137, -156, -157, -119, -227, -227,
+ -146, -148, -149, -150, -151, -152, -43, -228, -168, -228,
+ -171, -228, -228, -174, -175, -187, -182, -228, -190, -228,
+ -228, -228, -228, -228, 400, -6, -9, -12, -13, -14,
+ -15, -228, -18, -19, -20, -21, -22, -23, -24, -25,
+ -26, -27, -29, -30, -31, -32, -33, -34, -36, -37,
+ -38, -39, -40, -228, -41, -102, -228, -78, -228, -220,
+ -226, -214, -211, -209, -116, -128, -203, -131, -207, -228,
+ -217, -215, -223, -205, -206, -213, -218, -219, -221, -222,
+ -224, -127, -126, -228, -125, -228, -42, -209, -69, -79,
+ -228, -82, -209, -161, -164, -228, -76, -228, -228, -228,
+ -127, -228, -211, -227, -158, -228, -228, -228, -228, -154,
+ -228, -228, -228, -166, -228, -169, -228, -172, -228, -184,
+ -185, -186, -188, -228, -191, -192, -193, -209, -195, -198,
+ -200, -201, -107, -228, -17, -228, -228, -209, -104, -127,
+ -115, -228, -212, -228, -210, -228, -228, -209, -130, -132,
+ -214, -215, -216, -217, -220, -223, -225, -226, -123, -124,
+ -210, -228, -71, -228, -81, -228, -210, -228, -75, -228,
+ -87, -228, -93, -228, -228, -97, -211, -209, -211, -228,
+ -228, -140, -228, -159, -209, -227, -228, -147, -155, -153,
+ -44, -167, -170, -177, -173, -176, -189, -228, -228, -106,
+ -228, -210, -209, -110, -117, -111, -129, -133, -134, -228,
+ -68, -80, -83, -162, -163, -87, -86, -228, -228, -93,
+ -92, -228, -228, -101, -96, -98, -228, -228, -228, -113,
+ -227, -141, -142, -143, -228, -228, -138, -139, -228, -145,
+ -196, -199, -103, -105, -114, -121, -70, -85, -88, -228,
+ -91, -228, -228, -108, -109, -112, -228, -160, -135, -144,
+ -228, -90, -228, -95, -228, -100, -136, -89, -94, -99 ]
racc_goto_table = [
- 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,
+ 2, 112, 4, 146, 107, 109, 110, 128, 134, 185,
+ 259, 132, 222, 193, 365, 350, 184, 233, 346, 192,
+ 156, 271, 236, 334, 305, 157, 158, 159, 160, 73,
+ 317, 269, 318, 116, 118, 119, 120, 352, 232, 336,
+ 137, 139, 267, 136, 136, 141, 213, 215, 304, 257,
+ 145, 378, 310, 361, 237, 152, 219, 343, 325, 386,
+ 254, 309, 380, 377, 255, 3, 252, 253, 161, 251,
+ 148, 136, 162, 163, 164, 165, 166, 167, 168, 169,
+ 170, 171, 172, 173, 174, 175, 176, 177, 178, 179,
+ 180, 181, 182, 183, 268, 188, 155, 212, 212, 355,
+ 217, 150, 1, 136, 225, nil, nil, 136, nil, nil,
+ 273, nil, nil, nil, 188, nil, nil, nil, nil, nil,
+ nil, 277, nil, nil, nil, 234, nil, nil, nil, nil,
+ 234, 239, nil, 314, 291, 356, nil, 358, nil, 295,
+ 307, nil, nil, nil, nil, 262, 306, 308, nil, nil,
+ 256, nil, nil, 263, nil, nil, nil, nil, nil, 128,
+ nil, 134, nil, nil, 132, nil, nil, nil, nil, nil,
+ nil, nil, nil, nil, 327, nil, nil, nil, 183, 332,
+ 292, 116, 118, 119, 330, nil, 371, nil, nil, nil,
+ nil, nil, nil, nil, 339, nil, nil, 134, 326, 134,
+ 132, nil, 132, nil, nil, nil, nil, nil, nil, nil,
nil, nil, nil, nil, nil, nil, nil, nil, 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,
+ 293, 136, 188, 188, 357, nil, nil, 299, 301, nil,
+ nil, 364, nil, nil, 320, 311, 320, nil, 323, 373,
+ 141, nil, nil, nil, nil, 145, nil, nil, nil, 374,
+ nil, nil, nil, nil, nil, nil, nil, 320, 329, nil,
+ nil, nil, nil, nil, 188, nil, nil, 337, 338, nil,
+ nil, 362, nil, nil, nil, nil, nil, nil, nil, nil,
+ nil, nil, 320, nil, nil, nil, nil, nil, nil, 344,
+ nil, nil, nil, nil, nil, nil, 136, nil, nil, nil,
+ nil, nil, 376, nil, nil, nil, nil, nil, nil, nil,
+ nil, nil, nil, nil, nil, nil, 368, 367, nil, nil,
+ nil, nil, nil, 183, nil, nil, nil, nil, nil, nil,
+ nil, nil, nil, nil, nil, nil, nil, nil, nil, 116,
nil, nil, nil, nil, nil, nil, nil, nil, 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, 345, nil, 347, 349 ]
+ nil, nil, 367, nil, nil, nil, nil, nil, nil, nil,
+ nil, nil, nil, nil, nil, nil, nil, nil, nil, 390,
+ nil, 392, 394 ]
racc_goto_check = [
- 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, 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,
+ 2, 39, 4, 81, 10, 10, 10, 64, 31, 51,
+ 88, 37, 44, 56, 66, 47, 13, 65, 46, 54,
+ 7, 55, 65, 57, 49, 8, 8, 8, 8, 6,
+ 72, 58, 72, 10, 10, 10, 10, 50, 54, 61,
+ 12, 12, 52, 10, 10, 10, 60, 60, 48, 44,
+ 10, 45, 68, 69, 71, 10, 43, 74, 76, 66,
+ 77, 55, 47, 46, 78, 3, 82, 83, 12, 85,
+ 86, 10, 10, 10, 10, 10, 10, 10, 10, 10,
+ 10, 10, 10, 10, 10, 10, 10, 10, 10, 10,
+ 10, 10, 10, 10, 51, 10, 6, 10, 10, 49,
+ 12, 87, 1, 10, 12, nil, nil, 10, nil, nil,
+ 38, nil, nil, nil, 10, nil, nil, nil, nil, nil,
+ nil, 56, nil, nil, nil, 4, nil, nil, nil, nil,
+ 4, 4, nil, 44, 38, 55, nil, 55, nil, 38,
+ 56, nil, nil, nil, nil, 10, 54, 54, nil, nil,
+ 2, nil, nil, 2, nil, nil, nil, nil, nil, 64,
+ nil, 31, nil, nil, 37, nil, nil, nil, nil, nil,
+ nil, nil, nil, nil, 38, nil, nil, nil, 10, 56,
+ 39, 10, 10, 10, 38, nil, 88, nil, nil, nil,
+ nil, nil, nil, nil, 38, nil, nil, 31, 81, 31,
+ 37, nil, 37, nil, nil, nil, nil, nil, nil, nil,
nil, nil, nil, nil, nil, nil, nil, nil, nil, nil,
- 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,
+ 2, 10, 10, 10, 38, nil, nil, 2, 2, nil,
+ nil, 38, nil, nil, 10, 4, 10, nil, 10, 51,
+ 10, nil, nil, nil, nil, 10, nil, nil, nil, 38,
+ nil, nil, nil, nil, nil, nil, nil, 10, 10, nil,
+ nil, nil, nil, nil, 10, nil, nil, 10, 10, nil,
+ nil, 64, nil, nil, nil, nil, nil, nil, nil, nil,
+ nil, nil, 10, nil, nil, nil, nil, nil, nil, 10,
+ nil, nil, nil, nil, nil, nil, 10, nil, nil, nil,
+ nil, nil, 39, nil, nil, nil, nil, nil, nil, nil,
+ nil, nil, nil, nil, nil, nil, 2, 4, nil, nil,
+ nil, nil, nil, 10, nil, nil, nil, nil, nil, nil,
+ nil, nil, nil, nil, nil, nil, nil, nil, nil, 10,
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, nil,
- nil, nil, nil, nil, 2, nil, 2, 2 ]
+ nil, nil, 4, nil, nil, nil, nil, nil, nil, nil,
+ nil, nil, nil, nil, nil, nil, nil, nil, nil, 2,
+ nil, 2, 2 ]
racc_goto_pointer = [
- nil, 96, 0, 1, nil, 32, 29, -15, nil, -8,
- nil, 42, -33, -26, nil, nil, nil, nil, nil, nil,
+ nil, 102, 0, 65, 2, nil, 24, -54, -50, nil,
+ -8, nil, -11, -85, nil, nil, nil, nil, nil, nil,
nil, nil, nil, nil, nil, nil, nil, nil, nil, nil,
- -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 ]
+ nil, -41, nil, nil, nil, nil, nil, -38, -83, -38,
+ nil, nil, nil, -57, -102, -296, -282, -287, -181, -205,
+ -266, -92, -144, nil, -84, -171, -90, -249, -157, nil,
+ -59, -235, nil, nil, -41, -106, -301, nil, -181, -259,
+ nil, -75, -208, nil, -239, nil, -190, -89, -85, nil,
+ nil, -55, -81, -80, nil, -78, 10, 40, -142 ]
racc_goto_default = [
- 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, 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 ]
+ nil, nil, 366, nil, 214, 5, 6, 7, 8, 9,
+ 11, 10, 303, nil, 15, 38, 16, 17, 18, 19,
+ 20, 21, 22, 23, 24, 25, 26, 27, 28, 29,
+ 30, 31, 32, 33, 34, 35, 36, 37, nil, nil,
+ 39, 40, 113, nil, nil, 117, nil, nil, nil, nil,
+ nil, nil, nil, 44, nil, nil, nil, 194, nil, 104,
+ nil, 195, 199, 197, 124, nil, nil, 123, nil, nil,
+ 129, nil, 130, 131, 223, 142, 144, 54, 55, 56,
+ 58, nil, nil, nil, 147, nil, nil, nil, nil ]
racc_reduce_table = [
0, 0, :racc_error,
- 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,
- 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,
- 1, 92, :_reduce_none,
- 1, 92, :_reduce_none,
- 1, 92, :_reduce_none,
- 1, 92, :_reduce_none,
- 1, 92, :_reduce_none,
- 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_1,
+ 1, 89, :_reduce_2,
1, 89, :_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,
- 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, 90, :_reduce_4,
+ 1, 93, :_reduce_5,
+ 3, 93, :_reduce_6,
+ 2, 93, :_reduce_7,
+ 1, 94, :_reduce_8,
+ 3, 94, :_reduce_9,
+ 1, 95, :_reduce_none,
+ 1, 96, :_reduce_11,
+ 3, 96, :_reduce_12,
+ 3, 96, :_reduce_13,
+ 3, 96, :_reduce_14,
+ 3, 96, :_reduce_15,
+ 1, 98, :_reduce_none,
+ 4, 98, :_reduce_17,
+ 3, 98, :_reduce_18,
+ 3, 98, :_reduce_19,
+ 3, 98, :_reduce_20,
+ 3, 98, :_reduce_21,
+ 3, 98, :_reduce_22,
+ 3, 98, :_reduce_23,
+ 3, 98, :_reduce_24,
+ 3, 98, :_reduce_25,
+ 3, 98, :_reduce_26,
+ 3, 98, :_reduce_27,
+ 2, 98, :_reduce_28,
+ 3, 98, :_reduce_29,
+ 3, 98, :_reduce_30,
+ 3, 98, :_reduce_31,
+ 3, 98, :_reduce_32,
+ 3, 98, :_reduce_33,
+ 3, 98, :_reduce_34,
+ 2, 98, :_reduce_35,
+ 3, 98, :_reduce_36,
+ 3, 98, :_reduce_37,
+ 3, 98, :_reduce_38,
+ 3, 98, :_reduce_39,
+ 3, 98, :_reduce_40,
+ 3, 98, :_reduce_41,
+ 3, 98, :_reduce_42,
+ 1, 100, :_reduce_43,
+ 3, 100, :_reduce_44,
+ 1, 99, :_reduce_none,
+ 1, 103, :_reduce_none,
+ 1, 103, :_reduce_none,
+ 1, 103, :_reduce_none,
+ 1, 103, :_reduce_none,
+ 1, 103, :_reduce_none,
+ 1, 103, :_reduce_none,
+ 1, 103, :_reduce_none,
+ 1, 103, :_reduce_none,
+ 1, 103, :_reduce_none,
+ 1, 103, :_reduce_none,
+ 1, 103, :_reduce_none,
+ 1, 104, :_reduce_none,
+ 1, 104, :_reduce_none,
+ 1, 104, :_reduce_none,
+ 1, 104, :_reduce_none,
+ 1, 104, :_reduce_none,
+ 1, 104, :_reduce_none,
+ 1, 104, :_reduce_none,
+ 1, 104, :_reduce_none,
+ 1, 104, :_reduce_none,
+ 1, 120, :_reduce_66,
+ 1, 120, :_reduce_67,
+ 5, 102, :_reduce_68,
+ 3, 102, :_reduce_69,
+ 6, 102, :_reduce_70,
+ 4, 102, :_reduce_71,
+ 1, 102, :_reduce_72,
+ 1, 106, :_reduce_73,
+ 2, 106, :_reduce_74,
+ 4, 128, :_reduce_75,
+ 3, 128, :_reduce_76,
+ 1, 128, :_reduce_77,
+ 3, 129, :_reduce_78,
+ 2, 127, :_reduce_79,
+ 3, 131, :_reduce_80,
+ 2, 131, :_reduce_81,
+ 2, 130, :_reduce_82,
+ 4, 130, :_reduce_83,
+ 2, 109, :_reduce_84,
+ 5, 133, :_reduce_85,
+ 4, 133, :_reduce_86,
+ 0, 134, :_reduce_none,
+ 2, 134, :_reduce_88,
+ 4, 134, :_reduce_89,
+ 3, 134, :_reduce_90,
+ 6, 110, :_reduce_91,
+ 5, 110, :_reduce_92,
+ 0, 135, :_reduce_none,
+ 4, 135, :_reduce_94,
+ 3, 135, :_reduce_95,
+ 5, 108, :_reduce_96,
+ 1, 136, :_reduce_97,
+ 2, 136, :_reduce_98,
+ 5, 137, :_reduce_99,
+ 4, 137, :_reduce_100,
+ 1, 138, :_reduce_101,
+ 1, 101, :_reduce_none,
+ 4, 101, :_reduce_103,
+ 1, 140, :_reduce_104,
+ 3, 140, :_reduce_105,
+ 3, 139, :_reduce_106,
+ 1, 97, :_reduce_107,
+ 6, 97, :_reduce_108,
+ 6, 97, :_reduce_109,
+ 5, 97, :_reduce_110,
+ 5, 97, :_reduce_111,
+ 6, 97, :_reduce_112,
+ 5, 97, :_reduce_113,
+ 4, 145, :_reduce_114,
+ 1, 146, :_reduce_115,
+ 1, 142, :_reduce_116,
+ 3, 142, :_reduce_117,
+ 1, 141, :_reduce_118,
+ 2, 141, :_reduce_119,
+ 1, 141, :_reduce_120,
+ 6, 107, :_reduce_121,
+ 2, 107, :_reduce_122,
+ 3, 147, :_reduce_123,
+ 3, 147, :_reduce_124,
+ 1, 148, :_reduce_none,
+ 1, 148, :_reduce_none,
+ 0, 144, :_reduce_127,
+ 1, 144, :_reduce_128,
+ 3, 144, :_reduce_129,
+ 1, 150, :_reduce_none,
+ 1, 150, :_reduce_none,
+ 1, 150, :_reduce_none,
+ 3, 149, :_reduce_133,
+ 3, 149, :_reduce_134,
+ 6, 111, :_reduce_135,
+ 7, 112, :_reduce_136,
+ 1, 155, :_reduce_137,
+ 1, 154, :_reduce_none,
+ 1, 154, :_reduce_none,
+ 1, 156, :_reduce_none,
+ 2, 156, :_reduce_141,
+ 1, 157, :_reduce_none,
+ 1, 157, :_reduce_none,
+ 6, 113, :_reduce_144,
+ 5, 113, :_reduce_145,
+ 1, 158, :_reduce_146,
+ 3, 158, :_reduce_147,
+ 1, 160, :_reduce_148,
+ 1, 160, :_reduce_149,
+ 1, 160, :_reduce_150,
+ 1, 160, :_reduce_none,
+ 1, 161, :_reduce_152,
+ 3, 161, :_reduce_153,
+ 1, 159, :_reduce_none,
+ 2, 159, :_reduce_155,
+ 1, 152, :_reduce_156,
+ 1, 152, :_reduce_157,
+ 1, 153, :_reduce_158,
+ 2, 153, :_reduce_159,
+ 4, 153, :_reduce_160,
+ 1, 132, :_reduce_161,
+ 3, 132, :_reduce_162,
+ 3, 162, :_reduce_163,
+ 1, 162, :_reduce_164,
+ 1, 105, :_reduce_165,
+ 3, 115, :_reduce_166,
+ 4, 115, :_reduce_167,
+ 2, 115, :_reduce_168,
+ 3, 115, :_reduce_169,
+ 4, 115, :_reduce_170,
+ 2, 115, :_reduce_171,
+ 3, 118, :_reduce_172,
+ 4, 118, :_reduce_173,
+ 2, 118, :_reduce_174,
+ 1, 163, :_reduce_175,
+ 3, 163, :_reduce_176,
+ 3, 164, :_reduce_177,
+ 1, 125, :_reduce_none,
+ 1, 125, :_reduce_none,
+ 1, 125, :_reduce_none,
+ 1, 165, :_reduce_181,
+ 2, 166, :_reduce_182,
+ 1, 168, :_reduce_183,
+ 1, 170, :_reduce_184,
+ 1, 171, :_reduce_185,
+ 2, 169, :_reduce_186,
+ 1, 172, :_reduce_187,
+ 1, 173, :_reduce_188,
+ 2, 173, :_reduce_189,
+ 2, 167, :_reduce_190,
+ 2, 174, :_reduce_191,
+ 2, 174, :_reduce_192,
+ 3, 91, :_reduce_193,
+ 0, 175, :_reduce_194,
+ 2, 175, :_reduce_195,
+ 4, 175, :_reduce_196,
+ 1, 114, :_reduce_197,
+ 3, 114, :_reduce_198,
+ 5, 114, :_reduce_199,
+ 1, 176, :_reduce_none,
+ 1, 176, :_reduce_none,
+ 1, 121, :_reduce_202,
+ 1, 124, :_reduce_203,
+ 1, 122, :_reduce_204,
+ 1, 123, :_reduce_205,
+ 1, 117, :_reduce_206,
+ 1, 116, :_reduce_207,
+ 1, 119, :_reduce_208,
+ 0, 126, :_reduce_none,
+ 1, 126, :_reduce_210,
+ 0, 143, :_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
+ 1, 151, :_reduce_none,
+ 1, 151, :_reduce_none,
+ 1, 151, :_reduce_none,
+ 1, 151, :_reduce_none,
+ 1, 151, :_reduce_none,
+ 1, 151, :_reduce_none,
+ 1, 151, :_reduce_none,
+ 1, 151, :_reduce_none,
+ 1, 151, :_reduce_none,
+ 1, 151, :_reduce_none,
+ 1, 151, :_reduce_none,
+ 1, 151, :_reduce_none,
+ 1, 151, :_reduce_none,
+ 1, 151, :_reduce_none,
+ 0, 92, :_reduce_227 ]
+
+racc_reduce_n = 228
+
+racc_shift_n = 400
racc_token_table = {
false => 0,
:error => 1,
:STRING => 2,
:DQPRE => 3,
:DQMID => 4,
:DQPOST => 5,
:LBRACK => 6,
:RBRACK => 7,
:LBRACE => 8,
:RBRACE => 9,
:SYMBOL => 10,
:FARROW => 11,
:COMMA => 12,
:TRUE => 13,
:FALSE => 14,
:EQUALS => 15,
:APPENDS => 16,
- :LESSEQUAL => 17,
- :NOTEQUAL => 18,
- :DOT => 19,
- :COLON => 20,
- :LLCOLLECT => 21,
- :RRCOLLECT => 22,
- :QMARK => 23,
- :LPAREN => 24,
- :RPAREN => 25,
- :ISEQUAL => 26,
- :GREATEREQUAL => 27,
- :GREATERTHAN => 28,
- :LESSTHAN => 29,
- :IF => 30,
- :ELSE => 31,
- :DEFINE => 32,
- :ELSIF => 33,
- :VARIABLE => 34,
- :CLASS => 35,
- :INHERITS => 36,
- :NODE => 37,
- :BOOLEAN => 38,
- :NAME => 39,
- :SEMIC => 40,
- :CASE => 41,
- :DEFAULT => 42,
- :AT => 43,
- :LCOLLECT => 44,
- :RCOLLECT => 45,
- :CLASSREF => 46,
- :NOT => 47,
- :OR => 48,
- :AND => 49,
- :UNDEF => 50,
- :PARROW => 51,
- :PLUS => 52,
- :MINUS => 53,
- :TIMES => 54,
- :DIV => 55,
- :LSHIFT => 56,
- :RSHIFT => 57,
- :UMINUS => 58,
- :MATCH => 59,
- :NOMATCH => 60,
- :REGEX => 61,
- :IN_EDGE => 62,
- :OUT_EDGE => 63,
- :IN_EDGE_SUB => 64,
- :OUT_EDGE_SUB => 65,
- :IN => 66,
- :UNLESS => 67,
- :PIPE => 68,
- :SELBRACE => 69,
- :LOW => 70,
- :HIGH => 71,
- :CALL => 72,
- :MODULO => 73,
- :TITLE_COLON => 74,
- :CASE_COLON => 75 }
-
-racc_nt_base = 76
+ :DELETES => 17,
+ :LESSEQUAL => 18,
+ :NOTEQUAL => 19,
+ :DOT => 20,
+ :COLON => 21,
+ :LLCOLLECT => 22,
+ :RRCOLLECT => 23,
+ :QMARK => 24,
+ :LPAREN => 25,
+ :RPAREN => 26,
+ :ISEQUAL => 27,
+ :GREATEREQUAL => 28,
+ :GREATERTHAN => 29,
+ :LESSTHAN => 30,
+ :IF => 31,
+ :ELSE => 32,
+ :DEFINE => 33,
+ :ELSIF => 34,
+ :VARIABLE => 35,
+ :CLASS => 36,
+ :INHERITS => 37,
+ :NODE => 38,
+ :BOOLEAN => 39,
+ :NAME => 40,
+ :SEMIC => 41,
+ :CASE => 42,
+ :DEFAULT => 43,
+ :AT => 44,
+ :ATAT => 45,
+ :LCOLLECT => 46,
+ :RCOLLECT => 47,
+ :CLASSREF => 48,
+ :NOT => 49,
+ :OR => 50,
+ :AND => 51,
+ :UNDEF => 52,
+ :PARROW => 53,
+ :PLUS => 54,
+ :MINUS => 55,
+ :TIMES => 56,
+ :DIV => 57,
+ :LSHIFT => 58,
+ :RSHIFT => 59,
+ :UMINUS => 60,
+ :MATCH => 61,
+ :NOMATCH => 62,
+ :REGEX => 63,
+ :IN_EDGE => 64,
+ :OUT_EDGE => 65,
+ :IN_EDGE_SUB => 66,
+ :OUT_EDGE_SUB => 67,
+ :IN => 68,
+ :UNLESS => 69,
+ :PIPE => 70,
+ :LAMBDA => 71,
+ :SELBRACE => 72,
+ :NUMBER => 73,
+ :HEREDOC => 74,
+ :SUBLOCATE => 75,
+ :RENDER_STRING => 76,
+ :RENDER_EXPR => 77,
+ :EPP_START => 78,
+ :EPP_END => 79,
+ :EPP_END_TRIM => 80,
+ :LOW => 81,
+ :HIGH => 82,
+ :CALL => 83,
+ :LISTSTART => 84,
+ :MODULO => 85,
+ :TITLE_COLON => 86,
+ :CASE_COLON => 87 }
+
+racc_nt_base = 88
racc_use_result_var = true
Racc_arg = [
racc_action_table,
racc_action_check,
racc_action_default,
racc_action_pointer,
racc_goto_table,
racc_goto_check,
racc_goto_default,
racc_goto_pointer,
racc_nt_base,
racc_reduce_table,
racc_token_table,
racc_shift_n,
racc_reduce_n,
racc_use_result_var ]
Racc_token_to_s_table = [
"$end",
"error",
"STRING",
"DQPRE",
"DQMID",
"DQPOST",
"LBRACK",
"RBRACK",
"LBRACE",
"RBRACE",
"SYMBOL",
"FARROW",
"COMMA",
"TRUE",
"FALSE",
"EQUALS",
"APPENDS",
+ "DELETES",
"LESSEQUAL",
"NOTEQUAL",
"DOT",
"COLON",
"LLCOLLECT",
"RRCOLLECT",
"QMARK",
"LPAREN",
"RPAREN",
"ISEQUAL",
"GREATEREQUAL",
"GREATERTHAN",
"LESSTHAN",
"IF",
"ELSE",
"DEFINE",
"ELSIF",
"VARIABLE",
"CLASS",
"INHERITS",
"NODE",
"BOOLEAN",
"NAME",
"SEMIC",
"CASE",
"DEFAULT",
"AT",
+ "ATAT",
"LCOLLECT",
"RCOLLECT",
"CLASSREF",
"NOT",
"OR",
"AND",
"UNDEF",
"PARROW",
"PLUS",
"MINUS",
"TIMES",
"DIV",
"LSHIFT",
"RSHIFT",
"UMINUS",
"MATCH",
"NOMATCH",
"REGEX",
"IN_EDGE",
"OUT_EDGE",
"IN_EDGE_SUB",
"OUT_EDGE_SUB",
"IN",
"UNLESS",
"PIPE",
+ "LAMBDA",
"SELBRACE",
+ "NUMBER",
+ "HEREDOC",
+ "SUBLOCATE",
+ "RENDER_STRING",
+ "RENDER_EXPR",
+ "EPP_START",
+ "EPP_END",
+ "EPP_END_TRIM",
"LOW",
"HIGH",
"CALL",
+ "LISTSTART",
"MODULO",
"TITLE_COLON",
"CASE_COLON",
"$start",
"program",
"statements",
+ "epp_expression",
"nil",
"syntactic_statements",
"syntactic_statement",
"any_expression",
"relationship_expression",
"resource_expression",
"expression",
"higher_precedence",
"expressions",
- "match_rvalue",
"selector_entries",
"call_function_expression",
"primary_expression",
"literal_expression",
"variable",
"call_method_with_lambda_expression",
"collection_expression",
"case_expression",
"if_expression",
"unless_expression",
"definition_expression",
"hostclass_expression",
"node_definition_expression",
+ "epp_render_expression",
"array",
"boolean",
"default",
"hash",
"regex",
"text_or_name",
+ "number",
"type",
"undef",
"name",
"quotedtext",
"endcomma",
"lambda",
"call_method_expression",
"named_access",
"lambda_parameter_list",
"lambda_rest",
"parameters",
"if_part",
"else",
"unless_else",
"case_options",
"case_option",
"case_colon",
"selector_entry",
"selector_entry_list",
"at",
"resourceinstances",
"endsemi",
"attribute_operations",
"resourceinst",
"title_colon",
"collect_query",
"optional_query",
"attribute_operation",
"attribute_name",
"keyword",
"classname",
"parameter_list",
+ "opt_statements",
+ "stacked_classname",
"classparent",
"classnameordefault",
"hostnames",
"nodeparent",
"hostname",
+ "dotted_name",
"parameter",
"hashpairs",
"hashpair",
"string",
"dq_string",
+ "heredoc",
"dqpre",
"dqrval",
"dqpost",
"dqmid",
"text_expression",
- "dqtail" ]
+ "dqtail",
+ "sublocated_text",
+ "epp_parameters_list",
+ "epp_end" ]
Racc_debug_parser = false
##### State transition tables end #####
# reduce 0 omitted
-module_eval(<<'.,.,', 'egrammar.ra', 57)
+module_eval(<<'.,.,', 'egrammar.ra', 64)
def _reduce_1(val, _values, result)
- result = Factory.block_or_expression(*val[0])
+ result = create_program(Factory.block_or_expression(*val[0]))
result
end
.,.,
-# reduce 2 omitted
-
-module_eval(<<'.,.,', 'egrammar.ra', 62)
- def _reduce_3(val, _values, result)
- result = transform_calls(val[0])
+module_eval(<<'.,.,', 'egrammar.ra', 65)
+ def _reduce_2(val, _values, result)
+ result = create_program(Factory.block_or_expression(*val[0]))
result
end
.,.,
-module_eval(<<'.,.,', 'egrammar.ra', 68)
+# reduce 3 omitted
+
+module_eval(<<'.,.,', 'egrammar.ra', 70)
def _reduce_4(val, _values, result)
- result = [val[0]]
+ result = transform_calls(val[0])
result
end
.,.,
-module_eval(<<'.,.,', 'egrammar.ra', 69)
+module_eval(<<'.,.,', 'egrammar.ra', 76)
def _reduce_5(val, _values, result)
- result = val[0].push val[2]
+ result = [val[0]]
result
end
.,.,
-module_eval(<<'.,.,', 'egrammar.ra', 70)
+module_eval(<<'.,.,', 'egrammar.ra', 77)
def _reduce_6(val, _values, result)
- result = val[0].push val[1]
+ result = val[0].push val[2]
result
end
.,.,
-module_eval(<<'.,.,', 'egrammar.ra', 74)
+module_eval(<<'.,.,', 'egrammar.ra', 78)
def _reduce_7(val, _values, result)
- result = val[0]
+ result = val[0].push val[1]
result
end
.,.,
-module_eval(<<'.,.,', 'egrammar.ra', 75)
+module_eval(<<'.,.,', 'egrammar.ra', 82)
def _reduce_8(val, _values, result)
- result = aryfy(val[0]).push val[2]
+ result = val[0]
result
end
.,.,
-# reduce 9 omitted
-
-module_eval(<<'.,.,', 'egrammar.ra', 81)
- def _reduce_10(val, _values, result)
- result = val[0]
+module_eval(<<'.,.,', 'egrammar.ra', 83)
+ def _reduce_9(val, _values, result)
+ result = aryfy(val[0]).push val[2]
result
end
.,.,
-module_eval(<<'.,.,', 'egrammar.ra', 82)
+# reduce 10 omitted
+
+module_eval(<<'.,.,', 'egrammar.ra', 89)
def _reduce_11(val, _values, result)
- result = val[0].relop(val[1][:value], val[2]); loc result, val[1]
+ result = val[0]
result
end
.,.,
-module_eval(<<'.,.,', 'egrammar.ra', 83)
+module_eval(<<'.,.,', 'egrammar.ra', 90)
def _reduce_12(val, _values, result)
result = val[0].relop(val[1][:value], val[2]); loc result, val[1]
result
end
.,.,
-module_eval(<<'.,.,', 'egrammar.ra', 84)
+module_eval(<<'.,.,', 'egrammar.ra', 91)
def _reduce_13(val, _values, result)
result = val[0].relop(val[1][:value], val[2]); loc result, val[1]
result
end
.,.,
-module_eval(<<'.,.,', 'egrammar.ra', 85)
+module_eval(<<'.,.,', 'egrammar.ra', 92)
def _reduce_14(val, _values, result)
result = val[0].relop(val[1][:value], val[2]); loc result, val[1]
result
end
.,.,
-# reduce 15 omitted
-
-module_eval(<<'.,.,', 'egrammar.ra', 92)
- def _reduce_16(val, _values, result)
- result = val[0][*val[2]] ; loc result, val[0], val[3]
+module_eval(<<'.,.,', 'egrammar.ra', 93)
+ def _reduce_15(val, _values, result)
+ result = val[0].relop(val[1][:value], val[2]); loc result, val[1]
result
end
.,.,
-module_eval(<<'.,.,', 'egrammar.ra', 93)
+# reduce 16 omitted
+
+module_eval(<<'.,.,', 'egrammar.ra', 100)
def _reduce_17(val, _values, result)
- result = val[0].in val[2] ; loc result, val[1]
+ result = val[0][*val[2]] ; loc result, val[0], val[3]
result
end
.,.,
-module_eval(<<'.,.,', 'egrammar.ra', 94)
+module_eval(<<'.,.,', 'egrammar.ra', 101)
def _reduce_18(val, _values, result)
- result = val[0] =~ val[2] ; loc result, val[1]
+ result = val[0].in val[2] ; loc result, val[1]
result
end
.,.,
-module_eval(<<'.,.,', 'egrammar.ra', 95)
+module_eval(<<'.,.,', 'egrammar.ra', 102)
def _reduce_19(val, _values, result)
- result = val[0].mne val[2] ; loc result, val[1]
+ result = val[0] =~ val[2] ; loc result, val[1]
result
end
.,.,
-module_eval(<<'.,.,', 'egrammar.ra', 96)
+module_eval(<<'.,.,', 'egrammar.ra', 103)
def _reduce_20(val, _values, result)
- result = val[0] + val[2] ; loc result, val[1]
+ result = val[0].mne val[2] ; loc result, val[1]
result
end
.,.,
-module_eval(<<'.,.,', 'egrammar.ra', 97)
+module_eval(<<'.,.,', 'egrammar.ra', 104)
def _reduce_21(val, _values, result)
- result = val[0] - val[2] ; loc result, val[1]
+ result = val[0] + val[2] ; loc result, val[1]
result
end
.,.,
-module_eval(<<'.,.,', 'egrammar.ra', 98)
+module_eval(<<'.,.,', 'egrammar.ra', 105)
def _reduce_22(val, _values, result)
- result = val[0] / val[2] ; loc result, val[1]
+ result = val[0] - val[2] ; loc result, val[1]
result
end
.,.,
-module_eval(<<'.,.,', 'egrammar.ra', 99)
+module_eval(<<'.,.,', 'egrammar.ra', 106)
def _reduce_23(val, _values, result)
- result = val[0] * val[2] ; loc result, val[1]
+ result = val[0] / val[2] ; loc result, val[1]
result
end
.,.,
-module_eval(<<'.,.,', 'egrammar.ra', 100)
+module_eval(<<'.,.,', 'egrammar.ra', 107)
def _reduce_24(val, _values, result)
- result = val[0] % val[2] ; loc result, val[1]
+ result = val[0] * val[2] ; loc result, val[1]
result
end
.,.,
-module_eval(<<'.,.,', 'egrammar.ra', 101)
+module_eval(<<'.,.,', 'egrammar.ra', 108)
def _reduce_25(val, _values, result)
- result = val[0] << val[2] ; loc result, val[1]
+ result = val[0] % val[2] ; loc result, val[1]
result
end
.,.,
-module_eval(<<'.,.,', 'egrammar.ra', 102)
+module_eval(<<'.,.,', 'egrammar.ra', 109)
def _reduce_26(val, _values, result)
- result = val[0] >> val[2] ; loc result, val[1]
+ result = val[0] << val[2] ; loc result, val[1]
result
end
.,.,
-module_eval(<<'.,.,', 'egrammar.ra', 103)
+module_eval(<<'.,.,', 'egrammar.ra', 110)
def _reduce_27(val, _values, result)
- result = val[1].minus() ; loc result, val[0]
+ result = val[0] >> val[2] ; loc result, val[1]
result
end
.,.,
-module_eval(<<'.,.,', 'egrammar.ra', 104)
+module_eval(<<'.,.,', 'egrammar.ra', 111)
def _reduce_28(val, _values, result)
- result = val[0].ne val[2] ; loc result, val[1]
+ result = val[1].minus() ; loc result, val[0]
result
end
.,.,
-module_eval(<<'.,.,', 'egrammar.ra', 105)
+module_eval(<<'.,.,', 'egrammar.ra', 112)
def _reduce_29(val, _values, result)
- result = val[0] == val[2] ; loc result, val[1]
+ result = val[0].ne val[2] ; loc result, val[1]
result
end
.,.,
-module_eval(<<'.,.,', 'egrammar.ra', 106)
+module_eval(<<'.,.,', 'egrammar.ra', 113)
def _reduce_30(val, _values, result)
- result = val[0] > val[2] ; loc result, val[1]
+ result = val[0] == val[2] ; loc result, val[1]
result
end
.,.,
-module_eval(<<'.,.,', 'egrammar.ra', 107)
+module_eval(<<'.,.,', 'egrammar.ra', 114)
def _reduce_31(val, _values, result)
- result = val[0] >= val[2] ; loc result, val[1]
+ result = val[0] > val[2] ; loc result, val[1]
result
end
.,.,
-module_eval(<<'.,.,', 'egrammar.ra', 108)
+module_eval(<<'.,.,', 'egrammar.ra', 115)
def _reduce_32(val, _values, result)
- result = val[0] < val[2] ; loc result, val[1]
+ result = val[0] >= val[2] ; loc result, val[1]
result
end
.,.,
-module_eval(<<'.,.,', 'egrammar.ra', 109)
+module_eval(<<'.,.,', 'egrammar.ra', 116)
def _reduce_33(val, _values, result)
- result = val[0] <= val[2] ; loc result, val[1]
+ result = val[0] < val[2] ; loc result, val[1]
result
end
.,.,
-module_eval(<<'.,.,', 'egrammar.ra', 110)
+module_eval(<<'.,.,', 'egrammar.ra', 117)
def _reduce_34(val, _values, result)
- result = val[1].not ; loc result, val[0]
+ result = val[0] <= val[2] ; loc result, val[1]
result
end
.,.,
-module_eval(<<'.,.,', 'egrammar.ra', 111)
+module_eval(<<'.,.,', 'egrammar.ra', 118)
def _reduce_35(val, _values, result)
- result = val[0].and val[2] ; loc result, val[1]
+ result = val[1].not ; loc result, val[0]
result
end
.,.,
-module_eval(<<'.,.,', 'egrammar.ra', 112)
+module_eval(<<'.,.,', 'egrammar.ra', 119)
def _reduce_36(val, _values, result)
- result = val[0].or val[2] ; loc result, val[1]
+ result = val[0].and val[2] ; loc result, val[1]
result
end
.,.,
-module_eval(<<'.,.,', 'egrammar.ra', 113)
+module_eval(<<'.,.,', 'egrammar.ra', 120)
def _reduce_37(val, _values, result)
- result = val[0].set(val[2]) ; loc result, val[1]
+ result = val[0].or val[2] ; loc result, val[1]
result
end
.,.,
-module_eval(<<'.,.,', 'egrammar.ra', 114)
+module_eval(<<'.,.,', 'egrammar.ra', 121)
def _reduce_38(val, _values, result)
- result = val[0].plus_set(val[2]) ; loc result, val[1]
+ result = val[0].set(val[2]) ; loc result, val[1]
result
end
.,.,
-module_eval(<<'.,.,', 'egrammar.ra', 115)
+module_eval(<<'.,.,', 'egrammar.ra', 122)
def _reduce_39(val, _values, result)
- result = val[0].select(*val[2]) ; loc result, val[0]
+ result = val[0].plus_set(val[2]) ; loc result, val[1]
result
end
.,.,
-module_eval(<<'.,.,', 'egrammar.ra', 116)
+module_eval(<<'.,.,', 'egrammar.ra', 123)
def _reduce_40(val, _values, result)
- result = val[1].paren() ; loc result, val[0]
+ result = val[0].minus_set(val[2]); loc result, val[1]
result
end
.,.,
module_eval(<<'.,.,', 'egrammar.ra', 124)
def _reduce_41(val, _values, result)
- result = [val[0]]
+ result = val[0].select(*val[2]) ; loc result, val[0]
result
end
.,.,
module_eval(<<'.,.,', 'egrammar.ra', 125)
def _reduce_42(val, _values, result)
- result = val[0].push(val[2])
+ result = val[1].paren() ; loc result, val[0]
result
end
.,.,
-# reduce 43 omitted
+module_eval(<<'.,.,', 'egrammar.ra', 133)
+ def _reduce_43(val, _values, result)
+ result = [val[0]]
+ result
+ end
+.,.,
-# reduce 44 omitted
+module_eval(<<'.,.,', 'egrammar.ra', 134)
+ def _reduce_44(val, _values, result)
+ result = val[0].push(val[2])
+ result
+ end
+.,.,
# reduce 45 omitted
# reduce 46 omitted
# reduce 47 omitted
# reduce 48 omitted
# reduce 49 omitted
# reduce 50 omitted
# reduce 51 omitted
# reduce 52 omitted
# reduce 53 omitted
# reduce 54 omitted
# reduce 55 omitted
# reduce 56 omitted
# reduce 57 omitted
# reduce 58 omitted
# reduce 59 omitted
# reduce 60 omitted
# reduce 61 omitted
-module_eval(<<'.,.,', 'egrammar.ra', 155)
- def _reduce_62(val, _values, result)
+# reduce 62 omitted
+
+# reduce 63 omitted
+
+# reduce 64 omitted
+
+# reduce 65 omitted
+
+module_eval(<<'.,.,', 'egrammar.ra', 166)
+ def _reduce_66(val, _values, result)
result = val[0]
result
end
.,.,
-module_eval(<<'.,.,', 'egrammar.ra', 156)
- def _reduce_63(val, _values, result)
+module_eval(<<'.,.,', 'egrammar.ra', 167)
+ def _reduce_67(val, _values, result)
result = val[0]
result
end
.,.,
-module_eval(<<'.,.,', 'egrammar.ra', 164)
- def _reduce_64(val, _values, result)
+module_eval(<<'.,.,', 'egrammar.ra', 175)
+ def _reduce_68(val, _values, result)
result = Factory.CALL_NAMED(val[0], true, val[2])
loc result, val[0], val[4]
result
end
.,.,
-module_eval(<<'.,.,', 'egrammar.ra', 168)
- def _reduce_65(val, _values, result)
+module_eval(<<'.,.,', 'egrammar.ra', 179)
+ def _reduce_69(val, _values, result)
result = Factory.CALL_NAMED(val[0], true, [])
loc result, val[0], val[2]
result
end
.,.,
-module_eval(<<'.,.,', 'egrammar.ra', 172)
- def _reduce_66(val, _values, result)
+module_eval(<<'.,.,', 'egrammar.ra', 183)
+ def _reduce_70(val, _values, result)
result = Factory.CALL_NAMED(val[0], true, val[2])
loc result, val[0], val[4]
result.lambda = val[5]
result
end
.,.,
-module_eval(<<'.,.,', 'egrammar.ra', 177)
- def _reduce_67(val, _values, result)
+module_eval(<<'.,.,', 'egrammar.ra', 188)
+ def _reduce_71(val, _values, result)
result = Factory.CALL_NAMED(val[0], true, [])
loc result, val[0], val[2]
result.lambda = val[3]
result
end
.,.,
-module_eval(<<'.,.,', 'egrammar.ra', 181)
- def _reduce_68(val, _values, result)
+module_eval(<<'.,.,', 'egrammar.ra', 192)
+ def _reduce_72(val, _values, result)
result = val[0]
result
end
.,.,
-module_eval(<<'.,.,', 'egrammar.ra', 186)
- def _reduce_69(val, _values, result)
+module_eval(<<'.,.,', 'egrammar.ra', 197)
+ def _reduce_73(val, _values, result)
result = val[0]
result
end
.,.,
-module_eval(<<'.,.,', 'egrammar.ra', 187)
- def _reduce_70(val, _values, result)
+module_eval(<<'.,.,', 'egrammar.ra', 198)
+ def _reduce_74(val, _values, result)
result = val[0]; val[0].lambda = val[1]
result
end
.,.,
-module_eval(<<'.,.,', 'egrammar.ra', 190)
- def _reduce_71(val, _values, result)
+module_eval(<<'.,.,', 'egrammar.ra', 201)
+ def _reduce_75(val, _values, result)
result = Factory.CALL_METHOD(val[0], val[2]); loc result, val[1], val[3]
result
end
.,.,
-module_eval(<<'.,.,', 'egrammar.ra', 191)
- def _reduce_72(val, _values, result)
+module_eval(<<'.,.,', 'egrammar.ra', 202)
+ def _reduce_76(val, _values, result)
result = Factory.CALL_METHOD(val[0], []); loc result, val[1], val[3]
result
end
.,.,
-module_eval(<<'.,.,', 'egrammar.ra', 192)
- def _reduce_73(val, _values, result)
+module_eval(<<'.,.,', 'egrammar.ra', 203)
+ def _reduce_77(val, _values, result)
result = Factory.CALL_METHOD(val[0], []); loc result, val[0]
result
end
.,.,
-module_eval(<<'.,.,', 'egrammar.ra', 197)
- def _reduce_74(val, _values, result)
+module_eval(<<'.,.,', 'egrammar.ra', 208)
+ def _reduce_78(val, _values, result)
result = val[0].dot(Factory.fqn(val[2][:value]))
loc result, val[1], val[2]
result
end
.,.,
-module_eval(<<'.,.,', 'egrammar.ra', 209)
- def _reduce_75(val, _values, result)
+module_eval(<<'.,.,', 'egrammar.ra', 220)
+ def _reduce_79(val, _values, result)
result = Factory.LAMBDA(val[0], val[1])
# loc result, val[1] # TODO
result
end
.,.,
-module_eval(<<'.,.,', 'egrammar.ra', 214)
- def _reduce_76(val, _values, result)
+module_eval(<<'.,.,', 'egrammar.ra', 225)
+ def _reduce_80(val, _values, result)
result = val[1]
result
end
.,.,
-module_eval(<<'.,.,', 'egrammar.ra', 215)
- def _reduce_77(val, _values, result)
+module_eval(<<'.,.,', 'egrammar.ra', 226)
+ def _reduce_81(val, _values, result)
result = nil
result
end
.,.,
-module_eval(<<'.,.,', 'egrammar.ra', 219)
- def _reduce_78(val, _values, result)
+module_eval(<<'.,.,', 'egrammar.ra', 230)
+ def _reduce_82(val, _values, result)
result = []
result
end
.,.,
-module_eval(<<'.,.,', 'egrammar.ra', 220)
- def _reduce_79(val, _values, result)
+module_eval(<<'.,.,', 'egrammar.ra', 231)
+ def _reduce_83(val, _values, result)
result = val[1]
result
end
.,.,
-module_eval(<<'.,.,', 'egrammar.ra', 230)
- def _reduce_80(val, _values, result)
+module_eval(<<'.,.,', 'egrammar.ra', 241)
+ def _reduce_84(val, _values, result)
result = val[1]
loc(result, val[0], val[1])
result
end
.,.,
-module_eval(<<'.,.,', 'egrammar.ra', 237)
- def _reduce_81(val, _values, result)
+module_eval(<<'.,.,', 'egrammar.ra', 248)
+ def _reduce_85(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]))
result
end
.,.,
-module_eval(<<'.,.,', 'egrammar.ra', 241)
- def _reduce_82(val, _values, result)
+module_eval(<<'.,.,', 'egrammar.ra', 252)
+ def _reduce_86(val, _values, result)
result = Factory.IF(val[0], nil, val[3])
loc(result, val[0], (val[3] ? val[3] : val[2]))
result
end
.,.,
-# reduce 83 omitted
+# reduce 87 omitted
-module_eval(<<'.,.,', 'egrammar.ra', 249)
- def _reduce_84(val, _values, result)
+module_eval(<<'.,.,', 'egrammar.ra', 260)
+ def _reduce_88(val, _values, result)
result = val[1]
loc(result, val[0], val[1])
result
end
.,.,
-module_eval(<<'.,.,', 'egrammar.ra', 253)
- def _reduce_85(val, _values, result)
+module_eval(<<'.,.,', 'egrammar.ra', 264)
+ def _reduce_89(val, _values, result)
result = Factory.block_or_expression(*val[2])
loc result, val[0], val[3]
result
end
.,.,
-module_eval(<<'.,.,', 'egrammar.ra', 257)
- def _reduce_86(val, _values, result)
+module_eval(<<'.,.,', 'egrammar.ra', 268)
+ def _reduce_90(val, _values, result)
result = nil # don't think a nop is needed here either
result
end
.,.,
-module_eval(<<'.,.,', 'egrammar.ra', 266)
- def _reduce_87(val, _values, result)
+module_eval(<<'.,.,', 'egrammar.ra', 277)
+ def _reduce_91(val, _values, result)
result = Factory.UNLESS(val[1], Factory.block_or_expression(*val[3]), val[5])
loc result, val[0], val[4]
result
end
.,.,
-module_eval(<<'.,.,', 'egrammar.ra', 270)
- def _reduce_88(val, _values, result)
+module_eval(<<'.,.,', 'egrammar.ra', 281)
+ def _reduce_92(val, _values, result)
result = Factory.UNLESS(val[1], nil, nil)
loc result, val[0], val[4]
result
end
.,.,
-# reduce 89 omitted
+# reduce 93 omitted
-module_eval(<<'.,.,', 'egrammar.ra', 280)
- def _reduce_90(val, _values, result)
+module_eval(<<'.,.,', 'egrammar.ra', 291)
+ def _reduce_94(val, _values, result)
result = Factory.block_or_expression(*val[2])
loc result, val[0], val[3]
result
end
.,.,
-module_eval(<<'.,.,', 'egrammar.ra', 284)
- def _reduce_91(val, _values, result)
+module_eval(<<'.,.,', 'egrammar.ra', 295)
+ def _reduce_95(val, _values, result)
result = nil # don't think a nop is needed here either
result
end
.,.,
-module_eval(<<'.,.,', 'egrammar.ra', 292)
- def _reduce_92(val, _values, result)
+module_eval(<<'.,.,', 'egrammar.ra', 303)
+ def _reduce_96(val, _values, result)
result = Factory.CASE(val[1], *val[3])
loc result, val[0], val[4]
result
end
.,.,
-module_eval(<<'.,.,', 'egrammar.ra', 298)
- def _reduce_93(val, _values, result)
+module_eval(<<'.,.,', 'egrammar.ra', 309)
+ def _reduce_97(val, _values, result)
result = [val[0]]
result
end
.,.,
-module_eval(<<'.,.,', 'egrammar.ra', 299)
- def _reduce_94(val, _values, result)
+module_eval(<<'.,.,', 'egrammar.ra', 310)
+ def _reduce_98(val, _values, result)
result = val[0].push val[1]
result
end
.,.,
-module_eval(<<'.,.,', 'egrammar.ra', 304)
- def _reduce_95(val, _values, result)
+module_eval(<<'.,.,', 'egrammar.ra', 315)
+ def _reduce_99(val, _values, result)
result = Factory.WHEN(val[0], val[3])
loc result, val[1], val[4]
result
end
.,.,
-module_eval(<<'.,.,', 'egrammar.ra', 308)
- def _reduce_96(val, _values, result)
+module_eval(<<'.,.,', 'egrammar.ra', 319)
+ def _reduce_100(val, _values, result)
result = Factory.WHEN(val[0], nil)
loc result, val[1], val[3]
result
end
.,.,
-module_eval(<<'.,.,', 'egrammar.ra', 312)
- def _reduce_97(val, _values, result)
+module_eval(<<'.,.,', 'egrammar.ra', 323)
+ def _reduce_101(val, _values, result)
result = val[0]
result
end
.,.,
-# reduce 98 omitted
+# reduce 102 omitted
-module_eval(<<'.,.,', 'egrammar.ra', 323)
- def _reduce_99(val, _values, result)
+module_eval(<<'.,.,', 'egrammar.ra', 334)
+ def _reduce_103(val, _values, result)
result = val[1]
result
end
.,.,
-module_eval(<<'.,.,', 'egrammar.ra', 328)
- def _reduce_100(val, _values, result)
+module_eval(<<'.,.,', 'egrammar.ra', 339)
+ def _reduce_104(val, _values, result)
result = [val[0]]
result
end
.,.,
-module_eval(<<'.,.,', 'egrammar.ra', 329)
- def _reduce_101(val, _values, result)
+module_eval(<<'.,.,', 'egrammar.ra', 340)
+ def _reduce_105(val, _values, result)
result = val[0].push val[2]
result
end
.,.,
-module_eval(<<'.,.,', 'egrammar.ra', 334)
- def _reduce_102(val, _values, result)
+module_eval(<<'.,.,', 'egrammar.ra', 345)
+ def _reduce_106(val, _values, result)
result = Factory.MAP(val[0], val[2]) ; loc result, val[1]
result
end
.,.,
-module_eval(<<'.,.,', 'egrammar.ra', 350)
- def _reduce_103(val, _values, result)
+module_eval(<<'.,.,', 'egrammar.ra', 357)
+ def _reduce_107(val, _values, result)
result = val[0]
result
end
.,.,
-module_eval(<<'.,.,', 'egrammar.ra', 353)
- def _reduce_104(val, _values, result)
+module_eval(<<'.,.,', 'egrammar.ra', 360)
+ def _reduce_108(val, _values, result)
result = case Factory.resource_shape(val[1])
when :resource, :class
tmp = Factory.RESOURCE(Factory.fqn(token_text(val[1])), val[3])
tmp.form = val[0]
tmp
when :defaults
- error "A resource default can not be virtual or exported"
+ error val[1], "A resource default can not be virtual or exported"
when :override
- error "A resource override can not be virtual or exported"
+ error val[1], "A resource override can not be virtual or exported"
else
- error "Expression is not valid as a resource, resource-default, or resource-override"
+ error val[1], "Expression is not valid as a resource, resource-default, or resource-override"
end
loc result, val[1], val[4]
result
end
.,.,
-module_eval(<<'.,.,', 'egrammar.ra', 368)
- def _reduce_105(val, _values, result)
+module_eval(<<'.,.,', 'egrammar.ra', 375)
+ def _reduce_109(val, _values, result)
result = case Factory.resource_shape(val[1])
- when :resource, :class
- error "Defaults are not virtualizable"
- when :defaults
- error "Defaults are not virtualizable"
- when :override
- error "Defaults are not virtualizable"
+ when :resource, :class, :defaults, :override
+ error val[1], "Defaults are not virtualizable"
else
- error "Expression is not valid as a resource, resource-default, or resource-override"
+ error val[1], "Expression is not valid as a resource, resource-default, or resource-override"
end
result
end
.,.,
-module_eval(<<'.,.,', 'egrammar.ra', 380)
- def _reduce_106(val, _values, result)
+module_eval(<<'.,.,', 'egrammar.ra', 383)
+ def _reduce_110(val, _values, result)
result = case Factory.resource_shape(val[0])
when :resource, :class
Factory.RESOURCE(Factory.fqn(token_text(val[0])), val[2])
when :defaults
- error "A resource default can not specify a resource name"
+ error val[1], "A resource default can not specify a resource name"
when :override
- error "A resource override does not allow override of name of resource"
+ error val[1], "A resource override does not allow override of name of resource"
else
- error "Expression is not valid as a resource, resource-default, or resource-override"
+ error val[1], "Expression is not valid as a resource, resource-default, or resource-override"
end
loc result, val[0], val[4]
result
end
.,.,
-module_eval(<<'.,.,', 'egrammar.ra', 393)
- def _reduce_107(val, _values, result)
+module_eval(<<'.,.,', 'egrammar.ra', 396)
+ def _reduce_111(val, _values, result)
result = case Factory.resource_shape(val[0])
when :resource, :class
# This catches deprecated syntax.
- error "All resource specifications require names"
+ # If the attribute operations does not include +>, then the found expression
+ # is actually a LEFT followed by LITERAL_HASH
+ #
+ unless tmp = transform_resource_wo_title(val[0], val[2])
+ error val[1], "Syntax error resource body without title or hash with +>"
+ end
+ tmp
when :defaults
Factory.RESOURCE_DEFAULTS(val[0], val[2])
when :override
# This was only done for override in original - TODO shuld it be here at all
Factory.RESOURCE_OVERRIDE(val[0], val[2])
else
- error "Expression is not valid as a resource, resource-default, or resource-override"
+ error val[0], "Expression is not valid as a resource, resource-default, or resource-override"
end
loc result, val[0], val[4]
result
end
.,.,
-module_eval(<<'.,.,', 'egrammar.ra', 408)
- def _reduce_108(val, _values, result)
+module_eval(<<'.,.,', 'egrammar.ra', 417)
+ def _reduce_112(val, _values, result)
+ result = Factory.RESOURCE(Factory.fqn(token_text(val[1])), val[3])
+ result.form = val[0]
+ loc result, val[1], val[5]
+
+ result
+ end
+.,.,
+
+module_eval(<<'.,.,', 'egrammar.ra', 422)
+ def _reduce_113(val, _values, result)
result = Factory.RESOURCE(Factory.fqn(token_text(val[0])), val[2])
loc result, val[0], val[4]
result
end
.,.,
-module_eval(<<'.,.,', 'egrammar.ra', 413)
- def _reduce_109(val, _values, result)
+module_eval(<<'.,.,', 'egrammar.ra', 427)
+ def _reduce_114(val, _values, result)
result = Factory.RESOURCE_BODY(val[0], val[2])
result
end
.,.,
-module_eval(<<'.,.,', 'egrammar.ra', 415)
- def _reduce_110(val, _values, result)
+module_eval(<<'.,.,', 'egrammar.ra', 429)
+ def _reduce_115(val, _values, result)
result = val[0]
result
end
.,.,
-module_eval(<<'.,.,', 'egrammar.ra', 418)
- def _reduce_111(val, _values, result)
+module_eval(<<'.,.,', 'egrammar.ra', 432)
+ def _reduce_116(val, _values, result)
result = [val[0]]
result
end
.,.,
-module_eval(<<'.,.,', 'egrammar.ra', 419)
- def _reduce_112(val, _values, result)
+module_eval(<<'.,.,', 'egrammar.ra', 433)
+ def _reduce_117(val, _values, result)
result = val[0].push val[2]
result
end
.,.,
-module_eval(<<'.,.,', 'egrammar.ra', 424)
- def _reduce_113(val, _values, result)
+module_eval(<<'.,.,', 'egrammar.ra', 438)
+ def _reduce_118(val, _values, result)
result = :virtual
result
end
.,.,
-module_eval(<<'.,.,', 'egrammar.ra', 425)
- def _reduce_114(val, _values, result)
+module_eval(<<'.,.,', 'egrammar.ra', 439)
+ def _reduce_119(val, _values, result)
result = :exported
result
end
.,.,
-module_eval(<<'.,.,', 'egrammar.ra', 437)
- def _reduce_115(val, _values, result)
+module_eval(<<'.,.,', 'egrammar.ra', 440)
+ def _reduce_120(val, _values, result)
+ result = :exported
+ result
+ end
+.,.,
+
+module_eval(<<'.,.,', 'egrammar.ra', 452)
+ def _reduce_121(val, _values, result)
result = Factory.COLLECT(val[0], val[1], val[3])
loc result, val[0], val[5]
result
end
.,.,
-module_eval(<<'.,.,', 'egrammar.ra', 441)
- def _reduce_116(val, _values, result)
+module_eval(<<'.,.,', 'egrammar.ra', 456)
+ def _reduce_122(val, _values, result)
result = Factory.COLLECT(val[0], val[1], [])
loc result, val[0], val[1]
result
end
.,.,
-module_eval(<<'.,.,', 'egrammar.ra', 446)
- def _reduce_117(val, _values, result)
+module_eval(<<'.,.,', 'egrammar.ra', 461)
+ def _reduce_123(val, _values, result)
result = Factory.VIRTUAL_QUERY(val[1]) ; loc result, val[0], val[2]
result
end
.,.,
-module_eval(<<'.,.,', 'egrammar.ra', 447)
- def _reduce_118(val, _values, result)
+module_eval(<<'.,.,', 'egrammar.ra', 462)
+ def _reduce_124(val, _values, result)
result = Factory.EXPORTED_QUERY(val[1]) ; loc result, val[0], val[2]
result
end
.,.,
-# reduce 119 omitted
+# reduce 125 omitted
-# reduce 120 omitted
+# reduce 126 omitted
-module_eval(<<'.,.,', 'egrammar.ra', 460)
- def _reduce_121(val, _values, result)
+module_eval(<<'.,.,', 'egrammar.ra', 475)
+ def _reduce_127(val, _values, result)
result = []
result
end
.,.,
-module_eval(<<'.,.,', 'egrammar.ra', 461)
- def _reduce_122(val, _values, result)
+module_eval(<<'.,.,', 'egrammar.ra', 476)
+ def _reduce_128(val, _values, result)
result = [val[0]]
result
end
.,.,
-module_eval(<<'.,.,', 'egrammar.ra', 462)
- def _reduce_123(val, _values, result)
+module_eval(<<'.,.,', 'egrammar.ra', 477)
+ def _reduce_129(val, _values, result)
result = val[0].push(val[2])
result
end
.,.,
-# reduce 124 omitted
+# reduce 130 omitted
-# reduce 125 omitted
+# reduce 131 omitted
-# reduce 126 omitted
+# reduce 132 omitted
-module_eval(<<'.,.,', 'egrammar.ra', 478)
- def _reduce_127(val, _values, result)
+module_eval(<<'.,.,', 'egrammar.ra', 493)
+ def _reduce_133(val, _values, result)
result = Factory.ATTRIBUTE_OP(val[0][:value], :'=>', val[2])
loc result, val[0], val[2]
result
end
.,.,
-module_eval(<<'.,.,', 'egrammar.ra', 482)
- def _reduce_128(val, _values, result)
+module_eval(<<'.,.,', 'egrammar.ra', 497)
+ def _reduce_134(val, _values, result)
result = Factory.ATTRIBUTE_OP(val[0][:value], :'+>', val[2])
loc result, val[0], val[2]
result
end
.,.,
-module_eval(<<'.,.,', 'egrammar.ra', 492)
- def _reduce_129(val, _values, result)
- result = Factory.DEFINITION(classname(val[1][:value]), val[2], val[4])
+module_eval(<<'.,.,', 'egrammar.ra', 507)
+ def _reduce_135(val, _values, result)
+ result = add_definition(Factory.DEFINITION(classname(val[1][:value]), val[2], val[4]))
loc result, val[0], val[5]
- @lexer.indefine = false
-
- result
- end
-.,.,
-
-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
+ # New lexer does not keep track of this, this is done in validation
+ if @lexer.respond_to?(:'indefine=')
+ @lexer.indefine = false
+ end
result
end
.,.,
-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])
+module_eval(<<'.,.,', 'egrammar.ra', 521)
+ def _reduce_136(val, _values, result)
+ # Remove this class' name from the namestack as all nested classes have been parsed
+ namepop
+ result = add_definition(Factory.HOSTCLASS(classname(val[1][:value]), val[2], token_text(val[3]), val[5]))
loc result, val[0], val[6]
result
end
.,.,
-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]
-
+module_eval(<<'.,.,', 'egrammar.ra', 531)
+ def _reduce_137(val, _values, result)
+ namestack(val[0][:value]) ; result = val[0]
result
end
.,.,
-# reduce 133 omitted
+# reduce 138 omitted
-module_eval(<<'.,.,', 'egrammar.ra', 525)
- def _reduce_134(val, _values, result)
+# reduce 139 omitted
+
+# reduce 140 omitted
+
+module_eval(<<'.,.,', 'egrammar.ra', 540)
+ def _reduce_141(val, _values, result)
result = val[1]
result
end
.,.,
-# reduce 135 omitted
+# reduce 142 omitted
-# reduce 136 omitted
+# reduce 143 omitted
-module_eval(<<'.,.,', 'egrammar.ra', 542)
- def _reduce_137(val, _values, result)
- result = Factory.NODE(val[1], val[2], val[4])
+module_eval(<<'.,.,', 'egrammar.ra', 557)
+ def _reduce_144(val, _values, result)
+ result = add_definition(Factory.NODE(val[1], val[2], val[4]))
loc result, val[0], val[5]
result
end
.,.,
-module_eval(<<'.,.,', 'egrammar.ra', 546)
- def _reduce_138(val, _values, result)
- result = Factory.NODE(val[1], val[2], nil)
+module_eval(<<'.,.,', 'egrammar.ra', 561)
+ def _reduce_145(val, _values, result)
+ result = add_definition(Factory.NODE(val[1], val[2], nil))
loc result, val[0], val[4]
result
end
.,.,
-module_eval(<<'.,.,', 'egrammar.ra', 556)
- def _reduce_139(val, _values, result)
+module_eval(<<'.,.,', 'egrammar.ra', 571)
+ def _reduce_146(val, _values, result)
result = [result]
result
end
.,.,
-module_eval(<<'.,.,', 'egrammar.ra', 557)
- def _reduce_140(val, _values, result)
+module_eval(<<'.,.,', 'egrammar.ra', 572)
+ def _reduce_147(val, _values, result)
result = val[0].push(val[2])
result
end
.,.,
-module_eval(<<'.,.,', 'egrammar.ra', 562)
- def _reduce_141(val, _values, result)
- result = Factory.fqn(val[0][:value]); loc result, val[0]
+module_eval(<<'.,.,', 'egrammar.ra', 577)
+ def _reduce_148(val, _values, result)
+ result = val[0]
result
end
.,.,
-module_eval(<<'.,.,', 'egrammar.ra', 563)
- def _reduce_142(val, _values, result)
+module_eval(<<'.,.,', 'egrammar.ra', 578)
+ def _reduce_149(val, _values, result)
result = val[0]
result
end
.,.,
-module_eval(<<'.,.,', 'egrammar.ra', 564)
- def _reduce_143(val, _values, result)
+module_eval(<<'.,.,', 'egrammar.ra', 579)
+ def _reduce_150(val, _values, result)
result = Factory.literal(:default); loc result, val[0]
result
end
.,.,
-# reduce 144 omitted
+# reduce 151 omitted
-# reduce 145 omitted
+module_eval(<<'.,.,', 'egrammar.ra', 583)
+ def _reduce_152(val, _values, result)
+ result = Factory.literal(val[0][:value]); loc result, val[0]
+ result
+ end
+.,.,
-module_eval(<<'.,.,', 'egrammar.ra', 570)
- def _reduce_146(val, _values, result)
+module_eval(<<'.,.,', 'egrammar.ra', 584)
+ def _reduce_153(val, _values, result)
+ result = Factory.concat(val[0], '.', val[2][:value]); loc result, val[0], val[2]
+ result
+ end
+.,.,
+
+# reduce 154 omitted
+
+module_eval(<<'.,.,', 'egrammar.ra', 589)
+ def _reduce_155(val, _values, result)
result = val[1]
result
end
.,.,
-module_eval(<<'.,.,', 'egrammar.ra', 575)
- def _reduce_147(val, _values, result)
+module_eval(<<'.,.,', 'egrammar.ra', 595)
+ def _reduce_156(val, _values, result)
result = val[0]
result
end
.,.,
-module_eval(<<'.,.,', 'egrammar.ra', 576)
- def _reduce_148(val, _values, result)
- result = val[0]
+module_eval(<<'.,.,', 'egrammar.ra', 596)
+ def _reduce_157(val, _values, result)
+ error val[0], "'class' is not a valid classname"
result
end
.,.,
-module_eval(<<'.,.,', 'egrammar.ra', 580)
- def _reduce_149(val, _values, result)
+module_eval(<<'.,.,', 'egrammar.ra', 600)
+ def _reduce_158(val, _values, result)
result = []
result
end
.,.,
-module_eval(<<'.,.,', 'egrammar.ra', 581)
- def _reduce_150(val, _values, result)
+module_eval(<<'.,.,', 'egrammar.ra', 601)
+ def _reduce_159(val, _values, result)
result = []
result
end
.,.,
-module_eval(<<'.,.,', 'egrammar.ra', 582)
- def _reduce_151(val, _values, result)
+module_eval(<<'.,.,', 'egrammar.ra', 602)
+ def _reduce_160(val, _values, result)
result = val[1]
result
end
.,.,
-module_eval(<<'.,.,', 'egrammar.ra', 586)
- def _reduce_152(val, _values, result)
+module_eval(<<'.,.,', 'egrammar.ra', 606)
+ def _reduce_161(val, _values, result)
result = [val[0]]
result
end
.,.,
-module_eval(<<'.,.,', 'egrammar.ra', 587)
- def _reduce_153(val, _values, result)
+module_eval(<<'.,.,', 'egrammar.ra', 607)
+ def _reduce_162(val, _values, result)
result = val[0].push(val[2])
result
end
.,.,
-module_eval(<<'.,.,', 'egrammar.ra', 591)
- def _reduce_154(val, _values, result)
+module_eval(<<'.,.,', 'egrammar.ra', 611)
+ def _reduce_163(val, _values, result)
result = Factory.PARAM(val[0][:value], val[2]) ; loc result, val[0]
result
end
.,.,
-module_eval(<<'.,.,', 'egrammar.ra', 592)
- def _reduce_155(val, _values, result)
+module_eval(<<'.,.,', 'egrammar.ra', 612)
+ def _reduce_164(val, _values, result)
result = Factory.PARAM(val[0][:value]); loc result, val[0]
result
end
.,.,
-# reduce 156 omitted
+module_eval(<<'.,.,', 'egrammar.ra', 625)
+ def _reduce_165(val, _values, result)
+ result = Factory.fqn(val[0][:value]).var ; loc result, val[0]
+ result
+ end
+.,.,
-# reduce 157 omitted
+module_eval(<<'.,.,', 'egrammar.ra', 631)
+ def _reduce_166(val, _values, result)
+ result = Factory.LIST(val[1]); loc result, val[0], val[2]
+ result
+ end
+.,.,
-module_eval(<<'.,.,', 'egrammar.ra', 605)
- def _reduce_158(val, _values, result)
- result = Factory.fqn(val[0][:value]).var ; loc result, val[0]
+module_eval(<<'.,.,', 'egrammar.ra', 632)
+ def _reduce_167(val, _values, result)
+ result = Factory.LIST(val[1]); loc result, val[0], val[3]
result
end
.,.,
-module_eval(<<'.,.,', 'egrammar.ra', 611)
- def _reduce_159(val, _values, result)
+module_eval(<<'.,.,', 'egrammar.ra', 633)
+ def _reduce_168(val, _values, result)
+ result = Factory.literal([]) ; loc result, val[0]
+ result
+ end
+.,.,
+
+module_eval(<<'.,.,', 'egrammar.ra', 634)
+ def _reduce_169(val, _values, result)
result = Factory.LIST(val[1]); loc result, val[0], val[2]
result
end
.,.,
-module_eval(<<'.,.,', 'egrammar.ra', 612)
- def _reduce_160(val, _values, result)
+module_eval(<<'.,.,', 'egrammar.ra', 635)
+ def _reduce_170(val, _values, result)
result = Factory.LIST(val[1]); loc result, val[0], val[3]
result
end
.,.,
-module_eval(<<'.,.,', 'egrammar.ra', 613)
- def _reduce_161(val, _values, result)
+module_eval(<<'.,.,', 'egrammar.ra', 636)
+ def _reduce_171(val, _values, result)
result = Factory.literal([]) ; loc result, val[0]
result
end
.,.,
-module_eval(<<'.,.,', 'egrammar.ra', 616)
- def _reduce_162(val, _values, result)
+module_eval(<<'.,.,', 'egrammar.ra', 639)
+ def _reduce_172(val, _values, result)
result = Factory.HASH(val[1]); loc result, val[0], val[2]
result
end
.,.,
-module_eval(<<'.,.,', 'egrammar.ra', 617)
- def _reduce_163(val, _values, result)
+module_eval(<<'.,.,', 'egrammar.ra', 640)
+ def _reduce_173(val, _values, result)
result = Factory.HASH(val[1]); loc result, val[0], val[3]
result
end
.,.,
-module_eval(<<'.,.,', 'egrammar.ra', 618)
- def _reduce_164(val, _values, result)
+module_eval(<<'.,.,', 'egrammar.ra', 641)
+ def _reduce_174(val, _values, result)
result = Factory.literal({}) ; loc result, val[0], val[3]
result
end
.,.,
-module_eval(<<'.,.,', 'egrammar.ra', 621)
- def _reduce_165(val, _values, result)
+module_eval(<<'.,.,', 'egrammar.ra', 644)
+ def _reduce_175(val, _values, result)
result = [val[0]]
result
end
.,.,
-module_eval(<<'.,.,', 'egrammar.ra', 622)
- def _reduce_166(val, _values, result)
+module_eval(<<'.,.,', 'egrammar.ra', 645)
+ def _reduce_176(val, _values, result)
result = val[0].push val[2]
result
end
.,.,
-module_eval(<<'.,.,', 'egrammar.ra', 625)
- def _reduce_167(val, _values, result)
+module_eval(<<'.,.,', 'egrammar.ra', 648)
+ def _reduce_177(val, _values, result)
result = Factory.KEY_ENTRY(val[0], val[2]); loc result, val[1]
result
end
.,.,
-# reduce 168 omitted
+# reduce 178 omitted
-# reduce 169 omitted
+# reduce 179 omitted
-module_eval(<<'.,.,', 'egrammar.ra', 631)
- def _reduce_170(val, _values, result)
+# reduce 180 omitted
+
+module_eval(<<'.,.,', 'egrammar.ra', 655)
+ def _reduce_181(val, _values, result)
result = Factory.literal(val[0][:value]) ; loc result, val[0]
result
end
.,.,
-module_eval(<<'.,.,', 'egrammar.ra', 632)
- def _reduce_171(val, _values, result)
+module_eval(<<'.,.,', 'egrammar.ra', 656)
+ def _reduce_182(val, _values, result)
result = Factory.string(val[0], *val[1]) ; loc result, val[0], val[1][-1]
result
end
.,.,
-module_eval(<<'.,.,', 'egrammar.ra', 633)
- def _reduce_172(val, _values, result)
+module_eval(<<'.,.,', 'egrammar.ra', 657)
+ def _reduce_183(val, _values, result)
result = Factory.literal(val[0][:value]); loc result, val[0]
result
end
.,.,
-module_eval(<<'.,.,', 'egrammar.ra', 634)
- def _reduce_173(val, _values, result)
+module_eval(<<'.,.,', 'egrammar.ra', 658)
+ def _reduce_184(val, _values, result)
result = Factory.literal(val[0][:value]); loc result, val[0]
result
end
.,.,
-module_eval(<<'.,.,', 'egrammar.ra', 635)
- def _reduce_174(val, _values, result)
+module_eval(<<'.,.,', 'egrammar.ra', 659)
+ def _reduce_185(val, _values, result)
result = Factory.literal(val[0][:value]); loc result, val[0]
result
end
.,.,
-module_eval(<<'.,.,', 'egrammar.ra', 636)
- def _reduce_175(val, _values, result)
+module_eval(<<'.,.,', 'egrammar.ra', 660)
+ def _reduce_186(val, _values, result)
result = [val[0]] + val[1]
result
end
.,.,
-module_eval(<<'.,.,', 'egrammar.ra', 637)
- def _reduce_176(val, _values, result)
+module_eval(<<'.,.,', 'egrammar.ra', 661)
+ def _reduce_187(val, _values, result)
result = Factory.TEXT(val[0])
result
end
.,.,
-module_eval(<<'.,.,', 'egrammar.ra', 640)
- def _reduce_177(val, _values, result)
+module_eval(<<'.,.,', 'egrammar.ra', 664)
+ def _reduce_188(val, _values, result)
result = [val[0]]
result
end
.,.,
-module_eval(<<'.,.,', 'egrammar.ra', 641)
- def _reduce_178(val, _values, result)
+module_eval(<<'.,.,', 'egrammar.ra', 665)
+ def _reduce_189(val, _values, result)
result = [val[0]] + val[1]
result
end
.,.,
-module_eval(<<'.,.,', 'egrammar.ra', 643)
- def _reduce_179(val, _values, result)
+module_eval(<<'.,.,', 'egrammar.ra', 668)
+ def _reduce_190(val, _values, result)
+ result = Factory.HEREDOC(val[0][:value], val[1]); loc result, val[0]
+ result
+ end
+.,.,
+
+module_eval(<<'.,.,', 'egrammar.ra', 671)
+ def _reduce_191(val, _values, result)
+ result = Factory.SUBLOCATE(val[0], val[1]); loc result, val[0]
+ result
+ end
+.,.,
+
+module_eval(<<'.,.,', 'egrammar.ra', 672)
+ def _reduce_192(val, _values, result)
+ result = Factory.SUBLOCATE(val[0], val[1]); loc result, val[0]
+ result
+ end
+.,.,
+
+module_eval(<<'.,.,', 'egrammar.ra', 675)
+ def _reduce_193(val, _values, result)
+ result = Factory.EPP(val[1], val[2]); loc result, val[0]
+ result
+ end
+.,.,
+
+module_eval(<<'.,.,', 'egrammar.ra', 678)
+ def _reduce_194(val, _values, result)
+ result = nil
+ result
+ end
+.,.,
+
+module_eval(<<'.,.,', 'egrammar.ra', 679)
+ def _reduce_195(val, _values, result)
+ result = []
+ result
+ end
+.,.,
+
+module_eval(<<'.,.,', 'egrammar.ra', 680)
+ def _reduce_196(val, _values, result)
+ result = val[1]
+ result
+ end
+.,.,
+
+module_eval(<<'.,.,', 'egrammar.ra', 683)
+ def _reduce_197(val, _values, result)
+ result = Factory.RENDER_STRING(val[0][:value]); loc result, val[0]
+ result
+ end
+.,.,
+
+module_eval(<<'.,.,', 'egrammar.ra', 684)
+ def _reduce_198(val, _values, result)
+ result = Factory.RENDER_EXPR(val[1]); loc result, val[0], val[2]
+ result
+ end
+.,.,
+
+module_eval(<<'.,.,', 'egrammar.ra', 685)
+ def _reduce_199(val, _values, result)
+ result = Factory.RENDER_EXPR(Factory.block_or_expression(*val[2])); loc result, val[0], val[4]
+ result
+ end
+.,.,
+
+# reduce 200 omitted
+
+# reduce 201 omitted
+
+module_eval(<<'.,.,', 'egrammar.ra', 691)
+ def _reduce_202(val, _values, result)
+ result = Factory.NUMBER(val[0][:value]) ; loc result, val[0]
+ result
+ end
+.,.,
+
+module_eval(<<'.,.,', 'egrammar.ra', 692)
+ def _reduce_203(val, _values, result)
result = Factory.QNAME_OR_NUMBER(val[0][:value]) ; loc result, val[0]
result
end
.,.,
-module_eval(<<'.,.,', 'egrammar.ra', 644)
- def _reduce_180(val, _values, result)
+module_eval(<<'.,.,', 'egrammar.ra', 693)
+ def _reduce_204(val, _values, result)
result = Factory.QREF(val[0][:value]) ; loc result, val[0]
result
end
.,.,
-module_eval(<<'.,.,', 'egrammar.ra', 645)
- def _reduce_181(val, _values, result)
+module_eval(<<'.,.,', 'egrammar.ra', 694)
+ def _reduce_205(val, _values, result)
result = Factory.literal(:undef); loc result, val[0]
result
end
.,.,
-module_eval(<<'.,.,', 'egrammar.ra', 646)
- def _reduce_182(val, _values, result)
+module_eval(<<'.,.,', 'egrammar.ra', 695)
+ def _reduce_206(val, _values, result)
result = Factory.literal(:default); loc result, val[0]
result
end
.,.,
-module_eval(<<'.,.,', 'egrammar.ra', 651)
- def _reduce_183(val, _values, result)
+module_eval(<<'.,.,', 'egrammar.ra', 700)
+ def _reduce_207(val, _values, result)
result = Factory.literal(val[0][:value]) ; loc result, val[0]
result
end
.,.,
-module_eval(<<'.,.,', 'egrammar.ra', 654)
- def _reduce_184(val, _values, result)
+module_eval(<<'.,.,', 'egrammar.ra', 703)
+ def _reduce_208(val, _values, result)
result = Factory.literal(val[0][:value]); loc result, val[0]
result
end
.,.,
-# reduce 185 omitted
+# reduce 209 omitted
-module_eval(<<'.,.,', 'egrammar.ra', 660)
- def _reduce_186(val, _values, result)
+module_eval(<<'.,.,', 'egrammar.ra', 709)
+ def _reduce_210(val, _values, result)
result = nil
result
end
.,.,
-# reduce 187 omitted
+# reduce 211 omitted
-# reduce 188 omitted
+# reduce 212 omitted
-# reduce 189 omitted
+# reduce 213 omitted
-# reduce 190 omitted
+# reduce 214 omitted
-# reduce 191 omitted
+# reduce 215 omitted
-# reduce 192 omitted
+# reduce 216 omitted
-# reduce 193 omitted
+# reduce 217 omitted
-# reduce 194 omitted
+# reduce 218 omitted
-# reduce 195 omitted
+# reduce 219 omitted
-# reduce 196 omitted
+# reduce 220 omitted
-# reduce 197 omitted
+# reduce 221 omitted
-# reduce 198 omitted
+# reduce 222 omitted
-# reduce 199 omitted
+# reduce 223 omitted
-# reduce 200 omitted
+# reduce 224 omitted
-# reduce 201 omitted
+# reduce 225 omitted
-# reduce 202 omitted
+# reduce 226 omitted
-module_eval(<<'.,.,', 'egrammar.ra', 683)
- def _reduce_203(val, _values, result)
+module_eval(<<'.,.,', 'egrammar.ra', 732)
+ def _reduce_227(val, _values, result)
result = nil
result
end
.,.,
def _reduce_none(val, _values, result)
val[0]
end
end # class Parser
end # module Parser
end # module Pops
end # module Puppet
diff --git a/lib/puppet/pops/parser/epp_parser.rb b/lib/puppet/pops/parser/epp_parser.rb
new file mode 100644
index 000000000..ea5415a56
--- /dev/null
+++ b/lib/puppet/pops/parser/epp_parser.rb
@@ -0,0 +1,51 @@
+# The EppParser is a specialized Puppet Parser that starts parsing in Epp Text mode
+class Puppet::Pops::Parser::EppParser < Puppet::Pops::Parser::Parser
+
+ # Initializes the epp parser support by creating a new instance of {Puppet::Pops::Parser::Lexer}
+ # configured to start in Epp Lexing mode.
+ # @return [void]
+ #
+ def initvars
+ self.lexer = Puppet::Pops::Parser::Lexer2.new()# {:mode => :epp})
+ end
+
+ # Parses a file expected to contain epp text/DSL logic.
+ def parse_file(file)
+ unless FileTest.exist?(file)
+ unless file =~ /\.epp$/
+ file = file + ".epp"
+ end
+ end
+ @lexer.file = file
+ _parse()
+ end
+
+ # Performs the parsing and returns the resulting model.
+ # The lexer holds state, and this is setup with {#parse_string}, or {#parse_file}.
+ #
+ # TODO: deal with options containing origin (i.e. parsing a string from externally known location).
+ # TODO: should return the model, not a Hostclass
+ #
+ # @api private
+ #
+ def _parse()
+ begin
+ @yydebug = false
+ main = yyparse(@lexer,:scan_epp)
+ # #Commented out now because this hides problems in the racc grammar while developing
+ # # TODO include this when test coverage is good enough.
+ # rescue Puppet::ParseError => except
+ # except.line ||= @lexer.line
+ # except.file ||= @lexer.file
+ # except.pos ||= @lexer.pos
+ # raise except
+ # rescue => except
+ # raise Puppet::ParseError.new(except.message, @lexer.file, @lexer.line, @lexer.pos, except)
+ end
+ return main
+ ensure
+ @lexer.clear
+ @namestack = []
+ @definitions = []
+ end
+end
diff --git a/lib/puppet/pops/parser/epp_support.rb b/lib/puppet/pops/parser/epp_support.rb
new file mode 100644
index 000000000..9329ec0b8
--- /dev/null
+++ b/lib/puppet/pops/parser/epp_support.rb
@@ -0,0 +1,247 @@
+# This module is an integral part of the Lexer.
+# It handles scanning of EPP (Embedded Puppet), a form of string/expression interpolation similar to ERB.
+#
+require 'strscan'
+module Puppet::Pops::Parser::EppSupport
+
+ TOKEN_RENDER_STRING = [:RENDER_STRING, nil, 0]
+ TOKEN_RENDER_EXPR = [:RENDER_EXPR, nil, 0]
+
+ # Scans all of the content and returns it in an array
+ # Note that the terminating [false, false] token is included in the result.
+ #
+ def fullscan_epp
+ result = []
+ scan_epp {|token, value| result.push([token, value]) }
+ result
+ end
+
+ # A block must be passed to scan. It will be called with two arguments, a symbol for the token,
+ # and an instance of LexerSupport::TokenValue
+ # PERFORMANCE NOTE: The TokenValue is designed to reduce the amount of garbage / temporary data
+ # and to only convert the lexer's internal tokens on demand. It is slightly more costly to create an
+ # instance of a class defined in Ruby than an Array or Hash, but the gain is much bigger since transformation
+ # logic is avoided for many of its members (most are never used (e.g. line/pos information which is only of
+ # value in general for error messages, and for some expressions (which the lexer does not know about).
+ #
+ def scan_epp
+ # PERFORMANCE note: it is faster to access local variables than instance variables.
+ # This makes a small but notable difference since instance member access is avoided for
+ # every token in the lexed content.
+ #
+ scn = @scanner
+ ctx = @lexing_context
+ queue = @token_queue
+
+ lex_error "Internal Error: No string or file given to lexer to process." unless scn
+
+ ctx[:epp_mode] = :text
+ enqueue_completed([:EPP_START, nil, 0], 0)
+
+ interpolate_epp
+
+ # This is the lexer's main loop
+ until queue.empty? && scn.eos? do
+ if token = queue.shift || lex_token
+ yield [ ctx[:after] = token[0], token[1] ]
+ end
+ end
+ if ctx[:epp_open_position]
+ lex_error("Unbalanced epp tag, reached <eof> without closing tag.", ctx[:epp_position])
+ end
+
+ # Signals end of input
+ yield [false, false]
+ end
+
+ def interpolate_epp(skip_leading=false)
+ scn = @scanner
+ ctx = @lexing_context
+ eppscanner = EppScanner.new(scn)
+ before = scn.pos
+
+ s = eppscanner.scan(skip_leading)
+
+ case eppscanner.mode
+ when :text
+ # Should be at end of scan, or something is terribly wrong
+ lex_error("Internal error: template scanner returns text mode and is not and end of input") unless @scanner.eos?
+ if s
+ # s may be nil if scanned text ends with an epp tag (i.e. no trailing text).
+ enqueue_completed([:RENDER_STRING, s, scn.pos - before], before)
+ end
+ ctx[:epp_open_position] = nil
+ # do nothing else, scanner is at the end
+
+ when :error
+ lex_error(eppscanner.message())
+
+ when :epp
+ # It is meaningless to render empty string segments, and it is harmful to do this at
+ # the start of the scan as it prevents specification of parameters with <%- ($x, $y) -%>
+ #
+ if s && s.length > 0
+ enqueue_completed([:RENDER_STRING, s, scn.pos - before], before)
+ end
+ # switch epp_mode to general (embedded) pp logic (non rendered result)
+ ctx[:epp_mode] = :epp
+ ctx[:epp_open_position] = scn.pos
+
+ when :expr
+ # It is meaningless to render an empty string segment
+ if s && s.length > 0
+ enqueue_completed([:RENDER_STRING, s, scn.pos - before], before)
+ end
+ enqueue_completed(TOKEN_RENDER_EXPR, before)
+ # switch mode to "epp expr interpolation"
+ ctx[:epp_mode] = :expr
+ ctx[:epp_open_position] = scn.pos
+ else
+ lex_error("Internal Error, Unknown mode #{eppscanner.mode} returned by template scanner")
+ end
+ nil
+ end
+
+ # A scanner specialized in processing text with embedded EPP (Embedded Puppet) tags.
+ # The scanner is initialized with a StringScanner which it mutates as scanning takes place.
+ # The intent is to use one instance of EppScanner per wanted scan, and this instance represents
+ # the state after the scan.
+ #
+ # @example Sample usage
+ # a = "some text <% pp code %> some more text"
+ # scan = StringScanner.new(a)
+ # eppscan = EppScanner.new(scan)
+ # str = eppscan.scan
+ # eppscan.mode # => :epp
+ # eppscan.lines # => 0
+ # eppscan
+ #
+ # The scanner supports
+ # * scanning text until <%, <%-, <%=
+ # * while scanning text:
+ # * tokens <%% and %%> are translated to <% and %> respetively and is returned as text.
+ # * tokens <%# and %> (or ending with -%>) and the enclosed text is a comment and is not included in the returned text
+ # * text following a comment that ends with -%> gets trailing whitespace (up to and including a line break) trimmed
+ # and this whitespace is not included in the returned text.
+ # * The continuation {#mode} is set to one of:
+ # * `:epp` - for a <% token
+ # * `:expr` - for a <%= token
+ # * `:text` - when there was no continuation mode (e.g. when input ends with text)
+ # * ':error` - if the tokens are unbalanced (reaching the end without a closing matching token). An error message
+ # is then also available via the method {#message}.
+ #
+ # Note that the intent is to use this specialized scanner to scan the text parts, when continuation mode is `:epp` or `:expr`
+ # the pp lexer should advance scanning (using the string scanner) until it reaches and consumes a `-%>` or '%>´ token. If it
+ # finds a `-%> token it should pass this on as a `skip_leading` parameter when it performs the next {#scan}.
+ #
+ class EppScanner
+ # The original scanner used by the lexer/container using EppScanner
+ attr_reader :scanner
+
+ # The resulting mode after the scan.
+ # The mode is one of `:text` (the initial mode), `:epp` embedded code (no output), `:expr` (embedded
+ # expression), or `:error`
+ #
+ attr_reader :mode
+
+ # An error message if `mode == :error`, `nil` otherwise.
+ attr_reader :message
+
+ # If the first scan should skip leading whitespace (typically detected by the pp lexer when the
+ # pp mode end-token is found (i.e. `-%>`) and then passed on to the scanner.
+ #
+ attr_reader :skip_leading
+
+ # Creates an EppScanner based on a StringScanner that represents the state where EppScanner should start scanning.
+ # The given scanner will be mutated (i.e. position moved) to reflect the EppScanner's end state after a scan.
+ #
+ def initialize(scanner)
+ @scanner = scanner
+ end
+
+ # Scans from the current position in the configured scanner, advances this scanner's position until the end
+ # of the input, or to the first position after a mode switching token (`<%`, `<%-` or `<%=`). Number of processed
+ # lines and continuation mode can be obtained via {#lines}, and {#mode}.
+ #
+ # @return [String, nil] the scanned and processed text, or nil if at the end of the input.
+ #
+ def scan(skip_leading=false)
+ @mode = :text
+ @skip_leading = skip_leading
+
+ return nil if scanner.eos?
+ s = ""
+ until scanner.eos?
+ part = @scanner.scan_until(/(<%)|\z/)
+ if @skip_leading
+ part.gsub!(/^[ \t]*\r?\n?/,'')
+ @skip_leading = false
+ end
+ # The spec for %%> is to transform it into a literal %>. This is done here, as %%> otherwise would go
+ # undetected in text mode. (i.e. it is not really necessary to escape %> with %%> in text mode unless
+ # adding checks stating that a literal %> is illegal in text (unbalanced).
+ #
+ part.gsub!(/%%>/, '%>')
+ s += part
+ case @scanner.peek(1)
+ when ""
+ # at the end
+ # if s ends with <% then this is an error (unbalanced <% %>)
+ if s.end_with? "<%"
+ @mode = :error
+ @message = "Unbalanced embedded expression - opening <% and reaching end of input"
+ else
+ mode = :epp
+ end
+ return s
+
+ when "-"
+ # trim trailing whitespace on same line from accumulated s
+ # return text and signal switch to pp mode
+ @scanner.getch # drop the -
+ s.gsub!(/\r?\n?[ \t]*<%\z/, '')
+ @mode = :epp
+ return s
+
+ when "%"
+ # verbatim text
+ # keep the scanned <%, and continue scanning after skipping one %
+ # (i.e. do nothing here)
+ @scanner.getch # drop the % to get a literal <% in the output
+
+ when "="
+ # expression
+ # return text and signal switch to expression mode
+ # drop the scanned <%, and skip past -%>, or %>, but also skip %%>
+ @scanner.getch # drop the =
+ s.slice!(-2..-1)
+ @mode = :expr
+ return s
+
+ when "#"
+ # template comment
+ # drop the scanned <%, and skip past -%>, or %>, but also skip %%>
+ s.slice!(-2..-1)
+
+ # unless there is an immediate termination i.e. <%#%> scan for the next %> that is not
+ # preceded by a % (i.e. skip %%>)
+ part = scanner.scan_until(/[^%]%>/)
+ unless part
+ @message = "Reaching end after opening <%# without seeing %>"
+ @mode = :error
+ return s
+ end
+ @skip_leading = true if part.end_with?("-%>")
+ # Continue scanning for more text
+
+ else
+ # Switch to pp after having removed the <%
+ s.slice!(-2..-1)
+ @mode = :epp
+ return s
+ end
+ end
+ end
+ end
+
+end
diff --git a/lib/puppet/pops/parser/evaluating_parser.rb b/lib/puppet/pops/parser/evaluating_parser.rb
index 3a6cdbb66..22fe53720 100644
--- a/lib/puppet/pops/parser/evaluating_parser.rb
+++ b/lib/puppet/pops/parser/evaluating_parser.rb
@@ -1,162 +1,200 @@
# Does not support "import" and parsing ruby files
#
class Puppet::Pops::Parser::EvaluatingParser
+ attr_reader :parser
+
def initialize()
@parser = Puppet::Pops::Parser::Parser.new()
end
def parse_string(s, file_source = 'unknown')
@file_source = file_source
clear()
# Handling of syntax error can be much improved (in general), now it bails out of the parser
# and does not have as rich information (when parsing a string), need to update it with the file source
# (ideally, a syntax error should be entered as an issue, and not just thrown - but that is a general problem
# and an improvement that can be made in the eparser (rather than here).
# Also a possible improvement (if the YAML parser returns positions) is to provide correct output of position.
#
begin
- assert_and_report(@parser.parse_string(s))
+ assert_and_report(parser.parse_string(s))
rescue Puppet::ParseError => e
- e.file = @file_source unless e.file
+ # TODO: This is not quite right, why does not the exception have the correct file?
+ e.file = @file_source unless e.file.is_a?(String) && !e.file.empty?
raise e
end
end
def parse_file(file)
@file_source = file
clear()
- assert_and_report(@parser.parse_file(file))
+ assert_and_report(parser.parse_file(file))
end
def evaluate_string(scope, s, file_source='unknown')
evaluate(scope, parse_string(s, file_source))
end
def evaluate_file(file)
evaluate(parse_file(file))
end
def clear()
@acceptor = nil
end
def evaluate(scope, model)
return nil unless model
ast = Puppet::Pops::Model::AstTransformer.new(@file_source, nil).transform(model)
return nil unless ast
ast.safeevaluate(scope)
end
+ def validate(parse_result)
+ resulting_acceptor = acceptor()
+ validator(resulting_acceptor).validate(parse_result)
+ resulting_acceptor
+ end
+
def acceptor()
- @acceptor ||= Puppet::Pops::Validation::Acceptor.new
- @acceptor
+ Puppet::Pops::Validation::Acceptor.new
end
- def validator()
- @validator ||= Puppet::Pops::Validation::ValidatorFactory_3_1.new().validator(acceptor)
+ def validator(acceptor)
+ Puppet::Pops::Validation::ValidatorFactory_3_1.new().validator(acceptor)
end
def assert_and_report(parse_result)
return nil unless parse_result
- # make sure the result has an origin (if parsed from a string)
- unless Puppet::Pops::Adapters::OriginAdapter.get(parse_result.model)
- Puppet::Pops::Adapters::OriginAdapter.adapt(parse_result.model).origin = @file_source
+ if parse_result.source_ref.nil? or parse_result.source_ref == ''
+ parse_result.source_ref = @file_source
end
- validator.validate(parse_result)
+ validation_result = validate(parse_result)
max_errors = Puppet[:max_errors]
max_warnings = Puppet[:max_warnings] + 1
max_deprecations = Puppet[:max_deprecations] + 1
# If there are warnings output them
- warnings = acceptor.warnings
+ warnings = validation_result.warnings
if warnings.size > 0
formatter = Puppet::Pops::Validation::DiagnosticFormatterPuppetStyle.new
emitted_w = 0
emitted_dw = 0
- acceptor.warnings.each {|w|
+ validation_result.warnings.each {|w|
if w.severity == :deprecation
# Do *not* call Puppet.deprecation_warning it is for internal deprecation, not
# deprecation of constructs in manifests! (It is not designed for that purpose even if
# used throughout the code base).
#
Puppet.warning(formatter.format(w)) if emitted_dw < max_deprecations
emitted_dw += 1
else
Puppet.warning(formatter.format(w)) if emitted_w < max_warnings
emitted_w += 1
end
break if emitted_w > max_warnings && emitted_dw > max_deprecations # but only then
}
end
# If there were errors, report the first found. Use a puppet style formatter.
- errors = acceptor.errors
+ errors = validation_result.errors
if errors.size > 0
formatter = Puppet::Pops::Validation::DiagnosticFormatterPuppetStyle.new
if errors.size == 1 || max_errors <= 1
# raise immediately
- require 'debugger'; debugger
raise Puppet::ParseError.new(formatter.format(errors[0]))
end
emitted = 0
errors.each do |e|
Puppet.err(formatter.format(e))
emitted += 1
break if emitted >= max_errors
end
warnings_message = warnings.size > 0 ? ", and #{warnings.size} warnings" : ""
giving_up_message = "Found #{errors.size} errors#{warnings_message}. Giving up"
exception = Puppet::ParseError.new(giving_up_message)
exception.file = errors[0].file
raise exception
end
parse_result
end
def quote(x)
self.class.quote(x)
end
# Translates an already parsed string that contains control characters, quotes
# and backslashes into a quoted string where all such constructs have been escaped.
# Parsing the return value of this method using the puppet parser should yield
# exactly the same string as the argument passed to this method
#
# The method makes an exception for the two character sequences \$ and \s. They
# will not be escaped since they have a special meaning in puppet syntax.
#
+ # TODO: Handle \uXXXX characters ??
+ #
# @param x [String] The string to quote and "unparse"
# @return [String] The quoted string
#
def self.quote(x)
escaped = '"'
p = nil
x.each_char do |c|
case p
when nil
# do nothing
when "\t"
escaped << '\\t'
when "\n"
escaped << '\\n'
when "\f"
escaped << '\\f'
# TODO: \cx is a range of characters - skip for now
# when "\c"
# escaped << '\\c'
when '"'
escaped << '\\"'
when '\\'
escaped << if c == '$' || c == 's'; p; else '\\\\'; end # don't escape \ when followed by s or $
else
escaped << p
end
p = c
end
escaped << p unless p.nil?
escaped << '"'
end
+
+ # This is a temporary solution to making it possible to use the new evaluator. The main class
+ # will eventually have this behavior instead of using transformation to Puppet 3.x AST
+ class Transitional < Puppet::Pops::Parser::EvaluatingParser
+
+ def evaluator
+ @@evaluator ||= Puppet::Pops::Evaluator::EvaluatorImpl.new()
+ @@evaluator
+ end
+
+ def evaluate(scope, model)
+ return nil unless model
+ evaluator.evaluate(model, scope)
+ end
+
+ def validator(acceptor)
+ Puppet::Pops::Validation::ValidatorFactory_4_0.new().validator(acceptor)
+ end
+
+ # Create a closure that can be called in the given scope
+ def closure(model, scope)
+ Puppet::Pops::Evaluator::Closure.new(evaluator, model, scope)
+ end
+ end
+
+ class EvaluatingEppParser < Transitional
+ def initialize()
+ @parser = Puppet::Pops::Parser::EppParser.new()
+ end
+ end
end
diff --git a/lib/puppet/pops/parser/grammar.ra b/lib/puppet/pops/parser/grammar.ra
deleted file mode 100644
index 7352bdbfb..000000000
--- a/lib/puppet/pops/parser/grammar.ra
+++ /dev/null
@@ -1,746 +0,0 @@
-# vim: syntax=ruby
-
-# Parser using the Pops model
-# This grammar is a half step between the current 3.1. grammar and egrammar.
-# FIXME! Keep as reference until egrammar is proven to work.
-
-class Puppet::Pops::Impl::Parser::Parser
-
-token STRING DQPRE DQMID DQPOST
-token LBRACK RBRACK LBRACE RBRACE SYMBOL FARROW COMMA TRUE
-token FALSE EQUALS APPENDS LESSEQUAL NOTEQUAL DOT COLON LLCOLLECT RRCOLLECT
-token QMARK LPAREN RPAREN ISEQUAL GREATEREQUAL GREATERTHAN LESSTHAN
-token IF ELSE IMPORT DEFINE ELSIF VARIABLE CLASS INHERITS NODE BOOLEAN
-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
-
-prechigh
- left DOT
-# left LBRACE
-# left LCOLLECT LLCOLLECT
- right NOT
- nonassoc UMINUS
- left IN MATCH NOMATCH
- left TIMES DIV
- left MINUS PLUS
- left LSHIFT RSHIFT
- left NOTEQUAL ISEQUAL
- left GREATEREQUAL GREATERTHAN LESSTHAN LESSEQUAL
- left AND
- left OR
-# left IN_EDGE OUT_EDGE IN_EDGE_SUB OUT_EDGE_SUB
-preclow
-
-rule
-# Produces [Model::BlockExpression, Model::Expression, nil] depending on multiple statements, single statement or empty
-program
- : statements { result = Factory.block_or_expression(*val[0]) }
- | nil
-
-# Change may have issues with nil; i.e. program is a sequence of nils/nops
-# Simplified from original which had validation for top level constructs - see statement rule
-# Produces Array<Model::Expression>
-statements
- : statement { result = [val[0]]}
- | statements statement { result = val[0].push val[1] }
-
-# Removed validation construct regarding "top level statements" as it did not seem to catch all problems
-# and relied on a "top-level-ness" encoded in the abstract syntax tree objects
-#
-# The main list of valid statements
-# Produces Model::Expression
-#
-statement
- : resource
- | virtual_resource
- | collection
- | assignment
- | casestatement
- | if_expression
- | unless_expression
- | import
- | call_named_function
- | definition
- | hostclass
- | nodedef
- | resource_override
- | append
- | relationship
- | call_method_with_lambda
-
-keyword
- : AND
- | CASE
- | CLASS
- | DEFAULT
- | DEFINE
- | ELSE
- | ELSIF
- | IF
- | IN
- | IMPORT
- | INHERITS
- | NODE
- | OR
- | UNDEF
- | UNLESS
-
-# Produces Model::RelationshipExpression
-relationship
- : relationship_side edge relationship_side { result = val[0].relop(val[1][:value], val[2]); loc result, val[1] }
- | relationship edge relationship_side { result = val[0].relop(val[1][:value], val[2]); loc result, val[1] }
-
-# Produces Model::Expression
-relationship_side
- : resource
- | resourceref
- | collection
- | variable
- | quotedtext
- | selector
- | casestatement
- | hasharrayaccesses
-
-# Produces String
-edge
- : IN_EDGE
- | OUT_EDGE
- | IN_EDGE_SUB
- | OUT_EDGE_SUB
-
-# Produces Model::CallNamedFunctionExpression
-call_named_function
- : NAME LPAREN expressions RPAREN { result = Factory.CALL_NAMED(val[0][:value], false, val[2]) ; loc result, val[0], val[3] }
- | NAME LPAREN expressions COMMA RPAREN { result = Factory.CALL_NAMED(val[0][:value], false, val[2]) ; loc result, val[0], val[4] }
- | NAME LPAREN RPAREN { result = Factory.CALL_NAMED(val[0][:value], false, []) ; loc result, val[0], val[2] }
- | NAME func_call_args { result = Factory.CALL_NAMED(val[0][:value], false, val[1]) ; loc result, val[0] }
-
-call_method_with_lambda
- : call_method { result = val[0] }
- | call_method lambda { result = val[0]; val[0].lambda = val[1] }
-
-call_method
- : named_access LPAREN expressions RPAREN { result = Factory.CALL_METHOD(val[0], val[2]); loc result, val[1], val[3] }
- | named_access LPAREN RPAREN { result = Factory.CALL_METHOD(val[0], []); loc result, val[1], val[3] }
- | named_access { result = Factory.CALL_METHOD(val[0], []); loc result, val[0] }
-
-named_access
- : named_access_lval DOT NAME {
- result = val[0].dot(Factory.fqn(val[2][:value]))
- loc result, val[1], val[2]
- }
-
-# Obviously not ideal, it is not possible to use literal array or hash as lhs
-# These must be assigned to a variable - this is also an issue in other places
-#
-named_access_lval
- : variable
- | hasharrayaccesses
- | selector
- | quotedtext
- | call_named_rval_function
-
-lambda
- : 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 = [] }
- | PIPE parameters endcomma PIPE { result = val[1] }
-
-# Produces Array<Model::Expression>
-func_call_args
- : rvalue { result = [val[0]] }
- | func_call_args COMMA rvalue { result = val[0].push(val[2]) }
-
-# Produces Array<Model::Expression>
-expressions
- : expression { result = [val[0]] }
- | expressions comma expression { result = val[0].push(val[2]) }
-
-
-# Produces [Model::ResourceExpression, Model::ResourceDefaultsExpression]
-resource
- : classname LBRACE resourceinstances endsemi RBRACE {
- result = Factory.RESOURCE(Factory.fqn(token_text(val[0])), val[2])
- loc result, val[0], val[4]
- }
- | classname LBRACE attribute_operations endcomma RBRACE {
- # This is a deprecated syntax.
- # It also fails hard - TODO: create model and validate this case
- error "All resource specifications require names"
- }
- | type LBRACE attribute_operations endcomma RBRACE {
- # a defaults setting for a type
- result = Factory.RESOURCE_DEFAULTS(val[0], val[2])
- loc result, val[0], val[4]
- }
-
-# Override a value set elsewhere in the configuration.
-# Produces Model::ResourceOverrideExpression
-resource_override
- : resourceref LBRACE attribute_operations endcomma RBRACE {
- @lexer.commentpop
- result = Factory.RESOURCE_OVERRIDE(val[0], val[2])
- loc result, val[0], val[4]
- }
-
-# Exported and virtual resources; these don't get sent to the client
-# unless they get collected elsewhere in the db.
-# The original had validation here; checking if storeconfigs is on; this is moved to a validation step
-# Also, validation was performed if an attempt was made to virtualize or export a resource defaults
-# this is also now deferred to validation
-# Produces [Model::ResourceExpression, Model::ResourceDefaultsExpression]
-virtual_resource
- : at resource {
- val[1].form = val[0] # :virtual, :exported, (or :regular)
- result = val[1]
- }
-
-# Produces Symbol corresponding to resource form
-at
- : AT { result = :virtual }
- | AT AT { result = :exported }
-
-# A collection statement. Currently supports no arguments at all, but eventually
-# will, I assume.
-#
-# Produces Model::CollectExpression
-#
-collection
- : type collect_query LBRACE attribute_operations endcomma RBRACE {
- @lexer.commentpop
- result = Factory.COLLECT(val[0].value.downcase, val[1], val[3])
- loc result, val[0], val[5]
- }
- | type collect_query {
- result = Factory.COLLECT(val[0].value.downcase, val[1], [])
- loc result, val[0], val[1]
- }
-
-collect_query
- : LCOLLECT optional_query RCOLLECT { result = Factory.VIRTUAL_QUERY(val[1]) ; loc result, val[0], val[2] }
- | LLCOLLECT optional_query RRCOLLECT { result = Factory.EXPORTED_QUERY(val[1]) ; loc result, val[0], val[2] }
-
-# ORIGINAL COMMENT: A mini-language for handling collection comparisons. This is organized
-# to avoid the need for precedence indications.
-# (New implementation is slightly different; and when finished, it may be possible to streamline the
-# grammar - the difference is mostly in evaluation, not in grammar)
-#
-optional_query
- : nil
- | query
-
-# ORIGINAL: Had a weird list structure where AND and OR where at the same level, and hence, there was the
-# need to keep track of where parenthesis were (to get order correct).
-#
-# This is now not needed as AND has higher precedence than OR, and parenthesis are low in precedence
-
-query
- : predicate_lval ISEQUAL expression { result = (val[0] == val[2]) ; loc result, val[1] }
- | predicate_lval NOTEQUAL expression { result = (val[0].ne(val[2])) ; loc result, val[1] }
- | LPAREN query RPAREN { result = val[1] }
- | query AND query { result = val[0].and(val[2]) ; loc result, val[1] }
- | query OR query { result = val[0].or(val[2]) ; loc result, val[1] }
-
-
-# Produces Model::VariableExpression, or Model::QualifiedName
-predicate_lval
- : variable
- | name
-
-resourceinst
- : resourcename COLON attribute_operations endcomma { result = Factory.RESOURCE_BODY(val[0], val[2]) }
-
-resourceinstances
- : resourceinst { result = [val[0]] }
- | resourceinstances SEMIC resourceinst { result = val[0].push val[2] }
-
-
-resourcename
- : quotedtext
- | name
- | type
- | selector
- | variable
- | array
- | hasharrayaccesses
-
-# Assignment, only assignment to variable is legal, but parser builds expression for [] = anyway to
-# enable a better error message
-assignment
- : VARIABLE EQUALS expression { result = Factory.var(Factory.fqn(val[0][:value])).set(val[2]) ; loc result, val[1] }
- | hasharrayaccess EQUALS expression { result val[0].set(val[2]); loc result, val[1] }
-
-append
- : VARIABLE APPENDS expression { result = Factory.var(val[0][:value]).plus_set(val[1]) ; loc result, val[1] }
-
-# Produces Array<Model::AttributeOperation>
-attribute_operations
- : { result = [] }
- | attribute_operation { result = [val[0]] }
- | attribute_operations COMMA attribute_operation { result = val[0].push(val[2]) }
-
-# Produces String
-attribute_name
- : NAME
- | keyword
- | BOOLEAN
-
-# Several grammar issues here: the addparam did not allow keyword and booleans as names.
-# In this version, the wrong combinations are validated instead of producing syntax errors
-# (Can give nicer error message +> is not applicable to...)
-# WAT - Boolean as attribute name?
-# Produces Model::AttributeOperation
-#
-attribute_operation
- : attribute_name FARROW expression {
- result = Factory.ATTRIBUTE_OP(val[0][:value], :'=>', val[2])
- loc result, val[0], val[2]
- }
- | attribute_name PARROW expression {
- result = Factory.ATTRIBUTE_OP(val[0][:value], :'+>', val[2])
- loc result, val[0], val[2]
- }
-
-# Produces Model::CallNamedFunction
-call_named_rval_function
- : NAME LPAREN expressions RPAREN { result = Factory.CALL_NAMED(val[0][:value], true, val[2]) ; loc result, val[0], val[3] }
- | NAME LPAREN RPAREN { result = Factory.CALL_NAMED(val[0][:value], true, []) ; loc result, val[0], val[2] }
-
-quotedtext
- : STRING { result = Factory.literal(val[0][:value]) ; loc result, val[0] }
- | dqpre dqrval { result = Factory.string(val[0], *val[1]) ; loc result, val[0], val[1][-1] }
-
-dqpre : DQPRE { result = Factory.literal(val[0][:value]); loc result, val[0] }
-dqpost : DQPOST { result = Factory.literal(val[0][:value]); loc result, val[0] }
-dqmid : DQMID { result = Factory.literal(val[0][:value]); loc result, val[0] }
-text_expression : expression { result = Factory.TEXT(val[0]) }
-
-dqrval
- : text_expression dqtail { result = [val[0]] + val[1] }
-
-dqtail
- : dqpost { result = [val[0]] }
- | dqmid dqrval { result = [val[0]] + val[1] }
-
-
-# Reference to Resource (future also reference to other instances of other types than Resources).
-# First form (lower case name) is deprecated (deprecation message handled in validation). Note that
-# this requires use of token NAME since a rule call to name causes shift reduce conflict with
-# a function call NAME NAME (calling function with NAME as argument e.g. foo bar).
-#
-# Produces InstanceReference
-resourceref
- : NAME LBRACK expressions RBRACK {
- # Would want to use rule name here, but can't (need a NAME with higher precedence), so must
- # create a QualifiedName instance here for NAME
- result = Factory.INSTANCE(Factory.QNAME_OR_NUMBER(val[0][:value]), val[2]);
- loc result, val[0], val[2][-1]
- }
- | type LBRACK expressions RBRACK {
- result = Factory.INSTANCE(val[0], val[2]);
- loc result, val[0], val[2][-1]
- }
-
-# Changed from Puppet 3x where there is no else part on unless
-#
-unless_expression
- : UNLESS expression LBRACE statements RBRACE unless_else {
- @lexer.commentpop
- result = Factory.UNLESS(val[1], Factory.block_or_expression(*val[3]), val[5])
- loc result, val[0], val[4]
- }
- | UNLESS expression LBRACE RBRACE unless_else {
- @lexer.commentpop
- result = Factory.UNLESS(val[1], nil, nil)
- loc result, val[0], val[4]
- }
-
-# Different from else part of if, since "elsif" is not supported, but else is
-#
-# Produces [Model::Expression, nil] - nil if there is no else or elsif part
-unless_else
- : # nothing
- | ELSE LBRACE statements RBRACE {
- @lexer.commentpop
- result = Factory.block_or_expression(*val[2])
- loc result, val[0], val[3]
- }
- | ELSE LBRACE RBRACE {
- @lexer.commentpop
- result = nil # don't think a nop is needed here either
- }
-
-# Produces Model::IfExpression
-if_expression
- : IF if_expression_part {
- result = val[1]
- }
-
-# Produces Model::IfExpression
-if_expression_part
- : expression LBRACE statements RBRACE else {
- @lexer.commentpop
- result = Factory.IF(val[0], Factory.block_or_expression(*val[2]), val[4])
- loc(result, val[0], (val[4] ? val[4] : val[3]))
- }
- | expression LBRACE RBRACE else {
- result = Factory.IF(val[0], nil, val[3])
- loc(result, val[0], (val[3] ? val[3] : val[2]))
- }
-
-# Produces [Model::Expression, nil] - nil if there is no else or elsif part
-else
- : # nothing
- | ELSIF if_expression_part { result = val[1] }
- | ELSE LBRACE statements RBRACE {
- @lexer.commentpop
- result = Factory.block_or_expression(*val[2])
- loc result, val[0], val[3]
- }
- | ELSE LBRACE RBRACE {
- @lexer.commentpop
- result = nil # don't think a nop is needed here either
- }
-
-# Produces Model::Expression
-expression
- : rvalue
- | hash
- | expression IN expression { result = val[0].in val[2] ; loc result, val[1] }
- | expression MATCH match_rvalue { result = val[0] =~ val[2] ; loc result, val[1] }
- | expression NOMATCH match_rvalue { result = val[0].mne val[2] ; loc result, val[1] }
- | expression PLUS expression { result = val[0] + val[2] ; loc result, val[1] }
- | expression MINUS expression { result = val[0] - val[2] ; loc result, val[1] }
- | expression DIV expression { result = val[0] / val[2] ; loc result, val[1] }
- | expression TIMES expression { result = val[0] * val[2] ; loc result, val[1] }
- | expression LSHIFT expression { result = val[0] << val[2] ; loc result, val[1] }
- | expression RSHIFT expression { result = val[0] >> val[2] ; loc result, val[1] }
- | MINUS expression =UMINUS { result = val[1].minus() ; loc result, val[0] }
- | expression NOTEQUAL expression { result = val[0].ne val[2] ; loc result, val[1] }
- | expression ISEQUAL expression { result = val[0] == val[2] ; loc result, val[1] }
- | expression GREATERTHAN expression { result = val[0] > val[2] ; loc result, val[1] }
- | expression GREATEREQUAL expression { result = val[0] >= val[2] ; loc result, val[1] }
- | expression LESSTHAN expression { result = val[0] < val[2] ; loc result, val[1] }
- | expression LESSEQUAL expression { result = val[0] <= val[2] ; loc result, val[1] }
- | NOT expression { result = val[1].not ; loc result, val[0] }
- | expression AND expression { result = val[0].and val[2] ; loc result, val[1] }
- | expression OR expression { result = val[0].or val[2] ; loc result, val[1] }
- | LPAREN expression RPAREN { result = val[1] ; }
- | call_method_with_lambda
-
-match_rvalue
- : regex
- | quotedtext
-
-# Produces Model::CaseExpression
-casestatement
- : CASE expression LBRACE case_options RBRACE {
- @lexer.commentpop
- result = Factory.CASE(val[1], *val[3])
- loc result, val[0], val[4]
- }
-
-# Produces Array<Model::CaseOption>
-case_options
- : case_option { result = [val[0]] }
- | case_options case_option { result = val[0].push val[1] }
-
-# Produced Model::CaseOption (aka When)
-case_option
- : case_values COLON LBRACE statements RBRACE {
- @lexer.commentpop
- result = Factory.WHEN(val[0], val[3])
- loc result, val[1], val[4]
- }
- | case_values COLON LBRACE RBRACE {
- @lexer.commentpop
- result = Factory.WHEN(val[0], nil)
- loc result, val[1], val[3]
- }
-
-# Produces Array<Expression> mostly literals
-case_values
- : selectable { result = [val[0]] }
- | case_values COMMA selectable { result = val[0].push val[2] }
-
-# Produces Model::SelectorExpression
-selector
- : selectable QMARK selector_entries { result = val[0].select(*val[2]) ; loc result, val[1] }
-
-# Produces Array<Model::SelectorEntry>
-selector_entries
- : selector_entry { result = [val[0]] }
- | LBRACE selector_entry_list endcomma RBRACE {
- @lexer.commentpop
- result = val[1]
- }
-
-# Produces Array<Model::SelectorEntry>
-selector_entry_list
- : selector_entry { result = [val[0]] }
- | selector_entry_list COMMA selector_entry { result = val[0].push val[2] }
-
-# Produces a Model::SelectorEntry
-selector_entry
- : selectable FARROW rvalue { result = Factory.MAP(val[0], val[2]) ; loc result, val[1] }
-
-# Produces Model::Expression (most of the literals)
-selectable
- : name
- | type
- | quotedtext
- | variable
- | call_named_rval_function
- | boolean
- | undef
- | hasharrayaccess
- | default
- | regex
-
-
-
-# Produces nil (noop)
-import
- : IMPORT strings {
- error "Import not supported in this version of the parser", \
- :line => stmt.context[:line], :file => stmt.context[:file]
- result = nil
- }
-
-# IMPORT (T.B DEPRECATED IN PUPPET WHEN IT HAS BEEN FIGURED OUT HOW TO SUPPORT
-# THE THINGS IMPORTS ARE USED FOR.
-# BOLDLY DECIDED TO SKIP THIS COMPLETELY IN THIS IMPLEMENTATION - will trigger an error
-#
-# These are only used for importing, no interpolation
-string
- : STRING { result = [val[0][:value]] }
-
-strings
- : string
- | strings COMMA string { result = val[0].push val[2] }
-
-# Produces Model::Definition
-definition
- : DEFINE classname parameter_list LBRACE statements RBRACE {
- @lexer.commentpop
- result = Factory.DEFINITION(classname(val[1][:value]), val[2], val[4])
- loc result, val[0], val[5]
- @lexer.indefine = false
- }
- | DEFINE classname parameter_list LBRACE RBRACE {
- @lexer.commentpop
- result = Factory.DEFINITION(classname(val[1][:value]), val[2], nil)
- loc result, val[0], val[4]
- @lexer.indefine = false
- }
-
-# ORIGINAL COMMENT: Our class gets defined in the parent namespace, not our own.
-# WAT ??! This is way odd; should get its complete name, classnames do not nest
-# Seems like the call to classname makes use of the name scope
-# (This is uneccesary, since the parent name is known when evaluating)
-#
-# Produces Model::HostClassDefinition
-#
-hostclass
- : CLASS classname parameter_list classparent LBRACE statements RBRACE {
- @lexer.commentpop
- @lexer.namepop
- result = Factory.HOSTCLASS(classname(val[1][:value]), val[2], token_text(val[3]), val[5])
- loc result, val[0], val[6]
- }
- | CLASS classname parameter_list classparent LBRACE RBRACE {
- @lexer.commentpop
- @lexer.namepop
- result = Factory.HOSTCLASS(classname(val[1][:value]), val[2], token_text(val[3]), nil)
- loc result, val[0], val[5]
- }
-
-# Produces Model::NodeDefinition
-nodedef
- : NODE hostnames nodeparent LBRACE statements RBRACE {
- @lexer.commentpop
- result = Factory.NODE(val[1], val[2], val[4])
- loc result, val[0], val[5]
- }
- | NODE hostnames nodeparent LBRACE RBRACE {
- @lexer.commentpop
- result = Factory.NODE(val[1], val[2], nil)
- loc result, val[0], val[4]
- }
-
-# String result
-classname
- : NAME { result = val[0] }
- | CLASS { result = val[0] }
-
-# Hostnames is not a list of names, it is a list of name matchers (including a Regexp).
-# (The old implementation had a special "Hostname" object with some minimal validation)
-#
-# Produces Array<Model::LiteralExpression>
-#
-hostnames
- : nodename { result = [result] }
- | hostnames COMMA nodename { result = val[0].push(val[2]) }
-
-# Produces Model::LiteralExpression
-#
-nodename
- : hostname
-
-# Produces a LiteralExpression (string, :default, or regexp)
-hostname
- : NAME { result = Factory.fqn(val[0][:value]); loc result, val[0] }
- | STRING { result = Factory.literal(val[0][:value]); loc result, val[0] }
- | DEFAULT { result = Factory.literal(:default); loc result, val[0] }
- | regex
-
-
-# Produces Array<Model::Parameter>
-parameter_list
- : nil { result = [] }
- | LPAREN RPAREN { result = [] }
- | LPAREN parameters endcomma RPAREN { result = val[1] }
-
-# Produces Array<Model::Parameter>
-parameters
- : parameter { result = [val[0]] }
- | parameters COMMA parameter { result = val[0].push(val[2]) }
-
-# Produces Model::Parameter
-parameter
- : VARIABLE EQUALS expression { result = Factory.PARAM(val[0][:value], val[2]) ; loc result, val[0] }
- | VARIABLE { result = Factory.PARAM(val[0][:value]); loc result, val[0] }
-
-# Produces Expression, since hostname is an Expression
-nodeparent
- : nil
- | INHERITS hostname { result = val[1] }
-
-# Produces String, name or nil result
-classparent
- : nil
- | INHERITS classnameordefault { result = val[1] }
-
-# Produces String (this construct allows a class to be named "default" and to be referenced as
-# the parent class.
-# TODO: Investigate the validity
-# Produces a String (classname), or a token (DEFAULT).
-#
-classnameordefault
- : classname
- | DEFAULT
-
-rvalue
- : quotedtext
- | name
- | type
- | boolean
- | selector
- | variable
- | array
- | hasharrayaccesses
- | resourceref
- | call_named_rval_function
- | undef
-
-array
- : LBRACK expressions RBRACK { result = Factory.LIST(val[1]); loc result, val[0], val[2] }
- | LBRACK expressions COMMA RBRACK { result = Factory.LIST(val[1]); loc result, val[0], val[3] }
- | LBRACK RBRACK { result = Factory.literal([]) ; loc result, val[0] }
-
-
-hash
- : LBRACE hashpairs RBRACE { result = Factory.HASH(val[1]); loc result, val[0], val[2] }
- | LBRACE hashpairs COMMA RBRACE { result = Factory.HASH(val[1]); loc result, val[0], val[3] }
- | LBRACE RBRACE { result = Factory.literal({}) ; loc result, val[0], val[3] }
-
-hashpairs
- : hashpair { result = [val[0]] }
- | hashpairs COMMA hashpair { result = val[0].push val[2] }
-
-hashpair
- : key FARROW expression { result = Factory.KEY_ENTRY(val[0], val[2]); loc result, val[1] }
-
-key
- : NAME { result = Factory.literal(val[0][:value]) ; loc result, val[0] }
- | quotedtext { result = val[0] }
-
-# NOTE: Limitation that LHS is a variable, means that it is not possible to do foo(10)[2] without
-# using an intermediate variable
-#
-hasharrayaccess
- : variable LBRACK expression RBRACK { result = val[0][val[2]]; loc result, val[0], val[3] }
-
-hasharrayaccesses
- : hasharrayaccess
- | hasharrayaccesses LBRACK expression RBRACK { result = val[0][val[2]] ; loc result, val[1], val[3] }
-
-# Produces Model::VariableExpression
-variable : VARIABLE { result = Factory.fqn(val[0][:value]).var ; loc result, val[0] }
-undef : UNDEF { result = Factory.literal(:undef); loc result, val[0] }
-name : NAME { result = Factory.QNAME_OR_NUMBER(val[0][:value]) ; loc result, val[0] }
-type : CLASSREF { result = Factory.QREF(val[0][:value]) ; loc result, val[0] }
-
-default
- : DEFAULT { result = Factory.literal(:default); loc result, val[0] }
-
-boolean
- # Assumes lexer produces a Boolean value for booleans, or this will go wrong (e.g. produce. LiteralString)
- : BOOLEAN { result = Factory.literal(val[0][:value]) ; loc result, val[0] }
-
-regex
- : REGEX { result = Factory.literal(val[0][:value]); loc result, val[0] }
-
-# ---Special markers & syntactic sugar
-
-# WAT !!!! this means array can be [1=>2=>3], func (1=>2=>3), and other retarded constructs
-# TODO: Remove the FARROW (investigate if there is any validity)
-comma
- : FARROW
- | COMMA
-
-endcomma
- : #
- | COMMA { result = nil }
-
-endsemi
- : #
- | SEMIC
-
-nil
- : { result = nil}
-
-## Empty list - not really needed? TODO: Check if this can be removed
-#empty_list
-# : { result = [] }
-
-end
-
----- header ----
-require 'puppet'
-require 'puppet/util/loadedfile'
-require 'puppet/pops'
-
-module Puppet
- class ParseError < Puppet::Error; end
- class ImportError < Racc::ParseError; end
- class AlreadyImportedError < ImportError; end
-end
-
----- inner ----
-
-# Make emacs happy
-# Local Variables:
-# mode: ruby
-# End:
diff --git a/lib/puppet/pops/parser/heredoc_support.rb b/lib/puppet/pops/parser/heredoc_support.rb
new file mode 100644
index 000000000..76897525a
--- /dev/null
+++ b/lib/puppet/pops/parser/heredoc_support.rb
@@ -0,0 +1,139 @@
+module Puppet::Pops::Parser::HeredocSupport
+
+ # Pattern for heredoc `@(endtag[:syntax][/escapes])
+ # Produces groups for endtag (group 1), syntax (group 2), and escapes (group 3)
+ #
+ PATTERN_HEREDOC = %r{@\(([^:/\r\n\)]+)(?::[:blank:]*([a-z][a-zA-Z0-9_+]+)[:blank:]*)?(?:/((?:\w|[$])*)[:blank:]*)?\)}
+
+
+ def heredoc
+ scn = @scanner
+ ctx = @lexing_context
+ locator = @locator
+ before = scn.pos
+
+ # scanner is at position before @(
+ # find end of the heredoc spec
+ str = scn.scan_until(/\)/) || lexer.lex_error("Unclosed parenthesis after '@(' followed by '#{followed_by}'")
+ pos_after_heredoc = scn.pos
+
+ # Note: allows '+' as separator in syntax, but this needs validation as empty segments are not allowed
+ unless md = str.match(PATTERN_HEREDOC)
+ lex_error("Invalid syntax in heredoc expected @(endtag[:syntax][/escapes])")
+ end
+ endtag = md[1]
+ syntax = md[2] || ''
+ escapes = md[3]
+
+ endtag.strip!
+
+ # Is this a dq string style heredoc? (endtag enclosed in "")
+ if endtag =~ /^"(.*)"$/
+ dqstring_style = true
+ endtag = $1.strip
+ end
+
+ lexer.lex_error("Missing endtag in heredoc") unless endtag.length >= 1
+
+ resulting_escapes = []
+ if escapes
+ escapes = "trnsuL$" if escapes.length < 1
+
+ escapes = escapes.split('')
+ unless escapes.length == escapes.uniq.length
+ lex_error("An escape char for @() may only appear once. Got '#{escapes.join(', ')}")
+ end
+ resulting_escapes = ["\\"]
+ escapes.each do |e|
+ case e
+ when "t", "r", "n", "s", "u", "$"
+ resulting_escapes << e
+ when "L"
+ resulting_escapes += ["\n", "\r\n"]
+ else
+ lex_error("Invalid heredoc escape char. Only t, r, n, s, u, L, $ allowed. Got '#{e}'")
+ end
+ end
+ end
+
+ # Produce a heredoc token to make the syntax available to the grammar
+ enqueue_completed([:HEREDOC, syntax, pos_after_heredoc - before], before)
+
+ # If this is the second or subsequent heredoc on the line, the lexing context's :newline_jump contains
+ # the position after the \n where the next heredoc text should scan. If not set, this is the first
+ # and it should start scanning after the first found \n (or if not found == error).
+
+ if ctx[:newline_jump]
+ scn.pos = lexing_context[:newline_jump]
+ else
+ scn.scan_until(/\n/) || lex_error("Heredoc without any following lines of text")
+ end
+ # offset 0 for the heredoc, and its line number
+ heredoc_offset = scn.pos
+ heredoc_line = locator.line_for_offset(heredoc_offset)-1
+
+ # Compute message to emit if there is no end (to make it refer to the opening heredoc position).
+ eof_message = positioned_message("Heredoc without end-tagged line")
+
+ # Text from this position (+ lexing contexts offset for any preceding heredoc) is heredoc until a line
+ # that terminates the heredoc is found.
+
+ # (Endline in EBNF form): WS* ('|' WS*)? ('-' WS*)? endtag WS* \r? (\n|$)
+ endline_pattern = /([[:blank:]]*)(?:([|])[[:blank:]]*)?(?:(\-)[[:blank:]]*)?#{Regexp.escape(endtag)}[[:blank:]]*\r?(?:\n|\z)/
+ lines = []
+ while !scn.eos? do
+ one_line = scn.scan_until(/(?:\n|\z)/) || lexer.lex_error_without_pos(eof_message)
+ if md = one_line.match(endline_pattern)
+ leading = md[1]
+ has_margin = md[2] == '|'
+ remove_break = md[3] == '-'
+ # Record position where next heredoc (from same line as current @()) should start scanning for content
+ ctx[:newline_jump] = scn.pos
+
+
+ # Process captured lines - remove leading, and trailing newline
+ str = heredoc_text(lines, leading, has_margin, remove_break)
+
+ # Use a new lexer instance configured with a sub-locator to enable correct positioning
+ sublexer = self.class.new()
+ locator = Puppet::Pops::Parser::Locator::SubLocator.sub_locator(str,
+ locator.file, heredoc_line, heredoc_offset, leading.length())
+
+ # Emit a token that provides the grammar with location information about the lines on which the heredoc
+ # content is based.
+ enqueue([:SUBLOCATE,
+ Puppet::Pops::Parser::LexerSupport::TokenValue.new([:SUBLOCATE,
+ lines, lines.reduce(0) {|size, s| size + s.length} ],
+ heredoc_offset,
+ locator)])
+
+ sublexer.lex_unquoted_string(str, locator, resulting_escapes, dqstring_style)
+ sublexer.interpolate_uq_to(self)
+ # Continue scan after @(...)
+ scn.pos = pos_after_heredoc
+ return
+ else
+ lines << one_line
+ end
+ end
+ lex_error_without_pos(eof_message)
+ end
+
+ # Produces the heredoc text string given the individual (unprocessed) lines as an array.
+ # @param lines [Array<String>] unprocessed lines of text in the heredoc w/o terminating line
+ # @param leading [String] the leading text up (up to pipe or other terminating char)
+ # @param has_margin [Boolean] if the left margin should be adjusted as indicated by `leading`
+ # @param remove_break [Boolean] if the line break (\r?\n) at the end of the last line should be removed or not
+ #
+ def heredoc_text(lines, leading, has_margin, remove_break)
+ if has_margin
+ leading_pattern = /^#{Regexp.escape(leading)}/
+ lines = lines.collect {|s| s.gsub(leading_pattern, '') }
+ end
+ result = lines.join('')
+ result.gsub!(/\r?\n$/, '') if remove_break
+ result
+ end
+
+
+end
diff --git a/lib/puppet/pops/parser/interpolation_support.rb b/lib/puppet/pops/parser/interpolation_support.rb
new file mode 100644
index 000000000..951be392a
--- /dev/null
+++ b/lib/puppet/pops/parser/interpolation_support.rb
@@ -0,0 +1,227 @@
+# This module is an integral part of the Lexer.
+# It defines interpolation support
+# PERFORMANCE NOTE: There are 4 very similar methods in this module that are designed to be as
+# performant as possible. While it is possible to parameterize them into one common method, the overhead
+# of passing parameters and evaluating conditional logic has a negative impact on performance.
+#
+module Puppet::Pops::Parser::InterpolationSupport
+
+ PATTERN_VARIABLE = %r{(::)?(\w+::)*\w+}
+
+ # This is the starting point for a double quoted string with possible interpolation
+ # The structure mimics that of the grammar.
+ # The logic is explicit (where the former implementation used parameters/strucures) given to a
+ # generic handler.
+ # (This is both easier to understand and faster).
+ #
+ def interpolate_dq
+ scn = @scanner
+ ctx = @lexing_context
+ before = scn.pos
+ # skip the leading " by doing a scan since the slurp_dqstring uses last matched when there is an error
+ scn.scan(/"/)
+ value,terminator = slurp_dqstring()
+ text = value
+ after = scn.pos
+ while true
+ case terminator
+ when '"'
+ # simple case, there was no interpolation, return directly
+ return emit_completed([:STRING, text, scn.pos-before], before)
+ when '${'
+ count = ctx[:brace_count]
+ ctx[:brace_count] += 1
+ # The ${ terminator is counted towards the string part
+ enqueue_completed([:DQPRE, text, scn.pos-before], before)
+ # Lex expression tokens until a closing (balanced) brace count is reached
+ enqueue_until count
+ break
+ when '$'
+ if varname = scn.scan(PATTERN_VARIABLE)
+ # The $ is counted towards the variable
+ enqueue_completed([:DQPRE, text, after-before-1], before)
+ enqueue_completed([:VARIABLE, varname, scn.pos - after + 1], after -1)
+ break
+ else
+ # false $ variable start
+ text += value
+ value,terminator = slurp_dqstring()
+ after = scn.pos
+ end
+ end
+ end
+ interpolate_tail_dq
+ # return the first enqueued token and shift the queue
+ @token_queue.shift
+ end
+
+ def interpolate_tail_dq
+ scn = @scanner
+ ctx = @lexing_context
+ before = scn.pos
+ value,terminator = slurp_dqstring
+ text = value
+ after = scn.pos
+ while true
+ case terminator
+ when '"'
+ # simple case, there was no further interpolation, return directly
+ enqueue_completed([:DQPOST, text, scn.pos-before], before)
+ return
+ when '${'
+ count = ctx[:brace_count]
+ ctx[:brace_count] += 1
+ # The ${ terminator is counted towards the string part
+ enqueue_completed([:DQMID, text, scn.pos-before], before)
+ # Lex expression tokens until a closing (balanced) brace count is reached
+ enqueue_until count
+ break
+ when '$'
+ if varname = scn.scan(PATTERN_VARIABLE)
+ # The $ is counted towards the variable
+ enqueue_completed([:DQMID, text, after-before-1], before)
+ enqueue_completed([:VARIABLE, varname, scn.pos - after +1], after -1)
+ break
+ else
+ # false $ variable start
+ text += value
+ value,terminator = self.send(slurpfunc)
+ after = scn.pos
+ end
+ end
+ end
+ interpolate_tail_dq
+ end
+
+ # This is the starting point for a un-quoted string with possible interpolation
+ # The logic is explicit (where the former implementation used parameters/strucures) given to a
+ # generic handler.
+ # (This is both easier to understand and faster).
+ #
+ def interpolate_uq
+ scn = @scanner
+ ctx = @lexing_context
+ before = scn.pos
+ value,terminator = slurp_uqstring()
+ text = value
+ after = scn.pos
+ while true
+ case terminator
+ when ''
+ # simple case, there was no interpolation, return directly
+ enqueue_completed([:STRING, text, scn.pos-before], before)
+ return
+ when '${'
+ count = ctx[:brace_count]
+ ctx[:brace_count] += 1
+ # The ${ terminator is counted towards the string part
+ enqueue_completed([:DQPRE, text, scn.pos-before], before)
+ # Lex expression tokens until a closing (balanced) brace count is reached
+ enqueue_until count
+ break
+ when '$'
+ if varname = scn.scan(PATTERN_VARIABLE)
+ # The $ is counted towards the variable
+ enqueue_completed([:DQPRE, text, after-before-1], before)
+ enqueue_completed([:VARIABLE, varname, scn.pos - after + 1], after -1)
+ break
+ else
+ # false $ variable start
+ text += value
+ value,terminator = slurp_uqstring()
+ after = scn.pos
+ end
+ end
+ end
+ interpolate_tail_uq
+ nil
+ end
+
+ def interpolate_tail_uq
+ scn = @scanner
+ ctx = @lexing_context
+ before = scn.pos
+ value,terminator = slurp_uqstring
+ text = value
+ after = scn.pos
+ while true
+ case terminator
+ when ''
+ # simple case, there was no further interpolation, return directly
+ enqueue_completed([:DQPOST, text, scn.pos-before], before)
+ return
+ when '${'
+ count = ctx[:brace_count]
+ ctx[:brace_count] += 1
+ # The ${ terminator is counted towards the string part
+ enqueue_completed([:DQMID, text, scn.pos-before], before)
+ # Lex expression tokens until a closing (balanced) brace count is reached
+ enqueue_until count
+ break
+ when '$'
+ if varname = scn.scan(PATTERN_VARIABLE)
+ # The $ is counted towards the variable
+ enqueue_completed([:DQMID, text, after-before-1], before)
+ enqueue_completed([:VARIABLE, varname, scn.pos - after +1], after -1)
+ break
+ else
+ # false $ variable start
+ text += value
+ value,terminator = slurp_uqstring
+ after = scn.pos
+ end
+ end
+ end
+ interpolate_tail_uq
+ end
+
+ # Enqueues lexed tokens until either end of input, or the given brace_count is reached
+ #
+ def enqueue_until brace_count
+ scn = @scanner
+ ctx = @lexing_context
+ queue = @token_queue
+
+ scn.skip(self.class::PATTERN_WS)
+ queue_size = queue.size
+ until scn.eos? do
+ if token = lex_token
+ token_name = token[0]
+ ctx[:after] = token_name
+ if token_name == :RBRACE && ctx[:brace_count] == brace_count
+ if queue.size - queue_size == 1
+ # Single token is subject to replacement
+ queue[-1] = transform_to_variable(queue[-1])
+ end
+ return
+ end
+ queue << token
+ else
+ scn.skip(self.class::PATTERN_WS)
+ end
+ end
+ end
+
+ def transform_to_variable(token)
+ token_name = token[0]
+ if [:NUMBER, :NAME].include?(token_name) || self.class::KEYWORD_NAMES[token_name]
+ t = token[1]
+ ta = t.token_array
+ [:VARIABLE, self.class::TokenValue.new([:VARIABLE, ta[1], ta[2]], t.offset, t.locator)]
+ else
+ token
+ end
+ end
+
+ # Interpolates unquoted string and transfers the result to the given lexer
+ # (This is used when a second lexer instance is used to lex a substring)
+ #
+ def interpolate_uq_to(lexer)
+ interpolate_uq
+ queue = @token_queue
+ until queue.empty? do
+ lexer.enqueue(queue.shift)
+ end
+ end
+
+end
diff --git a/lib/puppet/pops/parser/lexer.rb b/lib/puppet/pops/parser/lexer.rb
index 994c4643f..97a330a56 100644
--- a/lib/puppet/pops/parser/lexer.rb
+++ b/lib/puppet/pops/parser/lexer.rb
@@ -1,862 +1,753 @@
# the scanner/lexer
require 'forwardable'
require 'strscan'
require 'puppet'
require 'puppet/util/methodhelper'
module Puppet
class LexError < RuntimeError; end
end
class Puppet::Pops::Parser::Lexer
extend Forwardable
attr_reader :file, :lexing_context, :token_queue
attr_reader :locator
attr_accessor :indefine
alias :indefine? :indefine
def lex_error msg
raise Puppet::LexError.new(msg)
end
class Token
ALWAYS_ACCEPTABLE = Proc.new { |context| true }
include Puppet::Util::MethodHelper
attr_accessor :regex, :name, :string, :skip, :skip_text
alias skip? skip
# @overload initialize(string)
# @param string [String] a literal string token matcher
# @param name [String] the token name (what it is known as in the grammar)
# @param options [Hash] see {#set_options}
# @overload initialize(regex)
# @param regex [Regexp] a regular expression token text matcher
# @param name [String] the token name (what it is known as in the grammar)
# @param options [Hash] see {#set_options}
#
def initialize(string_or_regex, name, options = {})
if string_or_regex.is_a?(String)
@name, @string = name, string_or_regex
@regex = Regexp.new(Regexp.escape(string_or_regex))
else
@name, @regex = name, string_or_regex
end
set_options(options)
@acceptable_when = ALWAYS_ACCEPTABLE
end
# @return [String] human readable token reference; the String if literal, else the token name
def to_s
string or @name.to_s
end
# @return [Boolean] if the token is acceptable in the given context or not.
- # this implementation always returns true.
- # @param context [Hash] ? ? ?
+ # @param context [Hash] the lexing context
#
def acceptable?(context={})
@acceptable_when.call(context)
end
# Defines when the token is able to match.
# This provides context that cannot be expressed otherwise, such as feature flags.
#
# @param block [Proc] a proc that given a context returns a boolean
def acceptable_when(block)
@acceptable_when = block
end
end
# Maintains a list of tokens.
class TokenList
extend Forwardable
attr_reader :regex_tokens, :string_tokens
def_delegator :@tokens, :[]
# Adds a new token to the set of recognized tokens
# @param name [String] the token name
# @param regex [Regexp, String] source text token matcher, a litral string or regular expression
# @param options [Hash] see {Token::set_options}
# @param block [Proc] optional block set as the created tokens `convert` method
# @raise [ArgumentError] if the token with the given name is already defined
#
def add_token(name, regex, options = {}, &block)
raise(ArgumentError, "Token #{name} already exists") if @tokens.include?(name)
token = Token.new(regex, name, options)
@tokens[token.name] = token
if token.string
@string_tokens << token
@tokens_by_string[token.string] = token
else
@regex_tokens << token
end
token.meta_def(:convert, &block) if block_given?
token
end
# Creates an empty token list
#
def initialize
@tokens = {}
@regex_tokens = []
@string_tokens = []
@tokens_by_string = {}
end
# Look up a token by its literal (match) value, rather than name.
# @param string [String, nil] the literal match string to obtain a {Token} for, or nil if it does not exist.
def lookup(string)
@tokens_by_string[string]
end
# Adds tokens from a hash where key is a matcher (literal string or regexp) and the
# value is the token's name
# @param hash [Hash<{String => Symbol}, Hash<{Regexp => Symbol}] map token text matcher to token name
# @return [void]
#
def add_tokens(hash)
hash.each do |regex, name|
add_token(name, regex)
end
end
# Sort literal (string-) tokens by length, so we know once we match, we're done.
# This helps avoid the O(n^2) nature of token matching.
# The tokens are sorted in place.
# @return [void]
def sort_tokens
@string_tokens.sort! { |a, b| b.string.length <=> a.string.length }
end
# Yield each token name and value in turn.
def each
@tokens.each {|name, value| yield name, value }
end
end
TOKENS = TokenList.new
TOKENS.add_tokens(
'[' => :LBRACK,
']' => :RBRACK,
# '{' => :LBRACE, # Specialized to handle lambda and brace count
# '}' => :RBRACE, # Specialized to handle brace count
'(' => :LPAREN,
')' => :RPAREN,
'=' => :EQUALS,
'+=' => :APPENDS,
+ '-=' => :DELETES,
'==' => :ISEQUAL,
'>=' => :GREATEREQUAL,
'>' => :GREATERTHAN,
'<' => :LESSTHAN,
'<=' => :LESSEQUAL,
'!=' => :NOTEQUAL,
'!' => :NOT,
',' => :COMMA,
'.' => :DOT,
':' => :COLON,
'@' => :AT,
'|' => :PIPE,
'<<|' => :LLCOLLECT,
'|>>' => :RRCOLLECT,
'->' => :IN_EDGE,
'<-' => :OUT_EDGE,
'~>' => :IN_EDGE_SUB,
'<~' => :OUT_EDGE_SUB,
'<|' => :LCOLLECT,
'|>' => :RCOLLECT,
';' => :SEMIC,
'?' => :QMARK,
'\\' => :BACKSLASH,
'=>' => :FARROW,
'+>' => :PARROW,
'+' => :PLUS,
'-' => :MINUS,
'/' => :DIV,
'*' => :TIMES,
'%' => :MODULO,
'<<' => :LSHIFT,
'>>' => :RSHIFT,
'=~' => :MATCH,
'!~' => :NOMATCH,
%r{((::){0,1}[A-Z][-\w]*)+} => :CLASSREF,
"<string>" => :STRING,
"<dqstring up to first interpolation>" => :DQPRE,
"<dqstring between two interpolations>" => :DQMID,
"<dqstring after final interpolation>" => :DQPOST,
"<boolean>" => :BOOLEAN,
"<select start>" => :SELBRACE # A QMARK followed by '{'
)
module Contextual
QUOTE_TOKENS = [:DQPRE,:DQMID]
REGEX_INTRODUCING_TOKENS = [:NODE,:LBRACE, :SELBRACE, :RBRACE,:MATCH,:NOMATCH,:COMMA]
NOT_INSIDE_QUOTES = Proc.new do |context|
!QUOTE_TOKENS.include? context[:after]
end
INSIDE_QUOTES = Proc.new do |context|
QUOTE_TOKENS.include? context[:after]
end
IN_REGEX_POSITION = Proc.new do |context|
REGEX_INTRODUCING_TOKENS.include? context[:after]
end
- DASHED_VARIABLES_ALLOWED = Proc.new do |context|
- Puppet[:allow_variables_with_dashes]
- end
-
- VARIABLE_AND_DASHES_ALLOWED = Proc.new do |context|
- Contextual::DASHED_VARIABLES_ALLOWED.call(context) and TOKENS[:VARIABLE].acceptable?(context)
- end
+# DASHED_VARIABLES_ALLOWED = Proc.new do |context|
+# Puppet[:allow_variables_with_dashes]
+# end
+#
+# VARIABLE_AND_DASHES_ALLOWED = Proc.new do |context|
+# Contextual::DASHED_VARIABLES_ALLOWED.call(context) and TOKENS[:VARIABLE].acceptable?(context)
+# 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)
[TOKENS[:NAME], value]
end
TOKENS[:NUMBER].acceptable_when Contextual::NOT_INSIDE_QUOTES
TOKENS.add_token :NAME, %r{((::)?[a-z0-9][-\w]*)(::[a-z0-9][-\w]*)*} do |lexer, value|
# A name starting with a number must be a valid numeric string (not that
# NUMBER token captures those names that do not comply with the name rule.
if value =~ /^[0-9].*$/
lexer.assert_numeric(value)
end
string_token = self
# we're looking for keywords here
if tmp = KEYWORDS.lookup(value)
string_token = tmp
if [:TRUE, :FALSE].include?(string_token.name)
value = eval(value)
string_token = TOKENS[:BOOLEAN]
end
end
[string_token, value]
end
[:NAME, :CLASSREF].each do |name_token|
TOKENS[name_token].acceptable_when Contextual::NOT_INSIDE_QUOTES
end
TOKENS.add_token :COMMENT, %r{#.*}, :skip => true do |lexer,value|
- value.sub!(/# ?/,'')
- [self, value]
+# value.sub!(/# ?/,'')
+ [self, ""]
end
TOKENS.add_token :MLCOMMENT, %r{/\*(.*?)\*/}m, :skip => true do |lexer, value|
- value.sub!(/^\/\* ?/,'')
- value.sub!(/ ?\*\/$/,'')
- [self,value]
+# value.sub!(/^\/\* ?/,'')
+# value.sub!(/ ?\*\/$/,'')
+ [self, ""]
end
TOKENS.add_token :REGEX, %r{/[^/\n]*/} do |lexer, value|
# Make sure we haven't matched an escaped /
while value[-2..-2] == '\\'
other = lexer.scan_until(%r{/})
value += other
end
regex = value.sub(%r{\A/}, "").sub(%r{/\Z}, '').gsub("\\/", "/")
[self, Regexp.new(regex)]
end
TOKENS[:REGEX].acceptable_when Contextual::IN_REGEX_POSITION
TOKENS.add_token :RETURN, "\n", :skip => true, :skip_text => true
TOKENS.add_token :SQUOTE, "'" do |lexer, value|
[TOKENS[:STRING], lexer.slurpstring(value,["'"],:ignore_invalid_escapes).first ]
end
DQ_initial_token_types = {'$' => :DQPRE,'"' => :STRING}
DQ_continuation_token_types = {'$' => :DQMID,'"' => :DQPOST}
TOKENS.add_token :DQUOTE, /"/ do |lexer, value|
lexer.tokenize_interpolated_string(DQ_initial_token_types)
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|
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.add_token :DOLLAR_VAR_WITH_DASH, %r{\$(?:::)?(?:[-\w]+::)*[-\w]+} do |lexer, value|
- lexer.warn_if_variable_has_hyphen(value)
-
- [TOKENS[:VARIABLE], value[1..-1]]
- end
- TOKENS[:DOLLAR_VAR_WITH_DASH].acceptable_when Contextual::DASHED_VARIABLES_ALLOWED
-
TOKENS.add_token :DOLLAR_VAR, %r{\$(::)?(\w+::)*\w+} do |lexer, value|
[TOKENS[:VARIABLE],value[1..-1]]
end
- TOKENS.add_token :VARIABLE_WITH_DASH, %r{(?:::)?(?:[-\w]+::)*[-\w]+} do |lexer, value|
- lexer.warn_if_variable_has_hyphen(value)
- # If the varname (following $, or ${ is followed by (, it is a function call, and not a variable
- # reference.
- #
- if lexer.match?(%r{[ \t\r]*\(})
- [TOKENS[:NAME],value]
- else
- [TOKENS[:VARIABLE], value]
- end
- end
- TOKENS[:VARIABLE_WITH_DASH].acceptable_when Contextual::VARIABLE_AND_DASHES_ALLOWED
-
TOKENS.add_token :VARIABLE, %r{(::)?(\w+::)*\w+} do |lexer, value|
# If the varname (following $, or ${ is followed by (, it is a function call, and not a variable
# reference.
#
if lexer.match?(%r{[ \t\r]*\(})
# 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]
end
end
TOKENS[:VARIABLE].acceptable_when Contextual::INSIDE_QUOTES
TOKENS.sort_tokens
@@pairs = {
"{" => "}",
"(" => ")",
"[" => "]",
"<|" => "|>",
"<<|" => "|>>",
"|" => "|"
}
KEYWORDS = TokenList.new
KEYWORDS.add_tokens(
"case" => :CASE,
"class" => :CLASS,
"default" => :DEFAULT,
"define" => :DEFINE,
# "import" => :IMPORT,
"if" => :IF,
"elsif" => :ELSIF,
"else" => :ELSE,
"inherits" => :INHERITS,
"node" => :NODE,
"and" => :AND,
"or" => :OR,
"undef" => :UNDEF,
"false" => :FALSE,
"true" => :TRUE,
"in" => :IN,
"unless" => :UNLESS
)
def clear
initvars
end
def expected
return nil if @expected.empty?
name = @expected[-1]
TOKENS.lookup(name) or lex_error "Internal Lexer Error: Could not find expected token #{name}"
end
# scan the whole file
# basically just used for testing
def fullscan
array = []
self.scan { |token, str|
# Ignore any definition nesting problems
@indefine = false
array.push([token,str])
}
array
end
def file=(file)
@file = file
- contents = Puppet::FileSystem::File.exist?(file) ? File.read(file) : ""
- @scanner = StringScanner.new(contents)
- @locator = Locator.new(contents, multibyte?)
+ contents = Puppet::FileSystem.exist?(file) ? Puppet::FileSystem.read(file) : ""
+ @scanner = StringScanner.new(contents.freeze)
+ @locator = Puppet::Pops::Parser::Locator.locator(contents, file)
end
def_delegator :@token_queue, :shift, :shift_token
def find_string_token
# We know our longest string token is three chars, so try each size in turn
# until we either match or run out of chars. This way our worst-case is three
# tries, where it is otherwise the number of string token we have. Also,
# the lookups are optimized hash lookups, instead of regex scans.
#
- s = @scanner.peek(3)
+ _scn = @scanner
+ s = _scn.peek(3)
token = TOKENS.lookup(s[0,3]) || TOKENS.lookup(s[0,2]) || TOKENS.lookup(s[0,1])
- [ token, token && @scanner.scan(token.regex) ]
+ unless token
+ return [nil, nil]
+ end
+ [ token, _scn.scan(token.regex) ]
end
# Find the next token that matches a regex. We look for these first.
def find_regex_token
best_token = nil
best_length = 0
# I tried optimizing based on the first char, but it had
# a slightly negative affect and was a good bit more complicated.
+ _lxc = @lexing_context
+ _scn = @scanner
TOKENS.regex_tokens.each do |token|
- if length = @scanner.match?(token.regex) and token.acceptable?(lexing_context)
+ if length = _scn.match?(token.regex) and token.acceptable?(_lxc)
# We've found a longer match
if length > best_length
best_length = length
best_token = token
end
end
end
- return best_token, @scanner.scan(best_token.regex) if best_token
+ return best_token, _scn.scan(best_token.regex) if best_token
end
# Find the next token, returning the string and the token.
def find_token
shift_token || find_regex_token || find_string_token
end
+ MULTIBYTE = Puppet::Pops::Parser::Locator::MULTIBYTE
+ SKIPPATTERN = MULTIBYTE ? %r{[[:blank:]\r]+} : %r{[ \t\r]+}
+
def initialize
- @multibyte = init_multibyte
initvars
end
def assert_numeric(value)
if value =~ /^0[xX].*$/
lex_error (positioned_message("Not a valid hex number #{value}")) unless value =~ /^0[xX][0-9A-Fa-f]+$/
elsif value =~ /^0[^.].*$/
lex_error(positioned_message("Not a valid octal number #{value}")) unless value =~ /^0[0-7]+$/
else
lex_error(positioned_message("Not a valid decimal number #{value}")) unless value =~ /0?\d+(?:\.\d+)?(?:[eE]-?\d+)?/
end
end
- # Returns true if ruby version >= 1.9.3 since regexp supports multi-byte matches and expanded
- # character categories like [[:blank:]].
- #
- # This implementation will fail if there are more than 255 minor or micro versions of ruby
- #
- def init_multibyte
- numver = RUBY_VERSION.split(".").collect {|s| s.to_i }
- return true if (numver[0] << 16 | numver[1] << 8 | numver[2]) >= (1 << 16 | 9 << 8 | 3)
- false
- end
-
- def multibyte?
- @multibyte
- end
-
def initvars
@previous_token = nil
@scanner = nil
@file = nil
# AAARRGGGG! okay, regexes in ruby are bloody annoying
# no one else has "\n" =~ /\s/
- if multibyte?
- # Skip all kinds of space, and CR, but not newlines
- @skip = %r{[[:blank:]\r]+}
- else
- @skip = %r{[ \t\r]+}
- end
-
@namestack = []
@token_queue = []
@indefine = false
@expected = []
@lexing_context = {
:after => nil,
:start_of_line => true,
:offset => 0, # byte offset before where token starts
:end_offset => 0, # byte offset after scanned token
:brace_count => 0, # nested depth of braces
:interpolation_stack => [] # matching interpolation brace level
}
end
# Make any necessary changes to the token and/or value.
def munge_token(token, value)
# A token may already have been munged (converted and positioned)
#
return token, value if value.is_a? Hash
- skip if token.skip_text
+ @scanner.skip(SKIPPATTERN) if token.skip_text
return if token.skip
token, value = token.convert(self, value) if token.respond_to?(:convert)
return unless token
return if token.skip
# If the conversion performed the munging/positioning
return token, value if value.is_a? Hash
- pos_hash = position_in_source
- pos_hash[:value] = value
-
- # Add one to pos, first char on line is 1
- return token, pos_hash
+ return token, positioned_value(value)
end
# Returns a hash with the current position in source based on the current lexing context
#
- def position_in_source
- pos = @locator.pos_on_line(lexing_context[:offset])
- offset = @locator.char_offset(lexing_context[:offset])
- length = @locator.char_length(lexing_context[:offset], lexing_context[:end_offset])
- start_line = @locator.line_for_offset(lexing_context[:offset])
-
- return { :line => start_line, :pos => pos, :offset => offset, :length => length}
+ def positioned_value(value)
+ {
+ :value => value,
+ :locator => @locator,
+ :offset => @lexing_context[:offset],
+ :end_offset => @lexing_context[:end_offset]
+ }
end
def pos
- @locator.pos_on_line(lexing_context[:offset])
+ @locator.pos_on_line(@lexing_context[:offset])
end
# Handling the namespace stack
def_delegator :@namestack, :pop, :namepop
# This value might have :: in it, but we don't care -- it'll be handled
# normally when joining, and when popping we want to pop this full value,
# however long the namespace is.
def_delegator :@namestack, :<<, :namestack
# Collect the current namespace.
def namespace
@namestack.join("::")
end
def_delegator :@scanner, :rest
+
+ LBRACE_CHAR = '{'
+
# this is the heart of the lexer
def scan
+ _scn = @scanner
#Puppet.debug("entering scan")
- lex_error "Internal Error: No string or file given to lexer to process." unless @scanner
+ lex_error "Internal Error: No string or file given to lexer to process." unless _scn
# Skip any initial whitespace.
- skip
+ _scn.skip(SKIPPATTERN)
+ _lbrace = '{'.freeze # faster to compare against a frozen string in
- until token_queue.empty? and @scanner.eos? do
- offset = @scanner.pos
+ until token_queue.empty? and _scn.eos? do
+ offset = _scn.pos
matched_token, value = find_token
- end_offset = @scanner.pos
+ end_offset = _scn.pos
# error out if we didn't match anything at all
- lex_error "Could not match #{@scanner.rest[/^(\S+|\s+|.*)/]}" unless matched_token
+ lex_error "Could not match #{_scn.rest[/^(\S+|\s+|.*)/]}" unless matched_token
newline = matched_token.name == :RETURN
- lexing_context[:start_of_line] = newline
- lexing_context[:offset] = offset
- lexing_context[:end_offset] = end_offset
+ _lxc = @lexing_context
+ _lxc[:start_of_line] = newline
+ _lxc[:offset] = offset
+ _lxc[:end_offset] = end_offset
final_token, token_value = munge_token(matched_token, value)
# update end position since munging may have moved the end offset
- lexing_context[:end_offset] = @scanner.pos
+ _lxc[:end_offset] = _scn.pos
unless final_token
- skip
+ _scn.skip(SKIPPATTERN)
next
end
- lexing_context[:after] = final_token.name unless newline
+ _lxc[:after] = final_token.name unless newline
if final_token.name == :DQPRE
- lexing_context[:interpolation_stack] << lexing_context[:brace_count]
+ _lxc[:interpolation_stack] << _lxc[:brace_count]
elsif final_token.name == :DQPOST
- lexing_context[:interpolation_stack].pop
+ _lxc[:interpolation_stack].pop
end
value = token_value[:value]
+ _expected = @expected
if match = @@pairs[value] and final_token.name != :DQUOTE and final_token.name != :SQUOTE
- @expected << match
- elsif exp = @expected[-1] and exp == value and final_token.name != :DQUOTE and final_token.name != :SQUOTE
- @expected.pop
+ _expected << match
+ elsif exp = _expected[-1] and exp == value and final_token.name != :DQUOTE and final_token.name != :SQUOTE
+ _expected.pop
end
yield [final_token.name, token_value]
- if @previous_token
- namestack(value) if @previous_token.name == :CLASS and value != '{'
+ _prv = @previous_token
+ if _prv
+ namestack(value) if _prv.name == :CLASS and value != LBRACE_CHAR
- if @previous_token.name == :DEFINE
+ # TODO: Lexer has no business dealing with this - it is semantic
+ if _prv.name == :DEFINE
if indefine?
msg = "Cannot nest definition #{value} inside #{@indefine}"
self.indefine = false
raise Puppet::ParseError, msg
end
@indefine = value
end
end
@previous_token = final_token
- skip
+ _scn.skip(SKIPPATTERN)
end
# Cannot reset @scanner to nil here - it is needed to answer questions about context after
# completed parsing.
# Seems meaningless to do this. Everything will be gc anyway.
#@scanner = nil
# This indicates that we're done parsing.
yield [false,false]
end
- # Skip any skipchars in our remaining string.
- def skip
- @scanner.skip(@skip)
- end
-
def match? r
@scanner.match?(r)
end
# Provide some limited access to the scanner, for those
# tokens that need it.
def_delegator :@scanner, :scan_until
# we've encountered the start of a string...
# slurp in the rest of the string and return it
def slurpstring(terminators,escapes=%w{ \\ $ ' " r n t s }+["\n"],ignore_invalid_escapes=false)
# we search for the next quote that isn't preceded by a
# backslash; the caret is there to match empty strings
last = @scanner.matched
str = @scanner.scan_until(/([^\\]|^|[^\\])([\\]{2})*[#{terminators}]/) || lex_error(positioned_message("Unclosed quote after #{format_quote(last)} followed by '#{followed_by}'"))
str.gsub!(/\\(.)/m) {
ch = $1
if escapes.include? ch
case ch
when 'r'; "\r"
when 'n'; "\n"
when 't'; "\t"
when 's'; " "
when "\n"; ''
else ch
end
else
Puppet.warning(positioned_message("Unrecognized escape sequence '\\#{ch}'")) unless ignore_invalid_escapes
"\\#{ch}"
end
}
[ str[0..-2],str[-1,1] ]
end
# Formats given message by appending file, line and position if available.
def positioned_message msg
result = [msg]
result << "in file #{file}" if file
result << "at line #{line}:#{pos}" if line
result.join(" ")
end
# Returns "<eof>" if at end of input, else the following 5 characters with \n \r \t escaped
def followed_by
return "<eof>" if @scanner.eos?
result = @scanner.rest[0,5] + "..."
result.gsub!("\t", '\t')
result.gsub!("\n", '\n')
result.gsub!("\r", '\r')
result
end
def format_quote q
if q == "'"
'"\'"'
else
"'#{q}'"
end
end
def tokenize_interpolated_string(token_type,preamble='')
# Expecting a (possibly empty) stretch of text terminated by end of string ", a variable $, or expression ${
# The length of this part includes the start and terminating characters.
value,terminator = slurpstring('"$')
# Advanced after '{' if this is in expression ${} interpolation
braced = terminator == '$' && @scanner.scan(/\{/)
# make offset to end_ofset be the length of the pre expression string including its start and terminating chars
- lexing_context[:end_offset] = @scanner.pos
+ lxc = @lexing_context
+ lxc[:end_offset] = @scanner.pos
- token_queue << [TOKENS[token_type[terminator]],position_in_source().merge!({:value => preamble+value})]
+ token_queue << [TOKENS[token_type[terminator]],positioned_value(preamble+value)]
variable_regex = if Puppet[:allow_variables_with_dashes]
TOKENS[:VARIABLE_WITH_DASH].regex
else
TOKENS[:VARIABLE].regex
end
if terminator != '$' or braced
return token_queue.shift
end
tmp_offset = @scanner.pos
if var_name = @scanner.scan(variable_regex)
- lexing_context[:offset] = tmp_offset
- lexing_context[:end_offset] = @scanner.pos
+ lxc[:offset] = tmp_offset
+ lxc[:end_offset] = @scanner.pos
warn_if_variable_has_hyphen(var_name)
# If the varname after ${ is followed by (, it is a function call, and not a variable
# reference.
#
if braced && @scanner.match?(%r{[ \t\r]*\(})
- token_queue << [TOKENS[:NAME], position_in_source().merge!({:value=>var_name})]
+ token_queue << [TOKENS[:NAME], positioned_value(var_name)]
else
- token_queue << [TOKENS[:VARIABLE],position_in_source().merge!({:value=>var_name})]
+ token_queue << [TOKENS[:VARIABLE],positioned_value(var_name)]
end
- lexing_context[:offset] = @scanner.pos
+ lxc[:offset] = @scanner.pos
tokenize_interpolated_string(DQ_continuation_token_types)
else
tokenize_interpolated_string(token_type, replace_false_start_with_text(terminator))
end
end
def replace_false_start_with_text(appendix)
last_token = token_queue.pop
value = last_token.last
if value.is_a? Hash
value[:value] + appendix
else
value + appendix
end
end
# just parse a string, not a whole file
- def string=(string)
- @scanner = StringScanner.new(string)
- @locator = Locator.new(string, multibyte?)
+ def string=(string, path='')
+ @scanner = StringScanner.new(string.freeze)
+ @locator = Puppet::Pops::Parser::Locator.locator(string, path)
end
def warn_if_variable_has_hyphen(var_name)
if var_name.include?('-')
Puppet.deprecation_warning("Using `-` in variable names is deprecated at #{file || '<string>'}:#{line}. See http://links.puppetlabs.com/puppet-hyphenated-variable-deprecation")
end
end
# Returns the line number (starting from 1) for the current position
# in the scanned text (at the end of the last produced, but not necessarily
# consumed.
#
def line
- return 1 unless lexing_context && locator
- locator.line_for_offset(lexing_context[:end_offset])
- end
-
- # Helper class that keeps track of where line breaks are located and can answer questions about positions.
- #
- class Locator
- attr_reader :line_index
- attr_reader :string
-
- # Create a locator based on a content string, and a boolean indicating if ruby version support multi-byte strings
- # or not.
- #
- def initialize(string, multibyte)
- @string = string
- @multibyte = multibyte
- compute_line_index
- end
-
- # Returns whether this a ruby version that supports multi-byte strings or not
- #
- def multibyte?
- @multibyte
- end
-
- # Computes the start offset for each line.
- #
- def compute_line_index
- scanner = StringScanner.new(@string)
- result = [0] # first line starts at 0
- while scanner.scan_until(/\n/)
- result << scanner.pos
- end
- @line_index = result
- end
-
- # Returns the line number (first line is 1) for the given offset
- def line_for_offset(offset)
- if line_nbr = line_index.index {|x| x > offset}
- return line_nbr
- end
- # If not found it is after last
- return line_index.size
- end
-
- # Returns the offset on line (first offset on a line is 0).
- #
- def offset_on_line(offset)
- line_offset = line_index[line_for_offset(offset)-1]
- if multibyte?
- @string.byteslice(line_offset, offset-line_offset).length
- else
- offset - line_offset
- end
- end
-
- # Returns the position on line (first position on a line is 1)
- def pos_on_line(offset)
- offset_on_line(offset) +1
- end
-
- # Returns the character offset for a given byte offset
- def char_offset(byte_offset)
- if multibyte?
- @string.byteslice(0, byte_offset).length
- else
- byte_offset
- end
- end
-
- # Returns the length measured in number of characters from the given start and end byte offseta
- def char_length(offset, end_offset)
- if multibyte?
- @string.byteslice(offset, end_offset - offset).length
- else
- end_offset - offset
- end
- end
+ return 1 unless @lexing_context && locator
+ locator.line_for_offset(@lexing_context[:end_offset])
end
end
diff --git a/lib/puppet/pops/parser/lexer2.rb b/lib/puppet/pops/parser/lexer2.rb
new file mode 100644
index 000000000..d9cec9352
--- /dev/null
+++ b/lib/puppet/pops/parser/lexer2.rb
@@ -0,0 +1,684 @@
+# The Lexer is responsbile for turning source text into tokens.
+# This version is a performance enhanced lexer (in comparison to the 3.x and earlier "future parser" lexer.
+#
+# Old returns tokens [:KEY, value, { locator = }
+# Could return [[token], locator]
+# or Token.new([token], locator) with the same API x[0] = token_symbol, x[1] = self, x[:key] = (:value, :file, :line, :pos) etc
+
+require 'strscan'
+require 'puppet/pops/parser/lexer_support'
+require 'puppet/pops/parser/heredoc_support'
+require 'puppet/pops/parser/interpolation_support'
+require 'puppet/pops/parser/epp_support'
+require 'puppet/pops/parser/slurp_support'
+
+class Puppet::Pops::Parser::Lexer2
+ include Puppet::Pops::Parser::LexerSupport
+ include Puppet::Pops::Parser::HeredocSupport
+ include Puppet::Pops::Parser::InterpolationSupport
+ include Puppet::Pops::Parser::SlurpSupport
+ include Puppet::Pops::Parser::EppSupport
+
+ # ALl tokens have three slots, the token name (a Symbol), the token text (String), and a token text length.
+ # All operator and punctuation tokens reuse singleton arrays Tokens that require unique values create
+ # a unique array per token.
+ #
+ # PEFORMANCE NOTES:
+ # This construct reduces the amount of object that needs to be created for operators and punctuation.
+ # The length is pre-calculated for all singleton tokens. The length is used both to signal the length of
+ # the token, and to advance the scanner position (without having to advance it with a scan(regexp)).
+ #
+ TOKEN_LBRACK = [:LBRACK, '['.freeze, 1].freeze
+ TOKEN_LISTSTART = [:LISTSTART, '['.freeze, 1].freeze
+ TOKEN_RBRACK = [:RBRACK, ']'.freeze, 1].freeze
+ TOKEN_LBRACE = [:LBRACE, '{'.freeze, 1].freeze
+ TOKEN_RBRACE = [:RBRACE, '}'.freeze, 1].freeze
+ TOKEN_SELBRACE = [:SELBRACE, '{'.freeze, 1].freeze
+ TOKEN_LPAREN = [:LPAREN, '('.freeze, 1].freeze
+ TOKEN_RPAREN = [:RPAREN, ')'.freeze, 1].freeze
+
+ TOKEN_EQUALS = [:EQUALS, '='.freeze, 1].freeze
+ TOKEN_APPENDS = [:APPENDS, '+='.freeze, 2].freeze
+ TOKEN_DELETES = [:DELETES, '-='.freeze, 2].freeze
+
+ TOKEN_ISEQUAL = [:ISEQUAL, '=='.freeze, 2].freeze
+ TOKEN_NOTEQUAL = [:NOTEQUAL, '!='.freeze, 2].freeze
+ TOKEN_MATCH = [:MATCH, '=~'.freeze, 2].freeze
+ TOKEN_NOMATCH = [:NOMATCH, '!~'.freeze, 2].freeze
+ TOKEN_GREATEREQUAL = [:GREATEREQUAL, '>='.freeze, 2].freeze
+ TOKEN_GREATERTHAN = [:GREATERTHAN, '>'.freeze, 1].freeze
+ TOKEN_LESSEQUAL = [:LESSEQUAL, '<='.freeze, 2].freeze
+ TOKEN_LESSTHAN = [:LESSTHAN, '<'.freeze, 1].freeze
+
+ TOKEN_FARROW = [:FARROW, '=>'.freeze, 2].freeze
+ TOKEN_PARROW = [:PARROW, '+>'.freeze, 2].freeze
+
+ TOKEN_LSHIFT = [:LSHIFT, '<<'.freeze, 2].freeze
+ TOKEN_LLCOLLECT = [:LLCOLLECT, '<<|'.freeze, 3].freeze
+ TOKEN_LCOLLECT = [:LCOLLECT, '<|'.freeze, 2].freeze
+
+ TOKEN_RSHIFT = [:RSHIFT, '>>'.freeze, 2].freeze
+ TOKEN_RRCOLLECT = [:RRCOLLECT, '|>>'.freeze, 3].freeze
+ TOKEN_RCOLLECT = [:RCOLLECT, '|>'.freeze, 2].freeze
+
+ TOKEN_PLUS = [:PLUS, '+'.freeze, 1].freeze
+ TOKEN_MINUS = [:MINUS, '-'.freeze, 1].freeze
+ TOKEN_DIV = [:DIV, '/'.freeze, 1].freeze
+ TOKEN_TIMES = [:TIMES, '*'.freeze, 1].freeze
+ TOKEN_MODULO = [:MODULO, '%'.freeze, 1].freeze
+
+ TOKEN_NOT = [:NOT, '!'.freeze, 1].freeze
+ TOKEN_DOT = [:DOT, '.'.freeze, 1].freeze
+ TOKEN_PIPE = [:PIPE, '|'.freeze, 1].freeze
+ TOKEN_AT = [:AT , '@'.freeze, 1].freeze
+ TOKEN_ATAT = [:ATAT , '@@'.freeze, 2].freeze
+ TOKEN_COLON = [:COLON, ':'.freeze, 1].freeze
+ TOKEN_COMMA = [:COMMA, ','.freeze, 1].freeze
+ TOKEN_SEMIC = [:SEMIC, ';'.freeze, 1].freeze
+ TOKEN_QMARK = [:QMARK, '?'.freeze, 1].freeze
+ TOKEN_TILDE = [:TILDE, '~'.freeze, 1].freeze # lexed but not an operator in Puppet
+
+ TOKEN_REGEXP = [:REGEXP, nil, 0].freeze
+
+ TOKEN_IN_EDGE = [:IN_EDGE, '->'.freeze, 2].freeze
+ TOKEN_IN_EDGE_SUB = [:IN_EDGE_SUB, '~>'.freeze, 2].freeze
+ TOKEN_OUT_EDGE = [:OUT_EDGE, '<-'.freeze, 2].freeze
+ TOKEN_OUT_EDGE_SUB = [:OUT_EDGE_SUB, '<~'.freeze, 2].freeze
+
+ # Tokens that are always unique to what has been lexed
+ TOKEN_STRING = [:STRING, nil, 0].freeze
+ TOKEN_DQPRE = [:DQPRE, nil, 0].freeze
+ TOKEN_DQMID = [:DQPRE, nil, 0].freeze
+ TOKEN_DQPOS = [:DQPRE, nil, 0].freeze
+ TOKEN_NUMBER = [:NUMBER, nil, 0].freeze
+ TOKEN_VARIABLE = [:VARIABLE, nil, 1].freeze
+ TOKEN_VARIABLE_EMPTY = [:VARIABLE, ''.freeze, 1].freeze
+
+ # HEREDOC has syntax as an argument.
+ TOKEN_HEREDOC = [:HEREDOC, nil, 0].freeze
+
+ # EPP_START is currently a marker token, may later get syntax
+ TOKEN_EPPSTART = [:EPP_START, nil, 0].freeze
+ TOKEN_EPPEND = [:EPP_END, '%>', 2].freeze
+ TOKEN_EPPEND_TRIM = [:EPP_END_TRIM, '-%>', 3].freeze
+
+ # This is used for unrecognized tokens, will always be a single character. This particular instance
+ # is not used, but is kept here for documentation purposes.
+ TOKEN_OTHER = [:OTHER, nil, 0]
+
+ # Keywords are all singleton tokens with pre calculated lengths.
+ # Booleans are pre-calculated (rather than evaluating the strings "false" "true" repeatedly.
+ #
+ KEYWORDS = {
+ "case" => [:CASE, 'case', 4],
+ "class" => [:CLASS, 'class', 5],
+ "default" => [:DEFAULT, 'default', 7],
+ "define" => [:DEFINE, 'define', 6],
+ "if" => [:IF, 'if', 2],
+ "elsif" => [:ELSIF, 'elsif', 5],
+ "else" => [:ELSE, 'else', 4],
+ "inherits" => [:INHERITS,'inherits', 8],
+ "node" => [:NODE, 'node', 4],
+ "and" => [:AND, 'and', 3],
+ "or" => [:OR, 'or', 2],
+ "undef" => [:UNDEF, 'undef', 5],
+ "false" => [:BOOLEAN, false, 5],
+ "true" => [:BOOLEAN, true, 4],
+ "in" => [:IN, 'in', 2],
+ "unless" => [:UNLESS, 'unless', 6],
+ }
+ KEYWORDS.each {|k,v| v[1].freeze; v.freeze }
+ KEYWORDS.freeze
+
+ # Reverse lookup of keyword name to string
+ KEYWORD_NAMES = {}
+ KEYWORDS.each {|k, v| KEYWORD_NAMES[v[0]] = k }
+ KEYWORD_NAMES.freeze
+
+ PATTERN_WS = %r{[[:blank:]\r]+}
+
+ # The single line comment includes the line ending.
+ PATTERN_COMMENT = %r{#.*\r?}
+ PATTERN_MLCOMMENT = %r{/\*(.*?)\*/}m
+
+ PATTERN_REGEX = %r{/[^/\n]*/}
+ PATTERN_REGEX_END = %r{/}
+ PATTERN_REGEX_A = %r{\A/} # for replacement to ""
+ PATTERN_REGEX_Z = %r{/\Z} # for replacement to ""
+ PATTERN_REGEX_ESC = %r{\\/} # for replacement to "/"
+
+ # The 3x patterns:
+ # PATTERN_CLASSREF = %r{((::){0,1}[A-Z][-\w]*)+}
+ # PATTERN_NAME = %r{((::)?[a-z0-9][-\w]*)(::[a-z0-9][-\w]*)*}
+
+ # The NAME and CLASSREF in 4x are strict. Each segment must start with
+ # a letter a-z and may not contain dashes (\w includes letters, digits and _).
+ #
+ PATTERN_CLASSREF = %r{((::){0,1}[A-Z][\w]*)+}
+ PATTERN_NAME = %r{((::)?[a-z][\w]*)(::[a-z][\w]*)*}
+ PATTERN_BARE_WORD = %r{[a-z_](?:[\w-]*[\w])?}
+
+ PATTERN_DOLLAR_VAR = %r{\$(::)?(\w+::)*\w+}
+ PATTERN_NUMBER = %r{\b(?:0[xX][0-9A-Fa-f]+|0?\d+(?:\.\d+)?(?:[eE]-?\d+)?)\b}
+
+ # PERFORMANCE NOTE:
+ # Comparison against a frozen string is faster (than unfrozen).
+ #
+ STRING_BSLASH_BSLASH = '\\'.freeze
+
+ attr_reader :locator
+
+ def initialize()
+ end
+
+ # Clears the lexer state (it is not required to call this as it will be garbage collected
+ # and the next lex call (lex_string, lex_file) will reset the internal state.
+ #
+ def clear()
+ # not really needed, but if someone wants to ensure garbage is collected as early as possible
+ @scanner = nil
+ @locator = nil
+ @lexing_context = nil
+ end
+
+ # Convenience method, and for compatibility with older lexer. Use the lex_string instead which allows
+ # passing the path to use without first having to call file= (which reads the file if it exists).
+ # (Bad form to use overloading of assignment operator for something that is not really an assignment. Also,
+ # overloading of = does not allow passing more than one argument).
+ #
+ def string=(string)
+ lex_string(string, '')
+ end
+
+ def lex_string(string, path='')
+ initvars
+ @scanner = StringScanner.new(string)
+ @locator = Puppet::Pops::Parser::Locator.locator(string, path)
+ end
+
+ # Lexes an unquoted string.
+ # @param string [String] the string to lex
+ # @param locator [Puppet::Pops::Parser::Locator] the locator to use (a default is used if nil is given)
+ # @param escapes [Array<String>] array of character strings representing the escape sequences to transform
+ # @param interpolate [Boolean] whether interpolation of expressions should be made or not.
+ #
+ def lex_unquoted_string(string, locator, escapes, interpolate)
+ initvars
+ @scanner = StringScanner.new(string)
+ @locator = locator || Puppet::Pops::Parser::Locator.locator(string, '')
+ @lexing_context[:escapes] = escapes || UQ_ESCAPES
+ @lexing_context[:uq_slurp_pattern] = (interpolate || !escapes.empty?) ? SLURP_UQ_PATTERN : SLURP_ALL_PATTERN
+ end
+
+ # Convenience method, and for compatibility with older lexer. Use the lex_file instead.
+ # (Bad form to use overloading of assignment operator for something that is not really an assignment).
+ #
+ def file=(file)
+ lex_file(file)
+ end
+
+ # TODO: This method should not be used, callers should get the locator since it is most likely required to
+ # compute line, position etc given offsets.
+ #
+ def file
+ @locator ? @locator.file : nil
+ end
+
+ # Initializes lexing of the content of the given file. An empty string is used if the file does not exist.
+ #
+ def lex_file(file)
+ initvars
+ contents = Puppet::FileSystem.exist?(file) ? Puppet::FileSystem.read(file) : ""
+ @scanner = StringScanner.new(contents.freeze)
+ @locator = Puppet::Pops::Parser::Locator.locator(contents, file)
+ end
+
+ def initvars
+ @token_queue = []
+ # NOTE: additional keys are used; :escapes, :uq_slurp_pattern, :newline_jump, :epp_*
+ @lexing_context = {
+ :brace_count => 0,
+ :after => nil,
+ }
+ end
+
+ # Scans all of the content and returns it in an array
+ # Note that the terminating [false, false] token is included in the result.
+ #
+ def fullscan
+ result = []
+ scan {|token, value| result.push([token, value]) }
+ result
+ end
+
+ # A block must be passed to scan. It will be called with two arguments, a symbol for the token,
+ # and an instance of LexerSupport::TokenValue
+ # PERFORMANCE NOTE: The TokenValue is designed to reduce the amount of garbage / temporary data
+ # and to only convert the lexer's internal tokens on demand. It is slightly more costly to create an
+ # instance of a class defined in Ruby than an Array or Hash, but the gain is much bigger since transformation
+ # logic is avoided for many of its members (most are never used (e.g. line/pos information which is only of
+ # value in general for error messages, and for some expressions (which the lexer does not know about).
+ #
+ def scan
+ # PERFORMANCE note: it is faster to access local variables than instance variables.
+ # This makes a small but notable difference since instance member access is avoided for
+ # every token in the lexed content.
+ #
+ scn = @scanner
+ ctx = @lexing_context
+ queue = @token_queue
+
+ lex_error_without_pos("Internal Error: No string or file given to lexer to process.") unless scn
+
+ scn.skip(PATTERN_WS)
+
+ # This is the lexer's main loop
+ until queue.empty? && scn.eos? do
+ if token = queue.shift || lex_token
+ yield [ ctx[:after] = token[0], token[1] ]
+ end
+ end
+
+ # Signals end of input
+ yield [false, false]
+ end
+
+ # This lexes one token at the current position of the scanner.
+ # PERFORMANCE NOTE: Any change to this logic should be performance measured.
+ #
+ def lex_token
+ # Using three char look ahead (may be faster to do 2 char look ahead since only 2 tokens require a third
+ scn = @scanner
+ ctx = @lexing_context
+ before = @scanner.pos
+
+ # A look ahead of 3 characters is used since the longest operator ambiguity is resolved at that point.
+ # PERFORMANCE NOTE: It is faster to peek once and use three separate variables for lookahead 0, 1 and 2.
+ #
+ la = scn.peek(3)
+ return nil if la.empty?
+
+ # Ruby 1.8.7 requires using offset and length (or integers are returned.
+ # PERFORMANCE NOTE.
+ # It is slightly faster to use these local variables than accessing la[0], la[1] etc. in ruby 1.9.3
+ # But not big enough to warrant two completely different implementations.
+ #
+ la0 = la[0,1]
+ la1 = la[1,1]
+ la2 = la[2,1]
+
+ # PERFORMANCE NOTE:
+ # A case when, where all the cases are literal values is the fastest way to map from data to code.
+ # It is much faster than using a hash with lambdas, hash with symbol used to then invoke send etc.
+ # This case statement is evaluated for most character positions in puppet source, and great care must
+ # be taken to not introduce performance regressions.
+ #
+ case la0
+
+ when '.'
+ emit(TOKEN_DOT, before)
+
+ when ','
+ emit(TOKEN_COMMA, before)
+
+ when '['
+ if ctx[:after] == :NAME && (before == 0 || scn.string[before-1,1] =~ /[[:blank:]\r\n]+/)
+ emit(TOKEN_LISTSTART, before)
+ else
+ emit(TOKEN_LBRACK, before)
+ end
+
+ when ']'
+ emit(TOKEN_RBRACK, before)
+
+ when '('
+ emit(TOKEN_LPAREN, before)
+
+ when ')'
+ emit(TOKEN_RPAREN, before)
+
+ when ';'
+ emit(TOKEN_SEMIC, before)
+
+ when '?'
+ emit(TOKEN_QMARK, before)
+
+ when '*'
+ emit(TOKEN_TIMES, before)
+
+ when '%'
+ if la1 == '>' && ctx[:epp_mode]
+ scn.pos += 2
+ if ctx[:epp_mode] == :expr
+ enqueue_completed(TOKEN_EPPEND, before)
+ end
+ ctx[:epp_mode] = :text
+ interpolate_epp
+ else
+ emit(TOKEN_MODULO, before)
+ end
+
+ when '{'
+ # The lexer needs to help the parser since the technology used cannot deal with
+ # lookahead of same token with different precedence. This is solved by making left brace
+ # after ? into a separate token.
+ #
+ ctx[:brace_count] += 1
+ emit(if ctx[:after] == :QMARK
+ TOKEN_SELBRACE
+ else
+ TOKEN_LBRACE
+ end, before)
+
+ when '}'
+ ctx[:brace_count] -= 1
+ emit(TOKEN_RBRACE, before)
+
+ # TOKENS @, @@, @(
+ when '@'
+ case la1
+ when '@'
+ emit(TOKEN_ATAT, before) # TODO; Check if this is good for the grammar
+ when '('
+ heredoc
+ else
+ emit(TOKEN_AT, before)
+ end
+
+ # TOKENS |, |>, |>>
+ when '|'
+ emit(case la1
+ when '>'
+ la2 == '>' ? TOKEN_RRCOLLECT : TOKEN_RCOLLECT
+ else
+ TOKEN_PIPE
+ end, before)
+
+ # TOKENS =, =>, ==, =~
+ when '='
+ emit(case la1
+ when '='
+ TOKEN_ISEQUAL
+ when '>'
+ TOKEN_FARROW
+ when '~'
+ TOKEN_MATCH
+ else
+ TOKEN_EQUALS
+ end, before)
+
+ # TOKENS '+', '+=', and '+>'
+ when '+'
+ emit(case la1
+ when '='
+ TOKEN_APPENDS
+ when '>'
+ TOKEN_PARROW
+ else
+ TOKEN_PLUS
+ end, before)
+
+ # TOKENS '-', '->', and epp '-%>' (end of interpolation with trim)
+ when '-'
+ if ctx[:epp_mode] && la1 == '%' && la2 == '>'
+ scn.pos += 3
+ if ctx[:epp_mode] == :expr
+ enqueue_completed(TOKEN_EPPEND_TRIM, before)
+ end
+ interpolate_epp(:with_trim)
+ else
+ emit(case la1
+ when '>'
+ TOKEN_IN_EDGE
+ when '='
+ TOKEN_DELETES
+ else
+ TOKEN_MINUS
+ end, before)
+ end
+
+ # TOKENS !, !=, !~
+ when '!'
+ emit(case la1
+ when '='
+ TOKEN_NOTEQUAL
+ when '~'
+ TOKEN_NOMATCH
+ else
+ TOKEN_NOT
+ end, before)
+
+ # TOKENS ~>, ~
+ when '~'
+ emit(la1 == '>' ? TOKEN_IN_EDGE_SUB : TOKEN_TILDE, before)
+
+ when '#'
+ scn.skip(PATTERN_COMMENT)
+ nil
+
+ # TOKENS '/', '/*' and '/ regexp /'
+ when '/'
+ case la1
+ when '*'
+ scn.skip(PATTERN_MLCOMMENT)
+ nil
+
+ else
+ # regexp position is a regexp, else a div
+ if regexp_acceptable? && value = scn.scan(PATTERN_REGEX)
+ # Ensure an escaped / was not matched
+ while value[-2..-2] == STRING_BSLASH_BSLASH # i.e. \\
+ value += scn.scan_until(PATTERN_REGEX_END)
+ end
+ regex = value.sub(PATTERN_REGEX_A, '').sub(PATTERN_REGEX_Z, '').gsub(PATTERN_REGEX_ESC, '/')
+ emit_completed([:REGEX, Regexp.new(regex), scn.pos-before], before)
+ else
+ emit(TOKEN_DIV, before)
+ end
+ end
+
+ # TOKENS <, <=, <|, <<|, <<, <-, <~
+ when '<'
+ emit(case la1
+ when '<'
+ if la2 == '|'
+ TOKEN_LLCOLLECT
+ else
+ TOKEN_LSHIFT
+ end
+ when '='
+ TOKEN_LESSEQUAL
+ when '|'
+ TOKEN_LCOLLECT
+ when '-'
+ TOKEN_OUT_EDGE
+ when '~'
+ TOKEN_OUT_EDGE_SUB
+ else
+ TOKEN_LESSTHAN
+ end, before)
+
+ # TOKENS >, >=, >>
+ when '>'
+ emit(case la1
+ when '>'
+ TOKEN_RSHIFT
+ when '='
+ TOKEN_GREATEREQUAL
+ else
+ TOKEN_GREATERTHAN
+ end, before)
+
+ # TOKENS :, ::CLASSREF, ::NAME
+ when ':'
+ if la1 == ':'
+ before = scn.pos
+ # PERFORMANCE NOTE: This could potentially be speeded up by using a case/when listing all
+ # upper case letters. Alternatively, the 'A', and 'Z' comparisons may be faster if they are
+ # frozen.
+ #
+ if la2 >= 'A' && la2 <= 'Z'
+ # CLASSREF or error
+ value = scn.scan(PATTERN_CLASSREF)
+ if value
+ after = scn.pos
+ emit_completed([:CLASSREF, value, after-before], before)
+ else
+ # move to faulty position ('::<uc-letter>' was ok)
+ scn.pos = scn.pos + 3
+ lex_error("Illegal fully qualified class reference")
+ end
+ else
+ # NAME or error
+ value = scn.scan(PATTERN_NAME)
+ if value
+ emit_completed([:NAME, value, scn.pos-before], before)
+ else
+ # move to faulty position ('::' was ok)
+ scn.pos = scn.pos + 2
+ lex_error("Illegal fully qualified name")
+ end
+ end
+ else
+ emit(TOKEN_COLON, before)
+ end
+
+ when '$'
+ if value = scn.scan(PATTERN_DOLLAR_VAR)
+ emit_completed([:VARIABLE, value[1..-1], scn.pos - before], before)
+ else
+ # consume the $ and let higher layer complain about the error instead of getting a syntax error
+ emit(TOKEN_VARIABLE_EMPTY, before)
+ end
+
+ when '"'
+ # Recursive string interpolation, 'interpolate' either returns a STRING token, or
+ # a DQPRE with the rest of the string's tokens placed in the @token_queue
+ interpolate_dq
+
+ when "'"
+ emit_completed([:STRING, slurp_sqstring, before-scn.pos], before)
+
+ when '0', '1', '2', '3', '4', '5', '6', '7', '8', '9'
+ value = scn.scan(PATTERN_NUMBER)
+ if value
+ length = scn.pos - before
+ assert_numeric(value, length)
+ emit_completed([:NUMBER, value, length], before)
+ else
+ # move to faulty position ([0-9] was ok)
+ scn.pos = scn.pos + 1
+ lex_error("Illegal number")
+ end
+
+ when 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm',
+ 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z'
+ value = scn.scan(PATTERN_NAME)
+ # NAME or false start because followed by hyphen(s) and word
+ if value && !scn.match?(/-+\w/)
+ emit_completed(KEYWORDS[value] || [:NAME, value, scn.pos - before], before)
+ else
+ # Restart and check entire pattern (for ease of detecting non allowed trailing hyphen)
+ scn.pos = before
+ value = scn.scan(PATTERN_BARE_WORD)
+ if value
+ emit_completed([:STRING, value, scn.pos - before], before)
+ else
+ # move to faulty position ([a-z] was ok)
+ scn.pos = scn.pos + 1
+ lex_error("Illegal name")
+ end
+ end
+
+ when 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M',
+ 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z'
+ value = scn.scan(PATTERN_CLASSREF)
+ if value
+ emit_completed([:CLASSREF, value, scn.pos - before], before)
+ else
+ # move to faulty position ([A-Z] was ok)
+ scn.pos = scn.pos + 1
+ lex_error("Illegal class reference")
+ end
+
+ when "\n"
+ # If heredoc_cont is in effect there are heredoc text lines to skip over
+ # otherwise just skip the newline.
+ #
+ if ctx[:newline_jump]
+ scn.pos = ctx[:newline_jump]
+ ctx[:newline_jump] = nil
+ else
+ scn.pos += 1
+ end
+ return nil
+
+ when ' ', "\t", "\r"
+ scn.skip(PATTERN_WS)
+ return nil
+
+ else
+ # In case of unicode spaces of various kinds that are captured by a regexp, but not by the
+ # simpler case expression above (not worth handling those special cases with better performance).
+ if scn.skip(PATTERN_WS)
+ nil
+ else
+ # "unrecognized char"
+ emit([:OTHER, la0, 1], before)
+ end
+ end
+ end
+
+ # Emits (produces) a token [:tokensymbol, TokenValue] and moves the scanner's position past the token
+ #
+ def emit(token, byte_offset)
+ @scanner.pos = byte_offset + token[2]
+ [token[0], TokenValue.new(token, byte_offset, @locator)]
+ end
+
+ # Emits the completed token on the form [:tokensymbol, TokenValue. This method does not alter
+ # the scanner's position.
+ #
+ def emit_completed(token, byte_offset)
+ [token[0], TokenValue.new(token, byte_offset, @locator)]
+ end
+
+ # Enqueues a completed token at the given offset
+ def enqueue_completed(token, byte_offset)
+ @token_queue << emit_completed(token, byte_offset)
+ end
+
+ # Allows subprocessors for heredoc etc to enqueue tokens that are tokenized by a different lexer instance
+ #
+ def enqueue(emitted_token)
+ @token_queue << emitted_token
+ end
+
+ # Answers after which tokens it is acceptable to lex a regular expression.
+ # PERFORMANCE NOTE:
+ # It may be beneficial to turn this into a hash with default value of true for missing entries.
+ # A case expression with literal values will however create a hash internally. Since a reference is
+ # always needed to the hash, this access is almost as costly as a method call.
+ #
+ def regexp_acceptable?
+ case @lexing_context[:after]
+
+ # Ends of (potential) R-value generating expressions
+ when :RPAREN, :RBRACK, :RRCOLLECT, :RCOLLECT
+ false
+
+ # End of (potential) R-value - but must be allowed because of case expressions
+ # Called out here to not be mistaken for a bug.
+ when :RBRACE
+ true
+
+ # Operands (that can be followed by DIV (even if illegal in grammar)
+ when :NAME, :CLASSREF, :NUMBER, :STRING, :BOOLEAN, :DQPRE, :DQMID, :DQPOST, :HEREDOC, :REGEX
+ false
+
+ else
+ true
+ end
+ end
+
+end
diff --git a/lib/puppet/pops/parser/lexer_support.rb b/lib/puppet/pops/parser/lexer_support.rb
new file mode 100644
index 000000000..c769255a5
--- /dev/null
+++ b/lib/puppet/pops/parser/lexer_support.rb
@@ -0,0 +1,107 @@
+# This is an integral part of the Lexer. It is broken out into a separate module
+# for maintainability of the code, and making the various parts of the lexer focused.
+#
+module Puppet::Pops::Parser::LexerSupport
+
+ # Formats given message by appending file, line and position if available.
+ def positioned_message(msg, pos = nil)
+ result = [msg]
+ file = @locator.file
+ line = @locator.line_for_offset(pos || @scanner.pos)
+ pos = @locator.pos_on_line(pos || @scanner.pos)
+
+ result << "in file #{file}" if file && file.is_a?(String) && !file.empty?
+ result << "at line #{line}:#{pos}"
+ result.join(" ")
+ end
+
+ # Returns "<eof>" if at end of input, else the following 5 characters with \n \r \t escaped
+ def followed_by
+ return "<eof>" if @scanner.eos?
+ result = @scanner.rest[0,5] + "..."
+ result.gsub!("\t", '\t')
+ result.gsub!("\n", '\n')
+ result.gsub!("\r", '\r')
+ result
+ end
+
+ # Returns a quoted string using " or ' depending on the given a strings's content
+ def format_quote(q)
+ if q == "'"
+ '"\'"'
+ else
+ "'#{q}'"
+ end
+ end
+
+ # Raises a Puppet::LexError with the given message
+ def lex_error_without_pos msg
+ raise Puppet::LexError.new(msg)
+ end
+
+ # Raises a Puppet::LexError with the given message
+ def lex_error(msg, pos=nil)
+ raise Puppet::LexError.new(positioned_message(msg, pos))
+ end
+
+ # Asserts that the given string value is a float, or an integer in decimal, octal or hex form.
+ # An error is raised if the given value does not comply.
+ #
+ def assert_numeric(value, length)
+ if value =~ /^0[xX].*$/
+ lex_error("Not a valid hex number #{value}", length) unless value =~ /^0[xX][0-9A-Fa-f]+$/
+
+ elsif value =~ /^0[^.].*$/
+ lex_error("Not a valid octal number #{value}", length) unless value =~ /^0[0-7]+$/
+
+ else
+ lex_error("Not a valid decimal number #{value}", length) unless value =~ /0?\d+(?:\.\d+)?(?:[eE]-?\d+)?/
+ end
+ end
+
+ # A TokenValue keeps track of the token symbol, the lexed text for the token, its length
+ # and its position in its source container. There is a cost associated with computing the
+ # line and position on line information.
+ #
+ class TokenValue < Puppet::Pops::Parser::Locatable
+ attr_reader :token_array
+ attr_reader :offset
+ attr_reader :locator
+
+ def initialize(token_array, offset, locator)
+ @token_array = token_array
+ @offset = offset
+ @locator = locator
+ end
+
+ def length
+ @token_array[2]
+ end
+
+ def [](key)
+ case key
+ when :value
+ @token_array[1]
+ when :file
+ @locator.file
+ when :line
+ @locator.line_for_offset(@offset)
+ when :pos
+ @locator.pos_on_line(@offset)
+ when :length
+ @token_array[2]
+ when :locator
+ @locator
+ when :offset
+ @offset
+ else
+ nil
+ end
+ end
+
+ # TODO: Make this comparable for testing
+ # vs symbolic, vs array with symbol and non hash, array with symbol and hash)
+ #
+ end
+
+end
diff --git a/lib/puppet/pops/parser/locatable.rb b/lib/puppet/pops/parser/locatable.rb
new file mode 100644
index 000000000..45fc5cecd
--- /dev/null
+++ b/lib/puppet/pops/parser/locatable.rb
@@ -0,0 +1,23 @@
+# Interface for something that is "locateable" (holds offset and length).
+class Puppet::Pops::Parser::Locatable
+
+ # The offset in the locator's content
+ def offset
+ end
+
+ # The length in the locator from the given offset
+ def length
+ end
+
+ # This class is useful for testing
+ class Fixed < Puppet::Pops::Parser::Locatable
+ attr_reader :offset
+ attr_reader :length
+
+ def initialize(offset, length)
+ @offset = offset
+ @length = length
+ end
+ end
+
+end
diff --git a/lib/puppet/pops/parser/locator.rb b/lib/puppet/pops/parser/locator.rb
new file mode 100644
index 000000000..526126aca
--- /dev/null
+++ b/lib/puppet/pops/parser/locator.rb
@@ -0,0 +1,291 @@
+# Helper class that keeps track of where line breaks are located and can answer questions about positions.
+#
+class Puppet::Pops::Parser::Locator
+
+ RUBY_1_9_3 = (1 << 16 | 9 << 8 | 3)
+ RUBY_2_0_0 = (2 << 16 | 0 << 8 | 0)
+ RUBYVER_ARRAY = RUBY_VERSION.split(".").collect {|s| s.to_i }
+ RUBYVER = (RUBYVER_ARRAY[0] << 16 | RUBYVER_ARRAY[1] << 8 | RUBYVER_ARRAY[2])
+
+ # Computes a symbol representing which ruby runtime this is running on
+ # This implementation will fail if there are more than 255 minor or micro versions of ruby
+ #
+ def self.locator_version
+ if RUBYVER >= RUBY_2_0_0
+ :ruby20
+ elsif RUBYVER >= RUBY_1_9_3
+ :ruby19
+ else
+ :ruby18
+ end
+ end
+ LOCATOR_VERSION = locator_version
+
+ # Constant set to true if multibyte is supported (includes multibyte extended regular expressions)
+ MULTIBYTE = !!(LOCATOR_VERSION == :ruby19 || LOCATOR_VERSION == :ruby20)
+
+ # Creates, or recreates a Locator. A Locator is created if index is not given (a scan is then
+ # performed of the given source string.
+ #
+ def self.locator(string, file, index = nil)
+ case LOCATOR_VERSION
+ when :ruby20, :ruby19
+ Locator19.new(string, file, index)
+ else
+ Locator18.new(string, file, index)
+ end
+ end
+
+ # Returns the file name associated with the string content
+ def file
+ end
+
+ # Returns the string content
+ def string
+ end
+
+ # Returns the position on line (first position on a line is 1)
+ def pos_on_line(offset)
+ end
+
+ # Returns the line number (first line is 1) for the given offset
+ def line_for_offset(offset)
+ end
+
+ # Returns the offset on line (first offset on a line is 0).
+ #
+ def offset_on_line(offset)
+ end
+
+ # Returns the character offset for a given reported offset
+ def char_offset(byte_offset)
+ end
+
+ # Returns the length measured in number of characters from the given start and end reported offseta
+ def char_length(offset, end_offset)
+ end
+
+ # Returns the line index - an array of line offsets for the start position of each line, starting at 0 for
+ # the first line.
+ #
+ def line_index()
+ end
+
+ # A Sublocator locates a concrete locator (subspace) in a virtual space.
+ # The `leading_line_count` is the (virtual) number of lines preceding the first line in the concrete locator.
+ # The `leading_offset` is the (virtual) byte offset of the first byte in the concrete locator.
+ # The `leading_line_offset` is the (virtual) offset / margin in characters for each line.
+ #
+ # This illustrates characters in the sublocator (`.`) inside the subspace (`X`):
+ #
+ # 1:XXXXXXXX
+ # 2:XXXX.... .. ... ..
+ # 3:XXXX. . .... ..
+ # 4:XXXX............
+ #
+ # This sublocator would be configured with leading_line_count = 1,
+ # leading_offset=8, and leading_line_offset=4
+ #
+ # Note that leading_offset must be the same for all lines and measured in characters.
+ #
+ class SubLocator < Puppet::Pops::Parser::Locator
+ attr_reader :locator
+ attr_reader :leading_line_count
+ attr_reader :leading_offset
+ attr_reader :leading_line_offset
+
+ def self.sub_locator(string, file, leading_line_count, leading_offset, leading_line_offset)
+ self.new(Puppet::Pops::Parser::Locator.locator(string, file),
+ leading_line_count,
+ leading_offset,
+ leading_line_offset)
+ end
+
+ def initialize(locator, leading_line_count, leading_offset, leading_line_offset)
+ @locator = locator
+ @leading_line_count = leading_line_count
+ @leading_offset = leading_offset
+ @leading_line_offset = leading_line_offset
+ end
+
+ def file
+ @locator.file
+ end
+
+ def string
+ @locator.string
+ end
+
+ # Given offset is offset in the subspace
+ def line_for_offset(offset)
+ @locator.line_for_offset(offset) + @leading_line_count
+ end
+
+ # Given offset is offset in the subspace
+ def offset_on_line(offset)
+ @locator.offset_on_line(offset) + @leading_line_offset
+ end
+
+ # Given offset is offset in the subspace
+ def char_offset(offset)
+ effective_line = @locator.line_for_offset(offset)
+ locator.char_offset(offset) + (effective_line * @leading_line_offset) + @leading_offset
+ end
+
+ # Given offsets are offsets in the subspace
+ def char_length(offset, end_offset)
+ effective_line = @locator.line_for_offset(end_offset) - @locator.line_for_offset(offset)
+ locator.char_length(offset, end_offset) + (effective_line * @leading_line_offset)
+ end
+
+ def pos_on_line(offset)
+ offset_on_line(offset) +1
+ end
+ end
+
+ private
+
+ class AbstractLocator < Puppet::Pops::Parser::Locator
+ attr_accessor :line_index
+ attr_accessor :string
+ attr_accessor :prev_offset
+ attr_accessor :prev_line
+ attr_reader :string
+ attr_reader :file
+
+ # Create a locator based on a content string, and a boolean indicating if ruby version support multi-byte strings
+ # or not.
+ #
+ def initialize(string, file, index = nil)
+ @string = string.freeze
+ @file = file.freeze
+ @prev_offset = nil
+ @prev_line = nil
+ @line_index = index
+ compute_line_index unless !index.nil?
+ end
+
+ # Returns the position on line (first position on a line is 1)
+ def pos_on_line(offset)
+ offset_on_line(offset) +1
+ end
+
+ def to_location_hash(reported_offset, end_offset)
+ pos = pos_on_line(reported_offset)
+ offset = char_offset(reported_offset)
+ length = char_length(reported_offset, end_offset)
+ start_line = line_for_offset(reported_offset)
+ { :line => start_line, :pos => pos, :offset => offset, :length => length}
+ end
+
+ # Returns the index of the smallest item for which the item > the given value
+ # This is a min binary search. Although written in Ruby it is only slightly slower than
+ # the corresponding method in C in Ruby 2.0.0 - the main benefit to use this method over
+ # the Ruby C version is that it returns the index (not the value) which means there is not need
+ # to have an additional structure to get the index (or record the index in the structure). This
+ # saves both memory and CPU. It also does not require passing a block that is called since this
+ # method is specialized to search the line index.
+ #
+ def ary_bsearch_i(ary, value)
+ low = 0
+ high = ary.length
+ mid = nil
+ smaller = false
+ satisfied = false
+ v = nil
+
+ while low < high do
+ mid = low + ((high - low) / 2)
+ v = (ary[mid] > value)
+ if v == true
+ satisfied = true
+ smaller = true
+ elsif !v
+ smaller = false
+ else
+ raise TypeError, "wrong argument, must be boolean or nil, got '#{v.class}'"
+ end
+
+ if smaller
+ high = mid
+ else
+ low = mid + 1;
+ end
+ end
+
+ return nil if low == ary.length
+ return nil if !satisfied
+ return low
+ end
+
+ # Common impl for 18 and 19 since scanner is byte based
+ def compute_line_index
+ scanner = StringScanner.new(string)
+ result = [0] # first line starts at 0
+ while scanner.scan_until(/\n/)
+ result << scanner.pos
+ end
+ self.line_index = result.freeze
+ end
+
+ # Returns the line number (first line is 1) for the given offset
+ def line_for_offset(offset)
+ if prev_offset == offset
+ # use cache
+ return prev_line
+ end
+ if line_nbr = ary_bsearch_i(line_index, offset)
+ # cache
+ prev_offset = offset
+ prev_line = line_nbr
+ return line_nbr
+ end
+ # If not found it is after last
+ # clear cache
+ prev_offset = prev_line = nil
+ return line_index.size
+ end
+ end
+
+ class Locator18 < AbstractLocator
+
+ def offset_on_line(offset)
+ line_offset = line_index[ line_for_offset(offset)-1 ]
+ offset - line_offset
+ end
+
+ def char_offset(char_offset)
+ char_offset
+ end
+
+ def char_length(offset, end_offset)
+ end_offset - offset
+ end
+
+ end
+
+ # This implementation is for Ruby19 and Ruby20. It uses byteslice to get strings from byte based offsets.
+ # For Ruby20 this is faster than using the Stringscanner.charpos method (byteslice outperforms it, when
+ # strings are frozen).
+ #
+ class Locator19 < AbstractLocator
+
+ # Returns the offset on line (first offset on a line is 0).
+ # Ruby 19 is multibyte but has no character position methods, must use byteslice
+ def offset_on_line(offset)
+ line_offset = line_index[ line_for_offset(offset)-1 ]
+ string.byteslice(line_offset, offset-line_offset).length
+ end
+
+ # Returns the character offset for a given byte offset
+ # Ruby 19 is multibyte but has no character position methods, must use byteslice
+ def char_offset(byte_offset)
+ string.byteslice(0, byte_offset).length
+ end
+
+ # Returns the length measured in number of characters from the given start and end byte offseta
+ def char_length(offset, end_offset)
+ string.byteslice(offset, end_offset - offset).length
+ end
+ end
+end
diff --git a/lib/puppet/pops/parser/makefile b/lib/puppet/pops/parser/makefile
index f521747cb..802382dd8 100644
--- a/lib/puppet/pops/parser/makefile
+++ b/lib/puppet/pops/parser/makefile
@@ -1,13 +1,6 @@
-#parser.rb: grammar.ra
-# racc -o$@ grammar.ra
-#
-#grammar.output: grammar.ra
-# racc -v -o$@ grammar.ra
-#
-
eparser.rb: egrammar.ra
racc -o$@ egrammar.ra
egrammar.output: egrammar.ra
- racc -v -o$@ egrammar.ra
\ No newline at end of file
+ racc -v -o$@ egrammar.ra
diff --git a/lib/puppet/pops/parser/parser_support.rb b/lib/puppet/pops/parser/parser_support.rb
index a22de7161..a351048d3 100644
--- a/lib/puppet/pops/parser/parser_support.rb
+++ b/lib/puppet/pops/parser/parser_support.rb
@@ -1,203 +1,231 @@
require 'puppet/parser/functions'
require 'puppet/parser/files'
require 'puppet/resource/type_collection'
require 'puppet/resource/type_collection_helper'
require 'puppet/resource/type'
require 'monitor'
# Supporting logic for the parser.
# This supporting logic has slightly different responsibilities compared to the original Puppet::Parser::Parser.
# It is only concerned with parsing.
#
class Puppet::Pops::Parser::Parser
# Note that the name of the contained class and the file name (currently parser_support.rb)
# needs to be different as the class is generated by Racc, and this file (parser_support.rb) is included as a mix in
#
# Simplify access to the Model factory
# Note that the parser/parser support does not have direct knowledge about the Model.
# All model construction/manipulation is made by the Factory.
#
Factory = Puppet::Pops::Model::Factory
Model = Puppet::Pops::Model
include Puppet::Resource::TypeCollectionHelper
attr_accessor :lexer
+ attr_reader :definitions
# Returns the token text of the given lexer token, or nil, if token is nil
def token_text t
return t if t.nil?
t = t.current if t.respond_to?(:current)
return t.value if t.is_a? Model::QualifiedName
# else it is a lexer token
t[:value]
end
# Produces the fully qualified name, with the full (current) namespace for a given name.
#
# This is needed because class bodies are lazily evaluated and an inner class' container(s) may not
# have been evaluated before some external reference is made to the inner class; its must therefore know its complete name
# before evaluation-time.
#
def classname(name)
- [@lexer.namespace, name].join("::").sub(/^::/, '')
+ [namespace, name].join("::").sub(/^::/, '')
end
- # Reinitializes variables (i.e. creates a new lexer instance
- #
- def clear
- initvars
- end
+# # Reinitializes variables (i.e. creates a new lexer instance
+# #
+# def clear
+# initvars
+# end
# Raises a Parse error.
- def error(message, options = {})
+ def error(value, message, options = {})
except = Puppet::ParseError.new(message)
- except.line = options[:line] || @lexer.line
- except.file = options[:file] || @lexer.file
- except.pos = options[:pos] || @lexer.pos
+ except.line = options[:line] || value[:line]
+ except.file = options[:file] || value[:file] # @lexer.file
+ except.pos = options[:pos] || value[:pos] # @lexer.pos
raise except
end
# Parses a file expected to contain pp DSL logic.
def parse_file(file)
- unless Puppet::FileSystem::File.exist?(file)
+ unless Puppet::FileSystem.exist?(file)
unless file =~ /\.pp$/
file = file + ".pp"
end
end
@lexer.file = file
_parse()
end
def initialize()
# Since the parser is not responsible for importing (removed), and does not perform linking,
# and there is no syntax that requires knowing if something referenced exists, it is safe
# to assume that no environment is needed when parsing. (All that comes later).
#
- initvars
+ @lexer = Puppet::Pops::Parser::Lexer2.new
+ @namestack = []
+ @definitions = []
end
- # Initializes the parser support by creating a new instance of {Puppet::Pops::Parser::Lexer}
- # @return [void]
- #
- def initvars
- @lexer = Puppet::Pops::Parser::Lexer.new
- end
+# # Initializes the parser support by creating a new instance of {Puppet::Pops::Parser::Lexer}
+# # @return [void]
+# #
+# def initvars
+# end
# This is a callback from the generated grammar (when an error occurs while parsing)
# TODO Picks up origin information from the lexer, probably needs this from the caller instead
# (for code strings, and when start line is not line 1 in a code string (or file), etc.)
#
def on_error(token,value,stack)
if token == 0 # denotes end of file
- value = 'end of file'
+ value_at = 'end of file'
else
- value = "'#{value[:value]}'"
+ value_at = "'#{value[:value]}'"
end
- error = "Syntax error at #{value}"
+ error = "Syntax error at #{value_at}"
# The 'expected' is only of value at end of input, otherwise any parse error involving a
- # start of a pair will be reported as expecting the close of the pair - e.g. "$x.each |$x {", would
+ # start of a pair will be reported as expecting the close of the pair - e.g. "$x.each |$x {|", would
# report that "seeing the '{', the '}' is expected. That would be wrong.
# Real "expected" tokens are very difficult to compute (would require parsing of racc output data). Output of the stack
# could help, but can require extensive backtracking and produce many options.
#
- if token == 0 && brace = @lexer.expected
- error += "; expected '#{brace}'"
- end
+ # The lexer should handle the "expected instead of end of file for strings, and interpolation", other expectancies
+ # must be handled by the grammar. The lexer may have enqueued tokens far ahead - the lexer's opinion about this
+ # is not trustworthy.
+ #
+# if token == 0 && brace = @lexer.expected
+# error += "; expected '#{brace}'"
+# end
except = Puppet::ParseError.new(error)
- except.line = @lexer.line
- except.file = @lexer.file if @lexer.file
- except.pos = @lexer.pos
+ if token != 0
+ path = value[:file]
+ except.line = value[:line]
+ except.pos = value[:pos]
+ else
+ # At end of input, use what the lexer thinks is the source file
+ path = lexer.file
+ end
+ except.file = path if path.is_a?(String) && !path.empty?
raise except
end
# Parses a String of pp DSL code.
# @todo make it possible to pass a given origin
#
def parse_string(code)
@lexer.string = code
_parse()
end
# Mark the factory wrapped model object with location information
# @todo the lexer produces :line for token, but no offset or length
# @return [Puppet::Pops::Model::Factory] the given factory
# @api private
#
- def loc(factory, start_token, end_token = nil)
- factory.record_position(sourcepos(start_token), sourcepos(end_token))
+ def loc(factory, start_locateable, end_locateable = nil)
+ factory.record_position(start_locateable, end_locateable)
+ end
+
+ def heredoc_loc(factory, start_locateabke, end_locateable = nil)
+ factory.record_heredoc_position(start_locatable, end_locatable)
end
# Associate documentation with the factory wrapped model object.
# @return [Puppet::Pops::Model::Factory] the given factory
# @api private
def doc factory, doc_string
factory.doc = doc_string
end
- def sourcepos(o)
- if !o
- Puppet::Pops::Adapters::SourcePosAdapter.new
- elsif o.is_a? Puppet::Pops::Model::Factory
- # It is a built model element with loc set returns start at pos 0
- o.loc
- else
- loc = Puppet::Pops::Adapters::SourcePosAdapter.new
- # It must be a token
- loc.line = o[:line]
- loc.pos = o[:pos]
- loc.offset = o[:offset]
- loc.length = o[:length]
- loc
- end
- end
-
def aryfy(o)
o = [o] unless o.is_a?(Array)
o
end
+ def namespace
+ @namestack.join('::')
+ end
+
+ def namestack(name)
+ @namestack << name
+ end
+
+ def namepop()
+ @namestack.pop
+ end
+
+ def add_definition(definition)
+ @definitions << definition.current
+ definition
+ end
+
# Transforms an array of expressions containing literal name expressions to calls if followed by an
# expression, or expression list
#
def transform_calls(expressions)
Factory.transform_calls(expressions)
end
+ # Transforms a LEFT followed by the result of attribute_operations, this may be a call or an invalid sequence
+ def transform_resource_wo_title(left, resource)
+ Factory.transform_resource_wo_title(left, resource)
+ end
+
+ # If there are definitions that require initialization a Program is produced, else the body
+ def create_program(body)
+ locator = @lexer.locator
+ Factory.PROGRAM(body, definitions, locator)
+ end
+
# Performs the parsing and returns the resulting model.
# The lexer holds state, and this is setup with {#parse_string}, or {#parse_file}.
#
# TODO: Drop support for parsing a ruby file this way (should be done where it is decided
# which file to load/run (i.e. loaders), and initial file to run
# TODO: deal with options containing origin (i.e. parsing a string from externally known location).
# TODO: should return the model, not a Hostclass
#
# @api private
#
def _parse()
begin
@yydebug = false
main = yyparse(@lexer,:scan)
# #Commented out now because this hides problems in the racc grammar while developing
# # TODO include this when test coverage is good enough.
# rescue Puppet::ParseError => except
# except.line ||= @lexer.line
# except.file ||= @lexer.file
# except.pos ||= @lexer.pos
# raise except
# rescue => except
# raise Puppet::ParseError.new(except.message, @lexer.file, @lexer.line, @lexer.pos, except)
end
- main.record_origin(@lexer.file) if main
return main
ensure
@lexer.clear
+ @namestack = []
+ @definitions = []
end
end
diff --git a/lib/puppet/pops/parser/slurp_support.rb b/lib/puppet/pops/parser/slurp_support.rb
new file mode 100644
index 000000000..a93bbb7b9
--- /dev/null
+++ b/lib/puppet/pops/parser/slurp_support.rb
@@ -0,0 +1,95 @@
+# This module is an integral part of the Lexer.
+# It defines the string slurping behavior - finding the string and non string parts in interpolated
+# strings, translating escape sequences in strings to their single character equivalence.
+#
+# PERFORMANCE NOTE: The various kinds of slurping could be made even more generic, but requires
+# additional parameter passing and evaluation of conditional logic.
+# TODO: More detailed performance analysis of excessive character escaping and interpolation.
+#
+module Puppet::Pops::Parser::SlurpSupport
+
+ SLURP_SQ_PATTERN = /(?:[^\\]|^|[^\\])(?:[\\]{2})*[']/
+ SLURP_DQ_PATTERN = /(?:[^\\]|^|[^\\])(?:[\\]{2})*(["]|[$]\{?)/
+ SLURP_UQ_PATTERN = /(?:[^\\]|^|[^\\])(?:[\\]{2})*([$]\{?|\z)/
+ SLURP_ALL_PATTERN = /.*(\z)/m
+ SQ_ESCAPES = %w{ \\ ' }
+ DQ_ESCAPES = %w{ \\ $ ' " r n t s u}+["\r\n", "\n"]
+ UQ_ESCAPES = %w{ \\ $ r n t s u}+["\r\n", "\n"]
+
+ def slurp_sqstring
+ # skip the leading '
+ @scanner.pos += 1
+ str = slurp(@scanner, SLURP_SQ_PATTERN, SQ_ESCAPES, :ignore_invalid_escapes) || lex_error("Unclosed quote after \"'\" followed by '#{followed_by}'")
+ str[0..-2] # strip closing "'" from result
+ end
+
+ def slurp_dqstring
+ scn = @scanner
+ last = scn.matched
+ str = slurp(scn, SLURP_DQ_PATTERN, DQ_ESCAPES, false)
+ unless str
+ lex_error("Unclosed quote after #{format_quote(last)} followed by '#{followed_by}'")
+ end
+
+ # Terminator may be a single char '"', '$', or two characters '${' group match 1 (scn[1]) from the last slurp holds this
+ terminator = scn[1]
+ [str[0..(-1 - terminator.length)], terminator]
+ end
+
+ # Copy from old lexer - can do much better
+ def slurp_uqstring
+ scn = @scanner
+ last = scn.matched
+ ignore = true
+ str = slurp(scn, @lexing_context[:uq_slurp_pattern], @lexing_context[:escapes], :ignore_invalid_escapes)
+
+ # Terminator may be a single char '$', two characters '${', or empty string '' at the end of intput.
+ # Group match 1 holds this.
+ # The exceptional case is found by looking at the subgroup 1 of the most recent match made by the scanner (i.e. @scanner[1]).
+ # This is the last match made by the slurp method (having called scan_until on the scanner).
+ # If there is a terminating character is must be stripped and returned separately.
+ #
+ terminator = scn[1]
+ [str[0..(-1 - terminator.length)], terminator]
+ end
+
+ # Slurps a string from the given scanner until the given pattern and then replaces any escaped
+ # characters given by escapes into their control-character equivalent or in case of line breaks, replaces the
+ # pattern \r?\n with an empty string.
+ # The returned string contains the terminating character. Returns nil if the scanner can not scan until the given
+ # pattern.
+ #
+ def slurp(scanner, pattern, escapes, ignore_invalid_escapes)
+ str = scanner.scan_until(pattern) || return
+
+ # Process unicode escapes first as they require getting 4 hex digits
+ # If later a \u is found it is warned not to be a unicode escape
+ if escapes.include?('u')
+ str.gsub!(/\\u([\da-fA-F]{4})/m) {
+ [$1.hex].pack("U")
+ }
+ end
+
+ str.gsub!(/\\([^\r\n]|(?:\r?\n))/m) {
+ ch = $1
+ if escapes.include? ch
+ case ch
+ when 'r' ; "\r"
+ when 'n' ; "\n"
+ when 't' ; "\t"
+ when 's' ; " "
+ when 'u'
+ Puppet.warning(positioned_message("Unicode escape '\\u' was not followed by 4 hex digits"))
+ "\\u"
+ when "\n" ; ''
+ when "\r\n"; ''
+ else ch
+ end
+ else
+ Puppet.warning(positioned_message("Unrecognized escape sequence '\\#{ch}'")) unless ignore_invalid_escapes
+ "\\#{ch}"
+ end
+ }
+ str
+ end
+end
diff --git a/lib/puppet/pops/patterns.rb b/lib/puppet/pops/patterns.rb
index b1c76ad43..a2534774d 100644
--- a/lib/puppet/pops/patterns.rb
+++ b/lib/puppet/pops/patterns.rb
@@ -1,35 +1,44 @@
# The Patterns module contains common regular expression patters for the Puppet DSL language
module Puppet::Pops::Patterns
# NUMERIC matches hex, octal, decimal, and floating point and captures three parts
# 0 = entire matched number, leading and trailing whitespace included
# 1 = hexadecimal number
# 2 = non hex integer portion, possibly with leading 0 (octal)
# 3 = floating point part, starts with ".", decimals and optional exponent
#
# Thus, a hex number has group 1 value, an octal value has group 2 (if it starts with 0), and no group 3
# and a floating point value has group 2 and group 3.
#
NUMERIC = %r{^\s*(?:(0[xX][0-9A-Fa-f]+)|(0?\d+)((?:\.\d+)?(?:[eE]-?\d+)?))\s*$}
# ILLEGAL_P3_1_HOSTNAME matches if a hostname contains illegal characters.
# This check does not prevent pathological names like 'a....b', '.....', "---". etc.
ILLEGAL_HOSTNAME_CHARS = %r{[^-\w.]}
# 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((::)?[a-z0-9]\w*)(::[a-z0-9]\w*)*\z}
+ NAME = %r{\A((::)?[a-z]\w*)(::[a-z]\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{\A((::){0,1}[A-Z][-\w]*)+\z}
+ CLASSREF_EXT = %r{\A((::){0,1}[A-Z][\w]*)+\z}
# 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{\A((::){0,1}[a-z][-\w]*)+\z}
+ CLASSREF = %r{\A((::){0,1}[a-z][\w]*)+\z}
+
+ # DOLLAR_VAR matches a variable name including the initial $ character
+ DOLLAR_VAR = %r{\$(::)?(\w+::)*\w+}
+
+ # VAR_NAME matches the name part of a variable (The $ character is not included)
+ # Note, that only the final segment may start with an underscore.
+ VAR_NAME = %r{\A(:?(::)?[a-z]\w*)*(:?(::)?[a-z_]\w*)\z}
+
+ # A Numeric var name must be the decimal number 0, or a decimal number not starting with 0
+ NUMERIC_VAR_NAME = %r{\A(?:0|(?:[1-9][0-9]*))\z}
end
diff --git a/lib/puppet/pops/types/class_loader.rb b/lib/puppet/pops/types/class_loader.rb
index abacd704e..0cd1b8c2f 100644
--- a/lib/puppet/pops/types/class_loader.rb
+++ b/lib/puppet/pops/types/class_loader.rb
@@ -1,118 +1,118 @@
require 'rgen/metamodel_builder'
# The ClassLoader provides a Class instance given a class name or a meta-type.
# If the class is not already loaded, it is loaded using the Puppet Autoloader.
# This means it can load a class from a gem, or from puppet modules.
#
class Puppet::Pops::Types::ClassLoader
@autoloader = Puppet::Util::Autoload.new("ClassLoader", "", :wrap => false)
# Returns a Class given a fully qualified class name.
# Lookup of class is never relative to the calling namespace.
# @param name [String, Array<String>, Array<Symbol>, Puppet::Pops::Types::PObjectType] A fully qualified
# class name String (e.g. '::Foo::Bar', 'Foo::Bar'), a PObjectType, or a fully qualified name in Array form where each part
# is either a String or a Symbol, e.g. `%w{Puppetx Puppetlabs SomeExtension}`.
# @return [Class, nil] the looked up class or nil if no such class is loaded
# @raise ArgumentError If the given argument has the wrong type
# @api public
#
def self.provide(name)
case name
when String
provide_from_string(name)
when Array
provide_from_name_path(name.join('::'), name)
when Puppet::Pops::Types::PObjectType, Puppet::Pops::Types::PType
provide_from_type(name)
else
raise ArgumentError, "Cannot provide a class from a '#{name.class.name}'"
end
end
private
def self.provide_from_type(type)
case type
when Puppet::Pops::Types::PRubyType
provide_from_string(type.ruby_class)
when Puppet::Pops::Types::PBooleanType
# There is no other thing to load except this Enum meta type
RGen::MetamodelBuilder::MMBase::Boolean
when Puppet::Pops::Types::PType
# TODO: PType should have a type argument (a PObjectType)
Class
# Although not expected to be the first choice for getting a concrete class for these
# types, these are of value if the calling logic just has a reference to type.
#
when Puppet::Pops::Types::PArrayType ; Array
when Puppet::Pops::Types::PHashType ; Hash
- when Puppet::Pops::Types::PPatternType ; Regexp
+ when Puppet::Pops::Types::PRegexpType ; Regexp
when Puppet::Pops::Types::PIntegerType ; Integer
when Puppet::Pops::Types::PStringType ; String
when Puppet::Pops::Types::PFloatType ; Float
when Puppet::Pops::Types::PNilType ; NilClass
else
nil
end
end
def self.provide_from_string(name)
name_path = name.split('::')
# always from the root, so remove an empty first segment
if name_path[0].empty?
name_path = name_path[1..-1]
end
provide_from_name_path(name, name_path)
end
def self.provide_from_name_path(name, name_path)
# If class is already loaded, try this first
result = find_class(name_path)
unless result.is_a?(Class)
# Attempt to load it using the auto loader
loaded_path = nil
if paths_for_name(name).find {|path| loaded_path = path; @autoloader.load(path) }
result = find_class(name_path)
unless result.is_a?(Class)
raise RuntimeError, "Loading of #{name} using relative path: '#{loaded_path}' did not create expected class"
end
end
end
return nil unless result.is_a?(Class)
result
end
def self.find_class(name_path)
name_path.reduce(Object) do |ns, name|
begin
ns.const_get(name)
rescue NameError
return nil
end
end
end
def self.paths_for_name(fq_name)
[de_camel(fq_name), downcased_path(fq_name)]
end
def self.downcased_path(fq_name)
fq_name.to_s.gsub(/::/, '/').downcase
end
def self.de_camel(fq_name)
fq_name.to_s.gsub(/::/, '/').
gsub(/([A-Z]+)([A-Z][a-z])/,'\1_\2').
gsub(/([a-z\d])([A-Z])/,'\1_\2').
tr("-", "_").
downcase
end
-end
\ No newline at end of file
+end
diff --git a/lib/puppet/pops/types/enumeration.rb b/lib/puppet/pops/types/enumeration.rb
new file mode 100644
index 000000000..76dc59b67
--- /dev/null
+++ b/lib/puppet/pops/types/enumeration.rb
@@ -0,0 +1,34 @@
+# The Enumeration class provides default Enumerable::Enumerator creation for Puppet Programming Language
+# runtime objects that supports the concept of enumeration.
+#
+class Puppet::Pops::Types::Enumeration
+ # Produces an Enumerable::Enumerator for Array, Hash, Integer, Integer Range, and String.
+ #
+ def self.enumerator(o)
+ @@singleton ||= new
+ @@singleton.enumerator(o)
+ end
+
+ # Produces an Enumerator for Array, Hash, Integer, Integer Range, and String.
+ #
+ def enumerator(o)
+ case o
+ when String
+ x = o.chars
+ # Ruby 1.8.7 returns Enumerable::Enumerator, Ruby 1.8.9 Enumerator, and 2.0.0 an Array
+ x.is_a?(Array) ? x.each : x
+ when Integer
+ o.times
+ when Array
+ o.each
+ when Hash
+ o.each
+ when Puppet::Pops::Types::PIntegerType
+ # Not enumerable if representing an infinite range
+ return nil if o.to.nil? || o.from.nil?
+ o.each
+ else
+ nil
+ end
+ end
+end
diff --git a/lib/puppet/pops/types/type_calculator.rb b/lib/puppet/pops/types/type_calculator.rb
index 3158a2cc5..b716cc350 100644
--- a/lib/puppet/pops/types/type_calculator.rb
+++ b/lib/puppet/pops/types/type_calculator.rb
@@ -1,557 +1,1482 @@
# The TypeCalculator can answer questions about puppet types.
#
# The Puppet type system is primarily based on sub-classing. When asking the type calculator to infer types from Ruby in general, it
# may not provide the wanted answer; it does not for instance take module inclusions and extensions into account. In general the type
# system should be unsurprising for anyone being exposed to the notion of type. The type `Data` may require a bit more explanation; this
-# is an abstract type that includes all literal types, as well as Array with an element type compatible with Data, and Hash with key
-# compatible with Literal and elements compatible with Data. Expressed differently; Data is what you typically express using JSON (with
-# the exception that the Puppet type system also includes Pattern (regular expression) as a literal.
+# is an abstract type that includes all scalar types, as well as Array with an element type compatible with Data, and Hash with key
+# compatible with scalar and elements compatible with Data. Expressed differently; Data is what you typically express using JSON (with
+# the exception that the Puppet type system also includes Pattern (regular expression) as a scalar.
#
# Inference
# ---------
-# The `infer(o)` method infers a Puppet type for literal Ruby objects, and for Arrays and Hashes.
+# The `infer(o)` method infers a Puppet type for scalar Ruby objects, and for Arrays and Hashes.
+# The inference result is instance specific for single typed collections
+# and allows answering questions about its embedded type. It does not however preserve multiple types in
+# a collection, and can thus not answer questions like `[1,a].infer() =~ Array[Integer, String]` since the inference
+# computes the common type Scalar when combining Integer and String.
+#
+# The `infer_generic(o)` method infers a generic Puppet type for scalar Ruby object, Arrays and Hashes.
+# This inference result does not contain instance specific information; e.g. Array[Integer] where the integer
+# range is the generic default. Just `infer` it also combines types into a common type.
+#
+# The `infer_set(o)` method works like `infer` but preserves all type information. It does not do any
+# reduction into common types or ranges. This method of inference is best suited for answering questions
+# about an object being an instance of a type. It correctly answers: `[1,a].infer_set() =~ Array[Integer, String]`
+#
+# The `generalize!(t)` method modifies an instance specific inference result to a generic. The method mutates
+# the given argument. Basically, this removes string instances from String, and range from Integer and Float.
#
# Assignability
# -------------
# The `assignable?(t1, t2)` method answers if t2 conforms to t1. The type t2 may be an instance, in which case
# its type is inferred, or a type.
#
# Instance?
# ---------
# The `instance?(t, o)` method answers if the given object (instance) is an instance that is assignable to the given type.
#
# String
# ------
# Creates a string representation of a type.
#
# Creation of Type instances
# --------------------------
# Instance of the classes in the {Puppet::Pops::Types type model} are used to denote a specific type. It is most convenient
# to use the {Puppet::Pops::Types::TypeFactory TypeFactory} when creating instances.
#
# @note
# In general, new instances of the wanted type should be created as they are assigned to models using containment, and a
# contained object can only be in one container at a time. Also, the type system may include more details in each type
# instance, such as if it may be nil, be empty, contain a certain count etc. Or put differently, the puppet types are not
# singletons.
#
+# All types support `copy` which should be used when assigning a type where it is unknown if it is bound or not
+# to a parent type. A check can be made with `t.eContainer().nil?`
+#
# Equality and Hash
# -----------------
# Type instances are equal in terms of Ruby eql? and `==` if they describe the same type, but they are not `equal?` if they are not
# the same type instance. Two types that describe the same type have identical hash - this makes them usable as hash keys.
#
# Types and Subclasses
# --------------------
# In general, the type calculator should be used to answer questions if a type is a subtype of another (using {#assignable?}, or
# {#instance?} if the question is if a given object is an instance of a given type (or is a subtype thereof).
# Many of the types also have a Ruby subtype relationship; e.g. PHashType and PArrayType are both subtypes of PCollectionType, and
-# PIntegerType, PFloatType, PStringType,... are subtypes of PLiteralType. Even if it is possible to answer certain questions about
+# PIntegerType, PFloatType, PStringType,... are subtypes of PScalarType. Even if it is possible to answer certain questions about
# type by looking at the Ruby class of the types this is considered an implementation detail, and such checks should in general
# be performed by the type_calculator which implements the type system semantics.
#
# The PRubyType
# -------------
# The PRubyType corresponds to a Ruby Class, except for the puppet types that are specialized (i.e. PRubyType should not be
# used for Integer, String, etc. since there are specialized types for those).
# When the type calculator deals with PRubyTypes and checks for assignability, it determines the "common ancestor class" of two classes.
# This check is made based on the superclasses of the two classes being compared. In order to perform this, the classes must be present
# (i.e. they are resolved from the string form in the PRubyType to a loaded, instantiated Ruby Class). In general this is not a problem,
# since the question to produce the common super type for two objects means that the classes must be present or there would have been
# no instances present in the first place. If however the classes are not present, the type calculator will fall back and state that
# the two types at least have Object in common.
#
# @see Puppet::Pops::Types::TypeFactory TypeFactory for how to create instances of types
# @see Puppet::Pops::Types::TypeParser TypeParser how to construct a type instance from a String
# @see Puppet::Pops::Types Types for details about the type model
#
+# Using the Type Calculator
+# -----
+# The type calculator can be directly used via its class methods. If doing time critical work and doing many
+# calls to the type calculator, it is more performant to create an instance and invoke the corresponding
+# instance methods. Note that inference is an expensive operation, rather than infering the same thing
+# several times, it is in general better to infer once and then copy the result if mutation to a more generic form is
+# required.
+#
# @api public
#
class Puppet::Pops::Types::TypeCalculator
Types = Puppet::Pops::Types
+ TheInfinity = 1.0 / 0.0 # because the Infinity symbol is not defined
+
+ # @api public
+ def self.assignable?(t1, t2)
+ singleton.assignable?(t1,t2)
+ end
+
+ # @api public
+ def self.string(t)
+ singleton.string(t)
+ end
+
+ # @api public
+ def self.infer(o)
+ singleton.infer(o)
+ end
+
+ # @api public
+ def self.generalize!(o)
+ singleton.generalize!(o)
+ end
+
+ # @api public
+ def self.infer_set(o)
+ singleton.infer_set(o)
+ end
+
+ # @api public
+ def self.debug_string(t)
+ singleton.debug_string(t)
+ end
+
+ # @api public
+ def self.enumerable(t)
+ singleton.enumerable(t)
+ end
+
+ # @api private
+ def self.singleton()
+ @tc_instance ||= new
+ end
# @api public
#
def initialize
@@assignable_visitor ||= Puppet::Pops::Visitor.new(nil,"assignable",1,1)
@@infer_visitor ||= Puppet::Pops::Visitor.new(nil,"infer",0,0)
+ @@infer_set_visitor ||= Puppet::Pops::Visitor.new(nil,"infer_set",0,0)
+ @@instance_of_visitor ||= Puppet::Pops::Visitor.new(nil,"instance_of",1,1)
@@string_visitor ||= Puppet::Pops::Visitor.new(nil,"string",0,0)
+ @@inspect_visitor ||= Puppet::Pops::Visitor.new(nil,"debug_string",0,0)
+ @@enumerable_visitor ||= Puppet::Pops::Visitor.new(nil,"enumerable",0,0)
+ @@extract_visitor ||= Puppet::Pops::Visitor.new(nil,"extract",0,0)
+ @@generalize_visitor ||= Puppet::Pops::Visitor.new(nil,"generalize",0,0)
da = Types::PArrayType.new()
da.element_type = Types::PDataType.new()
@data_array = da
h = Types::PHashType.new()
h.element_type = Types::PDataType.new()
- h.key_type = Types::PLiteralType.new()
+ h.key_type = Types::PScalarType.new()
@data_hash = h
@data_t = Types::PDataType.new()
- @literal_t = Types::PLiteralType.new()
+ @scalar_t = Types::PScalarType.new()
@numeric_t = Types::PNumericType.new()
@t = Types::PObjectType.new()
+
+ # Data accepts a Tuple that has 0-infinity Data compatible entries (e.g. a Tuple equivalent to Array).
+ data_tuple = Types::PTupleType.new()
+ data_tuple.addTypes(Types::PDataType.new())
+ data_tuple.size_type = Types::PIntegerType.new()
+ data_tuple.size_type.from = 0
+ data_tuple.size_type.to = nil # infinity
+ @data_tuple_t = data_tuple
+
+ # Variant type compatible with Data
+ data_variant = Types::PVariantType.new()
+ data_variant.addTypes(@data_hash.copy)
+ data_variant.addTypes(@data_array.copy)
+ data_variant.addTypes(Types::PScalarType.new)
+ data_variant.addTypes(Types::PNilType.new)
+ data_variant.addTypes(@data_tuple_t.copy)
+ @data_variant_t = data_variant
+
+ collection_default_size = Types::PIntegerType.new()
+ collection_default_size.from = 0
+ collection_default_size.to = nil # infinity
+ @collection_default_size_t = collection_default_size
+
+ non_empty_string = Types::PStringType.new
+ non_empty_string.size_type = Types::PIntegerType.new()
+ non_empty_string.size_type.from = 1
+ non_empty_string.size_type.to = nil # infinity
+ @non_empty_string_t = non_empty_string
end
# Convenience method to get a data type for comparisons
# @api private the returned value may not be contained in another element
#
def data
@data_t
end
+ # Convenience method to get a variant compatible with the Data type.
+ # @api private the returned value may not be contained in another element
+ #
+ def data_variant
+ @data_variant_t
+ end
+
+ def self.data_variant
+ singleton.data_variant
+ end
+
# Answers the question 'is it possible to inject an instance of the given class'
# A class is injectable if it has a special *assisted inject* class method called `inject` taking
# an injector and a scope as argument, or if it has a zero args `initialize` method.
#
# @param klazz [Class, PRubyType] the class/type to check if it is injectable
# @return [Class, nil] the injectable Class, or nil if not injectable
# @api public
#
def injectable_class(klazz)
# Handle case when we get a PType instead of a class
if klazz.is_a?(Types::PRubyType)
klazz = Puppet::Pops::Types::ClassLoader.provide(klazz)
end
# data types can not be injected (check again, it is not safe to assume that given RubyType klazz arg was ok)
return false unless type(klazz).is_a?(Types::PRubyType)
if (klazz.respond_to?(:inject) && klazz.method(:inject).arity() == -4) || klazz.instance_method(:initialize).arity() == 0
klazz
else
nil
end
end
# Answers 'can an instance of type t2 be assigned to a variable of type t'
# @api public
#
def assignable?(t, t2)
- # nil is assignable to anything
- if is_pnil?(t2)
- return true
- end
+ # nil is assignable to anything except to required types
+ return true if is_pnil?(t2)
if t.is_a?(Class)
t = type(t)
end
if t2.is_a?(Class)
t2 = type(t2)
end
- @@assignable_visitor.visit_this(self, t, t2)
+ @@assignable_visitor.visit_this_1(self, t, t2)
end
+ # Returns an enumerable if the t represents something that can be iterated
+ def enumerable(t)
+ @@enumerable_visitor.visit_this_0(self, t)
+ end
+
+ # Answers if the two given types describe the same type
+ def equals(left, right)
+ return false unless left.is_a?(Types::PAbstractType) && right.is_a?(Types::PAbstractType)
+ # Types compare per class only - an extra test must be made if the are mutually assignable
+ # to find all types that represent the same type of instance
+ #
+ left == right || (assignable?(right, left) && assignable?(left, right))
+ end
+
# Answers 'what is the Puppet Type corresponding to the given Ruby class'
- # @param c [Class] the class for which a puppet type is wanted
+ # @param c [Class] the class for which a puppet type is wanted
# @api public
#
def type(c)
raise ArgumentError, "Argument must be a Class" unless c.is_a? Class
# Can't use a visitor here since we don't have an instance of the class
case
when c <= Integer
type = Types::PIntegerType.new()
when c == Float
type = Types::PFloatType.new()
when c == Numeric
type = Types::PNumericType.new()
when c == String
type = Types::PStringType.new()
when c == Regexp
- type = Types::PPatternType.new()
+ type = Types::PRegexpType.new()
when c == NilClass
type = Types::PNilType.new()
when c == FalseClass, c == TrueClass
type = Types::PBooleanType.new()
when c == Class
type = Types::PType.new()
when c == Array
# Assume array of data values
type = Types::PArrayType.new()
type.element_type = Types::PDataType.new()
when c == Hash
- # Assume hash with literal keys and data values
+ # Assume hash with scalar keys and data values
type = Types::PHashType.new()
- type.key_type = Types::PLiteralType.new()
+ type.key_type = Types::PScalarType.new()
type.element_type = Types::PDataType.new()
else
type = Types::PRubyType.new()
type.ruby_class = c.name
end
type
end
- # Answers 'what is the Puppet Type of o'
+ # Generalizes value specific types. The given type is mutated and returned.
+ # @api public
+ def generalize!(o)
+ @@generalize_visitor.visit_this_0(self, o)
+ o.eAllContents.each { |x| @@generalize_visitor.visit_this_0(self, x) }
+ o
+ end
+
+ def generalize_Object(o)
+ # do nothing, there is nothing to change for most types
+ end
+
+ def generalize_PStringType(o)
+ o.values = []
+ o.size_type = nil
+ []
+ end
+
+ def generalize_PCollectionType(o)
+ # erase the size constraint from Array and Hash (if one exists, it is transformed to -Infinity - + Infinity, which is
+ # not desirable.
+ o.size_type = nil
+ end
+
+ def generalize_PFloatType(o)
+ o.to = nil
+ o.from = nil
+ end
+
+ def generalize_PIntegerType(o)
+ o.to = nil
+ o.from = nil
+ end
+
+ # Answers 'what is the single common Puppet Type describing o', or if o is an Array or Hash, what is the
+ # single common type of the elements (or keys and elements for a Hash).
# @api public
#
def infer(o)
- @@infer_visitor.visit_this(self, o)
+ @@infer_visitor.visit_this_0(self, o)
+ end
+
+ def infer_generic(o)
+ result = generalize!(infer(o))
+ result
+ end
+
+ # Answers 'what is the set of Puppet Types of o'
+ # @api public
+ #
+ def infer_set(o)
+ @@infer_set_visitor.visit_this_0(self, o)
+ end
+
+ def instance_of(t, o)
+# return true if o.nil? && !t.is_a?(Types::PRequiredType)
+ @@instance_of_visitor.visit_this_1(self, t, o)
+ end
+
+ def instance_of_Object(t, o)
+ # Undef is Undef and Object, but nothing else when checking instance?
+ return false if (o.nil? || o == :undef) && t.class != Types::PObjectType
+ assignable?(t, infer(o))
+ end
+
+ def instance_of_PArrayType(t, o)
+ return false unless o.is_a?(Array)
+ return false unless o.all? {|element| instance_of(t.element_type, element) }
+ size_t = t.size_type || @collection_default_size_t
+ size_t2 = size_as_type(o)
+ assignable?(size_t, size_t2)
+ end
+
+ def instance_of_PTupleType(t, o)
+ return false unless o.is_a?(Array)
+ # compute the tuple's min/max size, and check if that size matches
+ from, to = size_range(t.size_type)
+ size_t = Types::PIntegerType.new()
+ size_t.from = t.types.size - 1 + from
+ size_t.to = t.types.size - 1 + to
+ # compute the array's size as type
+ size_t2 = size_as_type(o)
+ return false unless assignable?(size_t, size_t2)
+ o.each_with_index do |element, index|
+ return false unless instance_of(t.types[index] || t.types[-1], element)
+ end
+ true
+ end
+
+ def instance_of_PStructType(t, o)
+ return false unless o.is_a?(Hash)
+ h = t.hashed_elements
+ # all keys must be present and have a value (even if nil/undef)
+ (o.keys - h.keys).empty? && h.all? { |k,v| instance_of(v, o[k]) }
+ end
+
+ def instance_of_PHashType(t, o)
+ return false unless o.is_a?(Hash)
+ key_t = t.key_type
+ element_t = t.element_type
+ return false unless o.keys.all? {|key| instance_of(key_t, key) } && o.values.all? {|value| instance_of(element_t, value) }
+ size_t = t.size_type || @collection_default_size_t
+ size_t2 = size_as_type(o)
+ assignable?(size_t, size_t2)
+ end
+
+ def instance_of_PDataType(t, o)
+ instance_of(@data_variant_t, o)
+ end
+
+ def instance_of_PNilType(t, o)
+ return o.nil? || o == :undef
+ end
+
+ def instance_of_POptionalType(t, o)
+ return true if (o.nil? || o == :undef)
+ instance_of(t.optional_type, o)
+ end
+
+ def instance_of_PVariantType(t, o)
+ # instance of variant if o is instance? of any of variant's types
+ t.types.any? { |option_t| instance_of(option_t, o) }
+ end
+
+ # Answers 'is o an instance of type t'
+ # @api public
+ #
+ def self.instance?(t, o)
+ singleton.instance_of(t,o)
end
# Answers 'is o an instance of type t'
# @api public
#
def instance?(t, o)
- assignable?(t, infer(o))
+ instance_of(t,o)
end
# Answers if t is a puppet type
# @api public
#
def is_ptype?(t)
- return t.is_a?(Types::PObjectType)
+ return t.is_a?(Types::PAbstractType)
end
# Answers if t represents the puppet type PNilType
# @api public
#
def is_pnil?(t)
return t.nil? || t.is_a?(Types::PNilType)
end
# Answers, 'What is the common type of t1 and t2?'
+ #
+ # TODO: The current implementation should be optimized for performance
+ #
# @api public
#
def common_type(t1, t2)
raise ArgumentError, 'two types expected' unless (is_ptype?(t1) || is_pnil?(t1)) && (is_ptype?(t2) || is_pnil?(t2))
# if either is nil, the common type is the other
if is_pnil?(t1)
return t2
elsif is_pnil?(t2)
return t1
end
# Simple case, one is assignable to the other
if assignable?(t1, t2)
return t1
elsif assignable?(t2, t1)
return t2
end
# when both are arrays, return an array with common element type
if t1.is_a?(Types::PArrayType) && t2.is_a?(Types::PArrayType)
type = Types::PArrayType.new()
type.element_type = common_type(t1.element_type, t2.element_type)
return type
end
# when both are hashes, return a hash with common key- and element type
if t1.is_a?(Types::PHashType) && t2.is_a?(Types::PHashType)
type = Types::PHashType.new()
type.key_type = common_type(t1.key_type, t2.key_type)
type.element_type = common_type(t1.element_type, t2.element_type)
return type
end
+ # when both are host-classes, reduce to PHostClass[] (since one was not assignable to the other)
+ if t1.is_a?(Types::PHostClassType) && t2.is_a?(Types::PHostClassType)
+ return Types::PHostClassType.new()
+ end
+
+ # when both are resources, reduce to Resource[T] or Resource[] (since one was not assignable to the other)
+ if t1.is_a?(Types::PResourceType) && t2.is_a?(Types::PResourceType)
+ result = Types::PResourceType.new()
+ # only Resource[] unless the type name is the same
+ if t1.type_name == t2.type_name then result.type_name = t1.type_name end
+ # the cross assignability test above has already determined that they do not have the same type and title
+ return result
+ end
+
+ # Integers have range, expand the range to the common range
+ if t1.is_a?(Types::PIntegerType) && t2.is_a?(Types::PIntegerType)
+ t1range = from_to_ordered(t1.from, t1.to)
+ t2range = from_to_ordered(t2.from, t2.to)
+ t = Types::PIntegerType.new()
+ from = [t1range[0], t2range[0]].min
+ to = [t1range[1], t2range[1]].max
+ t.from = from unless from == TheInfinity
+ t.to = to unless to == TheInfinity
+ return t
+ end
+
+ # Floats have range, expand the range to the common range
+ if t1.is_a?(Types::PFloatType) && t2.is_a?(Types::PFloatType)
+ t1range = from_to_ordered(t1.from, t1.to)
+ t2range = from_to_ordered(t2.from, t2.to)
+ t = Types::PFloatType.new()
+ from = [t1range[0], t2range[0]].min
+ to = [t1range[1], t2range[1]].max
+ t.from = from unless from == TheInfinity
+ t.to = to unless to == TheInfinity
+ return t
+ end
+
+ if t1.is_a?(Types::PStringType) && t2.is_a?(Types::PStringType)
+ t = Types::PStringType.new()
+ t.values = t1.values | t2.values
+ return t
+ end
+
+ if t1.is_a?(Types::PPatternType) && t2.is_a?(Types::PPatternType)
+ t = Types::PPatternType.new()
+ # must make copies since patterns are contained types, not data-types
+ t.patterns = (t1.patterns | t2.patterns).map {|p| p.copy }
+ return t
+ end
+
+ if t1.is_a?(Types::PEnumType) && t2.is_a?(Types::PEnumType)
+ # The common type is one that complies with either set
+ t = Types::PEnumType.new
+ t.values = t1.values | t2.values
+ return t
+ end
+
+ if t1.is_a?(Types::PVariantType) && t2.is_a?(Types::PVariantType)
+ # The common type is one that complies with either set
+ t = Types::PVariantType.new
+ t.types = (t1.types | t2.types).map {|opt_t| opt_t.copy }
+ return t
+ end
+
+ if t1.is_a?(Types::PRegexpType) && t2.is_a?(Types::PRegexpType)
+ # if they were identical, the general rule would return a parameterized regexp
+ # since they were not, the result is a generic regexp type
+ return Types::PPatternType.new()
+ end
+
# Common abstract types, from most specific to most general
if common_numeric?(t1, t2)
return Types::PNumericType.new()
end
- if common_literal?(t1, t2)
- return Types::PLiteralType.new()
+ if common_scalar?(t1, t2)
+ return Types::PScalarType.new()
end
if common_data?(t1,t2)
return Types::PDataType.new()
end
+ # Meta types Type[Integer] + Type[String] => Type[Data]
+ if t1.is_a?(Types::PType) && t2.is_a?(Types::PType)
+ type = Types::PType.new()
+ type.type = common_type(t1.type, t2.type)
+ return type
+ end
+
if t1.is_a?(Types::PRubyType) && t2.is_a?(Types::PRubyType)
if t1.ruby_class == t2.ruby_class
return t1
end
# finding the common super class requires that names are resolved to class
c1 = Types::ClassLoader.provide_from_type(t1)
c2 = Types::ClassLoader.provide_from_type(t2)
if c1 && c2
c2_superclasses = superclasses(c2)
superclasses(c1).each do|c1_super|
c2_superclasses.each do |c2_super|
if c1_super == c2_super
result = Types::PRubyType.new()
result.ruby_class = c1_super.name
return result
end
end
end
end
end
# If both are RubyObjects
if common_pobject?(t1, t2)
return Types::PObjectType.new()
end
end
# Produces the superclasses of the given class, including the class
def superclasses(c)
result = [c]
while s = c.superclass
result << s
c = s
end
result
end
+
# Produces a string representing the type
# @api public
#
def string(t)
- @@string_visitor.visit_this(self, t)
+ @@string_visitor.visit_this_0(self, t)
+ end
+
+ # Produces a debug string representing the type (possibly with more information that the regular string format)
+ # @api public
+ #
+ def debug_string(t)
+ @@inspect_visitor.visit_this_0(self, t)
end
# Reduces an enumerable of types to a single common type.
# @api public
#
def reduce_type(enumerable)
enumerable.reduce(nil) {|memo, t| common_type(memo, t) }
end
# Reduce an enumerable of objects to a single common type
# @api public
#
def infer_and_reduce_type(enumerable)
reduce_type(enumerable.collect() {|o| infer(o) })
end
# The type of all classes is PType
# @api private
#
def infer_Class(o)
Types::PType.new()
end
# @api private
def infer_Object(o)
type = Types::PRubyType.new()
type.ruby_class = o.class.name
type
end
# The type of all types is PType
# @api private
#
def infer_PObjectType(o)
- Types::PType.new()
+ type = Types::PType.new()
+ type.type = o.copy
+ type
end
# The type of all types is PType
# This is the metatype short circuit.
# @api private
#
def infer_PType(o)
- Types::PType.new()
+ type = Types::PType.new()
+ type.type = o.copy
+ type
end
# @api private
def infer_String(o)
- Types::PStringType.new()
+ t = Types::PStringType.new()
+ t.addValues(o)
+ t.size_type = size_as_type(o)
+ t
end
# @api private
def infer_Float(o)
- Types::PFloatType.new()
+ t = Types::PFloatType.new()
+ t.from = o
+ t.to = o
+ t
end
# @api private
def infer_Integer(o)
- Types::PIntegerType.new()
+ t = Types::PIntegerType.new()
+ t.from = o
+ t.to = o
+ t
end
# @api private
def infer_Regexp(o)
- Types::PPatternType.new()
+ t = Types::PRegexpType.new()
+ t.pattern = o.source
+ t
end
# @api private
def infer_NilClass(o)
Types::PNilType.new()
end
+ # Inference of :undef as PNilType, all other are Ruby[Symbol]
+ # @api private
+ def infer_Symbol(o)
+ o == :undef ? infer_NilClass(o) : infer_Object(o)
+ end
+
# @api private
def infer_TrueClass(o)
Types::PBooleanType.new()
end
# @api private
def infer_FalseClass(o)
Types::PBooleanType.new()
end
+ # @api private
+ # A Puppet::Parser::Resource, or Puppet::Resource
+ #
+ def infer_Resource(o)
+ t = Types::PResourceType.new()
+ t.type_name = o.type.to_s
+ t.title = o.title
+ t
+ end
+
# @api private
def infer_Array(o)
type = Types::PArrayType.new()
- type.element_type = if o.empty?
+ type.element_type =
+ if o.empty?
Types::PNilType.new()
else
infer_and_reduce_type(o)
end
+ type.size_type = size_as_type(o)
type
end
# @api private
def infer_Hash(o)
type = Types::PHashType.new()
if o.empty?
ktype = Types::PNilType.new()
etype = Types::PNilType.new()
else
ktype = infer_and_reduce_type(o.keys())
etype = infer_and_reduce_type(o.values())
end
type.key_type = ktype
type.element_type = etype
+ type.size_type = size_as_type(o)
type
end
+ def size_as_type(collection)
+ size = collection.size
+ t = Types::PIntegerType.new()
+ t.from = size
+ t.to = size
+ t
+ end
+
+ # Common case for everything that intrinsically only has a single type
+ def infer_set_Object(o)
+ infer(o)
+ end
+
+ def infer_set_Array(o)
+ type = Types::PArrayType.new()
+ type.element_type = if o.empty?
+ Types::PNilType.new()
+ else
+ t = Types::PVariantType.new()
+ t.types = o.map() {|x| infer_set(x) }
+ t.types.size == 1 ? t.types[0] : t
+ end
+ type.size_type = size_as_type(o)
+ type
+ end
+
+ def infer_set_Hash(o)
+ type = Types::PHashType.new()
+ if o.empty?
+ ktype = Types::PNilType.new()
+ etype = Types::PNilType.new()
+ else
+ ktype = Types::PVariantType.new()
+ ktype.types = o.keys.map() {|k| infer_set(k) }
+ etype = Types::PVariantType.new()
+ etype.types = o.values.map() {|e| infer_set(e) }
+ end
+ type.key_type = unwrap_single_variant(ktype)
+ type.element_type = unwrap_single_variant(vtype)
+ type.size_type = size_as_type(o)
+ type
+ end
+
+ def unwrap_single_variant(possible_variant)
+ if possible_variant.is_a?(Types::PVariantType) && possible_variant.types.size == 1
+ possible_variant.types[0]
+ else
+ possible_variant
+ end
+ end
# False in general type calculator
# @api private
def assignable_Object(t, t2)
false
end
# @api private
def assignable_PObjectType(t, t2)
t2.is_a?(Types::PObjectType)
end
# @api private
- def assignable_PLiteralType(t, t2)
- t2.is_a?(Types::PLiteralType)
+ def assignable_PNilType(t, t2)
+ # Only undef/nil is assignable to nil type
+ t2.is_a?(Types::PNilType)
+ end
+
+ # @api private
+ def assignable_PScalarType(t, t2)
+ t2.is_a?(Types::PScalarType)
end
# @api private
def assignable_PNumericType(t, t2)
t2.is_a?(Types::PNumericType)
end
# @api private
def assignable_PIntegerType(t, t2)
- t2.is_a?(Types::PIntegerType)
+ return false unless t2.is_a?(Types::PIntegerType)
+ trange = from_to_ordered(t.from, t.to)
+ t2range = from_to_ordered(t2.from, t2.to)
+ # If t2 min and max are within the range of t
+ trange[0] <= t2range[0] && trange[1] >= t2range[1]
+ end
+
+ # Transform int range to a size constraint
+ # if range == nil the constraint is 1,1
+ # if range.from == nil min size = 1
+ # if range.to == nil max size == Infinity
+ #
+ def size_range(range)
+ return [1,1] if range.nil?
+ from = range.from
+ to = range.to
+ x = from.nil? ? 1 : from
+ y = to.nil? ? TheInfinity : to
+ if x < y
+ [x, y]
+ else
+ [y, x]
+ end
+ end
+
+ # @api private
+ def from_to_ordered(from, to)
+ x = (from.nil? || from == :default) ? -TheInfinity : from
+ y = (to.nil? || to == :default) ? TheInfinity : to
+ if x < y
+ [x, y]
+ else
+ [y, x]
+ end
+ end
+
+ # @api private
+ def assignable_PVariantType(t, t2)
+ # Data is a specific variant
+ t2 = @data_variant_t if t2.is_a?(Types::PDataType)
+ if t2.is_a?(Types::PVariantType)
+ # A variant is assignable if all of its options are assignable to one of this type's options
+ return true if t == t2
+ t2.types.all? do |other|
+ # if the other is a Variant, all if its options, but be assignable to one of this type's options
+ other = other.is_a?(Types::PDataType) ? @data_variant_t : other
+ if other.is_a?(Types::PVariantType)
+ assignable?(t, other)
+ else
+ t.types.any? {|option_t| assignable?(option_t, other) }
+ end
+ end
+ else
+ # A variant is assignable if t2 is assignable to any of its types
+ t.types.any? { |option_t| assignable?(option_t, t2) }
+ end
+ end
+
+ def assignable_PTupleType(t, t2)
+ return true if t == t2 || t.types.empty? && (t2.is_a?(Types::PArrayType))
+ t_regular = t.types[0..-2]
+ t_ranged = t.types[-1]
+ t_from, t_to = size_range(t.size_type)
+ t_required = t_regular.size + t_from
+
+ if t2.is_a?(Types::PTupleType)
+ t2_regular = t2.types[0..-2]
+ t2_ranged = t2.types[-1]
+ t2_from, t2_to = size_range(t2.size_type)
+ t2_required = t2_regular.size + t2_from
+
+ # tuples with fewer required entries can not be assigned
+ return false if t_required > t2_required
+ # tuples with more optionally available entries can not be assigned
+ return false if t_regular.size + t_to < t2_regular.size + t2_to
+
+ t_required.times do |index|
+ t_entry = tuple_entry_at(t, t_from, t_to, index)
+ t2_entry = tuple_entry_at(t2, t2_from, t2_to, index)
+ return false if t2_entry.nil? || !assignable?(t_entry, t2_entry)
+ end
+ # Handle remainder in t2's required
+ (t2_required - t_required).times do |index|
+ t_entry = tuple_entry_at(t, t_from, t_to, t_required + index)
+ t2_entry = tuple_entry_at(t2, t2_from, t2_to, t_required + index)
+ return false if t2_entry.nil? || !assignable?(t_entry, t2_entry)
+ end
+ # Now only a trailing optional type remains - the last type must always be compatible
+ # irrespective of optionality and count
+ #
+ return assignable?(t_ranged, t2_ranged)
+
+ elsif t2.is_a?(Types::PArrayType)
+ t2_entry = t2.element_type
+
+ # Array of anything can not be assigned (unless tuple is tuple of anything) - this case
+ # was handled at the top of this method.
+ #
+ return false if t2_entry.nil?
+
+ # array type may be size constrained
+ size_t = t2.size_type || @collection_default_size_t
+ min, max = size_t.range
+ # Array with fewer min entries can not be assigned
+ return false if t_required > min
+ # Array with more optionally available entries can not be assigned
+ return false if t_regular.size + t_to < max
+ # each tuple type must be assignable to the element type
+ t_required.times do |index|
+ t_entry = tuple_entry_at(t, t_from, t_to, index)
+ return false unless assignable?(t_entry, t2_entry)
+ end
+ # ... and so must the last, possibly optional (ranged) type
+ return assignable?(t_ranged, t2_entry)
+ end
+ end
+
+ # Produces the tuple entry at the given index given a tuple type, its from/to constraints on the last
+ # type, and an index.
+ # Produces nil if the index is out of bounds
+ # from must be less than to, and from may not be less than 0
+ #
+ # @api private
+ #
+ def tuple_entry_at(tuple_t, from, to, index)
+ regular = (tuple_t.types.size - 1)
+ if index < regular
+ tuple_t.types[index]
+ elsif index < regular + to
+ # in the varargs part
+ tuple_t.types[-1]
+ else
+ nil
+ end
+ end
+
+ # @api private
+ #
+ def assignable_PStructType(t, t2)
+ return true if t == t2 || t.elements.empty? && (t2.is_a?(Types::PHashType))
+ h = t.hashed_elements
+ if t2.is_a?(Types::PStructType)
+ h2 = t2.hashed_elements
+ h.size == h2.size && h.all? {|k, v| assignable?(v, h2[k]) }
+ elsif t2.is_a?(Types::PHashType)
+ size_t2 = t2.size_type || @collection_default_size_t
+ size_t = Types::PIntegerType.new
+ size_t.from = size_t.to = h.size
+ # compatible size
+ # hash key type must be string of min 1 size
+ # hash value t must be assignable to each key
+ element_type = t2.element_type
+ assignable?(size_t, size_t2) &&
+ assignable?(@non_empty_string_t, t2.key_type) &&
+ h.all? {|k,v| assignable?(v, element_type) }
+ else
+ false
+ end
+ end
+
+ # @api private
+ def assignable_POptionalType(t, t2)
+ return true if t2.is_a?(Types::PNilType)
+ if t2.is_a?(Types::POptionalType)
+ assignable?(t.optional_type, t2.optional_type)
+ else
+ assignable?(t.optional_type, t2)
+ end
+ end
+
+ # @api private
+ def assignable_PEnumType(t, t2)
+ return true if t == t2 || (t.values.empty? && (t2.is_a?(Types::PStringType) || t2.is_a?(Types::PEnumType)))
+ if t2.is_a?(Types::PStringType)
+ # if the set of strings are all found in the set of enums
+ t2.values.all? { |s| t.values.any? { |e| e == s }}
+ else
+ false
+ end
end
# @api private
def assignable_PStringType(t, t2)
- t2.is_a?(Types::PStringType)
+ if t.values.empty?
+ # A general string is assignable by any other string or pattern restricted string
+ # if the string has a size constraint it does not match since there is no reasonable way
+ # to compute the min/max length a pattern will match. For enum, it is possible to test that
+ # each enumerator value is within range
+ size_t = t.size_type || @collection_default_size_t
+ case t2
+ when Types::PStringType
+ # true if size compliant
+ size_t2 = t2.size_type || @collection_default_size_t
+ assignable?(size_t, size_t2)
+
+ when Types::PPatternType
+ # true if size constraint is at least 0 to +Infinity (which is the same as the default)
+ assignable?(size_t, @collection_default_size_t)
+
+ when Types::PEnumType
+ if t2.values
+ # true if all enum values are within range
+ min, max = t2.values.map(&:size).minmax
+ trange = from_to_ordered(size_t.from, size_t.to)
+ t2range = [min, max]
+ # If t2 min and max are within the range of t
+ trange[0] <= t2range[0] && trange[1] >= t2range[1]
+ else
+ # no string can match this enum anyway since it does not accept anything
+ false
+ end
+ end
+ elsif t2.is_a?(Types::PStringType)
+ # A specific string acts as a set of strings - must have exactly the same strings
+ # In this case, size does not matter since the definition is very precise anyway
+ Set.new(t.values) == Set.new(t2.values)
+ else
+ # All others are false, since no other type describes the same set of specific strings
+ false
+ end
+ end
+
+ # @api private
+ def assignable_PPatternType(t, t2)
+ return true if t == t2
+ return false unless t2.is_a?(Types::PStringType) || t2.is_a?(Types::PEnumType)
+
+ if t2.values.empty?
+ # Strings / Enums (unknown which ones) cannot all match a pattern, but if there is no pattern it is ok
+ # (There should really always be a pattern, but better safe than sorry).
+ return t.patterns.empty? ? true : false
+ end
+ # all strings in String/Enum type must match one of the patterns in Pattern type
+ regexps = t.patterns.map {|p| p.regexp }
+ t2.values.all? { |v| regexps.any? {|re| re.match(v) } }
end
# @api private
def assignable_PFloatType(t, t2)
- t2.is_a?(Types::PFloatType)
+ return false unless t2.is_a?(Types::PFloatType)
+ trange = from_to_ordered(t.from, t.to)
+ t2range = from_to_ordered(t2.from, t2.to)
+ # If t2 min and max are within the range of t
+ trange[0] <= t2range[0] && trange[1] >= t2range[1]
end
# @api private
def assignable_PBooleanType(t, t2)
t2.is_a?(Types::PBooleanType)
end
# @api private
- def assignable_PPatternType(t, t2)
- t2.is_a?(Types::PPatternType)
+ def assignable_PRegexpType(t, t2)
+ t2.is_a?(Types::PRegexpType) && (t.pattern.nil? || t.pattern == t2.pattern)
end
# @api private
def assignable_PCollectionType(t, t2)
- t2.is_a?(Types::PCollectionType)
+ size_t = t.size_type || @collection_default_size_t
+ case t2
+ when Types::PCollectionType
+ size_t2 = t2.size_type || @collection_default_size_t
+ assignable?(size_t, size_t2)
+ when Types::PTupleType
+ # compute the tuple's min/max size, and check if that size matches
+ from, to = size_range(t2.size_type)
+ t2s = Types::PIntegerType.new()
+ t2s.from = t2.types.size - 1 + from
+ t2s.to = t2.types.size - 1 + to
+ assignable?(size_t, t2s)
+ when Types::PStructType
+ from = to = t2.elements.size
+ t2s = Types::PIntegerType.new()
+ t2s.from = from
+ t2s.to = to
+ assignable?(size_t, t2s)
+ else
+ false
+ end
+ end
+
+ # @api private
+ def assignable_PType(t, t2)
+ return false unless t2.is_a?(Types::PType)
+ return true if t.type.nil? # wide enough to handle all types
+ return false if t2.type.nil? # wider than t
+ assignable?(t.type, t2.type)
end
- # Array is assignable if t2 is an Array and t2's element type is assignable
+ # Array is assignable if t2 is an Array and t2's element type is assignable, or if t2 is a Tuple
+ # where
# @api private
def assignable_PArrayType(t, t2)
- return false unless t2.is_a?(Types::PArrayType)
- assignable?(t.element_type, t2.element_type)
+ if t2.is_a?(Types::PArrayType)
+ return false unless assignable?(t.element_type, t2.element_type)
+ assignable_PCollectionType(t, t2)
+
+ elsif t2.is_a?(Types::PTupleType)
+ return false unless t2.types.all? {|t2_element| assignable?(t.element_type, t2_element) }
+ t2_regular = t2.types[0..-2]
+ t2_ranged = t2.types[-1]
+ t2_from, t2_to = size_range(t2.size_type)
+ t2_required = t2_regular.size + t2_from
+
+ t_entry = t.element_type
+
+ # Tuple of anything can not be assigned (unless array is tuple of anything) - this case
+ # was handled at the top of this method.
+ #
+ return false if t_entry.nil?
+
+ # array type may be size constrained
+ size_t = t.size_type || @collection_default_size_t
+ min, max = size_t.range
+ # Tuple with fewer min entries can not be assigned
+ return false if t2_required < min
+ # Tuple with more optionally available entries can not be assigned
+ return false if t2_regular.size + t2_to > max
+ # each tuple type must be assignable to the element type
+ t2_required.times do |index|
+ t2_entry = tuple_entry_at(t2, t2_from, t2_to, index)
+ return false unless assignable?(t_entry, t2_entry)
+ end
+ # ... and so must the last, possibly optional (ranged) type
+ return assignable?(t_entry, t2_ranged)
+ else
+ false
+ end
end
# Hash is assignable if t2 is a Hash and t2's key and element types are assignable
# @api private
def assignable_PHashType(t, t2)
- return false unless t2.is_a?(Types::PHashType)
- assignable?(t.key_type, t2.key_type) && assignable?(t.element_type, t2.element_type)
+ case t2
+ when Types::PHashType
+ return false unless assignable?(t.key_type, t2.key_type) && assignable?(t.element_type, t2.element_type)
+ assignable_PCollectionType(t, t2)
+ when Types::PStructType
+ # hash must accept String as key type
+ # hash must accept all value types
+ # hash must accept the size of the struct
+ size_t = t.size_type || @collection_default_size_t
+ min, max = size_t.range
+ struct_size = t2.elements.size
+ element_type = t.element_type
+ ( struct_size >= min && struct_size <= max &&
+ assignable?(t.key_type, @non_emptry_string_t) &&
+ t2.hashed_elements.all? {|k,v| assignable?(element_type, v) })
+ else
+ false
+ end
end
- # Data is assignable by other Data and by Array[Data] and Hash[Literal, Data]
+ # @api private
+ def assignable_PCatalogEntryType(t1, t2)
+ t2.is_a?(Types::PCatalogEntryType)
+ end
+
+ # @api private
+ def assignable_PHostClassType(t1, t2)
+ return false unless t2.is_a?(Types::PHostClassType)
+ # Class = Class[name}, Class[name] != Class
+ return true if t1.class_name.nil?
+ # Class[name] = Class[name]
+ return t1.class_name == t2.class_name
+ end
+
+ # @api private
+ def assignable_PResourceType(t1, t2)
+ return false unless t2.is_a?(Types::PResourceType)
+ return true if t1.type_name.nil?
+ return false if t1.type_name != t2.type_name
+ return true if t1.title.nil?
+ return t1.title == t2.title
+ end
+
+ # Data is assignable by other Data and by Array[Data] and Hash[Scalar, Data]
# @api private
def assignable_PDataType(t, t2)
- t2.is_a?(Types::PDataType) || assignable?(@data_array, t2) || assignable?(@data_hash, t2)
+ t2.is_a?(Types::PDataType) || assignable?(@data_variant_t, t2)
end
# Assignable if t2's ruby class is same or subclass of t1's ruby class
# @api private
def assignable_PRubyType(t1, t2)
return false unless t2.is_a?(Types::PRubyType)
+ return true if t1.ruby_class.nil? # t1 is wider
+ return false if t2.ruby_class.nil? # t1 not nil, so t2 can not be wider
c1 = class_from_string(t1.ruby_class)
c2 = class_from_string(t2.ruby_class)
return false unless c1.is_a?(Class) && c2.is_a?(Class)
!!(c2 <= c1)
end
# @api private
- def string_PType(t) ; "Type" ; end
+ def debug_string_Object(t)
+ string(t)
+ end
+
+ # @api private
+ def string_PType(t)
+ if t.type.nil?
+ "Type"
+ else
+ "Type[#{string(t.type)}]"
+ end
+ end
+
+ # @api private
+ def string_NilClass(t) ; '?' ; end
+
+ # @api private
+ def string_String(t) ; t ; end
# @api private
def string_PObjectType(t) ; "Object" ; end
+ # @api private
+ def string_PNilType(t) ; 'Undef' ; end
+
# @api private
def string_PBooleanType(t) ; "Boolean" ; end
# @api private
- def string_PLiteralType(t) ; "Literal" ; end
+ def string_PScalarType(t) ; "Scalar" ; end
# @api private
def string_PDataType(t) ; "Data" ; end
# @api private
def string_PNumericType(t) ; "Numeric" ; end
# @api private
- def string_PIntegerType(t) ; "Integer" ; end
+ def string_PIntegerType(t)
+ range = range_array_part(t)
+ unless range.empty?
+ "Integer[#{range.join(', ')}]"
+ else
+ "Integer"
+ end
+ end
+ # Produces a string from an Integer range type that is used inside other type strings
# @api private
- def string_PFloatType(t) ; "Float" ; end
+ def range_array_part(t)
+ return [] if t.nil? || (t.from.nil? && t.to.nil?)
+ [t.from.nil? ? 'default' : t.from , t.to.nil? ? 'default' : t.to ]
+ end
# @api private
- def string_PPatternType(t) ; "Pattern" ; end
+ def string_PFloatType(t)
+ range = range_array_part(t)
+ unless range.empty?
+ "Float[#{range.join(', ')}]"
+ else
+ "Float"
+ end
+ end
# @api private
- def string_PStringType(t) ; "String" ; end
+ def string_PRegexpType(t)
+ t.pattern.nil? ? "Regexp" : "Regexp[#{t.regexp.inspect}]"
+ end
+
+ # @api private
+ def string_PStringType(t)
+ # skip values in regular output - see debug_string
+ range = range_array_part(t.size_type)
+ unless range.empty?
+ "String[#{range.join(', ')}]"
+ else
+ "String"
+ end
+ end
# @api private
- def string_PRubyType(t) ; "Ruby[#{t.ruby_class}]" ; end
+ def debug_string_PStringType(t)
+ range = range_array_part(t.size_type)
+ range_part = range.empty? ? '' : '[' << range.join(' ,') << '], '
+ "String[" << range_part << (t.values.map {|s| "'#{s}'" }).join(', ') << ']'
+ end
+
+ # @api private
+ def string_PEnumType(t)
+ return "Enum" if t.values.empty?
+ "Enum[" << t.values.map {|s| "'#{s}'" }.join(', ') << ']'
+ end
+
+ # @api private
+ def string_PVariantType(t)
+ return "Variant" if t.types.empty?
+ "Variant[" << t.types.map {|t2| string(t2) }.join(', ') << ']'
+ end
+
+ # @api private
+ def string_PTupleType(t)
+ range = range_array_part(t.size_type)
+ return "Tuple" if t.types.empty?
+ s = "Tuple[" << t.types.map {|t2| string(t2) }.join(', ')
+ unless range.empty?
+ s << ", " << range.join(', ')
+ end
+ s << "]"
+ s
+ end
+
+ # @api private
+ def string_PStructType(t)
+ return "Struct" if t.elements.empty?
+ "Struct[{" << t.elements.map {|element| string(element) }.join(', ') << "}]"
+ end
+
+ def string_PStructElement(t)
+ "'#{t.name}'=>#{string(t.type)}"
+ end
+
+ # @api private
+ def string_PPatternType(t)
+ return "Pattern" if t.patterns.empty?
+ "Pattern[" << t.patterns.map {|s| "#{s.regexp.inspect}" }.join(', ') << ']'
+ end
+
+ # @api private
+ def string_PCollectionType(t)
+ range = range_array_part(t.size_type)
+ unless range.empty?
+ "Collection[#{range.join(', ')}]"
+ else
+ "Collection"
+ end
+ end
+
+ # @api private
+ def string_PRubyType(t) ; "Ruby[#{string(t.ruby_class)}]" ; end
# @api private
def string_PArrayType(t)
- "Array[#{string(t.element_type)}]"
+ parts = [string(t.element_type)] + range_array_part(t.size_type)
+ "Array[#{parts.join(', ')}]"
end
# @api private
def string_PHashType(t)
- "Hash[#{string(t.key_type)}, #{string(t.element_type)}]"
+ parts = [string(t.key_type), string(t.element_type)] + range_array_part(t.size_type)
+ "Hash[#{parts.join(', ')}]"
+ end
+
+ # @api private
+ def string_PCatalogEntryType(t)
+ "CatalogEntry"
+ end
+
+ # @api private
+ def string_PHostClassType(t)
+ if t.class_name
+ "Class[#{t.class_name}]"
+ else
+ "Class"
+ end
+ end
+
+ # @api private
+ def string_PResourceType(t)
+ if t.type_name
+ if t.title
+ "#{t.type_name.capitalize}['#{t.title}']"
+ else
+ "#{t.type_name.capitalize}"
+ end
+ else
+ "Resource"
+ end
+ end
+
+ def string_POptionalType(t)
+ if t.optional_type.nil?
+ "Optional"
+ else
+ "Optional[#{string(t.optional_type)}]"
+ end
+ end
+
+ # Catches all non enumerable types
+ # @api private
+ def enumerable_Object(o)
+ nil
+ end
+
+ # @api private
+ def enumerable_PIntegerType(t)
+ # Not enumerable if representing an infinite range
+ return nil if t.size == TheInfinity
+ t
end
private
def class_from_string(str)
- str.split('::').inject(Object) do |memo, name_segment|
- memo.const_get(name_segment)
+ begin
+ str.split('::').inject(Object) do |memo, name_segment|
+ memo.const_get(name_segment)
+ end
+ rescue NameError
+ return nil
end
end
def common_data?(t1, t2)
assignable?(@data_t, t1) && assignable?(@data_t, t2)
end
- def common_literal?(t1, t2)
- assignable?(@literal_t, t1) && assignable?(@literal_t, t2)
+ def common_scalar?(t1, t2)
+ assignable?(@scalar_t, t1) && assignable?(@scalar_t, t2)
end
def common_numeric?(t1, t2)
assignable?(@numeric_t, t1) && assignable?(@numeric_t, t2)
end
def common_pobject?(t1, t2)
assignable?(@t, t1) && assignable?(@t, t2)
end
end
diff --git a/lib/puppet/pops/types/type_factory.rb b/lib/puppet/pops/types/type_factory.rb
index 2be11be11..3022b98db 100644
--- a/lib/puppet/pops/types/type_factory.rb
+++ b/lib/puppet/pops/types/type_factory.rb
@@ -1,147 +1,335 @@
# Helper module that makes creation of type objects simpler.
# @api public
#
module Puppet::Pops::Types::TypeFactory
@type_calculator = Puppet::Pops::Types::TypeCalculator.new()
Types = Puppet::Pops::Types
# Produces the Integer type
# @api public
#
def self.integer()
Types::PIntegerType.new()
end
+ # Produces an Integer range type
+ # @api public
+ #
+ def self.range(from, to)
+ t = Types::PIntegerType.new()
+ t.from = from unless (from == :default || from == 'default')
+ t.to = to unless (to == :default || to == 'default')
+ t
+ end
+
+ # Produces a Float range type
+ # @api public
+ #
+ def self.float_range(from, to)
+ t = Types::PFloatType.new()
+ t.from = Float(from) unless from == :default || from.nil?
+ t.to = Float(to) unless to == :default || to.nil?
+ t
+ end
+
# Produces the Float type
# @api public
#
def self.float()
Types::PFloatType.new()
end
+ # Produces the Numeric type
+ # @api public
+ #
+ def self.numeric()
+ Types::PNumericType.new()
+ end
+
# Produces a string representation of the type
# @api public
#
def self.label(t)
@type_calculator.string(t)
end
- # Produces the String type
+ # Produces the String type, optionally with specific string values
+ # @api public
+ #
+ def self.string(*values)
+ t = Types::PStringType.new()
+ values.each {|v| t.addValues(v) }
+ t
+ end
+
+ # Produces the Optional type, i.e. a short hand for Variant[T, Undef]
+ def self.optional(optional_type = nil)
+ t = Types::POptionalType.new
+ t.optional_type = type_of(optional_type)
+ t
+ end
+
+ # Produces the Enum type, optionally with specific string values
+ # @api public
+ #
+ def self.enum(*values)
+ t = Types::PEnumType.new()
+ values.each {|v| t.addValues(v) }
+ t
+ end
+
+ # Produces the Variant type, optionally with the "one of" types
# @api public
#
- def self.string()
- Types::PStringType.new()
+ def self.variant(*types)
+ t = Types::PVariantType.new()
+ types.each {|v| t.addTypes(type_of(v)) }
+ t
+ end
+
+ # Produces the Struct type, either a non parameterized instance representing all structs (i.e. all hashes)
+ # or a hash with a given set of keys of String type (names), bound to a value of a given type. Type may be
+ # a Ruby Class, a Puppet Type, or an instance from which the type is inferred.
+ #
+ def self.struct(name_type_hash = {})
+ t = Types::PStructType.new
+ name_type_hash.map do |name, type|
+ elem = Types::PStructElement.new
+ if name.is_a?(String) && name.empty?
+ raise ArgumentError, "An empty String can not be used where a String[1, default] is expected"
+ end
+ elem.name = name
+ elem.type = type_of(type)
+ elem
+ end.each {|elem| t.addElements(elem) }
+ t
+ end
+
+ def self.tuple(*types)
+ t = Types::PTupleType.new
+ types.each {|elem| t.addTypes(type_of(elem)) }
+ t
end
# Produces the Boolean type
# @api public
#
def self.boolean()
Types::PBooleanType.new()
end
- # Produces the Pattern type
+ # Produces the Object type
+ # @api public
+ #
+ def self.object()
+ Types::PObjectType.new()
+ end
+
+ # Produces the Regexp type
+ # @param pattern [Regexp, String, nil] (nil) The regular expression object or a regexp source string, or nil for bare type
# @api public
#
- def self.pattern()
- Types::PPatternType.new()
+ def self.regexp(pattern = nil)
+ t = Types::PRegexpType.new()
+ if pattern
+ t.pattern = pattern.is_a?(Regexp) ? pattern.inspect[1..-2] : pattern
+ end
+ t
+ end
+
+ def self.pattern(*regular_expressions)
+ t = Types::PPatternType.new()
+ regular_expressions.each do |re|
+ case re
+ when String
+ re_T = Types::PRegexpType.new()
+ re_T.pattern = re
+ t.addPatterns(re_T)
+
+ when Regexp
+ re_T = Types::PRegexpType.new()
+ # Regep.to_s includes options user did not enter and does not escape source
+ # to work either as a string or as a // regexp. The inspect method does a better
+ # job, but includes the //
+ re_T.pattern = re.inspect[1..-2]
+ t.addPatterns(re_T)
+
+ when Types::PRegexpType
+ t.addPatterns(re.copy)
+
+ when Types::PPatternType
+ re.patterns.each do |p|
+ t.addPatterns(p.copy)
+ end
+
+ else
+ raise ArgumentError, "Only String, Regexp, Pattern-Type, and Regexp-Type are allowed: got '#{re.class}"
+ end
+ end
+ t
end
# Produces the Literal type
# @api public
#
- def self.literal()
- Types::PLiteralType.new()
+ def self.scalar()
+ Types::PScalarType.new()
end
# Produces the abstract type Collection
# @api public
#
def self.collection()
Types::PCollectionType.new()
end
# Produces the Data type
# @api public
#
def self.data()
Types::PDataType.new()
end
+ # Creates an instance of the Undef type
+ # @api public
+ def self.undef()
+ Types::PNilType.new()
+ end
+
+ # Produces an instance of the abstract type PCatalogEntryType
+ def self.catalog_entry()
+ Types::PCatalogEntryType.new()
+ end
+
+ # Produces a PResourceType with a String type_name
+ # A PResourceType with a nil or empty name is compatible with any other PResourceType.
+ # A PResourceType with a given name is only compatible with a PResourceType with the same name.
+ # (There is no resource-type subtyping in Puppet (yet)).
+ #
+ def self.resource(type_name = nil, title = nil)
+ type = Types::PResourceType.new()
+ type_name = type_name.type_name if type_name.is_a?(Types::PResourceType)
+ type.type_name = type_name.downcase unless type_name.nil?
+ type.title = title
+ type
+ end
+
+ # Produces PHostClassType with a string class_name.
+ # A PHostClassType with nil or empty name is compatible with any other PHostClassType.
+ # A PHostClassType with a given name is only compatible with a PHostClassType with the same name.
+ #
+ def self.host_class(class_name = nil)
+ type = Types::PHostClassType.new()
+ type.class_name = class_name
+ type
+ end
+
# Produces a type for Array[o] where o is either a type, or an instance for which a type is inferred.
# @api public
#
def self.array_of(o)
type = Types::PArrayType.new()
type.element_type = type_of(o)
type
end
- # Produces a type for Hash[Literal, o] where o is either a type, or an instance for which a type is inferred.
+ # Produces a type for Hash[Scalar, o] where o is either a type, or an instance for which a type is inferred.
# @api public
#
- def self.hash_of(value, key = literal())
+ def self.hash_of(value, key = scalar())
type = Types::PHashType.new()
type.key_type = type_of(key)
type.element_type = type_of(value)
type
end
# Produces a type for Array[Data]
# @api public
#
def self.array_of_data()
type = Types::PArrayType.new()
type.element_type = data()
type
end
- # Produces a type for Hash[Literal, Data]
+ # Produces a type for Hash[Scalar, Data]
# @api public
#
def self.hash_of_data()
type = Types::PHashType.new()
- type.key_type = literal()
+ type.key_type = scalar()
type.element_type = data()
type
end
- # Produce a type corresponding to the class of given unless given is a String, Class or a PObjectType.
+ # Produces a type for Type[T]
+ # @api public
+ #
+ def self.type_type(inst_type = nil)
+ type = Types::PType.new()
+ type.type = inst_type
+ type
+ end
+
+ # Produce a type corresponding to the class of given unless given is a String, Class or a PAbstractType.
# When a String is given this is taken as a classname.
#
def self.type_of(o)
if o.is_a?(Class)
@type_calculator.type(o)
- elsif o.is_a?(Types::PObjectType)
+ elsif o.is_a?(Types::PAbstractType)
o
elsif o.is_a?(String)
type = Types::PRubyType.new()
type.ruby_class = o
type
else
- @type_calculator.infer(o)
+ @type_calculator.infer_generic(o)
end
end
# Produces a type for a class or infers a type for something that is not a class
# @note
# To get the type for the class' class use `TypeCalculator.infer(c)`
#
# @overload ruby(o)
# @param o [Class] produces the type corresponding to the class (e.g. Integer becomes PIntegerType)
# @overload ruby(o)
# @param o [Object] produces the type corresponding to the instance class (e.g. 3 becomes PIntegerType)
#
# @api public
#
def self.ruby(o)
if o.is_a?(Class)
@type_calculator.type(o)
else
type = Types::PRubyType.new()
type.ruby_class = o.class.name
type
end
end
+
+ # Generic creator of a RubyType - allows creating the Ruby type with nil name, or String name.
+ # Also see ruby(o) which performs inference, or mapps a Ruby Class to its name.
+ #
+ def self.ruby_type(class_name = nil)
+ type = Types::PRubyType.new()
+ type.ruby_class = class_name
+ type
+ end
+
+ # Sets the accepted size range of a collection if something other than the default 0 to Infinity
+ # is wanted. The semantics for from/to are the same as for #range
+ #
+ def self.constrain_size(collection_t, from, to)
+ collection_t.size_type = range(from, to)
+ collection_t
+ end
+
+ # Returns true if the given type t is of valid range parameter type (integer or literal default).
+ def self.is_range_parameter?(t)
+ t.is_a?(Integer) || t == 'default' || t == :default
+ end
+
end
diff --git a/lib/puppet/pops/types/type_parser.rb b/lib/puppet/pops/types/type_parser.rb
index f62178a4d..1b8cc8948 100644
--- a/lib/puppet/pops/types/type_parser.rb
+++ b/lib/puppet/pops/types/type_parser.rb
@@ -1,117 +1,460 @@
# This class provides parsing of Type Specification from a string into the Type
# Model that is produced by the Puppet::Pops::Types::TypeFactory.
#
# The Type Specifications that are parsed are the same as the stringified forms
# of types produced by the {Puppet::Pops::Types::TypeCalculator TypeCalculator}.
#
# @api public
class Puppet::Pops::Types::TypeParser
# @api private
TYPES = Puppet::Pops::Types::TypeFactory
# @api public
def initialize
@parser = Puppet::Pops::Parser::Parser.new()
@type_transformer = Puppet::Pops::Visitor.new(nil, "interpret", 0, 0)
end
# Produces a *puppet type* based on the given string.
#
# @example
# parser.parse('Integer')
# parser.parse('Array[String]')
# parser.parse('Hash[Integer, Array[String]]')
#
# @param string [String] a string with the type expressed in stringified form as produced by the
# {Puppet::Pops::Types::TypeCalculator#string TypeCalculator#string} method.
# @return [Puppet::Pops::Types::PObjectType] a specialization of the PObjectType representing the type.
#
# @api public
#
def parse(string)
+ # TODO: This state (@string) can be removed since the parse result of newer future parser
+ # contains a Locator in its SourcePosAdapter and the Locator keeps the string.
+ # This way, there is no difference between a parsed "string" and something that has been parsed
+ # earlier and fed to 'interpret'
+ #
@string = string
model = @parser.parse_string(@string)
if model
interpret(model.current)
else
raise_invalid_type_specification_error
end
end
# @api private
def interpret(ast)
- @type_transformer.visit_this(self, ast)
+ result = @type_transformer.visit_this_0(self, ast)
+ result = result.body if result.is_a?(Puppet::Pops::Model::Program)
+ raise_invalid_type_specification_error unless result.is_a?(Puppet::Pops::Types::PAbstractType)
+ result
end
# @api private
- def interpret_Object(anything)
+ def interpret_any(ast)
+ @type_transformer.visit_this_0(self, ast)
+ end
+
+ # @api private
+ def interpret_Object(o)
raise_invalid_type_specification_error
end
+ # @api private
+ def interpret_Program(o)
+ interpret(o.body)
+ end
+
+ # @api private
+ def interpret_QualifiedName(o)
+ o.value
+ end
+
+ # @api private
+ def interpret_LiteralString(o)
+ o.value
+ end
+
+ # @api private
+ def interpret_String(o)
+ o
+ end
+
+ # @api private
+ def interpret_LiteralDefault(o)
+ :default
+ end
+
+ # @api private
+ def interpret_LiteralInteger(o)
+ o.value
+ end
+
+ # @api private
+ def interpret_LiteralFloat(o)
+ o.value
+ end
+
+ # @api private
+ def interpret_LiteralHash(o)
+ result = {}
+ o.entries.each do |entry|
+ result[@type_transformer.visit_this_0(self, entry.key)] = @type_transformer.visit_this_0(self, entry.value)
+ end
+ result
+ end
+
# @api private
def interpret_QualifiedReference(name_ast)
case name_ast.value
when "integer"
TYPES.integer
+
when "float"
TYPES.float
+
+ when "numeric"
+ TYPES.numeric
+
when "string"
TYPES.string
+
+ when "enum"
+ TYPES.enum
+
when "boolean"
TYPES.boolean
+
when "pattern"
TYPES.pattern
+
+ when "regexp"
+ TYPES.regexp
+
when "data"
TYPES.data
+
when "array"
TYPES.array_of_data
+
when "hash"
TYPES.hash_of_data
+
+ when "class"
+ TYPES.host_class()
+
+ when "resource"
+ TYPES.resource()
+
+ when "collection"
+ TYPES.collection()
+
+ when "scalar"
+ TYPES.scalar()
+
+ when "catalogentry"
+ TYPES.catalog_entry()
+
+ when "undef"
+ # Should not be interpreted as Resource type
+ TYPES.undef()
+
+ when "object"
+ TYPES.object()
+
+ when "variant"
+ TYPES.variant()
+
+ when "optional"
+ TYPES.optional()
+
+ when "ruby"
+ TYPES.ruby_type()
+
+ when "type"
+ TYPES.type_type()
+
+ when "tuple"
+ TYPES.tuple()
+
+ when "struct"
+ TYPES.struct()
else
- raise_unknown_type_error(name_ast)
+ TYPES.resource(name_ast.value)
end
end
# @api private
def interpret_AccessExpression(parameterized_ast)
- parameters = parameterized_ast.keys.collect { |param| interpret(param) }
+ parameters = parameterized_ast.keys.collect { |param| interpret_any(param) }
+
+ unless parameterized_ast.left_expr.is_a?(Puppet::Pops::Model::QualifiedReference)
+ raise_invalid_type_specification_error
+ end
+
case parameterized_ast.left_expr.value
when "array"
- if parameters.size != 1
- raise_invalid_parameters_error("Array", 1, parameters.size)
+ case parameters.size
+ when 1
+ when 2
+ size_type =
+ if parameters[1].is_a?(Puppet::Pops::Types::PIntegerType)
+ parameters[1].copy
+ else
+ assert_range_parameter(parameters[1])
+ TYPES.range(parameters[1], :default)
+ end
+ when 3
+ assert_range_parameter(parameters[1])
+ assert_range_parameter(parameters[2])
+ size_type = TYPES.range(parameters[1], parameters[2])
+ else
+ raise_invalid_parameters_error("Array", "1 to 3", parameters.size)
end
- TYPES.array_of(parameters[0])
+ assert_type(parameters[0])
+ t = TYPES.array_of(parameters[0])
+ t.size_type = size_type if size_type
+ t
+
when "hash"
- if parameters.size == 1
+ result = case parameters.size
+ when 1
+ assert_type(parameters[0])
TYPES.hash_of(parameters[0])
+ when 2
+ assert_type(parameters[0])
+ assert_type(parameters[1])
+ TYPES.hash_of(parameters[1], parameters[0])
+ when 3
+ size_type =
+ if parameters[2].is_a?(Puppet::Pops::Types::PIntegerType)
+ parameters[2].copy
+ else
+ assert_range_parameter(parameters[2])
+ TYPES.range(parameters[2], :default)
+ end
+ assert_type(parameters[0])
+ assert_type(parameters[1])
+ TYPES.hash_of(parameters[1], parameters[0])
+ when 4
+ assert_range_parameter(parameters[2])
+ assert_range_parameter(parameters[3])
+ size_type = TYPES.range(parameters[2], parameters[3])
+ assert_type(parameters[0])
+ assert_type(parameters[1])
+ TYPES.hash_of(parameters[1], parameters[0])
+ else
+ raise_invalid_parameters_error("Hash", "1 to 4", parameters.size)
+ end
+ result.size_type = size_type if size_type
+ result
+
+ when "collection"
+ size_type = case parameters.size
+ when 1
+ if parameters[0].is_a?(Puppet::Pops::Types::PIntegerType)
+ parameters[0].copy
+ else
+ assert_range_parameter(parameters[0])
+ TYPES.range(parameters[0], :default)
+ end
+ when 2
+ assert_range_parameter(parameters[0])
+ assert_range_parameter(parameters[1])
+ TYPES.range(parameters[0], parameters[1])
+ else
+ raise_invalid_parameters_error("Collection", "1 to 2", parameters.size)
+ end
+ result = TYPES.collection
+ result.size_type = size_type
+ result
+
+ when "class"
+ if parameters.size != 1
+ raise_invalid_parameters_error("Class", 1, parameters.size)
+ end
+ TYPES.host_class(parameters[0])
+
+ when "resource"
+ if parameters.size == 1
+ TYPES.resource(parameters[0])
elsif parameters.size != 2
- raise_invalid_parameters_error("Hash", "1 or 2", parameters.size)
+ raise_invalid_parameters_error("Resource", "1 or 2", parameters.size)
else
- TYPES.hash_of(parameters[1], parameters[0])
+ TYPES.resource(parameters[0], parameters[1])
+ end
+
+ when "regexp"
+ # 1 parameter being a string, or regular expression
+ raise_invalid_parameters_error("Regexp", "1", parameters.size) unless parameters.size == 1
+ TYPES.regexp(parameters[0])
+
+ when "enum"
+ # 1..m parameters being strings
+ raise_invalid_parameters_error("Enum", "1 or more", parameters.size) unless parameters.size > 1
+ TYPES.enum(*parameters)
+
+ when "pattern"
+ # 1..m parameters being strings or regular expressions
+ raise_invalid_parameters_error("Pattern", "1 or more", parameters.size) unless parameters.size > 1
+ TYPES.pattern(*parameters)
+
+ when "variant"
+ # 1..m parameters being strings or regular expressions
+ raise_invalid_parameters_error("Variant", "1 or more", parameters.size) unless parameters.size > 1
+ TYPES.variant(*parameters)
+
+ when "tuple"
+ # 1..m parameters being types (last two optionally integer or literal default
+ raise_invalid_parameters_error("Tuple", "1 or more", parameters.size) unless parameters.size > 1
+ length = parameters.size
+ if TYPES.is_range_parameter?(parameters[-2])
+ # min, max specification
+ min = parameters[-2]
+ min = (min == :default || min == 'default') ? 0 : min
+ assert_range_parameter(parameters[-1])
+ max = parameters[-1]
+ max = max == :default ? nil : max
+ parameters = parameters[0, length-2]
+ elsif TYPES.is_range_parameter?(parameters[-1])
+ min = parameters[-1]
+ min = (min == :default || min == 'default') ? 0 : min
+ max = nil
+ parameters = parameters[0, length-1]
+ end
+ t = TYPES.tuple(*parameters)
+ if min || max
+ TYPES.constrain_size(t, min, max)
end
+ t
+
+ when "struct"
+ # 1..m parameters being types (last two optionally integer or literal default
+ raise_invalid_parameters_error("Struct", "1", parameters.size) unless parameters.size == 1
+ assert_struct_parameter(parameters[0])
+ TYPES.struct(parameters[0])
+
+ when "integer"
+ if parameters.size == 1
+ case parameters[0]
+ when Integer
+ TYPES.range(parameters[0], parameters[0])
+ when :default
+ TYPES.integer # unbound
+ end
+ elsif parameters.size != 2
+ raise_invalid_parameters_error("Integer", "1 or 2", parameters.size)
+ else
+ TYPES.range(parameters[0] == :default ? nil : parameters[0], parameters[1] == :default ? nil : parameters[1])
+ end
+
+ when "float"
+ if parameters.size == 1
+ case parameters[0]
+ when Integer, Float
+ TYPES.float_range(parameters[0], parameters[0])
+ when :default
+ TYPES.float # unbound
+ end
+ elsif parameters.size != 2
+ raise_invalid_parameters_error("Float", "1 or 2", parameters.size)
+ else
+ TYPES.float_range(parameters[0] == :default ? nil : parameters[0], parameters[1] == :default ? nil : parameters[1])
+ end
+
+ when "string"
+ size_type =
+ case parameters.size
+ when 1
+ if parameters[0].is_a?(Puppet::Pops::Types::PIntegerType)
+ parameters[0].copy
+ else
+ assert_range_parameter(parameters[0])
+ TYPES.range(parameters[0], :default)
+ end
+ when 2
+ assert_range_parameter(parameters[0])
+ assert_range_parameter(parameters[1])
+ TYPES.range(parameters[0], parameters[1])
+ else
+ raise_invalid_parameters_error("String", "1 to 2", parameters.size)
+ end
+ result = TYPES.string
+ result.size_type = size_type
+ result
+
+ when "optional"
+ if parameters.size != 1
+ raise_invalid_parameters_error("Optional", 1, parameters.size)
+ end
+ assert_type(parameters[0])
+ TYPES.optional(parameters[0])
+
+ when "object", "data", "catalogentry", "boolean", "scalar", "undef", "numeric"
+ raise_unparameterized_type_error(parameterized_ast.left_expr)
+
+ when "type"
+ if parameters.size != 1
+ raise_invalid_parameters_error("Type", 1, parameters.size)
+ end
+ assert_type(parameters[0])
+ TYPES.type_type(parameters[0])
+
+ when "ruby"
+ raise_invalid_parameters_error("Ruby", "1", parameters.size) unless parameters.size == 1
+ TYPES.ruby_type(parameters[0])
+
else
- raise_unknown_type_error(parameterized_ast.left_expr)
+ # It is a resource such a File['/tmp/foo']
+ type_name = parameterized_ast.left_expr.value
+ if parameters.size != 1
+ raise_invalid_parameters_error(type_name.capitalize, 1, parameters.size)
+ end
+ TYPES.resource(type_name, parameters[0])
end
end
private
+ def assert_type(t)
+ raise_invalid_type_specification_error unless t.is_a?(Puppet::Pops::Types::PObjectType)
+ true
+ end
+
+ def assert_range_parameter(t)
+ raise_invalid_type_specification_error unless TYPES.is_range_parameter?(t)
+ end
+
+ def assert_struct_parameter(h)
+ raise_invalid_type_specification_error unless h.is_a?(Hash)
+ h.each do |k,v|
+ # TODO: Should have stricter name rule
+ raise_invalid_type_specification_error unless k.is_a?(String) && !k.empty?
+ assert_type(v)
+ end
+ end
+
def raise_invalid_type_specification_error
raise Puppet::ParseError,
"The expression <#{@string}> is not a valid type specification."
end
def raise_invalid_parameters_error(type, required, given)
raise Puppet::ParseError,
"Invalid number of type parameters specified: #{type} requires #{required}, #{given} provided"
end
+ def raise_unparameterized_type_error(ast)
+ raise Puppet::ParseError, "Not a parameterized type <#{original_text_of(ast)}>"
+ end
+
def raise_unknown_type_error(ast)
raise Puppet::ParseError, "Unknown type <#{original_text_of(ast)}>"
end
def original_text_of(ast)
position = Puppet::Pops::Adapters::SourcePosAdapter.adapt(ast)
- position.extract_text_from_string(@string)
+ position.extract_text()
end
end
diff --git a/lib/puppet/pops/types/types.rb b/lib/puppet/pops/types/types.rb
index f524308b4..89fa24c34 100644
--- a/lib/puppet/pops/types/types.rb
+++ b/lib/puppet/pops/types/types.rb
@@ -1,132 +1,422 @@
require 'rgen/metamodel_builder'
# The Types model is a model of Puppet Language types.
#
# The exact relationship between types is not visible in this model wrt. the PDataType which is an abstraction
-# of Literal, Array[Data], and Hash[Literal, Data] nested to any depth. This means it is not possible to
+# of Scalar, Array[Data], and Hash[Scalar, Data] nested to any depth. This means it is not possible to
# infer the type by simply looking at the inheritance hierarchy. The {Puppet::Pops::Types::TypeCalculator} should
# be used to answer questions about types. The {Puppet::Pops::Types::TypeFactory} should be used to create an instance
# of a type whenever one is needed.
#
+# The implementation of the Types model contains methods that are required for the type objects to behave as
+# expected when comparing them and using them as keys in hashes. (No other logic is, or should be included directly in
+# the model's classes).
+#
# @api public
#
module Puppet::Pops::Types
- # The type of types.
- # @api public
- class PType < Puppet::Pops::Model::PopsObject
- end
-
- # Base type for all types except {Puppet::Pops::Types::PType PType}, the type of types.
- # @api public
- class PObjectType < Puppet::Pops::Model::PopsObject
-
+ class PAbstractType < Puppet::Pops::Model::PopsObject
+ abstract
module ClassModule
+ # Produce a deep copy of the type
+ def copy
+ Marshal.load(Marshal.dump(self))
+ end
+
def hash
self.class.hash
end
def ==(o)
self.class == o.class
end
alias eql? ==
+
+ def to_s
+ Puppet::Pops::Types::TypeCalculator.string(self)
+ end
+ end
+ end
+
+ # The type of types.
+ # @api public
+ class PType < PAbstractType
+ contains_one_uni 'type', PAbstractType
+ module ClassModule
+ def hash
+ [self.class, type].hash
+ end
+
+ def ==(o)
+ self.class == o.class && type == o.type
+ end
+ end
+ end
+
+ # Base type for all types except {Puppet::Pops::Types::PType PType}, the type of types.
+ # @api public
+ class PObjectType < PAbstractType
+
+ module ClassModule
end
end
# @api public
class PNilType < PObjectType
end
# A flexible data type, being assignable to its subtypes as well as PArrayType and PHashType with element type assignable to PDataType.
#
# @api public
class PDataType < PObjectType
+ module ClassModule
+ def ==(o)
+ self.class == o.class ||
+ o.class == PVariantType && o == Puppet::Pops::Types::TypeCalculator.data_variant()
+ end
+ end
end
- # Type that is PDataType compatible, but is not a PCollectionType.
+ # A flexible type describing an any? of other types
# @api public
- class PLiteralType < PDataType
+ class PVariantType < PObjectType
+ contains_many_uni 'types', PAbstractType, :lowerBound => 1
+
+ module ClassModule
+
+ def hash
+ [self.class, Set.new(self.types)].hash
+ end
+
+ def ==(o)
+ (self.class == o.class && Set.new(types) == Set.new(o.types)) ||
+ (o.class == PDataType && self == Puppet::Pops::Types::TypeCalculator.data_variant())
+ end
+ end
end
+ # Type that is PDataType compatible, but is not a PCollectionType.
# @api public
- class PStringType < PLiteralType
+ class PScalarType < PObjectType
+ end
+
+ # A string type describing the set of strings having one of the given values
+ #
+ class PEnumType < PScalarType
+ has_many_attr 'values', String, :lowerBound => 1
+
+ module ClassModule
+ def hash
+ [self.class, Set.new(self.values)].hash
+ end
+
+ def ==(o)
+ self.class == o.class && Set.new(values) == Set.new(o.values)
+ end
+ end
end
# @api public
- class PNumericType < PLiteralType
+ class PNumericType < PScalarType
end
# @api public
class PIntegerType < PNumericType
+ has_attr 'from', Integer, :lowerBound => 0
+ has_attr 'to', Integer, :lowerBound => 0
+
+ module ClassModule
+ # The integer type is enumerable when it defines a range
+ include Enumerable
+
+ # Returns Float.Infinity if one end of the range is unbound
+ def size
+ return 1.0 / 0.0 if from.nil? || to.nil?
+ 1+(to-from).abs
+ end
+
+ # Returns the range as an array ordered so the smaller number is always first.
+ # The number may be Infinity or -Infinity.
+ def range
+ f = from || -(1.0 / 0.0)
+ t = to || (1.0 / 0.0)
+ if f < t
+ [f, t]
+ else
+ [t,f]
+ end
+ end
+
+ # Returns Enumerator if no block is given
+ # Returns self if size is infinity (does not yield)
+ def each
+ return self.to_enum unless block_given?
+ return nil if from.nil? || to.nil?
+ if to < from
+ from.downto(to) {|x| yield x }
+ else
+ from.upto(to) {|x| yield x }
+ end
+ end
+
+ def hash
+ [self.class, from, to].hash
+ end
+
+ def ==(o)
+ self.class == o.class && from == o.from && to == o.to
+ end
+ end
end
# @api public
class PFloatType < PNumericType
+ has_attr 'from', Float, :lowerBound => 0
+ has_attr 'to', Float, :lowerBound => 0
+
+ module ClassModule
+ def hash
+ [self.class, from, to].hash
+ end
+
+ def ==(o)
+ self.class == o.class && from == o.from && to == o.to
+ end
+ end
+ end
+
+ # @api public
+ class PStringType < PScalarType
+ has_many_attr 'values', String, :lowerBound => 0, :upperBound => -1, :unique => true
+ contains_one_uni 'size_type', PIntegerType
+
+ module ClassModule
+
+ def hash
+ [self.class, self.size_type, Set.new(self.values)].hash
+ end
+
+ def ==(o)
+ self.class == o.class && self.size_type == o.size_type && Set.new(values) == Set.new(o.values)
+ end
+ end
+ end
+
+ # @api public
+ class PRegexpType < PScalarType
+ has_attr 'pattern', String, :lowerBound => 1
+ has_attr 'regexp', Object, :derived => true
+
+ module ClassModule
+ def regexp_derived
+ @_regexp = Regexp.new(pattern) unless @_regexp && @_regexp.source == pattern
+ @_regexp
+ end
+
+ def hash
+ [self.class, pattern].hash
+ end
+
+ def ==(o)
+ self.class == o.class && pattern == o.pattern
+ end
+ end
end
+ # Represents a subtype of String that narrows the string to those matching the patterns
+ # If specified without a pattern it is basically the same as the String type.
+ #
# @api public
- class PPatternType < PLiteralType
+ class PPatternType < PScalarType
+ contains_many_uni 'patterns', PRegexpType
+
+ module ClassModule
+
+ def hash
+ [self.class, Set.new(patterns)].hash
+ end
+
+ def ==(o)
+ self.class == o.class && Set.new(patterns) == Set.new(o.patterns)
+ end
+ end
end
# @api public
- class PBooleanType < PLiteralType
+ class PBooleanType < PScalarType
end
# @api public
class PCollectionType < PObjectType
- contains_one_uni 'element_type', PObjectType
+ contains_one_uni 'element_type', PAbstractType
+ contains_one_uni 'size_type', PIntegerType
+ module ClassModule
+ def hash
+ [self.class, element_type, size].hash
+ end
+
+ def ==(o)
+ self.class == o.class && element_type == o.element_type && size_type == o.size_type
+ end
+ end
+ end
+
+ class PStructElement < Puppet::Pops::Model::PopsObject
+ has_attr 'name', String, :lowerBound => 1
+ contains_one_uni 'type', PAbstractType
+
+ module ClassModule
+ def hash
+ [self.class, type, name].hash
+ end
+
+ def ==(o)
+ self.class == o.class && type == o.type && name == o.name
+ end
+ end
+ end
+
+ # @api public
+ class PStructType < PObjectType
+ contains_many_uni 'elements', PStructElement, :lowerBound => 1
+ has_attr 'hashed_elements', Object, :derived => true
+
+ module ClassModule
+ def hashed_elements_derived
+ @_hashed ||= elements.reduce({}) {|memo, e| memo[e.name] = e.type; memo }
+ @_hashed
+ end
+
+ def clear_hashed_elements
+ @_hashed = nil
+ end
+
+ def hash
+ [self.class, Set.new(elements)].hash
+ end
+
+ def ==(o)
+ self.class == o.class && hashed_elements == o.hashed_elements
+ end
+ end
+ end
+
+ # @api public
+ class PTupleType < PObjectType
+ contains_many_uni 'types', PAbstractType, :lowerBound => 1
+ # If set, describes repetition of the last type in types
+ contains_one_uni 'size_type', PIntegerType, :lowerBound => 0
+
module ClassModule
def hash
- [self.class, element_type].hash
+ [self.class, size_type, Set.new(types)].hash
end
def ==(o)
- self.class == o.class && element_type == o.element_type
+ self.class == o.class && types == o.types && size_type == o.size_type
end
end
end
# @api public
class PArrayType < PCollectionType
module ClassModule
def hash
- [self.class, element_type].hash
+ [self.class, self.element_type, self.size_type].hash
end
def ==(o)
- self.class == o.class && element_type == o.element_type
+ self.class == o.class && self.element_type == o.element_type && self.size_type == o.size_type
end
end
end
# @api public
class PHashType < PCollectionType
- contains_one_uni 'key_type', PObjectType
+ contains_one_uni 'key_type', PAbstractType
module ClassModule
def hash
- [self.class, key_type, element_type].hash
+ [self.class, key_type, self.element_type, self.size_type].hash
end
def ==(o)
- self.class == o.class && key_type == o.key_type && element_type == o.element_type
+ self.class == o.class &&
+ key_type == o.key_type &&
+ self.element_type == o.element_type &&
+ self.size_type == o.size_type
end
end
end
# @api public
class PRubyType < PObjectType
has_attr 'ruby_class', String
module ClassModule
def hash
[self.class, ruby_class].hash
end
def ==(o)
self.class == o.class && ruby_class == o.ruby_class
end
end
+ end
+
+ # Abstract representation of a type that can be placed in a Catalog.
+ # @api public
+ #
+ class PCatalogEntryType < PObjectType
+ end
+
+ # Represents a (host-) class in the Puppet Language.
+ # @api public
+ #
+ class PHostClassType < PCatalogEntryType
+ has_attr 'class_name', String
+ # contains_one_uni 'super_type', PHostClassType
+ module ClassModule
+ def hash
+ [self.class, host_class].hash
+ end
+ def ==(o)
+ self.class == o.class && class_name == o.class_name
+ end
+ end
+ end
+ # Represents a Resource Type in the Puppet Language
+ # @api public
+ #
+ class PResourceType < PCatalogEntryType
+ has_attr 'type_name', String
+ has_attr 'title', String
+ module ClassModule
+ def hash
+ [self.class, type_name, title].hash
+ end
+ def ==(o)
+ self.class == o.class && type_name == o.type_name && title == o.title
+ end
+ end
end
+
+ # Represents a type that accept PNilType instead of the type parameter
+ # required_type - is a short hand for Variant[T, Undef]
+ #
+ class POptionalType < PAbstractType
+ contains_one_uni 'optional_type', PAbstractType
+ module ClassModule
+ def hash
+ [self.class, optional_type].hash
+ end
+
+ def ==(o)
+ self.class == o.class && optional_type == o.optional_type
+ end
+ end
+ end
+
end
diff --git a/lib/puppet/pops/utils.rb b/lib/puppet/pops/utils.rb
index 01a540432..45e166984 100644
--- a/lib/puppet/pops/utils.rb
+++ b/lib/puppet/pops/utils.rb
@@ -1,104 +1,120 @@
# Provides utility methods
module Puppet::Pops::Utils
# Can the given o be converted to numeric? (or is numeric already)
# Accepts a leading '::'
# Returns a boolean if the value is numeric
# If testing if value can be converted it is more efficient to call {#to_n} or {#to_n_with_radix} directly
# and check if value is nil.
def self.is_numeric?(o)
case o
when Numeric, Integer, Fixnum, Float
!!o
else
!!Puppet::Pops::Patterns::NUMERIC.match(relativize_name(o.to_s))
end
end
- # To LiteralNumber with radix, or nil if not a number.
- # If the value is already a number it is returned verbatim with a radix of 10.
+ # To Numeric with radix, or nil if not a number.
+ # If the value is already Numeric it is returned verbatim with a radix of 10.
# @param o [String, Number] a string containing a number in octal, hex, integer (decimal) or floating point form
# @return [Array<Number, Integer>, nil] array with converted number and radix, or nil if not possible to convert
# @api public
#
def self.to_n_with_radix o
begin
case o
when String
match = Puppet::Pops::Patterns::NUMERIC.match(relativize_name(o))
if !match
nil
elsif match[3].to_s.length > 0
# Use default radix (default is decimal == 10) for floats
[Float(match[0]), 10]
else
# Set radix (default is decimal == 10)
radix = 10
if match[1].to_s.length > 0
radix = 16
- elsif match[2].to_s.length > 1 && match[2][0] == '0'
+ elsif match[2].to_s.length > 1 && match[2][0,1] == '0'
radix = 8
end
- [Integer(match[0], radix), radix]
+ # Ruby 1.8.7 does not have a second argument to Kernel method that creates an
+ # integer from a string, it relies on the prefix 0x, 0X, 0 (and unsupported in puppet binary 'b')
+ # We have the correct string here, match[0] is safe to parse without passing on radix
+ [Integer(match[0]), radix]
end
when Numeric, Fixnum, Integer, Float
# Impossible to calculate radix, assume decimal
[o, 10]
else
nil
end
rescue ArgumentError
nil
end
end
# To Numeric (or already numeric)
# Returns nil if value is not numeric, else an Integer or Float
# A leading '::' is accepted (and ignored)
#
def self.to_n o
begin
case o
when String
match = Puppet::Pops::Patterns::NUMERIC.match(relativize_name(o))
if !match
nil
elsif match[3].to_s.length > 0
Float(match[0])
else
Integer(match[0])
end
when Numeric, Fixnum, Integer, Float
o
else
nil
end
rescue ArgumentError
nil
end
end
# is the name absolute (i.e. starts with ::)
def self.is_absolute? name
name.start_with? "::"
end
def self.name_to_segments name
name.split("::")
end
def self.relativize_name name
is_absolute?(name) ? name[2..-1] : name
end
- # Finds an adapter for o or for one of its containers, or nil, if none of the containers
+ # Finds an existing adapter for o or for one of its containers, or nil, if none of the containers
# was adapted with the given adapter.
# This method can only be used with objects that respond to `:eContainer`.
- # with true, and Adaptable#adapters.
+ # with true.
+ #
+ # @see #find_closest_positioned
#
def self.find_adapter(o, adapter)
- return nil unless o
+ return nil if o.nil? || (o.is_a?(Array) && o.empty?)
a = adapter.get(o)
return a if a
return find_adapter(o.eContainer, adapter)
end
+
+ # Finds the closest positioned Puppet::Pops::Model::Positioned object, or object decorated with
+ # a SourcePosAdapter, and returns
+ # a SourcePosAdapter for the first found, or nil if not found.
+ #
+ def self.find_closest_positioned(o)
+ return nil if o.nil? || o.is_a?(Puppet::Pops::Model::Program) || (o.is_a?(Array) && o.empty?)
+ return find_adapter(o, Puppet::Pops::Adapters::SourcePosAdapter) unless o.is_a?(Puppet::Pops::Model::Positioned)
+ o.offset.nil? ? find_closest_positioned(o.eContainer) : Puppet::Pops::Adapters::SourcePosAdapter.adapt(o)
+ end
+
end
diff --git a/lib/puppet/pops/validation.rb b/lib/puppet/pops/validation.rb
index f003c1d42..eddd7ab2d 100644
--- a/lib/puppet/pops/validation.rb
+++ b/lib/puppet/pops/validation.rb
@@ -1,426 +1,432 @@
# A module with base functionality for validation of a model.
#
# * **Factory** - an abstract factory implementation that makes it easier to create a new validation factory.
# * **SeverityProducer** - produces a severity (:error, :warning, :ignore) for a given Issue
# * **DiagnosticProducer** - produces a Diagnostic which binds an Issue to an occurrence of that issue
# * **Acceptor** - the receiver/sink/collector of computed diagnostics
# * **DiagnosticFormatter** - produces human readable output for a Diagnostic
#
module Puppet::Pops::Validation
# This class is an abstract base implementation of a _model validation factory_ that creates a validator instance
# and associates it with a fully configured DiagnosticProducer.
#
# A _validator_ is responsible for validating a model. There may be different versions of validation available
# for one and the same model; e.g. different semantics for different puppet versions, or different types of
# validation configuration depending on the context/type of validation that should be performed (static, vs. runtime, etc.).
#
# This class is abstract and must be subclassed. The subclass must implement the methods
# {#label_provider} and {#checker}. It is also expected that the sublcass will override
# the severity_producer and configure the issues that should be reported as errors (i.e. if they should be ignored, produce
# a warning, or a deprecation warning).
#
# @abstract Subclass must implement {#checker}, and {#label_provider}
# @api public
#
class Factory
# Produces a validator with the given acceptor as the recipient of produced diagnostics.
# The acceptor is where detected issues are received (and typically collected).
#
# @param acceptor [Acceptor] the acceptor is the receiver of all detected issues
# @return [#validate] a validator responding to `validate(model)`
#
# @api public
#
def validator(acceptor)
checker(diagnostic_producer(acceptor))
end
# Produces the diagnostics producer to use given an acceptor of issues.
#
# @param acceptor [Acceptor] the acceptor is the receiver of all detected issues
# @return [DiagnosticProducer] a detector of issues
#
# @api public
#
def diagnostic_producer(acceptor)
Puppet::Pops::Validation::DiagnosticProducer.new(acceptor, severity_producer(), label_provider())
end
# Produces the SeverityProducer to use
# Subclasses should implement and add specific overrides
#
# @return [SeverityProducer] a severity producer producing error, warning or ignore per issue
#
# @api public
#
def severity_producer
Puppet::Pops::Validation::SeverityProducer.new
end
# Produces the checker to use.
#
# @abstract
#
# @api public
#
def checker(diagnostic_producer)
raise NoMethodError, "checker"
end
# Produces the label provider to use.
#
# @abstract
#
# @api public
#
def label_provider
raise NoMethodError, "label_provider"
end
end
# Decides on the severity of a given issue.
# The produced severity is one of `:error`, `:warning`, or `:ignore`.
# By default, a severity of `:error` is produced for all issues. To configure the severity
# of an issue call `#severity=(issue, level)`.
#
# @return [Symbol] a symbol representing the severity `:error`, `:warning`, or `:ignore`
#
# @api public
#
class SeverityProducer
+ @@severity_hash = {:ignore => true, :warning => true, :error => true, :deprecation => true }
# Creates a new instance where all issues are diagnosed as :error unless overridden.
# @api public
#
def initialize
# If diagnose is not set, the default is returned by the block
@severities = Hash.new :error
end
# Returns the severity of the given issue.
# @return [Symbol] severity level :error, :warning, or :ignore
# @api public
#
def severity(issue)
assert_issue(issue)
@severities[issue]
end
# @see {#severity}
# @api public
#
def [] issue
severity issue
end
# Override a default severity with the given severity level.
#
# @param issue [Puppet::Pops::Issues::Issue] the issue for which to set severity
# @param level [Symbol] the severity level (:error, :warning, or :ignore).
# @api public
#
def []=(issue, level)
- assert_issue(issue)
- assert_severity(level)
+ raise Puppet::DevError.new("Attempt to set validation severity for something that is not an Issue. (Got #{issue.class})") unless issue.is_a? Puppet::Pops::Issues::Issue
+ raise Puppet::DevError.new("Illegal severity level: #{option}") unless @@severity_hash[level]
raise Puppet::DevError.new("Attempt to demote the hard issue '#{issue.issue_code}' to #{level}") unless issue.demotable? || level == :error
@severities[issue] = level
end
# Returns `true` if the issue should be reported or not.
# @return [Boolean] this implementation returns true for errors and warnings
#
# @api public
#
def should_report? issue
- diagnose = self[issue]
+ diagnose = @severities[issue]
diagnose == :error || diagnose == :warning || diagnose == :deprecation
end
# Checks if the given issue is valid.
# @api private
#
def assert_issue issue
raise Puppet::DevError.new("Attempt to get validation severity for something that is not an Issue. (Got #{issue.class})") unless issue.is_a? Puppet::Pops::Issues::Issue
end
# Checks if the given severity level is valid.
# @api private
#
def assert_severity level
- raise Puppet::DevError.new("Illegal severity level: #{option}") unless [:ignore, :warning, :error, :deprecation].include? level
+ raise Puppet::DevError.new("Illegal severity level: #{option}") unless @@severity_hash[level]
end
end
# A producer of diagnostics.
# An producer of diagnostics is given each issue occurrence as they are found by a diagnostician/validator. It then produces
# a Diagnostic, which it passes on to a configured Acceptor.
#
# This class exists to aid a diagnostician/validator which will typically first check if a particular issue
# will be accepted at all (before checking for an occurrence of the issue; i.e. to perform check avoidance for expensive checks).
# A validator passes an instance of Issue, the semantic object (the "culprit"), a hash with arguments, and an optional
# exception. The semantic object is used to determine the location of the occurrence of the issue (file/line), and it
# sets keys in the given argument hash that may be used in the formatting of the issue message.
#
class DiagnosticProducer
# A producer of severity for a given issue
# @return [SeverityProducer]
#
attr_reader :severity_producer
# A producer of labels for objects involved in the issue
# @return [LabelProvider]
#
attr_reader :label_provider
# Initializes this producer.
#
# @param acceptor [Acceptor] a sink/collector of diagnostic results
# @param severity_producer [SeverityProducer] the severity producer to use to determine severity of a given issue
# @param label_provider [LabelProvider] a provider of model element type to human readable label
#
def initialize(acceptor, severity_producer, label_provider)
@acceptor = acceptor
@severity_producer = severity_producer
@label_provider = label_provider
end
def accept(issue, semantic, arguments={}, except=nil)
return unless will_accept? issue
# Set label provider unless caller provided a special label provider
arguments[:label] ||= @label_provider
arguments[:semantic] ||= semantic
# A detail message is always provided, but is blank by default.
+ # TODO: this support is questionable, it requires knowledge that :detail is special
arguments[:detail] ||= ''
- origin_adapter = Puppet::Pops::Utils.find_adapter(semantic, Puppet::Pops::Adapters::OriginAdapter)
- file = origin_adapter ? origin_adapter.origin : nil
- source_pos = Puppet::Pops::Utils.find_adapter(semantic, Puppet::Pops::Adapters::SourcePosAdapter)
+ source_pos = Puppet::Pops::Utils.find_closest_positioned(semantic)
+ file = source_pos ? source_pos.locator.file : nil
+
severity = @severity_producer.severity(issue)
- @acceptor.accept(Diagnostic.new(severity, issue, file, source_pos, arguments))
+ @acceptor.accept(Diagnostic.new(severity, issue, file, source_pos, arguments, except))
end
def will_accept? issue
@severity_producer.should_report? issue
end
end
class Diagnostic
attr_reader :severity
attr_reader :issue
attr_reader :arguments
attr_reader :exception
attr_reader :file
attr_reader :source_pos
def initialize severity, issue, file, source_pos, arguments={}, exception=nil
@severity = severity
@issue = issue
@file = file
@source_pos = source_pos
@arguments = arguments
+ # TODO: Currently unused, the intention is to provide more information (stack backtrace, etc.) when
+ # debugging or similar - this to catch internal problems reported as higher level issues.
@exception = exception
end
end
# Formats a diagnostic for output.
# Produces a diagnostic output typical for a compiler (suitable for interpretation by tools)
# The format is:
# `file:line:pos: Message`, where pos, line and file are included if available.
#
class DiagnosticFormatter
def format diagnostic
"#{loc(diagnostic)} #{format_severity(diagnostic)}#{format_message(diagnostic)}"
end
def format_message diagnostic
diagnostic.issue.format(diagnostic.arguments)
end
# This produces "Deprecation notice: " prefix if the diagnostic has :deprecation severity, otherwise "".
# The idea is that all other diagnostics are emitted with the methods Puppet.err (or an exception), and
# Puppet.warning.
# @note Note that it is not a good idea to use Puppet.deprecation_warning as it is for internal deprecation.
#
def format_severity diagnostic
diagnostic.severity == :deprecation ? "Deprecation notice: " : ""
end
def format_location diagnostic
file = diagnostic.file
+ file = (file.is_a?(String) && file.empty?) ? nil : file
line = pos = nil
if diagnostic.source_pos
line = diagnostic.source_pos.line
pos = diagnostic.source_pos.pos
end
if file && line && pos
"#{file}:#{line}:#{pos}:"
elsif file && line
"#{file}:#{line}:"
elsif file
"#{file}:"
else
""
end
end
end
# Produces a diagnostic output in the "puppet style", where the location is appended with an "at ..." if the
# location is known.
#
class DiagnosticFormatterPuppetStyle < DiagnosticFormatter
def format diagnostic
if (location = format_location diagnostic) != ""
"#{format_severity(diagnostic)}#{format_message(diagnostic)}#{location}"
else
format_message(diagnostic)
end
end
# The somewhat (machine) unusable format in current use by puppet.
# have to be used here for backwards compatibility.
def format_location diagnostic
file = diagnostic.file
+ file = (file.is_a?(String) && file.empty?) ? nil : file
line = pos = nil
if diagnostic.source_pos
line = diagnostic.source_pos.line
pos = diagnostic.source_pos.pos
end
if file && line && pos
" at #{file}:#{line}:#{pos}"
elsif file and line
" at #{file}:#{line}"
elsif line && pos
" at line #{line}:#{pos}"
elsif line
" at line #{line}"
elsif file
" in #{file}"
else
""
end
end
end
# An acceptor of diagnostics.
# An acceptor of diagnostics is given each issue as they are found by a diagnostician/validator. An
# acceptor can collect all found issues, or decide to collect a few and then report, or give up as the first issue
# if found.
# This default implementation collects all diagnostics in the order they are produced, and can then
# answer questions about what was diagnosed.
#
class Acceptor
# All diagnostic in the order they were issued
attr_reader :diagnostics
# The number of :warning severity issues + number of :deprecation severity issues
attr_reader :warning_count
# The number of :error severity issues
attr_reader :error_count
# Initializes this diagnostics acceptor.
# By default, the acceptor is configured with a default severity producer.
# @param severity_producer [SeverityProducer] the severity producer to use to determine severity of an issue
#
# TODO add semantic_label_provider
#
def initialize()
@diagnostics = []
@error_count = 0
@warning_count = 0
end
# Returns true when errors have been diagnosed.
def errors?
@error_count > 0
end
# Returns true when warnings have been diagnosed.
def warnings?
@warning_count > 0
end
# Returns true when errors and/or warnings have been diagnosed.
def errors_or_warnings?
errors? || warnings?
end
# Returns the diagnosed errors in the order thwy were reported.
def errors
@diagnostics.select {|d| d.severity == :error }
end
# Returns the diagnosed warnings in the order thwy were reported.
# (This includes :warning and :deprecation severity)
def warnings
@diagnostics.select {|d| d.severity == :warning || d.severity == :deprecation }
end
def errors_and_warnings
@diagnostics.select {|d| d.severity != :ignore }
end
# Returns the ignored diagnostics in the order thwy were reported (if reported at all)
def ignored
@diagnostics.select {|d| d.severity == :ignore }
end
# Add a diagnostic, or all diagnostics from another acceptor to the set of diagnostics
# @param diagnostic [Puppet::Pops::Validation::Diagnostic, Puppet::Pops::Validation::Acceptor] diagnostic(s) that should be accepted
def accept(diagnostic)
if diagnostic.is_a?(Acceptor)
diagnostic.diagnostics.each {|d| self.send(d.severity, d)}
else
self.send(diagnostic.severity, diagnostic)
end
end
# Prunes the contain diagnostics by removing those for which the given block returns true.
# The internal statistics is updated as a consequence of removing.
# @return [Array<Puppet::Pops::Validation::Diagnostic, nil] the removed set of diagnostics or nil if nothing was removed
#
def prune(&block)
removed = []
@diagnostics.delete_if do |d|
if should_remove = yield(d)
removed << d
end
should_remove
end
removed.each do |d|
case d.severity
when :error
@error_count -= 1
when :warning
@warning_count -= 1
# there is not ignore_count
end
end
removed.empty? ? nil : removed
end
private
def ignore diagnostic
@diagnostics << diagnostic
end
def error diagnostic
@diagnostics << diagnostic
@error_count += 1
end
def warning diagnostic
@diagnostics << diagnostic
@warning_count += 1
end
def deprecation diagnostic
warning diagnostic
end
end
end
diff --git a/lib/puppet/pops/validation/checker3_1.rb b/lib/puppet/pops/validation/checker3_1.rb
index ecc71faad..77f643fb1 100644
--- a/lib/puppet/pops/validation/checker3_1.rb
+++ b/lib/puppet/pops/validation/checker3_1.rb
@@ -1,553 +1,558 @@
# A Validator validates a model.
#
# Validation is performed on each model element in isolation. Each method should validate the model element's state
# but not validate its referenced/contained elements except to check their validity in their respective role.
# The intent is to drive the validation with a tree iterator that visits all elements in a model.
#
#
# TODO: Add validation of multiplicities - this is a general validation that can be checked for all
# Model objects via their metamodel. (I.e an extra call to multiplicity check in polymorph check).
# This is however mostly valuable when validating model to model transformations, and is therefore T.B.D
#
class Puppet::Pops::Validation::Checker3_1
Issues = Puppet::Pops::Issues
Model = Puppet::Pops::Model
attr_reader :acceptor
# Initializes the validator with a diagnostics producer. This object must respond to
# `:will_accept?` and `:accept`.
#
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, 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)
@@relation_visitor ||= Puppet::Pops::Visitor.new(nil, "relation", 1, 1)
@acceptor = diagnostics_producer
end
# Validates the entire model by visiting each model element and calling `check`.
# The result is collected (or acted on immediately) by the configured diagnostic provider/acceptor
# given when creating this Checker.
#
def validate(model)
# tree iterate the model, and call check for each element
check(model)
model.eAllContents.each {|m| check(m) }
end
# Performs regular validity check
def check(o)
- @@check_visitor.visit_this(self, o)
+ @@check_visitor.visit_this_0(self, o)
end
# Performs check if this is a vaid hostname expression
# @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)
+ @@hostname_visitor.visit_this_2(self, o, semantic, single_feature_name)
end
# Performs check if this is valid as a query
def query(o)
- @@query_visitor.visit_this(self, o)
+ @@query_visitor.visit_this_0(self, o)
end
# Performs check if this is valid as a relationship side
def relation(o, container)
- @@relation_visitor.visit_this(self, o, container)
+ @@relation_visitor.visit_this_1(self, o, container)
end
# Performs check if this is valid as a rvalue
def rvalue(o)
- @@rvalue_visitor.visit_this(self, o)
+ @@rvalue_visitor.visit_this_0(self, o)
end
# Performs check if this is valid as a container of a definition (class, define, node)
def top(o, definition)
- @@top_visitor.visit_this(self, o, definition)
+ @@top_visitor.visit_this_1(self, o, definition)
end
# Checks the LHS of an assignment (is it assignable?).
# If args[0] is true, assignment via index is checked.
#
- def assign(o, *args)
- @@assignment_visitor.visit_this(self, o, *args)
+ def assign(o, via_index = false)
+ @@assignment_visitor.visit_this_1(self, o, via_index)
end
#---ASSIGNMENT CHECKS
- def assign_VariableExpression(o, *args)
+ def assign_VariableExpression(o, via_index)
varname_string = varname_to_s(o.expr)
if varname_string =~ /^[0-9]+$/
acceptor.accept(Issues::ILLEGAL_NUMERIC_ASSIGNMENT, o, :varname => varname_string)
end
# Can not assign to something in another namespace (i.e. a '::' in the name is not legal)
if acceptor.will_accept? Issues::CROSS_SCOPE_ASSIGNMENT
if varname_string =~ /::/
acceptor.accept(Issues::CROSS_SCOPE_ASSIGNMENT, o, :name => varname_string)
end
end
# TODO: Could scan for reassignment of the same variable if done earlier in the same container
# Or if assigning to a parameter (more work).
# TODO: Investigate if there are invalid cases for += assignment
end
- def assign_AccessExpression(o, *args)
+ def assign_AccessExpression(o, via_index)
# Are indexed assignments allowed at all ? $x[x] = '...'
if acceptor.will_accept? Issues::ILLEGAL_INDEXED_ASSIGNMENT
acceptor.accept(Issues::ILLEGAL_INDEXED_ASSIGNMENT, o)
else
# Then the left expression must be assignable-via-index
assign(o.left_expr, true)
end
end
- def assign_Object(o, *args)
+ def assign_Object(o, via_index)
# Can not assign to anything else (differentiate if this is via index or not)
# i.e. 10 = 'hello' vs. 10['x'] = 'hello' (the root is reported as being in error in both cases)
#
- acceptor.accept(args[0] ? Issues::ILLEGAL_ASSIGNMENT_VIA_INDEX : Issues::ILLEGAL_ASSIGNMENT, o)
+ acceptor.accept(via_index ? Issues::ILLEGAL_ASSIGNMENT_VIA_INDEX : Issues::ILLEGAL_ASSIGNMENT, o)
end
#---CHECKS
def check_Object(o)
end
def check_Factory(o)
check(o.current)
end
def check_AccessExpression(o)
# Check multiplicity of keys
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.left_expr.value)
when Model::QualifiedReference
# ok, allows many - this is a resource reference
else
# i.e. for any other expression that may produce an array or hash
if o.keys.size > 1
acceptor.accept(Issues::UNSUPPORTED_RANGE, o, :count => o.keys.size)
end
if o.keys.size < 1
acceptor.accept(Issues::MISSING_INDEX, o)
end
end
end
def check_AssignmentExpression(o)
+ acceptor.accept(Issues::UNSUPPORTED_OPERATOR, o, {:operator => o.operator}) unless [:'=', :'+='].include? o.operator
assign(o.left_expr)
rvalue(o.right_expr)
end
# Checks that operation with :+> is contained in a ResourceOverride or Collector.
#
# Parent of an AttributeOperation can be one of:
# * CollectExpression
# * ResourceOverride
# * ResourceBody (ILLEGAL this is a regular resource expression)
# * ResourceDefaults (ILLEGAL)
#
def check_AttributeOperation(o)
if o.operator == :'+>'
# Append operator use is constrained
parent = o.eContainer
unless parent.is_a?(Model::CollectExpression) || parent.is_a?(Model::ResourceOverrideExpression)
acceptor.accept(Issues::ILLEGAL_ATTRIBUTE_APPEND, o, {:name=>o.attribute_name, :parent=>parent})
end
end
rvalue(o.value_expr)
end
def check_BinaryExpression(o)
rvalue(o.left_expr)
rvalue(o.right_expr)
end
def check_CallNamedFunctionExpression(o)
unless o.functor_expr.is_a? Model::QualifiedName
acceptor.accept(Issues::ILLEGAL_EXPRESSION, o.functor_expr, :feature => 'function name', :container => o)
end
end
def check_MethodCallExpression(o)
unless o.functor_expr.is_a? Model::QualifiedName
acceptor.accept(Issues::ILLEGAL_EXPRESSION, o.functor_expr, :feature => 'function name', :container => o)
end
end
def check_CaseExpression(o)
# There should only be one LiteralDefault case option value
# TODO: Implement this check
end
def check_CollectExpression(o)
unless o.type_expr.is_a? Model::QualifiedReference
acceptor.accept(Issues::ILLEGAL_EXPRESSION, o.type_expr, :feature=> 'type name', :container => o)
end
# If a collect expression tries to collect exported resources and storeconfigs is not on
# then it will not work... This was checked in the parser previously. This is a runtime checking
# thing as opposed to a language thing.
if acceptor.will_accept?(Issues::RT_NO_STORECONFIGS) && o.query.is_a?(Model::ExportedQuery)
acceptor.accept(Issues::RT_NO_STORECONFIGS, o)
end
end
# Only used for function names, grammar should not be able to produce something faulty, but
# check anyway if model is created programatically (it will fail in transformation to AST for sure).
def check_NamedAccessExpression(o)
name = o.right_expr
unless name.is_a? Model::QualifiedName
acceptor.accept(Issues::ILLEGAL_EXPRESSION, name, :feature=> 'function name', :container => o.eContainer)
end
end
# for 'class' and 'define'
def check_NamedDefinition(o)
top(o.eContainer, o)
if (acceptor.will_accept? Issues::NAME_WITH_HYPHEN) && o.name.include?('-')
acceptor.accept(Issues::NAME_WITH_HYPHEN, o, {:name => o.name})
end
end
def check_ImportExpression(o)
o.files.each do |f|
unless f.is_a? Model::LiteralString
acceptor.accept(Issues::ILLEGAL_EXPRESSION, f, :feature => 'file name', :container => o)
end
end
end
def check_InstanceReference(o)
# TODO: Original warning is :
# Puppet.warning addcontext("Deprecation notice: Resource references should now be capitalized")
# This model element is not used in the egrammar.
# Either implement checks or deprecate the use of InstanceReference (the same is acheived by
# transformation of AccessExpression when used where an Instance/Resource reference is allowed.
#
end
# Restrictions on hash key are because of the strange key comparisons/and merge rules in the AST evaluation
# (Even the allowed ones are handled in a strange way).
#
def transform_KeyedEntry(o)
case o.key
when Model::QualifiedName
when Model::LiteralString
when Model::LiteralNumber
when Model::ConcatenatedString
else
acceptor.accept(Issues::ILLEGAL_EXPRESSION, o.key, :feature => 'hash key', :container => o.eContainer)
end
end
# A Lambda is a Definition, but it may appear in other scopes that top scope (Which check_Definition asserts).
#
def check_LambdaExpression(o)
end
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
# No checking takes place - all expressions using a QualifiedName need to check. This because the
# rules are slightly different depending on the container (A variable allows a numeric start, but not
# other names). This means that (if the lexer/parser so chooses) a QualifiedName
# can be anything when it represents a Bare Word and evaluates to a String.
#
def check_QualifiedName(o)
end
# Checks that the value is a valid UpperCaseWord (a CLASSREF), and optionally if it contains a hypen.
# DOH: QualifiedReferences are created with LOWER CASE NAMES at parse time
def check_QualifiedReference(o)
# Is this a valid qualified name?
if o.value !~ Puppet::Pops::Patterns::CLASSREF
acceptor.accept(Issues::ILLEGAL_CLASSREF, o, {:name=>o.value})
elsif (acceptor.will_accept? Issues::NAME_WITH_HYPHEN) && o.value.include?('-')
acceptor.accept(Issues::NAME_WITH_HYPHEN, o, {:name => o.value})
end
end
def check_QueryExpression(o)
query(o.expr) if o.expr # is optional
end
def relation_Object(o, rel_expr)
acceptor.accept(Issues::ILLEGAL_EXPRESSION, o, {:feature => o.eContainingFeature, :container => rel_expr})
end
def relation_AccessExpression(o, rel_expr); end
def relation_CollectExpression(o, rel_expr); end
def relation_VariableExpression(o, rel_expr); end
def relation_LiteralString(o, rel_expr); end
def relation_ConcatenatedStringExpression(o, rel_expr); end
def relation_SelectorExpression(o, rel_expr); end
def relation_CaseExpression(o, rel_expr); end
def relation_ResourceExpression(o, rel_expr); end
def relation_RelationshipExpression(o, rel_expr); end
def check_Parameter(o)
if o.name =~ /^[0-9]+$/
acceptor.accept(Issues::ILLEGAL_NUMERIC_PARAMETER, o, :name => o.name)
end
end
#relationship_side: resource
# | resourceref
# | collection
# | variable
# | quotedtext
# | selector
# | casestatement
# | hasharrayaccesses
def check_RelationshipExpression(o)
relation(o.left_expr, o)
relation(o.right_expr, o)
end
def check_ResourceExpression(o)
# A resource expression must have a lower case NAME as its type e.g. 'file { ... }'
unless o.type_name.is_a? Model::QualifiedName
acceptor.accept(Issues::ILLEGAL_EXPRESSION, o.type_name, :feature => 'resource type', :container => o)
end
# This is a runtime check - the model is valid, but will have runtime issues when evaluated
# and storeconfigs is not set.
if acceptor.will_accept?(Issues::RT_NO_STORECONFIGS) && o.exported
acceptor.accept(Issues::RT_NO_STORECONFIGS_EXPORT, o)
end
end
def check_ResourceDefaultsExpression(o)
if o.form && o.form != :regular
acceptor.accept(Issues::NOT_VIRTUALIZEABLE, o)
end
end
# Transformation of SelectorExpression is limited to certain types of expressions.
# This is probably due to constraints in the old grammar rather than any real concerns.
def select_SelectorExpression(o)
case o.left_expr
when Model::CallNamedFunctionExpression
when Model::AccessExpression
when Model::VariableExpression
when Model::ConcatenatedString
else
acceptor.accept(Issues::ILLEGAL_EXPRESSION, o.left_expr, :feature => 'left operand', :container => o)
end
end
def check_UnaryExpression(o)
rvalue(o.expr)
end
def check_UnlessExpression(o)
# TODO: Unless may not have an elsif
# TODO: 3.x unless may not have an else
end
def check_VariableExpression(o)
# The expression must be a qualified name
if !o.expr.is_a? Model::QualifiedName
acceptor.accept(Issues::ILLEGAL_EXPRESSION, o, :feature => 'name', :container => o)
else
# Note, that if it later becomes illegal with hyphen in any name, this special check
# can be skipped in favor of the check in QualifiedName, which is now not done if contained in
# a VariableExpression
name = o.expr.value
if (acceptor.will_accept? Issues::VAR_WITH_HYPHEN) && name.include?('-')
acceptor.accept(Issues::VAR_WITH_HYPHEN, o, {:name => name})
end
end
end
#--- HOSTNAME CHECKS
# Transforms Array of host matching expressions into a (Ruby) array of AST::HostName
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, 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.
#
if o =~ Puppet::Pops::Patterns::ILLEGAL_HOSTNAME_CHARS
acceptor.accept(Issues::ILLEGAL_HOSTNAME_CHARS, semantic, :hostname => o)
end
end
def hostname_LiteralValue(o, semantic, single_feature_name)
hostname_String(o.value.to_s, o, single_feature_name)
end
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)
elsif o.segments.size() != 1
# corner case, bad model, concatenation of several plain strings
acceptor.accept(Issues::ILLEGAL_HOSTNAME_INTERPOLATION, o)
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], false)
end
end
def hostname_QualifiedName(o, semantic, single_feature_name)
hostname_String(o.value.to_s, o, single_feature_name)
end
def hostname_QualifiedReference(o, semantic, single_feature_name)
hostname_String(o.value.to_s, o, single_feature_name)
end
def hostname_LiteralNumber(o, semantic, single_feature_name)
# always ok
end
def hostname_LiteralDefault(o, semantic, single_feature_name)
# always ok
end
def hostname_LiteralRegularExpression(o, semantic, single_feature_name)
# always ok
end
def hostname_Object(o, semantic, single_feature_name)
acceptor.accept(Issues::ILLEGAL_EXPRESSION, o, {:feature=> single_feature_name || 'hostname', :container=>semantic})
end
#---QUERY CHECKS
# Anything not explicitly allowed is flagged as error.
def query_Object(o)
acceptor.accept(Issues::ILLEGAL_QUERY_EXPRESSION, o)
end
# Puppet AST only allows == and !=
#
def query_ComparisonExpression(o)
acceptor.accept(Issues::ILLEGAL_QUERY_EXPRESSION, o) unless [:'==', :'!='].include? o.operator
end
# Allows AND, OR, and checks if left/right are allowed in query.
def query_BooleanExpression(o)
query o.left_expr
query o.right_expr
end
def query_ParenthesizedExpression(o)
query(o.expr)
end
def query_VariableExpression(o); end
def query_QualifiedName(o); end
def query_LiteralNumber(o); end
def query_LiteralString(o); end
def query_LiteralBoolean(o); end
#---RVALUE CHECKS
# By default, all expressions are reported as being rvalues
# Implement specific rvalue checks for those that are not.
#
def rvalue_Expression(o); end
def rvalue_ImportExpression(o) ; acceptor.accept(Issues::NOT_RVALUE, o) ; end
def rvalue_BlockExpression(o) ; acceptor.accept(Issues::NOT_RVALUE, o) ; end
def rvalue_CaseExpression(o) ; acceptor.accept(Issues::NOT_RVALUE, o) ; end
def rvalue_IfExpression(o) ; acceptor.accept(Issues::NOT_RVALUE, o) ; end
def rvalue_UnlessExpression(o) ; acceptor.accept(Issues::NOT_RVALUE, o) ; end
def rvalue_ResourceExpression(o) ; acceptor.accept(Issues::NOT_RVALUE, o) ; end
def rvalue_ResourceDefaultsExpression(o); acceptor.accept(Issues::NOT_RVALUE, o) ; end
def rvalue_ResourceOverrideExpression(o); acceptor.accept(Issues::NOT_RVALUE, o) ; end
def rvalue_CollectExpression(o) ; acceptor.accept(Issues::NOT_RVALUE, o) ; end
def rvalue_Definition(o) ; acceptor.accept(Issues::NOT_RVALUE, o) ; end
def rvalue_NodeDefinition(o) ; acceptor.accept(Issues::NOT_RVALUE, o) ; end
def rvalue_UnaryExpression(o) ; rvalue o.expr ; end
#---TOP CHECK
def top_NilClass(o, definition)
# ok, reached the top, no more parents
end
def top_Object(o, definition)
# fail, reached a container that is not top level
acceptor.accept(Issues::NOT_TOP_LEVEL, definition)
end
def top_BlockExpression(o, definition)
# ok, if this is a block representing the body of a class, or is top level
top o.eContainer, definition
end
def top_HostClassDefinition(o, definition)
# ok, stop scanning parents
end
+ def top_Program(o, definition)
+ # ok
+ end
+
# A LambdaExpression is a BlockExpression, and this method is needed to prevent the polymorph method for BlockExpression
# to accept a lambda.
# A lambda can not iteratively create classes, nodes or defines as the lambda does not have a closure.
#
def top_LambdaExpression(o, definition)
# fail, stop scanning parents
acceptor.accept(Issues::NOT_TOP_LEVEL, definition)
end
#--- NON POLYMORPH, NON CHECKING CODE
# Produces string part of something named, or nil if not a QualifiedName or QualifiedReference
#
def varname_to_s(o)
case o
when Model::QualifiedName
o.value
when Model::QualifiedReference
o.value
else
nil
end
end
end
diff --git a/lib/puppet/pops/validation/checker3_1.rb b/lib/puppet/pops/validation/checker4_0.rb
similarity index 73%
copy from lib/puppet/pops/validation/checker3_1.rb
copy to lib/puppet/pops/validation/checker4_0.rb
index ecc71faad..cb2c632a1 100644
--- a/lib/puppet/pops/validation/checker3_1.rb
+++ b/lib/puppet/pops/validation/checker4_0.rb
@@ -1,553 +1,514 @@
# A Validator validates a model.
#
# Validation is performed on each model element in isolation. Each method should validate the model element's state
# but not validate its referenced/contained elements except to check their validity in their respective role.
# The intent is to drive the validation with a tree iterator that visits all elements in a model.
#
#
# TODO: Add validation of multiplicities - this is a general validation that can be checked for all
# Model objects via their metamodel. (I.e an extra call to multiplicity check in polymorph check).
# This is however mostly valuable when validating model to model transformations, and is therefore T.B.D
#
-class Puppet::Pops::Validation::Checker3_1
+class Puppet::Pops::Validation::Checker4_0
Issues = Puppet::Pops::Issues
Model = Puppet::Pops::Model
attr_reader :acceptor
# Initializes the validator with a diagnostics producer. This object must respond to
# `:will_accept?` and `:accept`.
#
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, 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)
- @@relation_visitor ||= Puppet::Pops::Visitor.new(nil, "relation", 1, 1)
+ @@relation_visitor ||= Puppet::Pops::Visitor.new(nil, "relation", 0, 0)
@acceptor = diagnostics_producer
end
# Validates the entire model by visiting each model element and calling `check`.
# The result is collected (or acted on immediately) by the configured diagnostic provider/acceptor
# given when creating this Checker.
#
def validate(model)
# tree iterate the model, and call check for each element
check(model)
model.eAllContents.each {|m| check(m) }
end
# Performs regular validity check
def check(o)
- @@check_visitor.visit_this(self, o)
+ @@check_visitor.visit_this_0(self, o)
end
# Performs check if this is a vaid hostname expression
# @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)
+ @@hostname_visitor.visit_this_2(self, o, semantic, single_feature_name)
end
# Performs check if this is valid as a query
def query(o)
- @@query_visitor.visit_this(self, o)
+ @@query_visitor.visit_this_0(self, o)
end
# Performs check if this is valid as a relationship side
- def relation(o, container)
- @@relation_visitor.visit_this(self, o, container)
+ def relation(o)
+ @@relation_visitor.visit_this_0(self, o)
end
# Performs check if this is valid as a rvalue
def rvalue(o)
- @@rvalue_visitor.visit_this(self, o)
+ @@rvalue_visitor.visit_this_0(self, o)
end
# Performs check if this is valid as a container of a definition (class, define, node)
def top(o, definition)
- @@top_visitor.visit_this(self, o, definition)
+ @@top_visitor.visit_this_1(self, o, definition)
end
# Checks the LHS of an assignment (is it assignable?).
# If args[0] is true, assignment via index is checked.
#
- def assign(o, *args)
- @@assignment_visitor.visit_this(self, o, *args)
+ def assign(o, via_index = false)
+ @@assignment_visitor.visit_this_1(self, o, via_index)
end
#---ASSIGNMENT CHECKS
- def assign_VariableExpression(o, *args)
+ def assign_VariableExpression(o, via_index)
varname_string = varname_to_s(o.expr)
- if varname_string =~ /^[0-9]+$/
+ if varname_string =~ Puppet::Pops::Patterns::NUMERIC_VAR_NAME
acceptor.accept(Issues::ILLEGAL_NUMERIC_ASSIGNMENT, o, :varname => varname_string)
end
# Can not assign to something in another namespace (i.e. a '::' in the name is not legal)
if acceptor.will_accept? Issues::CROSS_SCOPE_ASSIGNMENT
if varname_string =~ /::/
acceptor.accept(Issues::CROSS_SCOPE_ASSIGNMENT, o, :name => varname_string)
end
end
# TODO: Could scan for reassignment of the same variable if done earlier in the same container
# Or if assigning to a parameter (more work).
# TODO: Investigate if there are invalid cases for += assignment
end
- def assign_AccessExpression(o, *args)
+ def assign_AccessExpression(o, via_index)
# Are indexed assignments allowed at all ? $x[x] = '...'
if acceptor.will_accept? Issues::ILLEGAL_INDEXED_ASSIGNMENT
acceptor.accept(Issues::ILLEGAL_INDEXED_ASSIGNMENT, o)
else
# Then the left expression must be assignable-via-index
assign(o.left_expr, true)
end
end
- def assign_Object(o, *args)
+ def assign_Object(o, via_index)
# Can not assign to anything else (differentiate if this is via index or not)
# i.e. 10 = 'hello' vs. 10['x'] = 'hello' (the root is reported as being in error in both cases)
#
- acceptor.accept(args[0] ? Issues::ILLEGAL_ASSIGNMENT_VIA_INDEX : Issues::ILLEGAL_ASSIGNMENT, o)
+ acceptor.accept(via_index ? Issues::ILLEGAL_ASSIGNMENT_VIA_INDEX : Issues::ILLEGAL_ASSIGNMENT, o)
end
#---CHECKS
def check_Object(o)
end
def check_Factory(o)
check(o.current)
end
def check_AccessExpression(o)
- # Check multiplicity of keys
- 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.left_expr.value)
- when Model::QualifiedReference
- # ok, allows many - this is a resource reference
-
- else
- # i.e. for any other expression that may produce an array or hash
- if o.keys.size > 1
- acceptor.accept(Issues::UNSUPPORTED_RANGE, o, :count => o.keys.size)
- end
- if o.keys.size < 1
- acceptor.accept(Issues::MISSING_INDEX, o)
- end
+ # Only min range is checked, all other checks are RT checks as they depend on the resulting type
+ # of the LHS.
+ if o.keys.size < 1
+ acceptor.accept(Issues::MISSING_INDEX, o)
end
end
def check_AssignmentExpression(o)
+ acceptor.accept(Issues::UNSUPPORTED_OPERATOR, o, {:operator => o.operator}) unless [:'=', :'+=', :'-='].include? o.operator
assign(o.left_expr)
rvalue(o.right_expr)
end
# Checks that operation with :+> is contained in a ResourceOverride or Collector.
#
# Parent of an AttributeOperation can be one of:
# * CollectExpression
# * ResourceOverride
# * ResourceBody (ILLEGAL this is a regular resource expression)
# * ResourceDefaults (ILLEGAL)
#
def check_AttributeOperation(o)
if o.operator == :'+>'
# Append operator use is constrained
parent = o.eContainer
unless parent.is_a?(Model::CollectExpression) || parent.is_a?(Model::ResourceOverrideExpression)
acceptor.accept(Issues::ILLEGAL_ATTRIBUTE_APPEND, o, {:name=>o.attribute_name, :parent=>parent})
end
end
rvalue(o.value_expr)
end
def check_BinaryExpression(o)
rvalue(o.left_expr)
rvalue(o.right_expr)
end
def check_CallNamedFunctionExpression(o)
- unless o.functor_expr.is_a? Model::QualifiedName
- acceptor.accept(Issues::ILLEGAL_EXPRESSION, o.functor_expr, :feature => 'function name', :container => o)
+ case o.functor_expr
+ when Puppet::Pops::Model::QualifiedName
+ # ok
+ nil
+ when Puppet::Pops::Model::RenderStringExpression
+ # helpful to point out this easy to make Epp error
+ acceptor.accept(Issues::ILLEGAL_EPP_PARAMETERS, o)
+ else
+ acceptor.accept(Issues::ILLEGAL_EXPRESSION, o.functor_expr, {:feature=>'function name', :container => o})
end
end
def check_MethodCallExpression(o)
unless o.functor_expr.is_a? Model::QualifiedName
acceptor.accept(Issues::ILLEGAL_EXPRESSION, o.functor_expr, :feature => 'function name', :container => o)
end
end
def check_CaseExpression(o)
+ rvalue(o.test)
# There should only be one LiteralDefault case option value
# TODO: Implement this check
end
+ def check_CaseOption(o)
+ o.values.each { |v| rvalue(v) }
+ end
+
def check_CollectExpression(o)
unless o.type_expr.is_a? Model::QualifiedReference
acceptor.accept(Issues::ILLEGAL_EXPRESSION, o.type_expr, :feature=> 'type name', :container => o)
end
# If a collect expression tries to collect exported resources and storeconfigs is not on
# then it will not work... This was checked in the parser previously. This is a runtime checking
# thing as opposed to a language thing.
if acceptor.will_accept?(Issues::RT_NO_STORECONFIGS) && o.query.is_a?(Model::ExportedQuery)
acceptor.accept(Issues::RT_NO_STORECONFIGS, o)
end
end
# Only used for function names, grammar should not be able to produce something faulty, but
# check anyway if model is created programatically (it will fail in transformation to AST for sure).
def check_NamedAccessExpression(o)
name = o.right_expr
unless name.is_a? Model::QualifiedName
acceptor.accept(Issues::ILLEGAL_EXPRESSION, name, :feature=> 'function name', :container => o.eContainer)
end
end
# for 'class' and 'define'
def check_NamedDefinition(o)
top(o.eContainer, o)
- if (acceptor.will_accept? Issues::NAME_WITH_HYPHEN) && o.name.include?('-')
- acceptor.accept(Issues::NAME_WITH_HYPHEN, o, {:name => o.name})
+ if o.name !~ Puppet::Pops::Patterns::CLASSREF
+ acceptor.accept(Issues::ILLEGAL_DEFINITION_NAME, o, {:name=>o.name})
end
end
- def check_ImportExpression(o)
- o.files.each do |f|
- unless f.is_a? Model::LiteralString
- acceptor.accept(Issues::ILLEGAL_EXPRESSION, f, :feature => 'file name', :container => o)
- end
- end
+ def check_IfExpression(o)
+ rvalue(o.test)
end
- def check_InstanceReference(o)
- # TODO: Original warning is :
- # Puppet.warning addcontext("Deprecation notice: Resource references should now be capitalized")
- # This model element is not used in the egrammar.
- # Either implement checks or deprecate the use of InstanceReference (the same is acheived by
- # transformation of AccessExpression when used where an Instance/Resource reference is allowed.
- #
+ def check_KeyedEntry(o)
+ rvalue(o.key)
+ rvalue(o.value)
+ # In case there are additional things to forbid than non-rvalues
+ # acceptor.accept(Issues::ILLEGAL_EXPRESSION, o.key, :feature => 'hash key', :container => o.eContainer)
end
- # Restrictions on hash key are because of the strange key comparisons/and merge rules in the AST evaluation
- # (Even the allowed ones are handled in a strange way).
+ # A Lambda is a Definition, but it may appear in other scopes than top scope (Which check_Definition asserts).
#
- def transform_KeyedEntry(o)
- case o.key
- when Model::QualifiedName
- when Model::LiteralString
- when Model::LiteralNumber
- when Model::ConcatenatedString
- else
- acceptor.accept(Issues::ILLEGAL_EXPRESSION, o.key, :feature => 'hash key', :container => o.eContainer)
- end
+ def check_LambdaExpression(o)
end
- # A Lambda is a Definition, but it may appear in other scopes that top scope (Which check_Definition asserts).
- #
- def check_LambdaExpression(o)
+ def check_LiteralList(o)
+ o.values.each {|v| rvalue(v) }
end
def check_NodeDefinition(o)
- # Check that hostnames are valid hostnames (or regular expressons)
+ # Check that hostnames are valid hostnames (or regular expressions)
hostname(o.host_matches, o)
hostname(o.parent, o, 'parent') unless o.parent.nil?
top(o.eContainer, o)
end
# No checking takes place - all expressions using a QualifiedName need to check. This because the
# rules are slightly different depending on the container (A variable allows a numeric start, but not
# other names). This means that (if the lexer/parser so chooses) a QualifiedName
# can be anything when it represents a Bare Word and evaluates to a String.
#
def check_QualifiedName(o)
end
# Checks that the value is a valid UpperCaseWord (a CLASSREF), and optionally if it contains a hypen.
# DOH: QualifiedReferences are created with LOWER CASE NAMES at parse time
def check_QualifiedReference(o)
# Is this a valid qualified name?
if o.value !~ Puppet::Pops::Patterns::CLASSREF
acceptor.accept(Issues::ILLEGAL_CLASSREF, o, {:name=>o.value})
- elsif (acceptor.will_accept? Issues::NAME_WITH_HYPHEN) && o.value.include?('-')
- acceptor.accept(Issues::NAME_WITH_HYPHEN, o, {:name => o.value})
end
end
def check_QueryExpression(o)
query(o.expr) if o.expr # is optional
end
- def relation_Object(o, rel_expr)
- acceptor.accept(Issues::ILLEGAL_EXPRESSION, o, {:feature => o.eContainingFeature, :container => rel_expr})
+ def relation_Object(o)
+ rvalue(o)
end
- def relation_AccessExpression(o, rel_expr); end
+ def relation_CollectExpression(o); end
- def relation_CollectExpression(o, rel_expr); end
-
- def relation_VariableExpression(o, rel_expr); end
-
- def relation_LiteralString(o, rel_expr); end
-
- def relation_ConcatenatedStringExpression(o, rel_expr); end
-
- def relation_SelectorExpression(o, rel_expr); end
-
- def relation_CaseExpression(o, rel_expr); end
-
- def relation_ResourceExpression(o, rel_expr); end
-
- def relation_RelationshipExpression(o, rel_expr); end
+ def relation_RelationshipExpression(o); end
def check_Parameter(o)
if o.name =~ /^[0-9]+$/
acceptor.accept(Issues::ILLEGAL_NUMERIC_PARAMETER, o, :name => o.name)
end
end
#relationship_side: resource
# | resourceref
# | collection
# | variable
# | quotedtext
# | selector
# | casestatement
# | hasharrayaccesses
def check_RelationshipExpression(o)
- relation(o.left_expr, o)
- relation(o.right_expr, o)
+ relation(o.left_expr)
+ relation(o.right_expr)
end
def check_ResourceExpression(o)
# A resource expression must have a lower case NAME as its type e.g. 'file { ... }'
unless o.type_name.is_a? Model::QualifiedName
acceptor.accept(Issues::ILLEGAL_EXPRESSION, o.type_name, :feature => 'resource type', :container => o)
end
# This is a runtime check - the model is valid, but will have runtime issues when evaluated
# and storeconfigs is not set.
if acceptor.will_accept?(Issues::RT_NO_STORECONFIGS) && o.exported
acceptor.accept(Issues::RT_NO_STORECONFIGS_EXPORT, o)
end
end
def check_ResourceDefaultsExpression(o)
if o.form && o.form != :regular
acceptor.accept(Issues::NOT_VIRTUALIZEABLE, o)
end
end
- # Transformation of SelectorExpression is limited to certain types of expressions.
- # This is probably due to constraints in the old grammar rather than any real concerns.
- def select_SelectorExpression(o)
- case o.left_expr
- when Model::CallNamedFunctionExpression
- when Model::AccessExpression
- when Model::VariableExpression
- when Model::ConcatenatedString
- else
- acceptor.accept(Issues::ILLEGAL_EXPRESSION, o.left_expr, :feature => 'left operand', :container => o)
- end
+ def check_SelectorExpression(o)
+ rvalue(o.left_expr)
+ end
+
+ def check_SelectorEntry(o)
+ rvalue(o.matching_expr)
end
def check_UnaryExpression(o)
rvalue(o.expr)
end
def check_UnlessExpression(o)
- # TODO: Unless may not have an elsif
- # TODO: 3.x unless may not have an else
+ rvalue(o.test)
+ # TODO: Unless may not have an else part that is an IfExpression (grammar denies this though)
end
+ # Checks that variable is either strictly 0, or a non 0 starting decimal number, or a valid VAR_NAME
def check_VariableExpression(o)
# The expression must be a qualified name
- if !o.expr.is_a? Model::QualifiedName
+ if !o.expr.is_a?(Model::QualifiedName)
acceptor.accept(Issues::ILLEGAL_EXPRESSION, o, :feature => 'name', :container => o)
else
- # Note, that if it later becomes illegal with hyphen in any name, this special check
- # can be skipped in favor of the check in QualifiedName, which is now not done if contained in
- # a VariableExpression
+ # name must be either a decimal value, or a valid NAME
name = o.expr.value
- if (acceptor.will_accept? Issues::VAR_WITH_HYPHEN) && name.include?('-')
- acceptor.accept(Issues::VAR_WITH_HYPHEN, o, {:name => name})
+ if name[0,1] =~ /[0-9]/
+ unless name =~ Puppet::Pops::Patterns::NUMERIC_VAR_NAME
+ acceptor.accept(Issues::ILLEGAL_NUMERIC_VAR_NAME, o, :name => name)
+ end
+ else
+ unless name =~ Puppet::Pops::Patterns::VAR_NAME
+ acceptor.accept(Issues::ILLEGAL_VAR_NAME, o, :name => name)
+ end
end
end
end
#--- HOSTNAME CHECKS
# Transforms Array of host matching expressions into a (Ruby) array of AST::HostName
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, 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.
#
if o =~ Puppet::Pops::Patterns::ILLEGAL_HOSTNAME_CHARS
acceptor.accept(Issues::ILLEGAL_HOSTNAME_CHARS, semantic, :hostname => o)
end
end
def hostname_LiteralValue(o, semantic, single_feature_name)
hostname_String(o.value.to_s, o, single_feature_name)
end
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)
elsif o.segments.size() != 1
# corner case, bad model, concatenation of several plain strings
acceptor.accept(Issues::ILLEGAL_HOSTNAME_INTERPOLATION, o)
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], false)
end
end
def hostname_QualifiedName(o, semantic, single_feature_name)
hostname_String(o.value.to_s, o, single_feature_name)
end
def hostname_QualifiedReference(o, semantic, single_feature_name)
hostname_String(o.value.to_s, o, single_feature_name)
end
def hostname_LiteralNumber(o, semantic, single_feature_name)
# always ok
end
def hostname_LiteralDefault(o, semantic, single_feature_name)
# always ok
end
def hostname_LiteralRegularExpression(o, semantic, single_feature_name)
# always ok
end
def hostname_Object(o, semantic, single_feature_name)
acceptor.accept(Issues::ILLEGAL_EXPRESSION, o, {:feature=> single_feature_name || 'hostname', :container=>semantic})
end
#---QUERY CHECKS
# Anything not explicitly allowed is flagged as error.
def query_Object(o)
acceptor.accept(Issues::ILLEGAL_QUERY_EXPRESSION, o)
end
# Puppet AST only allows == and !=
#
def query_ComparisonExpression(o)
acceptor.accept(Issues::ILLEGAL_QUERY_EXPRESSION, o) unless [:'==', :'!='].include? o.operator
end
# Allows AND, OR, and checks if left/right are allowed in query.
def query_BooleanExpression(o)
query o.left_expr
query o.right_expr
end
def query_ParenthesizedExpression(o)
query(o.expr)
end
def query_VariableExpression(o); end
def query_QualifiedName(o); end
def query_LiteralNumber(o); end
def query_LiteralString(o); end
def query_LiteralBoolean(o); end
#---RVALUE CHECKS
# By default, all expressions are reported as being rvalues
# Implement specific rvalue checks for those that are not.
#
def rvalue_Expression(o); end
- def rvalue_ImportExpression(o) ; acceptor.accept(Issues::NOT_RVALUE, o) ; end
-
- def rvalue_BlockExpression(o) ; acceptor.accept(Issues::NOT_RVALUE, o) ; end
-
- def rvalue_CaseExpression(o) ; acceptor.accept(Issues::NOT_RVALUE, o) ; end
-
- def rvalue_IfExpression(o) ; acceptor.accept(Issues::NOT_RVALUE, o) ; end
-
- def rvalue_UnlessExpression(o) ; acceptor.accept(Issues::NOT_RVALUE, o) ; end
-
- def rvalue_ResourceExpression(o) ; acceptor.accept(Issues::NOT_RVALUE, o) ; end
-
def rvalue_ResourceDefaultsExpression(o); acceptor.accept(Issues::NOT_RVALUE, o) ; end
def rvalue_ResourceOverrideExpression(o); acceptor.accept(Issues::NOT_RVALUE, o) ; end
def rvalue_CollectExpression(o) ; acceptor.accept(Issues::NOT_RVALUE, o) ; end
def rvalue_Definition(o) ; acceptor.accept(Issues::NOT_RVALUE, o) ; end
def rvalue_NodeDefinition(o) ; acceptor.accept(Issues::NOT_RVALUE, o) ; end
def rvalue_UnaryExpression(o) ; rvalue o.expr ; end
#---TOP CHECK
def top_NilClass(o, definition)
# ok, reached the top, no more parents
end
def top_Object(o, definition)
# fail, reached a container that is not top level
acceptor.accept(Issues::NOT_TOP_LEVEL, definition)
end
def top_BlockExpression(o, definition)
# ok, if this is a block representing the body of a class, or is top level
top o.eContainer, definition
end
def top_HostClassDefinition(o, definition)
# ok, stop scanning parents
end
+ def top_Program(o, definition)
+ # ok
+ end
+
# A LambdaExpression is a BlockExpression, and this method is needed to prevent the polymorph method for BlockExpression
# to accept a lambda.
# A lambda can not iteratively create classes, nodes or defines as the lambda does not have a closure.
#
def top_LambdaExpression(o, definition)
# fail, stop scanning parents
acceptor.accept(Issues::NOT_TOP_LEVEL, definition)
end
#--- NON POLYMORPH, NON CHECKING CODE
# Produces string part of something named, or nil if not a QualifiedName or QualifiedReference
#
def varname_to_s(o)
case o
when Model::QualifiedName
o.value
when Model::QualifiedReference
o.value
else
nil
end
end
end
diff --git a/lib/puppet/pops/validation/validator_factory_4_0.rb b/lib/puppet/pops/validation/validator_factory_4_0.rb
new file mode 100644
index 000000000..95ff0d687
--- /dev/null
+++ b/lib/puppet/pops/validation/validator_factory_4_0.rb
@@ -0,0 +1,31 @@
+# Configures validation suitable for 4.0
+#
+class Puppet::Pops::Validation::ValidatorFactory_4_0 < Puppet::Pops::Validation::Factory
+ Issues = Puppet::Pops::Issues
+
+ # Produces the checker to use
+ def checker diagnostic_producer
+ Puppet::Pops::Validation::Checker4_0.new(diagnostic_producer)
+ end
+
+ # Produces the label provider to use
+ def label_provider
+ Puppet::Pops::Model::ModelLabelProvider.new()
+ end
+
+ # Produces the severity producer to use
+ def severity_producer
+ p = super
+
+ # Configure each issue that should **not** be an error
+ #
+ # Validate as per the current runtime configuration
+ p[Issues::RT_NO_STORECONFIGS_EXPORT] = Puppet[:storeconfigs] ? :ignore : :warning
+ p[Issues::RT_NO_STORECONFIGS] = Puppet[:storeconfigs] ? :ignore : :warning
+
+ p[Issues::NAME_WITH_HYPHEN] = :error
+ p[Issues::DEPRECATED_NAME_AS_TYPE] = :error
+
+ p
+ end
+end
diff --git a/lib/puppet/pops/visitor.rb b/lib/puppet/pops/visitor.rb
index d5526ac8b..c35fa0b93 100644
--- a/lib/puppet/pops/visitor.rb
+++ b/lib/puppet/pops/visitor.rb
@@ -1,50 +1,192 @@
# A Visitor performs delegation to a given receiver based on the configuration of the Visitor.
# A new visitor is created with a given receiver, a method prefix, min, and max argument counts.
# e.g.
# vistor = Visitor.new(self, "visit_from", 1, 1)
# will make the visitor call "self.visit_from_CLASS(x)" where CLASS is resolved to the given
# objects class, or one of is ancestors, the first class for which there is an implementation of
# a method will be selected.
#
# Raises RuntimeError if there are too few or too many arguments, or if the receiver is not
# configured to handle a given visiting object.
#
class Puppet::Pops::Visitor
attr_reader :receiver, :message, :min_args, :max_args, :cache
def initialize(receiver, message, min_args=0, max_args=nil)
raise ArgumentError.new("min_args must be >= 0") if min_args < 0
raise ArgumentError.new("max_args must be >= min_args or nil") if max_args && max_args < min_args
@receiver = receiver
@message = message
@min_args = min_args
@max_args = max_args
@cache = Hash.new
end
# Visit the configured receiver
def visit(thing, *args)
visit_this(@receiver, thing, *args)
end
# Visit an explicit receiver
def visit_this(receiver, thing, *args)
raise "Visitor Error: Too few arguments passed. min = #{@min_args}" unless args.length >= @min_args
if @max_args
raise "Visitor Error: Too many arguments passed. max = #{@max_args}" unless args.length <= @max_args
end
if method_name = @cache[thing.class]
return receiver.send(method_name, thing, *args)
else
thing.class.ancestors().each do |ancestor|
- method_name = :"#{@message}_#{ancestor.name.split("::").last}"
- # DEBUG OUTPUT
- # puts "Visitor checking: #{receiver.class}.#{method_name}, responds to: #{@receiver.respond_to? method_name}"
- next unless receiver.respond_to? method_name
+ method_name = :"#{@message}_#{ancestor.name.split(/::/).last}"
+ next unless receiver.respond_to?(method_name, true)
@cache[thing.class] = method_name
return receiver.send(method_name, thing, *args)
end
end
raise "Visitor Error: the configured receiver (#{receiver.class}) can't handle instance of: #{thing.class}"
end
+
+ # Visit an explicit receiver with 0 args
+ # (This is ~30% faster than calling the general method)
+ #
+ def visit_this_0(receiver, thing)
+ if method_name = @cache[thing.class]
+ return receiver.send(method_name, thing)
+ end
+ visit_this(receiver, thing)
+ end
+
+ # Visit an explicit receiver with 1 args
+ # (This is ~30% faster than calling the general method)
+ #
+ def visit_this_1(receiver, thing, arg)
+ if method_name = @cache[thing.class]
+ return receiver.send(method_name, thing, arg)
+ end
+ visit_this(receiver, thing, arg)
+ end
+
+ # Visit an explicit receiver with 2 args
+ # (This is ~30% faster than calling th general method)
+ #
+ def visit_this_2(receiver, thing, arg1, arg2)
+ if method_name = @cache[thing.class]
+ return receiver.send(method_name, thing, arg1, arg2)
+ end
+ visit_this(receiver, thing, arg1, arg2)
+ end
+
+ # Visit an explicit receiver with 3 args
+ # (This is ~30% faster than calling the general method)
+ #
+ def visit_this_3(receiver, thing, arg1, arg2, arg3)
+ if method_name = @cache[thing.class]
+ return receiver.send(method_name, thing, arg1, arg2, arg3)
+ end
+ visit_this(receiver, thing, arg1, arg2, arg3)
+ end
+
+ # This is an alternative implementation that separates the finding of method names
+ # (Cached in the Visitor2 class), and bound methods (in an inner Delegator class) that
+ # are cached for this receiver instance. This is based on micro benchmarks measuring that a send is slower
+ # that directly calling a bound method.
+ # Larger benchmark however show that the overhead is fractional. Additional (larger) tests may
+ # show otherwise.
+ # To use this class instead of the regular Visitor.
+ # @@the_visitor_c = Visitor2.new(...)
+ # @@the_visitor = @@the_visitor_c.instance(self)
+ # then visit with one of the Delegator's visit methods.
+ #
+ # Performance Note: there are still issues with this implementation (although cleaner) since it requires
+ # holding on to the first instance in order to compute respond_do?. This is required if the class
+ # is using method_missing? which cannot be computed by introspection of the class (which would be
+ # ideal). Another approach is to pre-scan all the available methods starting with the pattern for
+ # the visitor, scan the class, and just check if the class has this method. (This will not work
+ # for dispatch to methods that requires method missing. (Maybe that does not matter)
+ # Further experiments could try looking up unbound methods via the class, cloning and binding them
+ # instead of again looking them up with #method(name)
+ # Also note that this implementation does not check min/max args on each call - there was not much gain
+ # from skipping this. It is safe to skip, but produces less friendly errors if there is an error in the
+ # implementation.
+ #
+ class Visitor2
+ attr_reader :receiver, :message, :min_args, :max_args, :cache
+
+ def initialize(receiver, message, min_args=0, max_args=nil)
+ raise ArgumentError.new("receiver can not be nil") if receiver.nil?
+ raise ArgumentError.new("min_args must be >= 0") if min_args < 0
+ raise ArgumentError.new("max_args must be >= min_args or nil") if max_args && max_args < min_args
+
+ @receiver = receiver
+ @message = message
+ @min_args = min_args
+ @max_args = max_args
+ @cache = Hash.new
+ end
+
+ def instance(receiver)
+ # Create a visitable instance for the receiver
+ Delegator.new(receiver, self)
+ end
+
+ # Produce the name of the method to use
+ # @return [Symbol, nil] the method name symbol, or nil if there is no method to call for thing
+ #
+ def method_name_for(thing)
+ if method_name = @cache[thing.class]
+ return method_name
+ else
+ thing.class.ancestors().each do |ancestor|
+ method_name = :"#{@message}_#{ancestor.name.split(/::/).last}"
+ next unless receiver.respond_to?(method_name, true)
+ @cache[thing.class] = method_name
+ return method_name
+ end
+ end
+ end
+
+ class Delegator
+ attr_reader :receiver, :visitor, :cache
+ def initialize(receiver, visitor)
+ @receiver = receiver
+ @visitor = visitor
+ @cache = Hash.new
+ end
+
+ # Visit
+ def visit(thing, *args)
+ if method = @cache[thing.class]
+ return method.call(thing, *args)
+ else
+ method_name = visitor.method_name_for(thing)
+ method = receiver.method(method_name)
+ unless method
+ raise "Visitor Error: the configured receiver (#{receiver.class}) can't handle instance of: #{thing.class}"
+ end
+ @cache[thing.class] = method
+ method.call(thing, *args)
+ end
+ end
+
+ # Visit an explicit receiver with 0 args
+ # (This is ~30% faster than calling the general method)
+ #
+ def visit_0(thing)
+ (method = @cache[thing.class]) ? method.call(thing) : visit(thing)
+ end
+
+ def visit_1(thing, arg)
+ (method = @cache[thing.class]) ? method.call(thing, arg) : visit(thing, arg)
+ end
+
+ def visit_2(thing, arg1, arg2)
+ (method = @cache[thing.class]) ? method.call(thing, arg1, arg2) : visit(thing, arg1, arg2)
+ end
+
+ def visit_3(thing, arg1, arg2, arg3)
+ (method = @cache[thing.class]) ? method.call(thing, arg1, arg2, arg3) : visit(thing, arg1, arg2, arg3)
+ end
+
+ end
+ end
end
diff --git a/lib/puppet/property.rb b/lib/puppet/property.rb
index 74c6e0292..841f8675a 100644
--- a/lib/puppet/property.rb
+++ b/lib/puppet/property.rb
@@ -1,617 +1,617 @@
# The virtual base class for properties, which are the self-contained building
# blocks for actually doing work on the system.
require 'puppet'
require 'puppet/parameter'
# The Property class is the implementation of a resource's attributes of _property_ kind.
# A Property is a specialized Resource Type Parameter that has both an 'is' (current) state, and
# a 'should' (wanted state). However, even if this is conceptually true, the current _is_ value is
# obtained by asking the associated provider for the value, and hence it is not actually part of a
# property's state, and only available when a provider has been selected and can obtain the value (i.e. when
# running on an agent).
#
# A Property (also in contrast to a parameter) is intended to describe a managed attribute of
# some system entity, such as the name or mode of a file.
#
# The current value _(is)_ is read and written with the methods {#retrieve} and {#set}, and the wanted
# value _(should)_ is read and written with the methods {#value} and {#value=} which delegate to
# {#should} and {#should=}, i.e. when a property is used like any other parameter, it is the _should_ value
# that is operated on.
#
# All resource type properties in the puppet system are derived from this class.
#
# The intention is that new parameters are created by using the DSL method {Puppet::Type.newproperty}.
#
# @abstract
# @note Properties of Types are expressed using subclasses of this class. Such a class describes one
# named property of a particular Type (as opposed to describing a type of property in general). This
# limits the use of one (concrete) property class instance to occur only once for a given type's inheritance
# chain. An instance of a Property class is the value holder of one instance of the resource type (e.g. the
# mode of a file resource instance).
# A Property class may server as the superclass _(parent)_ of another; e.g. a Size property that describes
# handling of measurements such as kb, mb, gb. If a type requires two different size measurements it requires
# one concrete class per such measure; e.g. MinSize (:parent => Size), and MaxSize (:parent => Size).
#
# @todo Describe meta-parameter shadowing. This concept can not be understood by just looking at the descriptions
# of the methods involved.
#
# @see Puppet::Type
# @see Puppet::Parameter
#
# @api public
#
class Puppet::Property < Puppet::Parameter
require 'puppet/property/ensure'
# Returns the original wanted value(s) _(should)_ unprocessed by munging/unmunging.
# The original values are set by {#value=} or {#should=}.
# @return (see #should)
#
attr_reader :shouldorig
# The noop mode for this property.
# By setting a property's noop mode to `true`, any management of this property is inhibited. Calculation
# and reporting still takes place, but if a change of the underlying managed entity's state
# should take place it will not be carried out. This noop
# setting overrides the overall `Puppet[:noop]` mode as well as the noop mode in the _associated resource_
#
attr_writer :noop
class << self
# @todo Figure out what this is used for. Can not find any logic in the puppet code base that
# reads or writes this attribute.
# ??? Probably Unused
attr_accessor :unmanaged
# @return [Symbol] The name of the property as given when the property was created.
#
attr_reader :name
# @!attribute [rw] array_matching
# @comment note that $#46; is a period - char code require to not terminate sentence.
# The `is` vs&#46; `should` array matching mode; `:first`, or `:all`.
#
# @comment there are two blank chars after the symbols to cause a break - do not remove these.
# * `:first`
# This is primarily used for single value properties. When matched against an array of values
# a match is true if the `is` value matches any of the values in the `should` array. When the `is` value
# is also an array, the matching is performed against the entire array as the `is` value.
# * `:all`
# : This is primarily used for multi-valued properties. When matched against an array of
# `should` values, the size of `is` and `should` must be the same, and all values in `is` must match
# a value in `should`.
#
# @note The semantics of these modes are implemented by the method {#insync?}. That method is the default
# implementation and it has a backwards compatible behavior that imposes additional constraints
# on what constitutes a positive match. A derived property may override that method.
# @return [Symbol] (:first) the mode in which matching is performed
# @see #insync?
# @dsl type
# @api public
#
def array_matching
@array_matching ||= :first
end
# @comment This is documented as an attribute - see the {array_matching} method.
#
def array_matching=(value)
value = value.intern if value.is_a?(String)
raise ArgumentError, "Supported values for Property#array_matching are 'first' and 'all'" unless [:first, :all].include?(value)
@array_matching = value
end
end
# Looks up a value's name among valid values, to enable option lookup with result as a key.
# @param name [Object] the parameter value to match against valid values (names).
# @return {Symbol, Regexp} a value matching predicate
# @api private
#
def self.value_name(name)
if value = value_collection.match?(name)
value.name
end
end
# Returns the value of the given option (set when a valid value with the given "name" was defined).
# @param name [Symbol, Regexp] the valid value predicate as returned by {value_name}
# @param option [Symbol] the name of the wanted option
# @return [Object] value of the option
# @raise [NoMethodError] if the option is not supported
# @todo Guessing on result of passing a non supported option (it performs send(option)).
# @api private
#
def self.value_option(name, option)
if value = value_collection.value(name)
value.send(option)
end
end
# Defines a new valid value for this property.
# A valid value is specified as a literal (typically a Symbol), but can also be
# specified with a Regexp.
#
# @param name [Symbol, Regexp] a valid literal value, or a regexp that matches a value
# @param options [Hash] a hash with options
# @option options [Symbol] :event The event that should be emitted when this value is set.
# @todo Option :event original comment says "event should be returned...", is "returned" the correct word
# to use?
# @option options [Symbol] :call When to call any associated block. The default value is `:instead` which
# means that the block should be called instead of the provider. In earlier versions (before 20081031) it
# was possible to specify a value of `:before` or `:after` for the purpose of calling
# both the block and the provider. Use of these deprecated options will now raise an exception later
# in the process when the _is_ value is set (see #set).
# @option options [Symbol] :invalidate_refreshes Indicates a change on this property should invalidate and
# remove any scheduled refreshes (from notify or subscribe) targeted at the same resource. For example, if
# a change in this property takes into account any changes that a scheduled refresh would have performed,
# then the scheduled refresh would be deleted.
# @option options [Object] any Any other option is treated as a call to a setter having the given
# option name (e.g. `:required_features` calls `required_features=` with the option's value as an
# argument).
# @todo The original documentation states that the option `:method` will set the name of the generated
# setter method, but this is not implemented. Is the documentatin or the implementation in error?
# (The implementation is in Puppet::Parameter::ValueCollection#new_value).
# @todo verify that the use of :before and :after have been deprecated (or rather - never worked, and
# was never in use. (This means, that the option :call could be removed since calls are always :instead).
#
# @dsl type
# @api public
def self.newvalue(name, options = {}, &block)
value = value_collection.newvalue(name, options, &block)
define_method(value.method, &value.block) if value.method and value.block
value
end
# Calls the provider setter method for this property with the given value as argument.
# @return [Object] what the provider returns when calling a setter for this property's name
# @raise [Puppet::Error] when the provider can not handle this property.
# @see #set
# @api private
#
def call_provider(value)
method = self.class.name.to_s + "="
unless provider.respond_to? method
self.fail "The #{provider.class.name} provider can not handle attribute #{self.class.name}"
end
provider.send(method, value)
end
# Sets the value of this property to the given value by calling the dynamically created setter method associated with the "valid value" referenced by the given name.
# @param name [Symbol, Regexp] a valid value "name" as returned by {value_name}
# @param value [Object] the value to set as the value of the property
# @raise [Puppet::DevError] if there was no method to call
# @raise [Puppet::Error] if there were problems setting the value
# @raise [Puppet::ResourceError] if there was a problem setting the value and it was not raised
# as a Puppet::Error. The original exception is wrapped and logged.
# @todo The check for a valid value option called `:method` does not seem to be fully supported
# as it seems that this option is never consulted when the method is dynamically created. Needs to
# be investigated. (Bug, or documentation needs to be changed).
# @see #set
# @api private
#
def call_valuemethod(name, value)
if method = self.class.value_option(name, :method) and self.respond_to?(method)
begin
self.send(method)
rescue Puppet::Error
raise
rescue => detail
error = Puppet::ResourceError.new("Could not set '#{value}' on #{self.class.name}: #{detail}", @resource.line, @resource.file, detail)
error.set_backtrace detail.backtrace
Puppet.log_exception(detail, error.message)
raise error
end
elsif block = self.class.value_option(name, :block)
# FIXME It'd be better here to define a method, so that
# the blocks could return values.
self.instance_eval(&block)
else
devfail "Could not find method for value '#{name}'"
end
end
# Formats a message for a property change from the given `current_value` to the given `newvalue`.
# @return [String] a message describing the property change.
# @note If called with equal values, this is reported as a change.
# @raise [Puppet::DevError] if there were issues formatting the message
#
def change_to_s(current_value, newvalue)
begin
if current_value == :absent
return "defined '#{name}' as #{self.class.format_value_for_display should_to_s(newvalue)}"
elsif newvalue == :absent or newvalue == [:absent]
return "undefined '#{name}' from #{self.class.format_value_for_display is_to_s(current_value)}"
else
return "#{name} changed #{self.class.format_value_for_display is_to_s(current_value)} to #{self.class.format_value_for_display should_to_s(newvalue)}"
end
rescue Puppet::Error, Puppet::DevError
raise
rescue => detail
message = "Could not convert change '#{name}' to string: #{detail}"
Puppet.log_exception(detail, message)
- raise Puppet::DevError, message
+ raise Puppet::DevError, message, detail.backtrace
end
end
# Produces the name of the event to use to describe a change of this property's value.
# The produced event name is either the event name configured for this property, or a generic
# event based on the name of the property with suffix `_changed`, or if the property is
# `:ensure`, the name of the resource type and one of the suffixes `_created`, `_removed`, or `_changed`.
# @return [String] the name of the event that describes the change
#
def event_name
value = self.should
event_name = self.class.value_option(value, :event) and return event_name
name == :ensure or return (name.to_s + "_changed").to_sym
return (resource.type.to_s + case value
when :present; "_created"
when :absent; "_removed"
else
"_changed"
end).to_sym
end
# Produces an event describing a change of this property.
# In addition to the event attributes set by the resource type, this method adds:
#
# * `:name` - the event_name
# * `:desired_value` - a.k.a _should_ or _wanted value_
# * `:property` - reference to this property
# * `:source_description` - the _path_ (?? See todo)
# * `:invalidate_refreshes` - if scheduled refreshes should be invalidated
#
# @todo What is the intent of this method? What is the meaning of the :source_description passed in the
# options to the created event?
# @return [Puppet::Transaction::Event] the created event
# @see Puppet::Type#event
def event
attrs = { :name => event_name, :desired_value => should, :property => self, :source_description => path }
if should and value = self.class.value_collection.match?(should)
attrs[:invalidate_refreshes] = true if value.invalidate_refreshes
end
resource.event attrs
end
# @todo What is this?
# What is this used for?
attr_reader :shadow
# Initializes a Property the same way as a Parameter and handles the special case when a property is shadowing a meta-parameter.
# @todo There is some special initialization when a property is not a metaparameter but
# Puppet::Type.metaparamclass(for this class's name) is not nil - if that is the case a
# setup_shadow is performed for that class.
#
# @param hash [Hash] options passed to the super initializer {Puppet::Parameter#initialize}
# @note New properties of a type should be created via the DSL method {Puppet::Type.newproperty}.
# @see Puppet::Parameter#initialize description of Parameter initialize options.
# @api private
def initialize(hash = {})
super
if ! self.metaparam? and klass = Puppet::Type.metaparamclass(self.class.name)
setup_shadow(klass)
end
end
# Determines whether the property is in-sync or not in a way that is protected against missing value.
# @note If the wanted value _(should)_ is not defined or is set to a non-true value then this is
# a state that can not be fixed and the property is reported to be in sync.
# @return [Boolean] the protected result of `true` or the result of calling {#insync?}.
#
# @api private
# @note Do not override this method.
#
def safe_insync?(is)
# If there is no @should value, consider the property to be in sync.
return true unless @should
# Otherwise delegate to the (possibly derived) insync? method.
insync?(is)
end
# Protects against override of the {#safe_insync?} method.
# @raise [RuntimeError] if the added method is `:safe_insync?`
# @api private
#
def self.method_added(sym)
raise "Puppet::Property#safe_insync? shouldn't be overridden; please override insync? instead" if sym == :safe_insync?
end
# Checks if the current _(is)_ value is in sync with the wanted _(should)_ value.
# The check if the two values are in sync is controlled by the result of {#match_all?} which
# specifies a match of `:first` or `:all`). The matching of the _is_ value against the entire _should_ value
# or each of the _should_ values (as controlled by {#match_all?} is performed by {#property_matches?}.
#
# A derived property typically only needs to override the {#property_matches?} method, but may also
# override this method if there is a need to have more control over the array matching logic.
#
# @note The array matching logic in this method contains backwards compatible logic that performs the
# comparison in `:all` mode by checking equality and equality of _is_ against _should_ converted to array of String,
# and that the lengths are equal, and in `:first` mode by checking if one of the _should_ values
# is included in the _is_ values. This means that the _is_ value needs to be carefully arranged to
# match the _should_.
# @todo The implementation should really do return is.zip(@should).all? {|a, b| property_matches?(a, b) }
# instead of using equality check and then check against an array with converted strings.
# @param is [Object] The current _(is)_ value to check if it is in sync with the wanted _(should)_ value(s)
# @return [Boolean] whether the values are in sync or not.
# @raise [Puppet::DevError] if wanted value _(should)_ is not an array.
# @api public
#
def insync?(is)
self.devfail "#{self.class.name}'s should is not array" unless @should.is_a?(Array)
# an empty array is analogous to no should values
return true if @should.empty?
# Look for a matching value, either for all the @should values, or any of
# them, depending on the configuration of this property.
if match_all? then
# Emulate Array#== using our own comparison function.
# A non-array was not equal to an array, which @should always is.
return false unless is.is_a? Array
# If they were different lengths, they are not equal.
return false unless is.length == @should.length
# Finally, are all the elements equal? In order to preserve the
# behaviour of previous 2.7.x releases, we need to impose some fun rules
# on "equality" here.
#
# Specifically, we need to implement *this* comparison: the two arrays
# are identical if the is values are == the should values, or if the is
# values are == the should values, stringified.
#
# This does mean that property equality is not commutative, and will not
# work unless the `is` value is carefully arranged to match the should.
return (is == @should or is == @should.map(&:to_s))
# When we stop being idiots about this, and actually have meaningful
# semantics, this version is the thing we actually want to do.
#
# return is.zip(@should).all? {|a, b| property_matches?(a, b) }
else
return @should.any? {|want| property_matches?(is, want) }
end
end
# Checks if the given current and desired values are equal.
# This default implementation performs this check in a backwards compatible way where
# the equality of the two values is checked, and then the equality of current with desired
# converted to a string.
#
# A derived implementation may override this method to perform a property specific equality check.
#
# The intent of this method is to provide an equality check suitable for checking if the property
# value is in sync or not. It is typically called from {#insync?}.
#
def property_matches?(current, desired)
# This preserves the older Puppet behaviour of doing raw and string
# equality comparisons for all equality. I am not clear this is globally
# desirable, but at least it is not a breaking change. --daniel 2011-11-11
current == desired or current == desired.to_s
end
# Produces a pretty printing string for the given value.
# This default implementation simply returns the given argument. A derived implementation
# may perform property specific pretty printing when the _is_ and _should_ values are not
# already in suitable form.
# @return [String] a pretty printing string
def is_to_s(currentvalue)
currentvalue
end
# Emits a log message at the log level specified for the associated resource.
# The log entry is associated with this property.
# @param msg [String] the message to log
# @return [void]
#
def log(msg)
Puppet::Util::Log.create(
:level => resource[:loglevel],
:message => msg,
:source => self
)
end
# @return [Boolean] whether the {array_matching} mode is set to `:all` or not
def match_all?
self.class.array_matching == :all
end
# (see Puppet::Parameter#munge)
# If this property is a meta-parameter shadow, the shadow's munge is also called.
# @todo Incomprehensible ! The concept of "meta-parameter-shadowing" needs to be explained.
#
def munge(value)
self.shadow.munge(value) if self.shadow
super
end
# @return [Symbol] the name of the property as stated when the property was created.
# @note A property class (just like a parameter class) describes one specific property and
# can only be used once within one type's inheritance chain.
def name
self.class.name
end
# @return [Boolean] whether this property is in noop mode or not.
# Returns whether this property is in noop mode or not; if a difference between the
# _is_ and _should_ values should be acted on or not.
# The noop mode is a transitive setting. The mode is checked in this property, then in
# the _associated resource_ and finally in Puppet[:noop].
# @todo This logic is different than Parameter#noop in that the resource noop mode overrides
# the property's mode - in parameter it is the other way around. Bug or feature?
#
def noop
# This is only here to make testing easier.
if @resource.respond_to?(:noop?)
@resource.noop?
else
if defined?(@noop)
@noop
else
Puppet[:noop]
end
end
end
# Retrieves the current value _(is)_ of this property from the provider.
# This implementation performs this operation by calling a provider method with the
# same name as this property (i.e. if the property name is 'gid', a call to the
# 'provider.gid' is expected to return the current value.
# @return [Object] what the provider returns as the current value of the property
#
def retrieve
provider.send(self.class.name)
end
# Sets the current _(is)_ value of this property.
# The value is set using the provider's setter method for this property ({#call_provider}) if nothing
# else has been specified. If the _valid value_ for the given value defines a `:call` option with the
# value `:instead`, the
# value is set with {#call_valuemethod} which invokes a block specified for the valid value.
#
# @note In older versions (before 20081031) it was possible to specify the call types `:before` and `:after`
# which had the effect that both the provider method and the _valid value_ block were called.
# This is no longer supported.
#
# @param value [Object] the value to set as the value of this property
# @return [Object] returns what {#call_valuemethod} or {#call_provider} returns
# @raise [Puppet::Error] when the provider setter should be used but there is no provider set in the _associated
# resource_
# @raise [Puppet::DevError] when a deprecated call form was specified (e.g. `:before` or `:after`).
# @api public
#
def set(value)
# Set a name for looking up associated options like the event.
name = self.class.value_name(value)
call = self.class.value_option(name, :call) || :none
if call == :instead
call_valuemethod(name, value)
elsif call == :none
# They haven't provided a block, and our parent does not have
# a provider, so we have no idea how to handle this.
self.fail "#{self.class.name} cannot handle values of type #{value.inspect}" unless @resource.provider
call_provider(value)
else
# LAK:NOTE 20081031 This is a change in behaviour -- you could
# previously specify :call => [;before|:after], which would call
# the setter *in addition to* the block. I'm convinced this
# was never used, and it makes things unecessarily complicated.
# If you want to specify a block and still call the setter, then
# do so in the block.
devfail "Cannot use obsolete :call value '#{call}' for property '#{self.class.name}'"
end
end
# Sets up a shadow property for a shadowing meta-parameter.
# This construct allows the creation of a property with the
# same name as a meta-parameter. The metaparam will only be stored as a shadow.
# @param klass [Class<inherits Puppet::Parameter>] the class of the shadowed meta-parameter
# @return [Puppet::Parameter] an instance of the given class (a parameter or property)
#
def setup_shadow(klass)
@shadow = klass.new(:resource => self.resource)
end
# Returns the wanted _(should)_ value of this property.
# If the _array matching mode_ {#match_all?} is true, an array of the wanted values in unmunged format
# is returned, else the first value in the array of wanted values in unmunged format is returned.
# @return [Array<Object>, Object, nil] Array of values if {#match_all?} else a single value, or nil if there are no
# wanted values.
# @raise [Puppet::DevError] if the wanted value is non nil and not an array
#
# @note This method will potentially return different values than the original values as they are
# converted via munging/unmunging. If the original values are wanted, call {#shouldorig}.
#
# @see #shouldorig
# @api public
#
def should
return nil unless defined?(@should)
self.devfail "should for #{self.class.name} on #{resource.name} is not an array" unless @should.is_a?(Array)
if match_all?
return @should.collect { |val| self.unmunge(val) }
else
return self.unmunge(@should[0])
end
end
# Sets the wanted _(should)_ value of this property.
# If the given value is not already an Array, it will be wrapped in one before being set.
# This method also sets the cached original _should_ values returned by {#shouldorig}.
#
# @param values [Array<Object>, Object] the value(s) to set as the wanted value(s)
# @raise [StandardError] when validation of a value fails (see {#validate}).
# @api public
#
def should=(values)
values = [values] unless values.is_a?(Array)
@shouldorig = values
values.each { |val| validate(val) }
@should = values.collect { |val| self.munge(val) }
end
# Formats the given newvalue (following _should_ type conventions) for inclusion in a string describing a change.
# @return [String] Returns the given newvalue in string form with space separated entries if it is an array.
# @see #change_to_s
#
def should_to_s(newvalue)
[newvalue].flatten.join(" ")
end
# Synchronizes the current value _(is)_ and the wanted value _(should)_ by calling {#set}.
# @raise [Puppet::DevError] if {#should} is nil
# @todo The implementation of this method is somewhat inefficient as it computes the should
# array twice.
def sync
devfail "Got a nil value for should" unless should
set(should)
end
# Asserts that the given value is valid.
# If the developer uses a 'validate' hook, this method will get overridden.
# @raise [Exception] if the value is invalid, or value can not be handled.
# @return [void]
# @api private
#
def unsafe_validate(value)
super
validate_features_per_value(value)
end
# Asserts that all required provider features are present for the given property value.
# @raise [ArgumentError] if a required feature is not present
# @return [void]
# @api private
#
def validate_features_per_value(value)
if features = self.class.value_option(self.class.value_name(value), :required_features)
features = Array(features)
needed_features = features.collect { |f| f.to_s }.join(", ")
raise ArgumentError, "Provider must have features '#{needed_features}' to set '#{self.class.name}' to '#{value}'" unless provider.satisfies?(features)
end
end
# @return [Object, nil] Returns the wanted _(should)_ value of this property.
def value
self.should
end
# (see #should=)
def value=(values)
self.should = values
end
end
diff --git a/lib/puppet/property/ensure.rb b/lib/puppet/property/ensure.rb
index 22ba211e3..c87c3fa53 100644
--- a/lib/puppet/property/ensure.rb
+++ b/lib/puppet/property/ensure.rb
@@ -1,105 +1,105 @@
require 'puppet/property'
# This property is automatically added to any {Puppet::Type} that responds
# to the methods 'exists?', 'create', and 'destroy'.
#
# Ensure defaults to having the wanted _(should)_ value `:present`.
#
# @api public
#
class Puppet::Property::Ensure < Puppet::Property
@name = :ensure
def self.defaultvalues
newvalue(:present) do
if @resource.provider and @resource.provider.respond_to?(:create)
@resource.provider.create
else
@resource.create
end
nil # return nil so the event is autogenerated
end
newvalue(:absent) do
if @resource.provider and @resource.provider.respond_to?(:destroy)
@resource.provider.destroy
else
@resource.destroy
end
nil # return nil so the event is autogenerated
end
defaultto do
if @resource.managed?
:present
else
nil
end
end
# This doc will probably get overridden
@doc ||= "The basic property that the resource should be in."
end
def self.inherited(sub)
# Add in the two properties that everyone will have.
sub.class_eval do
end
end
def change_to_s(currentvalue, newvalue)
begin
if currentvalue == :absent or currentvalue.nil?
return "created"
elsif newvalue == :absent
return "removed"
else
return "#{self.name} changed '#{self.is_to_s(currentvalue)}' to '#{self.should_to_s(newvalue)}'"
end
rescue Puppet::Error, Puppet::DevError
raise
rescue => detail
- raise Puppet::DevError, "Could not convert change #{self.name} to string: #{detail}"
+ raise Puppet::DevError, "Could not convert change #{self.name} to string: #{detail}", detail.backtrace
end
end
# Retrieves the _is_ value for the ensure property.
# The existence of the resource is checked by first consulting the provider (if it responds to
# `:exists`), and secondly the resource. A a value of `:present` or `:absent` is returned
# depending on if the managed entity exists or not.
#
# @return [Symbol] a value of `:present` or `:absent` depending on if it exists or not
# @raise [Puppet::DevError] if neither the provider nor the resource responds to `:exists`
#
def retrieve
# XXX This is a problem -- whether the object exists or not often
# depends on the results of other properties, yet we're the first property
# to get checked, which means that those other properties do not have
# @is values set. This seems to be the source of quite a few bugs,
# although they're mostly logging bugs, not functional ones.
if prov = @resource.provider and prov.respond_to?(:exists?)
result = prov.exists?
elsif @resource.respond_to?(:exists?)
result = @resource.exists?
else
raise Puppet::DevError, "No ability to determine if #{@resource.class.name} exists"
end
if result
return :present
else
return :absent
end
end
# If they're talking about the thing at all, they generally want to
# say it should exist.
#defaultto :present
defaultto do
if @resource.managed?
:present
else
nil
end
end
end
diff --git a/lib/puppet/provider.rb b/lib/puppet/provider.rb
index d88eebea3..fe98e6981 100644
--- a/lib/puppet/provider.rb
+++ b/lib/puppet/provider.rb
@@ -1,630 +1,651 @@
# A Provider is an implementation of the actions that manage resources (of some type) on a system.
# This class is the base class for all implementation of a Puppet Provider.
#
# Concepts:
#--
# * **Confinement** - confinement restricts providers to only be applicable under certain conditions.
# It is possible to confine a provider several different ways:
# * the included {#confine} method which provides filtering on fact, feature, existence of files, or a free form
# predicate.
# * the {commands} method that filters on the availability of given system commands.
# * **Property hash** - the important instance variable `@property_hash` contains all current state values
# for properties (it is lazily built). It is important that these values are managed appropriately in the
# methods {instances}, {prefetch}, and in methods that alters the current state (those that change the
# lifecycle (creates, destroys), or alters some value reflected backed by a property).
# * **Flush** - is a hook that is called once per resource when everything has been applied. The intent is
# that an implementation may defer modification of the current state typically done in property setters
# and instead record information that allows flush to perform the changes more efficiently.
# * **Execution Methods** - The execution methods provides access to execution of arbitrary commands.
# As a convenience execution methods are available on both the instance and the class of a provider since a
# lot of provider logic switch between these contexts fairly freely.
# * **System Entity/Resource** - this documentation uses the term "system entity" for system resources to make
# it clear if talking about a resource on the system being managed (e.g. a file in the file system)
# or about a description of such a resource (e.g. a Puppet Resource).
# * **Resource Type** - this is an instance of Type that describes a classification of instances of Resource (e.g.
# the `File` resource type describes all instances of `file` resources).
# (The term is used to contrast with "type" in general, and specifically to contrast with the implementation
# class of Resource or a specific Type).
#
# @note An instance of a Provider is associated with one resource.
#
# @note Class level methods are only called once to configure the provider (when the type is created), and not
# for each resource the provider is operating on.
# The instance methods are however called for each resource.
#
# @api public
#
class Puppet::Provider
include Puppet::Util
include Puppet::Util::Errors
include Puppet::Util::Warnings
extend Puppet::Util::Warnings
require 'puppet/confiner'
require 'puppet/provider/command'
extend Puppet::Confiner
Puppet::Util.logmethods(self, true)
class << self
# Include the util module so we have access to things like 'which'
include Puppet::Util, Puppet::Util::Docs
include Puppet::Util::Logging
# @return [String] The name of the provider
attr_accessor :name
#
# @todo Original = _"The source parameter exists so that providers using the same
# source can specify this, so reading doesn't attempt to read the
# same package multiple times."_ This seems to be a package type specific attribute. Is this really
# used?
#
# @return [???] The source is WHAT?
attr_writer :source
# @todo Original = _"LAK 2007-05-09: Keep the model stuff around for backward compatibility"_
# Is this really needed? The comment about backwards compatibility was made in 2007.
#
# @return [???] A model kept for backwards compatibility.
# @api private
# @deprecated This attribute is available for backwards compatibility reasons.
attr_reader :model
# @todo What is this type? A reference to a Puppet::Type ?
# @return [Puppet::Type] the resource type (that this provider is ... WHAT?)
#
attr_accessor :resource_type
# @!attribute [r] doc
# The (full) documentation for this provider class. The documentation for the provider class itself
# should be set with the DSL method {desc=}. Setting the documentation with with {doc=} has the same effect
# as setting it with {desc=} (only the class documentation part is set). In essence this means that
# there is no getter for the class documentation part (since the getter returns the full
# documentation when there are additional contributors).
#
# @return [String] Returns the full documentation for the provider.
# @see Puppet::Utils::Docs
# @comment This is puzzling ... a write only doc attribute??? The generated setter never seems to be
# used, instead the instance variable @doc is set in the `desc` method. This seems wrong. It is instead
# documented as a read only attribute (to get the full documentation). Also see doc below for
# desc.
# @!attribute [w] desc
# Sets the documentation of this provider class. (The full documentation is read via the
# {doc} attribute).
#
# @dsl type
#
attr_writer :doc
end
# @todo original = _"LAK 2007-05-09: Keep the model stuff around for backward compatibility"_, why is it
# both here (instance) and at class level? Is this a different model?
# @return [???] model is WHAT?
attr_reader :model
# @return [???] This resource is what? Is an instance of a provider attached to one particular Puppet::Resource?
#
attr_accessor :resource
# Convenience methods - see class method with the same name.
# @see execute
# @return (see execute)
def execute(*args)
Puppet::Util::Execution.execute(*args)
end
# (see Puppet::Util::Execution.execute)
def self.execute(*args)
Puppet::Util::Execution.execute(*args)
end
# Convenience methods - see class method with the same name.
# @see execpipe
# @return (see execpipe)
def execpipe(*args, &block)
Puppet::Util::Execution.execpipe(*args, &block)
end
# (see Puppet::Util::Execution.execpipe)
def self.execpipe(*args, &block)
Puppet::Util::Execution.execpipe(*args, &block)
end
# Convenience methods - see class method with the same name.
# @see execfail
# @return (see execfail)
def execfail(*args)
Puppet::Util::Execution.execfail(*args)
end
# (see Puppet::Util::Execution.execfail)
def self.execfail(*args)
Puppet::Util::Execution.execfail(*args)
end
# Returns the absolute path to the executable for the command referenced by the given name.
# @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
if defined?(@commands) and command = @commands[name]
# nothing
elsif superclass.respond_to? :command and command = superclass.command(name)
# nothing
else
raise Puppet::DevError, "No command #{name} defined for provider #{self.name}"
end
which(command)
end
# Confines this provider to be suitable only on hosts where the given commands are present.
# 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
# to search for them using the PATH variable.
#
# @param command_specs [Hash{String => String}] Map of name to command that the provider will
# 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|
has_command(name, path)
end
end
# Defines optional commands.
# Since Puppet 2.7.8 this is typically not needed as evaluation of provider suitability
# is lazy (when a resource is evaluated) and the absence of commands
# that will be present after other resources have been applied no longer needs to be specified as
# optional.
# @param [Hash{String => String}] hash Named commands that the provider will
# 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
is_optional
end
end
end
# Creates a convenience method for invocation of a command.
#
# This generates a Provider method that allows easy execution of the command. The generated
# method may take arguments that will be passed through to the executable as the command line arguments
# when it is invoked.
#
# @example Use it like this:
# has_command(:echo, "/bin/echo")
# def some_method
# echo("arg 1", "arg 2")
# end
# @comment the . . . below is intentional to avoid the three dots to become an illegible ellipsis char.
# @example . . . or like this
# has_command(:echo, "/bin/echo") do
# is_optional
# environment :HOME => "/var/tmp", :PWD => "/tmp"
# end
#
# @param name [Symbol] The name of the command (will become the name of the generated method that executes the command)
# @param path [String] The path to the executable for the command
# @yield [ ] A block that configures the command (see {Puppet::Provider::Command})
# @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
command = CommandDefiner.define(name, path, self, &block)
@commands[name] = command.executable
# Now define the class and instance methods.
create_class_and_instance_method(name) do |*args|
return command.execute(*args)
end
end
# Internal helper class when creating commands - undocumented.
# @api private
class CommandDefiner
private_class_method :new
def self.define(name, path, confiner, &block)
definer = new(name, path, confiner)
definer.instance_eval(&block) if block
definer.command
end
def initialize(name, path, confiner)
@name = name
@path = path
@optional = false
@confiner = confiner
@custom_environment = {}
end
def is_optional
@optional = true
end
def environment(env)
@custom_environment = @custom_environment.merge(env)
end
def command
if not @optional
@confiner.confine :exists => @path, :for_binary => true
end
Puppet::Provider::Command.new(@name, @path, Puppet::Util, Puppet::Util::Execution, { :failonfail => true, :combine => true, :custom_environment => @custom_environment })
end
end
# @return [Boolean] Return whether the given feature has been declared or not.
def self.declared_feature?(name)
defined?(@declared_features) and @declared_features.include?(name)
end
# @return [Boolean] Returns whether this implementation satisfies all of the default requirements or not.
- # Returns false If defaults are empty.
+ # Returns false if there is no matching defaultfor
# @see Provider.defaultfor
#
def self.default?
- return false if @defaults.empty?
- if @defaults.find do |fact, values|
- values = [values] unless values.is_a? Array
- if fval = Facter.value(fact).to_s and fval != ""
- fval = fval.to_s.downcase.intern
- else
- return false
- end
+ default_match ? true : false
+ end
- # If any of the values match, we're a default.
- if values.find do |value| fval == value.to_s.downcase.intern end
- false
- else
- true
+ # Look through the array of defaultfor hashes and return the first match.
+ # @return [Hash<{String => Object}>] the matching hash specified by a defaultfor
+ # @see Provider.defaultfor
+ # @api private
+ def self.default_match
+ @defaults.find do |default|
+ default.all? do |key, values|
+ case key
+ when :feature
+ feature_match(values)
+ else
+ fact_match(key, values)
end
end
- return false
+ end
+ end
+
+ def self.fact_match(fact, values)
+ values = [values] unless values.is_a? Array
+ values.map! { |v| v.to_s.downcase.intern }
+
+ if fval = Facter.value(fact).to_s and fval != ""
+ fval = fval.to_s.downcase.intern
+
+ values.include?(fval)
else
- return true
+ false
end
end
+ def self.feature_match(value)
+ Puppet.features.send(value.to_s + "?")
+ end
+
# Sets a facts filter that determine which of several suitable providers should be picked by default.
# This selection only kicks in if there is more than one suitable provider.
# To filter on multiple facts the given hash may contain more than one fact name/value entry.
# The filter picks the provider if all the fact/value entries match the current set of facts. (In case
# there are still more than one provider after this filtering, the first found is picked).
# @param hash [Hash<{String => Object}>] hash of fact name to fact value.
# @return [void]
#
def self.defaultfor(hash)
- hash.each do |d,v|
- @defaults[d] = v
- end
+ @defaults << hash
end
# @return [Integer] Returns a numeric specificity for this provider based on how many requirements it has
# and number of _ancestors_. The higher the number the more specific the provider.
- # The number of requirements is based on the number of defaults set up with {Provider.defaultfor}.
+ # The number of requirements is based on the hash size of the matching {Provider.defaultfor}.
#
# The _ancestors_ is the Ruby Module::ancestors method and the number of classes returned is used
# to boost the score. The intent is that if two providers are equal, but one is more "derived" than the other
# (i.e. includes more classes), it should win because it is more specific).
# @note Because of how this value is
# calculated there could be surprising side effects if a provider included an excessive amount of classes.
#
def self.specificity
# This strange piece of logic attempts to figure out how many parent providers there
# are to increase the score. What is will actually do is count all classes that Ruby Module::ancestors
# returns (which can be other classes than those the parent chain) - in a way, an odd measure of the
# complexity of a provider).
- (@defaults.length * 100) + ancestors.select { |a| a.is_a? Class }.length
+ match = default_match
+ length = match ? match.length : 0
+ (length * 100) + ancestors.select { |a| a.is_a? Class }.length
end
# Initializes defaults and commands (i.e. clears them).
# @return [void]
def self.initvars
- @defaults = {}
+ @defaults = []
@commands = {}
end
# Returns a list of system resources (entities) this provider may/can manage.
# This is a query mechanism that lists entities that the provider may manage on a given system. It is
# is directly used in query services, but is also the foundation for other services; prefetching, and
# purging.
#
# As an example, a package provider lists all installed packages. (In contrast, the File provider does
# not list all files on the file-system as that would make execution incredibly slow). An implementation
# of this method should be made if it is possible to quickly (with a single system call) provide all
# instances.
#
# An implementation of this method should only cache the values of properties
# if they are discovered as part of the process for finding existing resources.
# Resource properties that require additional commands (than those used to determine existence/identity)
# should be implemented in their respective getter method. (This is important from a performance perspective;
# it may be expensive to compute, as well as wasteful as all discovered resources may perhaps not be managed).
#
# An implementation may return an empty list (naturally with the effect that it is not possible to query
# for manageable entities).
#
# By implementing this method, it is possible to use the `resources´ resource type to specify purging
# of all non managed entities.
#
# @note The returned instances are instance of some subclass of Provider, not resources.
# @return [Array<Puppet::Provider>] a list of providers referencing the system entities
# @abstract this method must be implemented by a subclass and this super method should never be called as it raises an exception.
# @raise [Puppet::DevError] Error indicating that the method should have been implemented by subclass.
# @see prefetch
def self.instances
raise Puppet::DevError, "Provider #{self.name} has not defined the 'instances' class method"
end
# Creates the methods for a given command.
# @api private
# @deprecated Use {commands}, {optional_commands}, or {has_command} instead. This was not meant to be part of a public API
def self.make_command_methods(name)
Puppet.deprecation_warning "Provider.make_command_methods is deprecated; use Provider.commands or Provider.optional_commands instead for creating command methods"
# Now define a method for that command
unless singleton_class.method_defined?(name)
meta_def(name) do |*args|
# This might throw an ExecutionFailure, but the system above
# will catch it, if so.
command = Puppet::Provider::Command.new(name, command(name), Puppet::Util, Puppet::Util::Execution)
return command.execute(*args)
end
# And then define an instance method that just calls the class method.
# We need both, so both instances and classes can easily run the commands.
unless method_defined?(name)
define_method(name) do |*args|
self.class.send(name, *args)
end
end
end
end
# Creates getter- and setter- methods for each property supported by the resource type.
# Call this method to generate simple accessors for all properties supported by the
# resource type. These simple accessors lookup and sets values in the property hash.
# The generated methods may be overridden by more advanced implementations if something
# else than a straight forward getter/setter pair of methods is required.
# (i.e. define such overriding methods after this method has been called)
#
# An implementor of a provider that makes use of `prefetch` and `flush` can use this method since it uses
# the internal `@property_hash` variable to store values. An implementation would then update the system
# state on a call to `flush` based on the current values in the `@property_hash`.
#
# @return [void]
#
def self.mk_resource_methods
[resource_type.validproperties, resource_type.parameters].flatten.each do |attr|
attr = attr.intern
next if attr == :name
define_method(attr) do
- @property_hash[attr] || :absent
+ if @property_hash[attr].nil?
+ :absent
+ else
+ @property_hash[attr]
+ end
end
define_method(attr.to_s + "=") do |val|
@property_hash[attr] = val
end
end
end
self.initvars
# This method is used to generate a method for a command.
# @return [void]
# @api private
#
def self.create_class_and_instance_method(name, &block)
unless singleton_class.method_defined?(name)
meta_def(name, &block)
end
unless method_defined?(name)
define_method(name) do |*args|
self.class.send(name, *args)
end
end
end
private_class_method :create_class_and_instance_method
# @return [String] Returns the data source, which is the provider name if no other source has been set.
# @todo Unclear what "the source" is used for?
def self.source
@source ||= self.name
end
# Returns true if the given attribute/parameter is supported by the provider.
# The check is made that the parameter is a valid parameter for the resource type, and then
# if all its required features (if any) are supported by the provider.
#
# @param param [Class, Puppet::Parameter] the parameter class, or a parameter instance
# @return [Boolean] Returns whether this provider supports the given parameter or not.
# @raise [Puppet::DevError] if the given parameter is not valid for the resource type
#
def self.supports_parameter?(param)
if param.is_a?(Class)
klass = param
else
unless klass = resource_type.attrclass(param)
raise Puppet::DevError, "'#{param}' is not a valid parameter for #{resource_type.name}"
end
end
return true unless features = klass.required_features
!!satisfies?(*features)
end
# def self.to_s
# unless defined?(@str)
# if self.resource_type
# @str = "#{resource_type.name} provider #{self.name}"
# else
# @str = "unattached provider #{self.name}"
# end
# end
# @str
# end
dochook(:defaults) do
if @defaults.length > 0
- return "Default for " + @defaults.collect do |f, v|
- "`#{f}` == `#{[v].flatten.join(', ')}`"
- end.sort.join(" and ") + "."
+ return @defaults.collect do |d|
+ "Default for " + d.collect do |f, v|
+ "`#{f}` == `#{[v].flatten.join(', ')}`"
+ end.sort.join(" and ") + "."
+ end.join(" ")
end
end
dochook(:commands) do
if @commands.length > 0
return "Required binaries: " + @commands.collect do |n, c|
"`#{c}`"
end.sort.join(", ") + "."
end
end
dochook(:features) do
if features.length > 0
return "Supported features: " + features.collect do |f|
"`#{f}`"
end.sort.join(", ") + "."
end
end
# Clears this provider instance to allow GC to clean up.
def clear
@resource = nil
@model = nil
end
# (see command)
def command(name)
self.class.command(name)
end
# Returns the value of a parameter value, or `:absent` if it is not defined.
# @param param [Puppet::Parameter] the parameter to obtain the value of
# @return [Object] the value of the parameter or `:absent` if not defined.
#
def get(param)
@property_hash[param.intern] || :absent
end
# Creates a new provider that is optionally initialized from a resource or a hash of properties.
# If no argument is specified, a new non specific provider is initialized. If a resource is given
# it is remembered for further operations. If a hash is used it becomes the internal `@property_hash`
# structure of the provider - this hash holds the current state property values of system entities
# as they are being discovered by querying or other operations (typically getters).
#
# @todo The use of a hash as a parameter needs a better exaplanation; why is this done? What is the intent?
# @param resource [Puppet::Resource, Hash] optional resource or hash
#
def initialize(resource = nil)
if resource.is_a?(Hash)
# We don't use a duplicate here, because some providers (ParsedFile, at least)
# use the hash here for later events.
@property_hash = resource
elsif resource
@resource = resource
# LAK 2007-05-09: Keep the model stuff around for backward compatibility
@model = resource
@property_hash = {}
else
@property_hash = {}
end
end
# Returns the name of the resource this provider is operating on.
# @return [String] the name of the resource instance (e.g. the file path of a File).
# @raise [Puppet::DevError] if no resource is set, or no name defined.
#
def name
if n = @property_hash[:name]
return n
elsif self.resource
resource.name
else
raise Puppet::DevError, "No resource and no name in property hash in #{self.class.name} instance"
end
end
# Sets the given parameters values as the current values for those parameters.
# Other parameters are unchanged.
# @param [Array<Puppet::Parameter>] params the parameters with values that should be set
# @return [void]
#
def set(params)
params.each do |param, value|
@property_hash[param.intern] = value
end
end
# @return [String] Returns a human readable string with information about the resource and the provider.
def to_s
"#{@resource}(provider=#{self.class.name})"
end
# Makes providers comparable.
include Comparable
# Compares this provider against another provider.
# Comparison is only possible with another provider (no other class).
# The ordering is based on the class name of the two providers.
#
# @return [-1,0,+1, nil] A comparison result -1, 0, +1 if this is before other, equal or after other. Returns
# nil oif not comparable to other.
# @see Comparable
def <=>(other)
# We can only have ordering against other providers.
return nil unless other.is_a? Puppet::Provider
# Otherwise, order by the providers class name.
return self.class.name <=> other.class.name
end
# @comment Document prefetch here as it does not exist anywhere else (called from transaction if implemented)
# @!method self.prefetch(resource_hash)
# @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 pre-fetch resource properties.
# If implemented it should set the provider instance of the managed resources to a provider with the
# 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()
# @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 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..53132a42e 100644
--- a/lib/puppet/provider/aixobject.rb
+++ b/lib/puppet/provider/aixobject.rb
@@ -1,392 +1,392 @@
#
# Common code for AIX providers. This class implements basic structure for
# AIX resources.
# Author:: Hector Rivas Gandara <keymon@gmail.com>
#
class Puppet::Provider::AixObject < Puppet::Provider
desc "Generic AIX resource provider"
# The real provider must implement these functions.
def lscmd( _value = @resource[:name] )
raise Puppet::Error, "Method not defined #{@resource.class.name} #{@resource.name}: Base AixObject provider doesn't implement lscmd"
end
def addcmd( _extra_attrs = [] )
raise Puppet::Error, "Method not defined #{@resource.class.name} #{@resource.name}: Base AixObject provider doesn't implement addcmd"
end
def modifycmd( _attributes_hash = {} )
raise Puppet::Error, "Method not defined #{@resource.class.name} #{@resource.name}: Base AixObject provider doesn't implement modifycmd"
end
def deletecmd
raise Puppet::Error, "Method not defined #{@resource.class.name} #{@resource.name}: Base AixObject provider doesn't implement deletecmd"
end
# Valid attributes to be managed by this provider.
# It is a list of hashes
# :aix_attr AIX command attribute name
# :puppet_prop Puppet propertie name
# :to Optional. Method name that adapts puppet property to aix command value.
# :from Optional. Method to adapt aix command line value to puppet property. Optional
class << self
attr_accessor :attribute_mapping
end
# Mapping from Puppet property to AIX attribute.
def self.attribute_mapping_to
if ! @attribute_mapping_to
@attribute_mapping_to = {}
attribute_mapping.each { |elem|
attribute_mapping_to[elem[:puppet_prop]] = {
:key => elem[:aix_attr],
:method => elem[:to]
}
}
end
@attribute_mapping_to
end
# Mapping from AIX attribute to Puppet property.
def self.attribute_mapping_from
if ! @attribute_mapping_from
@attribute_mapping_from = {}
attribute_mapping.each { |elem|
attribute_mapping_from[elem[:aix_attr]] = {
:key => elem[:puppet_prop],
:method => elem[:from]
}
}
end
@attribute_mapping_from
end
# This functions translates a key and value using the given mapping.
# Mapping can be nil (no translation) or a hash with this format
# {:key => new_key, :method => translate_method}
# It returns a list with the pair [key, value]
def translate_attr(key, value, mapping)
return [key, value] unless mapping
return nil unless mapping[key]
if mapping[key][:method]
new_value = method(mapping[key][:method]).call(value)
else
new_value = value
end
[mapping[key][:key], new_value]
end
# Loads an AIX attribute (key=value) and stores it in the given hash with
# puppet semantics. It translates the pair using the given mapping.
#
# This operation works with each property one by one,
# subclasses must reimplement this if more complex operations are needed
def load_attribute(key, value, mapping, objectinfo)
if mapping.nil?
objectinfo[key] = value
elsif mapping[key].nil?
# is not present in mapping, ignore it.
true
elsif mapping[key][:method].nil?
objectinfo[mapping[key][:key]] = value
elsif
objectinfo[mapping[key][:key]] = method(mapping[key][:method]).call(value)
end
return objectinfo
end
# Gets the given command line argument for the given key and value,
# using the given mapping to translate key and value.
# All the objectinfo hash (@resource or @property_hash) is passed.
#
# This operation works with each property one by one,
# and default behaviour is return the arguments as key=value pairs.
# Subclasses must reimplement this if more complex operations/arguments
# are needed
#
def get_arguments(key, value, mapping, objectinfo)
if mapping.nil?
new_key = key
new_value = value
elsif mapping[key].nil?
# is not present in mapping, ignore it.
new_key = nil
new_value = nil
elsif mapping[key][:method].nil?
new_key = mapping[key][:key]
new_value = value
elsif
new_key = mapping[key][:key]
new_value = method(mapping[key][:method]).call(value)
end
# convert it to string
new_value = Array(new_value).join(',')
if new_key
return [ "#{new_key}=#{new_value}" ]
else
return []
end
end
# Convert the provider properties (hash) to AIX command arguments
# (list of strings)
# This function will translate each value/key and generate the argument using
# the get_arguments function.
def hash2args(hash, mapping=self.class.attribute_mapping_to)
return "" unless hash
arg_list = []
hash.each {|key, val|
arg_list += self.get_arguments(key, val, mapping, hash)
}
arg_list
end
# Parse AIX command attributes from the output of an AIX command, that
# which format is a list of space separated of key=value pairs:
# "uid=100 groups=a,b,c".
# It returns a hash.
#
# If a mapping is provided, the keys are translated as defined in the
# mapping hash. And only values included in mapping will be added
#
# NOTE: it will ignore the items not including '='
def parse_attr_list(str, mapping=self.class.attribute_mapping_from)
properties = {}
attrs = []
if str.nil? or (attrs = str.split()).empty?
return nil
end
attrs.each { |i|
if i.include? "=" # Ignore if it does not include '='
(key_str, val) = i.split('=')
# Check the key
if key_str.nil? or key_str.empty?
info "Empty key in string 'i'?"
continue
end
key_str.strip!
key = key_str.to_sym
val.strip! if val
properties = self.load_attribute(key, val, mapping, properties)
end
}
properties.empty? ? nil : properties
end
# Parse AIX command output in a colon separated list of attributes,
# This function is useful to parse the output of commands like lsfs -c:
# #MountPoint:Device:Vfs:Nodename:Type:Size:Options:AutoMount:Acct
# /:/dev/hd4:jfs2::bootfs:557056:rw:yes:no
# /home:/dev/hd1:jfs2:::2129920:rw:yes:no
# /usr:/dev/hd2:jfs2::bootfs:9797632:rw:yes:no
#
# If a mapping is provided, the keys are translated as defined in the
# mapping hash. And only values included in mapping will be added
def parse_colon_list(str, key_list, mapping=self.class.attribute_mapping_from)
properties = {}
attrs = []
if str.nil? or (attrs = str.split(':')).empty?
return nil
end
attrs.each { |val|
key = key_list.shift.downcase.to_sym
properties = self.load_attribute(key, val, mapping, properties)
}
properties.empty? ? nil : properties
end
# Default parsing function for AIX commands.
# It will choose the method depending of the first line.
# For the colon separated list it will:
# 1. Get keys from first line.
# 2. Parse next line.
def parse_command_output(output, mapping=self.class.attribute_mapping_from)
lines = output.split("\n")
# if it begins with #something:... is a colon separated list.
if lines[0] =~ /^#.*:/
self.parse_colon_list(lines[1], lines[0][1..-1].split(':'), mapping)
else
self.parse_attr_list(lines[0], mapping)
end
end
# Retrieve all the information of an existing resource.
# It will execute 'lscmd' command and parse the output, using the mapping
# 'attribute_mapping_from' to translate the keys and values.
def getinfo(refresh = false)
if @objectinfo.nil? or refresh == true
# Execute lsuser, split all attributes and add them to a dict.
begin
output = execute(self.lscmd)
@objectinfo = self.parse_command_output(output)
# All attributtes without translation
@objectosinfo = self.parse_command_output(output, nil)
rescue Puppet::ExecutionFailure => detail
# Print error if needed. FIXME: Do not check the user here.
Puppet.debug "aix.getinfo(): Could not find #{@resource.class.name} #{@resource.name}: #{detail}"
end
end
@objectinfo
end
# Like getinfo, but it will not use the mapping to translate the keys and values.
# It might be usefult to retrieve some raw information.
def getosinfo(refresh = false)
if @objectosinfo.nil? or refresh == true
getinfo(refresh)
end
@objectosinfo || Hash.new
end
# List all elements of given type. It works for colon separated commands and
# list commands.
# It returns a list of names.
def self.list_all
names = []
begin
output = execute([self.command(:list), 'ALL'])
output = output.split("\n").select{ |line| line != /^#/ }
output.each do |line|
name = line.split(/[ :]/)[0]
names << name if not name.empty?
end
rescue Puppet::ExecutionFailure => detail
# Print error if needed
Puppet.debug "aix.list_all(): Could not get all resources of type #{@resource.class.name}: #{detail}"
end
names
end
#-------------
# Provider API
# ------------
# Clear out the cached values.
def flush
@property_hash.clear if @property_hash
@objectinfo.clear if @objectinfo
end
# Check that the user exists
def exists?
!!getinfo(true) # !! => converts to bool
end
# Return all existing instances
# The method for returning a list of provider instances. Note that it returns
# providers, preferably with values already filled in, not resources.
def self.instances
objects=[]
list_all.each { |entry|
objects << new(:name => entry, :ensure => :present)
}
objects
end
#- **ensure**
# The basic state that the object should be in. Valid values are
# `present`, `absent`, `role`.
# From ensurable: exists?, create, delete
def ensure
if exists?
:present
else
:absent
end
end
# Create a new instance of the resource
def create
if exists?
info "already exists"
# The object already exists
return nil
end
begin
execute(self.addcmd)
rescue Puppet::ExecutionFailure => detail
- raise Puppet::Error, "Could not create #{@resource.class.name} #{@resource.name}: #{detail}"
+ raise Puppet::Error, "Could not create #{@resource.class.name} #{@resource.name}: #{detail}", detail.backtrace
end
end
# Delete this instance of the resource
def delete
unless exists?
info "already absent"
# the object already doesn't exist
return nil
end
begin
execute(self.deletecmd)
rescue Puppet::ExecutionFailure => detail
- raise Puppet::Error, "Could not delete #{@resource.class.name} #{@resource.name}: #{detail}"
+ raise Puppet::Error, "Could not delete #{@resource.class.name} #{@resource.name}: #{detail}", detail.backtrace
end
end
#--------------------------------
# Call this method when the object is initialized.
# It creates getter/setter methods for each property our resource type supports.
# If setter or getter already defined it will not be overwritten
def self.mk_resource_methods
[resource_type.validproperties, resource_type.parameters].flatten.each do |prop|
next if prop == :ensure
define_method(prop) { get(prop) || :absent} unless public_method_defined?(prop)
define_method(prop.to_s + "=") { |*vals| set(prop, *vals) } unless public_method_defined?(prop.to_s + "=")
end
end
# Define the needed getters and setters as soon as we know the resource type
def self.resource_type=(resource_type)
super
mk_resource_methods
end
# Retrieve a specific value by name.
def get(param)
(hash = getinfo(false)) ? hash[param] : nil
end
# Set a property.
def set(param, value)
@property_hash[param.intern] = value
if getinfo().nil?
# This is weird...
raise Puppet::Error, "Trying to update parameter '#{param}' to '#{value}' for a resource that does not exists #{@resource.class.name} #{@resource.name}: #{detail}"
end
if value == getinfo()[param.to_sym]
return
end
#self.class.validate(param, value)
if cmd = modifycmd({param =>value})
begin
execute(cmd)
rescue Puppet::ExecutionFailure => detail
- raise Puppet::Error, "Could not set #{param} on #{@resource.class.name}[#{@resource.name}]: #{detail}"
+ raise Puppet::Error, "Could not set #{param} on #{@resource.class.name}[#{@resource.name}]: #{detail}", detail.backtrace
end
end
# Refresh de info.
getinfo(true)
end
def initialize(resource)
super
@objectinfo = nil
@objectosinfo = nil
end
end
diff --git a/lib/puppet/provider/augeas/augeas.rb b/lib/puppet/provider/augeas/augeas.rb
index f0369a420..e3203c81e 100644
--- a/lib/puppet/provider/augeas/augeas.rb
+++ b/lib/puppet/provider/augeas/augeas.rb
@@ -1,509 +1,509 @@
#
# Copyright 2011 Bryan Kearney <bkearney@redhat.com>
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
require 'augeas' if Puppet.features.augeas?
require 'strscan'
require 'puppet/util'
require 'puppet/util/diff'
require 'puppet/util/package'
Puppet::Type.type(:augeas).provide(:augeas) do
include Puppet::Util
include Puppet::Util::Diff
include Puppet::Util::Package
confine :feature => :augeas
has_features :parse_commands, :need_to_run?,:execute_changes
SAVE_NOOP = "noop"
SAVE_OVERWRITE = "overwrite"
SAVE_NEWFILE = "newfile"
SAVE_BACKUP = "backup"
COMMANDS = {
"set" => [ :path, :string ],
"setm" => [ :path, :string, :string ],
"rm" => [ :path ],
"clear" => [ :path ],
"clearm" => [ :path, :string ],
"mv" => [ :path, :path ],
"insert" => [ :string, :string, :path ],
"get" => [ :path, :comparator, :string ],
"defvar" => [ :string, :path ],
"defnode" => [ :string, :path, :string ],
"match" => [ :path, :glob ],
"size" => [:comparator, :int],
"include" => [:string],
"not_include" => [:string],
"==" => [:glob],
"!=" => [:glob]
}
COMMANDS["ins"] = COMMANDS["insert"]
COMMANDS["remove"] = COMMANDS["rm"]
COMMANDS["move"] = COMMANDS["mv"]
attr_accessor :aug
# Extracts an 2 dimensional array of commands which are in the
# form of command path value.
# The input can be
# - A string with one command
# - A string with many commands per line
# - An array of strings.
def parse_commands(data)
context = resource[:context]
# Add a trailing / if it is not there
if (context.length > 0)
context << "/" if context[-1, 1] != "/"
end
data = data.split($/) if data.is_a?(String)
data = data.flatten
args = []
data.each do |line|
line.strip!
next if line.nil? || line.empty?
argline = []
sc = StringScanner.new(line)
cmd = sc.scan(/\w+|==|!=/)
formals = COMMANDS[cmd]
fail("Unknown command #{cmd}") unless formals
argline << cmd
narg = 0
formals.each do |f|
sc.skip(/\s+/)
narg += 1
if f == :path
start = sc.pos
nbracket = 0
inSingleTick = false
inDoubleTick = false
begin
sc.skip(/([^\]\[\s\\'"]|\\.)+/)
ch = sc.getch
nbracket += 1 if ch == "["
nbracket -= 1 if ch == "]"
inSingleTick = !inSingleTick if ch == "'"
inDoubleTick = !inDoubleTick if ch == "\""
fail("unmatched [") if nbracket < 0
end until ((nbracket == 0 && !inSingleTick && !inDoubleTick && (ch =~ /\s/)) || sc.eos?)
len = sc.pos - start
len -= 1 unless sc.eos?
unless p = sc.string[start, len]
fail("missing path argument #{narg} for #{cmd}")
end
# Rip off any ticks if they are there.
p = p[1, (p.size - 2)] if p[0,1] == "'" || p[0,1] == "\""
p.chomp!("/")
if p[0,1] != '$' && p[0,1] != "/"
argline << context + p
else
argline << p
end
elsif f == :string
delim = sc.peek(1)
if delim == "'" || delim == "\""
sc.getch
argline << sc.scan(/([^\\#{delim}]|(\\.))*/)
sc.getch
else
argline << sc.scan(/[^\s]+/)
end
fail("missing string argument #{narg} for #{cmd}") unless argline[-1]
elsif f == :comparator
argline << sc.scan(/(==|!=|=~|<=|>=|<|>)/)
unless argline[-1]
puts sc.rest
fail("invalid comparator for command #{cmd}")
end
elsif f == :int
argline << sc.scan(/\d+/).to_i
elsif f== :glob
argline << sc.rest
end
end
args << argline
end
args
end
def open_augeas
unless @aug
flags = Augeas::NONE
flags = Augeas::TYPE_CHECK if resource[:type_check] == :true
if resource[:incl]
flags |= Augeas::NO_MODL_AUTOLOAD
else
flags |= Augeas::NO_LOAD
end
root = resource[:root]
load_path = get_load_path(resource)
debug("Opening augeas with root #{root}, lens path #{load_path}, flags #{flags}")
@aug = Augeas::open(root, load_path,flags)
debug("Augeas version #{get_augeas_version} is installed") if versioncmp(get_augeas_version, "0.3.6") >= 0
# Optimize loading if the context is given and it's a simple path,
# requires the glob function from Augeas 0.8.2 or up
glob_avail = !aug.match("/augeas/version/pathx/functions/glob").empty?
opt_ctx = resource[:context].match("^/files/[^'\"\\[\\]]+$") if resource[:context]
restricted = false
if resource[:incl]
aug.set("/augeas/load/Xfm/lens", resource[:lens])
aug.set("/augeas/load/Xfm/incl", resource[:incl])
- restricted = true
+ restricted_metadata = "/augeas//error"
elsif glob_avail and opt_ctx
# Optimize loading if the context is given, requires the glob function
# from Augeas 0.8.2 or up
ctx_path = resource[:context].sub(/^\/files(.*?)\/?$/, '\1/')
load_path = "/augeas/load/*['%s' !~ glob(incl) + regexp('/.*')]" % ctx_path
if aug.match(load_path).size < aug.match("/augeas/load/*").size
aug.rm(load_path)
- restricted = true
+ restricted_metadata = "/augeas/files#{ctx_path}/error"
else
# This will occur if the context is less specific than any glob
debug("Unable to optimize files loaded by context path, no glob matches")
end
end
aug.load
- print_load_errors(:warning => restricted)
+ print_load_errors(restricted_metadata)
end
@aug
end
def close_augeas
if @aug
@aug.close
debug("Closed the augeas connection")
@aug = nil
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
def process_get(cmd_array)
return_value = false
#validate and tear apart the command
fail ("Invalid command: #{cmd_array.join(" ")}") if cmd_array.length < 4
cmd = cmd_array.shift
path = cmd_array.shift
comparator = cmd_array.shift
arg = cmd_array.join(" ")
#check the value in augeas
result = @aug.get(path) || ''
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)
elsif comparator == "=~"
regex = Regexp.new(arg)
return_value = (result =~ regex)
else
return_value = (result.send(comparator, arg))
end
!!return_value
end
# Used by the need_to_run? method to process match filters. Returns
# true if there is a match, false if otherwise
def process_match(cmd_array)
return_value = false
#validate and tear apart the command
fail("Invalid command: #{cmd_array.join(" ")}") if cmd_array.length < 3
cmd = cmd_array.shift
path = cmd_array.shift
# Need to break apart the clause
clause_array = parse_commands(cmd_array.shift)[0]
verb = clause_array.shift
#Get the values from augeas
result = @aug.match(path) || []
fail("Error trying to match path '#{path}'") if (result == -1)
# Now do the work
case verb
when "size"
fail("Invalid command: #{cmd_array.join(" ")}") if clause_array.length != 2
comparator = clause_array.shift
arg = clause_array.shift
case comparator
when "!="
return_value = !(result.size.send(:==, arg))
else
return_value = (result.size.send(comparator, arg))
end
when "include"
arg = clause_array.shift
return_value = result.include?(arg)
when "not_include"
arg = clause_array.shift
return_value = !result.include?(arg)
when "=="
begin
arg = clause_array.shift
new_array = eval arg
return_value = (result == new_array)
rescue
fail("Invalid array in command: #{cmd_array.join(" ")}")
end
when "!="
begin
arg = clause_array.shift
new_array = eval arg
return_value = (result != new_array)
rescue
fail("Invalid array in command: #{cmd_array.join(" ")}")
end
end
!!return_value
end
# Generate lens load paths from user given paths and local pluginsync dir
def get_load_path(resource)
load_path = []
# Permits colon separated strings or arrays
if resource[:load_path]
load_path = [resource[:load_path]].flatten
load_path.map! { |path| path.split(/:/) }
load_path.flatten!
end
- if Puppet::FileSystem::File.exist?("#{Puppet[:libdir]}/augeas/lenses")
+ if Puppet::FileSystem.exist?("#{Puppet[:libdir]}/augeas/lenses")
load_path << "#{Puppet[:libdir]}/augeas/lenses"
end
load_path.join(":")
end
def get_augeas_version
@aug.get("/augeas/version") || ""
end
def set_augeas_save_mode(mode)
@aug.set("/augeas/save", mode)
end
- def print_load_errors(args={})
+ def print_load_errors(path)
errors = @aug.match("/augeas//error")
unless errors.empty?
- if args[:warning]
+ if path && !@aug.match(path).empty?
warning("Loading failed for one or more files, see debug for /augeas//error output")
else
debug("Loading failed for one or more files, output from /augeas//error:")
end
end
print_errors(errors)
end
def print_put_errors
errors = @aug.match("/augeas//error[. = 'put_failed']")
debug("Put failed on one or more files, output from /augeas//error:") unless errors.empty?
print_errors(errors)
end
def print_errors(errors)
errors.each do |errnode|
error = @aug.get(errnode)
debug("#{errnode} = #{error}") unless error.nil?
@aug.match("#{errnode}/*").each do |subnode|
subvalue = @aug.get(subnode)
debug("#{subnode} = #{subvalue}")
end
end
end
# Determines if augeas actually needs to run.
def need_to_run?
force = resource[:force]
return_value = true
begin
open_augeas
filter = resource[:onlyif]
unless filter == ""
cmd_array = parse_commands(filter)[0]
command = cmd_array[0];
begin
case command
when "get"; return_value = process_get(cmd_array)
when "match"; return_value = process_match(cmd_array)
end
rescue SystemExit,NoMemoryError
raise
rescue Exception => e
fail("Error sending command '#{command}' with params #{cmd_array[1..-1].inspect}/#{e.message}")
end
end
unless force
# If we have a verison of augeas which is at least 0.3.6 then we
# can make the changes now and see if changes were made.
if return_value and versioncmp(get_augeas_version, "0.3.6") >= 0
debug("Will attempt to save and only run if files changed")
# Execute in NEWFILE mode so we can show a diff
set_augeas_save_mode(SAVE_NEWFILE)
do_execute_changes
save_result = @aug.save
unless save_result
print_put_errors
- fail("Save failed with return code #{save_result}, see debug")
+ fail("Saving failed, see debug")
end
saved_files = @aug.match("/augeas/events/saved")
if saved_files.size > 0
root = resource[:root].sub(/^\/$/, "")
saved_files.map! {|key| @aug.get(key).sub(/^\/files/, root) }
saved_files.uniq.each do |saved_file|
if Puppet[:show_diff]
notice "\n" + diff(saved_file, saved_file + ".augnew")
end
File.delete(saved_file + ".augnew")
end
debug("Files changed, should execute")
return_value = true
else
debug("Skipping because no files were changed")
return_value = false
end
end
end
ensure
if not return_value or resource.noop? or not save_result
close_augeas
end
end
return_value
end
def execute_changes
# Workaround Augeas bug where changing the save mode doesn't trigger a
# reload of the previously saved file(s) when we call Augeas#load
@aug.match("/augeas/events/saved").each do |file|
@aug.rm("/augeas#{@aug.get(file)}/mtime")
end
# Reload augeas, and execute the changes for real
set_augeas_save_mode(SAVE_OVERWRITE) if versioncmp(get_augeas_version, "0.3.6") >= 0
@aug.load
do_execute_changes
- unless @aug.save
- print_put_errors
- fail("Save failed with return code #{success}, see debug")
- end
+ unless @aug.save
+ print_put_errors
+ fail("Save failed, see debug")
+ end
:executed
ensure
close_augeas
end
# Actually execute the augeas changes.
def do_execute_changes
commands = parse_commands(resource[:changes])
commands.each do |cmd_array|
fail("invalid command #{cmd_array.join[" "]}") if cmd_array.length < 2
command = cmd_array[0]
cmd_array.shift
begin
case command
when "set"
debug("sending command '#{command}' with params #{cmd_array.inspect}")
rv = aug.set(cmd_array[0], cmd_array[1])
fail("Error sending command '#{command}' with params #{cmd_array.inspect}") if (!rv)
when "setm"
if aug.respond_to?(command)
debug("sending command '#{command}' with params #{cmd_array.inspect}")
rv = aug.setm(cmd_array[0], cmd_array[1], cmd_array[2])
fail("Error sending command '#{command}' with params #{cmd_array.inspect}") if (rv == -1)
else
fail("command '#{command}' not supported in installed version of ruby-augeas")
end
when "rm", "remove"
debug("sending command '#{command}' with params #{cmd_array.inspect}")
rv = aug.rm(cmd_array[0])
fail("Error sending command '#{command}' with params #{cmd_array.inspect}") if (rv == -1)
when "clear"
debug("sending command '#{command}' with params #{cmd_array.inspect}")
rv = aug.clear(cmd_array[0])
fail("Error sending command '#{command}' with params #{cmd_array.inspect}") if (!rv)
when "clearm"
# Check command exists ... doesn't currently in ruby-augeas 0.4.1
if aug.respond_to?(command)
debug("sending command '#{command}' with params #{cmd_array.inspect}")
rv = aug.clearm(cmd_array[0], cmd_array[1])
fail("Error sending command '#{command}' with params #{cmd_array.inspect}") if (!rv)
else
fail("command '#{command}' not supported in installed version of ruby-augeas")
end
when "insert", "ins"
label = cmd_array[0]
where = cmd_array[1]
path = cmd_array[2]
case where
when "before"; before = true
when "after"; before = false
else fail("Invalid value '#{where}' for where param")
end
debug("sending command '#{command}' with params #{[label, where, path].inspect}")
rv = aug.insert(path, label, before)
fail("Error sending command '#{command}' with params #{cmd_array.inspect}") if (rv == -1)
when "defvar"
debug("sending command '#{command}' with params #{cmd_array.inspect}")
rv = aug.defvar(cmd_array[0], cmd_array[1])
fail("Error sending command '#{command}' with params #{cmd_array.inspect}") if (!rv)
when "defnode"
debug("sending command '#{command}' with params #{cmd_array.inspect}")
rv = aug.defnode(cmd_array[0], cmd_array[1], cmd_array[2])
fail("Error sending command '#{command}' with params #{cmd_array.inspect}") if (!rv)
when "mv", "move"
debug("sending command '#{command}' with params #{cmd_array.inspect}")
rv = aug.mv(cmd_array[0], cmd_array[1])
fail("Error sending command '#{command}' with params #{cmd_array.inspect}") if (rv == -1)
else fail("Command '#{command}' is not supported")
end
rescue SystemExit,NoMemoryError
raise
rescue Exception => e
fail("Error sending command '#{command}' with params #{cmd_array.inspect}/#{e.message}")
end
end
end
end
diff --git a/lib/puppet/provider/cron/crontab.rb b/lib/puppet/provider/cron/crontab.rb
index 91047af78..9931ee62e 100644
--- a/lib/puppet/provider/cron/crontab.rb
+++ b/lib/puppet/provider/cron/crontab.rb
@@ -1,240 +1,249 @@
require 'puppet/provider/parsedfile'
Puppet::Type.type(:cron).provide(:crontab, :parent => Puppet::Provider::ParsedFile, :default_target => ENV["USER"] || "root") do
commands :crontab => "crontab"
text_line :comment, :match => %r{^\s*#}, :post_parse => proc { |record|
record[:name] = $1 if record[:line] =~ /Puppet Name: (.+)\s*$/
}
text_line :blank, :match => %r{^\s*$}
text_line :environment, :match => %r{^\s*\w+=}
def self.filetype
tabname = case Facter.value(:osfamily)
when "Solaris"
:suntab
when "AIX"
:aixtab
else
:crontab
end
Puppet::Util::FileType.filetype(tabname)
end
self::TIME_FIELDS = [:minute, :hour, :monthday, :month, :weekday]
record_line :crontab,
:fields => %w{time command},
:match => %r{^\s*(@\w+|\S+\s+\S+\s+\S+\s+\S+\s+\S+)\s+(.+)$},
:absent => '*',
:block_eval => :instance do
def post_parse(record)
time = record.delete(:time)
if match = /@(\S+)/.match(time)
# is there another way to access the constant?
Puppet::Type::Cron::ProviderCrontab::TIME_FIELDS.each { |f| record[f] = :absent }
record[:special] = match.captures[0]
elsif match = /(\S+)\s+(\S+)\s+(\S+)\s+(\S+)\s+(\S+)/.match(time)
record[:special] = :absent
Puppet::Type::Cron::ProviderCrontab::TIME_FIELDS.zip(match.captures).each do |field,value|
if value == self.absent
record[field] = :absent
else
record[field] = value.split(",")
end
end
else
raise Puppet::Error, "Line got parsed as a crontab entry but cannot be handled. Please file a bug with the contents of your crontab"
end
record
end
def pre_gen(record)
if record[:special] and record[:special] != :absent
record[:special] = "@#{record[:special]}"
end
Puppet::Type::Cron::ProviderCrontab::TIME_FIELDS.each do |field|
if vals = record[field] and vals.is_a?(Array)
record[field] = vals.join(",")
end
end
record
end
def to_line(record)
str = ""
+ record[:name] = nil if record[:unmanaged]
str = "# Puppet Name: #{record[:name]}\n" if record[:name]
if record[:environment] and record[:environment] != :absent
str += record[:environment].map {|line| "#{line}\n"}.join('')
end
if record[:special] and record[:special] != :absent
fields = [:special, :command]
else
fields = Puppet::Type::Cron::ProviderCrontab::TIME_FIELDS + [:command]
end
str += record.values_at(*fields).map do |field|
if field.nil? or field == :absent
self.absent
else
field
end
end.join(self.joiner)
str
end
end
# Look up a resource with a given name whose user matches a record target
#
# @api private
#
# @note This overrides the ParsedFile method for finding resources by name,
# so that only records for a given user are matched to resources of the
# same user so that orphaned records in other crontabs don't get falsely
# matched (#2251)
#
# @param [Hash<Symbol, Object>] record
# @param [Array<Puppet::Resource>] resources
#
# @return [Puppet::Resource, nil] The resource if found, else nil
def self.resource_for_record(record, resources)
resource = super
if resource and record[:target] == resource[:user]
resource
end
end
# Return the header placed at the top of each generated file, warning
# users that modifying this file manually is probably a bad idea.
def self.header
%{# HEADER: This file was autogenerated at #{Time.now} by puppet.
# HEADER: While it can still be managed manually, it is definitely not recommended.
# HEADER: Note particularly that the comments starting with 'Puppet Name' should
# HEADER: not be deleted, as doing so could cause duplicate cron jobs.\n}
end
# Regex for finding one vixie cron header.
def self.native_header_regex
/# DO NOT EDIT THIS FILE.*?Cron version.*?vixie.*?\n/m
end
# If a vixie cron header is found, it should be dropped, cron will insert
# a new one in any case, so we need to avoid duplicates.
def self.drop_native_header
true
end
# See if we can match the record against an existing cron job.
def self.match(record, resources)
# if the record is named, do not even bother (#19876)
- return false if record[:name]
+ # except the resource name was implicitly generated (#3220)
+ return false if record[:name] and !record[:unmanaged]
resources.each do |name, resource|
# Match the command first, since it's the most important one.
next unless record[:target] == resource[:target]
next unless record[:command] == resource.value(:command)
# Now check the time fields
compare_fields = self::TIME_FIELDS + [:special]
matched = true
compare_fields.each do |field|
# If the resource does not manage a property (say monthday) it should
# always match. If it is the other way around (e.g. resource defines
# a should value for :special but the record does not have it, we do
# not match
next unless resource[field]
unless record.include?(field)
matched = false
break
end
if record_value = record[field] and resource_value = resource.value(field)
# The record translates '*' into absent in the post_parse hook and
# the resource type does exactly the opposite (alias :absent to *)
next if resource_value == '*' and record_value == :absent
next if resource_value == record_value
end
matched =false
break
end
return resource if matched
end
false
end
+ @name_index = 0
+
# Collapse name and env records.
def self.prefetch_hook(records)
name = nil
envs = nil
result = records.each { |record|
case record[:record_type]
when :comment
if record[:name]
name = record[:name]
record[:skip] = true
# Start collecting env values
envs = []
end
when :environment
# If we're collecting env values (meaning we're in a named cronjob),
# store the line and skip the record.
if envs
envs << record[:line]
record[:skip] = true
end
when :blank
# nothing
else
if name
record[:name] = name
name = nil
+ else
+ cmd_string = record[:command].gsub(/\s+/, "_")
+ index = ( @name_index += 1 )
+ record[:name] = "unmanaged:#{cmd_string}-#{ index.to_s }"
+ record[:unmanaged] = true
end
if envs.nil? or envs.empty?
record[:environment] = :absent
else
# Collect all of the environment lines, and mark the records to be skipped,
# since their data is included in our crontab record.
record[:environment] = envs
# And turn off env collection again
envs = nil
end
end
}.reject { |record| record[:skip] }
result
end
def self.to_file(records)
text = super
# Apparently Freebsd will "helpfully" add a new TZ line to every
# single cron line, but not in all cases (e.g., it doesn't do it
# on my machine). This is my attempt to fix it so the TZ lines don't
# multiply.
if text =~ /(^TZ=.+\n)/
tz = $1
text.sub!(tz, '')
text = tz + text
end
text
end
def user=(user)
# we have to mark the target as modified first, to make sure that if
# we move a cronjob from userA to userB, userA's crontab will also
# be rewritten
mark_target_modified
@property_hash[:user] = user
@property_hash[:target] = user
end
def user
@property_hash[:user] || @property_hash[:target]
end
end
diff --git a/lib/puppet/provider/exec.rb b/lib/puppet/provider/exec.rb
index b0db9ff94..e5b90b6e2 100644
--- a/lib/puppet/provider/exec.rb
+++ b/lib/puppet/provider/exec.rb
@@ -1,91 +1,91 @@
require 'puppet/provider'
require 'puppet/util/execution'
class Puppet::Provider::Exec < Puppet::Provider
include Puppet::Util::Execution
def run(command, check = false)
output = nil
status = nil
dir = nil
checkexe(command)
if dir = resource[:cwd]
unless File.directory?(dir)
if check
dir = nil
else
self.fail "Working directory '#{dir}' does not exist"
end
end
end
dir ||= Dir.pwd
debug "Executing#{check ? " check": ""} '#{command}'"
begin
# Do our chdir
Dir.chdir(dir) do
environment = {}
environment[:PATH] = resource[:path].join(File::PATH_SEPARATOR) if resource[:path]
if envlist = resource[:environment]
envlist = [envlist] unless envlist.is_a? Array
envlist.each do |setting|
if setting =~ /^(\w+)=((.|\n)+)$/
env_name = $1
value = $2
if environment.include?(env_name) || environment.include?(env_name.to_sym)
warning "Overriding environment setting '#{env_name}' with '#{value}'"
end
environment[env_name] = value
else
warning "Cannot understand environment setting #{setting.inspect}"
end
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 = 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 output.exitstatus == 127
raise ArgumentError, output
end
end
rescue Errno::ENOENT => detail
- self.fail detail.to_s
+ self.fail Puppet::Error, detail.to_s, detail
end
# 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)
if command.is_a? Array
command.first
elsif match = /^"([^"]+)"|^'([^']+)'/.match(command)
# extract whichever of the two sides matched the content.
match[1] or match[2]
else
command.split(/ /)[0]
end
end
def validatecmd(command)
exe = extractexe(command)
# if we're not fully qualified, require a path
self.fail "'#{command}' is not qualified and no path was specified. Please qualify the command or specify a path." if !absolute_path?(exe) and resource[:path].nil?
end
end
diff --git a/lib/puppet/provider/exec/posix.rb b/lib/puppet/provider/exec/posix.rb
index c552f9a03..1b0ae56a5 100644
--- a/lib/puppet/provider/exec/posix.rb
+++ b/lib/puppet/provider/exec/posix.rb
@@ -1,48 +1,48 @@
require 'puppet/provider/exec'
Puppet::Type.type(:exec).provide :posix, :parent => Puppet::Provider::Exec do
has_feature :umask
confine :feature => :posix
defaultfor :feature => :posix
desc <<-EOT
Executes external binaries directly, without passing through a shell or
performing any interpolation. This is a safer and more predictable way
to execute most commands, but prevents the use of globbing and shell
built-ins (including control logic like "for" and "if" statements).
EOT
# Verify that we have the executable
def checkexe(command)
exe = extractexe(command)
if File.expand_path(exe) == exe
- if !Puppet::FileSystem::File.exist?(exe)
+ if !Puppet::FileSystem.exist?(exe)
raise ArgumentError, "Could not find command '#{exe}'"
elsif !File.file?(exe)
raise ArgumentError, "'#{exe}' is a #{File.ftype(exe)}, not a file"
elsif !File.executable?(exe)
raise ArgumentError, "'#{exe}' is not executable"
end
return
end
if resource[:path]
Puppet::Util.withenv :PATH => resource[:path].join(File::PATH_SEPARATOR) do
return if which(exe)
end
end
# 'which' will only return the command if it's executable, so we can't
# 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 1c727ef0b..4c792af6d 100644
--- a/lib/puppet/provider/exec/windows.rb
+++ b/lib/puppet/provider/exec/windows.rb
@@ -1,55 +1,55 @@
require 'puppet/provider/exec'
Puppet::Type.type(:exec).provide :windows, :parent => Puppet::Provider::Exec do
confine :operatingsystem => :windows
defaultfor :operatingsystem => :windows
desc <<-'EOT'
Execute external binaries on Windows systems. As with the `posix`
provider, this provider directly calls the command with the arguments
given, without passing it through a shell or performing any interpolation.
To use shell built-ins --- that is, to emulate the `shell` provider on
Windows --- a command must explicitly invoke the shell:
exec {'echo foo':
command => 'cmd.exe /c echo "foo"',
}
If no extension is specified for a command, Windows will use the `PATHEXT`
environment variable to locate the executable.
**Note on PowerShell scripts:** PowerShell's default `restricted`
execution policy doesn't allow it to run saved scripts. To run PowerShell
scripts, specify the `remotesigned` execution policy as part of the
command:
exec { 'test':
path => 'C:/Windows/System32/WindowsPowerShell/v1.0',
command => 'powershell -executionpolicy remotesigned -file C:/test.ps1',
}
EOT
# Verify that we have the executable
def checkexe(command)
exe = extractexe(command)
if absolute_path?(exe)
- if !Puppet::FileSystem::File.exist?(exe)
+ if !Puppet::FileSystem.exist?(exe)
raise ArgumentError, "Could not find command '#{exe}'"
elsif !File.file?(exe)
raise ArgumentError, "'#{exe}' is a #{File.ftype(exe)}, not a file"
end
return
end
if resource[:path]
Puppet::Util.withenv :PATH => resource[:path].join(File::PATH_SEPARATOR) do
return if which(exe)
end
end
raise ArgumentError, "Could not find command '#{exe}'"
end
end
diff --git a/lib/puppet/provider/file/posix.rb b/lib/puppet/provider/file/posix.rb
index 629a380dd..0ce7d223e 100644
--- a/lib/puppet/provider/file/posix.rb
+++ b/lib/puppet/provider/file/posix.rb
@@ -1,136 +1,136 @@
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
require 'etc'
def uid2name(id)
return id.to_s if id.is_a?(Symbol) or id.is_a?(String)
return nil if id > Puppet[:maximum_uid].to_i
begin
user = Etc.getpwuid(id)
rescue TypeError, ArgumentError
return nil
end
if user.uid == ""
return nil
else
return user.name
end
end
# Determine if the user is valid, and if so, return the UID
def name2uid(value)
Integer(value) rescue uid(value) || false
end
def gid2name(id)
return id.to_s if id.is_a?(Symbol) or id.is_a?(String)
return nil if id > Puppet[:maximum_uid].to_i
begin
group = Etc.getgrgid(id)
rescue TypeError, ArgumentError
return nil
end
if group.gid == ""
return nil
else
return group.name
end
end
def name2gid(value)
Integer(value) rescue gid(value) || false
end
def owner
unless stat = resource.stat
return :absent
end
currentvalue = stat.uid
# On OS X, files that are owned by -2 get returned as really
# large UIDs instead of negative ones. This isn't a Ruby bug,
# it's an OS X bug, since it shows up in perl, too.
if currentvalue > Puppet[:maximum_uid].to_i
self.warning "Apparently using negative UID (#{currentvalue}) on a platform that does not consistently handle them"
currentvalue = :silly
end
currentvalue
end
def owner=(should)
# Set our method appropriately, depending on links.
if resource[:links] == :manage
method = :lchown
else
method = :chown
end
begin
File.send(method, should, nil, resource[:path])
rescue => detail
- raise Puppet::Error, "Failed to set owner to '#{should}': #{detail}"
+ raise Puppet::Error, "Failed to set owner to '#{should}': #{detail}", detail.backtrace
end
end
def group
return :absent unless stat = resource.stat
currentvalue = stat.gid
# On OS X, files that are owned by -2 get returned as really
# large GIDs instead of negative ones. This isn't a Ruby bug,
# it's an OS X bug, since it shows up in perl, too.
if currentvalue > Puppet[:maximum_uid].to_i
self.warning "Apparently using negative GID (#{currentvalue}) on a platform that does not consistently handle them"
currentvalue = :silly
end
currentvalue
end
def group=(should)
# Set our method appropriately, depending on links.
if resource[:links] == :manage
method = :lchown
else
method = :chown
end
begin
File.send(method, nil, should, resource[:path])
rescue => detail
- raise Puppet::Error, "Failed to set group to '#{should}': #{detail}"
+ raise Puppet::Error, "Failed to set group to '#{should}': #{detail}", detail.backtrace
end
end
def mode
if stat = resource.stat
return (stat.mode & 007777).to_s(8)
else
return :absent
end
end
def mode=(value)
begin
File.chmod(value.to_i(8), resource[:path])
rescue => detail
error = Puppet::Error.new("failed to set mode #{mode} on #{resource[:path]}: #{detail.message}")
error.set_backtrace detail.backtrace
raise error
end
end
end
diff --git a/lib/puppet/provider/file/windows.rb b/lib/puppet/provider/file/windows.rb
index 350948c47..5c8038db1 100644
--- a/lib/puppet/provider/file/windows.rb
+++ b/lib/puppet/provider/file/windows.rb
@@ -1,99 +1,105 @@
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
if Puppet.features.microsoft_windows?
require 'puppet/util/windows'
require 'puppet/util/adsi'
include Puppet::Util::Windows::Security
end
# Determine if the account is valid, and if so, return the UID
def name2id(value)
Puppet::Util::Windows::Security.name_to_sid(value)
end
# If it's a valid SID, get the name. Otherwise, it's already a name,
# so just return it.
def id2name(id)
if Puppet::Util::Windows::Security.valid_sid?(id)
Puppet::Util::Windows::Security.sid_to_name(id)
else
id
end
end
# We use users and groups interchangeably, so use the same methods for both
# (the type expects different methods, so we have to oblige).
alias :uid2name :id2name
alias :gid2name :id2name
alias :name2gid :name2id
alias :name2uid :name2id
def owner
return :absent unless resource.stat
get_owner(resource[:path])
end
def owner=(should)
begin
- path = resource[:links] == :manage ? file.path.to_s : file.readlink
-
- set_owner(should, path)
+ set_owner(should, resolved_path)
rescue => detail
- raise Puppet::Error, "Failed to set owner to '#{should}': #{detail}"
+ raise Puppet::Error, "Failed to set owner to '#{should}': #{detail}", detail.backtrace
end
end
def group
return :absent unless resource.stat
get_group(resource[:path])
end
def group=(should)
begin
- path = resource[:links] == :manage ? file.path.to_s : file.readlink
-
- set_group(should, path)
+ set_group(should, resolved_path)
rescue => detail
- raise Puppet::Error, "Failed to set group to '#{should}': #{detail}"
+ raise Puppet::Error, "Failed to set group to '#{should}': #{detail}", detail.backtrace
end
end
def mode
if resource.stat
mode = get_mode(resource[:path])
mode ? mode.to_s(8) : :absent
else
:absent
end
end
def mode=(value)
begin
set_mode(value.to_i(8), resource[:path])
rescue => detail
error = Puppet::Error.new("failed to set mode #{mode} on #{resource[:path]}: #{detail.message}")
error.set_backtrace detail.backtrace
raise error
end
:file_changed
end
def validate
if [:owner, :group, :mode].any?{|p| resource[p]} and !supports_acl?(resource[:path])
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])
+ @file ||= Puppet::FileSystem.pathname(resource[:path])
+ end
+
+ def resolved_path
+ path = file()
+ # under POSIX, :manage means use lchown - i.e. operate on the link
+ return path.to_s if resource[:links] == :manage
+
+ # otherwise, use chown -- that will resolve the link IFF it is a link
+ # otherwise it will operate on the path
+ Puppet::FileSystem.symlink?(path) ? Puppet::FileSystem.readlink(path) : path.to_s
end
end
diff --git a/lib/puppet/provider/group/aix.rb b/lib/puppet/provider/group/aix.rb
index 666748378..1722748d0 100644
--- a/lib/puppet/provider/group/aix.rb
+++ b/lib/puppet/provider/group/aix.rb
@@ -1,141 +1,141 @@
#
# Group Puppet provider for AIX. It uses standard commands to manage groups:
# mkgroup, rmgroup, lsgroup, chgroup
#
# Author:: Hector Rivas Gandara <keymon@gmail.com>
#
require 'puppet/provider/aixobject'
Puppet::Type.type(:group).provide :aix, :parent => Puppet::Provider::AixObject do
desc "Group management for AIX."
- # This will the the default provider for this platform
+ # This will the default provider for this platform
defaultfor :operatingsystem => :aix
confine :operatingsystem => :aix
# Provider features
has_features :manages_aix_lam
has_features :manages_members
# Commands that manage the element
commands :list => "/usr/sbin/lsgroup"
commands :add => "/usr/bin/mkgroup"
commands :delete => "/usr/sbin/rmgroup"
commands :modify => "/usr/bin/chgroup"
# Group attributes to ignore
def self.attribute_ignore
[]
end
# AIX attributes to properties mapping.
#
# Valid attributes to be managed by this provider.
# It is a list with of hash
# :aix_attr AIX command attribute name
# :puppet_prop Puppet propertie name
# :to Method to adapt puppet property to aix command value. Optional.
# :from Method to adapt aix command value to puppet property. Optional
self.attribute_mapping = [
#:name => :name,
{:aix_attr => :id, :puppet_prop => :gid },
{:aix_attr => :users, :puppet_prop => :members,
:from => :users_from_attr},
{:aix_attr => :attributes, :puppet_prop => :attributes},
]
#--------------
# Command definition
# Return the IA module arguments based on the resource param ia_load_module
def get_ia_module_args
if @resource[:ia_load_module]
["-R", @resource[:ia_load_module].to_s]
else
[]
end
end
def lscmd(value=@resource[:name])
[self.class.command(:list)] +
self.get_ia_module_args +
[ value]
end
def lsallcmd()
lscmd("ALL")
end
def addcmd(extra_attrs = [])
# Here we use the @resource.to_hash to get the list of provided parameters
# Puppet does not call to self.<parameter>= method if it does not exists.
#
# It gets an extra list of arguments to add to the user.
[self.class.command(:add) ] +
self.get_ia_module_args +
self.hash2args(@resource.to_hash) +
extra_attrs + [@resource[:name]]
end
def modifycmd(hash = property_hash)
args = self.hash2args(hash)
return nil if args.empty?
[self.class.command(:modify)] +
self.get_ia_module_args +
args + [@resource[:name]]
end
def deletecmd
[self.class.command(:delete)] +
self.get_ia_module_args +
[@resource[:name]]
end
#--------------
# Overwrite get_arguments to add the attributes arguments
def get_arguments(key, value, mapping, objectinfo)
# In the case of attributes, return a list of key=vlaue
if key == :attributes
raise Puppet::Error, "Attributes must be a list of pairs key=value on #{@resource.class.name}[#{@resource.name}]" \
unless value and value.is_a? Hash
return value.select { |k,v| true }.map { |pair| pair.join("=") }
end
super(key, value, mapping, objectinfo)
end
def filter_attributes(hash)
# Return only not managed attributtes.
hash.select {
|k,v| !self.class.attribute_mapping_from.include?(k) and
!self.class.attribute_ignore.include?(k)
}.inject({}) {
|hash, array| hash[array[0]] = array[1]; hash
}
end
def attributes
filter_attributes(getosinfo(false))
end
def attributes=(attr_hash)
#self.class.validate(param, value)
param = :attributes
cmd = modifycmd({param => filter_attributes(attr_hash)})
if cmd
begin
execute(cmd)
rescue Puppet::ExecutionFailure => detail
- raise Puppet::Error, "Could not set #{param} on #{@resource.class.name}[#{@resource.name}]: #{detail}"
+ raise Puppet::Error, "Could not set #{param} on #{@resource.class.name}[#{@resource.name}]: #{detail}", detail.backtrace
end
end
end
# Force convert users it a list.
def users_from_attr(value)
(value.is_a? String) ? value.split(',') : value
end
end
diff --git a/lib/puppet/provider/macauthorization/macauthorization.rb b/lib/puppet/provider/macauthorization/macauthorization.rb
index e410d6f43..ad431d22d 100644
--- a/lib/puppet/provider/macauthorization/macauthorization.rb
+++ b/lib/puppet/provider/macauthorization/macauthorization.rb
@@ -1,309 +1,309 @@
require 'facter'
require 'facter/util/plist'
require 'puppet'
require 'tempfile'
Puppet::Type.type(:macauthorization).provide :macauthorization, :parent => Puppet::Provider do
desc "Manage Mac OS X authorization database rules and rights.
"
commands :security => "/usr/bin/security"
commands :sw_vers => "/usr/bin/sw_vers"
confine :operatingsystem => :darwin
# 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 Puppet::FileSystem::File.exist?("/usr/bin/sw_vers")
+ if Puppet::FileSystem.exist?("/usr/bin/sw_vers")
product_version = sw_vers "-productVersion"
confine :true => unless /^10\.[0-4]/.match(product_version)
true
end
end
defaultfor :operatingsystem => :darwin
AuthDB = "/etc/authorization"
@rights = {}
@rules = {}
@parsed_auth_db = {}
@comment = "" # Not implemented yet. Is there any real need to?
# This map exists due to the use of hyphens and reserved words in
# the authorization schema.
PuppetToNativeAttributeMap = { :allow_root => "allow-root",
:authenticate_user => "authenticate-user",
:auth_class => "class",
:k_of_n => "k-of-n",
:session_owner => "session-owner", }
class << self
attr_accessor :parsed_auth_db
attr_accessor :rights
attr_accessor :rules
attr_accessor :comments # Not implemented yet.
def prefetch(resources)
self.populate_rules_rights
end
def instances
if self.parsed_auth_db == {}
self.prefetch(nil)
end
self.parsed_auth_db.collect do |k,v|
new(:name => k)
end
end
def populate_rules_rights
auth_plist = Plist::parse_xml(AuthDB)
raise Puppet::Error.new("Cannot parse: #{AuthDB}") if not auth_plist
self.rights = auth_plist["rights"].dup
self.rules = auth_plist["rules"].dup
self.parsed_auth_db = self.rights.dup
self.parsed_auth_db.merge!(self.rules.dup)
end
end
# standard required provider instance methods
def initialize(resource)
if self.class.parsed_auth_db == {}
self.class.prefetch(resource)
end
super
end
def create
# we just fill the @property_hash in here and let the flush method
# deal with it rather than repeating code.
new_values = {}
validprops = Puppet::Type.type(resource.class.name).validproperties
validprops.each do |prop|
next if prop == :ensure
if value = resource.should(prop) and value != ""
new_values[prop] = value
end
end
@property_hash = new_values.dup
end
def destroy
# We explicitly delete here rather than in the flush method.
case resource[:auth_type]
when :right
destroy_right
when :rule
destroy_rule
else
raise Puppet::Error.new("Must specify auth_type when destroying.")
end
end
def exists?
!!self.class.parsed_auth_db.has_key?(resource[:name])
end
def flush
# deletion happens in the destroy methods
if resource[:ensure] != :absent
case resource[:auth_type]
when :right
flush_right
when :rule
flush_rule
else
raise Puppet::Error.new("flush requested for unknown type.")
end
@property_hash.clear
end
end
# utility methods below
def destroy_right
security "authorizationdb", :remove, resource[:name]
end
def destroy_rule
authdb = Plist::parse_xml(AuthDB)
authdb_rules = authdb["rules"].dup
if authdb_rules[resource[:name]]
begin
authdb["rules"].delete(resource[:name])
Plist::Emit.save_plist(authdb, AuthDB)
rescue Errno::EACCES => e
- raise Puppet::Error.new("Error saving #{AuthDB}: #{e}")
+ raise Puppet::Error.new("Error saving #{AuthDB}: #{e}", e)
end
end
end
def flush_right
# first we re-read the right just to make sure we're in sync for
# values that weren't specified in the manifest. As we're supplying
# the whole plist when specifying the right it seems safest to be
# paranoid given the low cost of quering the db once more.
cmds = []
cmds << :security << "authorizationdb" << "read" << resource[:name]
output = execute(cmds, :failonfail => false, :combine => false)
current_values = Plist::parse_xml(output)
current_values ||= {}
specified_values = convert_plist_to_native_attributes(@property_hash)
# take the current values, merge the specified values to obtain a
# complete description of the new values.
new_values = current_values.merge(specified_values)
set_right(resource[:name], new_values)
end
def flush_rule
authdb = Plist::parse_xml(AuthDB)
authdb_rules = authdb["rules"].dup
current_values = {}
current_values = authdb_rules[resource[:name]] if authdb_rules[resource[:name]]
specified_values = convert_plist_to_native_attributes(@property_hash)
new_values = current_values.merge(specified_values)
set_rule(resource[:name], new_values)
end
def set_right(name, values)
# Both creates and modifies rights as it simply overwrites them.
# The security binary only allows for writes using stdin, so we
# dump the values to a tempfile.
values = convert_plist_to_native_attributes(values)
tmp = Tempfile.new('puppet_macauthorization')
begin
Plist::Emit.save_plist(values, tmp.path)
cmds = []
cmds << :security << "authorizationdb" << "write" << name
execute(cmds, :failonfail => false, :combine => false, :stdinfile => tmp.path.to_s)
rescue Errno::EACCES => e
- raise Puppet::Error.new("Cannot save right to #{tmp.path}: #{e}")
+ raise Puppet::Error.new("Cannot save right to #{tmp.path}: #{e}", e)
ensure
tmp.close
tmp.unlink
end
end
def set_rule(name, values)
# Both creates and modifies rules as it overwrites the entry in the
# rules dictionary. Unfortunately the security binary doesn't
# support modifying rules at all so we have to twiddle the whole
# plist... :( See Apple Bug #6386000
values = convert_plist_to_native_attributes(values)
authdb = Plist::parse_xml(AuthDB)
authdb["rules"][name] = values
begin
Plist::Emit.save_plist(authdb, AuthDB)
rescue
raise Puppet::Error.new("Error writing to: #{AuthDB}")
end
end
def convert_plist_to_native_attributes(propertylist)
# This mainly converts the keys from the puppet attributes to the
# 'native' ones, but also enforces that the keys are all Strings
# rather than Symbols so that any merges of the resultant Hash are
# sane. The exception is booleans, where we coerce to a proper bool
# if they come in as a symbol.
newplist = {}
propertylist.each_pair do |key, value|
next if key == :ensure # not part of the auth db schema.
next if key == :auth_type # not part of the auth db schema.
case value
when true, :true
value = true
when false, :false
value = false
end
new_key = key
if PuppetToNativeAttributeMap.has_key?(key)
new_key = PuppetToNativeAttributeMap[key].to_s
elsif not key.is_a?(String)
new_key = key.to_s
end
newplist[new_key] = value
end
newplist
end
def retrieve_value(resource_name, attribute)
# We set boolean values to symbols when retrieving values
raise Puppet::Error.new("Cannot find #{resource_name} in auth db") if not self.class.parsed_auth_db.has_key?(resource_name)
if PuppetToNativeAttributeMap.has_key?(attribute)
native_attribute = PuppetToNativeAttributeMap[attribute]
else
native_attribute = attribute.to_s
end
if self.class.parsed_auth_db[resource_name].has_key?(native_attribute)
value = self.class.parsed_auth_db[resource_name][native_attribute]
case value
when true, :true
value = :true
when false, :false
value = :false
end
@property_hash[attribute] = value
return value
else
@property_hash.delete(attribute)
return "" # so ralsh doesn't display it.
end
end
# property methods below
#
# We define them all dynamically apart from auth_type which is a special
# case due to not being in the actual authorization db schema.
properties = [ :allow_root, :authenticate_user, :auth_class, :comment,
:group, :k_of_n, :mechanisms, :rule, :session_owner,
:shared, :timeout, :tries ]
properties.each do |field|
define_method(field.to_s) do
retrieve_value(resource[:name], field)
end
define_method(field.to_s + "=") do |value|
@property_hash[field] = value
end
end
def auth_type
if resource.should(:auth_type) != nil
return resource.should(:auth_type)
elsif self.exists?
# this is here just for ralsh, so it can work out what type it is.
if self.class.rights.has_key?(resource[:name])
return :right
elsif self.class.rules.has_key?(resource[:name])
return :rule
else
raise Puppet::Error.new("#{resource[:name]} is unknown type.")
end
else
raise Puppet::Error.new("auth_type required for new resources.")
end
end
def auth_type=(value)
@property_hash[:auth_type] = value
end
end
diff --git a/lib/puppet/provider/mount.rb b/lib/puppet/provider/mount.rb
index 0839d99ce..0f3bc1023 100644
--- a/lib/puppet/provider/mount.rb
+++ b/lib/puppet/provider/mount.rb
@@ -1,51 +1,58 @@
require 'puppet'
# A module just to store the mount/unmount methods. Individual providers
# still need to add the mount commands manually.
module Puppet::Provider::Mount
# This only works when the mount point is synced to the fstab.
def mount
args = []
# In general we do not have to pass mountoptions because we always
# flush /etc/fstab before attempting to mount. But old code suggests
# that MacOS always needs the mount options to be explicitly passed to
# the mount command
if Facter.value(:kernel) == 'Darwin'
args << "-o" << self.options if self.options and self.options != :absent
end
args << resource[:name]
mountcmd(*args)
case get(:ensure)
when :absent; set(:ensure => :ghost)
when :unmounted; set(:ensure => :mounted)
end
end
def remount
info "Remounting"
if resource[:remounts] == :true
mountcmd "-o", "remount", resource[:name]
+ elsif ["FreeBSD", "DragonFly", "OpenBSD"].include?(Facter.value(:operatingsystem))
+ if self.options && !self.options.empty?
+ options = self.options + ",update"
+ else
+ options = "update"
+ end
+ mountcmd "-o", options, resource[:name]
else
unmount
mount
end
end
# This only works when the mount point is synced to the fstab.
def unmount
umount(resource[:name])
# Update property hash for future queries (e.g. refresh is called)
case get(:ensure)
when :mounted; set(:ensure => :unmounted)
when :ghost; set(:ensure => :absent)
end
end
# Is the mount currently mounted?
def mounted?
[:mounted, :ghost].include?(get(:ensure))
end
end
diff --git a/lib/puppet/provider/mount/parsed.rb b/lib/puppet/provider/mount/parsed.rb
index d935d7d13..dd765db8c 100644
--- a/lib/puppet/provider/mount/parsed.rb
+++ b/lib/puppet/provider/mount/parsed.rb
@@ -1,124 +1,123 @@
require 'puppet/provider/parsedfile'
require 'puppet/provider/mount'
fstab = nil
case Facter.value(:osfamily)
when "Solaris"; fstab = "/etc/vfstab"
else
fstab = "/etc/fstab"
end
Puppet::Type.type(:mount).provide(
:parsed,
:parent => Puppet::Provider::ParsedFile,
:default_target => fstab,
:filetype => :flat
) do
include Puppet::Provider::Mount
commands :mountcmd => "mount", :umount => "umount"
case Facter.value(:osfamily)
when "Solaris"
@fields = [:device, :blockdevice, :name, :fstype, :pass, :atboot, :options]
else
@fields = [:device, :name, :fstype, :options, :dump, :pass]
- @fielddefaults = [ nil ] * 4 + [ "0", "2" ]
end
text_line :comment, :match => /^\s*#/
text_line :blank, :match => /^\s*$/
optional_fields = @fields - [:device, :name, :blockdevice]
mandatory_fields = @fields - optional_fields
# fstab will ignore lines that have fewer than the mandatory number of columns,
# so we should, too.
field_pattern = '(\s*(?>\S+))'
text_line :incomplete, :match => /^(?!#{field_pattern}{#{mandatory_fields.length}})/
record_line self.name, :fields => @fields, :separator => /\s+/, :joiner => "\t", :optional => optional_fields
# Every entry in fstab is :unmounted until we can prove different
def self.prefetch_hook(target_records)
target_records.collect do |record|
record[:ensure] = :unmounted if record[:record_type] == :parsed
record
end
end
def self.instances
providers = super
mounts = mountinstances.dup
# Update fstab entries that are mounted
providers.each do |prov|
if mounts.delete({:name => prov.get(:name), :mounted => :yes}) then
prov.set(:ensure => :mounted)
end
end
# Add mounts that are not in fstab but mounted
mounts.each do |mount|
providers << new(:ensure => :ghost, :name => mount[:name])
end
providers
end
def self.prefetch(resources = nil)
# Get providers for all resources the user defined and that match
# a record in /etc/fstab.
super
# We need to do two things now:
# - Update ensure from :unmounted to :mounted if the resource is mounted
# - Check for mounted devices that are not in fstab and
# set ensure to :ghost (if the user wants to add an entry
# to fstab we need to know if the device was mounted before)
mountinstances.each do |hash|
if mount = resources[hash[:name]]
case mount.provider.get(:ensure)
when :absent # Mount not in fstab
mount.provider.set(:ensure => :ghost)
when :unmounted # Mount in fstab
mount.provider.set(:ensure => :mounted)
end
end
end
end
def self.mountinstances
# XXX: Will not work for mount points that have spaces in path (does fstab support this anyways?)
regex = case Facter.value(:osfamily)
when "Darwin"
/ on (?:\/private\/var\/automount)?(\S*)/
when "Solaris", "HP-UX"
/^(\S*) on /
when "AIX"
/^(?:\S*\s+\S+\s+)(\S+)/
else
/ on (\S*)/
end
instances = []
mount_output = mountcmd.split("\n")
if mount_output.length >= 2 and mount_output[1] =~ /^[- \t]*$/
# On some OSes (e.g. AIX) mount output begins with a header line
# followed by a line consisting of dashes and whitespace.
# Discard these two lines.
mount_output[0..1] = []
end
mount_output.each do |line|
if match = regex.match(line) and name = match.captures.first
instances << {:name => name, :mounted => :yes} # Only :name is important here
else
raise Puppet::Error, "Could not understand line #{line} from mount output"
end
end
instances
end
def flush
needs_mount = @property_hash.delete(:needs_mount)
super
mount if needs_mount
end
end
diff --git a/lib/puppet/provider/naginator.rb b/lib/puppet/provider/naginator.rb
index c84f75c98..731c55539 100644
--- a/lib/puppet/provider/naginator.rb
+++ b/lib/puppet/provider/naginator.rb
@@ -1,63 +1,63 @@
require 'puppet'
require 'puppet/provider/parsedfile'
require 'puppet/external/nagios'
# The base class for all Naginator providers.
class Puppet::Provider::Naginator < Puppet::Provider::ParsedFile
NAME_STRING = "## --PUPPET_NAME-- (called '_naginator_name' in the manifest)"
# Retrieve the associated class from Nagios::Base.
def self.nagios_type
unless @nagios_type
name = resource_type.name.to_s.sub(/^nagios_/, '')
unless @nagios_type = Nagios::Base.type(name.to_sym)
raise Puppet::DevError, "Could not find nagios type '#{name}'"
end
# And add our 'ensure' settings, since they aren't a part of
# Naginator by default
@nagios_type.send(:attr_accessor, :ensure, :target, :on_disk)
end
@nagios_type
end
def self.parse(text)
Nagios::Parser.new.parse(text.gsub(NAME_STRING, "_naginator_name"))
rescue => detail
- raise Puppet::Error, "Could not parse configuration for #{resource_type.name}: #{detail}"
+ raise Puppet::Error, "Could not parse configuration for #{resource_type.name}: #{detail}", detail.backtrace
end
def self.to_file(records)
header + records.collect { |record|
# Remap the TYPE_name or _naginator_name params to the
# name if the record is a template (register == 0)
if record.to_s =~ /register\s+0/
record.to_s.sub("_naginator_name", "name").sub(record.type.to_s + "_name", "name")
else
record.to_s.sub("_naginator_name", NAME_STRING)
end
}.join("\n")
end
def self.skip_record?(record)
false
end
def self.valid_attr?(klass, attr_name)
nagios_type.parameters.include?(attr_name)
end
def initialize(resource = nil)
if resource.is_a?(Nagios::Base)
# We don't use a duplicate here, because some providers (ParsedFile, at least)
# use the hash here for later events.
@property_hash = resource
elsif resource
@resource = resource if resource
# LAK 2007-05-09: Keep the model stuff around for backward compatibility
@model = resource
@property_hash = self.class.nagios_type.new
else
@property_hash = self.class.nagios_type.new
end
end
end
diff --git a/lib/puppet/provider/nameservice.rb b/lib/puppet/provider/nameservice.rb
index 8e76b67af..5bf1f4dce 100644
--- a/lib/puppet/provider/nameservice.rb
+++ b/lib/puppet/provider/nameservice.rb
@@ -1,292 +1,292 @@
require 'puppet'
# This is the parent class of all NSS classes. They're very different in
# their backend, but they're pretty similar on the front-end. This class
# provides a way for them all to be as similar as possible.
class Puppet::Provider::NameService < Puppet::Provider
class << self
def autogen_default(param)
defined?(@autogen_defaults) ? @autogen_defaults[param.intern] : nil
end
def autogen_defaults(hash)
@autogen_defaults ||= {}
hash.each do |param, value|
@autogen_defaults[param.intern] = value
end
end
def initvars
@checks = {}
@options = {}
super
end
def instances
objects = []
listbyname do |name|
objects << new(:name => name, :ensure => :present)
end
objects
end
def option(name, option)
name = name.intern if name.is_a? String
(defined?(@options) and @options.include? name and @options[name].include? option) ? @options[name][option] : nil
end
def options(name, hash)
raise Puppet::DevError, "#{name} is not a valid attribute for #{resource_type.name}" unless resource_type.valid_parameter?(name)
@options ||= {}
@options[name] ||= {}
# Set options individually, so we can call the options method
# multiple times.
hash.each do |param, value|
@options[name][param] = value
end
end
# List everything out by name. Abstracted a bit so that it works
# for both users and groups.
def listbyname
names = []
Etc.send("set#{section()}ent")
begin
while ent = Etc.send("get#{section()}ent")
names << ent.name
yield ent.name if block_given?
end
ensure
Etc.send("end#{section()}ent")
end
names
end
def resource_type=(resource_type)
super
@resource_type.validproperties.each do |prop|
next if prop == :ensure
define_method(prop) { get(prop) || :absent} unless public_method_defined?(prop)
define_method(prop.to_s + "=") { |*vals| set(prop, *vals) } unless public_method_defined?(prop.to_s + "=")
end
end
# This is annoying, but there really aren't that many options,
# and this *is* built into Ruby.
def section
unless resource_type
raise Puppet::DevError,
"Cannot determine Etc section without a resource type"
end
if @resource_type.name == :group
"gr"
else
"pw"
end
end
def validate(name, value)
name = name.intern if name.is_a? String
if @checks.include? name
block = @checks[name][:block]
raise ArgumentError, "Invalid value #{value}: #{@checks[name][:error]}" unless block.call(value)
end
end
def verify(name, error, &block)
name = name.intern if name.is_a? String
@checks[name] = {:error => error, :block => block}
end
private
def op(property)
@ops[property.name] || ("-#{property.name}")
end
end
# Autogenerate a value. Mostly used for uid/gid, but also used heavily
# with DirectoryServices, because DirectoryServices is stupid.
def autogen(field)
field = field.intern
id_generators = {:user => :uid, :group => :gid}
if id_generators[@resource.class.name] == field
return self.class.autogen_id(field, @resource.class.name)
else
if value = self.class.autogen_default(field)
return value
elsif respond_to?("autogen_#{field}")
return send("autogen_#{field}")
else
return nil
end
end
end
# Autogenerate either a uid or a gid. This is not very flexible: we can
# only generate one field type per class, and get kind of confused if asked
# for both.
def self.autogen_id(field, resource_type)
# Figure out what sort of value we want to generate.
case resource_type
when :user; database = :passwd; method = :uid
when :group; database = :group; method = :gid
else
raise Puppet::DevError, "Invalid resource name #{resource}"
end
# Initialize from the data set, if needed.
unless @prevauto
# Sadly, Etc doesn't return an enumerator, it just invokes the block
# given, or returns the first record from the database. There is no
# other, more convenient enumerator for these, so we fake one with this
# loop. Thanks, Ruby, for your awesome abstractions. --daniel 2012-03-23
highest = []
Etc.send(database) {|entry| highest << entry.send(method) }
highest = highest.reject {|x| x > 65000 }.max
@prevauto = highest || 1000
end
# ...and finally increment and return the next value.
@prevauto += 1
end
def create
if exists?
info "already exists"
# The object already exists
return nil
end
begin
execute(self.addcmd, {:failonfail => true, :combine => true, :custom_environment => @custom_environment})
if feature?(:manages_password_age) && (cmd = passcmd)
execute(cmd)
end
rescue Puppet::ExecutionFailure => detail
- raise Puppet::Error, "Could not create #{@resource.class.name} #{@resource.name}: #{detail}"
+ raise Puppet::Error, "Could not create #{@resource.class.name} #{@resource.name}: #{detail}", detail.backtrace
end
end
def delete
unless exists?
info "already absent"
# the object already doesn't exist
return nil
end
begin
execute(self.deletecmd)
rescue Puppet::ExecutionFailure => detail
- raise Puppet::Error, "Could not delete #{@resource.class.name} #{@resource.name}: #{detail}"
+ raise Puppet::Error, "Could not delete #{@resource.class.name} #{@resource.name}: #{detail}", detail.backtrace
end
end
def ensure
if exists?
:present
else
:absent
end
end
# Does our object exist?
def exists?
!!getinfo(true)
end
# Retrieve a specific value by name.
def get(param)
(hash = getinfo(false)) ? unmunge(param, hash[param]) : nil
end
def munge(name, value)
if block = self.class.option(name, :munge) and block.is_a? Proc
block.call(value)
else
value
end
end
def unmunge(name, value)
if block = self.class.option(name, :unmunge) and block.is_a? Proc
block.call(value)
else
value
end
end
# Retrieve what we can about our object
def getinfo(refresh)
if @objectinfo.nil? or refresh == true
@etcmethod ||= ("get" + self.class.section.to_s + "nam").intern
begin
@objectinfo = Etc.send(@etcmethod, @resource[:name])
rescue ArgumentError
@objectinfo = nil
end
end
# Now convert our Etc struct into a hash.
@objectinfo ? info2hash(@objectinfo) : nil
end
# The list of all groups the user is a member of. Different
# user mgmt systems will need to override this method.
def groups
groups = []
# Reset our group list
Etc.setgrent
user = @resource[:name]
# Now iterate across all of the groups, adding each one our
# user is a member of
while group = Etc.getgrent
members = group.mem
groups << group.name if members.include? user
end
# We have to close the file, so each listing is a separate
# reading of the file.
Etc.endgrent
groups.join(",")
end
# Convert the Etc struct into a hash.
def info2hash(info)
hash = {}
self.class.resource_type.validproperties.each do |param|
method = posixmethod(param)
hash[param] = info.send(posixmethod(param)) if info.respond_to? method
end
hash
end
def initialize(resource)
super
@custom_environment = {}
@objectinfo = nil
end
def set(param, value)
self.class.validate(param, value)
cmd = modifycmd(param, munge(param, value))
raise Puppet::DevError, "Nameservice command must be an array" unless cmd.is_a?(Array)
begin
execute(cmd)
rescue Puppet::ExecutionFailure => detail
- raise Puppet::Error, "Could not set #{param} on #{@resource.class.name}[#{@resource.name}]: #{detail}"
+ raise Puppet::Error, "Could not set #{param} on #{@resource.class.name}[#{@resource.name}]: #{detail}", detail.backtrace
end
end
end
diff --git a/lib/puppet/provider/nameservice/directoryservice.rb b/lib/puppet/provider/nameservice/directoryservice.rb
index e8fe9016f..0dd4113a0 100644
--- a/lib/puppet/provider/nameservice/directoryservice.rb
+++ b/lib/puppet/provider/nameservice/directoryservice.rb
@@ -1,589 +1,589 @@
require 'puppet'
require 'puppet/provider/nameservice'
require 'facter/util/plist'
require 'fileutils'
class Puppet::Provider::NameService::DirectoryService < Puppet::Provider::NameService
# JJM: Dive into the singleton_class
class << self
# JJM: This allows us to pass information when calling
# Puppet::Type.type
# e.g. Puppet::Type.type(:user).provide :directoryservice, :ds_path => "Users"
# This is referenced in the get_ds_path class method
attr_writer :ds_path
attr_writer :macosx_version_major
end
initvars
commands :dscl => "/usr/bin/dscl"
commands :dseditgroup => "/usr/sbin/dseditgroup"
commands :sw_vers => "/usr/bin/sw_vers"
commands :plutil => '/usr/bin/plutil'
confine :operatingsystem => :darwin
defaultfor :operatingsystem => :darwin
# JJM 2007-07-25: This map is used to map NameService attributes to their
# corresponding DirectoryService attribute names.
# See: http://images.apple.com/server/docs.Open_Directory_v10.4.pdf
# JJM: Note, this is de-coupled from the Puppet::Type, and must
# be actively maintained. There may also be collisions with different
# types (Users, Groups, Mounts, Hosts, etc...)
def ds_to_ns_attribute_map; self.class.ds_to_ns_attribute_map; end
def self.ds_to_ns_attribute_map
{
'RecordName' => :name,
'PrimaryGroupID' => :gid,
'NFSHomeDirectory' => :home,
'UserShell' => :shell,
'UniqueID' => :uid,
'RealName' => :comment,
'Password' => :password,
'GeneratedUID' => :guid,
'IPAddress' => :ip_address,
'ENetAddress' => :en_address,
'GroupMembership' => :members,
}
end
# JJM The same table as above, inverted.
def ns_to_ds_attribute_map; self.class.ns_to_ds_attribute_map end
def self.ns_to_ds_attribute_map
@ns_to_ds_attribute_map ||= ds_to_ns_attribute_map.invert
end
def self.password_hash_dir
'/var/db/shadow/hash'
end
def self.users_plist_dir
'/var/db/dslocal/nodes/Default/users'
end
def self.instances
# JJM Class method that provides an array of instance objects of this
# type.
# JJM: Properties are dependent on the Puppet::Type we're managine.
type_property_array = [:name] + @resource_type.validproperties
# Create a new instance of this Puppet::Type for each object present
# on the system.
list_all_present.collect do |name_string|
self.new(single_report(name_string, *type_property_array))
end
end
def self.get_ds_path
# JJM: 2007-07-24 This method dynamically returns the DS path we're concerned with.
# For example, if we're working with an user type, this will be /Users
# with a group type, this will be /Groups.
# @ds_path is an attribute of the class itself.
return @ds_path if defined?(@ds_path)
# JJM: "Users" or "Groups" etc ... (Based on the Puppet::Type)
# Remember this is a class method, so self.class is Class
# Also, @resource_type seems to be the reference to the
# Puppet::Type this class object is providing for.
@resource_type.name.to_s.capitalize + "s"
end
def self.get_macosx_version_major
return @macosx_version_major if defined?(@macosx_version_major)
begin
# Make sure we've loaded all of the facts
Facter.loadfacts
product_version_major = Facter.value(:macosx_productversion_major)
fail("#{product_version_major} is not supported by the directoryservice provider") if %w{10.0 10.1 10.2 10.3 10.4}.include?(product_version_major)
@macosx_version_major = product_version_major
return @macosx_version_major
rescue Puppet::ExecutionFailure => detail
fail("Could not determine OS X version: #{detail}")
end
end
def self.list_all_present
# JJM: List all objects of this Puppet::Type already present on the system.
begin
dscl_output = execute(get_exec_preamble("-list"))
rescue Puppet::ExecutionFailure
fail("Could not get #{@resource_type.name} list from DirectoryService")
end
dscl_output.split("\n")
end
def self.parse_dscl_plist_data(dscl_output)
Plist.parse_xml(dscl_output)
end
def self.generate_attribute_hash(input_hash, *type_properties)
attribute_hash = {}
input_hash.keys.each do |key|
ds_attribute = key.sub("dsAttrTypeStandard:", "")
next unless (ds_to_ns_attribute_map.keys.include?(ds_attribute) and type_properties.include? ds_to_ns_attribute_map[ds_attribute])
ds_value = input_hash[key]
case ds_to_ns_attribute_map[ds_attribute]
when :members
ds_value = ds_value # only members uses arrays so far
when :gid, :uid
# OS X stores objects like uid/gid as strings.
# Try casting to an integer for these cases to be
# consistent with the other providers and the group type
# validation
begin
ds_value = Integer(ds_value[0])
rescue ArgumentError
ds_value = ds_value[0]
end
else ds_value = ds_value[0]
end
attribute_hash[ds_to_ns_attribute_map[ds_attribute]] = ds_value
end
# NBK: need to read the existing password here as it's not actually
# stored in the user record. It is stored at a path that involves the
# UUID of the user record for non-Mobile local acccounts.
# Mobile Accounts are out of scope for this provider for now
attribute_hash[:password] = self.get_password(attribute_hash[:guid], attribute_hash[:name]) if @resource_type.validproperties.include?(:password) and Puppet.features.root?
attribute_hash
end
def self.single_report(resource_name, *type_properties)
# JJM 2007-07-24:
# Given a the name of an object and a list of properties of that
# object, return all property values in a hash.
#
# This class method returns nil if the object doesn't exist
# Otherwise, it returns a hash of the object properties.
all_present_str_array = list_all_present
# NBK: shortcut the process if the resource is missing
return nil unless all_present_str_array.include? resource_name
dscl_vector = get_exec_preamble("-read", resource_name)
begin
dscl_output = execute(dscl_vector)
rescue Puppet::ExecutionFailure
fail("Could not get report. command execution failed.")
end
# (#11593) Remove support for OS X 10.4 and earlier
fail_if_wrong_version
dscl_plist = self.parse_dscl_plist_data(dscl_output)
self.generate_attribute_hash(dscl_plist, *type_properties)
end
def self.fail_if_wrong_version
fail("Puppet does not support OS X versions < 10.5") unless self.get_macosx_version_major >= "10.5"
end
def self.get_exec_preamble(ds_action, resource_name = nil)
# JJM 2007-07-24
# DSCL commands are often repetitive and contain the same positional
# arguments over and over. See http://developer.apple.com/documentation/Porting/Conceptual/PortingUnix/additionalfeatures/chapter_10_section_9.html
# for an example of what I mean.
# This method spits out proper DSCL commands for us.
# We EXPECT name to be @resource[:name] when called from an instance object.
# (#11593) Remove support for OS X 10.4 and earlier
fail_if_wrong_version
command_vector = [ command(:dscl), "-plist", "." ]
# JJM: The actual action to perform. See "man dscl"
# Common actiosn: -create, -delete, -merge, -append, -passwd
command_vector << ds_action
# JJM: get_ds_path will spit back "Users" or "Groups",
# etc... Depending on the Puppet::Type of our self.
if resource_name
command_vector << "/#{get_ds_path}/#{resource_name}"
else
command_vector << "/#{get_ds_path}"
end
# JJM: This returns most of the preamble of the command.
# e.g. 'dscl / -create /Users/mccune'
command_vector
end
def self.set_password(resource_name, guid, password_hash)
# Use Puppet::Util::Package.versioncmp() to catch the scenario where a
# version '10.10' would be < '10.7' with simple string comparison. This
# if-statement only executes if the current version is less-than 10.7
if (Puppet::Util::Package.versioncmp(get_macosx_version_major, '10.7') == -1)
password_hash_file = "#{password_hash_dir}/#{guid}"
begin
File.open(password_hash_file, 'w') { |f| f.write(password_hash)}
rescue Errno::EACCES => detail
fail("Could not write to password hash file: #{detail}")
end
# NBK: For shadow hashes, the user AuthenticationAuthority must contain a value of
# ";ShadowHash;". The LKDC in 10.5 makes this more interesting though as it
# will dynamically generate ;Kerberosv5;;username@LKDC:SHA1 attributes if
# missing. Thus we make sure we only set ;ShadowHash; if it is missing, and
# we can do this with the merge command. This allows people to continue to
# use other custom AuthenticationAuthority attributes without stomping on them.
#
# There is a potential problem here in that we're only doing this when setting
# the password, and the attribute could get modified at other times while the
# hash doesn't change and so this doesn't get called at all... but
# without switching all the other attributes to merge instead of create I can't
# see a simple enough solution for this that doesn't modify the user record
# every single time. This should be a rather rare edge case. (famous last words)
dscl_vector = self.get_exec_preamble("-merge", resource_name)
dscl_vector << "AuthenticationAuthority" << ";ShadowHash;"
begin
execute(dscl_vector)
rescue Puppet::ExecutionFailure => detail
fail("Could not set AuthenticationAuthority.")
end
else
# 10.7 uses salted SHA512 password hashes which are 128 characters plus
# an 8 character salt. Previous versions used a SHA1 hash padded with
# zeroes. If someone attempts to use a password hash that worked with
# a previous version of OX X, we will fail early and warn them.
if password_hash.length != 136
fail("OS X 10.7 requires a Salted SHA512 hash password of 136 characters. \
Please check your password and try again.")
end
- if Puppet::FileSystem::File.exist?("#{users_plist_dir}/#{resource_name}.plist")
+ if Puppet::FileSystem.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'
# key, and then save the entire structure back.
users_plist = Plist::parse_xml(plutil( '-convert', 'xml1', '-o', '/dev/stdout', \
"#{users_plist_dir}/#{resource_name}.plist"))
# users_plist['ShadowHashData'][0].string is actually a binary plist
# that's nested INSIDE the user's plist (which itself is a binary
# plist). If we encounter a user plist that DOESN'T have a
# ShadowHashData field, create one.
if users_plist['ShadowHashData']
password_hash_plist = users_plist['ShadowHashData'][0].string
converted_hash_plist = convert_binary_to_xml(password_hash_plist)
else
users_plist['ShadowHashData'] = [StringIO.new]
converted_hash_plist = {'SALTED-SHA512' => StringIO.new}
end
# converted_hash_plist['SALTED-SHA512'].string expects a Base64 encoded
# string. The password_hash provided as a resource attribute is a
# hex value. We need to convert the provided hex value to a Base64
# encoded string to nest it in the converted hash plist.
converted_hash_plist['SALTED-SHA512'].string = \
password_hash.unpack('a2'*(password_hash.size/2)).collect { |i| i.hex.chr }.join
# Finally, we can convert the nested plist back to binary, embed it
# into the user's plist, and convert the resultant plist back to
# a binary plist.
changed_plist = convert_xml_to_binary(converted_hash_plist)
users_plist['ShadowHashData'][0].string = changed_plist
Plist::Emit.save_plist(users_plist, "#{users_plist_dir}/#{resource_name}.plist")
plutil('-convert', 'binary1', "#{users_plist_dir}/#{resource_name}.plist")
end
end
end
def self.get_password(guid, username)
# Use Puppet::Util::Package.versioncmp() to catch the scenario where a
# version '10.10' would be < '10.7' with simple string comparison. This
# if-statement only executes if the current version is less-than 10.7
if (Puppet::Util::Package.versioncmp(get_macosx_version_major, '10.7') == -1)
password_hash = nil
password_hash_file = "#{password_hash_dir}/#{guid}"
- if Puppet::FileSystem::File.exist?(password_hash_file) and File.file?(password_hash_file)
+ if Puppet::FileSystem.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
f.close
end
password_hash
else
- if Puppet::FileSystem::File.exist?("#{users_plist_dir}/#{username}.plist")
+ if Puppet::FileSystem.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.
users_plist = Plist::parse_xml(plutil('-convert', 'xml1', '-o', '/dev/stdout', "#{users_plist_dir}/#{username}.plist"))
if users_plist['ShadowHashData']
# users_plist['ShadowHashData'][0].string is actually a binary plist
# that's nested INSIDE the user's plist (which itself is a binary
# plist).
password_hash_plist = users_plist['ShadowHashData'][0].string
converted_hash_plist = convert_binary_to_xml(password_hash_plist)
# converted_hash_plist['SALTED-SHA512'].string is a Base64 encoded
# string. The password_hash provided as a resource attribute is a
# hex value. We need to convert the Base64 encoded string to a
# hex value and provide it back to Puppet.
password_hash = converted_hash_plist['SALTED-SHA512'].string.unpack("H*")[0]
password_hash
end
end
end
end
# This method will accept a hash that has been returned from Plist::parse_xml
# and convert it to a binary plist (string value).
def self.convert_xml_to_binary(plist_data)
Puppet.debug('Converting XML plist to binary')
Puppet.debug('Executing: \'plutil -convert binary1 -o - -\'')
IO.popen('plutil -convert binary1 -o - -', 'r+') do |io|
io.write plist_data.to_plist
io.close_write
@converted_plist = io.read
end
@converted_plist
end
# This method will accept a binary plist (as a string) and convert it to a
# hash via Plist::parse_xml.
def self.convert_binary_to_xml(plist_data)
Puppet.debug('Converting binary plist to XML')
Puppet.debug('Executing: \'plutil -convert xml1 -o - -\'')
IO.popen('plutil -convert xml1 -o - -', 'r+') do |io|
io.write plist_data
io.close_write
@converted_plist = io.read
end
Puppet.debug('Converting XML values to a hash.')
@plist_hash = Plist::parse_xml(@converted_plist)
@plist_hash
end
# Unlike most other *nixes, OS X doesn't provide built in functionality
# for automatically assigning uids and gids to accounts, so we set up these
# methods for consumption by functionality like --mkusers
# By default we restrict to a reasonably sane range for system accounts
def self.next_system_id(id_type, min_id=20)
dscl_args = ['.', '-list']
if id_type == 'uid'
dscl_args << '/Users' << 'uid'
elsif id_type == 'gid'
dscl_args << '/Groups' << 'gid'
else
fail("Invalid id_type #{id_type}. Only 'uid' and 'gid' supported")
end
dscl_out = dscl(dscl_args)
# We're ok with throwing away negative uids here.
ids = dscl_out.split.compact.collect { |l| l.to_i if l.match(/^\d+$/) }
ids.compact!.sort! { |a,b| a.to_f <=> b.to_f }
# We're just looking for an unused id in our sorted array.
ids.each_index do |i|
next_id = ids[i] + 1
return next_id if ids[i+1] != next_id and next_id >= min_id
end
end
def ensure=(ensure_value)
super
# We need to loop over all valid properties for the type we're
# managing and call the method which sets that property value
# dscl can't create everything at once unfortunately.
if ensure_value == :present
@resource.class.validproperties.each do |name|
next if name == :ensure
# LAK: We use property.sync here rather than directly calling
# the settor method because the properties might do some kind
# of conversion. In particular, the user gid property might
# have a string and need to convert it to a number
if @resource.should(name)
@resource.property(name).sync
elsif value = autogen(name)
self.send(name.to_s + "=", value)
else
next
end
end
end
end
def password=(passphrase)
exec_arg_vector = self.class.get_exec_preamble("-read", @resource.name)
exec_arg_vector << ns_to_ds_attribute_map[:guid]
begin
guid_output = execute(exec_arg_vector)
guid_plist = Plist.parse_xml(guid_output)
# Although GeneratedUID like all DirectoryService values can be multi-valued
# according to the schema, in practice user accounts cannot have multiple UUIDs
# otherwise Bad Things Happen, so we just deal with the first value.
guid = guid_plist["dsAttrTypeStandard:#{ns_to_ds_attribute_map[:guid]}"][0]
self.class.set_password(@resource.name, guid, passphrase)
rescue Puppet::ExecutionFailure => detail
fail("Could not set #{param} on #{@resource.class.name}[#{@resource.name}]: #{detail}")
end
end
# NBK: we override @parent.set as we need to execute a series of commands
# to deal with array values, rather than the single command nameservice.rb
# expects to be returned by modifycmd. Thus we don't bother defining modifycmd.
def set(param, value)
self.class.validate(param, value)
current_members = @property_value_cache_hash[:members]
if param == :members
# If we are meant to be authoritative for the group membership
# then remove all existing members who haven't been specified
# in the manifest.
remove_unwanted_members(current_members, value) if @resource[:auth_membership] and not current_members.nil?
# if they're not a member, make them one.
add_members(current_members, value)
else
exec_arg_vector = self.class.get_exec_preamble("-create", @resource[:name])
# JJM: The following line just maps the NS name to the DS name
# e.g. { :uid => 'UniqueID' }
exec_arg_vector << ns_to_ds_attribute_map[param.intern]
# JJM: The following line sends the actual value to set the property to
exec_arg_vector << value.to_s
begin
execute(exec_arg_vector)
rescue Puppet::ExecutionFailure => detail
fail("Could not set #{param} on #{@resource.class.name}[#{@resource.name}]: #{detail}")
end
end
end
# NBK: we override @parent.create as we need to execute a series of commands
# to create objects with dscl, rather than the single command nameservice.rb
# expects to be returned by addcmd. Thus we don't bother defining addcmd.
def create
if exists?
info "already exists"
return nil
end
# NBK: First we create the object with a known guid so we can set the contents
# of the password hash if required
# Shelling out sucks, but for a single use case it doesn't seem worth
# requiring people install a UUID library that doesn't come with the system.
# This should be revisited if Puppet starts managing UUIDs for other platform
# user records.
guid = %x{/usr/bin/uuidgen}.chomp
exec_arg_vector = self.class.get_exec_preamble("-create", @resource[:name])
exec_arg_vector << ns_to_ds_attribute_map[:guid] << guid
begin
execute(exec_arg_vector)
rescue Puppet::ExecutionFailure => detail
fail("Could not set GeneratedUID for #{@resource.class.name} #{@resource.name}: #{detail}")
end
if value = @resource.should(:password) and value != ""
self.class.set_password(@resource[:name], guid, value)
end
# Now we create all the standard properties
Puppet::Type.type(@resource.class.name).validproperties.each do |property|
next if property == :ensure
value = @resource.should(property)
if property == :gid and value.nil?
value = self.class.next_system_id(id_type='gid')
end
if property == :uid and value.nil?
value = self.class.next_system_id(id_type='uid')
end
if value != "" and not value.nil?
if property == :members
add_members(nil, value)
else
exec_arg_vector = self.class.get_exec_preamble("-create", @resource[:name])
exec_arg_vector << ns_to_ds_attribute_map[property.intern]
next if property == :password # skip setting the password here
exec_arg_vector << value.to_s
begin
execute(exec_arg_vector)
rescue Puppet::ExecutionFailure => detail
fail("Could not create #{@resource.class.name} #{@resource.name}: #{detail}")
end
end
end
end
end
def remove_unwanted_members(current_members, new_members)
current_members.each do |member|
if not new_members.flatten.include?(member)
cmd = [:dseditgroup, "-o", "edit", "-n", ".", "-d", member, @resource[:name]]
begin
execute(cmd)
rescue Puppet::ExecutionFailure => detail
# TODO: We're falling back to removing the member using dscl due to rdar://8481241
# This bug causes dseditgroup to fail to remove a member if that member doesn't exist
cmd = [:dscl, ".", "-delete", "/Groups/#{@resource.name}", "GroupMembership", member]
begin
execute(cmd)
rescue Puppet::ExecutionFailure => detail
fail("Could not remove #{member} from group: #{@resource.name}, #{detail}")
end
end
end
end
end
def add_members(current_members, new_members)
new_members.flatten.each do |new_member|
if current_members.nil? or not current_members.include?(new_member)
cmd = [:dseditgroup, "-o", "edit", "-n", ".", "-a", new_member, @resource[:name]]
begin
execute(cmd)
rescue Puppet::ExecutionFailure => detail
fail("Could not add #{new_member} to group: #{@resource.name}, #{detail}")
end
end
end
end
def deletecmd
# JJM: Like addcmd, only called when deleting the object itself
# Note, this isn't used to delete properties of the object,
# at least that's how I understand it...
self.class.get_exec_preamble("-delete", @resource[:name])
end
def getinfo(refresh = false)
# JJM 2007-07-24:
# Override the getinfo method, which is also defined in nameservice.rb
# This method returns and sets @infohash
# I'm not re-factoring the name "getinfo" because this method will be
# most likely called by nameservice.rb, which I didn't write.
if refresh or (! defined?(@property_value_cache_hash) or ! @property_value_cache_hash)
# JJM 2007-07-24: OK, there's a bit of magic that's about to
# happen... Let's see how strong my grip has become... =)
#
# self is a provider instance of some Puppet::Type, like
# Puppet::Type::User::ProviderDirectoryservice for the case of the
# user type and this provider.
#
# self.class looks like "user provider directoryservice", if that
# helps you ...
#
# self.class.resource_type is a reference to the Puppet::Type class,
# probably Puppet::Type::User or Puppet::Type::Group, etc...
#
# self.class.resource_type.validproperties is a class method,
# returning an Array of the valid properties of that specific
# Puppet::Type.
#
# So... something like [:comment, :home, :password, :shell, :uid,
# :groups, :ensure, :gid]
#
# Ultimately, we add :name to the list, delete :ensure from the
# list, then report on the remaining list. Pretty whacky, ehh?
type_properties = [:name] + self.class.resource_type.validproperties
type_properties.delete(:ensure) if type_properties.include? :ensure
type_properties << :guid # append GeneratedUID so we just get the report here
@property_value_cache_hash = self.class.single_report(@resource[:name], *type_properties)
[:uid, :gid].each do |param|
@property_value_cache_hash[param] = @property_value_cache_hash[param].to_i if @property_value_cache_hash and @property_value_cache_hash.include?(param)
end
end
@property_value_cache_hash
end
end
diff --git a/lib/puppet/provider/package/aix.rb b/lib/puppet/provider/package/aix.rb
index 026a982f0..ae54962bc 100644
--- a/lib/puppet/provider/package/aix.rb
+++ b/lib/puppet/provider/package/aix.rb
@@ -1,152 +1,152 @@
require 'puppet/provider/package'
require 'puppet/util/package'
Puppet::Type.type(:package).provide :aix, :parent => Puppet::Provider::Package do
desc "Installation from an AIX software directory, using the AIX `installp`
command. The `source` parameter is required for this provider, and should
be set to the absolute path (on the puppet agent machine) of a directory
containing one or more BFF package files.
The `installp` command will generate a table of contents file (named `.toc`)
in this directory, and the `name` parameter (or resource title) that you
specify for your `package` resource must match a package name that exists
in the `.toc` file.
Note that package downgrades are *not* supported; if your resource specifies
a specific version number and there is already a newer version of the package
installed on the machine, the resource will fail with an error message."
# The commands we are using on an AIX box are installed standard
# (except nimclient) nimclient needs the bos.sysmgt.nim.client fileset.
commands :lslpp => "/usr/bin/lslpp",
:installp => "/usr/sbin/installp"
# AIX supports versionable packages with and without a NIM server
has_feature :versionable
confine :operatingsystem => [ :aix ]
defaultfor :operatingsystem => :aix
attr_accessor :latest_info
def self.srclistcmd(source)
[ command(:installp), "-L", "-d", source ]
end
def self.prefetch(packages)
raise Puppet::Error, "The aix provider can only be used by root" if Process.euid != 0
return unless packages.detect { |name, package| package.should(:ensure) == :latest }
sources = packages.collect { |name, package| package[:source] }.uniq
updates = {}
sources.each do |source|
execute(self.srclistcmd(source)).each_line do |line|
if line =~ /^[^#][^:]*:([^:]*):([^:]*)/
current = {}
current[:name] = $1
current[:version] = $2
current[:source] = source
if updates.key?(current[:name])
previous = updates[current[:name]]
updates[ current[:name] ] = current unless Puppet::Util::Package.versioncmp(previous[:version], current[:version]) == 1
else
updates[current[:name]] = current
end
end
end
end
packages.each do |name, package|
if info = updates[package[:name]]
package.provider.latest_info = info[0]
end
end
end
def uninstall
# Automatically process dependencies when installing/uninstalling
# with the -g option to installp.
installp "-gu", @resource[:name]
# installp will return an exit code of zero even if it didn't uninstall
# anything... so let's make sure it worked.
unless query().nil?
self.fail "Failed to uninstall package '#{@resource[:name]}'"
end
end
def install(useversion = true)
unless source = @resource[:source]
self.fail "A directory is required which will be used to find packages"
end
pkg = @resource[:name]
pkg += " #{@resource.should(:ensure)}" if (! @resource.should(:ensure).is_a? Symbol) and useversion
output = installp "-acgwXY", "-d", source, pkg
# If the package is superseded, it means we're trying to downgrade and we
# can't do that.
if output =~ /^#{Regexp.escape(@resource[:name])}\s+.*\s+Already superseded by.*$/
self.fail "aix package provider is unable to downgrade packages"
end
end
def self.pkglist(hash = {})
cmd = [command(:lslpp), "-qLc"]
if name = hash[:pkgname]
cmd << name
end
begin
list = execute(cmd).scan(/^[^#][^:]*:([^:]*):([^:]*)/).collect { |n,e|
{ :name => n, :ensure => e, :provider => self.name }
}
rescue Puppet::ExecutionFailure => detail
if hash[:pkgname]
return nil
else
- raise Puppet::Error, "Could not list installed Packages: #{detail}"
+ raise Puppet::Error, "Could not list installed Packages: #{detail}", detail.backtrace
end
end
if hash[:pkgname]
return list.shift
else
return list
end
end
def self.instances
pkglist.collect do |hash|
new(hash)
end
end
def latest
upd = latest_info
unless upd.nil?
return "#{upd[:version]}"
else
raise Puppet::DevError, "Tried to get latest on a missing package" if properties[:ensure] == :absent
return properties[:ensure]
end
end
def query
self.class.pkglist(:pkgname => @resource[:name])
end
def update
self.install(false)
end
end
diff --git a/lib/puppet/provider/package/appdmg.rb b/lib/puppet/provider/package/appdmg.rb
index aaf5a7be2..46db070e8 100644
--- a/lib/puppet/provider/package/appdmg.rb
+++ b/lib/puppet/provider/package/appdmg.rb
@@ -1,109 +1,106 @@
# Jeff McCune <mccune.jeff@gmail.com>
# Changed to app.dmg by: Udo Waechter <root@zoide.net>
# Mac OS X Package Installer which handles application (.app)
# bundles inside an Apple Disk Image.
#
# Motivation: DMG files provide a true HFS file system
# and are easier to manage.
#
# Note: the 'apple' Provider checks for the package name
# in /L/Receipts. Since we possibly install multiple apps's from
# a single source, we treat the source .app.dmg file as the package name.
# As a result, we store installed .app.dmg file names
# in /var/db/.puppet_appdmg_installed_<name>
require 'puppet/provider/package'
Puppet::Type.type(:package).provide(:appdmg, :parent => Puppet::Provider::Package) do
desc "Package management which copies application bundles to a target."
confine :operatingsystem => :darwin
commands :hdiutil => "/usr/bin/hdiutil"
commands :curl => "/usr/bin/curl"
commands :ditto => "/usr/bin/ditto"
# JJM We store a cookie for each installed .app.dmg in /var/db
def self.instances_by_name
Dir.entries("/var/db").find_all { |f|
f =~ /^\.puppet_appdmg_installed_/
}.collect do |f|
name = f.sub(/^\.puppet_appdmg_installed_/, '')
yield name if block_given?
name
end
end
def self.instances
instances_by_name.collect do |name|
new(:name => name, :provider => :appdmg, :ensure => :installed)
end
end
def self.installapp(source, name, orig_source)
appname = File.basename(source);
ditto "--rsrc", source, "/Applications/#{appname}"
File.open("/var/db/.puppet_appdmg_installed_#{name}", "w") do |t|
t.print "name: '#{name}'\n"
t.print "source: '#{orig_source}'\n"
end
end
def self.installpkgdmg(source, name)
- unless source =~ /\.dmg$/i
- self.fail "Mac OS X PKG DMG's must specify a source string ending in .dmg"
- end
require 'open-uri'
require 'facter/util/plist'
cached_source = source
tmpdir = Dir.mktmpdir
begin
if %r{\A[A-Za-z][A-Za-z0-9+\-\.]*://} =~ cached_source
cached_source = File.join(tmpdir, name)
begin
curl "-o", cached_source, "-C", "-", "-k", "-L", "-s", "--url", source
Puppet.debug "Success: curl transfered [#{name}]"
rescue Puppet::ExecutionFailure
Puppet.debug "curl did not transfer [#{name}]. Falling back to slower open-uri transfer methods."
cached_source = source
end
end
open(cached_source) do |dmg|
xml_str = hdiutil "mount", "-plist", "-nobrowse", "-readonly", "-mountrandom", "/tmp", dmg.path
ptable = Plist::parse_xml xml_str
# JJM Filter out all mount-paths into a single array, discard the rest.
mounts = ptable['system-entities'].collect { |entity|
entity['mount-point']
}.select { |mountloc|; mountloc }
begin
mounts.each do |fspath|
Dir.entries(fspath).select { |f|
f =~ /\.app$/i
}.each do |pkg|
installapp("#{fspath}/#{pkg}", name, source)
end
end
ensure
hdiutil "eject", mounts[0]
end
end
ensure
FileUtils.remove_entry_secure(tmpdir, true)
end
end
def query
- Puppet::FileSystem::File.exist?("/var/db/.puppet_appdmg_installed_#{@resource[:name]}") ? {:name => @resource[:name], :ensure => :present} : nil
+ Puppet::FileSystem.exist?("/var/db/.puppet_appdmg_installed_#{@resource[:name]}") ? {:name => @resource[:name], :ensure => :present} : nil
end
def install
source = nil
unless source = @resource[:source]
self.fail "Mac OS X PKG DMG's must specify a package source."
end
unless name = @resource[:name]
self.fail "Mac OS X PKG DMG's must specify a package name."
end
self.class.installpkgdmg(source,name)
end
end
diff --git a/lib/puppet/provider/package/apple.rb b/lib/puppet/provider/package/apple.rb
index a603d0783..1d44c3e61 100644
--- a/lib/puppet/provider/package/apple.rb
+++ b/lib/puppet/provider/package/apple.rb
@@ -1,47 +1,47 @@
require 'puppet/provider/package'
# OS X Packaging sucks. We can install packages, but that's about it.
Puppet::Type.type(:package).provide :apple, :parent => Puppet::Provider::Package do
desc "Package management based on OS X's builtin packaging system. This is
essentially the simplest and least functional package system in existence --
it only supports installation; no deletion or upgrades. The provider will
automatically add the `.pkg` extension, so leave that off when specifying
the package name."
confine :operatingsystem => :darwin
commands :installer => "/usr/sbin/installer"
def self.instances
instance_by_name.collect do |name|
self.new(
:name => name,
:provider => :apple,
:ensure => :installed
)
end
end
def self.instance_by_name
Dir.entries("/Library/Receipts").find_all { |f|
f =~ /\.pkg$/
}.collect { |f|
name = f.sub(/\.pkg/, '')
yield name if block_given?
name
}
end
def query
- Puppet::FileSystem::File.exist?("/Library/Receipts/#{@resource[:name]}.pkg") ? {:name => @resource[:name], :ensure => :present} : nil
+ Puppet::FileSystem.exist?("/Library/Receipts/#{@resource[:name]}.pkg") ? {:name => @resource[:name], :ensure => :present} : nil
end
def install
source = nil
unless source = @resource[:source]
self.fail "Mac OS X packages must specify a package source"
end
installer "-pkg", source, "-target", "/"
end
end
diff --git a/lib/puppet/provider/package/apt.rb b/lib/puppet/provider/package/apt.rb
index df5cd725a..d959d5007 100644
--- a/lib/puppet/provider/package/apt.rb
+++ b/lib/puppet/provider/package/apt.rb
@@ -1,107 +1,107 @@
Puppet::Type.type(:package).provide :apt, :parent => :dpkg, :source => :dpkg do
# Provide sorting functionality
include Puppet::Util::Package
desc "Package management via `apt-get`."
has_feature :versionable
commands :aptget => "/usr/bin/apt-get"
commands :aptcache => "/usr/bin/apt-cache"
commands :preseed => "/usr/bin/debconf-set-selections"
defaultfor :operatingsystem => [:debian, :ubuntu]
ENV['DEBIAN_FRONTEND'] = "noninteractive"
# disable common apt helpers to allow non-interactive package installs
ENV['APT_LISTBUGS_FRONTEND'] = "none"
ENV['APT_LISTCHANGES_FRONTEND'] = "none"
# A derivative of DPKG; this is how most people actually manage
# Debian boxes, and the only thing that differs is that it can
# install packages from remote sites.
def checkforcdrom
have_cdrom = begin
!!(File.read("/etc/apt/sources.list") =~ /^[^#]*cdrom:/)
rescue
# This is basically pathological...
false
end
if have_cdrom and @resource[:allowcdrom] != :true
raise Puppet::Error,
"/etc/apt/sources.list contains a cdrom source; not installing. Use 'allowcdrom' to override this failure."
end
end
# Install a package using 'apt-get'. This function needs to support
# installing a specific version.
def install
self.run_preseed if @resource[:responsefile]
should = @resource[:ensure]
checkforcdrom
cmd = %w{-q -y}
if config = @resource[:configfiles]
if config == :keep
cmd << "-o" << 'DPkg::Options::=--force-confold'
else
cmd << "-o" << 'DPkg::Options::=--force-confnew'
end
end
str = @resource[:name]
case should
when true, false, Symbol
# pass
else
# Add the package version and --force-yes option
str += "=#{should}"
cmd << "--force-yes"
end
cmd << :install << str
aptget(*cmd)
end
# What's the latest package version available?
def latest
output = aptcache :policy, @resource[:name]
if output =~ /Candidate:\s+(\S+)\s/
return $1
else
self.err "Could not find latest version"
return nil
end
end
#
# preseeds answers to dpkg-set-selection from the "responsefile"
#
def run_preseed
- if response = @resource[:responsefile] and Puppet::FileSystem::File.exist?(response)
+ if response = @resource[:responsefile] and Puppet::FileSystem.exist?(response)
self.info("Preseeding #{response} to debconf-set-selections")
preseed response
else
self.info "No responsefile specified or non existant, not preseeding anything"
end
end
def uninstall
self.run_preseed if @resource[:responsefile]
aptget "-y", "-q", :remove, @resource[:name]
end
def purge
self.run_preseed if @resource[:responsefile]
aptget '-y', '-q', :remove, '--purge', @resource[:name]
# workaround a "bug" in apt, that already removed packages are not purged
super
end
end
diff --git a/lib/puppet/provider/package/blastwave.rb b/lib/puppet/provider/package/blastwave.rb
index fc0698e1a..27ce4d3b4 100644
--- a/lib/puppet/provider/package/blastwave.rb
+++ b/lib/puppet/provider/package/blastwave.rb
@@ -1,111 +1,111 @@
# Packaging using Blastwave's pkg-get program.
Puppet::Type.type(:package).provide :blastwave, :parent => :sun, :source => :sun do
desc "Package management using Blastwave.org's `pkg-get` command on Solaris."
pkgget = "pkg-get"
pkgget = "/opt/csw/bin/pkg-get" if FileTest.executable?("/opt/csw/bin/pkg-get")
confine :osfamily => :solaris
commands :pkgget => pkgget
def pkgget_with_cat(*args)
Puppet::Util.withenv(:PAGER => "/usr/bin/cat") { pkgget(*args) }
end
def self.extended(mod)
unless command(:pkgget) != "pkg-get"
raise Puppet::Error,
"The pkg-get command is missing; blastwave packaging unavailable"
end
- unless Puppet::FileSystem::File.exist?("/var/pkg-get/admin")
+ unless Puppet::FileSystem.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
end
def self.instances(hash = {})
blastlist(hash).collect do |bhash|
bhash.delete(:avail)
new(bhash)
end
end
# Turn our blastwave listing into a bunch of hashes.
def self.blastlist(hash)
command = ["-c"]
command << hash[:justme] if hash[:justme]
output = Puppet::Util.withenv(:PAGER => "/usr/bin/cat") { pkgget command }
list = output.split("\n").collect do |line|
next if line =~ /^#/
next if line =~ /^WARNING/
next if line =~ /localrev\s+remoterev/
blastsplit(line)
end.reject { |h| h.nil? }
if hash[:justme]
return list[0]
else
list.reject! { |h|
h[:ensure] == :absent
}
return list
end
end
# Split the different lines into hashes.
def self.blastsplit(line)
if line =~ /\s*(\S+)\s+((\[Not installed\])|(\S+))\s+(\S+)/
hash = {}
hash[:name] = $1
hash[:ensure] = if $2 == "[Not installed]"
:absent
else
$2
end
hash[:avail] = $5
hash[:avail] = hash[:ensure] if hash[:avail] == "SAME"
# Use the name method, so it works with subclasses.
hash[:provider] = self.name
return hash
else
Puppet.warning "Cannot match #{line}"
return nil
end
end
def install
pkgget_with_cat "-f", :install, @resource[:name]
end
# Retrieve the version from the current package file.
def latest
hash = self.class.blastlist(:justme => @resource[:name])
hash[:avail]
end
def query
if hash = self.class.blastlist(:justme => @resource[:name])
hash
else
{:ensure => :absent}
end
end
# Remove the old package, and install the new one
def update
pkgget_with_cat "-f", :upgrade, @resource[:name]
end
def uninstall
pkgget_with_cat "-f", :remove, @resource[:name]
end
end
diff --git a/lib/puppet/provider/package/fink.rb b/lib/puppet/provider/package/fink.rb
index cbf141462..6b495171b 100644
--- a/lib/puppet/provider/package/fink.rb
+++ b/lib/puppet/provider/package/fink.rb
@@ -1,79 +1,79 @@
Puppet::Type.type(:package).provide :fink, :parent => :dpkg, :source => :dpkg do
# Provide sorting functionality
include Puppet::Util::Package
desc "Package management via `fink`."
commands :fink => "/sw/bin/fink"
commands :aptget => "/sw/bin/apt-get"
commands :aptcache => "/sw/bin/apt-cache"
commands :dpkgquery => "/sw/bin/dpkg-query"
has_feature :versionable
# A derivative of DPKG; this is how most people actually manage
# Debian boxes, and the only thing that differs is that it can
# install packages from remote sites.
def finkcmd(*args)
fink(*args)
end
# Install a package using 'apt-get'. This function needs to support
# installing a specific version.
def install
self.run_preseed if @resource[:responsefile]
should = @resource.should(:ensure)
str = @resource[:name]
case should
when true, false, Symbol
# pass
else
# Add the package version
str += "=#{should}"
end
cmd = %w{-b -q -y}
cmd << :install << str
finkcmd(cmd)
end
# What's the latest package version available?
def latest
output = aptcache :policy, @resource[:name]
if output =~ /Candidate:\s+(\S+)\s/
return $1
else
self.err "Could not find latest version"
return nil
end
end
#
# preseeds answers to dpkg-set-selection from the "responsefile"
#
def run_preseed
- if response = @resource[:responsefile] and Puppet::FileSystem::File.exist?(response)
+ if response = @resource[:responsefile] and Puppet::FileSystem.exist?(response)
self.info("Preseeding #{response} to debconf-set-selections")
preseed response
else
self.info "No responsefile specified or non existant, not preseeding anything"
end
end
def update
self.install
end
def uninstall
finkcmd "-y", "-q", :remove, @model[:name]
end
def purge
aptget '-y', '-q', 'remove', '--purge', @resource[:name]
end
end
diff --git a/lib/puppet/provider/package/gem.rb b/lib/puppet/provider/package/gem.rb
index a13bbf35f..518fac289 100644
--- a/lib/puppet/provider/package/gem.rb
+++ b/lib/puppet/provider/package/gem.rb
@@ -1,125 +1,125 @@
require 'puppet/provider/package'
require 'uri'
# Ruby gems support.
Puppet::Type.type(:package).provide :gem, :parent => Puppet::Provider::Package do
desc "Ruby Gem support. If a URL is passed via `source`, then that URL is used as the
remote gem repository; if a source is present but is not a valid URL, it will be
interpreted as the path to a local gem file. If source is not present at all,
the gem will be installed from the default gem repositories."
has_feature :versionable
commands :gemcmd => "gem"
def self.gemlist(options)
gem_list_command = [command(:gemcmd), "list"]
if options[:local]
gem_list_command << "--local"
else
gem_list_command << "--remote"
end
if options[:source]
gem_list_command << "--source" << options[:source]
end
if name = options[:justme]
gem_list_command << name + "$"
end
begin
list = execute(gem_list_command).lines.
map {|set| gemsplit(set) }.
reject {|x| x.nil? }
rescue Puppet::ExecutionFailure => detail
- raise Puppet::Error, "Could not list gems: #{detail}"
+ raise Puppet::Error, "Could not list gems: #{detail}", detail.backtrace
end
if options[:justme]
return list.shift
else
return list
end
end
def self.gemsplit(desc)
# `gem list` when output console has a line like:
# *** LOCAL GEMS ***
# but when it's not to the console that line
# and all blank lines are stripped
# so we don't need to check for them
if desc =~ /^(\S+)\s+\((.+)\)/
name = $1
versions = $2.split(/,\s*/)
{
:name => name,
- :ensure => versions,
+ :ensure => versions.map{|v| v.split[0]},
:provider => :gem
}
else
Puppet.warning "Could not match #{desc}" unless desc.chomp.empty?
nil
end
end
def self.instances(justme = false)
gemlist(:local => true).collect do |hash|
new(hash)
end
end
def install(useversion = true)
command = [command(:gemcmd), "install"]
command << "-v" << resource[:ensure] if (! resource[:ensure].is_a? Symbol) and useversion
if source = resource[:source]
begin
uri = URI.parse(source)
rescue => detail
- fail "Invalid source '#{uri}': #{detail}"
+ self.fail Puppet::Error, "Invalid source '#{uri}': #{detail}", detail
end
case uri.scheme
when nil
# no URI scheme => interpret the source as a local file
command << source
when /file/i
command << uri.path
when 'puppet'
# we don't support puppet:// URLs (yet)
raise Puppet::Error.new("puppet:// URLs are not supported as gem sources")
else
# interpret it as a gem repository
command << "--source" << "#{source}" << resource[:name]
end
else
command << "--no-rdoc" << "--no-ri" << resource[:name]
end
output = execute(command)
# Apparently some stupid gem versions don't exit non-0 on failure
self.fail "Could not install: #{output.chomp}" if output.include?("ERROR")
end
def latest
# This always gets the latest version available.
gemlist_options = {:justme => resource[:name]}
gemlist_options.merge!({:source => resource[:source]}) unless resource[:source].nil?
hash = self.class.gemlist(gemlist_options)
hash[:ensure][0]
end
def query
self.class.gemlist(:justme => resource[:name], :local => true)
end
def uninstall
gemcmd "uninstall", "-x", "-a", resource[:name]
end
def update
self.install(false)
end
end
diff --git a/lib/puppet/provider/package/openbsd.rb b/lib/puppet/provider/package/openbsd.rb
index d51bc8f04..d2ebb87c3 100644
--- a/lib/puppet/provider/package/openbsd.rb
+++ b/lib/puppet/provider/package/openbsd.rb
@@ -1,173 +1,175 @@
require 'puppet/provider/package'
# Packaging on OpenBSD. Doesn't work anywhere else that I know of.
Puppet::Type.type(:package).provide :openbsd, :parent => Puppet::Provider::Package do
desc "OpenBSD's form of `pkg_add` support."
commands :pkginfo => "pkg_info", :pkgadd => "pkg_add", :pkgdelete => "pkg_delete"
defaultfor :operatingsystem => :openbsd
confine :operatingsystem => :openbsd
has_feature :versionable
has_feature :install_options
has_feature :uninstall_options
def self.instances
packages = []
begin
execpipe(listcmd) do |process|
# our regex for matching pkg_info output
regex = /^(.*)-(\d[^-]*)[-]?(\w*)(.*)$/
fields = [:name, :ensure, :flavor ]
hash = {}
# now turn each returned line into a package object
process.each_line { |line|
if match = regex.match(line.split[0])
fields.zip(match.captures) { |field,value|
hash[field] = value
}
hash[:provider] = self.name
packages << new(hash)
hash = {}
else
- # Print a warning on lines we can't match, but move
- # on, since it should be non-fatal
- warning("Failed to match line #{line}")
+ unless line =~ /Updating the pkgdb/
+ # Print a warning on lines we can't match, but move
+ # on, since it should be non-fatal
+ warning("Failed to match line #{line}")
+ end
end
}
end
return packages
rescue Puppet::ExecutionFailure
return nil
end
end
def self.listcmd
[command(:pkginfo), "-a"]
end
def parse_pkgconf
unless @resource[:source]
- if Puppet::FileSystem::File.exist?("/etc/pkg.conf")
+ if Puppet::FileSystem.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]
elsif matchdata = line.match(/^installpath\s*\+=\s*(.+)\s*$/i)
if @resource[:source].nil?
@resource[:source] = matchdata[1]
else
@resource[:source] += ":" + matchdata[1]
end
end
end
unless @resource[:source]
raise Puppet::Error,
"No valid installpath found in /etc/pkg.conf and no source was set"
end
else
raise Puppet::Error,
"You must specify a package source or configure an installpath in /etc/pkg.conf"
end
end
end
def install
cmd = []
parse_pkgconf
if @resource[:source][-1,1] == ::File::SEPARATOR
e_vars = { 'PKG_PATH' => @resource[:source] }
full_name = [ @resource[:name], get_version || @resource[:ensure], @resource[:flavor] ].join('-').chomp('-').chomp('-')
else
e_vars = {}
full_name = @resource[:source]
end
cmd << install_options
cmd << full_name
Puppet::Util.withenv(e_vars) { pkgadd cmd.flatten.compact.join(' ') }
end
def get_version
execpipe([command(:pkginfo), "-I", @resource[:name]]) do |process|
# our regex for matching pkg_info output
regex = /^(.*)-(\d[^-]*)[-]?(\D*)(.*)$/
master_version = 0
version = -1
process.each_line do |line|
if match = regex.match(line.split[0])
# now we return the first version, unless ensure is latest
version = match.captures[1]
return version unless @resource[:ensure] == "latest"
master_version = version unless master_version > version
end
end
return master_version unless master_version == 0
return '' if version == -1
raise Puppet::Error, "#{version} is not available for this package"
end
rescue Puppet::ExecutionFailure
return nil
end
def query
# Search for the version info
if pkginfo(@resource[:name]) =~ /Information for (inst:)?#{@resource[:name]}-(\S+)/
return { :ensure => $2 }
else
return nil
end
end
def install_options
join_options(resource[:install_options])
end
def uninstall_options
join_options(resource[:uninstall_options])
end
# Turns a array of options into flags to be passed to pkg_add(8) and
# pkg_delete(8). 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
def uninstall
pkgdelete uninstall_options.flatten.compact.join(' '), @resource[:name]
end
def purge
pkgdelete "-c", "-q", @resource[:name]
end
end
diff --git a/lib/puppet/provider/package/pacman.rb b/lib/puppet/provider/package/pacman.rb
index f811aa5a8..0c721c9d4 100644
--- a/lib/puppet/provider/package/pacman.rb
+++ b/lib/puppet/provider/package/pacman.rb
@@ -1,156 +1,209 @@
require 'puppet/provider/package'
+require 'set'
require 'uri'
Puppet::Type.type(:package).provide :pacman, :parent => Puppet::Provider::Package do
desc "Support for the Package Manager Utility (pacman) used in Archlinux."
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 Puppet::FileSystem::File.exist? '/usr/bin/yaourt'
+ commands :yaourt => "/usr/bin/yaourt" if Puppet::FileSystem.exist? '/usr/bin/yaourt'
confine :operatingsystem => :archlinux
defaultfor :operatingsystem => :archlinux
has_feature :upgradeable
# If yaourt is installed, we can make use of it
def yaourt?
- return Puppet::FileSystem::File.exist? '/usr/bin/yaourt'
+ return Puppet::FileSystem.exist?('/usr/bin/yaourt')
end
# Install a package using 'pacman', or 'yaourt' if available.
# Installs quietly, without confirmation or progressbar, updates package
# list from servers defined in pacman.conf.
def install
if @resource[:source]
install_from_file
else
install_from_repo
end
unless self.query
raise Puppet::ExecutionFailure.new("Could not find package %s" % self.name)
end
end
def install_from_repo
if yaourt?
yaourt "--noconfirm", "-S", @resource[:name]
else
pacman "--noconfirm", "--noprogressbar", "-Sy", @resource[:name]
end
end
private :install_from_repo
def install_from_file
source = @resource[:source]
begin
source_uri = URI.parse source
rescue => detail
- fail "Invalid source '#{source}': #{detail}"
+ self.fail Puppet::Error, "Invalid source '#{source}': #{detail}", detail
end
source = case source_uri.scheme
when nil then source
when /https?/i then source
when /ftp/i then source
when /file/i then source_uri.path
when /puppet/i
fail "puppet:// URL is not supported by pacman"
else
fail "Source #{source} is not supported by pacman"
end
pacman "--noconfirm", "--noprogressbar", "-Sy"
pacman "--noconfirm", "--noprogressbar", "-U", source
end
private :install_from_file
def self.listcmd
[command(:pacman), "-Q"]
end
- # Fetch the list of packages currently installed on the system.
- def self.instances
+ # Pacman has a concept of package groups as well.
+ # Package groups have no versions.
+ def self.listgroupcmd
+ [command(:pacman), "-Qg"]
+ end
+
+ # Get installed packages (pacman -Q)
+ def self.installedpkgs
packages = []
begin
execpipe(listcmd()) do |process|
# pacman -Q output is 'packagename version-rel'
regex = %r{^(\S+)\s(\S+)}
fields = [:name, :ensure]
hash = {}
process.each_line { |line|
if match = regex.match(line)
fields.zip(match.captures) { |field,value|
hash[field] = value
}
hash[:provider] = self.name
packages << new(hash)
+
hash = {}
else
warning("Failed to match line %s" % line)
end
}
end
rescue Puppet::ExecutionFailure
return nil
end
packages
end
+ # Get installed groups (pacman -Qg)
+ def self.installedgroups
+ packages = []
+ begin
+ execpipe(listgroupcmd()) do |process|
+ # pacman -Qg output is 'groupname packagename'
+ # Groups need to be deduplicated
+ groups = Set[]
+
+ process.each_line { |line|
+ groups.add(line.split[0])
+ }
+
+ groups.each { |line|
+ hash = {
+ :name => line,
+ :ensure => "1", # Groups don't have versions, so ensure => latest
+ # will still cause a reinstall.
+ :provider => self.name
+ }
+ packages << new(hash)
+ }
+ end
+ rescue Puppet::ExecutionFailure
+ return nil
+ end
+ packages
+ end
+
+ # Fetch the list of packages currently installed on the system.
+ def self.instances
+ packages = self.installedpkgs
+ groups = self.installedgroups
+ result = nil
+
+ if (!packages && !groups)
+ nil
+ elsif (packages && groups)
+ packages.concat(groups)
+ else
+ packages
+ end
+ end
+
+
# Because Archlinux is a rolling release based distro, installing a package
# should always result in the newest release.
def update
# Install in pacman can be used for update, too
self.install
end
# We rescue the main check from Pacman with a check on the AUR using yaourt, if installed
def latest
pacman "-Sy"
pacman_check = true # Query the main repos first
begin
if pacman_check
output = pacman "-Sp", "--print-format", "%v", @resource[:name]
return output.chomp
else
output = yaourt "-Qma", @resource[:name]
output.split("\n").each do |line|
return line.split[1].chomp if line =~ /^aur/
end
end
rescue Puppet::ExecutionFailure
if pacman_check and self.yaourt?
pacman_check = false # now try the AUR
retry
else
raise
end
end
end
# Querys the pacman master list for information about the package.
def query
begin
output = pacman("-Qi", @resource[:name])
if output =~ /Version.*:\s(.+)/
return { :ensure => $1 }
end
rescue Puppet::ExecutionFailure
return {
:ensure => :purged,
:status => 'missing',
:name => @resource[:name],
:error => 'ok',
}
end
nil
end
# Removes a package from the system.
def uninstall
pacman "--noconfirm", "--noprogressbar", "-R", @resource[:name]
end
end
diff --git a/lib/puppet/provider/package/pip.rb b/lib/puppet/provider/package/pip.rb
index d91753fa5..ddbbacdd0 100644
--- a/lib/puppet/provider/package/pip.rb
+++ b/lib/puppet/provider/package/pip.rb
@@ -1,119 +1,119 @@
# Puppet package provider for Python's `pip` package management frontend.
# <http://pip.openplans.org/>
require 'puppet/provider/package'
require 'xmlrpc/client'
Puppet::Type.type(:package).provide :pip,
:parent => ::Puppet::Provider::Package do
desc "Python packages via `pip`."
has_feature :installable, :uninstallable, :upgradeable, :versionable
# Parse lines of output from `pip freeze`, which are structured as
# _package_==_version_.
def self.parse(line)
if line.chomp =~ /^([^=]+)==([^=]+)$/
{:ensure => $2, :name => $1, :provider => name}
else
nil
end
end
# Return an array of structured information about every installed package
# that's managed by `pip` or an empty array if `pip` is not available.
def self.instances
packages = []
pip_cmd = which(cmd) or return []
execpipe "#{pip_cmd} freeze" do |process|
process.collect do |line|
next unless options = parse(line)
packages << new(options)
end
end
packages
end
def self.cmd
case Facter.value(:osfamily)
when "RedHat"
"pip-python"
else
"pip"
end
end
# Return structured information about a particular package or `nil` if
# it is not installed or `pip` itself is not available.
def query
self.class.instances.each do |provider_pip|
return provider_pip.properties if @resource[:name].downcase == provider_pip.name.downcase
end
return nil
end
# Ask the PyPI API for the latest version number. There is no local
# cache of PyPI's package list so this operation will always have to
# ask the web service.
def latest
client = XMLRPC::Client.new2("http://pypi.python.org/pypi")
client.http_header_extra = {"Content-Type" => "text/xml"}
client.timeout = 10
result = client.call("package_releases", @resource[:name])
result.first
rescue Timeout::Error => detail
- raise Puppet::Error, "Timeout while contacting pypi.python.org: #{detail}";
+ raise Puppet::Error, "Timeout while contacting pypi.python.org: #{detail}", detail.backtrace
end
# Install a package. The ensure parameter may specify installed,
# latest, a version number, or, in conjunction with the source
# parameter, an SCM revision. In that case, the source parameter
# gives the fully-qualified URL to the repository.
def install
args = %w{install -q}
if @resource[:source]
if String === @resource[:ensure]
args << "#{@resource[:source]}@#{@resource[:ensure]}#egg=#{
@resource[:name]}"
else
args << "#{@resource[:source]}#egg=#{@resource[:name]}"
end
else
case @resource[:ensure]
when String
args << "#{@resource[:name]}==#{@resource[:ensure]}"
when :latest
args << "--upgrade" << @resource[:name]
else
args << @resource[:name]
end
end
lazy_pip *args
end
# Uninstall a package. Uninstall won't work reliably on Debian/Ubuntu
# unless this issue gets fixed.
# <http://bugs.debian.org/cgi-bin/bugreport.cgi?bug=562544>
def uninstall
lazy_pip "uninstall", "-y", "-q", @resource[:name]
end
def update
install
end
# Execute a `pip` command. If Puppet doesn't yet know how to do so,
# try to teach it and if even that fails, raise the error.
private
def lazy_pip(*args)
pip *args
rescue NoMethodError => e
if pathname = which(self.class.cmd)
self.class.commands :pip => pathname
pip *args
else
- raise e, 'Could not locate the pip command.'
+ raise e, 'Could not locate the pip command.', e.backtrace
end
end
end
diff --git a/lib/puppet/provider/package/pkgdmg.rb b/lib/puppet/provider/package/pkgdmg.rb
index f14b53158..44c0922be 100644
--- a/lib/puppet/provider/package/pkgdmg.rb
+++ b/lib/puppet/provider/package/pkgdmg.rb
@@ -1,134 +1,149 @@
#
# Motivation: DMG files provide a true HFS file system
# and are easier to manage and .pkg bundles.
#
# Note: the 'apple' Provider checks for the package name
# in /L/Receipts. Since we install multiple pkg's from a single
# source, we treat the source .pkg.dmg file as the package name.
# As a result, we store installed .pkg.dmg file names
# in /var/db/.puppet_pkgdmg_installed_<name>
require 'puppet/provider/package'
require 'facter/util/plist'
require 'puppet/util/http_proxy'
Puppet::Type.type(:package).provide :pkgdmg, :parent => Puppet::Provider::Package do
- desc "Package management based on Apple's Installer.app and
- DiskUtility.app. This package works by checking the contents of a
- DMG image for Apple pkg or mpkg files. Any number of pkg or mpkg
- files may exist in the root directory of the DMG file system.
- Subdirectories are not checked for packages. See
- [the wiki docs on this provider](http://projects.puppetlabs.com/projects/puppet/wiki/Package_Management_With_Dmg_Patterns)
- for more detail."
+ desc "Package management based on Apple's Installer.app and DiskUtility.app.
+
+ This provider works by checking the contents of a DMG image for Apple pkg or
+ mpkg files. Any number of pkg or mpkg files may exist in the root directory
+ of the DMG file system, and Puppet will install all of them. Subdirectories
+ are not checked for packages.
+
+ This provider can also accept plain .pkg (but not .mpkg) files in addition
+ to .dmg files.
+
+ Notes:
+
+ * The `source` attribute is mandatory. It must be either a local disk path
+ or an HTTP, HTTPS, or FTP URL to the package.
+ * The `name` of the resource must be the filename (without path) of the DMG file.
+ * When installing the packages from a DMG, this provider writes a file to
+ disk at `/var/db/.puppet_pkgdmg_installed_NAME`. If that file is present,
+ Puppet assumes all packages from that DMG are already installed.
+ * This provider is not versionable and uses DMG filenames to determine
+ whether a package has been installed. Thus, to install new a version of a
+ package, you must create a new DMG with a different filename."
confine :operatingsystem => :darwin
defaultfor :operatingsystem => :darwin
commands :installer => "/usr/sbin/installer"
commands :hdiutil => "/usr/bin/hdiutil"
commands :curl => "/usr/bin/curl"
# JJM We store a cookie for each installed .pkg.dmg in /var/db
def self.instance_by_name
Dir.entries("/var/db").find_all { |f|
f =~ /^\.puppet_pkgdmg_installed_/
}.collect do |f|
name = f.sub(/^\.puppet_pkgdmg_installed_/, '')
yield name if block_given?
name
end
end
def self.instances
instance_by_name.collect do |name|
new(:name => name, :provider => :pkgdmg, :ensure => :installed)
end
end
def self.installpkg(source, name, orig_source)
installer "-pkg", source, "-target", "/"
# Non-zero exit status will throw an exception.
File.open("/var/db/.puppet_pkgdmg_installed_#{name}", "w") do |t|
t.print "name: '#{name}'\n"
t.print "source: '#{orig_source}'\n"
end
end
def self.installpkgdmg(source, name)
http_proxy_host = Puppet::Util::HttpProxy.http_proxy_host
http_proxy_port = Puppet::Util::HttpProxy.http_proxy_port
unless source =~ /\.dmg$/i || source =~ /\.pkg$/i
raise Puppet::Error.new("Mac OS X PKG DMG's must specify a source string ending in .dmg or flat .pkg file")
end
- require 'open-uri'
+ require 'open-uri' # Dead code; this is never used. The File.open call 20-ish lines south of here used to be Kernel.open but changed in '09. -NF
cached_source = source
tmpdir = Dir.mktmpdir
ext = /(\.dmg|\.pkg)$/i.match(source)[0]
begin
if %r{\A[A-Za-z][A-Za-z0-9+\-\.]*://} =~ cached_source
cached_source = File.join(tmpdir, "#{name}#{ext}")
args = [ "-o", cached_source, "-C", "-", "-k", "-L", "-s", "--fail", "--url", source ]
if http_proxy_host and http_proxy_port
args << "--proxy" << "#{http_proxy_host}:#{http_proxy_port}"
elsif http_proxy_host and not http_proxy_port
args << "--proxy" << http_proxy_host
end
begin
curl *args
Puppet.debug "Success: curl transfered [#{name}] (via: curl #{args.join(" ")})"
rescue Puppet::ExecutionFailure
- Puppet.debug "curl #{args.join(" ")} did not transfer [#{name}]. Falling back to slower open-uri transfer methods."
+ Puppet.debug "curl #{args.join(" ")} did not transfer [#{name}]. Falling back to local file." # This used to fall back to open-uri. -NF
cached_source = source
end
end
if source =~ /\.dmg$/i
+ # If you fix this to use open-uri again, you must update the docs above. -NF
File.open(cached_source) do |dmg|
xml_str = hdiutil "mount", "-plist", "-nobrowse", "-readonly", "-noidme", "-mountrandom", "/tmp", dmg.path
hdiutil_info = Plist::parse_xml(xml_str)
raise Puppet::Error.new("No disk entities returned by mount at #{dmg.path}") unless hdiutil_info.has_key?("system-entities")
mounts = hdiutil_info["system-entities"].collect { |entity|
entity["mount-point"]
}.compact
begin
mounts.each do |mountpoint|
Dir.entries(mountpoint).select { |f|
f =~ /\.m{0,1}pkg$/i
}.each do |pkg|
installpkg("#{mountpoint}/#{pkg}", name, source)
end
end
ensure
mounts.each do |mountpoint|
hdiutil "eject", mountpoint
end
end
end
else
installpkg(cached_source, name, source)
end
ensure
FileUtils.remove_entry_secure(tmpdir, true)
end
end
def query
- if Puppet::FileSystem::File.exist?("/var/db/.puppet_pkgdmg_installed_#{@resource[:name]}")
+ if Puppet::FileSystem.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
return nil
end
end
def install
source = nil
unless source = @resource[:source]
raise Puppet::Error.new("Mac OS X PKG DMG's must specify a package source.")
end
unless name = @resource[:name]
raise Puppet::Error.new("Mac OS X PKG DMG's must specify a package name.")
end
self.class.installpkgdmg(source,name)
end
end
diff --git a/lib/puppet/provider/package/pkgin.rb b/lib/puppet/provider/package/pkgin.rb
index 5061cd3c9..5cdb858d5 100644
--- a/lib/puppet/provider/package/pkgin.rb
+++ b/lib/puppet/provider/package/pkgin.rb
@@ -1,62 +1,87 @@
require "puppet/provider/package"
Puppet::Type.type(:package).provide :pkgin, :parent => Puppet::Provider::Package do
desc "Package management using pkgin, a binary package manager for pkgsrc."
commands :pkgin => "pkgin"
- defaultfor :operatingsystem => :dragonfly
+ defaultfor :operatingsystem => [ :dragonfly , :smartos ]
- has_feature :installable, :uninstallable
+ has_feature :installable, :uninstallable, :upgradeable, :versionable
- def self.parse_pkgin_line(package, force_status=nil)
+ def self.parse_pkgin_line(package)
# e.g.
# vim-7.2.446 = Vim editor (vi clone) without GUI
match, name, version, status = *package.match(/(\S+)-(\S+)(?: (=|>|<))?\s+.+$/)
if match
- ensure_status = if force_status
- force_status
- elsif status
- :present
- else
- :absent
- end
-
{
:name => name,
- :ensure => ensure_status,
- :provider => :pkgin
+ :status => status,
+ :ensure => version
}
end
end
+ def self.prefetch(packages)
+ super
+ # Withouth -f, no fresh pkg_summary files are downloaded
+ pkgin("-yf", :update)
+ end
+
def self.instances
pkgin(:list).split("\n").map do |package|
- new(parse_pkgin_line(package, :present))
+ new(parse_pkgin_line(package))
end
end
def query
+ packages = parse_pkgsearch_line
+
+ if packages.empty?
+ if @resource[:ensure] == :absent
+ notice "declared as absent but unavailable #{@resource.file}:#{resource.line}"
+ return false
+ else
+ @resource.fail "No candidate to be installed"
+ end
+ end
+
+ packages.first.update( :ensure => :absent )
+ end
+
+ def parse_pkgsearch_line
packages = pkgin(:search, resource[:name]).split("\n")
- # Remove the last three lines of help text.
- packages.slice!(-3, 3)
+ return [] if packages.length == 1
- matching_package = nil
- packages.detect do |package|
- properties = self.class.parse_pkgin_line(package)
- matching_package = properties if properties && resource[:name] == properties[:name]
- end
+ # Remove the last three lines of help text.
+ packages.slice!(-4, 4)
- matching_package
+ pkglist = packages.map{ |line| self.class.parse_pkgin_line(line) }
+ pkglist.select{ |package| resource[:name] == package[:name] }
end
def install
- pkgin("-y", :install, resource[:name])
+ if String === @resource[:ensure]
+ pkgin("-y", :install, "#{resource[:name]}-#{resource[:ensure]}")
+ else
+ pkgin("-y", :install, resource[:name])
+ end
end
def uninstall
pkgin("-y", :remove, resource[:name])
end
+
+ def latest
+ package = parse_pkgsearch_line.detect{ |package| package[:status] == '<' }
+ return properties[:ensure] if not package
+ return package[:ensure]
+ end
+
+ def update
+ pkgin("-y", :install, resource[:name])
+ end
+
end
diff --git a/lib/puppet/provider/package/pkgutil.rb b/lib/puppet/provider/package/pkgutil.rb
index c114fa949..71aa29177 100644
--- a/lib/puppet/provider/package/pkgutil.rb
+++ b/lib/puppet/provider/package/pkgutil.rb
@@ -1,186 +1,186 @@
# Packaging using Peter Bonivart's pkgutil program.
Puppet::Type.type(:package).provide :pkgutil, :parent => :sun, :source => :sun do
desc "Package management using Peter Bonivart's ``pkgutil`` command on Solaris."
pkgutil_bin = "pkgutil"
if FileTest.executable?("/opt/csw/bin/pkgutil")
pkgutil_bin = "/opt/csw/bin/pkgutil"
end
confine :osfamily => :solaris
has_command(:pkguti, pkgutil_bin) do
environment :HOME => ENV['HOME']
end
def self.healthcheck()
- unless Puppet::FileSystem::File.exist?("/var/opt/csw/pkgutil/admin")
+ unless Puppet::FileSystem.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
correct_wgetopts = false
[ "/opt/csw/etc/pkgutil.conf", "/etc/opt/csw/pkgutil.conf" ].each do |confpath|
File.open(confpath) do |conf|
conf.each_line {|line| correct_wgetopts = true if line =~ /^\s*wgetopts\s*=.*(-nv|-q|--no-verbose|--quiet)/ }
end
end
if ! correct_wgetopts
Puppet.notice "It is highly recommended that you set 'wgetopts=-nv' in your pkgutil.conf."
end
end
def self.instances(hash = {})
healthcheck
# Use the available pkg list (-a) to work out aliases
aliases = {}
availlist.each do |pkg|
aliases[pkg[:name]] = pkg[:alias]
end
# The -c pkglist lists installed packages
pkginsts = []
output = pkguti(["-c"])
parse_pkglist(output).each do |pkg|
pkg.delete(:avail)
pkginsts << new(pkg)
# Create a second instance with the alias if it's different
pkgalias = aliases[pkg[:name]]
if pkgalias and pkg[:name] != pkgalias
apkg = pkg.dup
apkg[:name] = pkgalias
pkginsts << new(apkg)
end
end
pkginsts
end
# Turns a pkgutil -a listing into hashes with the common alias, full
# package name and available version
def self.availlist
output = pkguti ["-a"]
output.split("\n").collect do |line|
next if line =~ /^common\s+package/ # header of package list
next if noise?(line)
if line =~ /\s*(\S+)\s+(\S+)\s+(.*)/
{ :alias => $1, :name => $2, :avail => $3 }
else
Puppet.warning "Cannot match %s" % line
end
end.reject { |h| h.nil? }
end
# Turn our pkgutil -c listing into a hash for a single package.
def pkgsingle(resource)
# The --single option speeds up the execution, because it queries
# the package managament system for one package only.
command = ["-c", "--single", resource[:name]]
self.class.parse_pkglist(run_pkgutil(resource, command), { :justme => resource[:name] })
end
# Turn our pkgutil -c listing into a bunch of hashes.
def self.parse_pkglist(output, hash = {})
output = output.split("\n")
if output[-1] == "Not in catalog"
Puppet.warning "Package not in pkgutil catalog: %s" % hash[:justme]
return nil
end
list = output.collect do |line|
next if line =~ /installed\s+catalog/ # header of package list
next if noise?(line)
pkgsplit(line)
end.reject { |h| h.nil? }
if hash[:justme]
# Single queries may have been for an alias so return the name requested
if list.any?
list[-1][:name] = hash[:justme]
return list[-1]
end
else
list.reject! { |h| h[:ensure] == :absent }
return list
end
end
# Identify common types of pkgutil noise as it downloads catalogs etc
def self.noise?(line)
true if line =~ /^#/
true if line =~ /^Checking integrity / # use_gpg
true if line =~ /^gpg: / # gpg verification
true if line =~ /^=+> / # catalog fetch
true if line =~ /\d+:\d+:\d+ URL:/ # wget without -q
false
end
# Split the different lines into hashes.
def self.pkgsplit(line)
if line =~ /\s*(\S+)\s+(\S+)\s+(.*)/
hash = {}
hash[:name] = $1
hash[:ensure] = if $2 == "notinst"
:absent
else
$2
end
hash[:avail] = $3
if hash[:avail] =~ /^SAME\s*$/
hash[:avail] = hash[:ensure]
end
# Use the name method, so it works with subclasses.
hash[:provider] = self.name
return hash
else
Puppet.warning "Cannot match %s" % line
return nil
end
end
def run_pkgutil(resource, *args)
# Allow source to be one or more URLs pointing to a repository that all
# get passed to pkgutil via one or more -t options
if resource[:source]
sources = [resource[:source]].flatten
pkguti *[sources.map{|src| [ "-t", src ]}, *args].flatten
else
pkguti *args.flatten
end
end
def install
run_pkgutil @resource, "-y", "-i", @resource[:name]
end
# Retrieve the version from the current package file.
def latest
hash = pkgsingle(@resource)
hash[:avail] if hash
end
def query
if hash = pkgsingle(@resource)
hash
else
{:ensure => :absent}
end
end
def update
run_pkgutil @resource, "-y", "-u", @resource[:name]
end
def uninstall
run_pkgutil @resource, "-y", "-r", @resource[:name]
end
end
diff --git a/lib/puppet/provider/package/ports.rb b/lib/puppet/provider/package/ports.rb
index 9141e30f5..ffc79d2df 100644
--- a/lib/puppet/provider/package/ports.rb
+++ b/lib/puppet/provider/package/ports.rb
@@ -1,94 +1,94 @@
Puppet::Type.type(:package).provide :ports, :parent => :freebsd, :source => :freebsd do
desc "Support for FreeBSD's ports. Note that this, too, mixes packages and ports."
commands :portupgrade => "/usr/local/sbin/portupgrade",
:portversion => "/usr/local/sbin/portversion",
:portuninstall => "/usr/local/sbin/pkg_deinstall",
:portinfo => "/usr/sbin/pkg_info"
defaultfor :operatingsystem => :freebsd
# I hate ports
%w{INTERACTIVE UNAME}.each do |var|
ENV.delete(var) if ENV.include?(var)
end
def install
# -N: install if the package is missing, otherwise upgrade
# -M: yes, we're a batch, so don't ask any questions
cmd = %w{-N -M BATCH=yes} << @resource[:name]
output = portupgrade(*cmd)
if output =~ /\*\* No such /
raise Puppet::ExecutionFailure, "Could not find package #{@resource[:name]}"
end
end
# If there are multiple packages, we only use the last one
def latest
cmd = ["-v", @resource[:name]]
begin
output = portversion(*cmd)
rescue Puppet::ExecutionFailure
- raise Puppet::Error.new(output)
+ raise Puppet::Error.new(output, $!)
end
line = output.split("\n").pop
unless line =~ /^(\S+)\s+(\S)\s+(.+)$/
# There's no "latest" version, so just return a placeholder
return :latest
end
pkgstuff = $1
match = $2
info = $3
unless pkgstuff =~ /^\S+-([^-\s]+)$/
raise Puppet::Error,
"Could not match package info '#{pkgstuff}'"
end
version = $1
if match == "=" or match == ">"
# we're up to date or more recent
return version
end
# Else, we need to be updated; we need to pull out the new version
unless info =~ /\((\w+) has (.+)\)/
raise Puppet::Error,
"Could not match version info '#{info}'"
end
source, newversion = $1, $2
debug "Newer version in #{source}"
newversion
end
def query
# support portorigin_glob such as "mail/postfix"
name = self.name
if name =~ /\//
name = self.name.split(/\//).slice(1)
end
self.class.instances.each do |instance|
if instance.name == name
return instance.properties
end
end
nil
end
def uninstall
portuninstall @resource[:name]
end
def update
install
end
end
diff --git a/lib/puppet/provider/package/portupgrade.rb b/lib/puppet/provider/package/portupgrade.rb
index 2d9f2aef1..5347a7981 100644
--- a/lib/puppet/provider/package/portupgrade.rb
+++ b/lib/puppet/provider/package/portupgrade.rb
@@ -1,241 +1,241 @@
# Whole new package, so include pack stuff
require 'puppet/provider/package'
Puppet::Type.type(:package).provide :portupgrade, :parent => Puppet::Provider::Package do
include Puppet::Util::Execution
desc "Support for FreeBSD's ports using the portupgrade ports management software.
Use the port's full origin as the resource name. eg (ports-mgmt/portupgrade)
for the portupgrade port."
## has_features is usually autodetected based on defs below.
# has_features :installable, :uninstallable, :upgradeable
commands :portupgrade => "/usr/local/sbin/portupgrade",
:portinstall => "/usr/local/sbin/portinstall",
:portversion => "/usr/local/sbin/portversion",
:portuninstall => "/usr/local/sbin/pkg_deinstall",
:portinfo => "/usr/sbin/pkg_info"
## Activate this only once approved by someone important.
# defaultfor :operatingsystem => :freebsd
# Remove unwanted environment variables.
%w{INTERACTIVE UNAME}.each do |var|
if ENV.include?(var)
ENV.delete(var)
end
end
######## instances sub command (builds the installed packages list)
def self.instances
Puppet.debug "portupgrade.rb Building packages list from installed ports"
# regex to match output from pkg_info
regex = %r{^(\S+)-([^-\s]+):(\S+)$}
# Corresponding field names
fields = [:portname, :ensure, :portorigin]
# define Temporary hash used, packages array of hashes
hash = Hash.new
packages = []
# exec command
cmdline = ["-aoQ"]
begin
output = portinfo(*cmdline)
rescue Puppet::ExecutionFailure
- raise Puppet::Error.new(output)
+ raise Puppet::Error.new(output, $!)
return nil
end
# split output and match it and populate temp hash
output.split("\n").each { |data|
# reset hash to nil for each line
hash.clear
if match = regex.match(data)
# Output matched regex
fields.zip(match.captures) { |field, value|
hash[field] = value
}
# populate the actual :name field from the :portorigin
# Set :provider to this object name
hash[:name] = hash[:portorigin]
hash[:provider] = self.name
# Add to the full packages listing
packages << new(hash)
else
# unrecognised output from pkg_info
Puppet.debug "portupgrade.Instances() - unable to match output: #{data}"
end
}
# return the packages array of hashes
return packages
end
######## Installation sub command
def install
Puppet.debug "portupgrade.install() - Installation call on #{@resource[:name]}"
# -M: yes, we're a batch, so don't ask any questions
cmdline = ["-M BATCH=yes", @resource[:name]]
# FIXME: it's possible that portinstall prompts for data so locks up.
begin
output = portinstall(*cmdline)
rescue Puppet::ExecutionFailure
- raise Puppet::Error.new(output)
+ raise Puppet::Error.new(output, $!)
end
if output =~ /\*\* No such /
raise Puppet::ExecutionFailure, "Could not find package #{@resource[:name]}"
end
# No return code required, so do nil to be clean
return nil
end
######## Latest subcommand (returns the latest version available, or current version if installed is latest)
def latest
Puppet.debug "portupgrade.latest() - Latest check called on #{@resource[:name]}"
# search for latest version available, or return current version.
# cmdline = "portversion -v <portorigin>", returns "<portname> <code> <stuff>"
# or "** No matching package found: <portname>"
cmdline = ["-v", @resource[:name]]
begin
output = portversion(*cmdline)
rescue Puppet::ExecutionFailure
- raise Puppet::Error.new(output)
+ raise Puppet::Error.new(output, $!)
end
# Check: output format.
if output =~ /^\S+-([^-\s]+)\s+(\S)\s+(.*)/
installedversion = $1
comparison = $2
otherdata = $3
# Only return a new version number when it's clear that there is a new version
# all others return the current version so no unexpected 'upgrades' occur.
case comparison
when "=", ">"
Puppet.debug "portupgrade.latest() - Installed package is latest (#{installedversion})"
return installedversion
when "<"
# "portpkg-1.7_5 < needs updating (port has 1.14)"
# "portpkg-1.7_5 < needs updating (port has 1.14) (=> 'newport/pkg')
if otherdata =~ /\(port has (\S+)\)/
newversion = $1
Puppet.debug "portupgrade.latest() - Installed version needs updating to (#{newversion})"
return newversion
else
Puppet.debug "portupgrade.latest() - Unable to determine new version from (#{otherdata})"
return installedversion
end
when "?", "!", "#"
Puppet.debug "portupgrade.latest() - Comparison Error reported from portversion (#{output})"
return installedversion
else
Puppet.debug "portupgrade.latest() - Unknown code from portversion output (#{output})"
return installedversion
end
else
# error: output not parsed correctly, error out with nil.
# Seriously - this section should never be called in a perfect world.
# as verification that the port is installed has already happened in query.
if output =~ /^\*\* No matching package /
raise Puppet::ExecutionFailure, "Could not find package #{@resource[:name]}"
else
# Any other error (dump output to log)
raise Puppet::ExecutionFailure, "Unexpected output from portversion: #{output}"
end
# Just in case we still are running, return nil
return nil
end
# At this point normal operation has finished and we shouldn't have been called.
# Error out and let the admin deal with it.
raise Puppet::Error, "portversion.latest() - fatal error with portversion: #{output}"
return nil
end
###### Query subcommand - return a hash of details if exists, or nil if it doesn't.
# Used to make sure the package is installed
def query
Puppet.debug "portupgrade.query() - Called on #{@resource[:name]}"
cmdline = ["-qO", @resource[:name]]
begin
output = portinfo(*cmdline)
rescue Puppet::ExecutionFailure
- raise Puppet::Error.new(output)
+ raise Puppet::Error.new(output, $!)
end
# Check: if output isn't in the right format, return nil
if output =~ /^(\S+)-([^-\s]+)/
# Fill in the details
hash = Hash.new
hash[:portorigin] = self.name
hash[:portname] = $1
hash[:ensure] = $2
# If more details are required, then we can do another pkg_info
# query here and parse out that output and add to the hash
# return the hash to the caller
return hash
else
Puppet.debug "portupgrade.query() - package (#{@resource[:name]}) not installed"
return nil
end
end
####### Uninstall command
def uninstall
Puppet.debug "portupgrade.uninstall() - called on #{@resource[:name]}"
# Get full package name from port origin to uninstall with
cmdline = ["-qO", @resource[:name]]
begin
output = portinfo(*cmdline)
rescue Puppet::ExecutionFailure
- raise Puppet::Error.new(output)
+ raise Puppet::Error.new(output, $!)
end
if output =~ /^(\S+)/
# output matches, so uninstall it
portuninstall $1
end
end
######## Update/upgrade command
def update
Puppet.debug "portupgrade.update() - called on (#{@resource[:name]})"
cmdline = ["-qO", @resource[:name]]
begin
output = portinfo(*cmdline)
rescue Puppet::ExecutionFailure
- raise Puppet::Error.new(output)
+ raise Puppet::Error.new(output, $!)
end
if output =~ /^(\S+)/
# output matches, so upgrade the software
cmdline = ["-M BATCH=yes", $1]
begin
output = portupgrade(*cmdline)
rescue Puppet::ExecutionFailure
- raise Puppet::Error.new(output)
+ raise Puppet::Error.new(output, $!)
end
end
end
## EOF
end
diff --git a/lib/puppet/provider/package/rpm.rb b/lib/puppet/provider/package/rpm.rb
index c97ba6f44..514752ded 100644
--- a/lib/puppet/provider/package/rpm.rb
+++ b/lib/puppet/provider/package/rpm.rb
@@ -1,197 +1,205 @@
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.
- 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.)"
+ This provider supports the `install_options` and `uninstall_options`
+ attributes, which allow command-line flags to be passed to the RPM binary.
+ These 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
+ has_feature :uninstall_options
# Note: self:: is required here to keep these constants in the context of what will
# eventually become this Puppet::Type::Package::ProviderRpm class.
# The query format by which we identify installed packages
self::NEVRA_FORMAT = %Q{%{NAME} %|EPOCH?{%{EPOCH}}:{0}| %{VERSION} %{RELEASE} %{ARCH}\\n}
self::NEVRA_REGEX = %r{^(\S+) (\S+) (\S+) (\S+) (\S+)$}
self::NEVRA_FIELDS = [:name, :epoch, :version, :release, :arch]
commands :rpm => "rpm"
if command('rpm')
confine :true => begin
rpm('--version')
rescue Puppet::ExecutionFailure
false
else
true
end
end
def self.current_version
return @current_version unless @current_version.nil?
output = rpm "--version"
@current_version = output.gsub('RPM version ', '').strip
end
# rpm < 4.1 don't support --nosignature
def self.nosignature
'--nosignature' unless Puppet::Util::Package.versioncmp(current_version, '4.1') < 0
end
# rpm < 4.0.2 don't support --nodigest
def self.nodigest
'--nodigest' unless Puppet::Util::Package.versioncmp(current_version, '4.0.2') < 0
end
def self.instances
packages = []
# list out all of the packages
begin
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)
packages << new(hash) unless hash.empty?
}
}
rescue Puppet::ExecutionFailure
- raise Puppet::Error, "Failed to list packages"
+ raise Puppet::Error, "Failed to list packages", $!.backtrace
end
packages
end
# Find the fully versioned package name and the version alone. Returns
# a hash with entries :instance => fully versioned package name, and
# :ensure => version-release
def query
#NOTE: Prior to a fix for issue 1243, this method potentially returned a cached value
#IF YOU CALL THIS METHOD, IT WILL CALL RPM
#Use get(:property) to check if cached values are available
- cmd = ["-q", @resource[:name], "#{self.class.nosignature}", "#{self.class.nodigest}", "--qf", self.class::NEVRA_FORMAT]
+ cmd = ["-q", '--whatprovides', @resource[:name], "#{self.class.nosignature}", "#{self.class.nodigest}", "--qf", self.class::NEVRA_FORMAT]
begin
output = rpm(*cmd)
rescue Puppet::ExecutionFailure
# rpm -q exits 1 if package not found
return nil
end
# FIXME: We could actually be getting back multiple packages
# for multilib and this will only return the first such package
@property_hash.update(self.class.nevra_to_hash(output))
@property_hash.dup
end
# Here we just retrieve the version from the file specified in the source.
def latest
unless source = @resource[:source]
@resource.fail "RPMs must specify a package source"
end
cmd = [command(:rpm), "-q", "--qf", self.class::NEVRA_FORMAT, "-p", source]
h = self.class.nevra_to_hash(execfail(cmd, Puppet::Error))
h[:ensure]
end
def install
source = nil
unless source = @resource[:source]
@resource.fail "RPMs must specify a package source"
end
# RPM gets pissy if you try to install an already
# installed package
if @resource.should(:ensure) == @property_hash[:ensure] or
@resource.should(:ensure) == :latest && @property_hash[:ensure] == latest
return
end
flag = ["-i"]
flag = ["-U", "--oldpackage"] if @property_hash[:ensure] and @property_hash[:ensure] != :absent
flag = flag + install_options
rpm flag, source
end
def uninstall
query if get(:arch) == :absent
nvr = "#{get(:name)}-#{get(:version)}-#{get(:release)}"
arch = ".#{get(:arch)}"
# If they specified an arch in the manifest, erase that Otherwise,
# erase the arch we got back from the query. If multiple arches are
# installed and only the package name is specified (without the
# arch), this will uninstall all of them on successive runs of the
# client, one after the other
# version of RPM prior to 4.2.1 can't accept the architecture as
# part of the package name.
unless Puppet::Util::Package.versioncmp(self.class.current_version, '4.2.1') < 0
if @resource[:name][-arch.size, arch.size] == arch
nvr += arch
else
nvr += ".#{get(:arch)}"
end
end
- rpm "-e", nvr
+
+ flag = ["-e"] + uninstall_options
+ rpm flag, nvr
end
def update
self.install
end
def install_options
join_options(resource[:install_options])
end
+ def uninstall_options
+ join_options(resource[:uninstall_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
# or an empty hash if we failed to parse
# @api private
def self.nevra_to_hash(line)
line.strip!
hash = {}
if match = self::NEVRA_REGEX.match(line)
self::NEVRA_FIELDS.zip(match.captures) { |f, v| hash[f] = v }
hash[:provider] = self.name
hash[:ensure] = "#{hash[:version]}-#{hash[:release]}"
else
Puppet.debug("Failed to match rpm line #{line}")
end
return hash
end
end
diff --git a/lib/puppet/provider/package/windows.rb b/lib/puppet/provider/package/windows.rb
index d4936c89b..aaf455181 100644
--- a/lib/puppet/provider/package/windows.rb
+++ b/lib/puppet/provider/package/windows.rb
@@ -1,123 +1,123 @@
require 'puppet/provider/package'
require 'puppet/util/windows'
require 'puppet/provider/package/windows/package'
Puppet::Type.type(:package).provide(:windows, :parent => Puppet::Provider::Package) do
desc "Windows package management.
This provider supports either MSI or self-extracting executable installers.
This provider requires a `source` attribute when installing the package.
- It accepts paths paths to local files, mapped drives, or UNC paths.
+ It accepts paths to local files, mapped drives, or UNC paths.
If the executable requires special arguments to perform a silent install or
uninstall, then the appropriate arguments should be specified using the
`install_options` or `uninstall_options` attributes, respectively. Puppet
will automatically quote any option that contains spaces."
confine :operatingsystem => :windows
defaultfor :operatingsystem => :windows
has_feature :installable
has_feature :uninstallable
has_feature :install_options
has_feature :uninstall_options
has_feature :versionable
attr_accessor :package
# Return an array of provider instances
def self.instances
Puppet::Provider::Package::Windows::Package.map do |pkg|
provider = new(to_hash(pkg))
provider.package = pkg
provider
end
end
def self.to_hash(pkg)
{
:name => pkg.name,
:ensure => pkg.version || :installed,
:provider => :windows
}
end
# Query for the provider hash for the current resource. The provider we
# are querying, may not have existed during prefetch
def query
Puppet::Provider::Package::Windows::Package.find do |pkg|
if pkg.match?(resource)
return self.class.to_hash(pkg)
end
end
nil
end
def install
installer = Puppet::Provider::Package::Windows::Package.installer_class(resource)
command = [installer.install_command(resource), install_options].flatten.compact.join(' ')
output = execute(command, :failonfail => false, :combine => true)
check_result(output.exitstatus)
end
def uninstall
command = [package.uninstall_command, uninstall_options].flatten.compact.join(' ')
output = execute(command, :failonfail => false, :combine => true)
check_result(output.exitstatus)
end
# http://msdn.microsoft.com/en-us/library/windows/desktop/aa368542(v=vs.85).aspx
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
# successfully, but we warn since we don't have a good reboot strategy.
def check_result(hr)
operation = resource[:ensure] == :absent ? 'uninstall' : 'install'
case hr
when self.class::ERROR_SUCCESS
# yeah
when self.class::ERROR_SUCCESS_REBOOT_INITIATED
warning("The package #{operation}ed successfully and the system is rebooting now.")
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)
end
end
# This only get's called if there is a value to validate, but not if it's absent
def validate_source(value)
fail("The source parameter cannot be empty when using the Windows provider.") if value.empty?
end
def install_options
join_options(resource[:install_options])
end
def uninstall_options
join_options(resource[:uninstall_options])
end
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
end
diff --git a/lib/puppet/provider/package/windows/package.rb b/lib/puppet/provider/package/windows/package.rb
index e26c6773c..07fb64d25 100644
--- a/lib/puppet/provider/package/windows/package.rb
+++ b/lib/puppet/provider/package/windows/package.rb
@@ -1,80 +1,80 @@
require 'puppet/provider/package'
require 'puppet/util/windows'
class Puppet::Provider::Package::Windows
class Package
extend Enumerable
extend Puppet::Util::Errors
include Puppet::Util::Windows::Registry
extend Puppet::Util::Windows::Registry
attr_reader :name, :version
# Enumerate each package. The appropriate package subclass
# will be yielded.
def self.each(&block)
with_key do |key, values|
name = key.name.match(/^.+\\([^\\]+)$/).captures[0]
[MsiPackage, ExePackage].find do |klass|
if pkg = klass.from_registry(name, values)
yield pkg
end
end
end
end
# Yield each registry key and its values associated with an
# installed package. This searches both per-machine and current
# user contexts, as well as packages associated with 64 and
# 32-bit installers.
def self.with_key(&block)
%w[HKEY_LOCAL_MACHINE HKEY_CURRENT_USER].each do |hive|
[KEY64, KEY32].each do |mode|
mode |= KEY_READ
begin
open(hive, 'Software\Microsoft\Windows\CurrentVersion\Uninstall', mode) do |uninstall|
uninstall.each_key do |name, wtime|
open(hive, "#{uninstall.keyname}\\#{name}", mode) do |key|
yield key, values(key)
end
end
end
rescue Puppet::Util::Windows::Error => e
raise e unless e.code == Windows::Error::ERROR_FILE_NOT_FOUND
end
end
end
end
# Get the class that knows how to install this resource
def self.installer_class(resource)
fail("The source parameter is required when using the Windows provider.") unless resource[:source]
case resource[:source]
when /\.msi"?\Z/i
# REMIND: can we install from URL?
# REMIND: what about msp, etc
MsiPackage
when /\.exe"?\Z/i
- fail("The source does not exist: '#{resource[:source]}'") unless Puppet::FileSystem::File.exist?(resource[:source])
+ fail("The source does not exist: '#{resource[:source]}'") unless Puppet::FileSystem.exist?(resource[:source])
ExePackage
else
fail("Don't know how to install '#{resource[:source]}'")
end
end
def self.quote(value)
value.include?(' ') ? %Q["#{value.gsub(/"/, '\"')}"] : value
end
def initialize(name, version)
@name = name
@version = version
end
end
end
require 'puppet/provider/package/windows/msi_package'
require 'puppet/provider/package/windows/exe_package'
diff --git a/lib/puppet/provider/parsedfile.rb b/lib/puppet/provider/parsedfile.rb
index 1a13c8df3..03ad1e194 100644
--- a/lib/puppet/provider/parsedfile.rb
+++ b/lib/puppet/provider/parsedfile.rb
@@ -1,443 +1,443 @@
require 'puppet'
require 'puppet/util/filetype'
require 'puppet/util/fileparsing'
# This provider can be used as the parent class for a provider that
# parses and generates files. Its content must be loaded via the
# 'prefetch' method, and the file will be written when 'flush' is called
# on the provider instance. At this point, the file is written once
# for every provider instance.
#
# Once the provider prefetches the data, it's the resource's job to copy
# that data over to the @is variables.
class Puppet::Provider::ParsedFile < Puppet::Provider
extend Puppet::Util::FileParsing
class << self
attr_accessor :default_target, :target
end
attr_accessor :property_hash
def self.clean(hash)
newhash = hash.dup
[:record_type, :on_disk].each do |p|
newhash.delete(p) if newhash.include?(p)
end
newhash
end
def self.clear
@target_objects.clear
@records.clear
end
def self.filetype
@filetype ||= Puppet::Util::FileType.filetype(:flat)
end
def self.filetype=(type)
if type.is_a?(Class)
@filetype = type
elsif klass = Puppet::Util::FileType.filetype(type)
@filetype = klass
else
raise ArgumentError, "Invalid filetype #{type}"
end
end
# Flush all of the targets for which there are modified records. The only
# reason we pass a record here is so that we can add it to the stack if
# necessary -- it's passed from the instance calling 'flush'.
def self.flush(record)
# Make sure this record is on the list to be flushed.
unless record[:on_disk]
record[:on_disk] = true
@records << record
# If we've just added the record, then make sure our
# target will get flushed.
modified(record[:target] || default_target)
end
return unless defined?(@modified) and ! @modified.empty?
flushed = []
begin
@modified.sort { |a,b| a.to_s <=> b.to_s }.uniq.each do |target|
Puppet.debug "Flushing #{@resource_type.name} provider target #{target}"
flushed << target
flush_target(target)
end
ensure
@modified.reject! { |t| flushed.include?(t) }
end
end
# Make sure our file is backed up, but only back it up once per transaction.
# We cheat and rely on the fact that @records is created on each prefetch.
def self.backup_target(target)
return nil unless target_object(target).respond_to?(:backup)
@backup_stats ||= {}
return nil if @backup_stats[target] == @records.object_id
target_object(target).backup
@backup_stats[target] = @records.object_id
end
# Flush all of the records relating to a specific target.
def self.flush_target(target)
backup_target(target)
records = target_records(target).reject { |r|
r[:ensure] == :absent
}
target_object(target).write(to_file(records))
end
# Return the header placed at the top of each generated file, warning
# users that modifying this file manually is probably a bad idea.
def self.header
%{# HEADER: This file was autogenerated at #{Time.now}
# HEADER: by puppet. While it can still be managed manually, it
# HEADER: is definitely not recommended.\n}
end
# An optional regular expression matched by third party headers.
#
# For example, this can be used to filter the vixie cron headers as
# erronously exported by older cron versions.
#
# @api private
# @abstract Providers based on ParsedFile may implement this to make it
# possible to identify a header maintained by a third party tool.
# The provider can then allow that header to remain near the top of the
# written file, or remove it after composing the file content.
# If implemented, the function must return a Regexp object.
# The expression must be tailored to match exactly one third party header.
# @see drop_native_header
# @note When specifying regular expressions in multiline mode, avoid
# greedy repititions such as '.*' (use .*? instead). Otherwise, the
# provider may drop file content between sparse headers.
def self.native_header_regex
nil
end
# How to handle third party headers.
# @api private
# @abstract Providers based on ParsedFile that make use of the support for
# third party headers may override this method to return +true+.
# When this is done, headers that are matched by the native_header_regex
# are not written back to disk.
# @see native_header_regex
def self.drop_native_header
false
end
# Add another type var.
def self.initvars
@records = []
@target_objects = {}
@target = nil
# Default to flat files
@filetype ||= Puppet::Util::FileType.filetype(:flat)
super
end
# Return a list of all of the records we can find.
def self.instances
targets.collect do |target|
prefetch_target(target)
end.flatten.reject { |r| skip_record?(r) }.collect do |record|
new(record)
end
end
# Override the default method with a lot more functionality.
def self.mk_resource_methods
[resource_type.validproperties, resource_type.parameters].flatten.each do |attr|
attr = attr.intern
define_method(attr) do
# If it's not a valid field for this record type (which can happen
# when different platforms support different fields), then just
# return the should value, so the resource shuts up.
if @property_hash[attr] or self.class.valid_attr?(self.class.name, attr)
@property_hash[attr] || :absent
else
if defined?(@resource)
@resource.should(attr)
else
nil
end
end
end
define_method(attr.to_s + "=") do |val|
mark_target_modified
@property_hash[attr] = val
end
end
end
# Always make the resource methods.
def self.resource_type=(resource)
super
mk_resource_methods
end
# Mark a target as modified so we know to flush it. This only gets
# used within the attr= methods.
def self.modified(target)
@modified ||= []
@modified << target unless @modified.include?(target)
end
# Retrieve all of the data from disk. There are three ways to know
# which files to retrieve: We might have a list of file objects already
# set up, there might be instances of our associated resource and they
# will have a path parameter set, and we will have a default path
# set. We need to turn those three locations into a list of files,
# prefetch each one, and make sure they're associated with each appropriate
# resource instance.
def self.prefetch(resources = nil)
# Reset the record list.
@records = prefetch_all_targets(resources)
match_providers_with_resources(resources)
end
# Match a list of catalog resources with provider instances
#
# @api private
#
# @param [Array<Puppet::Resource>] resources A list of resources using this class as a provider
def self.match_providers_with_resources(resources)
return unless resources
matchers = resources.dup
@records.each do |record|
# Skip things like comments and blank lines
next if skip_record?(record)
if (resource = resource_for_record(record, resources))
resource.provider = new(record)
elsif respond_to?(:match)
if resource = match(record, matchers)
matchers.delete(resource.title)
record[:name] = resource[:name]
resource.provider = new(record)
end
end
end
end
# Look up a resource based on a parsed file record
#
# @api private
#
# @param [Hash<Symbol, Object>] record
# @param [Array<Puppet::Resource>] resources
#
# @return [Puppet::Resource, nil] The resource if found, else nil
def self.resource_for_record(record, resources)
name = record[:name]
if name
resources[name]
end
end
def self.prefetch_all_targets(resources)
records = []
targets(resources).each do |target|
records += prefetch_target(target)
end
records
end
# Prefetch an individual target.
def self.prefetch_target(target)
begin
target_records = retrieve(target)
rescue Puppet::Util::FileType::FileReadError => detail
puts detail.backtrace if Puppet[:trace]
Puppet.err "Could not prefetch #{self.resource_type.name} provider '#{self.name}' target '#{target}': #{detail}. Treating as empty"
target_records = []
end
target_records.each do |r|
r[:on_disk] = true
r[:target] = target
r[:ensure] = :present
end
target_records = prefetch_hook(target_records) if respond_to?(:prefetch_hook)
raise Puppet::DevError, "Prefetching #{target} for provider #{self.name} returned nil" unless target_records
target_records
end
# Is there an existing record with this name?
def self.record?(name)
return nil unless @records
@records.find { |r| r[:name] == name }
end
# Retrieve the text for the file. Returns nil in the unlikely
# event that it doesn't exist.
def self.retrieve(path)
# XXX We need to be doing something special here in case of failure.
text = target_object(path).read
if text.nil? or text == ""
# there is no file
return []
else
# Set the target, for logging.
old = @target
begin
@target = path
return self.parse(text)
rescue Puppet::Error => detail
- detail.file = @target
+ detail.file = @target if detail.respond_to?(:file=)
raise detail
ensure
@target = old
end
end
end
# Should we skip the record? Basically, we skip text records.
# This is only here so subclasses can override it.
def self.skip_record?(record)
record_type(record[:record_type]).text?
end
# Initialize the object if necessary.
def self.target_object(target)
@target_objects[target] ||= filetype.new(target)
@target_objects[target]
end
# Find all of the records for a given target
def self.target_records(target)
@records.find_all { |r| r[:target] == target }
end
# Find a list of all of the targets that we should be reading. This is
# used to figure out what targets we need to prefetch.
def self.targets(resources = nil)
targets = []
# First get the default target
raise Puppet::DevError, "Parsed Providers must define a default target" unless self.default_target
targets << self.default_target
# Then get each of the file objects
targets += @target_objects.keys
# Lastly, check the file from any resource instances
if resources
resources.each do |name, resource|
if value = resource.should(:target)
targets << value
end
end
end
targets.uniq.compact
end
# Compose file contents from the set of records.
#
# If self.native_header_regex is not nil, possible vendor headers are
# identified by matching the return value against the expression.
# If one (or several consecutive) such headers, are found, they are
# either moved in front of the self.header if self.drop_native_header
# is false (this is the default), or removed from the return value otherwise.
#
# @api private
def self.to_file(records)
text = super
if native_header_regex and (match = text.match(native_header_regex))
if drop_native_header
# concatenate the text in front of and after the native header
text = match.pre_match + match.post_match
else
native_header = match[0]
return native_header + header + match.pre_match + match.post_match
end
end
header + text
end
def create
@resource.class.validproperties.each do |property|
if value = @resource.should(property)
@property_hash[property] = value
end
end
mark_target_modified
(@resource.class.name.to_s + "_created").intern
end
def destroy
# We use the method here so it marks the target as modified.
self.ensure = :absent
(@resource.class.name.to_s + "_deleted").intern
end
def exists?
!(@property_hash[:ensure] == :absent or @property_hash[:ensure].nil?)
end
# Write our data to disk.
def flush
# Make sure we've got a target and name set.
# If the target isn't set, then this is our first modification, so
# mark it for flushing.
unless @property_hash[:target]
@property_hash[:target] = @resource.should(:target) || self.class.default_target
self.class.modified(@property_hash[:target])
end
@resource.class.key_attributes.each do |attr|
@property_hash[attr] ||= @resource[attr]
end
self.class.flush(@property_hash)
end
def initialize(record)
super
# The 'record' could be a resource or a record, depending on how the provider
# is initialized. If we got an empty property hash (probably because the resource
# is just being initialized), then we want to set up some defaults.
@property_hash = self.class.record?(resource[:name]) || {:record_type => self.class.name, :ensure => :absent} if @property_hash.empty?
end
# Retrieve the current state from disk.
def prefetch
raise Puppet::DevError, "Somehow got told to prefetch with no resource set" unless @resource
self.class.prefetch(@resource[:name] => @resource)
end
def record_type
@property_hash[:record_type]
end
private
# Mark both the resource and provider target as modified.
def mark_target_modified
if defined?(@resource) and restarget = @resource.should(:target) and restarget != @property_hash[:target]
self.class.modified(restarget)
end
self.class.modified(@property_hash[:target]) if @property_hash[:target] != :absent and @property_hash[:target]
end
end
diff --git a/lib/puppet/provider/port/parsed.rb b/lib/puppet/provider/port/parsed.rb
deleted file mode 100644
index 5c973b6af..000000000
--- a/lib/puppet/provider/port/parsed.rb
+++ /dev/null
@@ -1,173 +0,0 @@
-require 'puppet/provider/parsedfile'
-
-#services = nil
-#case Facter.value(:operatingsystem)
-#when "Solaris"; services = "/etc/inet/services"
-#else
-# services = "/etc/services"
-#end
-#
-#Puppet::Type.type(:port).provide(:parsed,
-# :parent => Puppet::Provider::ParsedFile,
-# :default_target => services,
-# :filetype => :flat
-#) do
-# text_line :comment, :match => /^\s*#/
-# text_line :blank, :match => /^\s*$/
-#
-# # We're cheating horribly here -- we don't support ddp, because it assigns
-# # the same number to already-used names, and the same name to different
-# # numbers.
-# text_line :ddp, :match => /^\S+\s+\d+\/ddp/
-#
-# # Also, just ignore the lines on OS X that don't have service names.
-# text_line :funky_darwin, :match => /^\s+\d+\//
-#
-# # We have to manually parse the line, since it's so darn complicated.
-# record_line :parsed, :fields => %w{name port protocols alias description},
-# :optional => %w{alias description} do |line|
-# if line =~ /\/ddp/
-# raise "missed ddp in #{line}"
-# end
-# # The record might contain multiple port lines separated by \n.
-# hashes = line.split("\n").collect { |l| parse_port(l) }
-#
-# # It's easy if there's just one hash.
-# if hashes.length == 1
-# return hashes.shift
-# end
-#
-# # Else, merge the two records into one.
-# return port_merge(*hashes)
-# end
-#
-# # Override how we split into lines, so that we always treat both protocol
-# # lines as a single line. This drastically simplifies merging the two lines
-# # into one record.
-# def self.lines(text)
-# names = {}
-# lines = []
-#
-# # We organize by number, because that's apparently how the ports work.
-# # You'll never be able to use Puppet to manage multiple entries
-# # with the same name but different numbers, though.
-# text.split("\n").each do |line|
-# if line =~ /^([-\w]+)\s+(\d+)\/[^d]/ # We want to skip ddp proto stuff
-# names[$1] ||= []
-# names[$1] << line
-# lines << [:special, $1]
-# else
-# lines << line
-# end
-# end
-#
-# # Now, return each line in order, but join the ones with the same name
-# lines.collect do |line|
-# if line.is_a?(Array)
-# name = line[1]
-# if names[name]
-# t = names[name].join("\n")
-# names.delete(name)
-# t
-# end
-# else
-# line
-# end
-# end.reject { |l| l.nil? }
-# end
-#
-# # Parse a single port line, returning a hash.
-# def self.parse_port(line)
-# hash = {}
-# if line.sub!(/^(\S+)\s+(\d+)\/(\w+)\s*/, '')
-# hash[:name] = $1
-# hash[:number] = $2
-# hash[:protocols] = [$3]
-#
-# unless line == ""
-# line.sub!(/^([^#]+)\s*/) do |value|
-# aliases = $1
-#
-# # Remove any trailing whitespace
-# aliases.strip!
-# unless aliases =~ /^\s*$/
-# hash[:alias] = aliases.split(/\s+/)
-# end
-#
-# ""
-# end
-#
-# line.sub!(/^\s*#\s*(.+)$/) do |value|
-# desc = $1
-# unless desc =~ /^\s*$/
-# hash[:description] = desc.sub(/\s*$/, '')
-# end
-#
-# ""
-# end
-# end
-# else
-# if line =~ /^\s+\d+/ and
-# Facter["operatingsystem"].value == "Darwin"
-# #Puppet.notice "Skipping wonky OS X port entry %s" %
-# # line.inspect
-# next
-# end
-# Puppet.notice "Ignoring unparseable line '#{line}' in #{self.target}"
-# end
-#
-# if hash.empty?
-# return nil
-# else
-# return hash
-# end
-# end
-#
-# # Merge two records into one.
-# def self.port_merge(one, two)
-# keys = [one.keys, two.keys].flatten.uniq
-#
-# # We'll be returning the 'one' hash. so make any necessary modifications
-# # to it.
-# keys.each do |key|
-# # The easy case
-# if one[key] == two[key]
-# next
-# elsif one[key] and ! two[key]
-# next
-# elsif ! one[key] and two[key]
-# one[key] = two[key]
-# elsif one[key].is_a?(Array) and two[key].is_a?(Array)
-# one[key] = [one[key], two[key]].flatten.uniq
-# else
-# # Keep the info from the first hash, so don't do anything
-# #Puppet.notice "Cannot merge %s in %s with %s" %
-# # [key, one.inspect, two.inspect]
-# end
-# end
-#
-# return one
-# end
-#
-# # Convert the current object into one or more services entry.
-# def self.to_line(hash)
-# unless hash[:record_type] == :parsed
-# return super
-# end
-#
-# # Strangely, most sites seem to use tabs as separators.
-# hash[:protocols].collect { |proto|
-# str = "#{hash[:name]}\t\t#{hash[:number]}/#{proto}"
-#
-# if value = hash[:alias] and value != :absent
-# str += "\t\t#{value.join(" ")}"
-# end
-#
-# if value = hash[:description] and value != :absent
-# str += "\t# #{value}"
-# end
-# str
-# }.join("\n")
-# end
-#end
-
diff --git a/lib/puppet/provider/selboolean/getsetsebool.rb b/lib/puppet/provider/selboolean/getsetsebool.rb
index cacc41386..d8823fe14 100644
--- a/lib/puppet/provider/selboolean/getsetsebool.rb
+++ b/lib/puppet/provider/selboolean/getsetsebool.rb
@@ -1,47 +1,47 @@
Puppet::Type.type(:selboolean).provide(:getsetsebool) do
desc "Manage SELinux booleans using the getsebool and setsebool binaries."
commands :getsebool => "/usr/sbin/getsebool"
commands :setsebool => "/usr/sbin/setsebool"
def value
self.debug "Retrieving value of selboolean #{@resource[:name]}"
status = getsebool(@resource[:name])
if status =~ / off$/
return :off
elsif status =~ / on$/ then
return :on
else
status.chomp!
raise Puppet::Error, "Invalid response '#{status}' returned from getsebool"
end
end
def value=(new)
persist = ""
if @resource[:persistent] == :true
self.debug "Enabling persistence"
persist = "-P"
end
execoutput("#{command(:setsebool)} #{persist} #{@resource[:name]} #{new}")
:file_changed
end
# Required workaround, since SELinux policy prevents setsebool
# from writing to any files, even tmp, preventing the standard
# 'setsebool("...")' construct from working.
def execoutput (cmd)
output = ''
begin
execpipe(cmd) do |out|
output = out.readlines.join('').chomp!
end
rescue Puppet::ExecutionFailure
- raise Puppet::ExecutionFailure, output.split("\n")[0]
+ raise Puppet::ExecutionFailure, output.split("\n")[0], $!.backtrace
end
output
end
end
diff --git a/lib/puppet/provider/selmodule/semodule.rb b/lib/puppet/provider/selmodule/semodule.rb
index ba864b783..dc9a77d25 100644
--- a/lib/puppet/provider/selmodule/semodule.rb
+++ b/lib/puppet/provider/selmodule/semodule.rb
@@ -1,134 +1,134 @@
Puppet::Type.type(:selmodule).provide(:semodule) do
desc "Manage SELinux policy modules using the semodule binary."
commands :semodule => "/usr/sbin/semodule"
def create
begin
execoutput("#{command(:semodule)} --install #{selmod_name_to_filename}")
rescue Puppet::ExecutionFailure => detail
- raise Puppet::Error, "Could not load policy module: #{detail}";
+ raise Puppet::Error, "Could not load policy module: #{detail}", detail.backtrace
end
:true
end
def destroy
execoutput("#{command(:semodule)} --remove #{@resource[:name]}")
rescue Puppet::ExecutionFailure => detail
- raise Puppet::Error, "Could not remove policy module: #{detail}";
+ raise Puppet::Error, "Could not remove policy module: #{detail}", detail.backtrace
end
def exists?
self.debug "Checking for module #{@resource[:name]}"
execpipe("#{command(:semodule)} --list") do |out|
out.each_line do |line|
if line =~ /#{@resource[:name]}\b/
return :true
end
end
end
nil
end
def syncversion
self.debug "Checking syncversion on #{@resource[:name]}"
loadver = selmodversion_loaded
if(loadver) then
filever = selmodversion_file
if (filever == loadver)
return :true
end
end
:false
end
def syncversion= (dosync)
execoutput("#{command(:semodule)} --upgrade #{selmod_name_to_filename}")
rescue Puppet::ExecutionFailure => detail
- raise Puppet::Error, "Could not upgrade policy module: #{detail}";
+ raise Puppet::Error, "Could not upgrade policy module: #{detail}", detail.backtrace
end
# Helper functions
def execoutput (cmd)
output = ''
begin
execpipe(cmd) do |out|
output = out.readlines.join('').chomp!
end
rescue Puppet::ExecutionFailure
- raise Puppet::ExecutionFailure, output.split("\n")[0]
+ raise Puppet::ExecutionFailure, output.split("\n")[0], $!.backtrace
end
output
end
def selmod_name_to_filename
if @resource[:selmodulepath]
return @resource[:selmodulepath]
else
return "#{@resource[:selmoduledir]}/#{@resource[:name]}.pp"
end
end
def selmod_readnext (handle)
len = handle.read(4).unpack('V')[0]
handle.read(len)
end
def selmodversion_file
magic = 0xF97CFF8F
filename = selmod_name_to_filename
mod = File.new(filename, "r")
(hdr, ver, numsec) = mod.read(12).unpack('VVV')
raise Puppet::Error, "Found #{hdr} instead of magic #{magic} in #{filename}" if hdr != magic
raise Puppet::Error, "Unknown policy file version #{ver} in #{filename}" if ver != 1
# Read through (and throw away) the file section offsets, and also
# the magic header for the first section.
mod.read((numsec + 1) * 4)
## Section 1 should be "SE Linux Module"
selmod_readnext(mod)
selmod_readnext(mod)
# Skip past the section headers
mod.read(14)
# Module name
selmod_readnext(mod)
# At last! the version
v = selmod_readnext(mod)
self.debug "file version #{v}"
v
end
def selmodversion_loaded
lines = ()
begin
execpipe("#{command(:semodule)} --list") do |output|
output.each_line do |line|
line.chomp!
bits = line.split
if bits[0] == @resource[:name]
self.debug "load version #{bits[1]}"
return bits[1]
end
end
end
rescue Puppet::ExecutionFailure
- raise Puppet::ExecutionFailure, "Could not list policy modules: #{lines.join(' ').chomp!}"
+ raise Puppet::ExecutionFailure, "Could not list policy modules: #{lines.join(' ').chomp!}", $!.backtrace
end
nil
end
end
diff --git a/lib/puppet/provider/service/base.rb b/lib/puppet/provider/service/base.rb
index 0d960854f..9e37d1f61 100644
--- a/lib/puppet/provider/service/base.rb
+++ b/lib/puppet/provider/service/base.rb
@@ -1,106 +1,106 @@
Puppet::Type.type(:service).provide :base, :parent => :service do
desc "The simplest form of Unix service support.
You have to specify enough about your service for this to work; the
minimum you can specify is a binary for starting the process, and this
same binary will be searched for in the process table to stop the
service. As with `init`-style services, it is preferable to specify start,
stop, and status commands.
"
commands :kill => "kill"
# Get the process ID for a running process. Requires the 'pattern'
# parameter.
def getpid
@resource.fail "Either stop/status commands or a pattern must be specified" unless @resource[:pattern]
ps = Facter["ps"].value
@resource.fail "You must upgrade Facter to a version that includes 'ps'" unless ps and ps != ""
regex = Regexp.new(@resource[:pattern])
self.debug "Executing '#{ps}'"
IO.popen(ps) { |table|
table.each_line { |line|
if regex.match(line)
self.debug "Process matched: #{line}"
ary = line.sub(/^\s+/, '').split(/\s+/)
return ary[1]
end
}
}
nil
end
# Check if the process is running. Prefer the 'status' parameter,
# then 'statuscmd' method, then look in the process table. We give
# the object the option to not return a status command, which might
# happen if, for instance, it has an init script (and thus responds to
# 'statuscmd') but does not have 'hasstatus' enabled.
def status
if @resource[:status] or statuscmd
# Don't fail when the exit status is not 0.
ucommand(:status, false)
# Expicitly calling exitstatus to facilitate testing
if $CHILD_STATUS.exitstatus == 0
return :running
else
return :stopped
end
elsif pid = self.getpid
self.debug "PID is #{pid}"
return :running
else
return :stopped
end
end
# There is no default command, which causes other methods to be used
def statuscmd
end
# Run the 'start' parameter command, or the specified 'startcmd'.
def start
ucommand(:start)
end
# The command used to start. Generated if the 'binary' argument
# is passed.
def startcmd
if @resource[:binary]
return @resource[:binary]
else
raise Puppet::Error,
"Services must specify a start command or a binary"
end
end
# Stop the service. If a 'stop' parameter is specified, it
# takes precedence; otherwise checks if the object responds to
# a 'stopcmd' method, and if so runs that; otherwise, looks
# for the process in the process table.
# This method will generally not be overridden by submodules.
def stop
if @resource[:stop] or stopcmd
ucommand(:stop)
else
pid = getpid
unless pid
self.info "#{self.name} is not running"
return false
end
begin
output = kill pid
rescue Puppet::ExecutionFailure
- @resource.fail "Could not kill #{self.name}, PID #{pid}: #{output}"
+ @resource.fail Puppet::Error, "Could not kill #{self.name}, PID #{pid}: #{output}", $!
end
return true
end
end
# There is no default command, which causes other methods to be used
def stopcmd
end
end
diff --git a/lib/puppet/provider/service/bsd.rb b/lib/puppet/provider/service/bsd.rb
index a19608a51..d779aad12 100644
--- a/lib/puppet/provider/service/bsd.rb
+++ b/lib/puppet/provider/service/bsd.rb
@@ -1,51 +1,51 @@
# Manage FreeBSD services.
Puppet::Type.type(:service).provide :bsd, :parent => :init do
desc <<-EOT
FreeBSD's (and probably NetBSD's?) form of `init`-style service management.
Uses `rc.conf.d` for service enabling and disabling.
EOT
confine :operatingsystem => [:freebsd, :netbsd, :openbsd, :dragonfly]
def rcconf_dir
'/etc/rc.conf.d'
end
def self.defpath
superclass.defpath
end
# remove service file from rc.conf.d to disable it
def disable
rcfile = File.join(rcconf_dir, @model[:name])
- File.delete(rcfile) if Puppet::FileSystem::File.exist?(rcfile)
+ File.delete(rcfile) if Puppet::FileSystem.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 Puppet::FileSystem::File.exist?(rcfile)
+ return :true if Puppet::FileSystem.exist?(rcfile)
:false
end
# enable service by creating a service file under rc.conf.d with the
# proper contents
def enable
- Dir.mkdir(rcconf_dir) if not Puppet::FileSystem::File.exist?(rcconf_dir)
+ Dir.mkdir(rcconf_dir) if not Puppet::FileSystem.exist?(rcconf_dir)
rcfile = File.join(rcconf_dir, @model[:name])
open(rcfile, 'w') { |f| f << "%s_enable=\"YES\"\n" % @model[:name] }
end
# Override stop/start commands to use one<cmd>'s and the avoid race condition
# where provider trys to stop/start the service before it is enabled
def startcmd
[self.initscript, :onestart]
end
def stopcmd
[self.initscript, :onestop]
end
end
diff --git a/lib/puppet/provider/service/daemontools.rb b/lib/puppet/provider/service/daemontools.rb
index 32c0e0ff7..b510f2535 100644
--- a/lib/puppet/provider/service/daemontools.rb
+++ b/lib/puppet/provider/service/daemontools.rb
@@ -1,194 +1,194 @@
# Daemontools service management
#
# author Brice Figureau <brice-puppet@daysofwonder.com>
Puppet::Type.type(:service).provide :daemontools, :parent => :base do
desc <<-'EOT'
Daemontools service management.
This provider manages daemons supervised by D.J. Bernstein daemontools.
When detecting the service directory it will check, in order of preference:
* `/service`
* `/etc/service`
* `/var/lib/svscan`
The daemon directory should be in one of the following locations:
* `/var/lib/service`
* `/etc`
...or this can be overriden in the resource's attributes:
service { "myservice":
provider => "daemontools",
path => "/path/to/daemons",
}
This provider supports out of the box:
* start/stop (mapped to enable/disable)
* enable/disable
* restart
* status
If a service has `ensure => "running"`, it will link /path/to/daemon to
/path/to/service, which will automatically enable the service.
If a service has `ensure => "stopped"`, it will only shut down the service, not
remove the `/path/to/service` link.
EOT
commands :svc => "/usr/bin/svc", :svstat => "/usr/bin/svstat"
class << self
attr_writer :defpath
# Determine the daemon path.
def defpath(dummy_argument=:work_arround_for_ruby_GC_bug)
unless @defpath
["/var/lib/service", "/etc"].each do |path|
- if Puppet::FileSystem::File.exist?(path)
+ if Puppet::FileSystem.exist?(path)
@defpath = path
break
end
end
raise "Could not find the daemon directory (tested [/var/lib/service,/etc])" unless @defpath
end
@defpath
end
end
attr_writer :servicedir
# returns all providers for all existing services in @defpath
# ie enabled or not
def self.instances
path = self.defpath
unless FileTest.directory?(path)
Puppet.notice "Service path #{path} does not exist"
return
end
# reject entries that aren't either a directory
# or don't contain a run file
Dir.entries(path).reject { |e|
fullpath = File.join(path, e)
- e =~ /^\./ or ! FileTest.directory?(fullpath) or ! Puppet::FileSystem::File.exist?(File.join(fullpath,"run"))
+ e =~ /^\./ or ! FileTest.directory?(fullpath) or ! Puppet::FileSystem.exist?(File.join(fullpath,"run"))
}.collect do |name|
new(:name => name, :path => path)
end
end
# returns the daemon dir on this node
def self.daemondir
self.defpath
end
# find the service dir on this node
def servicedir
unless @servicedir
["/service", "/etc/service","/var/lib/svscan"].each do |path|
- if Puppet::FileSystem::File.exist?(path)
+ if Puppet::FileSystem.exist?(path)
@servicedir = path
break
end
end
raise "Could not find service directory" unless @servicedir
end
@servicedir
end
# returns the full path of this service when enabled
# (ie in the service directory)
def service
File.join(self.servicedir, resource[:name])
end
# returns the full path to the current daemon directory
# note that this path can be overriden in the resource
# definition
def daemon
File.join(resource[:path], resource[:name])
end
def status
begin
output = svstat self.service
if output =~ /:\s+up \(/
return :running
end
rescue Puppet::ExecutionFailure => detail
- raise Puppet::Error.new( "Could not get status for service #{resource.ref}: #{detail}" )
+ raise Puppet::Error.new( "Could not get status for service #{resource.ref}: #{detail}", detail)
end
:stopped
end
def setupservice
if resource[:manifest]
Puppet.notice "Configuring #{resource[:name]}"
command = [ resource[:manifest], resource[:name] ]
#texecute("setupservice", command)
system("#{command}")
end
rescue Puppet::ExecutionFailure => detail
- raise Puppet::Error.new( "Cannot config #{self.service} to enable it: #{detail}" )
+ raise Puppet::Error.new( "Cannot config #{self.service} to enable it: #{detail}", detail)
end
def enabled?
case self.status
when :running
# obviously if the daemon is running then it is enabled
return :true
else
# the service is enabled if it is linked
- return Puppet::FileSystem::File.new(self.service).symlink? ? :true : :false
+ return Puppet::FileSystem.symlink?(self.service) ? :true : :false
end
end
def enable
- if ! FileTest.directory?(self.daemon)
- Puppet.notice "No daemon dir, calling setupservice for #{resource[:name]}"
- self.setupservice
- end
- if self.daemon
- if ! Puppet::FileSystem::File.new(self.service).symlink?
- Puppet.notice "Enabling #{self.service}: linking #{self.daemon} -> #{self.service}"
- Puppet::FileSystem::File.new(self.daemon).symlink(self.service)
- end
+ if ! FileTest.directory?(self.daemon)
+ Puppet.notice "No daemon dir, calling setupservice for #{resource[:name]}"
+ self.setupservice
+ end
+ if self.daemon
+ if ! Puppet::FileSystem.symlink?(self.service)
+ Puppet.notice "Enabling #{self.service}: linking #{self.daemon} -> #{self.service}"
+ Puppet::FileSystem.symlink(self.daemon, self.service)
end
- rescue Puppet::ExecutionFailure
- raise Puppet::Error.new( "No daemon directory found for #{self.service}")
+ end
+rescue Puppet::ExecutionFailure
+ raise Puppet::Error.new( "No daemon directory found for #{self.service}", $!)
end
def disable
begin
if ! FileTest.directory?(self.daemon)
Puppet.notice "No daemon dir, calling setupservice for #{resource[:name]}"
self.setupservice
end
if self.daemon
- if Puppet::FileSystem::File.new(self.service).symlink?
+ if Puppet::FileSystem.symlink?(self.service)
Puppet.notice "Disabling #{self.service}: removing link #{self.daemon} -> #{self.service}"
- Puppet::FileSystem::File.unlink(self.service)
+ Puppet::FileSystem.unlink(self.service)
end
end
rescue Puppet::ExecutionFailure
- raise Puppet::Error.new( "No daemon directory found for #{self.service}")
+ raise Puppet::Error.new( "No daemon directory found for #{self.service}", $!)
end
self.stop
end
def restart
svc "-t", self.service
end
def start
enable unless enabled? == :true
svc "-u", self.service
end
def stop
svc "-d", self.service
end
end
diff --git a/lib/puppet/provider/service/debian.rb b/lib/puppet/provider/service/debian.rb
index 7d14eaa3c..5c0105092 100644
--- a/lib/puppet/provider/service/debian.rb
+++ b/lib/puppet/provider/service/debian.rb
@@ -1,64 +1,64 @@
# Manage debian services. Start/stop is the same as InitSvc, but enable/disable
# is special.
Puppet::Type.type(:service).provide :debian, :parent => :init do
desc <<-EOT
Debian's form of `init`-style management.
The only differences from `init` are support for enabling and disabling
services via `update-rc.d` and the ability to determine enabled status via
`invoke-rc.d`.
EOT
commands :update_rc => "/usr/sbin/update-rc.d"
# note this isn't being used as a command until
# http://projects.reductivelabs.com/issues/2538
# is resolved.
commands :invoke_rc => "/usr/sbin/invoke-rc.d"
defaultfor :operatingsystem => [:debian, :ubuntu]
# Remove the symlinks
def disable
if `dpkg --compare-versions $(dpkg-query -W --showformat '${Version}' sysv-rc) ge 2.88 ; echo $?`.to_i == 0
update_rc @resource[:name], "disable"
else
update_rc "-f", @resource[:name], "remove"
update_rc @resource[:name], "stop", "00", "1", "2", "3", "4", "5", "6", "."
end
end
def enabled?
# TODO: Replace system call when Puppet::Util::Execution.execute gives us a way
# to determine exit status. http://projects.reductivelabs.com/issues/2538
system("/usr/sbin/invoke-rc.d", "--quiet", "--query", @resource[:name], "start")
# 104 is the exit status when you query start an enabled service.
# 106 is the exit status when the policy layer supplies a fallback action
# See x-man-page://invoke-rc.d
if [104, 106].include?($CHILD_STATUS.exitstatus)
return :true
elsif [105].include?($CHILD_STATUS.exitstatus)
- # 105 is unknown, which generally means the the iniscript does not support query
+ # 105 is unknown, which generally means the iniscript does not support query
# The debian policy states that the initscript should support methods of query
# For those that do not, peform the checks manually
# http://www.debian.org/doc/debian-policy/ch-opersys.html
if get_start_link_count >= 4
return :true
else
return :false
end
else
return :false
end
end
def get_start_link_count
Dir.glob("/etc/rc*.d/S??#{@resource[:name]}").length
end
def enable
update_rc "-f", @resource[:name], "remove"
update_rc @resource[:name], "defaults"
end
end
diff --git a/lib/puppet/provider/service/freebsd.rb b/lib/puppet/provider/service/freebsd.rb
index 36de850c5..1e59fc47d 100644
--- a/lib/puppet/provider/service/freebsd.rb
+++ b/lib/puppet/provider/service/freebsd.rb
@@ -1,143 +1,143 @@
Puppet::Type.type(:service).provide :freebsd, :parent => :init do
desc "Provider for FreeBSD and DragonFly BSD. Uses the `rcvar` argument of init scripts and parses/edits rc files."
confine :operatingsystem => [:freebsd, :dragonfly]
defaultfor :operatingsystem => [:freebsd, :dragonfly]
def rcconf() '/etc/rc.conf' end
def rcconf_local() '/etc/rc.conf.local' end
def rcconf_dir() '/etc/rc.conf.d' end
def self.defpath
superclass.defpath
end
def error(msg)
raise Puppet::Error, msg
end
# Executing an init script with the 'rcvar' argument returns
# the service name, rcvar name and whether it's enabled/disabled
def rcvar
rcvar = execute([self.initscript, :rcvar], :failonfail => true, :combine => false, :squelch => false)
rcvar = rcvar.split("\n")
rcvar.delete_if {|str| str =~ /^#\s*$/}
rcvar[1] = rcvar[1].gsub(/^\$/, '')
rcvar
end
# Extract service name
def service_name
name = self.rcvar[0]
self.error("No service name found in rcvar") if name.nil?
name = name.gsub!(/# (.*)/, '\1')
self.error("Service name is empty") if name.nil?
self.debug("Service name is #{name}")
name
end
# Extract rcvar name
def rcvar_name
name = self.rcvar[1]
self.error("No rcvar name found in rcvar") if name.nil?
name = name.gsub!(/(.*?)(_enable)?=(.*)/, '\1')
self.error("rcvar name is empty") if name.nil?
self.debug("rcvar name is #{name}")
name
end
# Extract rcvar value
def rcvar_value
value = self.rcvar[1]
self.error("No rcvar value found in rcvar") if value.nil?
value = value.gsub!(/(.*)(_enable)?="?(\w+)"?/, '\3')
self.error("rcvar value is empty") if value.nil?
self.debug("rcvar value is #{value}")
value
end
# Edit rc files and set the service to yes/no
def rc_edit(yesno)
service = self.service_name
rcvar = self.rcvar_name
self.debug("Editing rc files: setting #{rcvar} to #{yesno} for #{service}")
self.rc_add(service, rcvar, yesno) if not self.rc_replace(service, rcvar, yesno)
end
# Try to find an existing setting in the rc files
# and replace the value
def rc_replace(service, rcvar, yesno)
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 Puppet::FileSystem::File.exist?(filename)
+ if Puppet::FileSystem.exist?(filename)
s = File.read(filename)
if s.gsub!(/^(#{rcvar}(_enable)?)=\"?(YES|NO)\"?/, "\\1=\"#{yesno}\"")
File.open(filename, File::WRONLY) { |f| f << s }
self.debug("Replaced in #{filename}")
success = true
end
end
end
success
end
# Add a new setting to the rc files
def rc_add(service, rcvar, yesno)
append = "\# Added by Puppet\n#{rcvar}_enable=\"#{yesno}\"\n"
# First, try the one-file-per-service style
- if Puppet::FileSystem::File.exist?(rcconf_dir)
+ if Puppet::FileSystem.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 Puppet::FileSystem::File.exist?(rcconf_local)
+ if Puppet::FileSystem.exist?(rcconf_local)
File.open(rcconf_local, File::WRONLY | File::APPEND) {
|f| f << append
self.debug("Appended to #{f.path}")
}
else
# At last use the standard rc.conf file
File.open(rcconf, File::WRONLY | File::APPEND | File::CREAT, 0644) {
|f| f << append
self.debug("Appended to #{f.path}")
}
end
end
end
def enabled?
if /YES$/ =~ self.rcvar_value
self.debug("Is enabled")
return :true
end
self.debug("Is disabled")
:false
end
def enable
self.debug("Enabling")
self.rc_edit("YES")
end
def disable
self.debug("Disabling")
self.rc_edit("NO")
end
def startcmd
[self.initscript, :onestart]
end
def stopcmd
[self.initscript, :onestop]
end
def statuscmd
[self.initscript, :onestatus]
end
end
diff --git a/lib/puppet/provider/service/gentoo.rb b/lib/puppet/provider/service/gentoo.rb
index f4750eb71..4fecbde9d 100644
--- a/lib/puppet/provider/service/gentoo.rb
+++ b/lib/puppet/provider/service/gentoo.rb
@@ -1,45 +1,45 @@
# Manage gentoo services. Start/stop is the same as InitSvc, but enable/disable
# is special.
Puppet::Type.type(:service).provide :gentoo, :parent => :init do
desc <<-EOT
Gentoo's form of `init`-style service management.
Uses `rc-update` for service enabling and disabling.
EOT
commands :update => "/sbin/rc-update"
confine :operatingsystem => :gentoo
def disable
output = update :del, @resource[:name], :default
rescue Puppet::ExecutionFailure
- raise Puppet::Error, "Could not disable #{self.name}: #{output}"
+ raise Puppet::Error, "Could not disable #{self.name}: #{output}", $!.backtrace
end
def enabled?
begin
output = update :show
rescue Puppet::ExecutionFailure
return :false
end
line = output.split(/\n/).find { |l| l.include?(@resource[:name]) }
return :false unless line
# If it's enabled then it will print output showing service | runlevel
if output =~ /^\s*#{@resource[:name]}\s*\|\s*(boot|default)/
return :true
else
return :false
end
end
def enable
output = update :add, @resource[:name], :default
rescue Puppet::ExecutionFailure
- raise Puppet::Error, "Could not enable #{self.name}: #{output}"
+ raise Puppet::Error, "Could not enable #{self.name}: #{output}", $!.backtrace
end
end
diff --git a/lib/puppet/provider/service/init.rb b/lib/puppet/provider/service/init.rb
index d46cc8772..291cc5b06 100644
--- a/lib/puppet/provider/service/init.rb
+++ b/lib/puppet/provider/service/init.rb
@@ -1,161 +1,161 @@
# The standard init-based service type. Many other service types are
# customizations of this module.
Puppet::Type.type(:service).provide :init, :parent => :base do
desc "Standard `init`-style service management."
def self.defpath
case Facter.value(:operatingsystem)
when "FreeBSD", "DragonFly"
["/etc/rc.d", "/usr/local/etc/rc.d"]
when "HP-UX"
"/sbin/init.d"
when "Archlinux"
"/etc/rc.d"
else
"/etc/init.d"
end
end
# We can't confine this here, because the init path can be overridden.
#confine :exists => defpath
# some init scripts are not safe to execute, e.g. we do not want
# to suddently run /etc/init.d/reboot.sh status and reboot our system. The
# exclude list could be platform agnostic but I assume an invalid init script
# on system A will never be a valid init script on system B
def self.excludes
excludes = []
# these exclude list was found with grep -L '\/sbin\/runscript' /etc/init.d/* on gentoo
excludes += %w{functions.sh reboot.sh shutdown.sh}
# this exclude list is all from /sbin/service (5.x), but I did not exclude kudzu
excludes += %w{functions halt killall single linuxconf reboot boot}
# 'wait-for-state' and 'portmap-wait' are excluded from instances here
# because they take parameters that have unclear meaning. It looks like
# 'wait-for-state' is a generic waiter mainly used internally for other
# upstart services as a 'sleep until something happens'
# (http://lists.debian.org/debian-devel/2012/02/msg01139.html), while
# 'portmap-wait' is a specific instance of a waiter. There is an open
# launchpad bug
# (https://bugs.launchpad.net/ubuntu/+source/upstart/+bug/962047) that may
# eventually explain how to use the wait-for-state service or perhaps why
# it should remain excluded. When that bug is adddressed this should be
# reexamined.
excludes += %w{wait-for-state portmap-wait}
# these excludes were found with grep -r -L start /etc/init.d
excludes += %w{rcS module-init-tools}
# Prevent puppet failing to get status of the new service introduced
# by the fix for this (bug https://bugs.launchpad.net/ubuntu/+source/lightdm/+bug/982889)
# due to puppet's inability to deal with upstart services with instances.
excludes += %w{plymouth-ready}
end
# List all services of this type.
def self.instances
get_services(self.defpath)
end
def self.get_services(defpath, exclude = self.excludes)
defpath = [defpath] unless defpath.is_a? Array
instances = []
defpath.each do |path|
unless FileTest.directory?(path)
Puppet.debug "Service path #{path} does not exist"
next
end
check = [:ensure]
check << :enable if public_method_defined? :enabled?
Dir.entries(path).each do |name|
fullpath = File.join(path, name)
next if name =~ /^\./
next if exclude.include? name
next if not FileTest.executable?(fullpath)
next if not is_init?(fullpath)
instances << new(:name => name, :path => path, :hasstatus => true)
end
end
instances
end
# Mark that our init script supports 'status' commands.
def hasstatus=(value)
case value
when true, "true"; @parameters[:hasstatus] = true
when false, "false"; @parameters[:hasstatus] = false
else
raise Puppet::Error, "Invalid 'hasstatus' value #{value.inspect}"
end
end
# Where is our init script?
def initscript
@initscript ||= self.search(@resource[:name])
end
def paths
@paths ||= @resource[:path].find_all do |path|
if File.directory?(path)
true
else
- if Puppet::FileSystem::File.exist?(path)
+ if Puppet::FileSystem.exist?(path)
self.debug "Search path #{path} is not a directory"
else
self.debug "Search path #{path} does not exist"
end
false
end
end
end
def search(name)
paths.each do |path|
fqname = File.join(path,name)
- if Puppet::FileSystem::File.exist? fqname
+ if Puppet::FileSystem.exist? fqname
return fqname
else
self.debug("Could not find #{name} in #{path}")
end
end
paths.each do |path|
fqname_sh = File.join(path,"#{name}.sh")
- if Puppet::FileSystem::File.exist? fqname_sh
+ if Puppet::FileSystem.exist? fqname_sh
return fqname_sh
else
self.debug("Could not find #{name}.sh in #{path}")
end
end
raise Puppet::Error, "Could not find init script for '#{name}'"
end
# The start command is just the init scriptwith 'start'.
def startcmd
[initscript, :start]
end
# The stop command is just the init script with 'stop'.
def stopcmd
[initscript, :stop]
end
def restartcmd
(@resource[:hasrestart] == :true) && [initscript, :restart]
end
# If it was specified that the init script has a 'status' command, then
# we just return that; otherwise, we return false, which causes it to
# fallback to other mechanisms.
def statuscmd
(@resource[:hasstatus] == :true) && [initscript, :status]
end
private
def self.is_init?(script = initscript)
- file = Puppet::FileSystem::File.new(script)
- !file.symlink? || file.readlink != "/lib/init/upstart-job"
+ file = Puppet::FileSystem.pathname(script)
+ !Puppet::FileSystem.symlink?(file) || Puppet::FileSystem.readlink(file) != "/lib/init/upstart-job"
end
end
diff --git a/lib/puppet/provider/service/launchd.rb b/lib/puppet/provider/service/launchd.rb
index 316fdbf46..49f14d619 100644
--- a/lib/puppet/provider/service/launchd.rb
+++ b/lib/puppet/provider/service/launchd.rb
@@ -1,356 +1,356 @@
require 'facter/util/plist'
Puppet::Type.type(:service).provide :launchd, :parent => :base do
desc <<-'EOT'
This provider manages jobs with `launchd`, which is the default service
framework for Mac OS X (and may be available for use on other platforms).
For `launchd` documentation, see:
* <http://developer.apple.com/macosx/launchd.html>
* <http://launchd.macosforge.org/>
This provider reads plists out of the following directories:
* `/System/Library/LaunchDaemons`
* `/System/Library/LaunchAgents`
* `/Library/LaunchDaemons`
* `/Library/LaunchAgents`
...and builds up a list of services based upon each plist's "Label" entry.
This provider supports:
* ensure => running/stopped,
* enable => true/false
* status
* restart
Here is how the Puppet states correspond to `launchd` states:
* stopped --- job unloaded
* started --- job loaded
* enabled --- 'Disable' removed from job plist file
* disabled --- 'Disable' added to job plist file
Note that this allows you to do something `launchctl` can't do, which is to
be in a state of "stopped/enabled" or "running/disabled".
Note that this provider does not support overriding 'restart' or 'status'.
EOT
include Puppet::Util::Warnings
commands :launchctl => "/bin/launchctl"
commands :sw_vers => "/usr/bin/sw_vers"
commands :plutil => "/usr/bin/plutil"
defaultfor :operatingsystem => :darwin
confine :operatingsystem => :darwin
has_feature :enableable
has_feature :refreshable
mk_resource_methods
# These are the paths in OS X where a launchd service plist could
# exist. This is a helper method, versus a constant, for easy testing
# and mocking
#
# @api private
def self.launchd_paths
[
"/Library/LaunchAgents",
"/Library/LaunchDaemons",
"/System/Library/LaunchAgents",
"/System/Library/LaunchDaemons"
]
end
# Defines the path to the overrides plist file where service enabling
# behavior is defined in 10.6 and greater.
#
# @api private
def self.launchd_overrides
"/var/db/launchd.db/com.apple.launchd/overrides.plist"
end
# Caching is enabled through the following three methods. Self.prefetch will
# call self.instances to create an instance for each service. Self.flush will
# clear out our cache when we're done.
def self.prefetch(resources)
instances.each do |prov|
if resource = resources[prov.name]
resource.provider = prov
end
end
end
# Self.instances will return an array with each element being a hash
# containing the name, provider, path, and status of each service on the
# system.
def self.instances
jobs = self.jobsearch
@job_list ||= self.job_list
jobs.keys.collect do |job|
job_status = @job_list.has_key?(job) ? :running : :stopped
new(:name => job, :provider => :launchd, :path => jobs[job], :status => job_status)
end
end
# This method will return a list of files in the passed directory. This method
# does not go recursively down the tree and does not return directories
#
# @param path [String] The directory to glob
#
# @api private
#
# @return [Array] of String instances modeling file paths
def self.return_globbed_list_of_file_paths(path)
array_of_files = Dir.glob(File.join(path, '*')).collect do |filepath|
File.file?(filepath) ? filepath : nil
end
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)
by_label = make_label_to_path_map
if label
if by_label.has_key? label
return { label => by_label[label] }
else
# 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
# caller wants the whole map
by_label
end
end
# This status method lists out all currently running services.
# This hash is returned at the end of the method.
def self.job_list
@job_list = Hash.new
begin
output = launchctl :list
raise Puppet::Error.new("launchctl list failed to return any data.") if output.nil?
output.split("\n").each do |line|
@job_list[line.split(/\s/).last] = :running
end
rescue Puppet::ExecutionFailure
- raise Puppet::Error.new("Unable to determine status of #{resource[:name]}")
+ raise Puppet::Error.new("Unable to determine status of #{resource[:name]}", $!)
end
@job_list
end
# Launchd implemented plist overrides in version 10.6.
# This method checks the major_version of OS X and returns true if
# it is 10.6 or greater. This allows us to implement different plist
# behavior for versions >= 10.6
def has_macosx_plist_overrides?
@product_version ||= self.class.get_macosx_version_major
# (#11593) Remove support for OS X 10.4 & earlier
# leaving this as is because 10.5 still didn't have plist support
return true unless /^10\.[0-5]/.match(@product_version)
return false
end
# Read a plist, whether its format is XML or in Apple's "binary1"
# format.
def self.read_plist(path)
begin
Plist::parse_xml(plutil('-convert', 'xml1', '-o', '/dev/stdout', path))
rescue Puppet::ExecutionFailure => detail
Puppet.warning("Cannot read file #{path}; Puppet is skipping it. \n" +
"Details: #{detail}")
return nil
end
end
# Clean out the @property_hash variable containing the cached list of services
def flush
@property_hash.clear
end
def exists?
Puppet.debug("Puppet::Provider::Launchd:Ensure for #{@property_hash[:name]}: #{@property_hash[:ensure]}")
@property_hash[:ensure] != :absent
end
def self.get_macosx_version_major
return @macosx_version_major if @macosx_version_major
begin
# Make sure we've loaded all of the facts
Facter.loadfacts
product_version_major = Facter.value(:macosx_productversion_major)
fail("#{product_version_major} is not supported by the launchd provider") if %w{10.0 10.1 10.2 10.3 10.4}.include?(product_version_major)
@macosx_version_major = product_version_major
return @macosx_version_major
rescue Puppet::ExecutionFailure => detail
- fail("Could not determine OS X version: #{detail}")
+ self.fail Puppet::Error, "Could not determine OS X version: #{detail}", detail
end
end
# finds the path for a given label and returns the path and parsed plist
# as an array of [path, plist]. Note plist is really a Hash here.
def plist_from_label(label)
job = self.class.jobsearch(label)
job_path = job[label]
if FileTest.file?(job_path)
job_plist = self.class.read_plist(job_path)
else
raise Puppet::Error.new("Unable to parse launchd plist at path: #{job_path}")
end
[job_path, job_plist]
end
# start the service. To get to a state of running/enabled, we need to
# conditionally enable at load, then disable by modifying the plist file
# directly.
def start
return ucommand(:start) if resource[:start]
job_path, job_plist = plist_from_label(resource[:name])
did_enable_job = false
cmds = []
cmds << :launchctl << :load
if self.enabled? == :false || self.status == :stopped # launchctl won't load disabled jobs
cmds << "-w"
did_enable_job = true
end
cmds << job_path
begin
execute(cmds)
rescue Puppet::ExecutionFailure
- raise Puppet::Error.new("Unable to start service: #{resource[:name]} at path: #{job_path}")
+ raise Puppet::Error.new("Unable to start service: #{resource[:name]} at path: #{job_path}", $!)
end
# As load -w clears the Disabled flag, we need to add it in after
self.disable if did_enable_job and resource[:enable] == :false
end
def stop
return ucommand(:stop) if resource[:stop]
job_path, job_plist = plist_from_label(resource[:name])
did_disable_job = false
cmds = []
cmds << :launchctl << :unload
if self.enabled? == :true # keepalive jobs can't be stopped without disabling
cmds << "-w"
did_disable_job = true
end
cmds << job_path
begin
execute(cmds)
rescue Puppet::ExecutionFailure
- raise Puppet::Error.new("Unable to stop service: #{resource[:name]} at path: #{job_path}")
+ raise Puppet::Error.new("Unable to stop service: #{resource[:name]} at path: #{job_path}", $!)
end
# As unload -w sets the Disabled flag, we need to add it in after
self.enable if did_disable_job and resource[:enable] == :true
end
def restart
Puppet.debug("A restart has been triggered for the #{resource[:name]} service")
Puppet.debug("Stopping the #{resource[:name]} service")
self.stop
Puppet.debug("Starting the #{resource[:name]} service")
self.start
end
# launchd jobs are enabled by default. They are only disabled if the key
# "Disabled" is set to true, but it can also be set to false to enable it.
# Starting in 10.6, the Disabled key in the job plist is consulted, but only
# if there is no entry in the global overrides plist. We need to draw a
# distinction between undefined, true and false for both locations where the
# Disabled flag can be defined.
def enabled?
job_plist_disabled = nil
overrides_disabled = nil
job_path, job_plist = plist_from_label(resource[:name])
job_plist_disabled = job_plist["Disabled"] if job_plist.has_key?("Disabled")
if has_macosx_plist_overrides?
if FileTest.file?(self.class.launchd_overrides) and overrides = self.class.read_plist(self.class.launchd_overrides)
if overrides.has_key?(resource[:name])
overrides_disabled = overrides[resource[:name]]["Disabled"] if overrides[resource[:name]].has_key?("Disabled")
end
end
end
if overrides_disabled.nil?
if job_plist_disabled.nil? or job_plist_disabled == false
return :true
end
elsif overrides_disabled == false
return :true
end
:false
end
# enable and disable are a bit hacky. We write out the plist with the appropriate value
# rather than dealing with launchctl as it is unable to change the Disabled flag
# without actually loading/unloading the job.
# Starting in 10.6 we need to write out a disabled key to the global
# overrides plist, in earlier versions this is stored in the job plist itself.
def enable
if has_macosx_plist_overrides?
overrides = self.class.read_plist(self.class.launchd_overrides)
overrides[resource[:name]] = { "Disabled" => false }
Plist::Emit.save_plist(overrides, self.class.launchd_overrides)
else
job_path, job_plist = plist_from_label(resource[:name])
if self.enabled? == :false
job_plist.delete("Disabled")
Plist::Emit.save_plist(job_plist, job_path)
end
end
end
def disable
if has_macosx_plist_overrides?
overrides = self.class.read_plist(self.class.launchd_overrides)
overrides[resource[:name]] = { "Disabled" => true }
Plist::Emit.save_plist(overrides, self.class.launchd_overrides)
else
job_path, job_plist = plist_from_label(resource[:name])
job_plist["Disabled"] = true
Plist::Emit.save_plist(job_plist, job_path)
end
end
end
diff --git a/lib/puppet/provider/service/redhat.rb b/lib/puppet/provider/service/redhat.rb
index c1c6401c1..7d07b9289 100644
--- a/lib/puppet/provider/service/redhat.rb
+++ b/lib/puppet/provider/service/redhat.rb
@@ -1,64 +1,64 @@
# Manage Red Hat services. Start/stop uses /sbin/service and enable/disable uses chkconfig
Puppet::Type.type(:service).provide :redhat, :parent => :init, :source => :init do
desc "Red Hat's (and probably many others') form of `init`-style service
management. Uses `chkconfig` for service enabling and disabling.
"
commands :chkconfig => "/sbin/chkconfig", :service => "/sbin/service"
defaultfor :osfamily => [:redhat, :suse]
# Remove the symlinks
def disable
# The off method operates on run levels 2,3,4 and 5 by default We ensure
# all run levels are turned off because the reset method may turn on the
# service in run levels 0, 1 and/or 6
output = chkconfig("--level", "0123456", @resource[:name], :off)
rescue Puppet::ExecutionFailure
- raise Puppet::Error, "Could not disable #{self.name}: #{output}"
+ raise Puppet::Error, "Could not disable #{self.name}: #{output}", $!.backtrace
end
def enabled?
# Checkconfig always returns 0 on SuSE unless the --check flag is used.
args = (Facter.value(:osfamily) == 'Suse' ? ['--check'] : [])
begin
chkconfig(@resource[:name], *args)
rescue Puppet::ExecutionFailure
return :false
end
:true
end
# Don't support them specifying runlevels; always use the runlevels
# in the init scripts.
def enable
chkconfig(@resource[:name], :on)
rescue Puppet::ExecutionFailure => detail
- raise Puppet::Error, "Could not enable #{self.name}: #{detail}"
+ raise Puppet::Error, "Could not enable #{self.name}: #{detail}", detail.backtrace
end
def initscript
raise Puppet::Error, "Do not directly call the init script for '#{@resource[:name]}'; use 'service' instead"
end
# use hasstatus=>true when its set for the provider.
def statuscmd
((@resource.provider.get(:hasstatus) == true) || (@resource[:hasstatus] == :true)) && [command(:service), @resource[:name], "status"]
end
def restartcmd
(@resource[:hasrestart] == :true) && [command(:service), @resource[:name], "restart"]
end
def startcmd
[command(:service), @resource[:name], "start"]
end
def stopcmd
[command(:service), @resource[:name], "stop"]
end
end
diff --git a/lib/puppet/provider/service/runit.rb b/lib/puppet/provider/service/runit.rb
index 8d835d2db..f73c70929 100644
--- a/lib/puppet/provider/service/runit.rb
+++ b/lib/puppet/provider/service/runit.rb
@@ -1,111 +1,111 @@
# Daemontools service management
#
# author Brice Figureau <brice-puppet@daysofwonder.com>
Puppet::Type.type(:service).provide :runit, :parent => :daemontools do
desc <<-'EOT'
Runit service management.
This provider manages daemons running supervised by Runit.
When detecting the service directory it will check, in order of preference:
* `/service`
* `/etc/service`
* `/var/service`
The daemon directory should be in one of the following locations:
* `/etc/sv`
* `/var/lib/service`
or this can be overriden in the service resource parameters::
service { "myservice":
provider => "runit",
path => "/path/to/daemons",
}
This provider supports out of the box:
* start/stop
* enable/disable
* restart
* status
EOT
commands :sv => "/usr/bin/sv"
class << self
# this is necessary to autodetect a valid resource
# default path, since there is no standard for such directory.
def defpath(dummy_argument=:work_arround_for_ruby_GC_bug)
unless @defpath
["/etc/sv", "/var/lib/service"].each do |path|
- if Puppet::FileSystem::File.exist?(path)
+ if Puppet::FileSystem.exist?(path)
@defpath = path
break
end
end
raise "Could not find the daemon directory (tested [/etc/sv,/var/lib/service])" unless @defpath
end
@defpath
end
end
# find the service dir on this node
def servicedir
unless @servicedir
["/service", "/etc/service","/var/service"].each do |path|
- if Puppet::FileSystem::File.exist?(path)
+ if Puppet::FileSystem.exist?(path)
@servicedir = path
break
end
end
raise "Could not find service directory" unless @servicedir
end
@servicedir
end
def status
begin
output = sv "status", self.daemon
return :running if output =~ /^run: /
rescue Puppet::ExecutionFailure => detail
unless detail.message =~ /(warning: |runsv not running$)/
- raise Puppet::Error.new( "Could not get status for service #{resource.ref}: #{detail}" )
+ raise Puppet::Error.new( "Could not get status for service #{resource.ref}: #{detail}", detail )
end
end
:stopped
end
def stop
sv "stop", self.service
end
def start
if enabled? != :true
enable
# Work around issue #4480
# runsvdir takes up to 5 seconds to recognize
# the symlink created by this call to enable
Puppet.info "Waiting 5 seconds for runsvdir to discover service #{self.service}"
sleep 5
end
sv "start", self.service
end
def restart
sv "restart", self.service
end
# disable by removing the symlink so that runit
# doesn't restart our service behind our back
# note that runit doesn't need to perform a stop
# before a disable
def disable
# unlink the daemon symlink to disable it
- Puppet::FileSystem::File.unlink(self.service) if Puppet::FileSystem::File.new(self.service).symlink?
+ Puppet::FileSystem.unlink(self.service) if Puppet::FileSystem.symlink?(self.service)
end
end
diff --git a/lib/puppet/provider/service/service.rb b/lib/puppet/provider/service/service.rb
index 27f9b6373..1b9554254 100644
--- a/lib/puppet/provider/service/service.rb
+++ b/lib/puppet/provider/service/service.rb
@@ -1,43 +1,42 @@
Puppet::Type.type(:service).provide :service do
desc "The simplest form of service support."
def self.instances
[]
end
# How to restart the process.
def restart
if @resource[:restart] or restartcmd
ucommand(:restart)
else
self.stop
self.start
end
end
# There is no default command, which causes other methods to be used
def restartcmd
end
# A simple wrapper so execution failures are a bit more informative.
- def texecute(type, command, fof = true)
+ def texecute(type, command, fof = true, squelch = false, combine = true)
begin
- # #565: Services generally produce no output, so squelch them.
- execute(command, :failonfail => fof, :override_locale => false, :squelch => true)
+ execute(command, :failonfail => fof, :override_locale => false, :squelch => squelch, :combine => combine)
rescue Puppet::ExecutionFailure => detail
- @resource.fail "Could not #{type} #{@resource.ref}: #{detail}"
+ @resource.fail Puppet::Error, "Could not #{type} #{@resource.ref}: #{detail}", detail
end
nil
end
# Use either a specified command or the default for our provider.
def ucommand(type, fof = true)
if c = @resource[type]
cmd = [c]
else
cmd = [send("#{type}cmd")].flatten
end
texecute(type, cmd, fof)
end
end
diff --git a/lib/puppet/provider/service/smf.rb b/lib/puppet/provider/service/smf.rb
index 25ae551de..fd7793f4e 100644
--- a/lib/puppet/provider/service/smf.rb
+++ b/lib/puppet/provider/service/smf.rb
@@ -1,116 +1,116 @@
# Solaris 10 SMF-style services.
Puppet::Type.type(:service).provide :smf, :parent => :base do
desc <<-EOT
Support for Sun's new Service Management Framework.
Starting a service is effectively equivalent to enabling it, so there is
only support for starting and stopping services, which also enables and
disables them, respectively.
By specifying `manifest => "/path/to/service.xml"`, the SMF manifest will
be imported if it does not exist.
EOT
defaultfor :osfamily => :solaris
confine :osfamily => :solaris
commands :adm => "/usr/sbin/svcadm", :svcs => "/usr/bin/svcs"
commands :svccfg => "/usr/sbin/svccfg"
def setupservice
if resource[:manifest]
[command(:svcs), "-l", @resource[:name]]
if $CHILD_STATUS.exitstatus == 1
Puppet.notice "Importing #{@resource[:manifest]} for #{@resource[:name]}"
svccfg :import, resource[:manifest]
end
end
rescue Puppet::ExecutionFailure => detail
- raise Puppet::Error.new( "Cannot config #{self.name} to enable it: #{detail}" )
+ raise Puppet::Error.new( "Cannot config #{self.name} to enable it: #{detail}", detail )
end
def self.instances
svcs.split("\n").select{|l| l !~ /^legacy_run/ }.collect do |line|
state,stime,fmri = line.split(/\s+/)
status = case state
when /online/; :running
when /maintenance/; :maintenance
else :stopped
end
new({:name => fmri, :ensure => status})
end
end
def enable
self.start
end
def enabled?
case self.status
when :running
return :true
else
return :false
end
end
def disable
self.stop
end
def restartcmd
[command(:adm), :restart, @resource[:name]]
end
def startcmd
self.setupservice
case self.status
when :maintenance
[command(:adm), :clear, @resource[:name]]
else
[command(:adm), :enable, "-s", @resource[:name]]
end
end
def status
if @resource[:status]
super
return
end
begin
# get the current state and the next state, and if the next
# state is set (i.e. not "-") use it for state comparison
states = svcs("-H", "-o", "state,nstate", @resource[:name]).chomp.split
state = states[1] == "-" ? states[0] : states[1]
rescue Puppet::ExecutionFailure
info "Could not get status on service #{self.name}"
return :stopped
end
case state
when "online"
#self.warning "matched running #{line.inspect}"
return :running
when "offline", "disabled", "uninitialized"
#self.warning "matched stopped #{line.inspect}"
return :stopped
when "maintenance"
return :maintenance
when "legacy_run"
raise Puppet::Error,
"Cannot manage legacy services through SMF"
else
raise Puppet::Error,
"Unmanageable state '#{state}' on service #{self.name}"
end
end
def stopcmd
[command(:adm), :disable, "-s", @resource[:name]]
end
end
diff --git a/lib/puppet/provider/service/src.rb b/lib/puppet/provider/service/src.rb
index 8028751ff..e4bcbd9b6 100644
--- a/lib/puppet/provider/service/src.rb
+++ b/lib/puppet/provider/service/src.rb
@@ -1,120 +1,120 @@
# AIX System Resource controller (SRC)
Puppet::Type.type(:service).provide :src, :parent => :base do
desc "Support for AIX's System Resource controller.
Services are started/stopped based on the `stopsrc` and `startsrc`
commands, and some services can be refreshed with `refresh` command.
Enabling and disabling services is not supported, as it requires
modifications to `/etc/inittab`. Starting and stopping groups of subsystems
is not yet supported.
"
defaultfor :operatingsystem => :aix
confine :operatingsystem => :aix
optional_commands :stopsrc => "/usr/bin/stopsrc",
:startsrc => "/usr/bin/startsrc",
:refresh => "/usr/bin/refresh",
:lssrc => "/usr/bin/lssrc",
:lsitab => "/usr/sbin/lsitab",
:mkitab => "/usr/sbin/mkitab",
:rmitab => "/usr/sbin/rmitab",
:chitab => "/usr/sbin/chitab"
has_feature :refreshable
def self.instances
services = lssrc('-S')
services.split("\n").reject { |x| x.strip.start_with? '#' }.collect do |line|
data = line.split(':')
service_name = data[0]
new(:name => service_name)
end
end
def startcmd
[command(:startsrc), "-s", @resource[:name]]
end
def stopcmd
[command(:stopsrc), "-s", @resource[:name]]
end
def default_runlevel
"2"
end
def default_action
"once"
end
def enabled?
execute([command(:lsitab), @resource[:name]], {:failonfail => false, :combine => true})
$CHILD_STATUS.exitstatus == 0 ? :true : :false
end
def enable
mkitab("%s:%s:%s:%s" % [@resource[:name], default_runlevel, default_action, startcmd.join(" ")])
end
def disable
rmitab(@resource[:name])
end
def restart
execute([command(:lssrc), "-Ss", @resource[:name]]).each_line do |line|
args = line.split(":")
next unless args[0] == @resource[:name]
# Subsystems with the -K flag can get refreshed (HUPed)
# While subsystems with -S (signals) must be stopped/started
method = args[11]
do_refresh = case method
when "-K" then :true
when "-S" then :false
else self.fail("Unknown service communication method #{method}")
end
begin
if do_refresh == :true
execute([command(:refresh), "-s", @resource[:name]])
else
self.stop
self.start
end
return :true
rescue Puppet::ExecutionFailure => detail
- raise Puppet::Error.new("Unable to restart service #{@resource[:name]}, error was: #{detail}" )
+ raise Puppet::Error.new("Unable to restart service #{@resource[:name]}, error was: #{detail}", detail )
end
end
self.fail("No such service found")
rescue Puppet::ExecutionFailure => detail
- raise Puppet::Error.new("Cannot get status of #{@resource[:name]}, error was: #{detail}" )
+ raise Puppet::Error.new("Cannot get status of #{@resource[:name]}, error was: #{detail}", detail )
end
def status
execute([command(:lssrc), "-s", @resource[:name]]).each_line do |line|
args = line.split
# This is the header line
next unless args[0] == @resource[:name]
# PID is the 3rd field, but inoperative subsystems
# skip this so split doesn't work right
state = case args[-1]
when "active" then :running
when "inoperative" then :stopped
end
Puppet.debug("Service #{@resource[:name]} is #{args[-1]}")
return state
end
self.fail("No such service found")
rescue Puppet::ExecutionFailure => detail
- raise Puppet::Error.new("Cannot get status of #{@resource[:name]}, error was: #{detail}" )
+ raise Puppet::Error.new("Cannot get status of #{@resource[:name]}, error was: #{detail}", detail )
end
end
diff --git a/lib/puppet/provider/service/systemd.rb b/lib/puppet/provider/service/systemd.rb
index 1ff070b86..64cde14be 100644
--- a/lib/puppet/provider/service/systemd.rb
+++ b/lib/puppet/provider/service/systemd.rb
@@ -1,65 +1,65 @@
# Manage systemd services using /bin/systemctl
Puppet::Type.type(:service).provide :systemd, :parent => :base do
desc "Manages `systemd` services using `systemctl`."
commands :systemctl => "systemctl"
- #defaultfor :osfamily => [:redhat, :suse]
defaultfor :osfamily => [:archlinux]
+ defaultfor :osfamily => :redhat, :operatingsystemmajrelease => "7"
def self.instances
i = []
- output = systemctl('list-units', '--full', '--all', '--no-pager')
+ output = systemctl('list-units', '--type', 'service', '--full', '--all', '--no-pager')
output.scan(/^(\S+)\s+(loaded|error)\s+(active|inactive)\s+(active|waiting|running|plugged|mounted|dead|exited|listening|elapsed)\s*?(\S.*?)?$/i).each do |m|
i << new(:name => m[0])
end
return i
rescue Puppet::ExecutionFailure
return []
end
def disable
output = systemctl(:disable, @resource[:name])
rescue Puppet::ExecutionFailure
- raise Puppet::Error, "Could not disable #{self.name}: #{output}"
+ raise Puppet::Error, "Could not disable #{self.name}: #{output}", $!.backtrace
end
def enabled?
begin
systemctl("is-enabled", @resource[:name])
rescue Puppet::ExecutionFailure
return :false
end
:true
end
def status
begin
systemctl("is-active", @resource[:name])
rescue Puppet::ExecutionFailure
return :stopped
end
return :running
end
def enable
output = systemctl("enable", @resource[:name])
rescue Puppet::ExecutionFailure
- raise Puppet::Error, "Could not enable #{self.name}: #{output}"
+ raise Puppet::Error, "Could not enable #{self.name}: #{output}", $!.backtrace
end
def restartcmd
[command(:systemctl), "restart", @resource[:name]]
end
def startcmd
[command(:systemctl), "start", @resource[:name]]
end
def stopcmd
[command(:systemctl), "stop", @resource[:name]]
end
end
diff --git a/lib/puppet/provider/service/upstart.rb b/lib/puppet/provider/service/upstart.rb
index c7fd58c4e..1aed8dae3 100644
--- a/lib/puppet/provider/service/upstart.rb
+++ b/lib/puppet/provider/service/upstart.rb
@@ -1,340 +1,355 @@
require 'semver'
Puppet::Type.type(:service).provide :upstart, :parent => :debian do
START_ON = /^\s*start\s+on/
COMMENTED_START_ON = /^\s*#+\s*start\s+on/
MANUAL = /^\s*manual\s*$/
desc "Ubuntu service management with `upstart`.
- This provider manages `upstart` jobs, which have replaced `initd` services
- on Ubuntu. For `upstart` documentation, see <http://upstart.ubuntu.com/>.
+ This provider manages `upstart` jobs on Ubuntu. For `upstart` documentation,
+ see <http://upstart.ubuntu.com/>.
"
- # confine to :ubuntu for now because I haven't tested on other platforms
- confine :operatingsystem => :ubuntu #[:ubuntu, :fedora, :debian]
+
+ confine :any => [
+ Facter.value(:operatingsystem) == 'Ubuntu',
+ (Facter.value(:osfamily) == 'RedHat' and Facter.value(:operatingsystemrelease) =~ /^6\./),
+ ]
defaultfor :operatingsystem => :ubuntu
commands :start => "/sbin/start",
:stop => "/sbin/stop",
:restart => "/sbin/restart",
:status_exec => "/sbin/status",
:initctl => "/sbin/initctl"
# upstart developer haven't implemented initctl enable/disable yet:
# http://www.linuxplanet.com/linuxplanet/tutorials/7033/2/
has_feature :enableable
def self.instances
self.get_services(self.excludes) # Take exclude list from init provider
end
+ def self.excludes
+ excludes = super
+ if Facter.value(:osfamily) == 'RedHat'
+ # Puppet cannot deal with services that have instances, so we have to
+ # ignore these services using instances on redhat based systems.
+ excludes += %w[serial tty]
+ end
+
+ excludes
+ end
+
+
def self.get_services(exclude=[])
instances = []
execpipe("#{command(:initctl)} list") { |process|
process.each_line { |line|
# needs special handling of services such as network-interface:
# initctl list:
# network-interface (lo) start/running
# network-interface (eth0) start/running
# network-interface-security start/running
name = \
if matcher = line.match(/^(network-interface)\s\(([^\)]+)\)/)
"#{matcher[1]} INTERFACE=#{matcher[2]}"
elsif matcher = line.match(/^(network-interface-security)\s\(([^\)]+)\)/)
"#{matcher[1]} JOB=#{matcher[2]}"
else
line.split.first
end
instances << new(:name => name)
}
}
instances.reject { |instance| exclude.include?(instance.name) }
end
def self.defpath
["/etc/init", "/etc/init.d"]
end
def upstart_version
@upstart_version ||= initctl("--version").match(/initctl \(upstart ([^\)]*)\)/)[1]
end
# Where is our override script?
def overscript
@overscript ||= initscript.gsub(/\.conf$/,".override")
end
def search(name)
# Search prefers .conf as that is what upstart uses
[".conf", "", ".sh"].each do |suffix|
paths.each do |path|
service_name = name.match(/^(\S+)/)[1]
fqname = File.join(path, service_name + suffix)
- if Puppet::FileSystem::File.exist?(fqname)
+ if Puppet::FileSystem.exist?(fqname)
return fqname
end
self.debug("Could not find #{name}#{suffix} in #{path}")
end
end
raise Puppet::Error, "Could not find init script or upstart conf file for '#{name}'"
end
def enabled?
return super if not is_upstart?
script_contents = read_script_from(initscript)
if version_is_pre_0_6_7
enabled_pre_0_6_7?(script_contents)
elsif version_is_pre_0_9_0
enabled_pre_0_9_0?(script_contents)
elsif version_is_post_0_9_0
enabled_post_0_9_0?(script_contents, read_override_file)
end
end
def enable
return super if not is_upstart?
script_text = read_script_from(initscript)
if version_is_pre_0_9_0
enable_pre_0_9_0(script_text)
else
enable_post_0_9_0(script_text, read_override_file)
end
end
def disable
return super if not is_upstart?
script_text = read_script_from(initscript)
if version_is_pre_0_6_7
disable_pre_0_6_7(script_text)
elsif version_is_pre_0_9_0
disable_pre_0_9_0(script_text)
elsif version_is_post_0_9_0
disable_post_0_9_0(read_override_file)
end
end
def startcmd
is_upstart? ? [command(:start), @resource[:name]] : super
end
def stopcmd
is_upstart? ? [command(:stop), @resource[:name]] : super
end
def restartcmd
is_upstart? ? (@resource[:hasrestart] == :true) && [command(:restart), @resource[:name]] : super
end
def statuscmd
is_upstart? ? nil : super #this is because upstart is broken with its return codes
end
def status
return super if not is_upstart?
output = status_exec(@resource[:name].split)
if output =~ /start\//
return :running
else
return :stopped
end
end
private
def is_upstart?(script = initscript)
- Puppet::FileSystem::File.exist?(script) && script.match(/\/etc\/init\/\S+\.conf/)
+ Puppet::FileSystem.exist?(script) && script.match(/\/etc\/init\/\S+\.conf/)
end
def version_is_pre_0_6_7
Puppet::Util::Package.versioncmp(upstart_version, "0.6.7") == -1
end
def version_is_pre_0_9_0
Puppet::Util::Package.versioncmp(upstart_version, "0.9.0") == -1
end
def version_is_post_0_9_0
Puppet::Util::Package.versioncmp(upstart_version, "0.9.0") >= 0
end
def enabled_pre_0_6_7?(script_text)
# Upstart version < 0.6.7 means no manual stanza.
if script_text.match(START_ON)
return :true
else
return :false
end
end
def enabled_pre_0_9_0?(script_text)
# Upstart version < 0.9.0 means no override files
# So we check to see if an uncommented start on or manual stanza is the last one in the file
# The last one in the file wins.
enabled = :false
script_text.each_line do |line|
if line.match(START_ON)
enabled = :true
elsif line.match(MANUAL)
enabled = :false
end
end
enabled
end
def enabled_post_0_9_0?(script_text, over_text)
# This version has manual stanzas and override files
# So we check to see if an uncommented start on or manual stanza is the last one in the
# conf file and any override files. The last one in the file wins.
enabled = :false
script_text.each_line do |line|
if line.match(START_ON)
enabled = :true
elsif line.match(MANUAL)
enabled = :false
end
end
over_text.each_line do |line|
if line.match(START_ON)
enabled = :true
elsif line.match(MANUAL)
enabled = :false
end
end if over_text
enabled
end
def enable_pre_0_9_0(text)
# We also need to remove any manual stanzas to ensure that it is enabled
text = remove_manual_from(text)
if enabled_pre_0_9_0?(text) == :false
enabled_script =
if text.match(COMMENTED_START_ON)
uncomment_start_block_in(text)
else
add_default_start_to(text)
end
else
enabled_script = text
end
write_script_to(initscript, enabled_script)
end
def enable_post_0_9_0(script_text, over_text)
over_text = remove_manual_from(over_text)
if enabled_post_0_9_0?(script_text, over_text) == :false
if script_text.match(START_ON)
over_text << extract_start_on_block_from(script_text)
else
over_text << "\nstart on runlevel [2,3,4,5]"
end
end
write_script_to(overscript, over_text)
end
def disable_pre_0_6_7(script_text)
disabled_script = comment_start_block_in(script_text)
write_script_to(initscript, disabled_script)
end
def disable_pre_0_9_0(script_text)
write_script_to(initscript, ensure_disabled_with_manual(script_text))
end
def disable_post_0_9_0(over_text)
write_script_to(overscript, ensure_disabled_with_manual(over_text))
end
def read_override_file
- if Puppet::FileSystem::File.exist?(overscript)
+ if Puppet::FileSystem.exist?(overscript)
read_script_from(overscript)
else
""
end
end
def uncomment(line)
line.gsub(/^(\s*)#+/, '\1')
end
def remove_trailing_comments_from_commented_line_of(line)
line.gsub(/^(\s*#+\s*[^#]*).*/, '\1')
end
def remove_trailing_comments_from(line)
line.gsub(/^(\s*[^#]*).*/, '\1')
end
def unbalanced_parens_on(line)
line.count('(') - line.count(')')
end
def remove_manual_from(text)
text.gsub(MANUAL, "")
end
def comment_start_block_in(text)
parens = 0
text.lines.map do |line|
if line.match(START_ON) || parens > 0
# If there are more opening parens than closing parens, we need to comment out a multiline 'start on' stanza
parens += unbalanced_parens_on(remove_trailing_comments_from(line))
"#" + line
else
line
end
end.join('')
end
def uncomment_start_block_in(text)
parens = 0
text.lines.map do |line|
if line.match(COMMENTED_START_ON) || parens > 0
parens += unbalanced_parens_on(remove_trailing_comments_from_commented_line_of(line))
uncomment(line)
else
line
end
end.join('')
end
def extract_start_on_block_from(text)
parens = 0
text.lines.map do |line|
if line.match(START_ON) || parens > 0
parens += unbalanced_parens_on(remove_trailing_comments_from(line))
line
end
end.join('')
end
def add_default_start_to(text)
text + "\nstart on runlevel [2,3,4,5]"
end
def ensure_disabled_with_manual(text)
remove_manual_from(text) + "\nmanual"
end
def read_script_from(filename)
File.open(filename) do |file|
file.read
end
end
def write_script_to(file, text)
Puppet::Util.replace_file(file, 0644) do |file|
file.write(text)
end
end
end
diff --git a/lib/puppet/provider/service/windows.rb b/lib/puppet/provider/service/windows.rb
index 1e58843bf..c084ffbc9 100644
--- a/lib/puppet/provider/service/windows.rb
+++ b/lib/puppet/provider/service/windows.rb
@@ -1,106 +1,106 @@
# Windows Service Control Manager (SCM) provider
Puppet::Type.type(:service).provide :windows, :parent => :service do
desc <<-EOT
Support for Windows Service Control Manager (SCM). This provider can
start, stop, enable, and disable services, and the SCM provides working
status methods for all services.
Control of service groups (dependencies) is not yet supported, nor is running
services as a specific user.
EOT
defaultfor :operatingsystem => :windows
confine :operatingsystem => :windows
has_feature :refreshable
commands :net => 'net.exe'
def enable
w32ss = Win32::Service.configure( 'service_name' => @resource[:name], 'start_type' => Win32::Service::SERVICE_AUTO_START )
raise Puppet::Error.new("Win32 service enable of #{@resource[:name]} failed" ) if( w32ss.nil? )
rescue Win32::Service::Error => detail
- raise Puppet::Error.new("Cannot enable #{@resource[:name]}, error was: #{detail}" )
+ raise Puppet::Error.new("Cannot enable #{@resource[:name]}, error was: #{detail}", detail )
end
def disable
w32ss = Win32::Service.configure( 'service_name' => @resource[:name], 'start_type' => Win32::Service::SERVICE_DISABLED )
raise Puppet::Error.new("Win32 service disable of #{@resource[:name]} failed" ) if( w32ss.nil? )
rescue Win32::Service::Error => detail
- raise Puppet::Error.new("Cannot disable #{@resource[:name]}, error was: #{detail}" )
+ raise Puppet::Error.new("Cannot disable #{@resource[:name]}, error was: #{detail}", detail )
end
def manual_start
w32ss = Win32::Service.configure( 'service_name' => @resource[:name], 'start_type' => Win32::Service::SERVICE_DEMAND_START )
raise Puppet::Error.new("Win32 service manual enable of #{@resource[:name]} failed" ) if( w32ss.nil? )
rescue Win32::Service::Error => detail
- raise Puppet::Error.new("Cannot enable #{@resource[:name]} for manual start, error was: #{detail}" )
+ raise Puppet::Error.new("Cannot enable #{@resource[:name]} for manual start, error was: #{detail}", detail )
end
def enabled?
w32ss = Win32::Service.config_info( @resource[:name] )
raise Puppet::Error.new("Win32 service query of #{@resource[:name]} failed" ) unless( !w32ss.nil? && w32ss.instance_of?( Struct::ServiceConfigInfo ) )
debug("Service #{@resource[:name]} start type is #{w32ss.start_type}")
case w32ss.start_type
when Win32::Service.get_start_type(Win32::Service::SERVICE_AUTO_START),
Win32::Service.get_start_type(Win32::Service::SERVICE_BOOT_START),
Win32::Service.get_start_type(Win32::Service::SERVICE_SYSTEM_START)
:true
when Win32::Service.get_start_type(Win32::Service::SERVICE_DEMAND_START)
:manual
when Win32::Service.get_start_type(Win32::Service::SERVICE_DISABLED)
:false
else
raise Puppet::Error.new("Unknown start type: #{w32ss.start_type}")
end
rescue Win32::Service::Error => detail
- raise Puppet::Error.new("Cannot get start type for #{@resource[:name]}, error was: #{detail}" )
+ raise Puppet::Error.new("Cannot get start type for #{@resource[:name]}, error was: #{detail}", detail )
end
def start
if enabled? == :false
# If disabled and not managing enable, respect disabled and fail.
if @resource[:enable].nil?
raise Puppet::Error, "Will not start disabled service #{@resource[:name]} without managing enable. Specify 'enable => false' to override."
# Otherwise start. If enable => false, we will later sync enable and
# disable the service again.
elsif @resource[:enable] == :true
enable
else
manual_start
end
end
net(:start, @resource[:name])
rescue Puppet::ExecutionFailure => detail
- raise Puppet::Error.new("Cannot start #{@resource[:name]}, error was: #{detail}" )
+ raise Puppet::Error.new("Cannot start #{@resource[:name]}, error was: #{detail}", detail )
end
def stop
net(:stop, @resource[:name])
rescue Puppet::ExecutionFailure => detail
- raise Puppet::Error.new("Cannot stop #{@resource[:name]}, error was: #{detail}" )
+ raise Puppet::Error.new("Cannot stop #{@resource[:name]}, error was: #{detail}", detail )
end
def status
w32ss = Win32::Service.status( @resource[:name] )
raise Puppet::Error.new("Win32 service query of #{@resource[:name]} failed" ) unless( !w32ss.nil? && w32ss.instance_of?( Struct::ServiceStatus ) )
state = case w32ss.current_state
when "stopped", "pause pending", "stop pending", "paused" then :stopped
when "running", "continue pending", "start pending" then :running
else
raise Puppet::Error.new("Unknown service state '#{w32ss.current_state}' for service '#{@resource[:name]}'")
end
debug("Service #{@resource[:name]} is #{w32ss.current_state}")
return state
rescue Win32::Service::Error => detail
- raise Puppet::Error.new("Cannot get status of #{@resource[:name]}, error was: #{detail}" )
+ raise Puppet::Error.new("Cannot get status of #{@resource[:name]}, error was: #{detail}", detail )
end
# returns all providers for all existing services and startup state
def self.instances
Win32::Service.services.collect { |s| new(:name => s.service_name) }
end
end
diff --git a/lib/puppet/provider/ssh_authorized_key/parsed.rb b/lib/puppet/provider/ssh_authorized_key/parsed.rb
index cb08b593f..af0e082ea 100644
--- a/lib/puppet/provider/ssh_authorized_key/parsed.rb
+++ b/lib/puppet/provider/ssh_authorized_key/parsed.rb
@@ -1,89 +1,89 @@
require 'puppet/provider/parsedfile'
Puppet::Type.type(:ssh_authorized_key).provide(
:parsed,
:parent => Puppet::Provider::ParsedFile,
:filetype => :flat,
:default_target => ''
) do
desc "Parse and generate authorized_keys files for SSH."
text_line :comment, :match => /^\s*#/
text_line :blank, :match => /^\s*$/
record_line :parsed,
:fields => %w{options type key name},
:optional => %w{options},
:rts => /^\s+/,
- :match => /^(?:(.+) )?(ssh-dss|ssh-rsa|ecdsa-sha2-nistp256|ecdsa-sha2-nistp384|ecdsa-sha2-nistp521) ([^ ]+) ?(.*)$/,
+ :match => /^(?:(.+) )?(ssh-dss|ssh-ed25519|ssh-rsa|ecdsa-sha2-nistp256|ecdsa-sha2-nistp384|ecdsa-sha2-nistp521) ([^ ]+) ?(.*)$/,
:post_parse => proc { |h|
h[:name] = "" if h[:name] == :absent
h[:options] ||= [:absent]
h[:options] = Puppet::Type::Ssh_authorized_key::ProviderParsed.parse_options(h[:options]) if h[:options].is_a? String
},
:pre_gen => proc { |h|
h[:options] = [] if h[:options].include?(:absent)
h[:options] = h[:options].join(',')
}
record_line :key_v1,
:fields => %w{options bits exponent modulus name},
:optional => %w{options},
:rts => /^\s+/,
:match => /^(?:(.+) )?(\d+) (\d+) (\d+)(?: (.+))?$/
def dir_perm
0700
end
def file_perm
0600
end
def user
- uid = Puppet::FileSystem::File.new(target).stat.uid
+ uid = Puppet::FileSystem.stat(target).uid
Etc.getpwuid(uid).name
end
def flush
raise Puppet::Error, "Cannot write SSH authorized keys without user" unless @resource.should(:user)
raise Puppet::Error, "User '#{@resource.should(:user)}' does not exist" unless Puppet::Util.uid(@resource.should(:user))
# ParsedFile usually calls backup_target much later in the flush process,
# but our SUID makes that fail to open filebucket files for writing.
# Fortunately, there's already logic to make sure it only ever happens once,
# so calling it here supresses the later attempt by our superclass's flush method.
self.class.backup_target(target)
Puppet::Util::SUIDManager.asuser(@resource.should(:user)) do
- unless Puppet::FileSystem::File.exist?(dir = File.dirname(target))
+ unless Puppet::FileSystem.exist?(dir = File.dirname(target))
Puppet.debug "Creating #{dir}"
Dir.mkdir(dir, dir_perm)
end
super
File.chmod(file_perm, target)
end
end
# parse sshv2 option strings, wich is a comma separated list of
# either key="values" elements or bare-word elements
def self.parse_options(options)
result = []
scanner = StringScanner.new(options)
while !scanner.eos?
scanner.skip(/[ \t]*/)
# scan a long option
if out = scanner.scan(/[-a-z0-9A-Z_]+=\".*?\"/) or out = scanner.scan(/[-a-z0-9A-Z_]+/)
result << out
else
# found an unscannable token, let's abort
break
end
# eat a comma
scanner.skip(/[ \t]*,[ \t]*/)
end
result
end
end
diff --git a/lib/puppet/provider/user/aix.rb b/lib/puppet/provider/user/aix.rb
index 0831f2e26..e75763a0d 100644
--- a/lib/puppet/provider/user/aix.rb
+++ b/lib/puppet/provider/user/aix.rb
@@ -1,369 +1,369 @@
#
# User Puppet provider for AIX. It uses standard commands to manage users:
# mkuser, rmuser, lsuser, chuser
#
# Notes:
# - AIX users can have expiry date defined with minute granularity,
# but puppet does not allow it. There is a ticket open for that (#5431)
# - AIX maximum password age is in WEEKs, not days
#
-# See http://projects.puppetlabs.com/projects/puppet/wiki/Development_Provider_Development
+# See http://docs.puppetlabs.com/guides/provider_development.html
# for more information
#
# Author:: Hector Rivas Gandara <keymon@gmail.com>
#
require 'puppet/provider/aixobject'
require 'tempfile'
require 'date'
Puppet::Type.type(:user).provide :aix, :parent => Puppet::Provider::AixObject do
desc "User management for AIX."
- # This will the the default provider for this platform
+ # This will the default provider for this platform
defaultfor :operatingsystem => :aix
confine :operatingsystem => :aix
# Commands that manage the element
commands :list => "/usr/sbin/lsuser"
commands :add => "/usr/bin/mkuser"
commands :delete => "/usr/sbin/rmuser"
commands :modify => "/usr/bin/chuser"
commands :lsgroup => "/usr/sbin/lsgroup"
commands :chpasswd => "/bin/chpasswd"
# Provider features
has_features :manages_aix_lam
- has_features :manages_homedir, :manages_passwords
+ has_features :manages_homedir, :manages_passwords, :manages_shell
has_features :manages_expiry, :manages_password_age
# Attribute verification (TODO)
#verify :gid, "GID must be a string or int of a valid group" do |value|
# value.is_a? String || value.is_a? Integer
#end
#
#verify :groups, "Groups must be comma-separated" do |value|
# value !~ /\s/
#end
# User attributes to ignore from AIX output.
def self.attribute_ignore
["name"]
end
# AIX attributes to properties mapping.
#
# Valid attributes to be managed by this provider.
# It is a list with of hash
# :aix_attr AIX command attribute name
# :puppet_prop Puppet propertie name
# :to Method to adapt puppet property to aix command value. Optional.
# :from Method to adapt aix command value to puppet property. Optional
self.attribute_mapping = [
{:aix_attr => :pgrp, :puppet_prop => :gid,
:to => :gid_to_attr,
:from => :gid_from_attr },
{:aix_attr => :id, :puppet_prop => :uid},
{:aix_attr => :groups, :puppet_prop => :groups},
{:aix_attr => :home, :puppet_prop => :home},
{:aix_attr => :shell, :puppet_prop => :shell},
{:aix_attr => :expires, :puppet_prop => :expiry,
:to => :expiry_to_attr,
:from => :expiry_from_attr },
{:aix_attr => :maxage, :puppet_prop => :password_max_age},
{:aix_attr => :minage, :puppet_prop => :password_min_age},
{:aix_attr => :attributes, :puppet_prop => :attributes},
{ :aix_attr => :gecos, :puppet_prop => :comment },
]
#--------------
# Command definition
# Return the IA module arguments based on the resource param ia_load_module
def get_ia_module_args
if @resource[:ia_load_module]
["-R", @resource[:ia_load_module].to_s]
else
[]
end
end
# List groups and Ids
def lsgroupscmd(value=@resource[:name])
[command(:lsgroup)] +
self.get_ia_module_args +
["-a", "id", value]
end
def lscmd(value=@resource[:name])
[self.class.command(:list), "-c"] + self.get_ia_module_args + [ value]
end
def lsallcmd()
lscmd("ALL")
end
def addcmd(extra_attrs = [])
# Here we use the @resource.to_hash to get the list of provided parameters
# Puppet does not call to self.<parameter>= method if it does not exists.
#
# It gets an extra list of arguments to add to the user.
[self.class.command(:add)] + self.get_ia_module_args +
self.hash2args(@resource.to_hash) +
extra_attrs + [@resource[:name]]
end
# Get modify command. Set translate=false if no mapping must be used.
# Needed for special properties like "attributes"
def modifycmd(hash = property_hash)
args = self.hash2args(hash)
return nil if args.empty?
[self.class.command(:modify)] + self.get_ia_module_args +
args + [@resource[:name]]
end
def deletecmd
[self.class.command(:delete)] + self.get_ia_module_args + [@resource[:name]]
end
#--------------
# We overwrite the create function to change the password after creation.
def create
super
# Reset the password if needed
self.password = @resource[:password] if @resource[:password]
end
def get_arguments(key, value, mapping, objectinfo)
# In the case of attributes, return a list of key=vlaue
if key == :attributes
raise Puppet::Error, "Attributes must be a list of pairs key=value on #{@resource.class.name}[#{@resource.name}]" \
unless value and value.is_a? Hash
return value.map { |k,v| k.to_s.strip + "=" + v.to_s.strip}
end
super(key, value, mapping, objectinfo)
end
# Get the groupname from its id
def groupname_by_id(gid)
groupname=nil
execute(lsgroupscmd("ALL")).each_line { |entry|
attrs = self.parse_attr_list(entry, nil)
if attrs and attrs.include? :id and gid == attrs[:id].to_i
groupname = entry.split(" ")[0]
end
}
groupname
end
# Get the groupname from its id
def groupid_by_name(groupname)
attrs = self.parse_attr_list(execute(lsgroupscmd(groupname)).split("\n")[0], nil)
attrs ? attrs[:id].to_i : nil
end
# Check that a group exists and is valid
def verify_group(value)
if value.is_a? Integer or value.is_a? Fixnum
groupname = groupname_by_id(value)
raise ArgumentError, "AIX group must be a valid existing group" unless groupname
else
raise ArgumentError, "AIX group must be a valid existing group" unless groupid_by_name(value)
groupname = value
end
groupname
end
# The user's primary group. Can be specified numerically or by name.
def gid_to_attr(value)
verify_group(value)
end
# Get the group gid from its name
def gid_from_attr(value)
groupid_by_name(value)
end
# The expiry date for this user. Must be provided in
# a zero padded YYYY-MM-DD HH:MM format
def expiry_to_attr(value)
# For chuser the expires parameter is a 10-character string in the MMDDhhmmyy format
# that is,"%m%d%H%M%y"
newdate = '0'
if value.is_a? String and value!="0000-00-00"
d = DateTime.parse(value, "%Y-%m-%d %H:%M")
newdate = d.strftime("%m%d%H%M%y")
end
newdate
end
def expiry_from_attr(value)
if value =~ /(..)(..)(..)(..)(..)/
#d= DateTime.parse("20#{$5}-#{$1}-#{$2} #{$3}:#{$4}")
#expiry_date = d.strftime("%Y-%m-%d %H:%M")
#expiry_date = d.strftime("%Y-%m-%d")
expiry_date = "20#{$5}-#{$1}-#{$2}"
else
Puppet.warn("Could not convert AIX expires date '#{value}' on #{@resource.class.name}[#{@resource.name}]") \
unless value == '0'
expiry_date = :absent
end
expiry_date
end
def open_security_passwd
# helper method for tests
File.open("/etc/security/passwd", 'r')
end
#--------------------------------
# Getter and Setter
# When the provider is initialized, create getter/setter methods for each
# property our resource type supports.
# If setter or getter already defined it will not be overwritten
#- **password**
# The user's password, in whatever encrypted format the local machine
# requires. Be sure to enclose any value that includes a dollar sign ($)
# in single quotes ('). Requires features manages_passwords.
#
# Retrieve the password parsing directly the /etc/security/passwd
def password
password = :absent
user = @resource[:name]
f = open_security_passwd
# Skip to the user
f.each_line { |l| break if l =~ /^#{user}:\s*$/ }
if ! f.eof?
f.each_line { |l|
# If there is a new user stanza, stop
break if l =~ /^\S*:\s*$/
# If the password= entry is found, return it, stripping trailing space
if l =~ /^\s*password\s*=\s*(\S*)\s*$/
password = $1; break;
end
}
end
f.close()
return password
end
def password=(value)
user = @resource[:name]
# Puppet execute does not support strings as input, only files.
tmpfile = Tempfile.new('puppet_#{user}_pw')
tmpfile << "#{user}:#{value}\n"
tmpfile.close()
# Options '-e', '-c', use encrypted password and clear flags
# Must receive "user:enc_password" as input
# command, arguments = {:failonfail => true, :combine => true}
# Fix for bugs #11200 and #10915
cmd = [self.class.command(:chpasswd), get_ia_module_args, '-e', '-c', user].flatten
begin
output = execute(cmd, {:failonfail => false, :combine => true, :stdinfile => tmpfile.path })
# chpasswd can return 1, even on success (at least on AIX 6.1); empty output indicates success
if output != ""
raise Puppet::ExecutionFailure, "chpasswd said #{output}"
end
rescue Puppet::ExecutionFailure => detail
- raise Puppet::Error, "Could not set #{param} on #{@resource.class.name}[#{@resource.name}]: #{detail}"
+ raise Puppet::Error, "Could not set #{param} on #{@resource.class.name}[#{@resource.name}]: #{detail}", detail.backtrace
ensure
tmpfile.delete()
end
end
def managed_attribute_keys(hash)
managed_attributes ||= @resource.original_parameters[:attributes] || hash.keys.map{|k| k.to_s}
managed_attributes.map {|attr| key, value = attr.split("="); key.strip.to_sym}
end
def should_include?(key, managed_keys)
!self.class.attribute_mapping_from.include?(key) and
!self.class.attribute_ignore.include?(key) and
managed_keys.include?(key)
end
def filter_attributes(hash)
# Return only managed attributtes.
managed_keys = managed_attribute_keys(hash)
results = hash.select {
|k,v| should_include?(k, managed_keys)
}.inject({}) {
|hash, array| hash[array[0]] = array[1]; hash
}
results
end
def attributes
filter_attributes(getosinfo(false))
end
def attributes=(attr_hash)
#self.class.validate(param, value)
param = :attributes
cmd = modifycmd({param => filter_attributes(attr_hash)})
if cmd
begin
execute(cmd)
rescue Puppet::ExecutionFailure => detail
- raise Puppet::Error, "Could not set #{param} on #{@resource.class.name}[#{@resource.name}]: #{detail}"
+ raise Puppet::Error, "Could not set #{param} on #{@resource.class.name}[#{@resource.name}]: #{detail}", detail.backtrace
end
end
end
# UNSUPPORTED
#- **profile_membership**
# Whether specified roles should be treated as the only roles
# of which the user is a member or whether they should merely
# be treated as the minimum membership list. Valid values are
# `inclusive`, `minimum`.
# UNSUPPORTED
#- **profiles**
# The profiles the user has. Multiple profiles should be
# specified as an array. Requires features manages_solaris_rbac.
# UNSUPPORTED
#- **project**
# The name of the project associated with a user Requires features
# manages_solaris_rbac.
# UNSUPPORTED
#- **role_membership**
# Whether specified roles should be treated as the only roles
# of which the user is a member or whether they should merely
# be treated as the minimum membership list. Valid values are
# `inclusive`, `minimum`.
# UNSUPPORTED
#- **roles**
# The roles the user has. Multiple roles should be
# specified as an array. Requires features manages_solaris_rbac.
# UNSUPPORTED
#- **key_membership**
# Whether specified key value pairs should be treated as the only
# attributes
# of the user or whether they should merely
# be treated as the minimum list. Valid values are `inclusive`,
# `minimum`.
# UNSUPPORTED
#- **keys**
# Specify user attributes in an array of keyvalue pairs Requires features
# manages_solaris_rbac.
# UNSUPPORTED
#- **allowdupe**
# Whether to allow duplicate UIDs. Valid values are `true`, `false`.
# UNSUPPORTED
#- **auths**
# The auths the user has. Multiple auths should be
# specified as an array. Requires features manages_solaris_rbac.
# UNSUPPORTED
#- **auth_membership**
# Whether specified auths should be treated as the only auths
# of which the user is a member or whether they should merely
# be treated as the minimum membership list. Valid values are
# `inclusive`, `minimum`.
# UNSUPPORTED
end
diff --git a/lib/puppet/provider/user/directoryservice.rb b/lib/puppet/provider/user/directoryservice.rb
index 6a59c4bee..e93663fca 100644
--- a/lib/puppet/provider/user/directoryservice.rb
+++ b/lib/puppet/provider/user/directoryservice.rb
@@ -1,668 +1,671 @@
require 'puppet'
require 'facter/util/plist'
require 'base64'
Puppet::Type.type(:user).provide :directoryservice do
desc "User management on OS X."
## ##
## Provider Settings ##
## ##
# Provider command declarations
commands :uuidgen => '/usr/bin/uuidgen'
commands :dsimport => '/usr/bin/dsimport'
commands :dscl => '/usr/bin/dscl'
commands :plutil => '/usr/bin/plutil'
commands :dscacheutil => '/usr/bin/dscacheutil'
# Provider confines and defaults
confine :operatingsystem => :darwin
defaultfor :operatingsystem => :darwin
# Need this to create getter/setter methods automagically
# This command creates methods that return @property_hash[:value]
mk_resource_methods
# JJM: OS X can manage passwords.
has_feature :manages_passwords
# 10.8 Passwords use a PBKDF2 salt value
has_features :manages_password_salt
+ #provider can set the user's shell
+ has_feature :manages_shell
+
## ##
## Class Methods ##
## ##
# This method exists to map the dscl values to the correct Puppet
# properties. This stays relatively consistent, but who knows what
# Apple will do next year...
def self.ds_to_ns_attribute_map
{
'RecordName' => :name,
'PrimaryGroupID' => :gid,
'NFSHomeDirectory' => :home,
'UserShell' => :shell,
'UniqueID' => :uid,
'RealName' => :comment,
'Password' => :password,
'GeneratedUID' => :guid,
'IPAddress' => :ip_address,
'ENetAddress' => :en_address,
'GroupMembership' => :members,
}
end
def self.ns_to_ds_attribute_map
@ns_to_ds_attribute_map ||= ds_to_ns_attribute_map.invert
end
# Prefetching is necessary to use @property_hash inside any setter methods.
# self.prefetch uses self.instances to gather an array of user instances
# on the system, and then populates the @property_hash instance variable
# with attribute data for the specific instance in question (i.e. it
# gathers the 'is' values of the resource into the @property_hash instance
# variable so you don't have to read from the system every time you need
# to gather the 'is' values for a resource. The downside here is that
# populating this instance variable for every resource on the system
# takes time and front-loads your Puppet run.
def self.prefetch(resources)
instances.each do |prov|
if resource = resources[prov.name]
resource.provider = prov
end
end
end
# This method assembles an array of provider instances containing
# information about every instance of the user type on the system (i.e.
# every user and its attributes). The `puppet resource` command relies
# on self.instances to gather an array of user instances in order to
# display its output.
def self.instances
get_all_users.collect do |user|
self.new(generate_attribute_hash(user))
end
end
# Return an array of hashes containing information about every user on
# the system.
def self.get_all_users
Plist.parse_xml(dscl '-plist', '.', 'readall', '/Users')
end
# This method accepts an individual user plist, passed as a hash, and
# strips the dsAttrTypeStandard: prefix that dscl adds for each key.
# An attribute hash is assembled and returned from the properties
# supported by the user type.
def self.generate_attribute_hash(input_hash)
attribute_hash = {}
input_hash.keys.each do |key|
ds_attribute = key.sub("dsAttrTypeStandard:", "")
next unless ds_to_ns_attribute_map.keys.include?(ds_attribute)
ds_value = input_hash[key]
case ds_to_ns_attribute_map[ds_attribute]
when :gid, :uid
# OS X stores objects like uid/gid as strings.
# Try casting to an integer for these cases to be
# consistent with the other providers and the group type
# validation
begin
ds_value = Integer(ds_value[0])
rescue ArgumentError
ds_value = ds_value[0]
end
else ds_value = ds_value[0]
end
attribute_hash[ds_to_ns_attribute_map[ds_attribute]] = ds_value
end
attribute_hash[:ensure] = :present
attribute_hash[:provider] = :directoryservice
attribute_hash[:shadowhashdata] = get_attribute_from_dscl('Users', attribute_hash[:name], 'ShadowHashData')
##############
# Get Groups #
##############
groups_array = []
get_list_of_groups.each do |group|
if group["dsAttrTypeStandard:GroupMembership"] and group["dsAttrTypeStandard:GroupMembership"].include?(attribute_hash[:name])
groups_array << group["dsAttrTypeStandard:RecordName"][0]
end
if group["dsAttrTypeStandard:GroupMembers"] and group["dsAttrTypeStandard:GroupMembers"].include?(attribute_hash[:guid])
groups_array << group["dsAttrTypeStandard:RecordName"][0]
end
end
attribute_hash[:groups] = groups_array.uniq.sort.join(',')
################################
# Get Password/Salt/Iterations #
################################
if (Puppet::Util::Package.versioncmp(get_os_version, '10.7') == -1)
attribute_hash[:password] = get_sha1(attribute_hash[:guid])
else
if attribute_hash[:shadowhashdata].empty?
attribute_hash[:password] = '*'
else
embedded_binary_plist = get_embedded_binary_plist(attribute_hash[:shadowhashdata])
if embedded_binary_plist['SALTED-SHA512']
attribute_hash[:password] = get_salted_sha512(embedded_binary_plist)
else
attribute_hash[:password] = get_salted_sha512_pbkdf2('entropy', embedded_binary_plist)
attribute_hash[:salt] = get_salted_sha512_pbkdf2('salt', embedded_binary_plist)
attribute_hash[:iterations] = get_salted_sha512_pbkdf2('iterations', embedded_binary_plist)
end
end
end
attribute_hash
end
def self.get_os_version
@os_version ||= Facter.value(:macosx_productversion_major)
end
# Use dscl to retrieve an array of hashes containing attributes about all
# of the local groups on the machine.
def self.get_list_of_groups
@groups ||= Plist.parse_xml(dscl '-plist', '.', 'readall', '/Groups')
end
# Perform a dscl lookup at the path specified for the specific keyname
# value. The value returned is the first item within the array returned
# from dscl
def self.get_attribute_from_dscl(path, username, keyname)
Plist.parse_xml(dscl '-plist', '.', 'read', "/#{path}/#{username}", keyname)
end
# The plist embedded in the ShadowHashData key is a binary plist. The
# facter/util/plist library doesn't read binary plists, so we need to
# extract the binary plist, convert it to XML, and return it.
def self.get_embedded_binary_plist(shadow_hash_data)
embedded_binary_plist = Array(shadow_hash_data['dsAttrTypeNative:ShadowHashData'][0].delete(' ')).pack('H*')
convert_binary_to_xml(embedded_binary_plist)
end
# This method will accept a hash that has been returned from Plist::parse_xml
# and convert it to a binary plist (string value).
def self.convert_xml_to_binary(plist_data)
Puppet.debug('Converting XML plist to binary')
Puppet.debug('Executing: \'plutil -convert binary1 -o - -\'')
IO.popen('plutil -convert binary1 -o - -', 'r+') do |io|
io.write Plist::Emit.dump(plist_data)
io.close_write
@converted_plist = io.read
end
@converted_plist
end
# This method will accept a binary plist (as a string) and convert it to a
# hash via Plist::parse_xml.
def self.convert_binary_to_xml(plist_data)
Puppet.debug('Converting binary plist to XML')
Puppet.debug('Executing: \'plutil -convert xml1 -o - -\'')
IO.popen('plutil -convert xml1 -o - -', 'r+') do |io|
io.write plist_data
io.close_write
@converted_plist = io.read
end
Puppet.debug('Converting XML values to a hash.')
Plist::parse_xml(@converted_plist)
end
# The salted-SHA512 password hash in 10.7 is stored in the 'SALTED-SHA512'
# key as binary data. That data is extracted and converted to a hex string.
def self.get_salted_sha512(embedded_binary_plist)
embedded_binary_plist['SALTED-SHA512'].string.unpack("H*")[0]
end
# This method reads the passed embedded_binary_plist hash and returns values
# according to which field is passed. Arguments passed are the hash
# containing the value read from the 'ShadowHashData' key in the User's
# plist, and the field to be read (one of 'entropy', 'salt', or 'iterations')
def self.get_salted_sha512_pbkdf2(field, embedded_binary_plist)
case field
when 'salt', 'entropy'
embedded_binary_plist['SALTED-SHA512-PBKDF2'][field].string.unpack('H*').first
when 'iterations'
Integer(embedded_binary_plist['SALTED-SHA512-PBKDF2'][field])
else
raise Puppet::Error, 'Puppet has tried to read an incorrect value from the ' +
"SALTED-SHA512-PBKDF2 hash. Acceptable fields are 'salt', " +
"'entropy', or 'iterations'."
end
end
# In versions 10.5 and 10.6 of OS X, the password hash is stored in a file
# in the /var/db/shadow/hash directory that matches the GUID of the user.
def self.get_sha1(guid)
password_hash = nil
password_hash_file = "#{password_hash_dir}/#{guid}"
- if Puppet::FileSystem::File.exist?(password_hash_file) and File.file?(password_hash_file)
+ if Puppet::FileSystem.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
f.close
end
password_hash
end
## ##
## Ensurable Methods ##
## ##
def exists?
begin
dscl '.', 'read', "/Users/#{@resource.name}"
rescue Puppet::ExecutionFailure => e
Puppet.debug("User was not found, dscl returned: #{e.inspect}")
return false
end
true
end
# This method is called if ensure => present is passed and the exists?
# method returns false. Dscl will directly set most values, but the
# setter methods will be used for any exceptions.
def create
create_new_user(@resource.name)
# Retrieve the user's GUID
@guid = self.class.get_attribute_from_dscl('Users', @resource.name, 'GeneratedUID')['dsAttrTypeStandard:GeneratedUID'][0]
# Get an array of valid User type properties
valid_properties = Puppet::Type.type('User').validproperties
# Iterate through valid User type properties
valid_properties.each do |attribute|
next if attribute == :ensure
value = @resource.should(attribute)
# Value defaults
if value.nil?
value = case attribute
when :gid
'20'
when :uid
next_system_id
when :comment
@resource.name
when :shell
'/bin/bash'
when :home
"/Users/#{@resource.name}"
else
nil
end
end
# Ensure group names are converted to integers.
value = Puppet::Util.gid(value) if attribute == :gid
## Set values ##
# For the :password and :groups properties, call the setter methods
# to enforce those values. For everything else, use dscl with the
# ns_to_ds_attribute_map to set the appropriate values.
if value != "" and not value.nil?
case attribute
when :password
self.password = value
when :iterations
self.iterations = value
when :salt
self.salt = value
when :groups
value.split(',').each do |group|
merge_attribute_with_dscl('Groups', group, 'GroupMembership', @resource.name)
merge_attribute_with_dscl('Groups', group, 'GroupMembers', @guid)
end
else
merge_attribute_with_dscl('Users', @resource.name, self.class.ns_to_ds_attribute_map[attribute], value)
end
end
end
end
# This method is called when ensure => absent has been set.
# Deleting a user is handled by dscl
def delete
dscl '.', '-delete', "/Users/#{@resource.name}"
end
## ##
## Getter/Setter Methods ##
## ##
# In the setter method we're only going to take action on groups for which
# the user is not currently a member.
def groups=(value)
guid = self.class.get_attribute_from_dscl('Users', @resource.name, 'GeneratedUID')['dsAttrTypeStandard:GeneratedUID'][0]
groups_to_add = value.split(',') - groups.split(',')
groups_to_add.each do |group|
merge_attribute_with_dscl('Groups', group, 'GroupMembership', @resource.name)
merge_attribute_with_dscl('Groups', group, 'GroupMembers', guid)
end
end
# If you thought GETTING a password was bad, try SETTING it. This method
# makes me want to cry. A thousand tears...
#
# I've been unsuccessful in tracking down a way to set the password for
# a user using dscl that DOESN'T require passing it as plaintext. We were
# also unable to get dsimport to work like this. Due to these downfalls,
# the sanest method requires opening the user's plist, dropping in the
# password hash, and serializing it back to disk. The problems with THIS
# method revolve around dscl. Any time you directly modify a user's plist,
# you need to flush the cache that dscl maintains.
def password=(value)
if (Puppet::Util::Package.versioncmp(self.class.get_os_version, '10.7') == -1)
write_sha1_hash(value)
else
if self.class.get_os_version == '10.7'
if value.length != 136
raise Puppet::Error, "OS X 10.7 requires a Salted SHA512 hash password of 136 characters. Please check your password and try again."
end
else
if value.length != 256
raise Puppet::Error, "OS X versions > 10.7 require a Salted SHA512 PBKDF2 password hash of 256 characters. Please check your password and try again."
end
end
# Methods around setting the password on OS X are the ONLY methods that
# cannot use dscl (because the only way to set it via dscl is by passing
# a plaintext password - which is bad). Because of this, we have to change
# the user's plist directly. DSCL has its own caching mechanism, which
# means that every time we call dscl in this provider we're not directly
# changing values on disk (instead, those calls are cached and written
# to disk according to Apple's prioritization algorithms). When Puppet
# needs to set the password property on OS X > 10.6, the provider has to
# tell dscl to write its cache to disk before modifying the user's
# plist. The 'dscacheutil -flushcache' command does this. Another issue
# is how fast Puppet makes calls to dscl and how long it takes dscl to
# enter those calls into its cache. We have to sleep for 2 seconds before
# flushing the dscl cache to allow all dscl calls to get INTO the cache
# first. This could be made faster (and avoid a sleep call) by finding
# a way to enter calls into the dscl cache faster. A sleep time of 1
# second would intermittantly require a second Puppet run to set
# properties, so 2 seconds seems to be the minimum working value.
sleep 2
flush_dscl_cache
write_password_to_users_plist(value)
# Since we just modified the user's plist, we need to flush the ds cache
# again so dscl can pick up on the changes we made.
flush_dscl_cache
end
end
# The iterations and salt properties, like the password property, can only
# be modified by directly changing the user's plist. Because of this fact,
# we have to treat the ds cache just like you would in the password=
# method.
def iterations=(value)
if (Puppet::Util::Package.versioncmp(self.class.get_os_version, '10.7') > 0)
sleep 2
flush_dscl_cache
users_plist = get_users_plist(@resource.name)
shadow_hash_data = get_shadow_hash_data(users_plist)
set_salted_pbkdf2(users_plist, shadow_hash_data, 'iterations', value)
flush_dscl_cache
end
end
# The iterations and salt properties, like the password property, can only
# be modified by directly changing the user's plist. Because of this fact,
# we have to treat the ds cache just like you would in the password=
# method.
def salt=(value)
if (Puppet::Util::Package.versioncmp(self.class.get_os_version, '10.7') > 0)
sleep 2
flush_dscl_cache
users_plist = get_users_plist(@resource.name)
shadow_hash_data = get_shadow_hash_data(users_plist)
set_salted_pbkdf2(users_plist, shadow_hash_data, 'salt', value)
flush_dscl_cache
end
end
#####
# Dynamically create setter methods for dscl properties
#####
#
# Setter methods are only called when a resource currently has a value for
# that property and it needs changed (true here since all of these values
# have a default that is set in the create method). We don't want to merge
# in additional values if an incorrect value is set, we want to CHANGE it.
# When using the -change argument in dscl, the old value needs to be passed
# first (followed by the new value). Because of this, we get the current
# value from the @property_hash variable and then use the value passed as
# the new value. Because we're prefetching instances of the provider, it's
# possible that the value determined at the start of the run may be stale
# (i.e. someone changed the value by hand during a Puppet run) - if that's
# the case we rescue the error from dscl and alert the user.
#
# In the event that the user doesn't HAVE a value for the attribute, the
# provider should use the -merge option with dscl to add the attribute value
# for the user record
['home', 'uid', 'gid', 'comment', 'shell'].each do |setter_method|
define_method("#{setter_method}=") do |value|
if @property_hash[setter_method.intern]
begin
dscl '.', '-change', "/Users/#{resource.name}", self.class.ns_to_ds_attribute_map[setter_method.intern], @property_hash[setter_method.intern], value
rescue Puppet::ExecutionFailure => e
raise Puppet::Error, "Cannot set the #{setter_method} value of '#{value}' for user " +
- "#{@resource.name} due to the following error: #{e.inspect}"
+ "#{@resource.name} due to the following error: #{e.inspect}", e.backtrace
end
else
begin
dscl '.', '-merge', "/Users/#{resource.name}", self.class.ns_to_ds_attribute_map[setter_method.intern], value
rescue Puppet::ExecutionFailure => e
raise Puppet::Error, "Cannot set the #{setter_method} value of '#{value}' for user " +
- "#{@resource.name} due to the following error: #{e.inspect}"
+ "#{@resource.name} due to the following error: #{e.inspect}", e.backtrace
end
end
end
end
## ##
## Helper Methods ##
## ##
def users_plist_dir
'/var/db/dslocal/nodes/Default/users'
end
def self.password_hash_dir
'/var/db/shadow/hash'
end
# This method will merge in a given value using dscl
def merge_attribute_with_dscl(path, username, keyname, value)
begin
dscl '.', '-merge', "/#{path}/#{username}", keyname, value
rescue Puppet::ExecutionFailure => detail
- raise Puppet::Error, "Could not set the dscl #{keyname} key with value: #{value} - #{detail.inspect}"
+ raise Puppet::Error, "Could not set the dscl #{keyname} key with value: #{value} - #{detail.inspect}", detail.backtrace
end
end
# Create the new user with dscl
def create_new_user(username)
dscl '.', '-create', "/Users/#{username}"
end
# Get the next available uid on the system by getting a list of user ids,
# sorting them, grabbing the last one, and adding a 1. Scientific stuff here.
def next_system_id(min_id=20)
dscl_output = dscl '.', '-list', '/Users', 'uid'
# We're ok with throwing away negative uids here. Also, remove nil values.
user_ids = dscl_output.split.compact.collect { |l| l.to_i if l.match(/^\d+$/) }
ids = user_ids.compact!.sort! { |a,b| a.to_f <=> b.to_f }
# We're just looking for an unused id in our sorted array.
ids.each_index do |i|
next_id = ids[i] + 1
return next_id if ids[i+1] != next_id and next_id >= min_id
end
end
# This method is only called on version 10.7 or greater. On 10.7 machines,
# passwords are set using a salted-SHA512 hash, and on 10.8 machines,
# passwords are set using PBKDF2. It's possible to have users on 10.8
# who have upgraded from 10.7 and thus have a salted-SHA512 password hash.
# If we encounter this, do what 10.8 does - remove that key and give them
# a 10.8-style PBKDF2 password.
def write_password_to_users_plist(value)
users_plist = get_users_plist(@resource.name)
shadow_hash_data = get_shadow_hash_data(users_plist)
if self.class.get_os_version == '10.7'
set_salted_sha512(users_plist, shadow_hash_data, value)
else
# It's possible that a user could exist on the system and NOT have
# a ShadowHashData key (especially if the system was upgraded from 10.6).
# In this case, a conditional check is needed to determine if the
# shadow_hash_data variable is a Hash (it would be false if the key
# didn't exist for this user on the system). If the shadow_hash_data
# variable IS a Hash and contains the 'SALTED-SHA512' key (indicating an
# older 10.7-style password hash), it will be deleted and a newer
# 10.8-style (PBKDF2) password hash will be generated.
if (shadow_hash_data.class == Hash) && (shadow_hash_data.has_key?('SALTED-SHA512'))
shadow_hash_data.delete('SALTED-SHA512')
end
set_salted_pbkdf2(users_plist, shadow_hash_data, 'entropy', value)
end
end
def flush_dscl_cache
dscacheutil '-flushcache'
end
def get_users_plist(username)
# This method will retrieve the data stored in a user's plist and
# return it as a native Ruby hash.
Plist::parse_xml(plutil('-convert', 'xml1', '-o', '/dev/stdout', "#{users_plist_dir}/#{username}.plist"))
end
# This method will return the binary plist that's embedded in the
# ShadowHashData key of a user's plist, or false if it doesn't exist.
def get_shadow_hash_data(users_plist)
if users_plist['ShadowHashData']
password_hash_plist = users_plist['ShadowHashData'][0].string
self.class.convert_binary_to_xml(password_hash_plist)
else
false
end
end
# This method will embed the binary plist data comprising the user's
# password hash (and Salt/Iterations value if the OS is 10.8 or greater)
# into the ShadowHashData key of the user's plist.
def set_shadow_hash_data(users_plist, binary_plist)
if users_plist.has_key?('ShadowHashData')
users_plist['ShadowHashData'][0].string = binary_plist
else
users_plist['ShadowHashData'] = [new_stringio_object(binary_plist)]
end
write_users_plist_to_disk(users_plist)
end
# This method returns a new StringIO object. Why does it exist?
# Well, StringIO objects have their own 'serial number', so when
# writing rspec tests it's difficult to compare StringIO objects
# due to this serial number. If this action is wrapped in its own
# method, it can be mocked for easier testing.
def new_stringio_object(value = '')
StringIO.new(value)
end
# This method accepts an argument of a hex password hash, and base64
# decodes it into a format that OS X 10.7 and 10.8 will store
# in the user's plist.
def base64_decode_string(value)
Base64.decode64([[value].pack("H*")].pack("m").strip)
end
# Puppet requires a salted-sha512 password hash for 10.7 users to be passed
# in Hex, but the embedded plist stores that value as a Base64 encoded
# string. This method converts the string and calls the
# set_shadow_hash_data method to serialize and write the plist to disk.
def set_salted_sha512(users_plist, shadow_hash_data, value)
unless shadow_hash_data
shadow_hash_data = Hash.new
shadow_hash_data['SALTED-SHA512'] = new_stringio_object
end
shadow_hash_data['SALTED-SHA512'].string = base64_decode_string(value)
binary_plist = self.class.convert_xml_to_binary(shadow_hash_data)
set_shadow_hash_data(users_plist, binary_plist)
end
# This method accepts a passed value and one of three fields: 'salt',
# 'entropy', or 'iterations'. These fields correspond with the fields
# utilized in a PBKDF2 password hashing system
# (see http://en.wikipedia.org/wiki/PBKDF2 ) where 'entropy' is the
# password hash, 'salt' is the password hash salt value, and 'iterations'
# is an integer recommended to be > 10,000. The remaining arguments are
# the user's plist itself, and the shadow_hash_data hash containing the
# existing PBKDF2 values.
def set_salted_pbkdf2(users_plist, shadow_hash_data, field, value)
shadow_hash_data = Hash.new unless shadow_hash_data
shadow_hash_data['SALTED-SHA512-PBKDF2'] = Hash.new unless shadow_hash_data['SALTED-SHA512-PBKDF2']
case field
when 'salt', 'entropy'
shadow_hash_data['SALTED-SHA512-PBKDF2'][field] = new_stringio_object unless shadow_hash_data['SALTED-SHA512-PBKDF2'][field]
shadow_hash_data['SALTED-SHA512-PBKDF2'][field].string = base64_decode_string(value)
when 'iterations'
shadow_hash_data['SALTED-SHA512-PBKDF2'][field] = Integer(value)
else
raise Puppet::Error "Puppet has tried to set an incorrect field for the 'SALTED-SHA512-PBKDF2' hash. Acceptable fields are 'salt', 'entropy', or 'iterations'."
end
# on 10.8, this field *must* contain 8 stars, or authentication will
# fail.
users_plist['passwd'] = ('*' * 8)
# Convert shadow_hash_data to a binary plist, and call the
# set_shadow_hash_data method to serialize and write the data
# back to the user's plist.
binary_plist = self.class.convert_xml_to_binary(shadow_hash_data)
set_shadow_hash_data(users_plist, binary_plist)
end
# This method will accept a plist in XML format, save it to disk, convert
# the plist to a binary format, and flush the dscl cache.
def write_users_plist_to_disk(users_plist)
Plist::Emit.save_plist(users_plist, "#{users_plist_dir}/#{@resource.name}.plist")
plutil'-convert', 'binary1', "#{users_plist_dir}/#{@resource.name}.plist"
end
# This is a simple wrapper method for writing values to a file.
def write_to_file(filename, value)
begin
File.open(filename, 'w') { |f| f.write(value)}
rescue Errno::EACCES => detail
- raise Puppet::Error, "Could not write to file #{filename}: #{detail}"
+ raise Puppet::Error, "Could not write to file #{filename}: #{detail}", detail.backtrace
end
end
def write_sha1_hash(value)
users_guid = self.class.get_attribute_from_dscl('Users', @resource.name, 'GeneratedUID')['dsAttrTypeStandard:GeneratedUID'][0]
password_hash_file = "#{self.class.password_hash_dir}/#{users_guid}"
write_to_file(password_hash_file, value)
# NBK: For shadow hashes, the user AuthenticationAuthority must contain a value of
# ";ShadowHash;". The LKDC in 10.5 makes this more interesting though as it
# will dynamically generate ;Kerberosv5;;username@LKDC:SHA1 attributes if
# missing. Thus we make sure we only set ;ShadowHash; if it is missing, and
# we can do this with the merge command. This allows people to continue to
# use other custom AuthenticationAuthority attributes without stomping on them.
#
# There is a potential problem here in that we're only doing this when setting
# the password, and the attribute could get modified at other times while the
# hash doesn't change and so this doesn't get called at all... but
# without switching all the other attributes to merge instead of create I can't
# see a simple enough solution for this that doesn't modify the user record
# every single time. This should be a rather rare edge case. (famous last words)
merge_attribute_with_dscl('Users', @resource.name, 'AuthenticationAuthority', ';ShadowHash;')
end
end
diff --git a/lib/puppet/provider/user/ldap.rb b/lib/puppet/provider/user/ldap.rb
index 4de89666c..6dbda5f9b 100644
--- a/lib/puppet/provider/user/ldap.rb
+++ b/lib/puppet/provider/user/ldap.rb
@@ -1,128 +1,128 @@
require 'puppet/provider/ldap'
Puppet::Type.type(:user).provide :ldap, :parent => Puppet::Provider::Ldap do
desc "User management via LDAP.
This provider requires that you have valid values for all of the
LDAP-related settings in `puppet.conf`, including `ldapbase`. You will
almost definitely need settings for `ldapuser` and `ldappassword` in order
for your clients to write to LDAP.
Note that this provider will automatically generate a UID for you if
you do not specify one, but it is a potentially expensive operation,
as it iterates across all existing users to pick the appropriate next one."
confine :feature => :ldap, :false => (Puppet[:ldapuser] == "")
- has_feature :manages_passwords
+ has_feature :manages_passwords, :manages_shell
manages(:posixAccount, :person).at("ou=People").named_by(:uid).and.maps :name => :uid,
:password => :userPassword,
:comment => :cn,
:uid => :uidNumber,
:gid => :gidNumber,
:home => :homeDirectory,
:shell => :loginShell
# Use the last field of a space-separated array as
# the sn. LDAP requires a surname, for some stupid reason.
manager.generates(:sn).from(:cn).with do |cn|
cn[0].split(/\s+/)[-1]
end
# Find the next uid after the current largest uid.
provider = self
manager.generates(:uidNumber).with do
largest = 500
if existing = provider.manager.search
existing.each do |hash|
next unless value = hash[:uid]
num = value[0].to_i
largest = num if num > largest
end
end
largest + 1
end
# Convert our gid to a group name, if necessary.
def gid=(value)
value = group2id(value) unless [Fixnum, Bignum].include?(value.class)
@property_hash[:gid] = value
end
# Find all groups this user is a member of in ldap.
def groups
# We want to cache the current result, so we know if we
# have to remove old values.
unless @property_hash[:groups]
unless result = group_manager.search("memberUid=#{name}")
return @property_hash[:groups] = :absent
end
return @property_hash[:groups] = result.collect { |r| r[:name] }.sort.join(",")
end
@property_hash[:groups]
end
# Manage the list of groups this user is a member of.
def groups=(values)
should = values.split(",")
if groups == :absent
is = []
else
is = groups.split(",")
end
modes = {}
[is, should].flatten.uniq.each do |group|
# Skip it when they're in both
next if is.include?(group) and should.include?(group)
# We're adding a group.
modes[group] = :add and next unless is.include?(group)
# We're removing a group.
modes[group] = :remove and next unless should.include?(group)
end
modes.each do |group, form|
self.fail "Could not find ldap group #{group}" unless ldap_group = group_manager.find(group)
current = ldap_group[:members]
if form == :add
if current.is_a?(Array) and ! current.empty?
new = current + [name]
else
new = [name]
end
else
new = current - [name]
new = :absent if new.empty?
end
group_manager.update(group, {:ensure => :present, :members => current}, {:ensure => :present, :members => new})
end
end
# Convert a gropu name to an id.
def group2id(group)
Puppet::Type.type(:group).provider(:ldap).name2id(group)
end
private
def group_manager
Puppet::Type.type(:group).provider(:ldap).manager
end
def group_properties(values)
if values.empty? or values == :absent
{:ensure => :present}
else
{:ensure => :present, :members => values}
end
end
end
diff --git a/lib/puppet/provider/user/pw.rb b/lib/puppet/provider/user/pw.rb
index 343fd8a7d..c1e9a08a8 100644
--- a/lib/puppet/provider/user/pw.rb
+++ b/lib/puppet/provider/user/pw.rb
@@ -1,97 +1,97 @@
require 'puppet/provider/nameservice/pw'
require 'open3'
Puppet::Type.type(:user).provide :pw, :parent => Puppet::Provider::NameService::PW do
desc "User management via `pw` on FreeBSD and DragonFly BSD."
commands :pw => "pw"
- has_features :manages_homedir, :allows_duplicates, :manages_passwords, :manages_expiry
+ has_features :manages_homedir, :allows_duplicates, :manages_passwords, :manages_expiry, :manages_shell
defaultfor :operatingsystem => [:freebsd, :dragonfly]
options :home, :flag => "-d", :method => :dir
options :comment, :method => :gecos
options :groups, :flag => "-G"
options :expiry, :method => :expire, :munge => proc { |value|
value = '0000-00-00' if value == :absent
value.split("-").reverse.join("-")
}
verify :gid, "GID must be an integer" do |value|
value.is_a? Integer
end
verify :groups, "Groups must be comma-separated" do |value|
value !~ /\s/
end
def addcmd
cmd = [command(:pw), "useradd", @resource[:name]]
@resource.class.validproperties.each do |property|
next if property == :ensure or property == :password
if value = @resource.should(property) and value != ""
cmd << flag(property) << munge(property,value)
end
end
cmd << "-o" if @resource.allowdupe?
cmd << "-m" if @resource.managehome?
cmd
end
def modifycmd(param, value)
if param == :expiry
# FreeBSD uses DD-MM-YYYY rather than YYYY-MM-DD
value = value.split("-").reverse.join("-")
end
cmd = super(param, value)
cmd << "-m" if @resource.managehome?
cmd
end
def deletecmd
cmd = super
cmd << "-r" if @resource.managehome?
cmd
end
def create
super
# Set the password after create if given
self.password = @resource[:password] if @resource[:password]
end
# use pw to update password hash
def password=(cryptopw)
Puppet.debug "change password for user '#{@resource[:name]}' method called with hash '#{cryptopw}'"
stdin, stdout, stderr = Open3.popen3("pw user mod #{@resource[:name]} -H 0")
stdin.puts(cryptopw)
stdin.close
Puppet.debug "finished password for user '#{@resource[:name]}' method called with hash '#{cryptopw}'"
end
# get password from /etc/master.passwd
def password
Puppet.debug "checking password for user '#{@resource[:name]}' method called"
current_passline = `getent passwd #{@resource[:name]}`
current_password = current_passline.chomp.split(':')[1] if current_passline
Puppet.debug "finished password for user '#{@resource[:name]}' method called : '#{current_password}'"
current_password
end
# Get expiry from system and convert to Puppet-style date
def expiry
expiry = self.get(:expiry)
expiry = :absent if expiry == 0
if expiry != :absent
t = Time.at(expiry)
expiry = "%4d-%02d-%02d" % [t.year, t.month, t.mday]
end
expiry
end
end
diff --git a/lib/puppet/provider/user/user_role_add.rb b/lib/puppet/provider/user/user_role_add.rb
index 3ec49507d..96b4928fd 100644
--- a/lib/puppet/provider/user/user_role_add.rb
+++ b/lib/puppet/provider/user/user_role_add.rb
@@ -1,209 +1,209 @@
require 'puppet/util'
require 'puppet/util/user_attr'
require 'date'
Puppet::Type.type(:user).provide :user_role_add, :parent => :useradd, :source => :useradd do
desc "User and role management on Solaris, via `useradd` and `roleadd`."
defaultfor :osfamily => :solaris
commands :add => "useradd", :delete => "userdel", :modify => "usermod", :password => "passwd", :role_add => "roleadd", :role_delete => "roledel", :role_modify => "rolemod"
options :home, :flag => "-d", :method => :dir
options :comment, :method => :gecos
options :groups, :flag => "-G"
options :roles, :flag => "-R"
options :auths, :flag => "-A"
options :profiles, :flag => "-P"
options :password_min_age, :flag => "-n"
options :password_max_age, :flag => "-x"
verify :gid, "GID must be an integer" do |value|
value.is_a? Integer
end
verify :groups, "Groups must be comma-separated" do |value|
value !~ /\s/
end
has_features :manages_homedir, :allows_duplicates, :manages_solaris_rbac, :manages_passwords, :manages_password_age
#must override this to hand the keyvalue pairs
def add_properties
cmd = []
Puppet::Type.type(:user).validproperties.each do |property|
#skip the password because we can't create it with the solaris useradd
next if [:ensure, :password, :password_min_age, :password_max_age].include?(property)
# 1680 Now you can set the hashed passwords on solaris:lib/puppet/provider/user/user_role_add.rb
# the value needs to be quoted, mostly because -c might
# have spaces in it
if value = @resource.should(property) and value != ""
if property == :keys
cmd += build_keys_cmd(value)
else
cmd << flag(property) << value
end
end
end
cmd
end
def user_attributes
@user_attributes ||= UserAttr.get_attributes_by_name(@resource[:name])
end
def flush
@user_attributes = nil
end
def command(cmd)
cmd = ("role_#{cmd}").intern if is_role? or (!exists? and @resource[:ensure] == :role)
super(cmd)
end
def is_role?
user_attributes and user_attributes[:type] == "role"
end
def run(cmd, msg)
execute(cmd)
rescue Puppet::ExecutionFailure => detail
- raise Puppet::Error, "Could not #{msg} #{@resource.class.name} #{@resource.name}: #{detail}"
+ raise Puppet::Error, "Could not #{msg} #{@resource.class.name} #{@resource.name}: #{detail}", detail.backtrace
end
def transition(type)
cmd = [command(:modify)]
cmd << "-K" << "type=#{type}"
cmd += add_properties
cmd << @resource[:name]
end
def create
if is_role?
run(transition("normal"), "transition role to")
else
run(addcmd, "create")
if cmd = passcmd
run(cmd, "change password policy for")
end
end
# added to handle case when password is specified
self.password = @resource[:password] if @resource[:password]
end
def destroy
run(deletecmd, "delete "+ (is_role? ? "role" : "user"))
end
def create_role
if exists? and !is_role?
run(transition("role"), "transition user to")
else
run(addcmd, "create role")
end
end
def roles
user_attributes[:roles] if user_attributes
end
def auths
user_attributes[:auths] if user_attributes
end
def profiles
user_attributes[:profiles] if user_attributes
end
def project
user_attributes[:project] if user_attributes
end
def managed_attributes
[:name, :type, :roles, :auths, :profiles, :project]
end
def remove_managed_attributes
managed = managed_attributes
user_attributes.select { |k,v| !managed.include?(k) }.inject({}) { |hash, array| hash[array[0]] = array[1]; hash }
end
def keys
if user_attributes
#we have to get rid of all the keys we are managing another way
remove_managed_attributes
end
end
def build_keys_cmd(keys_hash)
cmd = []
keys_hash.each do |k,v|
cmd << "-K" << "#{k}=#{v}"
end
cmd
end
def keys=(keys_hash)
run([command(:modify)] + build_keys_cmd(keys_hash) << @resource[:name], "modify attribute key pairs")
end
# This helper makes it possible to test this on stub data without having to
# do too many crazy things!
def target_file_path
"/etc/shadow"
end
private :target_file_path
#Read in /etc/shadow, find the line for this user (skipping comments, because who knows) and return it
#No abstraction, all esoteric knowledge of file formats, yay
def shadow_entry
return @shadow_entry if defined? @shadow_entry
@shadow_entry = File.readlines(target_file_path).
reject { |r| r =~ /^[^\w]/ }.
collect { |l| l.chomp.split(':') }.
find { |user, _| user == @resource[:name] }
end
def password
shadow_entry[1] if shadow_entry
end
def password_min_age
shadow_entry ? shadow_entry[3] : :absent
end
def password_max_age
return :absent unless shadow_entry
shadow_entry[4] || -1
end
# Read in /etc/shadow, find the line for our used and rewrite it with the
# new pw. Smooth like 80 grit sandpaper.
#
# Now uses the `replace_file` mechanism to minimize the chance that we lose
# data, but it is still terrible. We still skip platform locking, so a
# concurrent `vipw -s` session will have no idea we risk data loss.
def password=(cryptopw)
begin
shadow = File.read(target_file_path)
# Go Mifune loves the race here where we can lose data because
# /etc/shadow changed between reading it and writing it.
# --daniel 2012-02-05
Puppet::Util.replace_file(target_file_path, 0640) do |fh|
shadow.each_line do |line|
line_arr = line.split(':')
if line_arr[0] == @resource[:name]
line_arr[1] = cryptopw
line_arr[2] = (Date.today - Date.new(1970,1,1)).to_i.to_s
line = line_arr.join(':')
end
fh.print line
end
end
rescue => detail
- fail "Could not write replace #{target_file_path}: #{detail}"
+ self.fail Puppet::Error, "Could not write replace #{target_file_path}: #{detail}", detail
end
end
end
diff --git a/lib/puppet/provider/user/useradd.rb b/lib/puppet/provider/user/useradd.rb
index 2c6552c73..dd2d24b7d 100644
--- a/lib/puppet/provider/user/useradd.rb
+++ b/lib/puppet/provider/user/useradd.rb
@@ -1,217 +1,231 @@
require 'puppet/provider/nameservice/objectadd'
require 'date'
require 'puppet/util/libuser'
require 'time'
require 'puppet/error'
Puppet::Type.type(:user).provide :useradd, :parent => Puppet::Provider::NameService::ObjectAdd do
desc "User management via `useradd` and its ilk. Note that you will need to
install Ruby's shadow password library (often known as `ruby-libshadow`)
if you wish to manage user passwords."
commands :add => "useradd", :delete => "userdel", :modify => "usermod", :password => "chage"
options :home, :flag => "-d", :method => :dir
options :comment, :method => :gecos
options :groups, :flag => "-G"
options :password_min_age, :flag => "-m", :method => :sp_min
options :password_max_age, :flag => "-M", :method => :sp_max
options :password, :method => :sp_pwdp
options :expiry, :method => :sp_expire,
:munge => proc { |value|
if value == :absent
''
else
case Facter.value(:operatingsystem)
when 'Solaris'
# Solaris uses %m/%d/%Y for useradd/usermod
expiry_year, expiry_month, expiry_day = value.split('-')
[expiry_month, expiry_day, expiry_year].join('/')
else
value
end
end
},
:unmunge => proc { |value|
if value == -1
:absent
else
# Expiry is days after 1970-01-01
(Date.new(1970,1,1) + value).strftime('%Y-%m-%d')
end
}
optional_commands :localadd => "luseradd"
has_feature :libuser if Puppet.features.libuser?
def exists?
return !!localuid if @resource.forcelocal?
super
end
def uid
return localuid if @resource.forcelocal?
get(:uid)
end
def finduser(key, value)
passwd_file = "/etc/passwd"
passwd_keys = ['account', 'password', 'uid', 'gid', 'gecos', 'directory', 'shell']
index = passwd_keys.index(key)
File.open(passwd_file) do |f|
f.each_line do |line|
user = line.split(":")
if user[index] == value
f.close
return user
end
end
end
false
end
def local_username
finduser('uid', @resource.uid)
end
def localuid
user = finduser('account', resource[:name])
return user[2] if user
false
end
+ def shell=(value)
+ check_valid_shell
+ set("shell", value)
+ end
+
verify :gid, "GID must be an integer" do |value|
value.is_a? Integer
end
verify :groups, "Groups must be comma-separated" do |value|
value !~ /\s/
end
has_features :manages_homedir, :allows_duplicates, :manages_expiry
has_features :system_users unless %w{HP-UX Solaris}.include? Facter.value(:operatingsystem)
has_features :manages_passwords, :manages_password_age if Puppet.features.libshadow?
+ has_features :manages_shell
def check_allow_dup
# We have to manually check for duplicates when using libuser
# because by default duplicates are allowed. This check is
# to ensure consistent behaviour of the useradd provider when
# using both useradd and luseradd
if not @resource.allowdupe? and @resource.forcelocal?
if @resource.should(:uid) and finduser('uid', @resource.should(:uid).to_s)
raise(Puppet::Error, "UID #{@resource.should(:uid).to_s} already exists, use allowdupe to force user creation")
end
elsif @resource.allowdupe? and not @resource.forcelocal?
return ["-o"]
end
[]
end
+ def check_valid_shell
+ unless File.exists?(@resource.should(:shell))
+ raise(Puppet::Error, "Shell #{@resource.should(:shell)} must exist")
+ end
+ unless File.executable?(@resource.should(:shell).to_s)
+ raise(Puppet::Error, "Shell #{@resource.should(:shell)} must be executable")
+ end
+ end
+
def check_manage_home
cmd = []
if @resource.managehome? and not @resource.forcelocal?
cmd << "-m"
elsif not @resource.managehome? and Facter.value(:osfamily) == 'RedHat'
cmd << "-M"
end
cmd
end
def check_manage_expiry
cmd = []
if @resource[:expiry] and not @resource.forcelocal?
cmd << "-e #{@resource[:expiry]}"
end
cmd
end
def check_system_users
if self.class.system_users? and resource.system?
["-r"]
else
[]
end
end
def add_properties
cmd = []
# validproperties is a list of properties in undefined order
# sort them to have a predictable command line in tests
Puppet::Type.type(:user).validproperties.sort.each do |property|
next if property == :ensure
next if property.to_s =~ /password_.+_age/
next if property == :groups and @resource.forcelocal?
next if property == :expiry and @resource.forcelocal?
# the value needs to be quoted, mostly because -c might
# have spaces in it
if value = @resource.should(property) and value != ""
cmd << flag(property) << munge(property, value)
end
end
cmd
end
def addcmd
if @resource.forcelocal?
cmd = [command(:localadd)]
@custom_environment = Puppet::Util::Libuser.getenv
else
cmd = [command(:add)]
end
if not @resource.should(:gid) and Puppet::Util.gid(@resource[:name])
cmd += ["-g", @resource[:name]]
end
cmd += add_properties
cmd += check_allow_dup
cmd += check_manage_home
cmd += check_system_users
cmd << @resource[:name]
end
def deletecmd
- if @resource.forcelocal?
- cmd = [command(:localdelete)]
- else
- cmd = [command(:delete)]
- end
+ cmd = [command(:delete)]
cmd += @resource.managehome? ? ['-r'] : []
cmd << @resource[:name]
end
def passcmd
age_limits = [:password_min_age, :password_max_age].select { |property| @resource.should(property) }
if age_limits.empty?
nil
else
[command(:password),age_limits.collect { |property| [flag(property), @resource.should(property)]}, @resource[:name]].flatten
end
end
[:expiry, :password_min_age, :password_max_age, :password].each do |shadow_property|
define_method(shadow_property) do
if Puppet.features.libshadow?
if ent = Shadow::Passwd.getspnam(@resource.name)
method = self.class.option(shadow_property, :method)
return unmunge(shadow_property, ent.send(method))
end
end
:absent
end
end
def create
+ if @resource[:shell]
+ check_valid_shell
+ end
super
if @resource.forcelocal? and self.groups?
set(:groups, @resource[:groups])
end
if @resource.forcelocal? and @resource[:expiry]
set(:expiry, @resource[:expiry])
end
end
def groups?
!!@resource[:groups]
end
end
diff --git a/lib/puppet/provider/yumrepo/inifile.rb b/lib/puppet/provider/yumrepo/inifile.rb
new file mode 100644
index 000000000..4882dedb8
--- /dev/null
+++ b/lib/puppet/provider/yumrepo/inifile.rb
@@ -0,0 +1,187 @@
+require 'puppet/util/inifile'
+
+Puppet::Type.type(:yumrepo).provide(:inifile) do
+ desc 'Manage yum repos'
+
+ PROPERTIES = Puppet::Type.type(:yumrepo).validproperties
+
+ # @return [Array<Puppet::Providers>] Return all the providers built up from
+ # discovered content on the local node.
+ def self.instances
+ instances = []
+ # Iterate over each section of our virtual file.
+ virtual_inifile.each_section do |section|
+ attributes_hash = {:name => section.name, :ensure => :present, :provider => :yumrepo}
+ # We need to build up a attributes hash
+ section.entries.each do |key, value|
+ key = key.to_sym
+ if valid_property?(key)
+ # We strip the values here to handle cases where distros set values
+ # like enabled = 1 with spaces.
+ attributes_hash[key] = value.strip
+ end
+ end
+ instances << new(attributes_hash)
+ end
+ return instances
+ end
+
+ # @param resources [Array<Puppet::Resource>] Resources to prefetch.
+ # @return [Array<Puppet::Resource>] Resources with providers set.
+ def self.prefetch(resources)
+ repos = instances
+ resources.keys.each do |name|
+ if provider = repos.find { |repo| repo.name == name }
+ resources[name].provider = provider
+ end
+ end
+ end
+
+ # Return a list of existing directories that could contain repo files. Fail if none found.
+ # @param conf [String] Configuration file to look for directories in.
+ # @param dirs [Array] Default locations for yum repos.
+ # @return [Array] Directories that were found to exist on the node.
+ def self.reposdir(conf='/etc/yum.conf', dirs=['/etc/yum.repos.d', '/etc/yum/repos.d'])
+ reposdir = find_conf_value('reposdir', conf)
+ dirs << reposdir if reposdir
+
+ # We can't use the below due to Ruby 1.8.7
+ # dirs.select! { |dir| Puppet::FileSystem.exist?(dir) }
+ dirs.delete_if { |dir| ! Puppet::FileSystem.exist?(dir) }
+ if dirs.empty?
+ fail('No yum directories were found on the local filesystem')
+ else
+ return dirs
+ end
+ end
+
+ # Helper method to look up specific values in ini style files.
+ # @todo Migrate this into Puppet::Util::IniConfig.
+ # @param value [String] Value to look for in the configuration file.
+ # @param conf [String] Configuration file to check for value.
+ # @return [String] The value of a looked up key from the configuration file.
+ def self.find_conf_value(value, conf='/etc/yum.conf')
+ if Puppet::FileSystem.exist?(conf)
+ contents = Puppet::FileSystem.read(conf)
+ match = /^#{value}\s*=\s*(.*)/.match(contents)
+ end
+
+ return match.captures[0] if match
+ end
+
+ # Build a virtual inifile by reading in numerous .repo
+ # files into a single virtual file to ease manipulation.
+ # @return [Puppet::Util::IniConfig::File] The virtual inifile representing
+ # multiple real files.
+ def self.virtual_inifile
+ unless @virtual
+ @virtual = Puppet::Util::IniConfig::File.new
+ reposdir.each do |dir|
+ Dir.glob("#{dir}/*.repo").each do |file|
+ @virtual.read(file) if Puppet::FileSystem.file?(file)
+ end
+ end
+ end
+ return @virtual
+ end
+
+ # @param key [String] The property to look up.
+ # @return [Boolean] Returns true if the property is defined in the type.
+ def self.valid_property?(key)
+ PROPERTIES.include?(key)
+ end
+
+ # We need to return a valid section from the larger virtual inifile here,
+ # which we do by first looking it up and then creating a new section for
+ # the appropriate name if none was found.
+ # @param name [String] Section name to lookup in the virtual inifile.
+ # @return [Puppet::Util::IniConfig] The IniConfig section
+ def self.section(name)
+ result = self.virtual_inifile[name]
+ # Create a new section if not found.
+ unless result
+ # Previously we did an .each on reposdir with the effect that we
+ # constantly created and overwrote result until the last entry of
+ # the array. This was done because the ordering is
+ # [defaults, custom] for reposdir and we want to use the custom if
+ # we have it and the defaults if not.
+ path = ::File.join(reposdir.last, "#{name}.repo")
+ Puppet.info("create new repo #{name} in file #{path}")
+ result = self.virtual_inifile.add_section(name, path)
+ end
+ result
+ end
+
+ # Here we store all modifications to disk, forcing the output file to 0644 if it differs.
+ # @return [void]
+ def self.store
+ inifile = self.virtual_inifile
+ inifile.store
+
+ target_mode = 0644
+ inifile.each_file do |file|
+ current_mode = Puppet::FileSystem.stat(file).mode & 0777
+ unless current_mode == target_mode
+ Puppet.info "changing mode of #{file} from %03o to %03o" % [current_mode, target_mode]
+ Puppet::FileSystem.chmod(target_mode, file)
+ end
+ end
+ end
+
+ # @return [void]
+ def create
+ @property_hash[:ensure] = :present
+
+ new_section = section(@resource[:name])
+
+ # We fetch a list of properties from the type, then iterate
+ # over them, avoiding ensure. We're relying on .should to
+ # check if the property has been set and should be modified,
+ # and if so we set it in the virtual inifile.
+ PROPERTIES.each do |property|
+ next if property == :ensure
+ if value = @resource.should(property)
+ new_section[property.to_s] = value
+ @property_hash[property] = value
+ end
+ end
+ end
+
+ # We don't actually destroy the file here, merely mark it for
+ # destruction in the section.
+ # @return [void]
+ def destroy
+ # Flag file for deletion on flush.
+ section(@resource[:name]).destroy=(true)
+
+ @property_hash.clear
+ end
+
+ # @return [void]
+ def flush
+ self.class.store
+ end
+
+ # @return [void]
+ def section(name)
+ self.class.section(name)
+ end
+
+ # Create all of our setters.
+ mk_resource_methods
+ PROPERTIES.each do |property|
+ # Exclude ensure, as we don't need to create an ensure=
+ next if property == :ensure
+ # Builds the property= method.
+ define_method("#{property.to_s}=") do |value|
+ section(@property_hash[:name])[property.to_s] = value
+ @property_hash[property] = value
+ end
+ end
+
+ # @return [Boolean] Returns true if ensure => present.
+ def exists?
+ @property_hash[:ensure] == :present
+ end
+
+end
diff --git a/lib/puppet/provider/zone/solaris.rb b/lib/puppet/provider/zone/solaris.rb
index 2cd0e99bb..e681aca75 100644
--- a/lib/puppet/provider/zone/solaris.rb
+++ b/lib/puppet/provider/zone/solaris.rb
@@ -1,361 +1,361 @@
Puppet::Type.type(:zone).provide(:solaris) do
desc "Provider for Solaris Zones."
commands :adm => "/usr/sbin/zoneadm", :cfg => "/usr/sbin/zonecfg"
defaultfor :osfamily => :solaris
mk_resource_methods
# Convert the output of a list into a hash
def self.line2hash(line)
fields = [:id, :name, :ensure, :path, :uuid, :brand, :iptype]
properties = Hash[fields.zip(line.split(':'))]
del_id = [:brand, :uuid]
# Configured but not installed zones do not have IDs
del_id << :id if properties[:id] == "-"
del_id.each { |p| properties.delete(p) }
properties[:ensure] = properties[:ensure].intern
properties[:iptype] = 'exclusive' if properties[:iptype] == 'excl'
properties
end
def self.instances
adm(:list, "-cp").split("\n").collect do |line|
new(line2hash(line))
end
end
def multi_conf(name, should, &action)
has = properties[name]
has = [] if has == :absent
rms = has - should
adds = should - has
(rms.map{|o| action.call(:rm,o)} + adds.map{|o| action.call(:add,o)}).join("\n")
end
def self.def_prop(var, str)
define_method('%s_conf' % var.to_s) do |v|
str % v
end
define_method('%s=' % var.to_s) do |v|
setconfig self.send( ('%s_conf'% var).intern, v)
end
end
def self.def_multiprop(var, &conf)
define_method(var.to_s) do |v|
o = properties[var]
return '' if o.nil? or o == :absent
o.join(' ')
end
define_method('%s=' % var.to_s) do |v|
setconfig self.send( ('%s_conf'% var).intern, v)
end
define_method('%s_conf' % var.to_s) do |v|
multi_conf(var, v, &conf)
end
end
def_prop :iptype, "set ip-type=%s"
def_prop :autoboot, "set autoboot=%s"
def_prop :path, "set zonepath=%s"
def_prop :pool, "set pool=%s"
def_prop :shares, "add rctl\nset name=zone.cpu-shares\nadd value (priv=privileged,limit=%s,action=none)\nend"
def_multiprop :ip do |action, str|
interface, ip, defrouter = str.split(':')
case action
when :add
cmd = ["add net"]
cmd << "set physical=#{interface}" if interface
cmd << "set address=#{ip}" if ip
cmd << "set defrouter=#{defrouter}" if defrouter
cmd << "end"
cmd.join("\n")
when :rm
if ip
"remove net address=#{ip}"
elsif interface
"remove net physical=#{interface}"
else
raise ArgumentError, "can not remove network based on default router"
end
else self.fail action
end
end
def_multiprop :dataset do |action, str|
case action
when :add; ['add dataset',"set name=#{str}",'end'].join("\n")
when :rm; "remove dataset name=#{str}"
else self.fail action
end
end
def_multiprop :inherit do |action, str|
case action
when :add; ['add inherit-pkg-dir', "set dir=#{str}",'end'].join("\n")
when :rm; "remove inherit-pkg-dir dir=#{str}"
else self.fail action
end
end
def my_properties
[:path, :iptype, :autoboot, :pool, :shares, :ip, :dataset, :inherit]
end
# Perform all of our configuration steps.
def configure
self.fail "Path is required" unless @resource[:path]
arr = ["create -b #{@resource[:create_args]}"]
# Then perform all of our configuration steps. It's annoying
# that we need this much internal info on the resource.
self.resource.properties.each do |property|
next unless my_properties.include? property.name
method = (property.name.to_s + '_conf').intern
arr << self.send(method ,@resource[property.name]) unless property.safe_insync?(properties[property.name])
end
setconfig(arr.join("\n"))
end
def destroy
zonecfg :delete, "-F"
end
def add_cmd(cmd)
@cmds = [] if @cmds.nil?
@cmds << cmd
end
def exists?
properties[:ensure] != :absent
end
# We cannot use the execpipe in util because the pipe is not opened in
# read/write mode.
def exec_cmd(var)
# In bash, the exit value of the last command is the exit value of the
# entire pipeline
out = execute("echo \"#{var[:input]}\" | #{var[:cmd]}", :failonfail => false, :combine => true)
st = $?.exitstatus
{:out => out, :exit => st}
end
# Clear out the cached values.
def flush
return if @cmds.nil? || @cmds.empty?
str = (@cmds << "commit" << "exit").join("\n")
@cmds = []
@property_hash.clear
command = "#{command(:cfg)} -z #{@resource[:name]} -f -"
r = exec_cmd(:cmd => command, :input => str)
if r[:exit] != 0 or r[:out] =~ /not allowed/
raise ArgumentError, "Failed to apply configuration"
end
end
def install(dummy_argument=:work_arround_for_ruby_GC_bug)
if @resource[:clone] # TODO: add support for "-s snapshot"
zoneadm :clone, @resource[:clone]
elsif @resource[:install_args]
zoneadm :install, @resource[:install_args].split(" ")
else
zoneadm :install
end
end
# Look up the current status.
def properties
if @property_hash.empty?
@property_hash = status || {}
if @property_hash.empty?
@property_hash[:ensure] = :absent
else
@resource.class.validproperties.each do |name|
@property_hash[name] ||= :absent
end
end
end
@property_hash.dup
end
# We need a way to test whether a zone is in process. Our 'ensure'
# property models the static states, but we need to handle the temporary ones.
def processing?
hash = status
return false unless hash
["incomplete", "ready", "shutting_down"].include? hash[:ensure]
end
# Collect the configuration of the zone. The output looks like:
# zonename: z1
# zonepath: /export/z1
# brand: native
# autoboot: true
# bootargs:
# pool:
# limitpriv:
# scheduling-class:
# ip-type: shared
# hostid:
# net:
# address: 192.168.1.1
# physical: eg0001
# defrouter not specified
# net:
# address: 192.168.1.3
# physical: eg0002
# defrouter not specified
#
def getconfig
output = zonecfg :info
name = nil
current = nil
hash = {}
output.split("\n").each do |line|
case line
when /^(\S+):\s*$/
name = $1
current = nil # reset it
when /^(\S+):\s*(\S+)$/
hash[$1.intern] = $2
when /^\s+(\S+):\s*(.+)$/
if name
hash[name] ||= []
unless current
current = {}
hash[name] << current
end
current[$1.intern] = $2
else
err "Ignoring '#{line}'"
end
else
debug "Ignoring zone output '#{line}'"
end
end
hash
end
# Execute a configuration string. Can't be private because it's called
# by the properties.
def setconfig(str)
add_cmd str
end
def start
# Check the sysidcfg stuff
if cfg = @resource[:sysidcfg]
self.fail "Path is required" unless @resource[:path]
zoneetc = File.join(@resource[:path], "root", "etc")
sysidcfg = File.join(zoneetc, "sysidcfg")
# if the zone root isn't present "ready" the zone
# which makes zoneadmd mount the zone root
zoneadm :ready unless File.directory?(zoneetc)
- unless Puppet::FileSystem::File.exist?(sysidcfg)
+ unless Puppet::FileSystem.exist?(sysidcfg)
begin
File.open(sysidcfg, "w", 0600) do |f|
f.puts cfg
end
rescue => detail
puts detail.stacktrace if Puppet[:debug]
- raise Puppet::Error, "Could not create sysidcfg: #{detail}"
+ raise Puppet::Error, "Could not create sysidcfg: #{detail}", detail.backtrace
end
end
end
zoneadm :boot
end
# Return a hash of the current status of this zone.
def status
begin
output = adm "-z", @resource[:name], :list, "-p"
rescue Puppet::ExecutionFailure
return nil
end
main = self.class.line2hash(output.chomp)
# Now add in the configuration information
config_status.each do |name, value|
main[name] = value
end
main
end
def ready
zoneadm :ready
end
def stop
zoneadm :halt
end
def unconfigure
zonecfg :delete, "-F"
end
def uninstall
zoneadm :uninstall, "-F"
end
private
# Turn the results of getconfig into status information.
def config_status
config = getconfig
result = {}
result[:autoboot] = config[:autoboot] ? config[:autoboot].intern : :true
result[:pool] = config[:pool]
result[:shares] = config[:shares]
if dir = config["inherit-pkg-dir"]
result[:inherit] = dir.collect { |dirs| dirs[:dir] }
end
if datasets = config["dataset"]
result[:dataset] = datasets.collect { |dataset| dataset[:name] }
end
result[:iptype] = config[:'ip-type'] if config[:'ip-type']
if net = config["net"]
result[:ip] = net.collect do |params|
if params[:defrouter]
"#{params[:physical]}:#{params[:address]}:#{params[:defrouter]}"
elsif params[:address]
"#{params[:physical]}:#{params[:address]}"
else
params[:physical]
end
end
end
result
end
def zoneadm(*cmd)
adm("-z", @resource[:name], *cmd)
rescue Puppet::ExecutionFailure => detail
- self.fail "Could not #{cmd[0]} zone: #{detail}"
+ self.fail Puppet::Error, "Could not #{cmd[0]} zone: #{detail}", detail
end
def zonecfg(*cmd)
# You apparently can't get the configuration of the global zone (strictly in solaris11)
return "" if self.name == "global"
begin
cfg("-z", self.name, *cmd)
rescue Puppet::ExecutionFailure => detail
- self.fail "Could not #{cmd[0]} zone: #{detail}"
+ self.fail Puppet::Error, "Could not #{cmd[0]} zone: #{detail}", detail
end
end
end
diff --git a/lib/puppet/rails.rb b/lib/puppet/rails.rb
index 36b4043ea..2c97c02c6 100644
--- a/lib/puppet/rails.rb
+++ b/lib/puppet/rails.rb
@@ -1,134 +1,139 @@
# Load the appropriate libraries, or set a class indicating they aren't available
require 'facter'
require 'puppet'
require 'logger'
module Puppet::Rails
TIME_DEBUG = true
def self.connect
# This global init does not work for testing, because we remove
# the state dir on every test.
return if ActiveRecord::Base.connected?
Puppet.settings.use(:main, :rails, :master)
ActiveRecord::Base.logger = Logger.new(Puppet[:railslog])
begin
loglevel = Logger.const_get(Puppet[:rails_loglevel].upcase)
ActiveRecord::Base.logger.level = loglevel
rescue => detail
Puppet.warning "'#{Puppet[:rails_loglevel]}' is not a valid Rails log level; using debug"
ActiveRecord::Base.logger.level = Logger::DEBUG
end
# As of ActiveRecord 2.2 allow_concurrency has been deprecated and no longer has any effect.
ActiveRecord::Base.allow_concurrency = true if Puppet::Util.activerecord_version < 2.2
ActiveRecord::Base.verify_active_connections!
begin
args = database_arguments
Puppet.info "Connecting to #{args[:adapter]} database: #{args[:database]}"
ActiveRecord::Base.establish_connection(args)
rescue => detail
message = "Could not connect to database: #{detail}"
Puppet.log_exception(detail, message)
- raise Puppet::Error, message
+ raise Puppet::Error, message, detail.backtrace
end
end
# The arguments for initializing the database connection.
def self.database_arguments
adapter = Puppet[:dbadapter]
args = {:adapter => adapter, :log_level => Puppet[:rails_loglevel]}
case adapter
when "sqlite3"
args[:database] = Puppet[:dblocation]
when "mysql", "mysql2", "postgresql"
args[:host] = Puppet[:dbserver] unless Puppet[:dbserver].to_s.empty?
args[:port] = Puppet[:dbport] unless Puppet[:dbport].to_s.empty?
args[:username] = Puppet[:dbuser] unless Puppet[:dbuser].to_s.empty?
args[:password] = Puppet[:dbpassword] unless Puppet[:dbpassword].to_s.empty?
args[:pool] = Puppet[:dbconnections].to_i unless Puppet[:dbconnections].to_i <= 0
args[:database] = Puppet[:dbname]
args[:reconnect]= true
socket = Puppet[:dbsocket]
args[:socket] = socket unless socket.to_s.empty?
when "oracle_enhanced"
args[:database] = Puppet[:dbname] unless Puppet[:dbname].to_s.empty?
args[:username] = Puppet[:dbuser] unless Puppet[:dbuser].to_s.empty?
args[:password] = Puppet[:dbpassword] unless Puppet[:dbpassword].to_s.empty?
args[:pool] = Puppet[:dbconnections].to_i unless Puppet[:dbconnections].to_i <= 0
else
raise ArgumentError, "Invalid db adapter #{adapter}"
end
args
end
# Set up our database connection. It'd be nice to have a "use" system
# that could make callbacks.
def self.init
raise Puppet::DevError, "No activerecord, cannot init Puppet::Rails" unless Puppet.features.rails?
connect
unless ActiveRecord::Base.connection.tables.include?("resources")
require 'puppet/rails/database/schema'
Puppet::Rails::Schema.init
end
migrate if Puppet[:dbmigrate]
end
# Migrate to the latest db schema.
def self.migrate
dbdir = nil
$LOAD_PATH.each { |d|
tmp = File.join(d, "puppet/rails/database")
if FileTest.directory?(tmp)
dbdir = tmp
break
end
}
raise Puppet::Error, "Could not find Puppet::Rails database dir" unless dbdir
raise Puppet::Error, "Database has problems, can't migrate." unless ActiveRecord::Base.connection.tables.include?("resources")
Puppet.notice "Migrating"
begin
ActiveRecord::Migrator.migrate(dbdir)
rescue => detail
message = "Could not migrate database: #{detail}"
Puppet.log_exception(detail, message)
- raise Puppet::Error, "Could not migrate database: #{detail}"
+ raise Puppet::Error, "Could not migrate database: #{detail}", detail.backtrace
end
end
# Tear down the database. Mostly only used during testing.
def self.teardown
raise Puppet::DevError, "No activerecord, cannot init Puppet::Rails" unless Puppet.features.rails?
- Puppet.settings.use(:master, :rails)
-
begin
- ActiveRecord::Base.establish_connection(database_arguments)
- rescue => detail
- Puppet.log_exception(detail)
- raise Puppet::Error, "Could not connect to database: #{detail}"
- end
+ Puppet.settings.use(:master, :rails)
- ActiveRecord::Base.connection.tables.each do |t|
- ActiveRecord::Base.connection.drop_table t
+ begin
+ ActiveRecord::Base.establish_connection(database_arguments)
+ rescue => detail
+ Puppet.log_exception(detail)
+ raise Puppet::Error, "Could not connect to database: #{detail}", detail.backtrace
+ end
+
+ ActiveRecord::Base.connection.tables.each do |t|
+ ActiveRecord::Base.connection.drop_table t
+ end
+ ensure
+ # allow temp files to get cleaned up
+ ActiveRecord::Base.logger.close if ActiveRecord::Base.logger
end
end
end
require 'puppet/rails/host' if Puppet.features.rails?
diff --git a/lib/puppet/rails/benchmark.rb b/lib/puppet/rails/benchmark.rb
index 8d88280c3..cf1afd8d3 100644
--- a/lib/puppet/rails/benchmark.rb
+++ b/lib/puppet/rails/benchmark.rb
@@ -1,63 +1,63 @@
require 'benchmark'
require 'yaml'
module Puppet::Rails::Benchmark
$benchmarks = {:accumulated => {}}
def time_debug?
Puppet::Rails::TIME_DEBUG
end
def railsmark(message)
result = nil
seconds = Benchmark.realtime { result = yield }
Puppet.debug(message + " in %0.2f seconds" % seconds)
$benchmarks[message] = seconds if time_debug?
result
end
def debug_benchmark(message)
return yield unless Puppet::Rails::TIME_DEBUG
railsmark(message) { yield }
end
# Collect partial benchmarks to be logged when they're
# all done.
# These are always low-level debugging so we only
# print them if time_debug is enabled.
def accumulate_benchmark(message, label)
return yield unless time_debug?
$benchmarks[:accumulated][message] ||= Hash.new(0)
$benchmarks[:accumulated][message][label] += Benchmark.realtime { yield }
end
# Log the accumulated marks.
def log_accumulated_marks(message)
return unless time_debug?
return if $benchmarks[:accumulated].empty? or $benchmarks[:accumulated][message].nil? or $benchmarks[:accumulated][message].empty?
$benchmarks[:accumulated][message].each do |label, value|
Puppet.debug(message + ("(#{label})") + (" in %0.2f seconds" % value))
end
end
def write_benchmarks
return unless time_debug?
branch = %x{git branch}.split("\n").find { |l| l =~ /^\*/ }.sub("* ", '')
file = "/tmp/time_debugging.yaml"
- if Puppet::FileSystem::File.exist?(file)
+ if Puppet::FileSystem.exist?(file)
data = YAML.load_file(file)
else
data = {}
end
data[branch] = $benchmarks
Puppet::Util.replace_file(file, 0644) { |f| f.print YAML.dump(data) }
end
end
diff --git a/lib/puppet/rails/resource.rb b/lib/puppet/rails/resource.rb
index cded3f3c1..a2f732613 100644
--- a/lib/puppet/rails/resource.rb
+++ b/lib/puppet/rails/resource.rb
@@ -1,231 +1,235 @@
require 'puppet'
require 'puppet/rails/param_name'
require 'puppet/rails/param_value'
require 'puppet/rails/puppet_tag'
require 'puppet/rails/benchmark'
require 'puppet/util/rails/collection_merger'
class Puppet::Rails::Resource < ActiveRecord::Base
include Puppet::Util::CollectionMerger
include Puppet::Util::ReferenceSerializer
include Puppet::Rails::Benchmark
has_many :param_values, :dependent => :destroy, :class_name => "Puppet::Rails::ParamValue"
has_many :param_names, :through => :param_values, :class_name => "Puppet::Rails::ParamName"
has_many :resource_tags, :dependent => :destroy, :class_name => "Puppet::Rails::ResourceTag"
has_many :puppet_tags, :through => :resource_tags, :class_name => "Puppet::Rails::PuppetTag"
belongs_to :source_file
belongs_to :host
@tags = {}
def self.tags
@tags
end
# Determine the basic details on the resource.
def self.rails_resource_initial_args(resource)
result = [:type, :title, :line].inject({}) do |hash, param|
# 'type' isn't a valid column name, so we have to use another name.
to = (param == :type) ? :restype : param
if value = resource.send(param)
hash[to] = value
end
hash
end
# We always want a value here, regardless of what the resource has,
# so we break it out separately.
result[:exported] = resource.exported || false
result
end
def add_resource_tag(tag)
pt = Puppet::Rails::PuppetTag.accumulate_by_name(tag)
resource_tags.build(:puppet_tag => pt)
end
def file
(f = self.source_file) ? f.filename : nil
end
def file=(file)
self.source_file = Puppet::Rails::SourceFile.find_or_create_by_filename(file)
end
def title
unserialize_value(self[:title])
end
def params_list
@params_list ||= []
end
def params_list=(params)
@params_list = params
end
def add_param_to_list(param)
params_list << param
end
def tags_list
@tags_list ||= []
end
def tags_list=(tags)
@tags_list = tags
end
def add_tag_to_list(tag)
tags_list << tag
end
def [](param)
- super || parameter(param)
+ if param == 'id'
+ super
+ else
+ super || parameter(param)
+ end
end
# Make sure this resource is equivalent to the provided Parser resource.
def merge_parser_resource(resource)
accumulate_benchmark("Individual resource merger", :attributes) { merge_attributes(resource) }
accumulate_benchmark("Individual resource merger", :parameters) { merge_parameters(resource) }
accumulate_benchmark("Individual resource merger", :tags) { merge_tags(resource) }
save
end
def merge_attributes(resource)
args = self.class.rails_resource_initial_args(resource)
args.each do |param, value|
self[param] = value unless resource[param] == value
end
# Handle file specially
self.file = resource.file if (resource.file and (!resource.file or self.file != resource.file))
end
def merge_parameters(resource)
catalog_params = {}
resource.each do |param, value|
catalog_params[param.to_s] = value
end
db_params = {}
deletions = []
params_list.each do |value|
# First remove any parameters our catalog resource doesn't have at all.
deletions << value['id'] and next unless catalog_params.include?(value['name'])
# Now store them for later testing.
db_params[value['name']] ||= []
db_params[value['name']] << value
end
# Now get rid of any parameters whose value list is different.
# This might be extra work in cases where an array has added or lost
# a single value, but in the most common case (a single value has changed)
# this makes sense.
db_params.each do |name, value_hashes|
values = value_hashes.collect { |v| v['value'] }
value_hashes.each { |v| deletions << v['id'] } unless value_compare(catalog_params[name], values)
end
# Perform our deletions.
Puppet::Rails::ParamValue.delete(deletions) unless deletions.empty?
# Lastly, add any new parameters.
catalog_params.each do |name, value|
next if db_params.include?(name) && ! db_params[name].find{ |val| deletions.include?( val["id"] ) }
values = value.is_a?(Array) ? value : [value]
values.each do |v|
param_values.build(:value => serialize_value(v), :line => resource.line, :param_name => Puppet::Rails::ParamName.accumulate_by_name(name))
end
end
end
# Make sure the tag list is correct.
def merge_tags(resource)
in_db = []
deletions = []
resource_tags = resource.tags
tags_list.each do |tag|
deletions << tag['id'] and next unless resource_tags.include?(tag['name'])
in_db << tag['name']
end
Puppet::Rails::ResourceTag.delete(deletions) unless deletions.empty?
(resource_tags - in_db).each do |tag|
add_resource_tag(tag)
end
end
def value_compare(v,db_value)
v = [v] unless v.is_a?(Array)
v == db_value
end
def name
ref
end
def parameter(param)
if pn = param_names.find_by_name(param)
return (pv = param_values.find(:first, :conditions => [ 'param_name_id = ?', pn])) ? pv.value : nil
end
end
def ref(dummy_argument=:work_arround_for_ruby_GC_bug)
"#{self[:restype].split("::").collect { |s| s.capitalize }.join("::")}[#{self.title}]"
end
# Returns a hash of parameter names and values, no ActiveRecord instances.
def to_hash
Puppet::Rails::ParamValue.find_all_params_from_resource(self).inject({}) do |hash, value|
hash[value['name']] ||= []
hash[value['name']] << value.value
hash
end
end
# Convert our object to a resource. Do not retain whether the object
# is exported, though, since that would cause it to get stripped
# from the configuration.
def to_resource(scope)
hash = self.attributes
hash["type"] = hash["restype"]
hash.delete("restype")
# FIXME At some point, we're going to want to retain this information
# for logging and auditing.
hash.delete("host_id")
hash.delete("updated_at")
hash.delete("source_file_id")
hash.delete("created_at")
hash.delete("id")
hash.each do |p, v|
hash.delete(p) if v.nil?
end
hash[:scope] = scope
hash[:source] = scope.source
hash[:parameters] = []
names = []
self.param_names.each do |pname|
# We can get the same name multiple times because of how the
# db layout works.
next if names.include?(pname.name)
names << pname.name
hash[:parameters] << pname.to_resourceparam(self, scope.source)
end
obj = Puppet::Parser::Resource.new(hash.delete("type"), hash.delete("title"), hash)
# Store the ID, so we can check if we're re-collecting the same resource.
obj.collector_id = self.id
obj
end
end
diff --git a/lib/puppet/reference/configuration.rb b/lib/puppet/reference/configuration.rb
index c1580fe35..0475067a3 100644
--- a/lib/puppet/reference/configuration.rb
+++ b/lib/puppet/reference/configuration.rb
@@ -1,73 +1,73 @@
-config = Puppet::Util::Reference.newreference(:configuration, :depth => 1, :doc => "A reference for all configuration parameters") do
+config = Puppet::Util::Reference.newreference(:configuration, :depth => 1, :doc => "A reference for all settings") do
docs = {}
Puppet.settings.each do |name, object|
docs[name] = object
end
str = ""
docs.sort { |a, b|
a[0].to_s <=> b[0].to_s
}.each do |name, object|
# Make each name an anchor
header = name.to_s
str << markdown_header(header, 3)
# Print the doc string itself
begin
str << Puppet::Util::Docs.scrub(object.desc)
rescue => detail
Puppet.log_exception(detail)
end
str << "\n\n"
# Now print the data about the item.
val = object.default
if name.to_s == "vardir"
val = "/var/lib/puppet"
elsif name.to_s == "confdir"
val = "/etc/puppet"
end
# Leave out the section information; it was apparently confusing people.
#str << "- **Section**: #{object.section}\n"
unless val == ""
str << "- *Default*: #{val}\n"
end
str << "\n"
end
return str
end
config.header = <<EOT
## Configuration Settings
* Each of these settings can be specified in `puppet.conf` or on the
command line.
* When using boolean settings on the command line, use `--setting` and
`--no-setting` instead of `--setting (true|false)`.
* Settings can be interpolated as `$variables` in other settings; `$environment`
is special, in that puppet master will interpolate each agent node's
environment instead of its own.
* Multiple values should be specified as comma-separated lists; multiple
directories should be separated with the system path separator (usually
a colon).
* Settings that represent time intervals should be specified in duration format:
an integer immediately followed by one of the units 'y' (years of 365 days),
'd' (days), 'h' (hours), 'm' (minutes), or 's' (seconds). The unit cannot be
combined with other units, and defaults to seconds when omitted. Examples are
'3600' which is equivalent to '1h' (one hour), and '1825d' which is equivalent
to '5y' (5 years).
* Settings that take a single file or directory can optionally set the owner,
group, and mode for their value: `rundir = $vardir/run { owner = puppet,
group = puppet, mode = 644 }`
* The Puppet executables will ignore any setting that isn't relevant to
their function.
See the [configuration guide][confguide] for more details.
[confguide]: http://docs.puppetlabs.com/guides/configuring.html
* * *
EOT
diff --git a/lib/puppet/reference/report.rb b/lib/puppet/reference/report.rb
index 47fc779ab..b161a4aa5 100644
--- a/lib/puppet/reference/report.rb
+++ b/lib/puppet/reference/report.rb
@@ -1,23 +1,23 @@
require 'puppet/reports'
report = Puppet::Util::Reference.newreference :report, :doc => "All available transaction reports" do
Puppet::Reports.reportdocs
end
report.header = "
Puppet clients can report back to the server after each transaction. This
transaction report is sent as a YAML dump of the
`Puppet::Transaction::Report` class and includes every log message that was
generated during the transaction along with as many metrics as Puppet knows how
-to collect. See [Reports and Reporting](http://projects.puppetlabs.com/projects/puppet/wiki/Reports_And_Reporting) for more information on how to use reports.
+to collect. See [Reports and Reporting](http://docs.puppetlabs.com/guides/reporting.html) for more information on how to use reports.
Currently, clients default to not sending in reports; you can enable reporting
by setting the `report` parameter to true.
To use a report, set the `reports` parameter on the server; multiple
reports must be comma-separated. You can also specify `none` to disable
reports entirely.
Puppet provides multiple report handlers that will process client reports:
"
diff --git a/lib/puppet/relationship.rb b/lib/puppet/relationship.rb
index ebac97e7e..d0a3e2455 100644
--- a/lib/puppet/relationship.rb
+++ b/lib/puppet/relationship.rb
@@ -1,98 +1,103 @@
# subscriptions are permanent associations determining how different
# objects react to an event
require 'puppet/util/pson'
# This is Puppet's class for modeling edges in its configuration graph.
# It used to be a subclass of GRATR::Edge, but that class has weird hash
# overrides that dramatically slow down the graphing.
class Puppet::Relationship
extend Puppet::Util::Pson
attr_accessor :source, :target, :callback
attr_reader :event
- def self.from_pson(pson)
- source = pson["source"]
- target = pson["target"]
+ def self.from_data_hash(data)
+ source = data["source"]
+ target = data["target"]
args = {}
- if event = pson["event"]
+ if event = data["event"]
args[:event] = event
end
- if callback = pson["callback"]
+ if callback = data["callback"]
args[:callback] = callback
end
new(source, target, args)
end
+ def self.from_pson(pson)
+ Puppet.deprecation_warning("from_pson is being removed in favour of from_data_hash.")
+ self.from_data_hash(pson)
+ end
+
def event=(event)
raise ArgumentError, "You must pass a callback for non-NONE events" if event != :NONE and ! callback
@event = event
end
def initialize(source, target, options = {})
@source, @target = source, target
options = (options || {}).inject({}) { |h,a| h[a[0].to_sym] = a[1]; h }
[:callback, :event].each do |option|
if value = options[option]
send(option.to_s + "=", value)
end
end
end
# Does the passed event match our event? This is where the meaning
# of :NONE comes from.
def match?(event)
if self.event.nil? or event == :NONE or self.event == :NONE
return false
elsif self.event == :ALL_EVENTS or event == self.event
return true
else
return false
end
end
def label
result = {}
result[:callback] = callback if callback
result[:event] = event if event
result
end
def ref
"#{source} => #{target}"
end
def inspect
"{ #{source} => #{target} }"
end
def to_data_hash
data = {
'source' => source.to_s,
'target' => target.to_s
}
["event", "callback"].each do |attr|
next unless value = send(attr)
data[attr] = value
end
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_data_hash.to_pson(*args)
end
def to_s
ref
end
end
diff --git a/lib/puppet/reports/http.rb b/lib/puppet/reports/http.rb
index 475400797..403e81ad7 100644
--- a/lib/puppet/reports/http.rb
+++ b/lib/puppet/reports/http.rb
@@ -1,25 +1,31 @@
require 'puppet'
require 'puppet/network/http_pool'
require 'uri'
Puppet::Reports.register_report(:http) do
desc <<-DESC
Send reports via HTTP or HTTPS. This report processor submits reports as
POST requests to the address in the `reporturl` setting. The body of each POST
request is the YAML dump of a Puppet::Transaction::Report object, and the
Content-Type is set as `application/x-yaml`.
DESC
def process
url = URI.parse(Puppet[:reporturl])
- body = self.to_yaml
headers = { "Content-Type" => "application/x-yaml" }
+ options = {}
+ if url.user && url.password
+ options[:basic_auth] = {
+ :user => url.user,
+ :password => url.password
+ }
+ end
use_ssl = url.scheme == 'https'
conn = Puppet::Network::HttpPool.http_instance(url.host, url.port, use_ssl)
- response = conn.post(url.path, body, headers)
+ response = conn.post(url.path, self.to_yaml, headers, options)
unless response.kind_of?(Net::HTTPSuccess)
Puppet.err "Unable to submit report to #{Puppet[:reporturl].to_s} [#{response.code}] #{response.msg}"
end
end
end
diff --git a/lib/puppet/reports/rrdgraph.rb b/lib/puppet/reports/rrdgraph.rb
index e55d653c4..6238ddda9 100644
--- a/lib/puppet/reports/rrdgraph.rb
+++ b/lib/puppet/reports/rrdgraph.rb
@@ -1,128 +1,128 @@
Puppet::Reports.register_report(:rrdgraph) do
desc "Graph all available data about hosts using the RRD library. You
must have the Ruby RRDtool library installed to use this report, which
you can get from
[the RubyRRDTool RubyForge page](http://rubyforge.org/projects/rubyrrdtool/).
This package may also be available as `librrd-ruby`, `ruby-rrd`, or `rrdtool-ruby` in your
distribution's package management system. The library and/or package will both
require the binary `rrdtool` package from your distribution to be installed.
This report will create, manage, and graph RRD database files for each
of the metrics generated during transactions, and it will create a
few simple html files to display the reporting host's graphs. At this
point, it will not create a common index file to display links to
all hosts.
All RRD files and graphs get created in the `rrddir` directory. If
you want to serve these publicly, you should be able to just alias that
directory in a web server.
If you really know what you're doing, you can tune the `rrdinterval`,
which defaults to the `runinterval`."
def hostdir
@hostdir ||= File.join(Puppet[:rrddir], self.host)
end
def htmlfile(type, graphs, field)
file = File.join(hostdir, "#{type}.html")
File.open(file, "w") do |of|
of.puts "<html><head><title>#{type.capitalize} graphs for #{host}</title></head><body>"
graphs.each do |graph|
if field == :first
name = graph.sub(/-\w+.png/, '').capitalize
else
name = graph.sub(/\w+-/, '').sub(".png", '').capitalize
end
of.puts "<img src=#{graph}><br>"
end
of.puts "</body></html>"
end
file
end
def mkhtml
images = Dir.entries(hostdir).find_all { |d| d =~ /\.png/ }
periodorder = %w{daily weekly monthly yearly}
periods = {}
types = {}
images.each do |n|
type, period = n.sub(".png", '').split("-")
periods[period] ||= []
types[type] ||= []
periods[period] << n
types[type] << n
end
files = []
# Make the period html files
periodorder.each do |period|
unless ary = periods[period]
raise Puppet::Error, "Could not find graphs for #{period}"
end
files << htmlfile(period, ary, :first)
end
# make the type html files
types.sort { |a,b| a[0] <=> b[0] }.each do |type, ary|
newary = []
periodorder.each do |period|
if graph = ary.find { |g| g.include?("-#{period}.png") }
newary << graph
else
raise "Could not find #{type}-#{period} graph"
end
end
files << htmlfile(type, newary, :second)
end
File.open(File.join(hostdir, "index.html"), "w") do |of|
of.puts "<html><head><title>Report graphs for #{host}</title></head><body>"
files.each do |file|
of.puts "<a href='#{File.basename(file)}'>#{File.basename(file).sub(".html",'').capitalize}</a><br/>"
end
of.puts "</body></html>"
end
end
def process(time = nil)
time ||= Time.now.to_i
unless File.directory?(hostdir) and FileTest.writable?(hostdir)
# Some hackishness to create the dir with all of the right modes and ownership
config = Puppet::Settings.new
config.define_settings(:reports, :hostdir => {:type => :directory, :default => hostdir, :owner => 'service', :mode => 0755, :group => 'service', :desc => "eh"})
# This creates the dir.
config.use(:reports)
end
self.metrics.each do |name, metric|
metric.basedir = hostdir
if name == "time"
timeclean(metric)
end
metric.store(time)
metric.graph
end
- mkhtml unless Puppet::FileSystem::File.exist?(File.join(hostdir, "index.html"))
+ mkhtml unless Puppet::FileSystem.exist?(File.join(hostdir, "index.html"))
end
# Unfortunately, RRD does not deal well with changing lists of values,
# so we have to pick a list of values and stick with it. In this case,
# that means we record the total time, the config time, and that's about
# it. We should probably send each type's time as a separate metric.
def timeclean(metric)
metric.values = metric.values.find_all { |name, label, value| ['total', 'config_retrieval'].include?(name.to_s) }
end
end
diff --git a/lib/puppet/reports/store.rb b/lib/puppet/reports/store.rb
index 27d649355..5b82f6d0f 100644
--- a/lib/puppet/reports/store.rb
+++ b/lib/puppet/reports/store.rb
@@ -1,73 +1,73 @@
require 'puppet'
require 'fileutils'
require 'tempfile'
SEPARATOR = [Regexp.escape(File::SEPARATOR.to_s), Regexp.escape(File::ALT_SEPARATOR.to_s)].join
Puppet::Reports.register_report(:store) do
desc "Store the yaml report on disk. Each host sends its report as a YAML dump
and this just stores the file on disk, in the `reportdir` directory.
These files collect quickly -- one every half hour -- so it is a good idea
to perform some maintenance on them if you use this report (it's the only
default report)."
def process
validate_host(host)
dir = File.join(Puppet[:reportdir], host)
- if ! Puppet::FileSystem::File.exist?(dir)
+ if ! Puppet::FileSystem.exist?(dir)
FileUtils.mkdir_p(dir)
FileUtils.chmod_R(0750, dir)
end
# Now store the report.
now = Time.now.gmtime
name = %w{year month day hour min}.collect do |method|
# Make sure we're at least two digits everywhere
"%02d" % now.send(method).to_s
end.join("") + ".yaml"
file = File.join(dir, name)
f = Tempfile.new(name, dir)
begin
begin
f.chmod(0640)
f.print to_yaml
ensure
f.close
end
FileUtils.mv(f.path, file)
rescue => detail
Puppet.log_exception(detail, "Could not write report for #{host} at #{file}: #{detail}")
end
# Only testing cares about the return value
file
end
# removes all reports for a given host?
def self.destroy(host)
validate_host(host)
dir = File.join(Puppet[:reportdir], host)
- if Puppet::FileSystem::File.exist?(dir)
+ if Puppet::FileSystem.exist?(dir)
Dir.entries(dir).each do |file|
next if ['.','..'].include?(file)
file = File.join(dir, file)
- Puppet::FileSystem::File.unlink(file) if File.file?(file)
+ Puppet::FileSystem.unlink(file) if File.file?(file)
end
Dir.rmdir(dir)
end
end
def validate_host(host)
if host =~ Regexp.union(/[#{SEPARATOR}]/, /\A\.\.?\Z/)
raise ArgumentError, "Invalid node name #{host.inspect}"
end
end
module_function :validate_host
end
diff --git a/lib/puppet/reports/tagmail.rb b/lib/puppet/reports/tagmail.rb
index a07f98986..eb9888edc 100644
--- a/lib/puppet/reports/tagmail.rb
+++ b/lib/puppet/reports/tagmail.rb
@@ -1,179 +1,179 @@
require 'puppet'
require 'pp'
require 'net/smtp'
require 'time'
Puppet::Reports.register_report(:tagmail) do
desc "This report sends specific log messages to specific email addresses
based on the tags in the log messages.
- See the [documentation on tags](http://projects.puppetlabs.com/projects/puppet/wiki/Using_Tags) for more information.
+ See the [documentation on tags](http://docs.puppetlabs.com/puppet/latest/reference/lang_tags.html) for more information.
To use this report, you must create a `tagmail.conf` file in the location
specified by the `tagmap` setting. This is a simple file that maps tags to
email addresses: Any log messages in the report that match the specified
tags will be sent to the specified email addresses.
Lines in the `tagmail.conf` file consist of a comma-separated list
of tags, a colon, and a comma-separated list of email addresses.
Tags can be !negated with a leading exclamation mark, which will
subtract any messages with that tag from the set of events handled
by that line.
Puppet's log levels (`debug`, `info`, `notice`, `warning`, `err`,
`alert`, `emerg`, `crit`, and `verbose`) can also be used as tags,
and there is an `all` tag that will always match all log messages.
An example `tagmail.conf`:
all: me@domain.com
webserver, !mailserver: httpadmins@domain.com
This will send all messages to `me@domain.com`, and all messages from
webservers that are not also from mailservers to `httpadmins@domain.com`.
If you are using anti-spam controls such as grey-listing on your mail
server, you should whitelist the sending email address (controlled by
`reportfrom` configuration option) to ensure your email is not discarded as spam.
"
# Find all matching messages.
def match(taglists)
matching_logs = []
taglists.each do |emails, pos, neg|
# First find all of the messages matched by our positive tags
messages = nil
if pos.include?("all")
messages = self.logs
else
# Find all of the messages that are tagged with any of our
# tags.
messages = self.logs.find_all do |log|
pos.detect { |tag| log.tagged?(tag) }
end
end
# Now go through and remove any messages that match our negative tags
messages = messages.reject do |log|
true if neg.detect do |tag| log.tagged?(tag) end
end
if messages.empty?
Puppet.info "No messages to report to #{emails.join(",")}"
next
else
matching_logs << [emails, messages.collect { |m| m.to_report }.join("\n")]
end
end
matching_logs
end
# Load the config file
def parse(text)
taglists = []
text.split("\n").each do |line|
taglist = emails = nil
case line.chomp
when /^\s*#/; next
when /^\s*$/; next
when /^\s*(.+)\s*:\s*(.+)\s*$/
taglist = $1
emails = $2.sub(/#.*$/,'')
else
raise ArgumentError, "Invalid tagmail config file"
end
pos = []
neg = []
taglist.sub(/\s+$/,'').split(/\s*,\s*/).each do |tag|
unless tag =~ /^!?[-\w\.]+$/
raise ArgumentError, "Invalid tag #{tag.inspect}"
end
case tag
when /^\w+/; pos << tag
when /^!\w+/; neg << tag.sub("!", '')
else
raise Puppet::Error, "Invalid tag '#{tag}'"
end
end
# Now split the emails
emails = emails.sub(/\s+$/,'').split(/\s*,\s*/)
taglists << [emails, pos, neg]
end
taglists
end
# Process the report. This just calls the other associated messages.
def process
- unless Puppet::FileSystem::File.exist?(Puppet[:tagmap])
+ unless Puppet::FileSystem.exist?(Puppet[:tagmap])
Puppet.notice "Cannot send tagmail report; no tagmap file #{Puppet[:tagmap]}"
return
end
metrics = raw_summary['resources'] || {} rescue {}
if metrics['out_of_sync'] == 0 && metrics['changed'] == 0
Puppet.notice "Not sending tagmail report; no changes"
return
end
taglists = parse(File.read(Puppet[:tagmap]))
# Now find any appropriately tagged messages.
reports = match(taglists)
send(reports) unless reports.empty?
end
# Send the email reports.
def send(reports)
pid = Puppet::Util.safe_posix_fork do
if Puppet[:smtpserver] != "none"
begin
Net::SMTP.start(Puppet[:smtpserver], Puppet[:smtpport], Puppet[:smtphelo]) do |smtp|
reports.each do |emails, messages|
smtp.open_message_stream(Puppet[:reportfrom], *emails) do |p|
p.puts "From: #{Puppet[:reportfrom]}"
p.puts "Subject: Puppet Report for #{self.host}"
p.puts "To: " + emails.join(", ")
p.puts "Date: #{Time.now.rfc2822}"
p.puts
p.puts messages
end
end
end
rescue => detail
message = "Could not send report emails through smtp: #{detail}"
Puppet.log_exception(detail, message)
- raise Puppet::Error, message
+ raise Puppet::Error, message, detail.backtrace
end
elsif Puppet[:sendmail] != ""
begin
reports.each do |emails, messages|
# We need to open a separate process for every set of email addresses
IO.popen(Puppet[:sendmail] + " " + emails.join(" "), "w") do |p|
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
rescue => detail
message = "Could not send report emails via sendmail: #{detail}"
Puppet.log_exception(detail, message)
- raise Puppet::Error, message
+ raise Puppet::Error, message, detail.backtrace
end
else
raise Puppet::Error, "SMTP server is unset and could not find sendmail"
end
end
# Don't bother waiting for the pid to return.
Process.detach(pid)
end
end
diff --git a/lib/puppet/resource.rb b/lib/puppet/resource.rb
index d4eac6f6d..249740b93 100644
--- a/lib/puppet/resource.rb
+++ b/lib/puppet/resource.rb
@@ -1,549 +1,542 @@
require 'puppet'
require 'puppet/util/tagging'
require 'puppet/util/pson'
require 'puppet/parameter'
# The simplest resource class. Eventually it will function as the
# base class for all resource-like behaviour.
#
# @api public
class Puppet::Resource
# This stub class is only needed for serialization compatibility with 0.25.x.
# Specifically, it exists to provide a compatibility API when using YAML
# serialized objects loaded from StoreConfigs.
Reference = Puppet::Resource
include Puppet::Util::Tagging
- require 'puppet/resource/type_collection_helper'
- include Puppet::Resource::TypeCollectionHelper
-
extend Puppet::Util::Pson
include Enumerable
attr_accessor :file, :line, :catalog, :exported, :virtual, :validate_parameters, :strict
attr_reader :type, :title
require 'puppet/indirector'
extend Puppet::Indirector
indirects :resource, :terminus_class => :ral
ATTRIBUTES = [:file, :line, :exported]
- def self.from_pson(pson)
- raise ArgumentError, "No resource type provided in serialized data" unless type = pson['type']
- raise ArgumentError, "No resource title provided in serialized data" unless title = pson['title']
+ def self.from_data_hash(data)
+ raise ArgumentError, "No resource type provided in serialized data" unless type = data['type']
+ raise ArgumentError, "No resource title provided in serialized data" unless title = data['title']
resource = new(type, title)
- if params = pson['parameters']
+ if params = data['parameters']
params.each { |param, value| resource[param] = value }
end
- if tags = pson['tags']
+ if tags = data['tags']
tags.each { |tag| resource.tag(tag) }
end
ATTRIBUTES.each do |a|
- if value = pson[a.to_s]
+ if value = data[a.to_s]
resource.send(a.to_s + "=", value)
end
end
resource
end
+ def self.from_pson(pson)
+ Puppet.deprecation_warning("from_pson is being removed in favour of from_data_hash.")
+ self.from_data_hash(pson)
+ end
+
def inspect
"#{@type}[#{@title}]#{to_hash.inspect}"
end
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
hash
end
data["exported"] ||= false
params = self.to_hash.inject({}) do |hash, ary|
param, value = ary
# Don't duplicate the title as the namevar
next hash if param == namevar and value == title
hash[param] = Puppet::Resource.value_to_pson_data(value)
hash
end
data["parameters"] = params unless params.empty?
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) }
elsif value.is_a? Puppet::Resource
value.to_s
else
value
end
end
def yaml_property_munge(x)
case x
when Hash
x.inject({}) { |h,kv|
k,v = kv
h[k] = self.class.value_to_pson_data(v)
h
}
else self.class.value_to_pson_data(x)
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_data_hash.to_pson(*args)
end
# Proxy these methods to the parameters hash. It's likely they'll
# be overridden at some point, but this works for now.
%w{has_key? keys length delete empty? <<}.each do |method|
define_method(method) do |*args|
parameters.send(method, *args)
end
end
# Set a given parameter. Converts all passed names
# to lower-case symbols.
def []=(param, value)
validate_parameter(param) if validate_parameters
parameters[parameter_name(param)] = value
end
# Return a given parameter's value. Converts all passed names
# to lower-case symbols.
def [](param)
parameters[parameter_name(param)]
end
def ==(other)
return false unless other.respond_to?(:title) and self.type == other.type and self.title == other.title
return false unless to_hash == other.to_hash
true
end
# Compatibility method.
def builtin?
builtin_type?
end
# Is this a builtin resource type?
def builtin_type?
resource_type.is_a?(Class)
end
# Iterate over each param/value pair, as required for Enumerable.
def each
parameters.each { |p,v| yield p, v }
end
def include?(parameter)
super || parameters.keys.include?( parameter_name(parameter) )
end
- # These two methods are extracted into a Helper
- # module, but file load order prevents me
- # from including them in the class, and I had weird
- # behaviour (i.e., sometimes it didn't work) when
- # I directly extended each resource with the helper.
- def environment
- Puppet::Node::Environment.new(@environment)
- end
-
- def environment=(env)
- if env.is_a?(String) or env.is_a?(Symbol)
- @environment = env
- else
- @environment = env.name
- end
- end
-
%w{exported virtual strict}.each do |m|
define_method(m+"?") do
self.send(m)
end
end
def class?
@is_class ||= @type == "Class"
end
def stage?
@is_stage ||= @type.to_s.downcase == "stage"
end
# Create our resource.
def initialize(type, title = nil, attributes = {})
@parameters = {}
# Set things like strictness first.
attributes.each do |attr, value|
next if attr == :parameters
send(attr.to_s + "=", value)
end
@type, @title = extract_type_and_title(type, title)
@type = munge_type_name(@type)
if self.class?
@title = :main if @title == ""
@title = munge_type_name(@title)
end
if params = attributes[:parameters]
extract_parameters(params)
end
tag(self.type)
tag(self.title) if valid_tag?(self.title)
@reference = self # for serialization compatibility with 0.25.x
if strict? and ! resource_type
if self.class?
raise ArgumentError, "Could not find declared class #{title}"
else
raise ArgumentError, "Invalid resource type #{type}"
end
end
end
def ref
to_s
end
# Find our resource.
def resolve
catalog ? catalog.resource(to_s) : nil
end
+ # The resource's type implementation
+ # @return [Puppet::Type, Puppet::Resource::Type]
+ # @api private
def resource_type
@rstype ||= case type
- when "Class"; known_resource_types.hostclass(title == :main ? "" : title)
- when "Node"; known_resource_types.node(title)
+ when "Class"; environment.known_resource_types.hostclass(title == :main ? "" : title)
+ when "Node"; environment.known_resource_types.node(title)
else
- Puppet::Type.type(type) || known_resource_types.definition(type)
+ Puppet::Type.type(type) || environment.known_resource_types.definition(type)
end
end
+ # Set the resource's type implementation
+ # @param type [Puppet::Type, Puppet::Resource::Type]
+ # @api private
+ def resource_type=(type)
+ @rstype = type
+ end
+
+ def environment
+ @environment ||= Puppet.lookup(:environments).get(Puppet[:environment])
+ end
+
+ def environment=(environment)
+ @environment = environment
+ end
+
# Produce a simple hash of our parameters.
def to_hash
parse_title.merge parameters
end
def to_s
"#{type}[#{title}]"
end
def uniqueness_key
# Temporary kludge to deal with inconsistant use patters
h = self.to_hash
h[namevar] ||= h[:name]
h[:name] ||= h[namevar]
h.values_at(*key_attributes.sort_by { |k| k.to_s })
end
def key_attributes
resource_type.respond_to?(:key_attributes) ? resource_type.key_attributes : [:name]
end
# Convert our resource to Puppet code.
def to_manifest
# Collect list of attributes to align => and move ensure first
attr = parameters.keys
attr_max = attr.inject(0) { |max,k| k.to_s.length > max ? k.to_s.length : max }
attr.sort!
if attr.first != :ensure && attr.include?(:ensure)
attr.delete(:ensure)
attr.unshift(:ensure)
end
attributes = attr.collect { |k|
v = parameters[k]
" %-#{attr_max}s => %s,\n" % [k, Puppet::Parameter.format_value_for_display(v)]
}.join
"%s { '%s':\n%s}" % [self.type.to_s.downcase, self.title, attributes]
end
def to_ref
ref
end
# Convert our resource to a RAL resource instance. Creates component
# instances for resource types that don't exist.
def to_ral
- if typeklass = Puppet::Type.type(self.type)
- return typeklass.new(self)
- else
- return Puppet::Type::Component.new(self)
- end
+ typeklass = Puppet::Type.type(self.type) || Puppet::Type.type(:component)
+ typeklass.new(self)
end
def name
# this is potential namespace conflict
# between the notion of an "indirector name"
# and a "resource name"
[ type, title ].join('/')
end
def missing_arguments
resource_type.arguments.select do |param, default|
param = param.to_sym
parameters[param].nil? || parameters[param].value == :undef
end
end
private :missing_arguments
# Consult external data bindings for class parameter values which must be
# namespaced in the backend.
#
# Example:
#
# class foo($port=0){ ... }
#
# We make a request to the backend for the key 'foo::port' not 'foo'
#
def lookup_external_default_for(param, scope)
# Only lookup parameters for host classes
return nil unless resource_type.type == :hostclass
name = "#{resource_type.name}::#{param}"
- # Lookup with injector (optionally), and if no value bound, lookup with "classic hiera"
- result = nil
- if scope.compiler.is_binder_active?
- result = scope.compiler.injector.lookup(scope, name)
- end
- if result.nil?
- lookup_with_databinding(name, scope)
- else
- result
- end
+ lookup_with_databinding(name, scope)
end
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)
unless is_a?(Puppet::Parser::Resource)
fail Puppet::DevError, "Cannot evaluate default parameters for #{self} - not a parser resource"
end
missing_arguments.collect do |param, default|
external_value = lookup_external_default_for(param, scope)
if external_value.nil? && default.nil?
next
elsif external_value.nil?
value = default.safeevaluate(scope)
else
value = external_value
end
self[param.to_sym] = value
param
end.compact
end
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.environment = environment
+ result.instance_variable_set(:@rstype, resource_type)
result
end
def valid_parameter?(name)
resource_type.valid_parameter?(name)
end
# Verify that all required arguments are either present or
# have been provided with defaults.
# Must be called after 'set_default_parameters'. We can't join the methods
# because Type#set_parameters needs specifically ordered behavior.
def validate_complete
return unless resource_type and resource_type.respond_to?(:arguments)
resource_type.arguments.each do |param, default|
param = param.to_sym
fail Puppet::ParseError, "Must pass #{param} to #{self}" unless parameters.include?(param)
end
end
def validate_parameter(name)
raise ArgumentError, "Invalid parameter #{name}" unless valid_parameter?(name)
end
def prune_parameters(options = {})
properties = resource_type.properties.map(&:name)
dup.collect do |attribute, value|
if value.to_s.empty? or Array(value).empty?
delete(attribute)
elsif value.to_s == "absent" and attribute.to_s != "ensure"
delete(attribute)
end
parameters_to_include = options[:parameters_to_include] || []
delete(attribute) unless properties.include?(attribute) || parameters_to_include.include?(attribute)
end
self
end
private
# Produce a canonical method name.
def parameter_name(param)
param = param.to_s.downcase.to_sym
if param == :name and namevar
param = namevar
end
param
end
# The namevar for our resource type. If the type doesn't exist,
# always use :name.
def namevar
if builtin_type? and t = resource_type and t.key_attributes.length == 1
t.key_attributes.first
else
:name
end
end
def extract_parameters(params)
params.each do |param, value|
validate_parameter(param) if strict?
self[param] = value
end
end
def extract_type_and_title(argtype, argtitle)
if (argtitle || argtype) =~ /^([^\[\]]+)\[(.+)\]$/m then [ $1, $2 ]
elsif argtitle then [ argtype, argtitle ]
elsif argtype.is_a?(Puppet::Type) then [ argtype.class.name, argtype.title ]
elsif argtype.is_a?(Hash) then
raise ArgumentError, "Puppet::Resource.new does not take a hash as the first argument. "+
"Did you mean (#{(argtype[:type] || argtype["type"]).inspect}, #{(argtype[:title] || argtype["title"]).inspect }) ?"
else raise ArgumentError, "No title provided and #{argtype.inspect} is not a valid resource reference"
end
end
def munge_type_name(value)
return :main if value == :main
return "Class" if value == "" or value.nil? or value.to_s.downcase == "component"
value.to_s.split("::").collect { |s| s.capitalize }.join("::")
end
def parse_title
h = {}
type = resource_type
if type.respond_to? :title_patterns
type.title_patterns.each { |regexp, symbols_and_lambdas|
if captures = regexp.match(title.to_s)
symbols_and_lambdas.zip(captures[1..-1]).each do |symbol_and_lambda,capture|
symbol, proc = symbol_and_lambda
# Many types pass "identity" as the proc; we might as well give
# them a shortcut to delivering that without the extra cost.
#
# Especially because the global type defines title_patterns and
# uses the identity patterns.
#
# This was worth about 8MB of memory allocation saved in my
# testing, so is worth the complexity for the API.
if proc then
h[symbol] = proc.call(capture)
else
h[symbol] = capture
end
end
return h
end
}
# If we've gotten this far, then none of the provided title patterns
# matched. Since there's no way to determine the title then the
# resource should fail here.
raise Puppet::Error, "No set of title patterns matched the title \"#{title}\"."
else
return { :name => title.to_s }
end
end
def parameters
# @parameters could have been loaded from YAML, causing it to be nil (by
# bypassing initialize).
@parameters ||= {}
end
end
diff --git a/lib/puppet/resource/catalog.rb b/lib/puppet/resource/catalog.rb
index 61b4f0b81..47872032f 100644
--- a/lib/puppet/resource/catalog.rb
+++ b/lib/puppet/resource/catalog.rb
@@ -1,545 +1,543 @@
require 'puppet/node'
require 'puppet/indirector'
require 'puppet/transaction'
require 'puppet/util/pson'
require 'puppet/util/tagging'
require 'puppet/graph'
# This class models a node catalog. It is the thing meant to be passed
# from server to client, and it contains all of the information in the
# catalog, including the resources and the relationships between them.
#
# @api public
class Puppet::Resource::Catalog < Puppet::Graph::SimpleGraph
class DuplicateResourceError < Puppet::Error
include Puppet::ExternalFileError
end
extend Puppet::Indirector
indirects :catalog, :terminus_setting => :catalog_terminus
include Puppet::Util::Tagging
extend Puppet::Util::Pson
# The host name this is a catalog for.
attr_accessor :name
# The catalog version. Used for testing whether a catalog
# is up to date.
attr_accessor :version
# How long this catalog took to retrieve. Used for reporting stats.
attr_accessor :retrieval_duration
# Whether this is a host catalog, which behaves very differently.
# In particular, reports are sent, graphs are made, and state is
# stored in the state database. If this is set incorrectly, then you often
# end up in infinite loops, because catalogs are used to make things
# that the host catalog needs.
attr_accessor :host_config
# Whether this catalog was retrieved from the cache, which affects
# whether it is written back out again.
attr_accessor :from_cache
# Some metadata to help us compile and generally respond to the current state.
attr_accessor :client_version, :server_version
# The environment for this catalog
attr_accessor :environment
# Add classes to our class list.
def add_class(*classes)
classes.each do |klass|
@classes << klass
end
# Add the class names as tags, too.
tag(*classes)
end
def title_key_for_ref( ref )
ref =~ /^([-\w:]+)\[(.*)\]$/m
[$1, $2]
end
def add_resource(*resources)
resources.each do |resource|
add_one_resource(resource)
end
end
# @param resource [A Resource] a resource in the catalog
# @return [A Resource, nil] the resource that contains the given resource
# @api public
def container_of(resource)
adjacent(resource, :direction => :in)[0]
end
def add_one_resource(resource)
fail_on_duplicate_type_and_title(resource)
add_resource_to_table(resource)
create_resource_aliases(resource)
resource.catalog = self if resource.respond_to?(:catalog=)
add_resource_to_graph(resource)
end
private :add_one_resource
def add_resource_to_table(resource)
title_key = title_key_for_ref(resource.ref)
@resource_table[title_key] = resource
@resources << title_key
end
private :add_resource_to_table
def add_resource_to_graph(resource)
add_vertex(resource)
@relationship_graph.add_vertex(resource) if @relationship_graph
end
private :add_resource_to_graph
def create_resource_aliases(resource)
if resource.respond_to?(:isomorphic?) and resource.isomorphic? and resource.name != resource.title
self.alias(resource, resource.uniqueness_key)
end
end
private :create_resource_aliases
# Create an alias for a resource.
def alias(resource, key)
resource.ref =~ /^(.+)\[/
class_name = $1 || resource.class.name
newref = [class_name, key].flatten
if key.is_a? String
ref_string = "#{class_name}[#{key}]"
return if ref_string == resource.ref
end
# LAK:NOTE It's important that we directly compare the references,
# because sometimes an alias is created before the resource is
# added to the catalog, so comparing inside the below if block
# isn't sufficient.
if existing = @resource_table[newref]
return if existing == resource
resource_declaration = " at #{resource.file}:#{resource.line}" if resource.file and resource.line
existing_declaration = " at #{existing.file}:#{existing.line}" if existing.file and existing.line
msg = "Cannot alias #{resource.ref} to #{key.inspect}#{resource_declaration}; resource #{newref.inspect} already declared#{existing_declaration}"
raise ArgumentError, msg
end
@resource_table[newref] = resource
@aliases[resource.ref] ||= []
@aliases[resource.ref] << newref
end
# Apply our catalog to the local host.
# @param options [Hash{Symbol => Object}] a hash of options
# @option options [Puppet::Transaction::Report] :report
# The report object to log this transaction to. This is optional,
# and the resulting transaction will create a report if not
# supplied.
# @option options [Array[String]] :tags
# Tags used to filter the transaction. If supplied then only
# resources tagged with any of these tags will be evaluated.
# @option options [Boolean] :ignoreschedules
# Ignore schedules when evaluating resources
# @option options [Boolean] :for_network_device
# Whether this catalog is for a network device
#
# @return [Puppet::Transaction] the transaction created for this
# application
#
# @api public
def apply(options = {})
Puppet::Util::Storage.load if host_config?
transaction = create_transaction(options)
begin
transaction.report.as_logging_destination do
transaction.evaluate
end
rescue Puppet::Error => detail
Puppet.log_exception(detail, "Could not apply complete catalog: #{detail}")
rescue => detail
Puppet.log_exception(detail, "Got an uncaught exception of type #{detail.class}: #{detail}")
ensure
# Don't try to store state unless we're a host config
# too recursive.
Puppet::Util::Storage.store if host_config?
end
yield transaction if block_given?
transaction
end
# The relationship_graph form of the catalog. This contains all of the
# dependency edges that are used for determining order.
#
# @return [Puppet::Graph::RelationshipGraph]
# @api public
def relationship_graph
if @relationship_graph.nil?
@relationship_graph = Puppet::Graph::RelationshipGraph.new(prioritizer)
@relationship_graph.populate_from(self)
end
@relationship_graph
end
def clear(remove_resources = true)
super()
# We have to do this so that the resources clean themselves up.
@resource_table.values.each { |resource| resource.remove } if remove_resources
@resource_table.clear
@resources = []
if @relationship_graph
@relationship_graph.clear
@relationship_graph = nil
end
end
def classes
@classes.dup
end
# Create a new resource and register it in the catalog.
def create_resource(type, options)
unless klass = Puppet::Type.type(type)
raise ArgumentError, "Unknown resource type #{type}"
end
return unless resource = klass.new(options)
add_resource(resource)
resource
end
# Make sure all of our resources are "finished".
def finalize
make_default_resources
@resource_table.values.each { |resource| resource.finish }
write_graph(:resources)
end
def host_config?
host_config
end
def initialize(name = nil)
super()
@name = name if name
@classes = []
@resource_table = {}
@resources = []
@relationship_graph = nil
@host_config = true
@aliases = {}
if block_given?
yield(self)
finalize
end
end
# Make the default objects necessary for function.
def make_default_resources
# We have to add the resources to the catalog, or else they won't get cleaned up after
# the transaction.
# First create the default scheduling objects
Puppet::Type.type(:schedule).mkdefaultschedules.each { |res| add_resource(res) unless resource(res.ref) }
# And filebuckets
if bucket = Puppet::Type.type(:filebucket).mkdefaultbucket
add_resource(bucket) unless resource(bucket.ref)
end
end
# Remove the resource from our catalog. Notice that we also call
# 'remove' on the resource, at least until resource classes no longer maintain
# references to the resource instances.
def remove_resource(*resources)
resources.each do |resource|
title_key = title_key_for_ref(resource.ref)
@resource_table.delete(title_key)
if aliases = @aliases[resource.ref]
aliases.each { |res_alias| @resource_table.delete(res_alias) }
@aliases.delete(resource.ref)
end
remove_vertex!(resource) if vertex?(resource)
@relationship_graph.remove_vertex!(resource) if @relationship_graph and @relationship_graph.vertex?(resource)
@resources.delete(title_key)
resource.remove
end
end
# Look a resource up by its reference (e.g., File[/etc/passwd]).
def resource(type, title = nil)
# Always create a resource reference, so that it always
# canonicalizes how we are referring to them.
if title
res = Puppet::Resource.new(type, title)
else
# If they didn't provide a title, then we expect the first
# argument to be of the form 'Class[name]', which our
# Reference class canonicalizes for us.
res = Puppet::Resource.new(nil, type)
end
title_key = [res.type, res.title.to_s]
uniqueness_key = [res.type, res.uniqueness_key].flatten
@resource_table[title_key] || @resource_table[uniqueness_key]
end
def resource_refs
resource_keys.collect{ |type, name| name.is_a?( String ) ? "#{type}[#{name}]" : nil}.compact
end
def resource_keys
@resource_table.keys
end
def resources
@resources.collect do |key|
@resource_table[key]
end
end
- def self.from_pson(data)
+ def self.from_data_hash(data)
result = new(data['name'])
if tags = data['tags']
result.tag(*tags)
end
if version = data['version']
result.version = version
end
if environment = data['environment']
result.environment = environment
end
if resources = data['resources']
result.add_resource(*resources.collect do |res|
- Puppet::Resource.from_pson(res)
+ Puppet::Resource.from_data_hash(res)
end)
end
if edges = data['edges']
- edges = PSON.parse(edges) if edges.is_a?(String)
- edges.each do |edge|
- edge_from_pson(result, edge)
+ edges.each do |edge_hash|
+ edge = Puppet::Relationship.from_data_hash(edge_hash)
+ unless source = result.resource(edge.source)
+ raise ArgumentError, "Could not intern from data: Could not find relationship source #{edge.source.inspect}"
+ end
+ edge.source = source
+
+ unless target = result.resource(edge.target)
+ raise ArgumentError, "Could not intern from data: Could not find relationship target #{edge.target.inspect}"
+ end
+ edge.target = target
+
+ result.add_edge(edge)
end
end
if classes = data['classes']
result.add_class(*classes)
end
result
end
- def self.edge_from_pson(result, edge)
- # If no type information was presented, we manually find
- # the class.
- edge = Puppet::Relationship.from_pson(edge) if edge.is_a?(Hash)
- unless source = result.resource(edge.source)
- raise ArgumentError, "Could not convert from pson: Could not find relationship source #{edge.source.inspect}"
- end
- edge.source = source
-
- unless target = result.resource(edge.target)
- raise ArgumentError, "Could not convert from pson: Could not find relationship target #{edge.target.inspect}"
- end
- edge.target = target
-
- result.add_edge(edge)
+ def self.from_pson(data)
+ Puppet.deprecation_warning("from_pson is being removed in favour of from_data_hash.")
+ self.from_data_hash(data)
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' => to_data_hash,
'metadata' => {
'api_version' => 1
}
}
end
def to_pson(*args)
to_pson_data_hash.to_pson(*args)
end
# Convert our catalog into a RAL catalog.
def to_ral
to_catalog :to_ral
end
# Convert our catalog into a catalog of Puppet::Resource instances.
def to_resource
to_catalog :to_resource
end
# filter out the catalog, applying +block+ to each resource.
# If the block result is false, the resource will
# be kept otherwise it will be skipped
def filter(&block)
to_catalog :to_resource, &block
end
# Store the classes in the classfile.
def write_class_file
::File.open(Puppet[:classfile], "w") do |f|
f.puts classes.join("\n")
end
rescue => detail
Puppet.err "Could not create class file #{Puppet[:classfile]}: #{detail}"
end
# Store the list of resources we manage
def write_resource_file
::File.open(Puppet[:resourcefile], "w") do |f|
to_print = resources.map do |resource|
next unless resource.managed?
if resource.name_var
"#{resource.type}[#{resource[resource.name_var]}]"
else
"#{resource.ref.downcase}"
end
end.compact
f.puts to_print.join("\n")
end
rescue => detail
Puppet.err "Could not create resource file #{Puppet[:resourcefile]}: #{detail}"
end
# Produce the graph files if requested.
def write_graph(name)
# We only want to graph the main host catalog.
return unless host_config?
super
end
private
def prioritizer
@prioritizer ||= case Puppet[:ordering]
when "title-hash"
Puppet::Graph::TitleHashPrioritizer.new
when "manifest"
Puppet::Graph::SequentialPrioritizer.new
when "random"
Puppet::Graph::RandomPrioritizer.new
else
raise Puppet::DevError, "Unknown ordering type #{Puppet[:ordering]}"
end
end
def create_transaction(options)
transaction = Puppet::Transaction.new(self, options[:report], prioritizer)
transaction.tags = options[:tags] if options[:tags]
transaction.ignoreschedules = true if options[:ignoreschedules]
transaction.for_network_device = options[:network_device]
transaction
end
# Verify that the given resource isn't declared elsewhere.
def fail_on_duplicate_type_and_title(resource)
# 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
msg = "Duplicate declaration: #{resource.ref} is already declared"
msg << " in file #{existing_resource.file}:#{existing_resource.line}" if existing_resource.file and existing_resource.line
msg << "; cannot redeclare"
raise DuplicateResourceError.new(msg, resource.file, resource.line)
end
# An abstracted method for converting one catalog into another type of catalog.
# This pretty much just converts all of the resources from one class to another, using
# a conversion method.
def to_catalog(convert)
result = self.class.new(self.name)
result.version = self.version
result.environment = self.environment
map = {}
resources.each do |resource|
next if virtual_not_exported?(resource)
next if block_given? and yield resource
newres = resource.copy_as_resource
newres.catalog = result
if convert != :to_resource
newres = newres.to_ral
end
# We can't guarantee that resources don't munge their names
# (like files do with trailing slashes), so we have to keep track
# of what a resource got converted to.
map[resource.ref] = newres
result.add_resource newres
end
message = convert.to_s.gsub "_", " "
edges.each do |edge|
# Skip edges between virtual resources.
next if virtual_not_exported?(edge.source)
next if block_given? and yield edge.source
next if virtual_not_exported?(edge.target)
next if block_given? and yield edge.target
unless source = map[edge.source.ref]
raise Puppet::DevError, "Could not find resource #{edge.source.ref} when converting #{message} resources"
end
unless target = map[edge.target.ref]
raise Puppet::DevError, "Could not find resource #{edge.target.ref} when converting #{message} resources"
end
result.add_edge(source, target, edge.label)
end
map.clear
result.add_class(*self.classes)
result.tag(*self.tags)
result
end
def virtual_not_exported?(resource)
resource.respond_to?(:virtual?) and resource.virtual? and (resource.respond_to?(:exported?) and not resource.exported?)
end
end
diff --git a/lib/puppet/resource/status.rb b/lib/puppet/resource/status.rb
index eae5aeed9..c923c6fad 100644
--- a/lib/puppet/resource/status.rb
+++ b/lib/puppet/resource/status.rb
@@ -1,154 +1,155 @@
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
STATES = [:skipped, :failed, :failed_to_restart, :restarted, :changed, :out_of_sync, :scheduled]
attr_accessor *STATES
attr_reader :source_description, :containment_path,
:default_log_level, :time, :resource, :change_count,
:out_of_sync_count, :resource_type, :title
YAML_ATTRIBUTES = %w{@resource @file @line @evaluation_time @change_count
@out_of_sync_count @tags @time @events @out_of_sync
@changed @resource_type @title @skipped @failed
@containment_path}.
map(&:to_sym)
- def self.from_pson(data)
+ def self.from_data_hash(data)
obj = self.allocate
obj.initialize_from_hash(data)
obj
end
+ def self.from_pson(data)
+ Puppet.deprecation_warning("from_pson is being removed in favour of from_data_hash.")
+ self.from_data_hash(data)
+ end
+
# Provide a boolean method for each of the states.
STATES.each do |attr|
define_method("#{attr}?") do
!! send(attr)
end
end
def <<(event)
add_event(event)
self
end
def add_event(event)
@events << event
if event.status == 'failure'
self.failed = true
elsif event.status == 'success'
@change_count += 1
@changed = true
end
if event.status != 'audit'
@out_of_sync_count += 1
@out_of_sync = true
end
end
def events
@events
end
def failed_because(detail)
@real_resource.log_exception(detail, "Could not evaluate: #{detail}")
failed = true
# There's a contract (implicit unfortunately) that a status of failed
# 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(:name => :resource_error, :status => "failure", :message => detail.to_s))
end
def initialize(resource)
@real_resource = resource
@source_description = resource.path
@containment_path = resource.pathbuilder
@resource = resource.to_s
@change_count = 0
@out_of_sync_count = 0
@changed = false
@out_of_sync = false
@skipped = false
@failed = false
@file = resource.file
@line = resource.line
tag(*resource.tags)
@time = Time.now
@events = []
@resource_type = resource.type.to_s.capitalize
@title = resource.title
end
def initialize_from_hash(data)
@resource_type = data['resource_type']
@title = data['title']
@resource = data['resource']
@containment_path = data['containment_path']
@file = data['file']
@line = data['line']
@evaluation_time = data['evaluation_time']
@change_count = data['change_count']
@out_of_sync_count = data['out_of_sync_count']
@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']
@changed = data['changed']
@skipped = data['skipped']
@failed = data['failed']
@events = data['events'].map do |event|
- Puppet::Transaction::Event.from_pson(event)
+ Puppet::Transaction::Event.from_data_hash(event)
end
end
def to_data_hash
{
'title' => @title,
'file' => @file,
'line' => @line,
'resource' => @resource,
'resource_type' => @resource_type,
'containment_path' => @containment_path,
'evaluation_time' => @evaluation_time,
'tags' => @tags,
'time' => @time.iso8601(9),
'failed' => @failed,
'changed' => @changed,
'out_of_sync' => @out_of_sync,
'skipped' => @skipped,
'change_count' => @change_count,
'out_of_sync_count' => @out_of_sync_count,
'events' => @events,
}
end
- def to_pson(*args)
- to_data_hash.to_pson(*args)
- end
-
def to_yaml_properties
YAML_ATTRIBUTES & super
end
private
def log_source
source_description
end
end
end
end
diff --git a/lib/puppet/resource/type.rb b/lib/puppet/resource/type.rb
index 4cf841475..27884df9f 100644
--- a/lib/puppet/resource/type.rb
+++ b/lib/puppet/resource/type.rb
@@ -1,385 +1,386 @@
require 'puppet/parser'
require 'puppet/util/warnings'
require 'puppet/util/errors'
require 'puppet/util/inline_docs'
require 'puppet/parser/ast/leaf'
require 'puppet/parser/ast/block_expression'
require 'puppet/dsl'
# Puppet::Resource::Type represents nodes, classes and defined types.
#
# It has a standard format for external consumption, usable from the
# resource_type indirection via rest and the resource_type face. See the
# {file:api_docs/http_resource_type.md#Schema resource type schema
# description}.
#
# @api public
class Puppet::Resource::Type
Puppet::ResourceType = self
include Puppet::Util::InlineDocs
include Puppet::Util::Warnings
include Puppet::Util::Errors
RESOURCE_KINDS = [:hostclass, :node, :definition]
# Map the names used in our documentation to the names used internally
RESOURCE_KINDS_TO_EXTERNAL_NAMES = {
:hostclass => "class",
:node => "node",
:definition => "defined_type",
}
RESOURCE_EXTERNAL_NAMES_TO_KINDS = RESOURCE_KINDS_TO_EXTERNAL_NAMES.invert
attr_accessor :file, :line, :doc, :code, :ruby_code, :parent, :resource_type_collection
attr_reader :namespace, :arguments, :behaves_like, :module_name
# This should probably be renamed to 'kind' eventually, in accordance with the changes
# made for serialization and API usability (#14137). At the moment that seems like
# it would touch a whole lot of places in the code, though. --cprice 2012-04-23
attr_reader :type
RESOURCE_KINDS.each do |t|
define_method("#{t}?") { self.type == t }
end
require 'puppet/indirector'
extend Puppet::Indirector
indirects :resource_type, :terminus_class => :parser
- def self.from_pson(data)
+ def self.from_data_hash(data)
name = data.delete('name') or raise ArgumentError, "Resource Type names must be specified"
kind = data.delete('kind') || "definition"
unless type = RESOURCE_EXTERNAL_NAMES_TO_KINDS[kind]
raise ArgumentError, "Unsupported resource kind '#{kind}'"
end
data = data.inject({}) { |result, ary| result[ary[0].intern] = ary[1]; result }
# External documentation uses "parameters" but the internal name
# is "arguments"
data[:arguments] = data.delete(:parameters)
new(type, name, data)
end
- def to_pson(*args)
- to_data_hash.to_pson(*args)
+ def self.from_pson(data)
+ Puppet.deprecation_warning("from_pson is being removed in favour of from_data_hash.")
+ self.from_data_hash(data)
end
def to_data_hash
data = [:doc, :line, :file, :parent].inject({}) do |hash, param|
next hash unless (value = self.send(param)) and (value != "")
hash[param.to_s] = value
hash
end
# External documentation uses "parameters" but the internal name
# is "arguments"
data['parameters'] = arguments.dup unless arguments.empty?
data['name'] = name
unless RESOURCE_KINDS_TO_EXTERNAL_NAMES.has_key?(type)
raise ArgumentError, "Unsupported resource kind '#{type}'"
end
data['kind'] = RESOURCE_KINDS_TO_EXTERNAL_NAMES[type]
data
end
# Are we a child of the passed class? Do a recursive search up our
# parentage tree to figure it out.
def child_of?(klass)
return false unless parent
return(klass == parent_type ? true : parent_type.child_of?(klass))
end
# Now evaluate the code associated with this class or definition.
def evaluate_code(resource)
static_parent = evaluate_parent_type(resource)
scope = static_parent || resource.scope
scope = scope.newscope(:namespace => namespace, :source => self, :resource => resource) unless resource.title == :main
scope.compiler.add_class(name) unless definition?
set_resource_parameters(resource, scope)
resource.add_edge_to_stage
if code
if @match # Only bother setting up the ephemeral scope if there are match variables to add into it
begin
elevel = scope.ephemeral_level
scope.ephemeral_from(@match, file, line)
code.safeevaluate(scope)
ensure
scope.unset_ephemeral_var(elevel)
end
else
code.safeevaluate(scope)
end
end
evaluate_ruby_code(resource, scope) if ruby_code
end
def initialize(type, name, options = {})
@type = type.to_s.downcase.to_sym
raise ArgumentError, "Invalid resource supertype '#{type}'" unless RESOURCE_KINDS.include?(@type)
name = convert_from_ast(name) if name.is_a?(Puppet::Parser::AST::HostName)
set_name_and_namespace(name)
[:code, :doc, :line, :file, :parent].each do |param|
next unless value = options[param]
send(param.to_s + "=", value)
end
set_arguments(options[:arguments])
@match = nil
@module_name = options[:module_name]
end
# This is only used for node names, and really only when the node name
# is a regexp.
def match(string)
return string.to_s.downcase == name unless name_is_regex?
@match = @name.match(string)
end
# Add code from a new instance to our code.
def merge(other)
fail "#{name} is not a class; cannot add code to it" unless type == :hostclass
fail "#{other.name} is not a class; cannot add code from it" unless other.type == :hostclass
fail "Cannot have code outside of a class/node/define because 'freeze_main' is enabled" if name == "" and Puppet.settings[:freeze_main]
if parent and other.parent and parent != other.parent
fail "Cannot merge classes with different parent classes (#{name} => #{parent} vs. #{other.name} => #{other.parent})"
end
# We know they're either equal or only one is set, so keep whichever parent is specified.
self.parent ||= other.parent
if other.doc
self.doc ||= ""
self.doc += other.doc
end
# This might just be an empty, stub class.
return unless other.code
unless self.code
self.code = other.code
return
end
- self.code = self.code.sequence_with(other.code)
+ self.code = Puppet::Parser::ParserFactory.code_merger.concatenate([self, other])
+# self.code = self.code.sequence_with(other.code)
end
# Make an instance of the resource type, and place it in the catalog
# if it isn't in the catalog already. This is only possible for
# classes and nodes. No parameters are be supplied--if this is a
# parameterized class, then all parameters take on their default
# values.
def ensure_in_catalog(scope, parameters=nil)
type == :definition and raise ArgumentError, "Cannot create resources for defined resource types"
resource_type = type == :hostclass ? :class : :node
# Do nothing if the resource already exists; this makes sure we don't
# get multiple copies of the class resource, which helps provide the
# singleton nature of classes.
# we should not do this for classes with parameters
# if parameters are passed, we should still try to create the resource
# even if it exists so that we can fail
# this prevents us from being able to combine param classes with include
if resource = scope.catalog.resource(resource_type, name) and !parameters
return resource
end
resource = Puppet::Parser::Resource.new(resource_type, name, :scope => scope, :source => self)
assign_parameter_values(parameters, resource)
instantiate_resource(scope, resource)
scope.compiler.add_resource(scope, resource)
resource
end
def instantiate_resource(scope, resource)
# Make sure our parent class has been evaluated, if we have one.
if parent && !scope.catalog.resource(resource.type, parent)
parent_type(scope).ensure_in_catalog(scope)
end
if ['Class', 'Node'].include? resource.type
scope.catalog.tag(*resource.tags)
end
end
def name
return @name unless @name.is_a?(Regexp)
@name.source.downcase.gsub(/[^-\w:.]/,'').sub(/^\.+/,'')
end
def name_is_regex?
@name.is_a?(Regexp)
end
def assign_parameter_values(parameters, resource)
return unless parameters
# It'd be nice to assign default parameter values here,
# but we can't because they often rely on local variables
# created during set_resource_parameters.
parameters.each do |name, value|
resource.set_parameter name, value
end
end
# MQR TODO:
#
# The change(s) introduced by the fix for #4270 are mostly silly & should be
# removed, though we didn't realize it at the time. If it can be established/
# ensured that nodes never call parent_type and that resource_types are always
# (as they should be) members of exactly one resource_type_collection the
# following method could / should be replaced with:
#
# def parent_type
# @parent_type ||= parent && (
# resource_type_collection.find_or_load([name],parent,type.to_sym) ||
# fail Puppet::ParseError, "Could not find parent resource type '#{parent}' of type #{type} in #{resource_type_collection.environment}"
# )
# end
#
# ...and then the rest of the changes around passing in scope reverted.
#
def parent_type(scope = nil)
return nil unless parent
unless @parent_type
raise "Must pass scope to parent_type when called first time" unless scope
unless @parent_type = scope.environment.known_resource_types.send("find_#{type}", [name], parent)
fail Puppet::ParseError, "Could not find parent resource type '#{parent}' of type #{type} in #{scope.environment}"
end
end
@parent_type
end
# Set any arguments passed by the resource as variables in the scope.
def set_resource_parameters(resource, scope)
set = {}
resource.to_hash.each do |param, value|
param = param.to_sym
fail Puppet::ParseError, "#{resource.ref} does not accept attribute #{param}" unless valid_parameter?(param)
exceptwrap { scope[param.to_s] = value }
set[param] = true
end
if @type == :hostclass
scope["title"] = resource.title.to_s.downcase unless set.include? :title
scope["name"] = resource.name.to_s.downcase unless set.include? :name
else
scope["title"] = resource.title unless set.include? :title
scope["name"] = resource.name unless set.include? :name
end
scope["module_name"] = module_name if module_name and ! set.include? :module_name
if caller_name = scope.parent_module_name and ! set.include?(:caller_module_name)
scope["caller_module_name"] = caller_name
end
scope.class_set(self.name,scope) if hostclass? or node?
# Evaluate the default parameters, now that all other variables are set
default_params = resource.set_default_parameters(scope)
default_params.each { |param| scope[param] = resource[param] }
# This has to come after the above parameters so that default values
# can use their values
resource.validate_complete
end
# Check whether a given argument is valid.
def valid_parameter?(param)
param = param.to_s
return true if param == "name"
return true if Puppet::Type.metaparam?(param)
return false unless defined?(@arguments)
return(arguments.include?(param) ? true : false)
end
def set_arguments(arguments)
@arguments = {}
return if arguments.nil?
arguments.each do |arg, default|
arg = arg.to_s
warn_if_metaparam(arg, default)
@arguments[arg] = default
end
end
private
def convert_from_ast(name)
value = name.value
if value.is_a?(Puppet::Parser::AST::Regex)
name = value.value
else
name = value
end
end
def evaluate_parent_type(resource)
return unless klass = parent_type(resource.scope) and parent_resource = resource.scope.compiler.catalog.resource(:class, klass.name) || resource.scope.compiler.catalog.resource(:node, klass.name)
parent_resource.evaluate unless parent_resource.evaluated?
parent_scope(resource.scope, klass)
end
def evaluate_ruby_code(resource, scope)
Puppet::DSL::ResourceAPI.new(resource, scope, ruby_code).evaluate
end
# Split an fq name into a namespace and name
def namesplit(fullname)
ary = fullname.split("::")
n = ary.pop || ""
ns = ary.join("::")
return ns, n
end
def parent_scope(scope, klass)
scope.class_scope(klass) || raise(Puppet::DevError, "Could not find scope for #{klass.name}")
end
def set_name_and_namespace(name)
if name.is_a?(Regexp)
@name = name
@namespace = ""
else
@name = name.to_s.downcase
# Note we're doing something somewhat weird here -- we're setting
# the class's namespace to its fully qualified name. This means
# anything inside that class starts looking in that namespace first.
@namespace, ignored_shortname = @type == :hostclass ? [@name, ''] : namesplit(@name)
end
end
def warn_if_metaparam(param, default)
return unless Puppet::Type.metaparamclass(param)
if default
warnonce "#{param} is a metaparam; this value will inherit to all contained resources in the #{self.name} definition"
else
raise Puppet::ParseError, "#{param} is a metaparameter; please choose another parameter name in the #{self.name} definition"
end
end
end
-
diff --git a/lib/puppet/resource/type_collection.rb b/lib/puppet/resource/type_collection.rb
index 4ca8a9689..21a953eee 100644
--- a/lib/puppet/resource/type_collection.rb
+++ b/lib/puppet/resource/type_collection.rb
@@ -1,230 +1,230 @@
require 'puppet/parser/type_loader'
require 'puppet/util/file_watcher'
require 'puppet/util/warnings'
class Puppet::Resource::TypeCollection
attr_reader :environment
attr_accessor :parse_failed
include Puppet::Util::Warnings
def clear
@hostclasses.clear
@definitions.clear
@nodes.clear
@watched_files.clear
@notfound.clear
end
def initialize(env)
- @environment = env.is_a?(String) ? Puppet::Node::Environment.new(env) : env
+ @environment = env
@hostclasses = {}
@definitions = {}
@nodes = {}
@notfound = {}
# So we can keep a list and match the first-defined regex
@node_list = []
@watched_files = Puppet::Util::FileWatcher.new
end
def import_ast(ast, modname)
ast.instantiate(modname).each do |instance|
add(instance)
end
end
def inspect
"TypeCollection" + { :hostclasses => @hostclasses.keys, :definitions => @definitions.keys, :nodes => @nodes.keys }.inspect
end
def <<(thing)
add(thing)
self
end
def add(instance)
if instance.type == :hostclass and other = @hostclasses[instance.name] and other.type == :hostclass
other.merge(instance)
return other
end
method = "add_#{instance.type}"
send(method, instance)
instance.resource_type_collection = self
instance
end
def add_hostclass(instance)
dupe_check(instance, @hostclasses) { |dupe| "Class '#{instance.name}' is already defined#{dupe.error_context}; cannot redefine" }
dupe_check(instance, @definitions) { |dupe| "Definition '#{instance.name}' is already defined#{dupe.error_context}; cannot be redefined as a class" }
@hostclasses[instance.name] = instance
instance
end
def hostclass(name)
@hostclasses[munge_name(name)]
end
def add_node(instance)
dupe_check(instance, @nodes) { |dupe| "Node '#{instance.name}' is already defined#{dupe.error_context}; cannot redefine" }
@node_list << instance
@nodes[instance.name] = instance
instance
end
def loader
@loader ||= Puppet::Parser::TypeLoader.new(environment)
end
def node(name)
name = munge_name(name)
if node = @nodes[name]
return node
end
@node_list.each do |node|
next unless node.name_is_regex?
return node if node.match(name)
end
nil
end
def node_exists?(name)
@nodes[munge_name(name)]
end
def nodes?
@nodes.length > 0
end
def add_definition(instance)
dupe_check(instance, @hostclasses) { |dupe| "'#{instance.name}' is already defined#{dupe.error_context} as a class; cannot redefine as a definition" }
dupe_check(instance, @definitions) { |dupe| "Definition '#{instance.name}' is already defined#{dupe.error_context}; cannot be redefined" }
@definitions[instance.name] = instance
end
def definition(name)
@definitions[munge_name(name)]
end
def find_node(namespaces, name)
@nodes[munge_name(name)]
end
def find_hostclass(namespaces, name, options = {})
find_or_load(namespaces, name, :hostclass, options)
end
def find_definition(namespaces, name)
find_or_load(namespaces, name, :definition)
end
[:hostclasses, :nodes, :definitions].each do |m|
define_method(m) do
instance_variable_get("@#{m}").dup
end
end
def require_reparse?
@parse_failed || stale?
end
def stale?
@watched_files.changed?
end
def version
if !defined?(@version)
if environment[:config_version] == ""
@version = Time.now.to_i
else
@version = Puppet::Util::Execution.execute([environment[:config_version]]).strip
end
end
@version
rescue Puppet::ExecutionFailure => e
- raise Puppet::ParseError, "Execution of config_version command `#{environment[:config_version]}` failed: #{e.message}"
+ raise Puppet::ParseError, "Execution of config_version command `#{environment[:config_version]}` failed: #{e.message}", e.backtrace
end
def watch_file(filename)
@watched_files.watch(filename)
end
def watching_file?(filename)
@watched_files.watching?(filename)
end
private
# Return a list of all possible fully-qualified names that might be
# meant by the given name, in the context of namespaces.
def resolve_namespaces(namespaces, name)
name = name.downcase
if name =~ /^::/
# name is explicitly fully qualified, so just return it, sans
# initial "::".
return [name.sub(/^::/, '')]
end
if name == ""
# The name "" has special meaning--it always refers to a "main"
# hostclass which contains all toplevel resources.
return [""]
end
namespaces = [namespaces] unless namespaces.is_a?(Array)
namespaces = namespaces.collect { |ns| ns.downcase }
result = []
namespaces.each do |namespace|
ary = namespace.split("::")
# Search each namespace nesting in innermost-to-outermost order.
while ary.length > 0
result << "#{ary.join("::")}::#{name}"
ary.pop
end
# Finally, search the toplevel namespace.
result << name
end
return result.uniq
end
# Resolve namespaces and find the given object. Autoload it if
# necessary.
def find_or_load(namespaces, name, type, options = {})
searchspace = options[:assume_fqname] ? [name].flatten : resolve_namespaces(namespaces, name)
searchspace.each do |fqname|
result = send(type, fqname)
unless result
if @notfound[fqname] and Puppet[:ignoremissingtypes]
# do not try to autoload if we already tried and it wasn't conclusive
# as this is a time consuming operation. Warn the user.
debug_once "Not attempting to load #{type} #{fqname} as this object was missing during a prior compilation"
else
result = loader.try_load_fqname(type, fqname)
@notfound[fqname] = result.nil?
end
end
return result if result
end
return nil
end
def munge_name(name)
name.to_s.downcase
end
def dupe_check(instance, hash)
return unless dupe = hash[instance.name]
message = yield dupe
instance.fail Puppet::ParseError, message
end
end
diff --git a/lib/puppet/run.rb b/lib/puppet/run.rb
index 1d89304be..b94fb7756 100644
--- a/lib/puppet/run.rb
+++ b/lib/puppet/run.rb
@@ -1,108 +1,109 @@
require 'puppet/agent'
require 'puppet/configurer'
require 'puppet/indirector'
# A basic class for running the agent. Used by
# `puppet kick` to kick off agents remotely.
class Puppet::Run
extend Puppet::Indirector
indirects :run, :terminus_class => :local
attr_reader :status, :background, :options
def agent
# Forking disabled for "puppet kick" runs
Puppet::Agent.new(Puppet::Configurer, false)
end
def background?
background
end
def initialize(options = {})
if options.include?(:background)
@background = options[:background]
options.delete(:background)
end
valid_options = [:tags, :ignoreschedules, :pluginsync]
options.each do |key, value|
raise ArgumentError, "Run does not accept #{key}" unless valid_options.include?(key)
end
@options = options
end
def initialize_from_hash(hash)
@options = {}
hash['options'].each do |key, value|
@options[key.to_sym] = value
end
@background = hash['background']
@status = hash['status']
end
def log_run
msg = ""
msg += "triggered run" % if options[:tags]
msg += " with tags #{options[:tags].inspect}"
end
msg += " ignoring schedules" if options[:ignoreschedules]
Puppet.notice msg
end
def run
if agent.running?
@status = "running"
return self
end
log_run
if background?
Thread.new { agent.run(options) }
else
agent.run(options)
end
@status = "success"
self
end
def self.from_hash(hash)
obj = allocate
obj.initialize_from_hash(hash)
obj
end
- def self.from_pson(hash)
- if hash['options']
- return from_hash(hash)
+ def self.from_data_hash(data)
+ if data['options']
+ return from_hash(data)
end
options = { :pluginsync => Puppet[:pluginsync] }
- hash.each do |key, value|
+ data.each do |key, value|
options[key.to_sym] = value
end
new(options)
end
+ def self.from_pson(hash)
+ Puppet.deprecation_warning("from_pson is being removed in favour of from_data_hash.")
+ self.from_data_hash(hash)
+ end
+
def to_data_hash
{
:options => @options,
:background => @background,
:status => @status
}
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 ca017ae1e..1188a7b85 100644
--- a/lib/puppet/settings.rb
+++ b/lib/puppet/settings.rb
@@ -1,1161 +1,1312 @@
require 'puppet'
require 'getoptlong'
require 'puppet/util/watched_file'
require 'puppet/util/command_line/puppet_option_parser'
# The class for handling configuration files.
class Puppet::Settings
include Enumerable
require 'puppet/settings/errors'
require 'puppet/settings/base_setting'
require 'puppet/settings/string_setting'
require 'puppet/settings/enum_setting'
require 'puppet/settings/file_setting'
require 'puppet/settings/directory_setting'
require 'puppet/settings/path_setting'
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'
# local reference for convenience
PuppetOptionParser = Puppet::Util::CommandLine::PuppetOptionParser
attr_accessor :files
attr_reader :timer
# These are the settings that every app is required to specify; there are reasonable defaults defined in application.rb.
REQUIRED_APP_SETTINGS = [:logdir, :confdir, :vardir]
# This method is intended for puppet internal use only; it is a convenience method that
# returns reasonable application default settings values for a given run_mode.
def self.app_defaults_for_run_mode(run_mode)
{
:name => run_mode.to_s,
:run_mode => run_mode.name,
:confdir => run_mode.conf_dir,
:vardir => run_mode.var_dir,
:rundir => run_mode.run_dir,
:logdir => run_mode.log_dir,
}
end
def self.default_certname()
hostname = hostname_fact
domain = domain_fact
if domain and domain != ""
fqdn = [hostname, domain].join(".")
else
fqdn = hostname
end
fqdn.to_s.gsub(/\.$/, '')
end
def self.hostname_fact()
Facter["hostname"].value
end
def self.domain_fact()
Facter["domain"].value
end
def self.default_config_file_name
"puppet.conf"
end
# Create a new collection of config settings.
def initialize
@config = {}
@shortnames = {}
@created = []
- @searchpath = nil
# Keep track of set values.
- @values = Hash.new { |hash, key| hash[key] = {} }
-
- # Hold parsed metadata until run_mode is known
- @metas = {}
+ @value_sets = {
+ :cli => Values.new(:cli, @config),
+ :memory => Values.new(:memory, @config),
+ :application_defaults => Values.new(:application_defaults, @config),
+ :overridden_defaults => Values.new(:overridden_defaults, @config),
+ }
+ @configuration_file = nil
# And keep a per-environment cache
@cache = Hash.new { |hash, key| hash[key] = {} }
# The list of sections we've used.
@used = []
@hooks_to_call_on_application_initialization = []
@translate = Puppet::Settings::ValueTranslator.new
@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
+ # @param param [Symbol] the name of the setting
+ # @return [Object] the value of the setting
+ # @api private
def [](param)
value(param)
end
# Set a config value. This doesn't set the defaults, it sets the value itself.
+ # @param param [Symbol] the name of the setting
+ # @param value [Object] the new value of the setting
+ # @api private
def []=(param, value)
- set_value(param, value, :memory)
+ @value_sets[:memory].set(param, value)
+ unsafe_flush_cache
+ end
+
+ # Create a new default value for the given setting. The default overrides are
+ # higher precedence than the defaults given in defaults.rb, but lower
+ # precedence than any other values for the setting. This allows one setting
+ # `a` to change the default of setting `b`, but still allow a user to provide
+ # a value for setting `b`.
+ #
+ # @param param [Symbol] the name of the setting
+ # @param value [Object] the new default value for the setting
+ # @api private
+ def override_default(param, value)
+ @value_sets[:overridden_defaults].set(param, value)
+ unsafe_flush_cache
end
# Generate the list of valid arguments, in a format that GetoptLong can
# understand, and add them to the passed option list.
def addargs(options)
- # Add all of the config parameters as valid options.
+ # Add all of the settings as valid options.
self.each { |name, setting|
setting.getopt_args.each { |args| options << args }
}
options
end
# Generate the list of valid arguments, in a format that OptionParser can
# understand, and add them to the passed option list.
def optparse_addargs(options)
- # Add all of the config parameters as valid options.
+ # Add all of the settings as valid options.
self.each { |name, setting|
options << setting.optparse_args
}
options
end
- # Is our parameter a boolean parameter?
+ # Is our setting a boolean setting?
def boolean?(param)
param = param.to_sym
@config.include?(param) and @config[param].kind_of?(BooleanSetting)
end
# Remove all set values, potentially skipping cli values.
def clear
unsafe_clear
end
# Remove all set values, potentially skipping cli values.
def unsafe_clear(clear_cli = true, clear_application_defaults = false)
- @values.each do |name, values|
- next if ((name == :application_defaults) and !clear_application_defaults)
- next if ((name == :cli) and !clear_cli)
- @values.delete(name)
+ if clear_application_defaults
+ @value_sets[:application_defaults] = Values.new(:application_defaults, @config)
+ @app_defaults_initialized = false
end
- # Only clear the 'used' values if we were explicitly asked to clear out
- # :cli values; otherwise, it may be just a config file reparse,
- # and we want to retain this cli values.
- @used = [] if clear_cli
+ if clear_cli
+ @value_sets[:cli] = Values.new(:cli, @config)
+
+ # Only clear the 'used' values if we were explicitly asked to clear out
+ # :cli values; otherwise, it may be just a config file reparse,
+ # and we want to retain this cli values.
+ @used = []
+ end
- @app_defaults_initialized = false if clear_application_defaults
+ @value_sets[:memory] = Values.new(:memory, @config)
+ @value_sets[:overridden_defaults] = Values.new(:overridden_defaults, @config)
@cache.clear
end
private :unsafe_clear
# Clear @cache, @used and the Environment.
#
# Whenever an object is returned by Settings, a copy is stored in @cache.
# As long as Setting attributes that determine the content of returned
# objects remain unchanged, Settings can keep returning objects from @cache
# without re-fetching or re-generating them.
#
# Whenever a Settings attribute changes, such as @values or @preferred_run_mode,
# this method must be called to clear out the caches so that updated
# objects will be returned.
def flush_cache
unsafe_flush_cache
end
def unsafe_flush_cache
clearused
# Clear the list of environments, because they cache, at least, the module path.
# We *could* preferentially just clear them if the modulepath is changed,
# but we don't really know if, say, the vardir is changed and the modulepath
# is defined relative to it. We need the defined?(stuff) because of loading
# order issues.
Puppet::Node::Environment.clear if defined?(Puppet::Node) and defined?(Puppet::Node::Environment)
end
private :unsafe_flush_cache
def clearused
@cache.clear
@used = []
end
def global_defaults_initialized?()
@global_defaults_initialized
end
def initialize_global_settings(args = [])
raise Puppet::DevError, "Attempting to initialize global default settings more than once!" if global_defaults_initialized?
# The first two phases of the lifecycle of a puppet application are:
# 1) Parse the command line options and handle any of them that are
# registered, defined "global" puppet settings (mostly from defaults.rb).
# 2) Parse the puppet config file(s).
parse_global_options(args)
parse_config_files
@global_defaults_initialized = true
end
# This method is called during application bootstrapping. It is responsible for parsing all of the
# command line options and initializing the settings accordingly.
#
# It will ignore options that are not defined in the global puppet settings list, because they may
# be valid options for the specific application that we are about to launch... however, at this point
# in the bootstrapping lifecycle, we don't yet know what that application is.
def parse_global_options(args)
# Create an option parser
option_parser = PuppetOptionParser.new
option_parser.ignore_invalid_options = true
# Add all global options to it.
self.optparse_addargs([]).each do |option|
option_parser.on(*option) do |arg|
opt, val = Puppet::Settings.clean_opt(option[0], arg)
handlearg(opt, val)
end
end
option_parser.on('--run_mode',
"The effective 'run mode' of the application: master, agent, or user.",
:REQUIRED) do |arg|
Puppet.settings.preferred_run_mode = arg
end
option_parser.parse(args)
# remove run_mode options from the arguments so that later parses don't think
# it is an unknown option.
while option_index = args.index('--run_mode') do
args.delete_at option_index
args.delete_at option_index
end
args.reject! { |arg| arg.start_with? '--run_mode=' }
end
private :parse_global_options
# A utility method (public, is used by application.rb and perhaps elsewhere) that munges a command-line
# option string into the format that Puppet.settings expects. (This mostly has to deal with handling the
# "no-" prefix on flag/boolean options).
#
# @param [String] opt the command line option that we are munging
# @param [String, TrueClass, FalseClass] val the value for the setting (as determined by the OptionParser)
def self.clean_opt(opt, val)
# rewrite --[no-]option to --no-option if that's what was given
if opt =~ /\[no-\]/ and !val
opt = opt.gsub(/\[no-\]/,'no-')
end
# otherwise remove the [no-] prefix to not confuse everybody
opt = opt.gsub(/\[no-\]/, '')
[opt, val]
end
def app_defaults_initialized?
@app_defaults_initialized
end
def initialize_app_defaults(app_defaults)
REQUIRED_APP_SETTINGS.each do |key|
raise SettingsError, "missing required app default setting '#{key}'" unless app_defaults.has_key?(key)
end
app_defaults.each do |key, value|
if key == :run_mode
self.preferred_run_mode = value
else
- set_value(key, value, :application_defaults)
+ @value_sets[:application_defaults].set(key, value)
+ unsafe_flush_cache
end
end
apply_metadata
call_hooks_deferred_to_application_initialization
@app_defaults_initialized = true
end
def call_hooks_deferred_to_application_initialization(options = {})
@hooks_to_call_on_application_initialization.each do |setting|
begin
setting.handle(self.value(setting.name))
rescue InterpolationError => err
- raise err unless options[:ignore_interpolation_dependency_errors]
+ raise InterpolationError, err, err.backtrace unless options[:ignore_interpolation_dependency_errors]
#swallow. We're not concerned if we can't call hooks because dependencies don't exist yet
#we'll get another chance after application defaults are initialized
end
end
end
private :call_hooks_deferred_to_application_initialization
- # Do variable interpolation on the value.
- def convert(value, environment = nil)
- return nil if value.nil?
- return value unless value.is_a? String
- newval = value.gsub(/\$(\w+)|\$\{(\w+)\}/) do |value|
- varname = $2 || $1
- if varname == "environment" and environment
- environment
- elsif varname == "run_mode"
- preferred_run_mode
- elsif pval = self.value(varname, environment)
- pval
- else
- raise InterpolationError, "Could not find value for #{value}"
- end
- end
-
- newval
- end
-
# Return a value's description.
def description(name)
if obj = @config[name.to_sym]
obj.desc
else
nil
end
end
def each
@config.each { |name, object|
yield name, object
}
end
# Iterate over each section name.
def eachsection
yielded = []
@config.each do |name, object|
section = object.section
unless yielded.include? section
yield section
yielded << section
end
end
end
# Return an object by name.
def setting(param)
param = param.to_sym
@config[param]
end
# Handle a command-line argument.
def handlearg(opt, value = nil)
@cache.clear
if value.is_a?(FalseClass)
value = "false"
elsif value.is_a?(TrueClass)
value = "true"
end
value &&= @translate[value]
str = opt.sub(/^--/,'')
bool = true
newstr = str.sub(/^no-/, '')
if newstr != str
str = newstr
bool = false
end
str = str.intern
if @config[str].is_a?(Puppet::Settings::BooleanSetting)
if value == "" or value.nil?
value = bool
end
end
- set_value(str, value, :cli)
+ @value_sets[:cli].set(str, value)
+ unsafe_flush_cache
end
def include?(name)
name = name.intern if name.is_a? String
@config.include?(name)
end
# check to see if a short name is already defined
def shortinclude?(short)
short = short.intern if name.is_a? String
@shortnames.include?(short)
end
# Prints the contents of a config file with the available config settings, or it
# prints a single value of a config setting.
def print_config_options
env = value(:environment)
val = value(:configprint)
if val == "all"
hash = {}
each do |name, obj|
val = value(name,env)
val = val.inspect if val == ""
hash[name] = val
end
hash.sort { |a,b| a[0].to_s <=> b[0].to_s }.each do |name, val|
puts "#{name} = #{val}"
end
else
val.split(/\s*,\s*/).sort.each do |v|
if include?(v)
#if there is only one value, just print it for back compatibility
if v == val
puts value(val,env)
break
end
puts "#{v} = #{value(v,env)}"
else
- puts "invalid parameter: #{v}"
+ puts "invalid setting: #{v}"
return false
end
end
end
true
end
def generate_config
puts to_config
true
end
def generate_manifest
puts to_manifest
true
end
def print_configs
return print_config_options if value(:configprint) != ""
return generate_config if value(:genconfig)
generate_manifest if value(:genmanifest)
end
def print_configs?
(value(:configprint) != "" || value(:genconfig) || value(:genmanifest)) && true
end
# Return a given object's file metadata.
def metadata(param)
if obj = @config[param.to_sym] and obj.is_a?(FileSetting)
- return [:owner, :group, :mode].inject({}) do |meta, p|
- if v = obj.send(p)
- meta[p] = v
- end
- meta
- end
+ {
+ :owner => obj.owner,
+ :group => obj.group,
+ :mode => obj.mode
+ }.delete_if { |key, value| value.nil? }
else
nil
end
end
# Make a directory with the appropriate user, group, and mode
def mkdir(default)
obj = get_config_file_default(default)
Puppet::Util::SUIDManager.asuser(obj.owner, obj.group) do
mode = obj.mode || 0750
Dir.mkdir(obj.value, mode)
end
end
# The currently configured run mode that is preferred for constructing the application configuration.
def preferred_run_mode
@preferred_run_mode_name || :user
end
# PRIVATE! This only exists because we need a hook to validate the run mode when it's being set, and
# it should never, ever, ever, ever be called from outside of this file.
# This method is also called when --run_mode MODE is used on the command line to set the default
#
# @param mode [String|Symbol] the name of the mode to have in effect
# @api private
def preferred_run_mode=(mode)
mode = mode.to_s.downcase.intern
raise ValidationError, "Invalid run mode '#{mode}'" unless [:master, :agent, :user].include?(mode)
@preferred_run_mode_name = mode
# Changing the run mode has far-reaching consequences. Flush any cached
# settings so they will be re-generated.
flush_cache
mode
end
- # Return all of the parameters associated with a given section.
+ # Return all of the settings associated with a given section.
def params(section = nil)
if section
section = section.intern if section.is_a? String
@config.find_all { |name, obj|
obj.section == section
}.collect { |name, obj|
name
}
else
@config.keys
end
end
+ def parse_config(text, file = "text")
+ begin
+ data = @config_file_parser.parse_file(file, text)
+ rescue => detail
+ Puppet.log_exception(detail, "Could not parse #{file}: #{detail}")
+ return
+ end
+
+ # If we get here and don't have any data, we just return and don't muck with the current state of the world.
+ return if data.nil?
+
+ # If we get here then we have some data, so we need to clear out any previous settings that may have come from
+ # config files.
+ unsafe_clear(false, false)
+
+ # And now we can repopulate with the values from our last parsing of the config files.
+ @configuration_file = data
+
+ # Determine our environment, if we have one.
+ if @config[:environment]
+ env = self.value(:environment).to_sym
+ else
+ env = "none"
+ end
+
+ # Call any hooks we should be calling.
+ @config.values.select(&:has_hook?).each do |setting|
+ value_sets_for(env, self.preferred_run_mode).each do |source|
+ if source.include?(setting.name)
+ # We still have to use value to retrieve the value, since
+ # we want the fully interpolated value, not $vardir/lib or whatever.
+ # This results in extra work, but so few of the settings
+ # will have associated hooks that it ends up being less work this
+ # way overall.
+ 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
+ end
+
+ call_hooks_deferred_to_application_initialization :ignore_interpolation_dependency_errors => true
+ apply_metadata
+ end
+
# Parse the configuration file. Just provides thread safety.
def parse_config_files
- unsafe_parse(which_configuration_file)
+ file = which_configuration_file
+ if Puppet::FileSystem.exist?(file)
+ begin
+ text = read_file(file)
+ rescue => detail
+ Puppet.log_exception(detail, "Could not load #{file}: #{detail}")
+ return
+ end
+ else
+ return
+ end
- call_hooks_deferred_to_application_initialization :ignore_interpolation_dependency_errors => true
+ parse_config(text, file)
end
private :parse_config_files
def main_config_file
if explicit_config_file?
return self[:config]
else
return File.join(Puppet::Util::RunMode[:master].conf_dir, config_file_name)
end
end
private :main_config_file
def user_config_file
return File.join(Puppet::Util::RunMode[:user].conf_dir, config_file_name)
end
private :user_config_file
# This method is here to get around some life-cycle issues. We need to be
# able to determine the config file name before the settings / defaults are
# fully loaded. However, we also need to respect any overrides of this value
# that the user may have specified on the command line.
#
# The easiest way to do this is to attempt to read the setting, and if we
# catch an error (meaning that it hasn't been set yet), we'll fall back to
# the default value.
def config_file_name
begin
return self[:config_file_name] if self[:config_file_name]
rescue SettingsError
# This just means that the setting wasn't explicitly set on the command line, so we will ignore it and
# fall through to the default name.
end
return self.class.default_config_file_name
end
private :config_file_name
- # Unsafely parse the file -- this isn't thread-safe and causes plenty of problems if used directly.
- def unsafe_parse(file)
- # build up a single data structure that contains the values from all of the parsed files.
- data = {}
- if Puppet::FileSystem::File.exist?(file)
- begin
- file_data = parse_file(file)
-
- # This is a little kludgy; basically we are merging a hash of hashes. We can't use "merge" at the
- # outermost level or we risking losing data from the hash we're merging into.
- file_data.keys.each do |key|
- if data.has_key?(key)
- data[key].merge!(file_data[key])
- else
- data[key] = file_data[key]
- end
- end
- rescue => detail
- Puppet.log_exception(detail, "Could not parse #{file}: #{detail}")
- return
- end
- end
-
- # If we get here and don't have any data, we just return and don't muck with the current state of the world.
- return if data.empty?
-
- # If we get here then we have some data, so we need to clear out any previous settings that may have come from
- # config files.
- unsafe_clear(false, false)
-
- # And now we can repopulate with the values from our last parsing of the config files.
- data.each do |area, values|
- @metas[area] = values.delete(:_meta)
- values.each do |key,value|
- set_value(key, value, area, :dont_trigger_handles => true, :ignore_bad_settings => true )
- end
- end
-
- # Determine our environment, if we have one.
- if @config[:environment]
- env = self.value(:environment).to_sym
- else
- env = "none"
- end
-
- # Call any hooks we should be calling.
- settings_with_hooks.each do |setting|
- each_source(env) do |source|
- if @values[source][setting.name]
- # We still have to use value to retrieve the value, since
- # we want the fully interpolated value, not $vardir/lib or whatever.
- # This results in extra work, but so few of the settings
- # will have associated hooks that it ends up being less work this
- # way overall.
- 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
- end
-
- # Take a best guess at metadata based on uninitialized run_mode
- apply_metadata
- end
- private :unsafe_parse
-
def apply_metadata
# We have to do it in the reverse of the search path,
# because multiple sections could set the same value
# and I'm too lazy to only set the metadata once.
- searchpath.reverse.each do |source|
- source = preferred_run_mode if source == :run_mode
- source = @name if (@name && source == :name)
- if meta = @metas[source]
- set_metadata(meta)
+ if @configuration_file
+ searchpath.reverse.each do |source|
+ source = preferred_run_mode if source == :run_mode
+ if section = @configuration_file.sections[source]
+ apply_metadata_from_section(section)
+ end
end
end
end
private :apply_metadata
+ def apply_metadata_from_section(section)
+ section.settings.each do |setting|
+ if setting.has_metadata? && type = @config[setting.name]
+ type.set_meta(setting.meta)
+ end
+ end
+ end
+
SETTING_TYPES = {
:string => StringSetting,
:file => FileSetting,
:directory => DirectorySetting,
:path => PathSetting,
:boolean => BooleanSetting,
: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
# what kind of setting we're creating, but the value itself might be either
# a default or a value, so we can't actually assign it.
#
# See #define_settings for documentation on the legal values for the ":type" option.
def newsetting(hash)
klass = nil
hash[:section] = hash[:section].to_sym if hash[:section]
if type = hash[:type]
unless klass = SETTING_TYPES[type]
raise ArgumentError, "Invalid setting type '#{type}'"
end
hash.delete(:type)
else
# The only implicit typing we still do for settings is to fall back to "String" type if they didn't explicitly
# specify a type. Personally I'd like to get rid of this too, and make the "type" option mandatory... but
# there was a little resistance to taking things quite that far for now. --cprice 2012-03-19
klass = StringSetting
end
hash[:settings] = self
setting = klass.new(hash)
setting
end
# This has to be private, because it doesn't add the settings to @config
private :newsetting
# Iterate across all of the objects in a given section.
def persection(section)
section = section.to_sym
self.each { |name, obj|
if obj.section == section
yield obj
end
}
end
# Reparse our config file, if necessary.
def reparse_config_files
if files
if filename = any_files_changed?
Puppet.notice "Config file #{filename} changed; triggering re-parse of all config files."
parse_config_files
reuse
end
end
end
def files
return @files if @files
@files = []
[main_config_file, user_config_file].each do |path|
- if Puppet::FileSystem::File.exist?(path)
+ if Puppet::FileSystem.exist?(path)
@files << Puppet::Util::WatchedFile.new(path)
end
end
@files
end
private :files
# Checks to see if any of the config files have been modified
# @return the filename of the first file that is found to have changed, or
# nil if no files have changed
def any_files_changed?
files.each do |file|
return file.to_str if file.changed?
end
nil
end
private :any_files_changed?
def reuse
return unless defined?(@used)
new = @used
@used = []
self.use(*new)
end
# The order in which to search for values.
def searchpath(environment = nil)
- if environment
- [:cli, :memory, environment, :run_mode, :main, :application_defaults]
- else
- [:cli, :memory, :run_mode, :main, :application_defaults]
- end
+ [:memory, :cli, environment, :run_mode, :main, :application_defaults, :overridden_defaults].compact
end
# Get a list of objects per section
def sectionlist
sectionlist = []
self.each { |name, obj|
section = obj.section || "puppet"
sections[section] ||= []
sectionlist << section unless sectionlist.include?(section)
sections[section] << obj
}
return sectionlist, sections
end
def service_user_available?
return @service_user_available if defined?(@service_user_available)
if self[:user]
user = Puppet::Type.type(:user).new :name => self[:user], :audit => :ensure
@service_user_available = user.exists?
else
@service_user_available = false
end
end
def service_group_available?
return @service_group_available if defined?(@service_group_available)
if self[:group]
group = Puppet::Type.type(:group).new :name => self[:group], :audit => :ensure
@service_group_available = group.exists?
else
@service_group_available = false
end
end
# Allow later inspection to determine if the setting was set on the
# command line, or through some other code path. Used for the
# `dns_alt_names` option during cert generate. --daniel 2011-10-18
def set_by_cli?(param)
param = param.to_sym
- !@values[:cli][param].nil?
+ !@value_sets[:cli].lookup(param).nil?
end
def set_value(param, value, type, options = {})
- param = param.to_sym
-
- if !(setting = @config[param])
- if options[:ignore_bad_settings]
- return
- else
- raise ArgumentError,
- "Attempt to assign a value to unknown configuration parameter #{param.inspect}"
- end
+ Puppet.deprecation_warning("Puppet.settings.set_value is deprecated. Use Puppet[]= instead.")
+ if @value_sets[type]
+ @value_sets[type].set(param, value)
+ unsafe_flush_cache
end
-
- setting.handle(value) if setting.has_hook? and not options[:dont_trigger_handles]
-
- @values[type][param] = value
- unsafe_flush_cache
-
- value
end
-
-
-
# Deprecated; use #define_settings instead
def setdefaults(section, defs)
Puppet.deprecation_warning("'setdefaults' is deprecated and will be removed; please call 'define_settings' instead")
define_settings(section, defs)
end
# Define a group of settings.
#
# @param [Symbol] section a symbol to use for grouping multiple settings together into a conceptual unit. This value
# (and the conceptual separation) is not used very often; the main place where it will have a potential impact
# is when code calls Settings#use method. See docs on that method for further details, but basically that method
# just attempts to do any preparation that may be necessary before code attempts to leverage the value of a particular
# setting. This has the most impact for file/directory settings, where #use will attempt to "ensure" those
# files / directories.
# @param [Hash[Hash]] defs the settings to be defined. This argument is a hash of hashes; each key should be a symbol,
# which is basically the name of the setting that you are defining. The value should be another hash that specifies
# the parameters for the particular setting. Legal values include:
# [:default] => required; this is a string value that will be used as a default value for a setting if no other
# value is specified (via cli, config file, etc.) This string may include "variables", demarcated with $ or ${},
# which will be interpolated with values of other settings.
# [:desc] => required; a description of the setting, used in documentation / help generation
# [:type] => not required, but highly encouraged! This specifies the data type that the setting represents. If
# you do not specify it, it will default to "string". Legal values include:
# :string - A generic string setting
# :boolean - A boolean setting; values are expected to be "true" or "false"
# :file - A (single) file path; puppet may attempt to create this file depending on how the settings are used. This type
# also supports additional options such as "mode", "owner", "group"
# :directory - A (single) directory path; puppet may attempt to create this file depending on how the settings are used. This type
# also supports additional options such as "mode", "owner", "group"
# :path - This is intended to be used for settings whose value can contain multiple directory paths, respresented
# as strings separated by the system path separator (e.g. system path, module path, etc.).
# [:mode] => an (optional) octal value to be used as the permissions/mode for :file and :directory settings
# [:owner] => optional owner username/uid for :file and :directory settings
# [:group] => optional group name/gid for :file and :directory settings
#
def define_settings(section, defs)
section = section.to_sym
call = []
defs.each do |name, hash|
raise ArgumentError, "setting definition for '#{name}' is not a hash!" unless hash.is_a? Hash
name = name.to_sym
hash[:name] = name
hash[:section] = section
- raise ArgumentError, "Parameter #{name} is already defined" if @config.include?(name)
+ raise ArgumentError, "Setting #{name} is already defined" if @config.include?(name)
tryconfig = newsetting(hash)
if short = tryconfig.short
if other = @shortnames[short]
- raise ArgumentError, "Parameter #{other.name} is already using short name '#{short}'"
+ raise ArgumentError, "Setting #{other.name} is already using short name '#{short}'"
end
@shortnames[short] = tryconfig
end
@config[name] = tryconfig
# Collect the settings that need to have their hooks called immediately.
# We have to collect them so that we can be sure we're fully initialized before
# the hook is called.
- if tryconfig.call_hook_on_define?
- call << tryconfig
- elsif tryconfig.call_hook_on_initialize?
- @hooks_to_call_on_application_initialization << tryconfig
+ if tryconfig.has_hook?
+ if tryconfig.call_hook_on_define?
+ call << tryconfig
+ elsif tryconfig.call_hook_on_initialize?
+ @hooks_to_call_on_application_initialization << tryconfig
+ end
end
end
- call.each { |setting| setting.handle(self.value(setting.name)) }
+ call.each do |setting|
+ setting.handle(self.value(setting.name))
+ end
end
# Convert the settings we manage into a catalog full of resources that model those settings.
def to_catalog(*sections)
sections = nil if sections.empty?
catalog = Puppet::Resource::Catalog.new("Settings")
@config.keys.find_all { |key| @config[key].is_a?(FileSetting) }.each do |key|
file = @config[key]
next unless (sections.nil? or sections.include?(file.section))
next unless resource = file.to_resource
next if catalog.resource(resource.ref)
Puppet.debug("Using settings: adding file resource '#{key}': '#{resource.inspect}'")
catalog.add_resource(resource)
end
add_user_resources(catalog, sections)
catalog
end
# Convert our list of config settings into a configuration file.
def to_config
str = %{The configuration file for #{Puppet.run_mode.name}. Note that this file
-is likely to have unused configuration parameters in it; any parameter that's
+is likely to have unused settings in it; any setting that's
valid anywhere in Puppet can be in any config file, even if it's not used.
Every section can specify three special parameters: owner, group, and mode.
These parameters affect the required permissions of any files specified after
their specification. Puppet will sometimes use these parameters to check its
own configured state, so they can be used to make Puppet a bit more self-managing.
The file format supports octothorpe-commented lines, but not partial-line comments.
Generated on #{Time.now}.
}.gsub(/^/, "# ")
# Add a section heading that matches our name.
str += "[#{preferred_run_mode}]\n"
eachsection do |section|
persection(section) do |obj|
str += obj.to_config + "\n" unless obj.name == :genconfig
end
end
return str
end
# Convert to a parseable manifest
def to_manifest
catalog = to_catalog
catalog.resource_refs.collect do |ref|
catalog.resource(ref).to_manifest
end.join("\n\n")
end
# Create the necessary objects to use a section. This is idempotent;
# you can 'use' a section as many times as you want.
def use(*sections)
sections = sections.collect { |s| s.to_sym }
sections = sections.reject { |s| @used.include?(s) }
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
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("; ")}"
+ status_failures = report.resource_statuses.values.select { |r| r.failed? }
+ status_fail_msg = status_failures.
+ collect(&:events).
+ flatten.
+ select { |event| event.status == 'failure' }.
+ collect { |event| "#{event.resource}: #{event.message}" }.join("; ")
+
+ raise "Got #{status_failures.length} failure(s) while initializing: #{status_fail_msg}"
end
end
sections.each { |s| @used << s }
@used.uniq!
end
def valid?(param)
param = param.to_sym
@config.has_key?(param)
end
def uninterpolated_value(param, environment = nil)
+ Puppet.deprecation_warning("Puppet.settings.uninterpolated_value is deprecated. Use Puppet.settings.value instead")
param = param.to_sym
environment &&= environment.to_sym
- # See if we can find it within our searchable list of values
- val = find_value(environment, param)
-
- # If we didn't get a value, use the default
- val = @config[param].default if val.nil?
-
- val
+ values(environment, self.preferred_run_mode).lookup(param)
end
- def find_value(environment, param)
- 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.
- return @values[source][param] if @values[source].include?(param)
- end
- return nil
+ # Retrieve an object that can be used for looking up values of configuration
+ # settings.
+ #
+ # @param environment [Symbol] The name of the environment in which to lookup
+ # @param section [Symbol] The name of the configuration section in which to lookup
+ # @return [Puppet::Settings::ChainedValues] An object to perform lookups
+ # @api public
+ def values(environment, section)
+ ChainedValues.new(
+ section,
+ environment,
+ value_sets_for(environment, section),
+ @config)
end
- private :find_value
# Find the correct value using our search path.
#
# @param param [String, Symbol] The value to look up
# @param environment [String, Symbol] The environment to check for the value
# @param bypass_interpolation [true, false] Whether to skip interpolation
#
# @return [Object] The looked up value
#
# @raise [InterpolationError]
def value(param, environment = nil, bypass_interpolation = false)
param = param.to_sym
environment &&= environment.to_sym
setting = @config[param]
- # Short circuit to nil for undefined parameters.
- return nil unless @config.include?(param)
-
- # Yay, recursion.
- #self.reparse unless [:config, :filetimeout].include?(param)
+ # Short circuit to nil for undefined settings.
+ return nil if setting.nil?
# Check the cache first. It needs to be a per-environment
# cache so that we don't spread values from one env
# to another.
if @cache[environment||"none"].has_key?(param)
return @cache[environment||"none"][param]
+ elsif bypass_interpolation
+ val = values(environment, self.preferred_run_mode).lookup(param)
+ else
+ val = values(environment, self.preferred_run_mode).interpolate(param)
end
- val = uninterpolated_value(param, environment)
-
- return val if bypass_interpolation
- if param == :code
- # if we interpolate code, all hell breaks loose.
- return val
- end
-
- # Convert it if necessary
- begin
- val = convert(val, environment)
- rescue InterpolationError => err
- # This happens because we don't have access to the param name when the
- # exception is originally raised, but we want it in the message
- raise InterpolationError, "Error converting value for param '#{param}': #{err}", err.backtrace
- end
-
- val = setting.munge(val) if setting.respond_to?(:munge)
- # And cache it
@cache[environment||"none"][param] = val
val
end
+ ##
+ # (#15337) All of the logic to determine the configuration file to use
+ # should be centralized into this method. The simplified approach is:
+ #
+ # 1. If there is an explicit configuration file, use that. (--confdir or
+ # --config)
+ # 2. If we're running as a root process, use the system puppet.conf
+ # (usually /etc/puppet/puppet.conf)
+ # 3. Otherwise, use the user puppet.conf (usually ~/.puppet/puppet.conf)
+ #
+ # @api private
+ # @todo this code duplicates {Puppet::Util::RunMode#which_dir} as described
+ # in {http://projects.puppetlabs.com/issues/16637 #16637}
+ def which_configuration_file
+ if explicit_config_file? or Puppet.features.root? then
+ return main_config_file
+ else
+ return user_config_file
+ end
+ end
+
+
private
def get_config_file_default(default)
obj = nil
unless obj = @config[default]
raise ArgumentError, "Unknown default #{default}"
end
raise ArgumentError, "Default #{default} is not a file" unless obj.is_a? FileSetting
obj
end
def add_user_resources(catalog, sections)
return unless Puppet.features.root?
return if Puppet.features.microsoft_windows?
return unless self[:mkusers]
@config.each do |name, setting|
next unless setting.respond_to?(:owner)
next unless sections.nil? or sections.include?(setting.section)
if user = setting.owner and user != "root" and catalog.resource(:user, user).nil?
resource = Puppet::Resource.new(:user, user, :parameters => {:ensure => :present})
resource[:gid] = self[:group] if self[:group]
catalog.add_resource resource
end
if group = setting.group and ! %w{root wheel}.include?(group) and catalog.resource(:group, group).nil?
catalog.add_resource Puppet::Resource.new(:group, group, :parameters => {:ensure => :present})
end
end
end
# Yield each search source in turn.
- def each_source(environment)
- searchpath(environment).each do |source|
-
- # Modify the source as necessary.
- source = self.preferred_run_mode if source == :run_mode
- yield source
- end
- end
-
- # Return all settings that have associated hooks; this is so
- # we can call them after parsing the configuration file.
- def settings_with_hooks
- @config.values.find_all { |setting| setting.has_hook? }
+ def value_sets_for(environment, mode)
+ searchpath(environment).collect do |name|
+ case name
+ when :cli, :memory, :application_defaults, :overridden_defaults
+ @value_sets[name]
+ when :run_mode
+ if @configuration_file
+ section = @configuration_file.sections[mode]
+ if section
+ ValuesFromSection.new(mode, section)
+ end
+ end
+ else
+ values_from_section = nil
+ if @configuration_file
+ if section = @configuration_file.sections[name]
+ values_from_section = ValuesFromSection.new(name, section)
+ end
+ end
+ if values_from_section.nil? && @global_defaults_initialized
+ values_from_section = ValuesFromCurrentEnvironment.new(name)
+ end
+ values_from_section
+ end
+ end.compact
end
# This method just turns a file in to a hash of hashes.
def parse_file(file)
@config_file_parser.parse_file(file, read_file(file))
end
# Read the file in.
def read_file(file)
begin
return File.read(file)
rescue Errno::ENOENT
- raise ArgumentError, "No such file #{file}"
+ raise ArgumentError, "No such file #{file}", $!.backtrace
rescue Errno::EACCES
- raise ArgumentError, "Permission denied to file #{file}"
- end
- end
-
- # Set file metadata.
- def set_metadata(meta)
- meta.each do |var, values|
- values.each do |param, value|
- @config[var].send(param.to_s + "=", value)
- end
+ raise ArgumentError, "Permission denied to file #{file}", $!.backtrace
end
end
# Private method for internal test use only; allows to do a comprehensive clear of all settings between tests.
#
# @return nil
def clear_everything_for_tests()
unsafe_clear(true, true)
@global_defaults_initialized = false
@app_defaults_initialized = false
end
private :clear_everything_for_tests
- ##
- # (#15337) All of the logic to determine the configuration file to use
- # should be centralized into this method. The simplified approach is:
- #
- # 1. If there is an explicit configuration file, use that. (--confdir or
- # --config)
- # 2. If we're running as a root process, use the system puppet.conf
- # (usually /etc/puppet/puppet.conf)
- # 3. Otherwise, use the user puppet.conf (usually ~/.puppet/puppet.conf)
- #
- # @todo this code duplicates {Puppet::Util::RunMode#which_dir} as described
- # in {http://projects.puppetlabs.com/issues/16637 #16637}
- def which_configuration_file
- if explicit_config_file? or Puppet.features.root? then
- return main_config_file
- else
- return user_config_file
- end
- end
-
def explicit_config_file?
# Figure out if the user has provided an explicit configuration file. If
# so, return the path to the file, if not return nil.
#
# The easiest way to determine whether an explicit one has been specified
# is to simply attempt to evaluate the value of ":config". This will
# obviously be successful if they've passed an explicit value for :config,
# but it will also result in successful interpolation if they've only
# passed an explicit value for :confdir.
#
# If they've specified neither, then the interpolation will fail and we'll
# get an exception.
#
begin
return true if self[:config]
rescue InterpolationError
# This means we failed to interpolate, which means that they didn't
# explicitly specify either :config or :confdir... so we'll fall out to
# the default value.
return false
end
end
private :explicit_config_file?
+ # Lookup configuration setting value through a chain of different value sources.
+ #
+ # @api public
+ class ChainedValues
+ # @see Puppet::Settings.values
+ # @api private
+ def initialize(mode, environment, value_sets, defaults)
+ @mode = mode
+ @environment = environment
+ @value_sets = value_sets
+ @defaults = defaults
+ end
+
+ # Lookup the uninterpolated value.
+ #
+ # @param name [Symbol] The configuration setting name to look up
+ # @return [Object] The configuration setting value or nil if the setting is not known
+ # @api public
+ def lookup(name)
+ set = @value_sets.find do |set|
+ set.include?(name)
+ end
+ if set
+ value = set.lookup(name)
+ if !value.nil?
+ return value
+ end
+ end
+
+ @defaults[name].default
+ end
+
+ # Lookup the interpolated value. All instances of `$name` in the value will
+ # be replaced by performing a lookup of `name` and substituting the text
+ # for `$name` in the original value. This interpolation is only performed
+ # if the looked up value is a String.
+ #
+ # @param name [Symbol] The configuration setting name to look up
+ # @return [Object] The configuration setting value or nil if the setting is not known
+ # @api public
+ def interpolate(name)
+ setting = @defaults[name]
+
+ if setting
+ val = lookup(name)
+ # if we interpolate code, all hell breaks loose.
+ if name == :code
+ val
+ else
+ # Convert it if necessary
+ begin
+ val = convert(val)
+ rescue InterpolationError => err
+ # This happens because we don't have access to the param name when the
+ # exception is originally raised, but we want it in the message
+ raise InterpolationError, "Error converting value for param '#{name}': #{err}", err.backtrace
+ end
+
+ setting.munge(val)
+ end
+ else
+ nil
+ end
+ end
+
+ private
+
+ def convert(value)
+ return nil if value.nil?
+ return value unless value.is_a? String
+ value.gsub(/\$(\w+)|\$\{(\w+)\}/) do |value|
+ varname = $2 || $1
+ if varname == "environment"
+ @environment
+ elsif varname == "run_mode"
+ @mode
+ elsif !(pval = interpolate(varname.to_sym)).nil?
+ pval
+ else
+ raise InterpolationError, "Could not find value for #{value}"
+ end
+ end
+ end
+ end
+
+ class Values
+ def initialize(name, defaults)
+ @name = name
+ @values = {}
+ @defaults = defaults
+ end
+
+ def include?(name)
+ @values.include?(name)
+ end
+
+ def set(name, value)
+ if !@defaults[name]
+ raise ArgumentError,
+ "Attempt to assign a value to unknown setting #{name.inspect}"
+ end
+
+ if @defaults[name].has_hook?
+ @defaults[name].handle(value)
+ end
+
+ @values[name] = value
+ end
+
+ def lookup(name)
+ @values[name]
+ end
+ end
+
+ class ValuesFromSection
+ def initialize(name, section)
+ @name = name
+ @section = section
+ end
+
+ def include?(name)
+ !@section.setting(name).nil?
+ end
+
+ def lookup(name)
+ setting = @section.setting(name)
+ if setting
+ setting.value
+ end
+ end
+ end
+
+ # @api private
+ class ValuesFromCurrentEnvironment
+ def initialize(desired_environment)
+ @desired_environment = desired_environment
+ end
+
+ def include?(name)
+ return false unless name == :modulepath || name == :manifest
+ if i = instance
+ i.include?(name)
+ end
+ end
+
+ def lookup(name)
+ return nil unless name == :modulepath || name == :manifest
+ if i = instance
+ i[name]
+ end
+ end
+
+ private
+
+ def instance
+ unless @instance
+ env = Puppet.lookup(:current_environment)
+ if env.name == @desired_environment
+ @instance = {
+ :modulepath => env.full_modulepath.join(File::PATH_SEPARATOR),
+ :manifest => env.manifest,
+ }
+ end
+ end
+ return @instance
+ end
+ end
end
diff --git a/lib/puppet/settings/base_setting.rb b/lib/puppet/settings/base_setting.rb
index ac38b35f3..e6c12de42 100644
--- a/lib/puppet/settings/base_setting.rb
+++ b/lib/puppet/settings/base_setting.rb
@@ -1,162 +1,168 @@
require 'puppet/settings/errors'
# The base setting type
class Puppet::Settings::BaseSetting
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 call_on_define
Puppet.deprecation_warning "call_on_define has been deprecated. Please use call_hook_on_define?"
call_hook_on_define?
end
def call_on_define=(value)
if value
Puppet.deprecation_warning ":call_on_define has been changed to :call_hook => :on_define_and_write. Please change #{name}."
@call_hook = :on_define_and_write
else
Puppet.deprecation_warning ":call_on_define => :false has been changed to :call_hook => :on_write_only. Please change #{name}."
@call_hook = :on_write_only
end
end
def call_hook=(value)
if value.nil?
Puppet.warning "Setting :#{name} :call_hook is nil, defaulting to :on_write_only"
value ||= :on_write_only
end
raise ArgumentError, "Invalid option #{value} for call_hook" unless self.class.available_call_hook_values.include? value
@call_hook = value
end
def call_hook_on_define?
call_hook == :on_define_and_write
end
def call_hook_on_initialize?
call_hook == :on_initialize_and_write
end
#added as a proper method, only to generate a deprecation warning
#and return value from
def setbycli
Puppet.deprecation_warning "Puppet.settings.setting(#{name}).setbycli is deprecated. Use Puppet.settings.set_by_cli?(#{name}) instead."
@settings.set_by_cli?(name)
end
def setbycli=(value)
Puppet.deprecation_warning "Puppet.settings.setting(#{name}).setbycli= is deprecated. You should not manually set that values were specified on the command line."
@settings.set_value(name, @settings[name], :cli) if value
raise ArgumentError, "Cannot unset setbycli" unless value
end
# get the arguments in getopt format
def getopt_args
if short
[["--#{name}", "-#{short}", GetoptLong::REQUIRED_ARGUMENT]]
else
[["--#{name}", GetoptLong::REQUIRED_ARGUMENT]]
end
end
# get the arguments in OptionParser format
def optparse_args
if short
["--#{name}", "-#{short}", desc, :REQUIRED]
else
["--#{name}", desc, :REQUIRED]
end
end
- def has_hook?
- respond_to? :handle
- end
-
def hook=(block)
+ @has_hook = true
meta_def :handle, &block
end
+ def has_hook?
+ @has_hook
+ end
+
# Create the new element. Pretty much just sets the name.
def initialize(args = {})
unless @settings = args.delete(:settings)
raise ArgumentError.new("You must refer to a settings object")
end
# explicitly set name prior to calling other param= methods to provide meaningful feedback during
# other warnings
@name = args[:name] if args.include? :name
#set the default value for call_hook
@call_hook = :on_write_only if args[:hook] and not args[:call_hook]
+ @has_hook = false
raise ArgumentError, "Cannot reference :call_hook for :#{@name} if no :hook is defined" if args[:call_hook] and not args[:hook]
args.each do |param, value|
method = param.to_s + "="
raise ArgumentError, "#{self.class} (setting '#{args[:name]}') does not accept #{param}" unless self.respond_to? method
self.send(method, value)
end
raise ArgumentError, "You must provide a description for the #{self.name} config option" unless self.desc
end
def iscreated
@iscreated = true
end
def iscreated?
@iscreated
end
# short name for the celement
def short=(value)
raise ArgumentError, "Short names can only be one character." if value.to_s.length != 1
@short = value.to_s
end
def default(check_application_defaults_first = false)
return @default unless check_application_defaults_first
return @settings.value(name, :application_defaults, true) || @default
end
# Convert the object to a config statement.
def to_config
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)
# If the value has not been overridden, then print it out commented
# and unconverted, so it's clear that that's the default and how it
# works.
value = @settings.value(self.name)
if value != @default
line = "#{@name} = #{value}"
else
line = "# #{@name} = #{@default}"
end
str << (line + "\n")
# Indent
str.gsub(/^/, " ")
end
# Retrieves the value, or if it's not set, retrieves the default.
def value
@settings.value(self.name)
end
# Modify the value when it is first evaluated
def munge(value)
value
end
+
+ def set_meta(meta)
+ Puppet.notice("#{name} does not support meta data. Ignoring.")
+ end
end
diff --git a/lib/puppet/settings/config_file.rb b/lib/puppet/settings/config_file.rb
index b1120105b..2fab65a84 100644
--- a/lib/puppet/settings/config_file.rb
+++ b/lib/puppet/settings/config_file.rb
@@ -1,97 +1,134 @@
+require 'puppet/settings/ini_file'
+
##
# @api private
#
# Parses puppet configuration files
#
class Puppet::Settings::ConfigFile
##
# @param value_converter [Proc] a function that will convert strings into ruby types
#
def initialize(value_converter)
@value_converter = value_converter
end
def parse_file(file, text)
- result = {}
- count = 0
-
- # Default to 'main' for the section.
- section_name = :main
- result[section_name] = empty_section
- text.split(/\n/).each do |line|
- count += 1
- case line
- when /^\s*\[(\w+)\]\s*$/
- section_name = $1.intern
- fail_when_illegal_section_name(section_name, file, line)
- if result[section_name].nil?
- result[section_name] = empty_section
- end
- when /^\s*#/; next # Skip comments
- when /^\s*$/; next # Skip blanks
- when /^\s*(\w+)\s*=\s*(.*?)\s*$/ # settings
- var = $1.intern
-
- # We don't want to munge modes, because they're specified in octal, so we'll
- # just leave them as a String, since Puppet handles that case correctly.
- if var == :mode
- value = $2
- else
- value = @value_converter[$2]
- end
+ result = Conf.new
- # Check to see if this is a file argument and it has extra options
- begin
- if value.is_a?(String) and options = extract_fileinfo(value)
- value = options[:value]
- options.delete(:value)
- result[section_name][:_meta][var] = options
- end
- result[section_name][var] = value
- rescue Puppet::Error => detail
- raise Puppet::Settings::ParseError.new(detail.message, file, line, detail)
+ ini = Puppet::Settings::IniFile.parse(StringIO.new(text))
+ unique_sections_in(ini, file).each do |section_name|
+ section = Section.new(section_name.to_sym)
+ result.with_section(section)
+
+ ini.lines_in(section_name).each do |line|
+ if line.is_a?(Puppet::Settings::IniFile::SettingLine)
+ parse_setting(line, section)
+ elsif line.text !~ /^\s*#|^\s*$/
+ raise Puppet::Settings::ParseError.new("Could not match line #{line.text}", file, line.line_number)
end
- else
- raise Puppet::Settings::ParseError.new("Could not match line #{line}", file, line)
end
end
result
end
+ Conf = Struct.new(:sections) do
+ def initialize
+ super({})
+ end
+
+ def with_section(section)
+ sections[section.name] = section
+ self
+ end
+ end
+
+ Section = Struct.new(:name, :settings) do
+ def initialize(name)
+ super(name, [])
+ end
+
+ def with_setting(name, value, meta)
+ settings << Setting.new(name, value, meta)
+ self
+ end
+
+ def setting(name)
+ settings.find { |setting| setting.name == name }
+ end
+ end
+
+ Setting = Struct.new(:name, :value, :meta) do
+ def has_metadata?
+ meta != NO_META
+ end
+ end
+
+ Meta = Struct.new(:owner, :group, :mode)
+ NO_META = Meta.new(nil, nil, nil)
+
private
- def empty_section
- { :_meta => {} }
+ def unique_sections_in(ini, file)
+ ini.section_lines.collect do |section|
+ if section.name == "application_defaults" || section.name == "global_defaults"
+ raise Puppet::Error, "Illegal section '#{section.name}' in config file #{file} at line #{section.line_number}"
+ end
+ section.name
+ end.uniq
end
- def fail_when_illegal_section_name(section, file, line)
- if section == :application_defaults or section == :global_defaults
- raise Puppet::Error, "Illegal section '#{section}' in config file #{file} at line #{line}"
+ def parse_setting(setting, section)
+ var = setting.name.intern
+
+ # We don't want to munge modes, because they're specified in octal, so we'll
+ # just leave them as a String, since Puppet handles that case correctly.
+ if var == :mode
+ value = setting.value
+ else
+ value = @value_converter[setting.value]
+ end
+
+ # Check to see if this is a file argument and it has extra options
+ begin
+ if value.is_a?(String) and options = extract_fileinfo(value)
+ section.with_setting(var, options[:value], Meta.new(options[:owner],
+ options[:group],
+ options[:mode]))
+ else
+ section.with_setting(var, value, NO_META)
+ end
+ rescue Puppet::Error => detail
+ raise Puppet::Settings::ParseError.new(detail.message, file, setting.line_number, detail)
end
end
+ def empty_section
+ { :_meta => {} }
+ end
+
def extract_fileinfo(string)
result = {}
value = string.sub(/\{\s*([^}]+)\s*\}/) do
params = $1
params.split(/\s*,\s*/).each do |str|
if str =~ /^\s*(\w+)\s*=\s*([\w\d]+)\s*$/
param, value = $1.intern, $2
result[param] = value
raise ArgumentError, "Invalid file option '#{param}'" unless [:owner, :mode, :group].include?(param)
if param == :mode and value !~ /^\d+$/
raise ArgumentError, "File modes must be numbers"
end
else
raise ArgumentError, "Could not parse '#{string}'"
end
end
''
end
result[:value] = value.sub(/\s*$/, '')
result
end
end
diff --git a/lib/puppet/settings/directory_setting.rb b/lib/puppet/settings/directory_setting.rb
index 9f67f96ab..ea795e39c 100644
--- a/lib/puppet/settings/directory_setting.rb
+++ b/lib/puppet/settings/directory_setting.rb
@@ -1,13 +1,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)
+ Puppet::FileSystem.open(filename, mode, option, &block)
end
end
end
diff --git a/lib/puppet/settings/file_setting.rb b/lib/puppet/settings/file_setting.rb
index 243805edb..e20767374 100644
--- a/lib/puppet/settings/file_setting.rb
+++ b/lib/puppet/settings/file_setting.rb
@@ -1,219 +1,226 @@
# A file.
class Puppet::Settings::FileSetting < Puppet::Settings::StringSetting
class SettingError < StandardError; end
# An unspecified user or group
#
# @api private
class Unspecified
def value
nil
end
end
# A "root" user or group
#
# @api private
class Root
def value
"root"
end
end
# A "service" user or group that picks up values from settings when the
# referenced user or group is safe to use (it exists or will be created), and
# uses the given fallback value when not safe.
#
# @api private
class Service
# @param name [Symbol] the name of the setting to use as the service value
# @param fallback [String, nil] the value to use when the service value cannot be used
# @param settings [Puppet::Settings] the puppet settings object
# @param available_method [Symbol] the name of the method to call on
# settings to determine if the value in settings is available on the system
#
def initialize(name, fallback, settings, available_method)
@settings = settings
@available_method = available_method
@name = name
@fallback = fallback
end
def value
if safe_to_use_settings_value?
@settings[@name]
else
@fallback
end
end
private
def safe_to_use_settings_value?
@settings[:mkusers] or @settings.send(@available_method)
end
end
attr_accessor :mode, :create
def initialize(args)
@group = Unspecified.new
@owner = Unspecified.new
super(args)
end
# Should we create files, rather than just directories?
def create_files?
create
end
# @param value [String] the group to use on the created file (can only be "root" or "service")
# @api public
def group=(value)
@group = case value
when "root"
Root.new
when "service"
# Group falls back to `nil` because we cannot assume that a "root" group exists.
# Some systems have root group, others have wheel, others have something else.
Service.new(:group, nil, @settings, :service_group_available?)
else
unknown_value(':group', value)
end
end
# @param value [String] the owner to use on the created file (can only be "root" or "service")
# @api public
def owner=(value)
@owner = case value
when "root"
Root.new
when "service"
Service.new(:user, "root", @settings, :service_user_available?)
else
unknown_value(':owner', value)
end
end
# @return [String, nil] the name of the group to use for the file or nil if the group should not be managed
# @api public
def group
@group.value
end
# @return [String, nil] the name of the user to use for the file or nil if the user should not be managed
# @api public
def owner
@owner.value
end
+ def set_meta(meta)
+ self.owner = meta.owner if meta.owner
+ self.group = meta.group if meta.group
+ self.mode = meta.mode if meta.mode
+ end
+
def munge(value)
if value.is_a?(String) and value != ':memory:' # for sqlite3 in-memory tests
value = File.expand_path(value)
end
value
end
def type
:file
end
# Turn our setting thing into a Puppet::Resource instance.
def to_resource
return nil unless type = self.type
path = self.value
return nil unless path.is_a?(String)
# Make sure the paths are fully qualified.
path = File.expand_path(path)
- return nil unless type == :directory or create_files? or Puppet::FileSystem::File.exist?(path)
+ return nil unless type == :directory or create_files? or Puppet::FileSystem.exist?(path)
return nil if path =~ /^\/dev/ or path =~ /^[A-Z]:\/dev/i
resource = Puppet::Resource.new(:file, path)
if Puppet[:manage_internal_file_permissions]
if self.mode
# This ends up mimicking the munge method of the mode
# parameter to make sure that we're always passing the string
# version of the octal number. If we were setting the
# 'should' value for mode rather than the 'is', then the munge
# method would be called for us automatically. Normally, one
# wouldn't need to call the munge method manually, since
# 'should' gets set by the provider and it should be able to
# provide the data in the appropriate format.
mode = self.mode
mode = mode.to_i(8) if mode.is_a?(String)
mode = mode.to_s(8)
resource[:mode] = mode
end
# REMIND fails on Windows because chown/chgrp functionality not supported yet
if Puppet.features.root? and !Puppet.features.microsoft_windows?
resource[:owner] = self.owner if self.owner
resource[:group] = self.group if self.group
end
end
resource[:ensure] = type
resource[:loglevel] = :debug
resource[:links] = :follow
resource[:backup] = false
resource.tag(self.section, self.name, "settings")
resource
end
# Make sure any provided variables look up to something.
def validate(value)
return true unless value.is_a? String
value.scan(/\$(\w+)/) { |name|
name = $1
unless @settings.include?(name)
raise ArgumentError,
"Settings parameter '#{name}' is undefined"
end
}
end
# @api private
def exclusive_open(option = 'r', &block)
controlled_access do |mode|
- file.exclusive_open(mode, option, &block)
+ Puppet::FileSystem.exclusive_open(file(), mode, option, &block)
end
end
# @api private
def open(option = 'r', &block)
controlled_access do |mode|
- file.open(mode, option, &block)
+ Puppet::FileSystem.open(file, mode, option, &block)
end
end
-private
+ private
+
def file
- Puppet::FileSystem::File.new(value)
+ Puppet::FileSystem.pathname(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/ini_file.rb b/lib/puppet/settings/ini_file.rb
new file mode 100644
index 000000000..5bf4bc057
--- /dev/null
+++ b/lib/puppet/settings/ini_file.rb
@@ -0,0 +1,171 @@
+# @api private
+class Puppet::Settings::IniFile
+ DEFAULT_SECTION_NAME = "main"
+
+ def self.update(config_fh, &block)
+ config = parse(config_fh)
+ manipulator = Manipulator.new(config)
+ yield manipulator
+ config.write(config_fh)
+ end
+
+ def self.parse(config_fh)
+ config = new([DefaultSection.new])
+ config_fh.each_line do |line|
+ case line
+ when /^(\s*)\[(\w+)\](\s*)$/
+ config.append(SectionLine.new($1, $2, $3))
+ when /^(\s*)(\w+)(\s*=\s*)(.*?)(\s*)$/
+ config.append(SettingLine.new($1, $2, $3, $4, $5))
+ else
+ config.append(Line.new(line))
+ end
+ end
+
+ config
+ end
+
+ def initialize(lines = [])
+ @lines = lines
+ end
+
+ def append(line)
+ line.previous = @lines.last
+ @lines << line
+ end
+
+ def insert_after(line, new_line)
+ new_line.previous = line
+
+ insertion_point = @lines.index(line)
+ @lines.insert(insertion_point + 1, new_line)
+ if @lines.length > insertion_point + 2
+ @lines[insertion_point + 2].previous = new_line
+ end
+ end
+
+ def section_lines
+ @lines.select { |line| line.is_a?(SectionLine) }
+ end
+
+ def section_line(name)
+ section_lines.find { |section| section.name == name }
+ end
+
+ def setting(section, name)
+ settings_in(lines_in(section)).find do |line|
+ line.name == name
+ end
+ end
+
+ def lines_in(section_name)
+ section_lines = []
+ current_section_name = DEFAULT_SECTION_NAME
+ @lines.each do |line|
+ if line.is_a?(SectionLine)
+ current_section_name = line.name
+ elsif current_section_name == section_name
+ section_lines << line
+ end
+ end
+
+ section_lines
+ end
+
+ def settings_in(lines)
+ lines.select { |line| line.is_a?(SettingLine) }
+ end
+
+ def write(fh)
+ fh.truncate(0)
+ fh.rewind
+ @lines.each do |line|
+ line.write(fh)
+ end
+ fh.flush
+ end
+
+ class Manipulator
+ def initialize(config)
+ @config = config
+ end
+
+ def set(section, name, value)
+ setting = @config.setting(section, name)
+ if setting
+ setting.value = value
+ else
+ add_setting(section, name, value)
+ end
+ end
+
+ private
+
+ def add_setting(section_name, name, value)
+ section = @config.section_line(section_name)
+ if section.nil?
+ previous_line = SectionLine.new("", section_name, "")
+ @config.append(previous_line)
+ else
+ previous_line = @config.settings_in(@config.lines_in(section_name)).last || section
+ end
+
+ @config.insert_after(previous_line, SettingLine.new("", name, " = ", value, ""))
+ end
+ end
+
+ module LineNumber
+ attr_accessor :previous
+
+ def line_number
+ line = 0
+ previous_line = previous
+ while previous_line
+ line += 1
+ previous_line = previous_line.previous
+ end
+ line
+ end
+ end
+
+ Line = Struct.new(:text) do
+ include LineNumber
+
+ def write(fh)
+ fh.puts(text)
+ end
+ end
+
+ SettingLine = Struct.new(:prefix, :name, :infix, :value, :suffix) do
+ include LineNumber
+
+ def write(fh)
+ fh.write(prefix)
+ fh.write(name)
+ fh.write(infix)
+ fh.write(value)
+ fh.puts(suffix)
+ end
+ end
+
+ SectionLine = Struct.new(:prefix, :name, :suffix) do
+ include LineNumber
+
+ def write(fh)
+ fh.write(prefix)
+ fh.write("[")
+ fh.write(name)
+ fh.write("]")
+ fh.puts(suffix)
+ end
+ end
+
+ class DefaultSection < SectionLine
+ def initialize
+ super("", DEFAULT_SECTION_NAME, "")
+ end
+
+ def write(fh)
+ end
+ end
+end
diff --git a/lib/puppet/ssl/base.rb b/lib/puppet/ssl/base.rb
index 5ceda6245..0889ba731 100644
--- a/lib/puppet/ssl/base.rb
+++ b/lib/puppet/ssl/base.rb
@@ -1,136 +1,140 @@
require 'openssl'
require 'puppet/ssl'
require 'puppet/ssl/digest'
require 'puppet/util/ssl'
# The base class for wrapping SSL instances.
class Puppet::SSL::Base
# For now, use the YAML separator.
SEPARATOR = "\n---\n"
# Only allow printing ascii characters, excluding /
VALID_CERTNAME = /\A[ -.0-~]+\Z/
def self.from_multiple_s(text)
text.split(SEPARATOR).collect { |inst| from_s(inst) }
end
def self.to_multiple_s(instances)
instances.collect { |inst| inst.to_s }.join(SEPARATOR)
end
def self.wraps(klass)
@wrapped_class = klass
end
def self.wrapped_class
raise(Puppet::DevError, "#{self} has not declared what class it wraps") unless defined?(@wrapped_class)
@wrapped_class
end
def self.validate_certname(name)
raise "Certname #{name.inspect} must not contain unprintable or non-ASCII characters" unless name =~ VALID_CERTNAME
end
attr_accessor :name, :content
# Is this file for the CA?
def ca?
name == Puppet::SSL::Host.ca_name
end
def generate
raise Puppet::DevError, "#{self.class} did not override 'generate'"
end
def initialize(name)
@name = name.to_s.downcase
self.class.validate_certname(@name)
end
##
# name_from_subject extracts the common name attribute from the subject of an
# x.509 certificate certificate
#
# @api private
#
# @param [OpenSSL::X509::Name] subject The full subject (distinguished name) of the x.509
# certificate.
#
# @return [String] the name (CN) extracted from the subject.
def self.name_from_subject(subject)
Puppet::Util::SSL.cn_from_subject(subject)
end
# Create an instance of our Puppet::SSL::* class using a given instance of the wrapped class
def self.from_instance(instance, name = nil)
raise ArgumentError, "Object must be an instance of #{wrapped_class}, #{instance.class} given" unless instance.is_a? wrapped_class
raise ArgumentError, "Name must be supplied if it cannot be determined from the instance" if name.nil? and !instance.respond_to?(:subject)
name ||= name_from_subject(instance.subject)
result = new(name)
result.content = instance
result
end
# Convert a string into an instance
def self.from_s(string, name = nil)
instance = wrapped_class.new(string)
from_instance(instance, name)
end
# Read content from disk appropriately.
def read(path)
@content = wrapped_class.new(File.read(path))
end
# Convert our thing to pem.
def to_s
return "" unless content
content.to_pem
end
+ def to_data_hash
+ to_s
+ end
+
# Provide the full text of the thing we're dealing with.
def to_text
return "" unless content
content.to_text
end
def fingerprint(md = :SHA256)
mds = md.to_s.upcase
digest(mds).to_hex
end
def digest(algorithm=nil)
unless algorithm
algorithm = digest_algorithm
end
Puppet::SSL::Digest.new(algorithm, content.to_der)
end
def digest_algorithm
# The signature_algorithm on the X509 cert is a combination of the digest
# algorithm and the encryption algorithm
# e.g. md5WithRSAEncryption, sha256WithRSAEncryption
# Unfortunately there isn't a consistent pattern
# See RFCs 3279, 5758
digest_re = Regexp.union(
/ripemd160/i,
/md[245]/i,
/sha\d*/i
)
ln = content.signature_algorithm
if match = digest_re.match(ln)
match[0].downcase
else
raise Puppet::Error, "Unknown signature algorithm '#{ln}'"
end
end
private
def wrapped_class
self.class.wrapped_class
end
end
diff --git a/lib/puppet/ssl/certificate_authority.rb b/lib/puppet/ssl/certificate_authority.rb
index 3ff4d5eb5..0fb6b20b8 100644
--- a/lib/puppet/ssl/certificate_authority.rb
+++ b/lib/puppet/ssl/certificate_authority.rb
@@ -1,504 +1,509 @@
require 'puppet/ssl/host'
require 'puppet/ssl/certificate_request'
require 'puppet/ssl/certificate_signer'
require 'puppet/util'
# The class that knows how to sign certificates. It creates
# a 'special' SSL::Host whose name is 'ca', thus indicating
# that, well, it's the CA. There's some magic in the
# indirector/ssl_file terminus base class that does that
# for us.
# This class mostly just signs certs for us, but
# it can also be seen as a general interface into all of the
# SSL stuff.
class Puppet::SSL::CertificateAuthority
# We will only sign extensions on this whitelist, ever. Any CSR with a
# requested extension that we don't recognize is rejected, against the risk
# that it will introduce some security issue through our ignorance of it.
#
# Adding an extension to this whitelist simply means we will consider it
# further, not that we will always accept a certificate with an extension
# requested on this list.
RequestExtensionWhitelist = %w{subjectAltName}
require 'puppet/ssl/certificate_factory'
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'
class CertificateVerificationError < RuntimeError
attr_accessor :error_code
def initialize(code)
@error_code = code
end
end
def self.singleton_instance
@singleton_instance ||= new
end
class CertificateSigningError < RuntimeError
attr_accessor :host
def initialize(host)
@host = host
end
end
def self.ca?
# 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.
def self.instance
ca? ? singleton_instance : nil
end
attr_reader :name, :host
# 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
# 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]
decider = case auto
when 'false', false, nil
AutosignNever.new
when 'true', true
AutosignAlways.new
else
- file = Puppet::FileSystem::File.new(auto)
- if file.executable?
+ file = Puppet::FileSystem.pathname(auto)
+ if Puppet::FileSystem.executable?(file)
Puppet::SSL::CertificateAuthority::AutosignCommand.new(auto)
- elsif file.exist?
+ elsif Puppet::FileSystem.exist?(file)
AutosignConfig.new(file)
else
AutosignNever.new
end
end
decider.allowed?(csr)
end
# 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)
@crl = Puppet::SSL::CertificateRevocationList.new(Puppet::SSL::CA_NAME)
@crl.generate(host.certificate.content, host.key.content)
Puppet::SSL::CertificateRevocationList.indirection.save(@crl)
end
end
@crl
end
# Delegates this to our Host class.
def destroy(name)
Puppet::SSL::Host.destroy(name)
end
# 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)
# Pass on any requested subjectAltName field.
san = options[:dns_alt_names]
host = Puppet::SSL::Host.new(name)
host.generate_certificate_request(:dns_alt_names => san)
# CSR may have been implicitly autosigned, generating a certificate
# Or sign explicitly
host.certificate || sign(name, !!san)
end
# Generate our CA certificate.
def generate_ca_certificate
generate_password unless password?
host.generate_key unless host.key
# Create a new cert request. We do this specially, because we don't want
# to actually save the request anywhere.
request = Puppet::SSL::CertificateRequest.new(host.name)
# We deliberately do not put any subjectAltName in here: the CA
# certificate absolutely does not need them. --daniel 2011-10-13
request.generate(host.key)
# Create a self-signed certificate.
@certificate = sign(host.name, false, request)
# And make sure we initialize our CRL.
crl
end
def initialize
Puppet.settings.use :main, :ssl, :ca
@name = Puppet[:certname]
@host = Puppet::SSL::Host.new(Puppet::SSL::Host.ca_name)
setup
end
# Retrieve (or create, if necessary) our inventory manager.
def inventory
@inventory ||= Puppet::SSL::Inventory.new
end
# Generate a new password for the CA.
def generate_password
pass = ""
20.times { pass += (rand(74) + 48).chr }
begin
Puppet.settings.setting(:capass).open('w') { |f| f.print pass }
rescue Errno::EACCES => detail
- raise Puppet::Error, "Could not write CA password: #{detail}"
+ raise Puppet::Error, "Could not write CA password: #{detail}", detail.backtrace
end
@password = pass
pass
end
# Lists the names of all signed certificates.
#
+ # @param name [Array<string>] filter to cerificate names
+ #
# @return [Array<String>]
- def list
- list_certificates.collect { |c| c.name }
+ def list(name='*')
+ list_certificates(name).collect { |c| c.name }
end
# Return all the certificate objects as found by the indirector
# API for PE license checking.
#
# Created to prevent the case of reading all certs from disk, getting
# just their names and verifying the cert for each name, which then
# causes the cert to again be read from disk.
+ # @param name [Array<string>] filter to cerificate names
#
# @author Jeff Weiss <jeff.weiss@puppetlabs.com>
# @api Puppet Enterprise Licensing
#
+ # @param name [Array<string>] filter to cerificate names
+ #
# @return [Array<Puppet::SSL::Certificate>]
- def list_certificates
- Puppet::SSL::Certificate.indirection.search("*")
+ def list_certificates(name='*')
+ Puppet::SSL::Certificate.indirection.search(name)
end
# Read the next serial from the serial file, and increment the
# file so this one is considered used.
def next_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
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?
- Puppet::FileSystem::File.exist? Puppet[:capass]
+ Puppet::FileSystem.exist?(Puppet[:capass])
end
# Print a given host's certificate as text.
def print(name)
(cert = Puppet::SSL::Certificate.indirection.find(name)) ? cert.to_text : nil
end
# Revoke a given certificate.
def revoke(name)
raise ArgumentError, "Cannot revoke certificates when the CRL is disabled" unless crl
if cert = Puppet::SSL::Certificate.indirection.find(name)
serial = cert.content.serial
elsif name =~ /^0x[0-9A-Fa-f]+$/
serial = name.hex
elsif ! serial = inventory.serial(name)
raise ArgumentError, "Could not find a serial number for #{name}"
end
crl.revoke(serial, host.key.content)
end
# This initializes our CA so it actually works. This should be a private
# method, except that you can't any-instance stub private methods, which is
# *awesome*. This method only really exists to provide a stub-point during
# testing.
def setup
generate_ca_certificate unless @host.certificate
end
# Sign a given certificate request.
def sign(hostname, allow_dns_alt_names = false, self_signing_csr = nil)
# This is a self-signed certificate
if self_signing_csr
# # This is a self-signed certificate, which is for the CA. Since this
# # forces the certificate to be self-signed, anyone who manages to trick
# # the system into going through this path gets a certificate they could
# # generate anyway. There should be no security risk from that.
csr = self_signing_csr
cert_type = :ca
issuer = csr.content
else
allow_dns_alt_names = true if hostname == Puppet[:certname].downcase
unless csr = Puppet::SSL::CertificateRequest.indirection.find(hostname)
raise ArgumentError, "Could not find certificate request for #{hostname}"
end
cert_type = :server
issuer = host.certificate.content
# Make sure that the CSR conforms to our internal signing policies.
# This will raise if the CSR doesn't conform, but just in case...
check_internal_signing_policies(hostname, csr, allow_dns_alt_names) or
raise CertificateSigningError.new(hostname), "CSR had an unknown failure checking internal signing policies, will not sign!"
end
cert = Puppet::SSL::Certificate.new(hostname)
cert.content = Puppet::SSL::CertificateFactory.
build(cert_type, csr, issuer, next_serial)
signer = Puppet::SSL::CertificateSigner.new
signer.sign(cert.content, host.key.content)
Puppet.notice "Signed certificate request for #{hostname}"
# Add the cert to the inventory before we save it, since
# otherwise we could end up with it being duplicated, if
# this is the first time we build the inventory file.
inventory.add(cert)
# Save the now-signed cert. This should get routed correctly depending
# on the certificate type.
Puppet::SSL::Certificate.indirection.save(cert)
# And remove the CSR if this wasn't self signed.
Puppet::SSL::CertificateRequest.indirection.destroy(csr.name) unless self_signing_csr
cert
end
def check_internal_signing_policies(hostname, csr, allow_dns_alt_names)
# Reject unknown request extensions.
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(", ")
raise CertificateSigningError.new(hostname), "CSR has request extensions that are not permitted: #{names}"
end
# Do not sign misleading CSRs
cn = csr.content.subject.to_a.assoc("CN")[1]
if hostname != cn
raise CertificateSigningError.new(hostname), "CSR subject common name #{cn.inspect} does not match expected certname #{hostname.inspect}"
end
if hostname !~ Puppet::SSL::Base::VALID_CERTNAME
raise CertificateSigningError.new(hostname), "CSR #{hostname.inspect} subject contains unprintable or non-ASCII characters"
end
# Wildcards: we don't allow 'em at any point.
#
# The stringification here makes the content visible, and saves us having
# to scrobble through the content of the CSR subject field to make sure it
# is what we expect where we expect it.
if csr.content.subject.to_s.include? '*'
raise CertificateSigningError.new(hostname), "CSR subject contains a wildcard, which is not allowed: #{csr.content.subject.to_s}"
end
unless csr.content.verify(csr.content.public_key)
raise CertificateSigningError.new(hostname), "CSR contains a public key that does not correspond to the signing key"
end
unless csr.subject_alt_names.empty?
# If you alt names are allowed, they are required. Otherwise they are
# disallowed. Self-signed certs are implicitly trusted, however.
unless allow_dns_alt_names
raise CertificateSigningError.new(hostname), "CSR '#{csr.name}' contains subject alternative names (#{csr.subject_alt_names.join(', ')}), which are disallowed. Use `puppet cert --allow-dns-alt-names sign #{csr.name}` to sign this request."
end
# If subjectAltNames are present, validate that they are only for DNS
# labels, not any other kind.
unless csr.subject_alt_names.all? {|x| x =~ /^DNS:/ }
raise CertificateSigningError.new(hostname), "CSR '#{csr.name}' contains a subjectAltName outside the DNS label space: #{csr.subject_alt_names.join(', ')}. To continue, this CSR needs to be cleaned."
end
# Check for wildcards in the subjectAltName fields too.
if csr.subject_alt_names.any? {|x| x.include? '*' }
raise CertificateSigningError.new(hostname), "CSR '#{csr.name}' subjectAltName contains a wildcard, which is not allowed: #{csr.subject_alt_names.join(', ')} To continue, this CSR needs to be cleaned."
end
end
return true # good enough for us!
end
# Utility method for optionally caching the X509 Store for verifying a
# large number of certificates in a short amount of time--exactly the
# case we have during PE license checking.
#
# @example Use the cached X509 store
# x509store(:cache => true)
#
# @example Use a freshly create X509 store
# x509store
# x509store(:cache => false)
#
# @param [Hash] options the options used for retrieving the X509 Store
# @option options [Boolean] :cache whether or not to use a cached version
# of the X509 Store
#
# @return [OpenSSL::X509::Store]
def x509_store(options = {})
if (options[:cache])
return @x509store unless @x509store.nil?
@x509store = create_x509_store
else
create_x509_store
end
end
private :x509_store
# Creates a brand new OpenSSL::X509::Store with the appropriate
# Certificate Revocation List and flags
#
# @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.purpose = OpenSSL::X509::PURPOSE_SSL_CLIENT
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
# Utility method which is API for PE license checking.
- # This is used rather than `verify` because
+ # This is used rather than `verify` because
# 1) We have already read the certificate from disk into memory.
# To read the certificate from disk again is just wasteful.
# 2) Because we're checking a large number of certificates against
# a transient CertificateAuthority, we can relatively safely cache
# the X509 Store that actually does the verification.
#
# Long running instances of CertificateAuthority will certainly
# want to use `verify` because it will recreate the X509 Store with
# the absolutely latest CRL.
#
# Additionally, this method explicitly returns a boolean whereas
# `verify` will raise an error if the certificate has been revoked.
#
# @author Jeff Weiss <jeff.weiss@puppetlabs.com>
# @api Puppet Enterprise Licensing
#
# @param cert [Puppet::SSL::Certificate] the certificate to check validity of
#
# @return [Boolean] true if signed, false if unsigned or revoked
def certificate_is_alive?(cert)
x509_store(:cache => true).verify(cert.content)
end
# Verify a given host's certificate. The certname is passed in, and
# the indirector will be used to locate the actual contents of the
# certificate with that name.
#
# @param name [String] certificate name to verify
#
# @raise [ArgumentError] if the certificate name cannot be found
# (i.e. doesn't exist or is unsigned)
# @raise [CertificateVerficationError] if the certificate has been revoked
#
# @return [Boolean] true if signed, there are no cases where false is returned
def verify(name)
unless cert = Puppet::SSL::Certificate.indirection.find(name)
raise ArgumentError, "Could not find a certificate for #{name}"
end
store = x509_store
raise CertificateVerificationError.new(store.error), store.error_string unless store.verify(cert.content)
end
def fingerprint(name, md = :SHA256)
unless cert = Puppet::SSL::Certificate.indirection.find(name) || Puppet::SSL::CertificateRequest.indirection.find(name)
raise ArgumentError, "Could not find a certificate or csr for #{name}"
end
cert.fingerprint(md)
end
# List the waiting certificate requests.
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|
+ Puppet::FileSystem.each_line(@config) 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/interface.rb b/lib/puppet/ssl/certificate_authority/interface.rb
index b68368b8d..20f4d87fd 100644
--- a/lib/puppet/ssl/certificate_authority/interface.rb
+++ b/lib/puppet/ssl/certificate_authority/interface.rb
@@ -1,177 +1,178 @@
module Puppet
module SSL
class CertificateAuthority
# 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, :reinventory]
SUBJECTLESS_METHODS = [:list, :reinventory]
class InterfaceError < ArgumentError; end
attr_reader :method, :subjects, :digest, :options
# Actually perform the work.
def apply(ca)
unless subjects || SUBJECTLESS_METHODS.include?(method)
raise ArgumentError, "You must provide hosts or --all when using #{method}"
end
# 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
def generate(ca)
raise InterfaceError, "It makes no sense to generate all hosts; you must specify a list" if subjects == :all
subjects.each do |host|
ca.generate(host, options)
end
end
def initialize(method, options)
self.method = method
self.subjects = options.delete(:to)
@digest = options.delete(:digest)
@options = options
end
# List the hosts.
def list(ca)
- signed = ca.list
+ signed = ca.list if [:signed, :all].include?(subjects)
requests = ca.waiting?
case subjects
when :all
hosts = [signed, requests].flatten
when :signed
hosts = signed.flatten
when nil
hosts = requests
else
hosts = subjects
+ signed = ca.list(hosts)
end
certs = {:signed => {}, :invalid => {}, :request => {}}
return if hosts.empty?
hosts.uniq.sort.each do |host|
begin
ca.verify(host) unless requests.include?(host)
rescue Puppet::SSL::CertificateAuthority::CertificateVerificationError => details
verify_error = details.to_s
end
if verify_error
certs[:invalid][host] = [ Puppet::SSL::Certificate.indirection.find(host), verify_error ]
- elsif signed.include?(host)
+ elsif (signed and signed.include?(host))
certs[:signed][host] = Puppet::SSL::Certificate.indirection.find(host)
else
certs[:request][host] = Puppet::SSL::CertificateRequest.indirection.find(host)
end
end
names = certs.values.map(&:keys).flatten
name_width = names.sort_by(&:length).last.length rescue 0
# We quote these names, so account for those characters
name_width += 2
output = [:request, :signed, :invalid].map do |type|
next if certs[type].empty?
certs[type].map do |host,info|
format_host(ca, host, type, info, name_width)
end
end.flatten.compact.sort.join("\n")
puts output
end
def format_host(ca, host, type, info, width)
cert, verify_error = info
alt_names = case type
when :signed
cert.subject_alt_names
when :request
cert.subject_alt_names
else
[]
end
alt_names.delete(host)
alt_str = "(alt names: #{alt_names.map(&:inspect).join(', ')})" unless alt_names.empty?
glyph = {:signed => '+', :request => ' ', :invalid => '-'}[type]
name = host.inspect.ljust(width)
fingerprint = cert.digest(@digest).to_s
explanation = "(#{verify_error})" if verify_error
[glyph, name, fingerprint, alt_str, explanation].compact.join(' ')
end
# Set the method to apply.
def method=(method)
raise ArgumentError, "Invalid method #{method} to apply" unless INTERFACE_METHODS.include?(method)
@method = method
end
# Print certificate information.
def print(ca)
(subjects == :all ? ca.list : subjects).each do |host|
if value = ca.print(host)
puts value
else
Puppet.err "Could not find certificate for #{host}"
end
end
end
# Print certificate information.
def fingerprint(ca)
(subjects == :all ? ca.list + ca.waiting?: subjects).each do |host|
if cert = (Puppet::SSL::Certificate.indirection.find(host) || Puppet::SSL::CertificateRequest.indirection.find(host))
puts "#{host} #{cert.digest(@digest)}"
else
Puppet.err "Could not find certificate for #{host}"
end
end
end
# 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?
list.each do |host|
ca.sign(host, options[:allow_dns_alt_names])
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 || value == :signed || value.is_a?(Array)
raise ArgumentError, "Subjects must be an array or :all; not #{value}"
end
@subjects = (value == []) ? nil : value
end
end
end
end
end
diff --git a/lib/puppet/ssl/certificate_factory.rb b/lib/puppet/ssl/certificate_factory.rb
index c6d01026c..124ce4b62 100644
--- a/lib/puppet/ssl/certificate_factory.rb
+++ b/lib/puppet/ssl/certificate_factory.rb
@@ -1,174 +1,219 @@
require 'puppet/ssl'
-# The tedious class that does all the manipulations to the
-# certificate to correctly sign it. Yay.
+# This class encapsulates the logic of creating and adding extensions to X509
+# certificates.
+#
+# @api private
module Puppet::SSL::CertificateFactory
+
+ # Create, add extensions to, and sign a new X509 certificate.
+ #
+ # @param cert_type [Symbol] The certificate type to create, which specifies
+ # what extensions are added to the certificate.
+ # One of (:ca, :terminalsubca, :server, :ocsp, :client)
+ # @param csr [OpenSSL::X509::Request] The signing request associated with
+ # the certificate being created.
+ # @param issuer [OpenSSL::X509::Certificate, OpenSSL::X509::Request] An X509 CSR
+ # if this is a self signed certificate, or the X509 certificate of the CA if
+ # this is a CA signed certificate.
+ # @param serial [Integer] The serial number for the given certificate, which
+ # MUST be unique for the given CA.
+ # @param ttl [String] The duration of the validity for the given certificate.
+ # defaults to Puppet[:ca_ttl]
+ #
+ # @api public
+ #
+ # @return [OpenSSL::X509::Certificate]
def self.build(cert_type, csr, issuer, serial, ttl = nil)
# Work out if we can even build the requested type of certificate.
build_extensions = "build_#{cert_type.to_s}_extensions"
respond_to?(build_extensions) or
raise ArgumentError, "#{cert_type.to_s} is an invalid certificate type!"
raise ArgumentError, "Certificate TTL must be an integer" unless ttl.nil? || ttl.is_a?(Fixnum)
# set up the certificate, and start building the content.
cert = OpenSSL::X509::Certificate.new
cert.version = 2 # X509v3
cert.subject = csr.content.subject
cert.issuer = issuer.subject
cert.public_key = csr.content.public_key
cert.serial = serial
# Make the certificate valid as of yesterday, because so many people's
# clocks are out of sync. This gives one more day of validity than people
# might expect, but is better than making every person who has a messed up
# clock fail, and better than having every cert we generate expire a day
# before the user expected it to when they asked for "one year".
cert.not_before = Time.now - (60*60*24)
cert.not_after = Time.now + (ttl || Puppet[:ca_ttl])
add_extensions_to(cert, csr, issuer, send(build_extensions))
return cert
end
private
+ # Add X509v3 extensions to the given certificate.
+ #
+ # @param cert [OpenSSL::X509::Certificate] The certificate to add the
+ # extensions to.
+ # @param csr [OpenSSL::X509::Request] The CSR associated with the given
+ # certificate, which may specify requested extensions for the given cert.
+ # See http://tools.ietf.org/html/rfc2985 Section 5.4.2 Extension request
+ # @param issuer [OpenSSL::X509::Certificate, OpenSSL::X509::Request] An X509 CSR
+ # if this is a self signed certificate, or the X509 certificate of the CA if
+ # this is a CA signed certificate.
+ # @param extensions [Hash<String, Array<String> | String>] The extensions to
+ # add to the certificate, based on the certificate type being created (CA,
+ # server, client, etc)
+ #
+ # @api private
+ #
+ # @return [void]
def self.add_extensions_to(cert, csr, issuer, extensions)
- ef = OpenSSL::X509::ExtensionFactory.
- new(cert, issuer.is_a?(OpenSSL::X509::Request) ? cert : issuer)
+ ef = OpenSSL::X509::ExtensionFactory.new
+ ef.subject_certificate = cert
+ ef.issuer_certificate = issuer.is_a?(OpenSSL::X509::Request) ? cert : issuer
# Extract the requested extensions from the CSR.
requested_exts = csr.request_extensions.inject({}) do |hash, re|
hash[re["oid"]] = [re["value"], re["critical"]]
hash
end
# Produce our final set of extensions. We deliberately order these to
# build the way we want:
# 1. "safe" default values, like the comment, that no one cares about.
# 2. request extensions, from the CSR
# 3. extensions based on the type we are generating
# 4. overrides, which we always want to have in their form
#
# This ordering *is* security-critical, but we want to allow the user
# enough rope to shoot themselves in the foot, if they want to ignore our
# advice and externally approve a CSR that sets the basicConstraints.
#
# Swapping the order of 2 and 3 would ensure that you couldn't slip a
# certificate through where the CA constraint was true, though, if
# something went wrong up there. --daniel 2011-10-11
defaults = { "nsComment" => "Puppet Ruby/OpenSSL Internal Certificate" }
- override = { "subjectKeyIdentifier" => "hash" }
+
+ # See http://www.openssl.org/docs/apps/x509v3_config.html
+ # for information about the special meanings of 'hash', 'keyid', 'issuer'
+ override = {
+ "subjectKeyIdentifier" => "hash",
+ "authorityKeyIdentifier" => "keyid,issuer"
+ }
exts = [defaults, requested_exts, extensions, override].
inject({}) {|ret, val| ret.merge(val) }
cert.extensions = exts.map do |oid, val|
generate_extension(ef, oid, *val)
end
end
# Woot! We're a CA.
def self.build_ca_extensions
{
# This was accidentally omitted in the previous version of this code: an
# effort was made to add it last, but that actually managed to avoid
# adding it to the certificate at all.
#
# We have some sort of bug, which means that when we add it we get a
# complaint that the issuer keyid can't be fetched, which breaks all
# sorts of things in our test suite and, e.g., bootstrapping the CA.
#
# http://tools.ietf.org/html/rfc5280#section-4.2.1.1 says that, to be a
# conforming CA we MAY omit the field if we are self-signed, which I
# think gives us a pass in the specific case.
#
# It also notes that we MAY derive the ID from the subject and serial
# number of the issuer, or from the key ID, and we definitely have the
# former data, should we want to restore this...
#
# Anyway, preserving this bug means we don't risk breaking anything in
# the field, even though it would be nice to have. --daniel 2011-10-11
#
# "authorityKeyIdentifier" => "keyid:always,issuer:always",
"keyUsage" => [%w{cRLSign keyCertSign}, true],
"basicConstraints" => ["CA:TRUE", true],
}
end
# We're a terminal CA, probably not self-signed.
def self.build_terminalsubca_extensions
{
"keyUsage" => [%w{cRLSign keyCertSign}, true],
"basicConstraints" => ["CA:TRUE,pathlen:0", true],
}
end
# We're a normal server.
def self.build_server_extensions
{
"keyUsage" => [%w{digitalSignature keyEncipherment}, true],
"extendedKeyUsage" => [%w{serverAuth clientAuth}, true],
"basicConstraints" => ["CA:FALSE", true],
}
end
# Um, no idea.
def self.build_ocsp_extensions
{
"keyUsage" => [%w{nonRepudiation digitalSignature}, true],
"extendedKeyUsage" => [%w{serverAuth OCSPSigning}, true],
"basicConstraints" => ["CA:FALSE", true],
}
end
# Normal client.
def self.build_client_extensions
{
"keyUsage" => [%w{nonRepudiation digitalSignature keyEncipherment}, true],
# We don't seem to use this, but that seems much more reasonable here...
"extendedKeyUsage" => [%w{clientAuth emailProtection}, true],
"basicConstraints" => ["CA:FALSE", true],
"nsCertType" => "client,email",
}
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 cd68eb34a..4afe01b0c 100644
--- a/lib/puppet/ssl/certificate_request.rb
+++ b/lib/puppet/ssl/certificate_request.rb
@@ -1,299 +1,299 @@
require 'puppet/ssl/base'
require 'puppet/ssl/certificate_signer'
# 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
extend Puppet::Indirector
# If auto-signing is on, sign any certificate requests as they are saved.
module AutoSigner
def save(instance, key = nil)
super
# Try to autosign the CSR.
if ca = Puppet::SSL::CertificateAuthority.instance
ca.autosign(instance)
end
end
end
indirects :certificate_request, :terminus_class => :file, :extend => AutoSigner, :doc => <<DOC
This indirection wraps an `OpenSSL::X509::Request` object, representing a certificate signing request (CSR).
The indirection key is the certificate CN (generally a hostname).
DOC
# Because of how the format handler class is included, this
# can't be in the base class.
def self.supported_formats
[:s]
end
def extension_factory
@ef ||= OpenSSL::X509::ExtensionFactory.new
end
# 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}"
# Support either an actual SSL key, or a Puppet key.
key = key.content if key.is_a?(Puppet::SSL::Key)
# If we're a CSR for the CA, then use the real ca_name, rather than the
# fake 'ca' name. This is mostly for backward compatibility with 0.24.x,
# but it's also just a good idea.
common_name = name == Puppet::SSL::CA_NAME ? Puppet.settings[:ca_name] : name
csr = OpenSSL::X509::Request.new
csr.version = 0
csr.subject = OpenSSL::X509::Name.new([["CN", common_name]])
csr.public_key = key.public_key
if options[:csr_attributes]
add_csr_attributes(csr, options[:csr_attributes])
end
if (ext_req_attribute = extension_request_attribute(options))
csr.add_attribute(ext_req_attribute)
end
signer = Puppet::SSL::CertificateSigner.new
signer.sign(csr, key)
raise Puppet::Error, "CSR sign verification failed; you need to clean the certificate request for #{name} on the server" unless csr.verify(key.public_key)
@content = csr
Puppet.info "Certificate Request fingerprint (#{digest.name}): #{digest.to_hex}"
@content
end
# Return the set of extensions requested on this CSR, in a form designed to
# 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.
attribute = @content.attributes.find {|x| x.oid == "extReq" }
attribute ||= @content.attributes.find {|x| x.oid == "msExtReq" }
return [] unless attribute
extensions = unpack_extension_request(attribute)
index = -1
extensions.map do |ext_values|
index += 1
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 ext_values.length
when 2
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(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 #{attribute.oid}, expected extension record #{index} to have two or three items, but found #{ext_values.length}"
end
end
end
def subject_alt_names
@subject_alt_names ||= request_extensions.
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}"
+ raise Puppet::Error, "Cannot create CSR with attribute #{oid}: #{e.message}", e.backtrace
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}"
+ raise Puppet::Error, "Cannot create CSR with extension request #{oid}: #{e.message}", e.backtrace
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
index e65b01443..9a2a4673a 100644
--- a/lib/puppet/ssl/certificate_request_attributes.rb
+++ b/lib/puppet/ssl/certificate_request_attributes.rb
@@ -1,37 +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)
+ if Puppet::FileSystem.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 e19534764..f686ed0a9 100644
--- a/lib/puppet/ssl/certificate_revocation_list.rb
+++ b/lib/puppet/ssl/certificate_revocation_list.rb
@@ -1,108 +1,110 @@
require 'puppet/ssl/base'
require 'puppet/indirector'
# Manage the CRL.
class Puppet::SSL::CertificateRevocationList < Puppet::SSL::Base
FIVE_YEARS = 5 * 365*24*60*60
wraps OpenSSL::X509::CRL
extend Puppet::Indirector
indirects :certificate_revocation_list, :terminus_class => :file, :doc => <<DOC
This indirection wraps an `OpenSSL::X509::CRL` object, representing a certificate revocation list (CRL).
The indirection key is the CA name (usually literally `ca`).
DOC
# Convert a string into an instance.
def self.from_s(string)
super(string, 'foo') # The name doesn't matter
end
# Because of how the format handler class is included, this
# can't be in the base class.
def self.supported_formats
[:s]
end
# Knows how to create a CRL with our system defaults.
def generate(cert, cakey)
Puppet.info "Creating a new certificate revocation list"
create_crl_issued_by(cert)
start_at_initial_crl_number
update_valid_time_range_to_start_at(Time.now)
sign_with(cakey)
@content
end
# The name doesn't actually matter; there's only one CRL.
# We just need the name so our Indirector stuff all works more easily.
def initialize(fakename)
@name = "crl"
end
# Revoke the certificate with serial number SERIAL issued by this
# CA, then write the CRL back to disk. The REASON must be one of the
# OpenSSL::OCSP::REVOKED_* reasons
def revoke(serial, cakey, reason = OpenSSL::OCSP::REVOKED_STATUS_KEYCOMPROMISE)
Puppet.notice "Revoked certificate with serial #{serial}"
time = Time.now
add_certificate_revocation_for(serial, reason, time)
update_to_next_crl_number
update_valid_time_range_to_start_at(time)
sign_with(cakey)
Puppet::SSL::CertificateRevocationList.indirection.save(self)
end
private
def create_crl_issued_by(cert)
+ ef = OpenSSL::X509::ExtensionFactory.new(cert)
@content = wrapped_class.new
@content.issuer = cert.subject
+ @content.add_extension(ef.create_ext("authorityKeyIdentifier", "keyid:always"))
@content.version = 1
end
def start_at_initial_crl_number
- @content.extensions = [crl_number_of(0)]
+ @content.add_extension(crl_number_of(0))
end
def add_certificate_revocation_for(serial, reason, time)
revoked = OpenSSL::X509::Revoked.new
revoked.serial = serial
revoked.time = time
enum = OpenSSL::ASN1::Enumerated(reason)
ext = OpenSSL::X509::Extension.new("CRLReason", enum)
revoked.add_extension(ext)
@content.add_revoked(revoked)
end
def update_valid_time_range_to_start_at(time)
# The CRL is not valid if the time of checking == the time of last_update.
# So to have it valid right now we need to say that it was updated one second ago.
@content.last_update = time - 1
@content.next_update = time + FIVE_YEARS
end
def update_to_next_crl_number
@content.extensions = with_next_crl_number_from(@content.extensions)
end
def with_next_crl_number_from(existing_extensions)
existing_crl_num = existing_extensions.find { |e| e.oid == 'crlNumber' }
new_crl_num = existing_crl_num ? existing_crl_num.value.to_i + 1 : 0
extensions_without_crl_num = existing_extensions.reject { |e| e.oid == 'crlNumber' }
extensions_without_crl_num + [crl_number_of(new_crl_num)]
end
def crl_number_of(number)
OpenSSL::X509::Extension.new('crlNumber', OpenSSL::ASN1::Integer(number))
end
def sign_with(cakey)
@content.sign(cakey, OpenSSL::Digest::SHA1.new)
end
end
diff --git a/lib/puppet/ssl/host.rb b/lib/puppet/ssl/host.rb
index f30b4dee7..e78ca9a61 100644
--- a/lib/puppet/ssl/host.rb
+++ b/lib/puppet/ssl/host.rb
@@ -1,370 +1,371 @@
require 'puppet/indirector'
require 'puppet/ssl'
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.
class Puppet::SSL::Host
# Yay, ruby's strange constant lookups.
Key = Puppet::SSL::Key
CA_NAME = Puppet::SSL::CA_NAME
Certificate = Puppet::SSL::Certificate
CertificateRequest = Puppet::SSL::CertificateRequest
CertificateRevocationList = Puppet::SSL::CertificateRevocationList
extend Puppet::Indirector
indirects :certificate_status, :terminus_class => :file, :doc => <<DOC
This indirection represents the host that ties a key, certificate, and certificate request together.
The indirection key is the certificate CN (generally a hostname).
DOC
attr_reader :name
attr_accessor :ca
attr_writer :key, :certificate, :certificate_request
# This accessor is used in instances for indirector requests to hold desired state
attr_accessor :desired_state
def self.localhost
return @localhost if @localhost
@localhost = new
@localhost.generate unless @localhost.certificate
@localhost.key
@localhost
end
def self.reset
@localhost = nil
end
# This is the constant that people will use to mark that a given host is
# a certificate authority.
def self.ca_name
CA_NAME
end
class << self
attr_reader :ca_location
end
# Configure how our various classes interact with their various terminuses.
def self.configure_indirection(terminus, cache = nil)
Certificate.indirection.terminus_class = terminus
CertificateRequest.indirection.terminus_class = terminus
CertificateRevocationList.indirection.terminus_class = terminus
host_map = {:ca => :file, :disabled_ca => nil, :file => nil, :rest => :rest}
if term = host_map[terminus]
self.indirection.terminus_class = term
else
self.indirection.reset_terminus_class
end
if cache
# This is weird; we don't actually cache our keys, we
# use what would otherwise be the cache as our normal
# terminus.
Key.indirection.terminus_class = cache
else
Key.indirection.terminus_class = terminus
end
if cache
Certificate.indirection.cache_class = cache
CertificateRequest.indirection.cache_class = cache
CertificateRevocationList.indirection.cache_class = cache
else
# Make sure we have no cache configured. puppet master
# switches the configurations around a bit, so it's important
# that we specify the configs for absolutely everything, every
# time.
Certificate.indirection.cache_class = nil
CertificateRequest.indirection.cache_class = nil
CertificateRevocationList.indirection.cache_class = nil
end
end
CA_MODES = {
# Our ca is local, so we use it as the ultimate source of information
# And we cache files locally.
:local => [:ca, :file],
# We're a remote CA client.
:remote => [:rest, :file],
# We are the CA, so we don't have read/write access to the normal certificates.
:only => [:ca],
# We have no CA, so we just look in the local file store.
:none => [:disabled_ca]
}
# Specify how we expect to interact with our certificate authority.
def self.ca_location=(mode)
modes = CA_MODES.collect { |m, vals| m.to_s }.join(", ")
raise ArgumentError, "CA Mode can only be one of: #{modes}" unless CA_MODES.include?(mode)
@ca_location = mode
configure_indirection(*CA_MODES[@ca_location])
end
# Puppet::SSL::Host is actually indirected now so the original implementation
# has been moved into the certificate_status indirector. This method is in-use
# in `puppet cert -c <certname>`.
def self.destroy(name)
indirection.destroy(name)
end
- def self.from_pson(pson)
- instance = new(pson["name"])
- if pson["desired_state"]
- instance.desired_state = pson["desired_state"]
+ def self.from_data_hash(data)
+ instance = new(data["name"])
+ if data["desired_state"]
+ instance.desired_state = data["desired_state"]
end
instance
end
+ def self.from_pson(pson)
+ Puppet.deprecation_warning("from_pson is being removed in favour of from_data_hash.")
+ self.from_data_hash(pson)
+ end
+
# Puppet::SSL::Host is actually indirected now so the original implementation
# has been moved into the certificate_status indirector. This method does not
# appear to be in use in `puppet cert -l`.
def self.search(options = {})
indirection.search("*", options)
end
# Is this a ca host, meaning that all of its files go in the CA location?
def ca?
ca
end
def key
@key ||= Key.indirection.find(name)
end
# This is the private key; we can create it from scratch
# with no inputs.
def generate_key
@key = Key.new(name)
@key.generate
begin
Key.indirection.save(@key)
rescue
@key = nil
raise
end
true
end
def certificate_request
@certificate_request ||= CertificateRequest.indirection.find(name)
end
# Our certificate request requires the key but that's all.
def generate_certificate_request(options = {})
generate_key unless key
# If this CSR is for the current machine...
if name == Puppet[:certname].downcase
# ...add our configured dns_alt_names
if Puppet[:dns_alt_names] and Puppet[:dns_alt_names] != ''
options[:dns_alt_names] ||= Puppet[:dns_alt_names]
elsif Puppet::SSL::CertificateAuthority.ca? and fqdn = Facter.value(:fqdn) and domain = Facter.value(:domain)
options[:dns_alt_names] = "puppet, #{fqdn}, puppet.#{domain}"
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
CertificateRequest.indirection.save(@certificate_request)
rescue
@certificate_request = nil
raise
end
true
end
def certificate
unless @certificate
generate_key unless key
# get the CA cert first, since it's required for the normal cert
# to be of any use.
return nil unless Certificate.indirection.find("ca") unless ca?
return nil unless @certificate = Certificate.indirection.find(name)
validate_certificate_with_key
end
@certificate
end
def validate_certificate_with_key
raise Puppet::Error, "No certificate to validate." unless certificate
raise Puppet::Error, "No private key with which to validate certificate with fingerprint: #{certificate.fingerprint}" unless key
unless certificate.content.check_private_key(key.content)
raise Puppet::Error, <<ERROR_STRING
The certificate retrieved from the master does not match the agent's private key.
Certificate fingerprint: #{certificate.fingerprint}
To fix this, remove the certificate from both the master and the agent and then start a puppet run, which will automatically regenerate a certficate.
On the master:
puppet cert clean #{Puppet[:certname]}
On the agent:
rm -f #{Puppet[:hostcert]}
puppet agent -t
ERROR_STRING
end
end
# Generate all necessary parts of our ssl host.
def generate
generate_key unless key
generate_certificate_request unless certificate_request
# If we can get a CA instance, then we're a valid CA, and we
# should use it to sign our request; else, just try to read
# the cert.
if ! certificate and ca = Puppet::SSL::CertificateAuthority.instance
ca.sign(self.name, true)
end
end
def initialize(name = nil)
@name = (name || Puppet[:certname]).downcase
Puppet::SSL::Base.validate_certname(@name)
@key = @certificate = @certificate_request = nil
@ca = (name == self.class.ca_name)
end
# Extract the public key from the private key.
def public_key
key.content.public_key
end
# Create/return a store that uses our SSL info to validate
# connections.
def ssl_store(purpose = OpenSSL::X509::PURPOSE_ANY)
unless @ssl_store
@ssl_store = OpenSSL::X509::Store.new
@ssl_store.purpose = purpose
# Use the file path here, because we don't want to cause
# a lookup in the middle of setting our ssl connection.
@ssl_store.add_file(Puppet[:localcacert])
# If we're doing revocation and there's a CRL, add it to our store.
if Puppet.settings[:certificate_revocation]
if crl = Puppet::SSL::CertificateRevocationList.indirection.find(CA_NAME)
@ssl_store.flags = OpenSSL::X509::V_FLAG_CRL_CHECK_ALL|OpenSSL::X509::V_FLAG_CRL_CHECK
@ssl_store.add_crl(crl.content)
end
end
return @ssl_store
end
@ssl_store
end
def to_data_hash
my_cert = Puppet::SSL::Certificate.indirection.find(name)
result = { :name => name }
my_state = state
result[:state] = my_state
result[:desired_state] = desired_state if desired_state
thing_to_use = (my_state == 'requested') ? certificate_request : my_cert
# this is for backwards-compatibility
# we should deprecate it and transition people to using
# pson[:fingerprints][:default]
# It appears that we have no internal consumers of this api
# --jeffweiss 30 aug 2012
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
# we move to FIPS 140-2 compliance, MD5 is no longer allowed (and, gasp, will
# segfault in rubies older than 1.9.3)
# So, when we add the newer fingerprints, we're explicit about the hashing
# algorithm used.
# --jeffweiss 31 july 2012
result[:fingerprints] = {}
result[:fingerprints][:default] = thing_to_use.fingerprint
suitable_message_digest_algorithms.each do |md|
result[:fingerprints][md] = thing_to_use.fingerprint md
end
result[:dns_alt_names] = thing_to_use.subject_alt_names
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
# configurable
# --jeffweiss 29 aug 2012
def suitable_message_digest_algorithms
[:SHA1, :SHA256, :SHA512]
end
# Attempt to retrieve a cert, if we don't already have one.
def wait_for_cert(time)
begin
return if certificate
generate
return if certificate
rescue SystemExit,NoMemoryError
raise
rescue Exception => detail
Puppet.log_exception(detail, "Could not request certificate: #{detail.message}")
if time < 1
puts "Exiting; failed to retrieve certificate and waitforcert is disabled"
exit(1)
else
sleep(time)
end
retry
end
if time < 1
puts "Exiting; no certificate found and waitforcert is disabled"
exit(1)
end
while true
sleep time
begin
break if certificate
Puppet.notice "Did not receive certificate"
rescue StandardError => detail
Puppet.log_exception(detail, "Could not request certificate: #{detail.message}")
end
end
end
def state
if certificate_request
return 'requested'
end
begin
Puppet::SSL::CertificateAuthority.new.verify(name)
return 'signed'
rescue Puppet::SSL::CertificateAuthority::CertificateVerificationError
return 'revoked'
end
end
end
require 'puppet/ssl/certificate_authority'
diff --git a/lib/puppet/ssl/inventory.rb b/lib/puppet/ssl/inventory.rb
index 3aae02a65..e3ad3121f 100644
--- a/lib/puppet/ssl/inventory.rb
+++ b/lib/puppet/ssl/inventory.rb
@@ -1,50 +1,50 @@
require 'puppet/ssl'
require 'puppet/ssl/certificate'
# Keep track of all of our known certificates.
class Puppet::SSL::Inventory
attr_reader :path
# Add a certificate to our inventory.
def add(cert)
cert = cert.content if cert.is_a?(Puppet::SSL::Certificate)
Puppet.settings.setting(:cert_inventory).open("a") do |f|
f.print format(cert)
end
end
# Format our certificate for output.
def format(cert)
iso = '%Y-%m-%dT%H:%M:%S%Z'
"0x%04x %s %s %s\n" % [cert.serial, cert.not_before.strftime(iso), cert.not_after.strftime(iso), cert.subject]
end
def initialize
@path = Puppet[:cert_inventory]
end
# Rebuild the inventory from scratch. This should happen if
# the file is entirely missing or if it's somehow corrupted.
def rebuild
Puppet.notice "Rebuilding inventory file"
Puppet.settings.setting(:cert_inventory).open('w') do |f|
Puppet::SSL::Certificate.indirection.search("*").each do |cert|
f.print format(cert.content)
end
end
end
# Find the serial number for a given certificate.
def serial(name)
- return nil unless Puppet::FileSystem::File.exist?(@path)
+ return nil unless Puppet::FileSystem.exist?(@path)
File.readlines(@path).each do |line|
next unless line =~ /^(\S+).+\/CN=#{name}$/
return Integer($1)
end
return nil
end
end
diff --git a/lib/puppet/ssl/key.rb b/lib/puppet/ssl/key.rb
index b64fde544..f8440b0a8 100644
--- a/lib/puppet/ssl/key.rb
+++ b/lib/puppet/ssl/key.rb
@@ -1,59 +1,59 @@
require 'puppet/ssl/base'
require 'puppet/indirector'
# Manage private and public keys as a pair.
class Puppet::SSL::Key < Puppet::SSL::Base
wraps OpenSSL::PKey::RSA
extend Puppet::Indirector
indirects :key, :terminus_class => :file, :doc => <<DOC
This indirection wraps an `OpenSSL::PKey::RSA object, representing a private key.
The indirection key is the certificate CN (generally a hostname).
DOC
# Because of how the format handler class is included, this
# can't be in the base class.
def self.supported_formats
[:s]
end
attr_accessor :password_file
# Knows how to create keys with our system defaults.
def generate
Puppet.info "Creating a new SSL key for #{name}"
@content = OpenSSL::PKey::RSA.new(Puppet[:keylength].to_i)
end
def initialize(name)
super
if ca?
@password_file = Puppet[:capass]
else
@password_file = Puppet[:passfile]
end
end
def password
- return nil unless password_file and Puppet::FileSystem::File.exist?(password_file)
+ return nil unless password_file and Puppet::FileSystem.exist?(password_file)
::File.read(password_file)
end
# Optionally support specifying a password file.
def read(path)
return super unless password_file
#@content = wrapped_class.new(::File.read(path), password)
@content = wrapped_class.new(::File.read(path), password)
end
def to_s
if pass = password
@content.export(OpenSSL::Cipher::DES.new(:EDE3, :CBC), pass)
else
return super
end
end
end
diff --git a/lib/puppet/ssl/validator/default_validator.rb b/lib/puppet/ssl/validator/default_validator.rb
index 1f238f4c4..3cb8a0f02 100644
--- a/lib/puppet/ssl/validator/default_validator.rb
+++ b/lib/puppet/ssl/validator/default_validator.rb
@@ -1,153 +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)
+ Puppet::FileSystem.exist?(Puppet[:hostcert]) && Puppet::FileSystem.exist?(@ssl_configuration.ca_auth_file)
end
end
diff --git a/lib/puppet/status.rb b/lib/puppet/status.rb
index 0b26ae22e..7fe048146 100644
--- a/lib/puppet/status.rb
+++ b/lib/puppet/status.rb
@@ -1,44 +1,45 @@
require 'puppet/indirector'
class Puppet::Status
extend Puppet::Indirector
indirects :status, :terminus_class => :local
attr :status, true
def initialize( status = nil )
@status = status || {"is_alive" => true}
end
def to_data_hash
@status
end
- def to_pson(*args)
- @status.to_pson
+ def self.from_data_hash(data)
+ if data.include?('status')
+ self.new(data['status'])
+ else
+ self.new(data)
+ end
end
def self.from_pson(pson)
- if pson.include?('status')
- self.new(pson['status'])
- else
- self.new(pson)
- end
+ Puppet.deprecation_warning("from_pson is being removed in favour of from_data_hash.")
+ self.from_data_hash(pson)
end
def name
"status"
end
def name=(name)
# NOOP
end
def version
@status['version']
end
def version=(version)
@status['version'] = version
end
end
diff --git a/lib/puppet/test/test_helper.rb b/lib/puppet/test/test_helper.rb
index f80e699e7..5c083715a 100644
--- a/lib/puppet/test/test_helper.rb
+++ b/lib/puppet/test/test_helper.rb
@@ -1,185 +1,216 @@
require 'puppet/indirector/data_binding/hiera'
+require 'tmpdir'
+require 'fileutils'
+
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
# allow us to vary the implementation details of managing puppet's state
# for testing, from one version of puppet to the next--without forcing
# the external projects to do any of that state management or be aware of
# the implementation details.
#
# For now, this consists of a few very simple signatures. The plan is
# that it should be the responsibility of the puppetlabs_spec_helper
# to broker between external projects and this API; thus, if any
# hacks are required (e.g. to determine whether or not a particular)
# version of puppet supports this API, those hacks will be consolidated in
# one place and won't need to be duplicated in every external project.
#
# This should also alleviate the anti-pattern that we've been following,
# wherein each external project starts off with a copy of puppet core's
# test_helper.rb and is exposed to risk of that code getting out of
# sync with core.
#
# Since this class will be "library code" that ships with puppet, it does
# not use API from any existing test framework such as rspec. This should
# theoretically allow it to be used with other unit test frameworks in the
# future, if desired.
#
# Note that in the future this API could potentially be expanded to handle
# other features such as "around_test", but we didn't see a compelling
# reason to deal with that right now.
class TestHelper
# Call this method once, as early as possible, such as before loading tests
# that call Puppet.
# @return nil
def self.initialize()
- initialize_settings_before_each
+ owner = Process.pid
+ @environmentdir = Dir.mktmpdir('environments')
+ Puppet.push_context(Puppet.base_context({
+ :environmentpath => @environmentdir,
+ :basemodulepath => "",
+ :manifest => "/dev/null"
+ }), "Initial for specs")
+ Puppet::Parser::Functions.reset
+
+ ObjectSpace.define_finalizer(Puppet.lookup(:environments), proc {
+ if Process.pid == owner
+ FileUtils.rm_rf(@environmentdir)
+ end
+ })
end
# Call this method once, when beginning a test run--prior to running
# any individual tests.
# @return nil
def self.before_all_tests()
-
+ # Make sure that all of the setup is also done for any before(:all) blocks
end
# Call this method once, at the end of a test run, when no more tests
# will be run.
# @return nil
def self.after_all_tests()
-
end
# Call this method once per test, prior to execution of each invididual test.
# @return nil
def self.before_each_test()
+
# We need to preserve the current state of all our indirection cache and
# terminus classes. This is pretty important, because changes to these
# are global and lead to order dependencies in our testing.
#
# We go direct to the implementation because there is no safe, sane public
# API to manage restoration of these to their default values. This
# should, once the value is proved, be moved to a standard API on the
# indirector.
#
# To make things worse, a number of the tests stub parts of the
# indirector. These stubs have very specific expectations that what
# little of the public API we could use is, well, likely to explode
# randomly in some tests. So, direct access. --daniel 2011-08-30
$saved_indirection_state = {}
indirections = Puppet::Indirector::Indirection.send(:class_variable_get, :@@indirections)
indirections.each do |indirector|
$saved_indirection_state[indirector.name] = {
:@terminus_class => indirector.instance_variable_get(:@terminus_class),
:@cache_class => indirector.instance_variable_get(:@cache_class)
}
end
# The process environment is a shared, persistent resource.
$old_env = ENV.to_hash
# So is the load_path
$old_load_path = $LOAD_PATH.dup
initialize_settings_before_each()
- Puppet::Node::Environment.clear
+ Puppet.push_context(
+ {
+ :trusted_information =>
+ Puppet::Context::TrustedInformation.new('local', 'testing', {}),
+ },
+ "Context for specs")
+
Puppet::Parser::Functions.reset
+ Puppet::Node::Environment.clear
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.
# @return nil
def self.after_each_test()
Puppet.settings.send(:clear_everything_for_tests)
Puppet::Util::Storage.clear
Puppet::Util::ExecutionStub.reset
Puppet.clear_deprecation_warnings
# uncommenting and manipulating this can be useful when tracking down calls to deprecated code
#Puppet.log_deprecations_to_file("deprecations.txt", /^Puppet::Util.exec/)
# Restore the indirector configuration. See before hook.
indirections = Puppet::Indirector::Indirection.send(:class_variable_get, :@@indirections)
indirections.each do |indirector|
$saved_indirection_state.fetch(indirector.name, {}).each do |variable, value|
indirector.instance_variable_set(variable, value)
end
end
$saved_indirection_state = nil
# Restore the global process environment. Can't just assign because this
# is a magic variable, sadly, and doesn't do that™. It is sufficiently
# faster to use the compare-then-set model to avoid excessive work that it
# justifies the complexity. --daniel 2012-03-15
unless ENV.to_hash == $old_env
ENV.clear
$old_env.each {|k, v| ENV[k] = v }
end
# Some tests can cause us to connect, in which case the lingering
# connection is a resource that can cause unexpected failure in later
# tests, as well as sharing state accidentally.
# We're testing if ActiveRecord::Base is defined because some test cases
# may stub Puppet.features.rails? which is how we should normally
# introspect for this functionality.
ActiveRecord::Base.remove_connection if defined?(ActiveRecord::Base)
# Restore the load_path late, to avoid messing with stubs from the test.
$LOAD_PATH.clear
$old_load_path.each {|x| $LOAD_PATH << x }
+ Puppet.pop_context
end
#########################################################################################
# PRIVATE METHODS (not part of the public TestHelper API--do not call these from outside
# of this class!)
#########################################################################################
def self.app_defaults_for_tests()
{
:logdir => "/dev/null",
:confdir => "/dev/null",
:vardir => "/dev/null",
:rundir => "/dev/null",
:hiera_config => "/dev/null",
}
end
private_class_method :app_defaults_for_tests
def self.initialize_settings_before_each()
Puppet.settings.preferred_run_mode = "user"
# Initialize "app defaults" settings to a good set of test values
Puppet.settings.initialize_app_defaults(app_defaults_for_tests)
# Avoid opening ports to the outside world
Puppet.settings[:bindaddress] = "127.0.0.1"
# We don't want to depend upon the reported domain name of the
# machine running the tests, nor upon the DNS setup of that
# domain.
Puppet.settings[:use_srv_records] = false
# Longer keys are secure, but they sure make for some slow testing - both
# in terms of generating keys, and in terms of anything the next step down
# the line doing validation or whatever. Most tests don't care how long
# or secure it is, just that it exists, so these are better and faster
# defaults, in testing only.
#
# I would make these even shorter, but OpenSSL doesn't support anything
# below 512 bits. Sad, really, because a 0 bit key would be just fine.
Puppet[:req_bits] = 512
Puppet[:keylength] = 512
+
+ # Although we setup a testing context during initialization, some tests
+ # will end up creating their own context using the real context objects
+ # and use the setting for the environments. In order to avoid those tests
+ # having to deal with a missing environmentpath we can just set it right
+ # here.
+ Puppet[:environmentpath] = @environmentpath
end
private_class_method :initialize_settings_before_each
end
end
diff --git a/lib/puppet/transaction/event.rb b/lib/puppet/transaction/event.rb
index ca4255937..fc635a708 100644
--- a/lib/puppet/transaction/event.rb
+++ b/lib/puppet/transaction/event.rb
@@ -1,106 +1,107 @@
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_accessor :time
attr_reader :default_log_level
EVENT_STATUSES = %w{noop success failure audit}
- def self.from_pson(data)
+ def self.from_data_hash(data)
obj = self.allocate
obj.initialize_from_hash(data)
obj
end
+ def self.from_pson(data)
+ Puppet.deprecation_warning("from_pson is being removed in favour of from_data_hash.")
+ self.from_data_hash(data)
+ end
+
def initialize(options = {})
@audited = false
set_options(options)
@time = Time.now
end
def initialize_from_hash(data)
@audited = data['audited']
@property = data['property']
@previous_value = data['previous_value']
@desired_value = data['desired_value']
@historical_value = data['historical_value']
@message = data['message']
@name = data['name'].intern if data['name']
@status = data['status']
@time = data['time']
@time = Time.parse(@time) if @time.is_a? String
end
def to_data_hash
{
'audited' => @audited,
'property' => @property,
'previous_value' => @previous_value,
'desired_value' => @desired_value,
'historical_value' => @historical_value,
'message' => @message,
'name' => @name,
'status' => @status,
'time' => @time.iso8601(9),
}
end
- def to_pson(*args)
- to_data_hash.to_pson(*args)
- end
-
def property=(prop)
@property = prop.to_s
end
def resource=(res)
if res.respond_to?(:[]) and level = res[:loglevel]
@default_log_level = level
end
@resource = res.to_s
end
def send_log
super(log_level, message)
end
def status=(value)
raise ArgumentError, "Event status can only be #{EVENT_STATUSES.join(', ')}" unless EVENT_STATUSES.include?(value)
@status = value
end
def to_s
message
end
def to_yaml_properties
YAML_ATTRIBUTES & super
end
private
# If it's a failure, use 'err', else use either the resource's log level (if available)
# or 'notice'.
def log_level
status == "failure" ? :err : (@default_log_level || :notice)
end
# Used by the Logging module
def log_source
source_description || property || resource
end
end
diff --git a/lib/puppet/transaction/report.rb b/lib/puppet/transaction/report.rb
index 0469965cd..05306547e 100644
--- a/lib/puppet/transaction/report.rb
+++ b/lib/puppet/transaction/report.rb
@@ -1,383 +1,383 @@
require 'puppet'
require 'puppet/indirector'
# This class is used to report what happens on a client.
# There are two types of data in a report; _Logs_ and _Metrics_.
#
# * **Logs** - are the output that each change produces.
# * **Metrics** - are all of the numerical data involved in the transaction.
#
# Use {Puppet::Reports} class to create a new custom report type. This class is indirectly used
# as a source of data to report in such a registered report.
#
# ##Metrics
# There are three types of metrics in each report, and each type of metric has one or more values.
#
# * Time: Keeps track of how long things took.
# * Total: Total time for the configuration run
# * File:
# * Exec:
# * User:
# * Group:
# * Config Retrieval: How long the configuration took to retrieve
# * Service:
# * Package:
# * Resources: Keeps track of the following stats:
# * Total: The total number of resources being managed
# * Skipped: How many resources were skipped, because of either tagging or scheduling restrictions
# * Scheduled: How many resources met any scheduling restrictions
# * Out of Sync: How many resources were out of sync
# * Applied: How many resources were attempted to be fixed
# * Failed: How many resources were not successfully fixed
# * Restarted: How many resources were restarted because their dependencies changed
# * Failed Restarts: How many resources could not be restarted
# * Changes: The total number of changes in the transaction.
#
# @api public
class Puppet::Transaction::Report
extend Puppet::Indirector
indirects :report, :terminus_class => :processor
# The version of the configuration
# @todo Uncertain what this is?
# @return [???] the configuration version
attr_accessor :configuration_version
# An agent generated transaction uuid, useful for connecting catalog and report
# @return [String] uuid
attr_accessor :transaction_uuid
# The host name for which the report is generated
# @return [String] the host name
attr_accessor :host
# The name of the environment the host is in
# @return [String] the environment name
attr_accessor :environment
# A hash with a map from resource to status
- # @return [Hash<{String => String}>] Resource name to status string.
- # @todo Uncertain if the types in the hash are correct...
+ # @return [Hash{String => Puppet::Resource::Status}] Resource name to status.
attr_reader :resource_statuses
# A list of log messages.
# @return [Array<Puppet::Util::Log>] logged messages
attr_reader :logs
# A hash of metric name to metric value.
# @return [Hash<{String => Object}>] A map of metric name to value.
# @todo Uncertain if all values are numbers - now marked as Object.
#
attr_reader :metrics
# The time when the report data was generated.
# @return [Time] A time object indicating when the report data was generated
#
attr_reader :time
# The 'kind' of report is the name of operation that triggered the report to be produced.
# Typically "apply".
# @return [String] the kind of operation that triggered the generation of the report.
#
attr_reader :kind
# The status of the client run is an enumeration: 'failed', 'changed' or 'unchanged'
# @return [String] the status of the run - one of the values 'failed', 'changed', or 'unchanged'
#
attr_reader :status
# @return [String] The Puppet version in String form.
# @see Puppet::version()
#
attr_reader :puppet_version
# @return [Integer] report format version number. This value is constant for
# a given version of Puppet; it is incremented when a new release of Puppet
# changes the API for the various objects that make up a report.
#
attr_reader :report_format
- def self.from_pson(data)
+ def self.from_data_hash(data)
obj = self.allocate
obj.initialize_from_hash(data)
obj
end
+ def self.from_pson(data)
+ Puppet.deprecation_warning("from_pson is being removed in favour of from_data_hash.")
+ self.from_data_hash(data)
+ end
+
def as_logging_destination(&block)
Puppet::Util::Log.with_destination(self, &block)
end
# @api private
def <<(msg)
@logs << msg
self
end
# @api private
def add_times(name, value)
@external_times[name] = value
end
# @api private
def add_metric(name, hash)
metric = Puppet::Util::Metric.new(name)
hash.each do |name, value|
metric.newvalue(name, value)
end
@metrics[metric.name] = metric
metric
end
# @api private
def add_resource_status(status)
@resource_statuses[status.resource] = status
end
# @api private
def compute_status(resource_metrics, change_metric)
if (resource_metrics["failed"] || 0) > 0
'failed'
elsif change_metric > 0
'changed'
else
'unchanged'
end
end
# @api private
def prune_internal_data
resource_statuses.delete_if {|name,res| res.resource_type == 'Whit'}
end
# @api private
def finalize_report
prune_internal_data
resource_metrics = add_metric(:resources, calculate_resource_metrics)
add_metric(:time, calculate_time_metrics)
change_metric = calculate_change_metric
add_metric(:changes, {"total" => change_metric})
add_metric(:events, calculate_event_metrics)
@status = compute_status(resource_metrics, change_metric)
end
# @api private
def initialize(kind, configuration_version=nil, environment=nil, transaction_uuid=nil)
@metrics = {}
@logs = []
@resource_statuses = {}
@external_times ||= {}
@host = Puppet[:node_name_value]
@time = Time.now
@kind = kind
@report_format = 4
@puppet_version = Puppet.version
@configuration_version = configuration_version
@transaction_uuid = transaction_uuid
@environment = environment
@status = 'failed' # assume failed until the report is finalized
end
# @api private
def initialize_from_hash(data)
@puppet_version = data['puppet_version']
@report_format = data['report_format']
@configuration_version = data['configuration_version']
@transaction_uuid = data['transaction_uuid']
@environment = data['environment']
@status = data['status']
@host = data['host']
@time = data['time']
if @time.is_a? String
@time = Time.parse(@time)
end
@kind = data['kind']
@metrics = {}
data['metrics'].each do |name, hash|
- @metrics[name] = Puppet::Util::Metric.from_pson(hash)
+ @metrics[name] = Puppet::Util::Metric.from_data_hash(hash)
end
@logs = data['logs'].map do |record|
- Puppet::Util::Log.from_pson(record)
+ Puppet::Util::Log.from_data_hash(record)
end
@resource_statuses = {}
data['resource_statuses'].map do |record|
if record[1] == {}
status = nil
else
- status = Puppet::Resource::Status.from_pson(record[1])
+ status = Puppet::Resource::Status.from_data_hash(record[1])
end
@resource_statuses[record[0]] = status
end
end
def to_data_hash
{
'host' => @host,
'time' => @time.iso8601(9),
'configuration_version' => @configuration_version,
'transaction_uuid' => @transaction_uuid,
'report_format' => @report_format,
'puppet_version' => @puppet_version,
'kind' => @kind,
'status' => @status,
'environment' => @environment,
'logs' => @logs,
'metrics' => @metrics,
'resource_statuses' => @resource_statuses,
}
end
- def to_pson
- to_data_hash.to_pson
- end
-
# @return [String] the host name
# @api public
#
def name
host
end
# Provide a human readable textual summary of this report.
# @note This is intended for debugging purposes
# @return [String] A string with a textual summary of this report.
# @api public
#
def summary
report = raw_summary
ret = ""
report.keys.sort { |a,b| a.to_s <=> b.to_s }.each do |key|
ret += "#{Puppet::Util::Metric.labelize(key)}:\n"
report[key].keys.sort { |a,b|
# sort by label
if a == :total
1
elsif b == :total
-1
else
report[key][a].to_s <=> report[key][b].to_s
end
}.each do |label|
value = report[key][label]
next if value == 0
value = "%0.2f" % value if value.is_a?(Float)
ret += " %15s %s\n" % [Puppet::Util::Metric.labelize(label) + ":", value]
end
end
ret
end
# Provides a raw hash summary of this report.
# @return [Hash<{String => Object}>] A hash with metrics key to value map
# @api public
#
def raw_summary
report = { "version" => { "config" => configuration_version, "puppet" => Puppet.version } }
@metrics.each do |name, metric|
key = metric.name.to_s
report[key] = {}
metric.values.each do |name, label, value|
report[key][name.to_s] = value
end
report[key]["total"] = 0 unless key == "time" or report[key].include?("total")
end
(report["time"] ||= {})["last_run"] = Time.now.tv_sec
report
end
# Computes a single number that represents the report's status.
# The computation is based on the contents of this report's metrics.
# The resulting number is a bitmask where
# individual bits represent the presence of different metrics.
#
# * 0x2 set if there are changes
# * 0x4 set if there are resource failures or resources that failed to restart
# @return [Integer] A bitmask where 0x2 is set if there are changes, and 0x4 is set of there are failures.
# @api public
#
def exit_status
status = 0
status |= 2 if @metrics["changes"]["total"] > 0
status |= 4 if @metrics["resources"]["failed"] > 0
status |= 4 if @metrics["resources"]["failed_to_restart"] > 0
status
end
# @api private
#
def to_yaml_properties
super - [:@external_times]
end
def self.supported_formats
[:pson, :yaml]
end
def self.default_format
Puppet[:report_serialization_format].intern
end
private
def calculate_change_metric
resource_statuses.map { |name, status| status.change_count || 0 }.inject(0) { |a,b| a+b }
end
def calculate_event_metrics
metrics = Hash.new(0)
%w{total failure success}.each { |m| metrics[m] = 0 }
resource_statuses.each do |name, status|
metrics["total"] += status.events.length
status.events.each do |event|
metrics[event.status] += 1
end
end
metrics
end
def calculate_resource_metrics
metrics = {}
metrics["total"] = resource_statuses.length
# force every resource key in the report to be present
# even if no resources is in this given state
Puppet::Resource::Status::STATES.each do |state|
metrics[state.to_s] = 0
end
resource_statuses.each do |name, status|
Puppet::Resource::Status::STATES.each do |state|
metrics[state.to_s] += 1 if status.send(state)
end
end
metrics
end
def calculate_time_metrics
metrics = Hash.new(0)
resource_statuses.each do |name, status|
type = Puppet::Resource.new(name).type
metrics[type.to_s.downcase] += status.evaluation_time if status.evaluation_time
end
@external_times.each do |name, value|
metrics[name.to_s.downcase] = value
end
metrics["total"] = metrics.values.inject(0) { |a,b| a+b }
metrics
end
end
diff --git a/lib/puppet/type.rb b/lib/puppet/type.rb
index 5cdee64b6..f382f30ca 100644
--- a/lib/puppet/type.rb
+++ b/lib/puppet/type.rb
@@ -1,2432 +1,2430 @@
require 'puppet'
require 'puppet/util/log'
require 'puppet/util/metric'
require 'puppet/property'
require 'puppet/parameter'
require 'puppet/util'
require 'puppet/util/autoload'
require 'puppet/metatype/manager'
require 'puppet/util/errors'
require 'puppet/util/logging'
require 'puppet/util/tagging'
# see the bottom of the file for the rest of the inclusions
module Puppet
# The base class for all Puppet types.
#
# A type describes:
#--
# * **Attributes** - properties, parameters, and meta-parameters are different types of attributes of a type.
# * **Properties** - these are the properties of the managed resource (attributes of the entity being managed; like
# a file's owner, group and mode). A property describes two states; the 'is' (current state) and the 'should' (wanted
# state).
# * **Ensurable** - a set of traits that control the lifecycle (create, remove, etc.) of a managed entity.
# There is a default set of operations associated with being _ensurable_, but this can be changed.
# * **Name/Identity** - one property is the name/identity of a resource, the _namevar_ that uniquely identifies
# one instance of a type from all others.
# * **Parameters** - additional attributes of the type (that does not directly related to an instance of the managed
# resource; if an operation is recursive or not, where to look for things, etc.). A Parameter (in contrast to Property)
# has one current value where a Property has two (current-state and wanted-state).
# * **Meta-Parameters** - parameters that are available across all types. A meta-parameter typically has
# additional semantics; like the `require` meta-parameter. A new type typically does not add new meta-parameters,
# but you need to be aware of their existence so you do not inadvertently shadow an existing meta-parameters.
# * **Parent** - a type can have a super type (that it inherits from).
# * **Validation** - If not just a basic data type, or an enumeration of symbolic values, it is possible to provide
# validation logic for a type, properties and parameters.
# * **Munging** - munging/unmunging is the process of turning a value in external representation (as used
# by a provider) into an internal representation and vice versa. A Type supports adding custom logic for these.
# * **Auto Requirements** - a type can specify automatic relationships to resources to ensure that if they are being
# managed, they will be processed before this type.
# * **Providers** - a provider is an implementation of a type's behavior - the management of a resource in the
# system being managed. A provider is often platform specific and is selected at runtime based on
# criteria/predicates specified in the configured providers. See {Puppet::Provider} for details.
# * **Device Support** - A type has some support for being applied to a device; i.e. something that is managed
# by running logic external to the device itself. There are several methods that deals with type
# applicability for these special cases such as {apply_to_device}.
#
# Additional Concepts:
# --
# * **Resource-type** - A _resource type_ is a term used to denote the type of a resource; internally a resource
# is really an instance of a Ruby class i.e. {Puppet::Resource} which defines its behavior as "resource data".
# Conceptually however, a resource is an instance of a subclass of Type (e.g. File), where such a class describes
# its interface (what can be said/what is known about a resource of this type),
# * **Managed Entity** - This is not a term in general use, but is used here when there is a need to make
# a distinction between a resource (a description of what/how something should be managed), and what it is
# managing (a file in the file system). The term _managed entity_ is a reference to the "file in the file system"
# * **Isomorphism** - the quality of being _isomorphic_ means that two resource instances with the same name
# refers to the same managed entity. Or put differently; _an isomorphic name is the identity of a resource_.
# As an example, `exec` resources (that executes some command) have the command (i.e. the command line string) as
# their name, and these resources are said to be non-isomorphic.
#
# @note The Type class deals with multiple concerns; some methods provide an internal DSL for convenient definition
# of types, other methods deal with various aspects while running; wiring up a resource (expressed in Puppet DSL
# or Ruby DSL) with its _resource type_ (i.e. an instance of Type) to enable validation, transformation of values
# (munge/unmunge), etc. Lastly, Type is also responsible for dealing with Providers; the concrete implementations
# of the behavior that constitutes how a particular Type behaves on a particular type of system (e.g. how
# commands are executed on a flavor of Linux, on Windows, etc.). This means that as you are reading through the
# documentation of this class, you will be switching between these concepts, as well as switching between
# the conceptual level "a resource is an instance of a resource-type" and the actual implementation classes
# (Type, Resource, Provider, and various utility and helper classes).
#
# @api public
#
#
class Type
include Puppet::Util
include Puppet::Util::Errors
include Puppet::Util::Logging
include Puppet::Util::Tagging
# Comparing type instances.
include Comparable
# Compares this type against the given _other_ (type) and returns -1, 0, or +1 depending on the order.
# @param other [Object] the object to compare against (produces nil, if not kind of Type}
# @return [-1, 0, +1, nil] produces -1 if this type is before the given _other_ type, 0 if equals, and 1 if after.
# Returns nil, if the given _other_ is not a kind of Type.
# @see Comparable
#
def <=>(other)
# We only order against other types, not arbitrary objects.
return nil unless other.is_a? Puppet::Type
# Our natural order is based on the reference name we use when comparing
# against other type instances.
self.ref <=> other.ref
end
# Code related to resource type attributes.
class << self
include Puppet::Util::ClassGen
include Puppet::Util::Warnings
# @return [Array<Puppet::Property>] The list of declared properties for the resource type.
# The returned lists contains instances if Puppet::Property or its subclasses.
attr_reader :properties
end
# Returns all the attribute names of the type in the appropriate order.
# The {key_attributes} come first, then the {provider}, then the {properties}, and finally
# the {parameters} and {metaparams},
# all in the order they were specified in the respective files.
# @return [Array<String>] all type attribute names in a defined order.
#
def self.allattrs
key_attributes | (parameters & [:provider]) | properties.collect { |property| property.name } | parameters | metaparams
end
# Returns the class associated with the given attribute name.
# @param name [String] the name of the attribute to obtain the class for
# @return [Class, nil] the class for the given attribute, or nil if the name does not refer to an existing attribute
#
def self.attrclass(name)
@attrclasses ||= {}
# We cache the value, since this method gets called such a huge number
# of times (as in, hundreds of thousands in a given run).
unless @attrclasses.include?(name)
@attrclasses[name] = case self.attrtype(name)
when :property; @validproperties[name]
when :meta; @@metaparamhash[name]
when :param; @paramhash[name]
end
end
@attrclasses[name]
end
# Returns the attribute type (`:property`, `;param`, `:meta`).
# @comment What type of parameter are we dealing with? Cache the results, because
# this method gets called so many times.
# @return [Symbol] a symbol describing the type of attribute (`:property`, `;param`, `:meta`)
#
def self.attrtype(attr)
@attrtypes ||= {}
unless @attrtypes.include?(attr)
@attrtypes[attr] = case
when @validproperties.include?(attr); :property
when @paramhash.include?(attr); :param
when @@metaparamhash.include?(attr); :meta
end
end
@attrtypes[attr]
end
# Provides iteration over meta-parameters.
# @yieldparam p [Puppet::Parameter] each meta parameter
# @return [void]
#
def self.eachmetaparam
@@metaparams.each { |p| yield p.name }
end
# Creates a new `ensure` property with configured default values or with configuration by an optional block.
# This method is a convenience method for creating a property `ensure` with default accepted values.
# If no block is specified, the new `ensure` property will accept the default symbolic
# values `:present`, and `:absent` - see {Puppet::Property::Ensure}.
# If something else is wanted, pass a block and make calls to {Puppet::Property.newvalue} from this block
# to define each possible value. If a block is passed, the defaults are not automatically added to the set of
# valid values.
#
# @note This method will be automatically called without a block if the type implements the methods
# specified by {ensurable?}. It is recommended to always call this method and not rely on this automatic
# specification to clearly state that the type is ensurable.
#
# @overload ensurable()
# @overload ensurable({|| ... })
# @yield [ ] A block evaluated in scope of the new Parameter
# @yieldreturn [void]
# @return [void]
# @dsl type
# @api public
#
def self.ensurable(&block)
if block_given?
self.newproperty(:ensure, :parent => Puppet::Property::Ensure, &block)
else
self.newproperty(:ensure, :parent => Puppet::Property::Ensure) do
self.defaultvalues
end
end
end
# Returns true if the type implements the default behavior expected by being _ensurable_ "by default".
# A type is _ensurable_ by default if it responds to `:exists`, `:create`, and `:destroy`.
# If a type implements these methods and have not already specified that it is _ensurable_, it will be
# made so with the defaults specified in {ensurable}.
# @return [Boolean] whether the type is _ensurable_ or not.
#
def self.ensurable?
# If the class has all three of these methods defined, then it's
# ensurable.
[:exists?, :create, :destroy].all? { |method|
self.public_method_defined?(method)
}
end
# @comment These `apply_to` methods are horrible. They should really be implemented
# as part of the usual system of constraints that apply to a type and
# provider pair, but were implemented as a separate shadow system.
#
# @comment We should rip them out in favour of a real constraint pattern around the
# target device - whatever that looks like - and not have this additional
# magic here. --daniel 2012-03-08
#
# Makes this type applicable to `:device`.
# @return [Symbol] Returns `:device`
# @api private
#
def self.apply_to_device
@apply_to = :device
end
# Makes this type applicable to `:host`.
# @return [Symbol] Returns `:host`
# @api private
#
def self.apply_to_host
@apply_to = :host
end
# Makes this type applicable to `:both` (i.e. `:host` and `:device`).
# @return [Symbol] Returns `:both`
# @api private
#
def self.apply_to_all
@apply_to = :both
end
# Makes this type apply to `:host` if not already applied to something else.
# @return [Symbol] a `:device`, `:host`, or `:both` enumeration
# @api private
def self.apply_to
@apply_to ||= :host
end
# Returns true if this type is applicable to the given target.
# @param target [Symbol] should be :device, :host or :target, if anything else, :host is enforced
# @return [Boolean] true
# @api private
#
def self.can_apply_to(target)
[ target == :device ? :device : :host, :both ].include?(apply_to)
end
# Processes the options for a named parameter.
# @param name [String] the name of a parameter
# @param options [Hash] a hash of options
# @option options [Boolean] :boolean if option set to true, an access method on the form _name_? is added for the param
# @return [void]
#
def self.handle_param_options(name, options)
# If it's a boolean parameter, create a method to test the value easily
if options[:boolean]
define_method(name.to_s + "?") do
val = self[name]
if val == :true or val == true
return true
end
end
end
end
# Is the given parameter a meta-parameter?
# @return [Boolean] true if the given parameter is a meta-parameter.
#
def self.metaparam?(param)
@@metaparamhash.include?(param.intern)
end
# Returns the meta-parameter class associated with the given meta-parameter name.
# Accepts a `nil` name, and return nil.
# @param name [String, nil] the name of a meta-parameter
# @return [Class,nil] the class for the given meta-parameter, or `nil` if no such meta-parameter exists, (or if
# the given meta-parameter name is `nil`.
#
def self.metaparamclass(name)
return nil if name.nil?
@@metaparamhash[name.intern]
end
# Returns all meta-parameter names.
# @return [Array<String>] all meta-parameter names
#
def self.metaparams
@@metaparams.collect { |param| param.name }
end
# Returns the documentation for a given meta-parameter of this type.
# @todo the type for the param metaparam
# @param metaparam [??? Puppet::Parameter] the meta-parameter to get documentation for.
# @return [String] the documentation associated with the given meta-parameter, or nil of not such documentation
# exists.
# @raise if the given metaparam is not a meta-parameter in this type
#
def self.metaparamdoc(metaparam)
@@metaparamhash[metaparam].doc
end
# Creates a new meta-parameter.
# This creates a new meta-parameter that is added to all types.
# @param name [Symbol] the name of the parameter
# @param options [Hash] a hash with options.
# @option options [Class<inherits Puppet::Parameter>] :parent (Puppet::Parameter) the super class of this parameter
# @option options [Hash{String => Object}] :attributes a hash that is applied to the generated class
# by calling setter methods corresponding to this hash's keys/value pairs. This is done before the given
# block is evaluated.
# @option options [Boolean] :boolean (false) specifies if this is a boolean parameter
# @option options [Boolean] :namevar (false) specifies if this parameter is the namevar
# @option options [Symbol, Array<Symbol>] :required_features specifies required provider features by name
# @return [Class<inherits Puppet::Parameter>] the created parameter
# @yield [ ] a required block that is evaluated in the scope of the new meta-parameter
# @api public
# @dsl type
# @todo Verify that this description is ok
#
def self.newmetaparam(name, options = {}, &block)
@@metaparams ||= []
@@metaparamhash ||= {}
name = name.intern
param = genclass(
name,
:parent => options[:parent] || Puppet::Parameter,
:prefix => "MetaParam",
:hash => @@metaparamhash,
:array => @@metaparams,
:attributes => options[:attributes],
&block
)
# Grr.
param.required_features = options[:required_features] if options[:required_features]
handle_param_options(name, options)
param.metaparam = true
param
end
# Returns parameters that act as a key.
# All parameters that return true from #isnamevar? or is named `:name` are included in the returned result.
# @todo would like a better explanation
# @return [Array<Puppet::Parameter>] WARNING: this return type is uncertain
def self.key_attribute_parameters
@key_attribute_parameters ||= (
@parameters.find_all { |param|
param.isnamevar? or param.name == :name
}
)
end
# Returns cached {key_attribute_parameters} names
# @todo what is a 'key_attribute' ?
# @return [Array<String>] cached key_attribute names
#
def self.key_attributes
# This is a cache miss around 0.05 percent of the time. --daniel 2012-07-17
@key_attributes_cache ||= key_attribute_parameters.collect { |p| p.name }
end
# Returns a mapping from the title string to setting of attribute value(s).
# This default implementation provides a mapping of title to the one and only _namevar_ present
# in the type's definition.
# @note Advanced: some logic requires this mapping to be done differently, using a different
# validation/pattern, breaking up the title
# into several parts assigning each to an individual attribute, or even use a composite identity where
# all namevars are seen as part of the unique identity (such computation is done by the {#uniqueness} method.
# These advanced options are rarely used (only one of the built in puppet types use this, and then only
# a small part of the available functionality), and the support for these advanced mappings is not
# implemented in a straight forward way. For these reasons, this method has been marked as private).
#
# @raise [Puppet::DevError] if there is no title pattern and there are two or more key attributes
# @return [Array<Array<Regexp, Array<Array <Symbol, Proc>>>>, nil] a structure with a regexp and the first key_attribute ???
# @comment This wonderful piece of logic creates a structure used by Resource.parse_title which
# has the capability to assign parts of the title to one or more attributes; It looks like an implementation
# of a composite identity key (all parts of the key_attributes array are in the key). This can also
# be seen in the method uniqueness_key.
# The implementation in this method simply assigns the title to the one and only namevar (which is name
# or a variable marked as namevar).
# If there are multiple namevars (any in addition to :name?) then this method MUST be implemented
# as it raises an exception if there is more than 1. Note that in puppet, it is only File that uses this
# to create a different pattern for assigning to the :path attribute
# This requires further digging.
# The entire construct is somewhat strange, since resource checks if the method "title_patterns" is
# implemented (it seems it always is) - why take this more expensive regexp mathching route for all
# other types?
# @api private
#
def self.title_patterns
case key_attributes.length
when 0; []
when 1;
[ [ /(.*)/m, [ [key_attributes.first] ] ] ]
else
raise Puppet::DevError,"you must specify title patterns when there are two or more key attributes"
end
end
# Produces a _uniqueness_key_
# @todo Explain what a uniqueness_key is
# @return [Object] an object that is a _uniqueness_key_ for this object
#
def uniqueness_key
self.class.key_attributes.sort_by { |attribute_name| attribute_name.to_s }.map{ |attribute_name| self[attribute_name] }
end
# Creates a new parameter.
# @param name [Symbol] the name of the parameter
# @param options [Hash] a hash with options.
# @option options [Class<inherits Puppet::Parameter>] :parent (Puppet::Parameter) the super class of this parameter
# @option options [Hash{String => Object}] :attributes a hash that is applied to the generated class
# by calling setter methods corresponding to this hash's keys/value pairs. This is done before the given
# block is evaluated.
# @option options [Boolean] :boolean (false) specifies if this is a boolean parameter
# @option options [Boolean] :namevar (false) specifies if this parameter is the namevar
# @option options [Symbol, Array<Symbol>] :required_features specifies required provider features by name
# @return [Class<inherits Puppet::Parameter>] the created parameter
# @yield [ ] a required block that is evaluated in the scope of the new parameter
# @api public
# @dsl type
#
def self.newparam(name, options = {}, &block)
options[:attributes] ||= {}
param = genclass(
name,
:parent => options[:parent] || Puppet::Parameter,
:attributes => options[:attributes],
:block => block,
:prefix => "Parameter",
:array => @parameters,
:hash => @paramhash
)
handle_param_options(name, options)
# Grr.
param.required_features = options[:required_features] if options[:required_features]
param.isnamevar if options[:namevar]
param
end
# Creates a new property.
# @param name [Symbol] the name of the property
# @param options [Hash] a hash with options.
# @option options [Symbol] :array_matching (:first) specifies how the current state is matched against
# the wanted state. Use `:first` if the property is single valued, and (`:all`) otherwise.
# @option options [Class<inherits Puppet::Property>] :parent (Puppet::Property) the super class of this property
# @option options [Hash{String => Object}] :attributes a hash that is applied to the generated class
# by calling setter methods corresponding to this hash's keys/value pairs. This is done before the given
# block is evaluated.
# @option options [Boolean] :boolean (false) specifies if this is a boolean parameter
# @option options [Symbol] :retrieve the method to call on the provider (or `parent` if `provider` is not set)
# to retrieve the current value of this property.
# @option options [Symbol, Array<Symbol>] :required_features specifies required provider features by name
# @return [Class<inherits Puppet::Property>] the created property
# @yield [ ] a required block that is evaluated in the scope of the new property
# @api public
# @dsl type
#
def self.newproperty(name, options = {}, &block)
name = name.intern
# This is here for types that might still have the old method of defining
# a parent class.
unless options.is_a? Hash
raise Puppet::DevError,
"Options must be a hash, not #{options.inspect}"
end
raise Puppet::DevError, "Class #{self.name} already has a property named #{name}" if @validproperties.include?(name)
if parent = options[:parent]
options.delete(:parent)
else
parent = Puppet::Property
end
# We have to create our own, new block here because we want to define
# an initial :retrieve method, if told to, and then eval the passed
# block if available.
prop = genclass(name, :parent => parent, :hash => @validproperties, :attributes => options) do
# If they've passed a retrieve method, then override the retrieve
# method on the class.
if options[:retrieve]
define_method(:retrieve) do
provider.send(options[:retrieve])
end
end
class_eval(&block) if block
end
# If it's the 'ensure' property, always put it first.
if name == :ensure
@properties.unshift prop
else
@properties << prop
end
prop
end
def self.paramdoc(param)
@paramhash[param].doc
end
# @return [Array<String>] Returns the parameter names
def self.parameters
return [] unless defined?(@parameters)
@parameters.collect { |klass| klass.name }
end
# @return [Puppet::Parameter] Returns the parameter class associated with the given parameter name.
def self.paramclass(name)
@paramhash[name]
end
# @return [Puppet::Property] Returns the property class ??? associated with the given property name
def self.propertybyname(name)
@validproperties[name]
end
# Returns whether or not the given name is the name of a property, parameter or meta-parameter
# @return [Boolean] true if the given attribute name is the name of an existing property, parameter or meta-parameter
#
def self.validattr?(name)
name = name.intern
return true if name == :name
@validattrs ||= {}
unless @validattrs.include?(name)
@validattrs[name] = !!(self.validproperty?(name) or self.validparameter?(name) or self.metaparam?(name))
end
@validattrs[name]
end
# @return [Boolean] Returns true if the given name is the name of an existing property
def self.validproperty?(name)
name = name.intern
@validproperties.include?(name) && @validproperties[name]
end
# @return [Array<Symbol>, {}] Returns a list of valid property names, or an empty hash if there are none.
# @todo An empty hash is returned if there are no defined parameters (not an empty array). This looks like
# a bug.
#
def self.validproperties
return {} unless defined?(@parameters)
@validproperties.keys
end
# @return [Boolean] Returns true if the given name is the name of an existing parameter
def self.validparameter?(name)
raise Puppet::DevError, "Class #{self} has not defined parameters" unless defined?(@parameters)
!!(@paramhash.include?(name) or @@metaparamhash.include?(name))
end
# (see validattr?)
# @note see comment in code - how should this be documented? Are some of the other query methods deprecated?
# (or should be).
# @comment This is a forward-compatibility method - it's the validity interface we'll use in Puppet::Resource.
def self.valid_parameter?(name)
validattr?(name)
end
# @return [Boolean] Returns true if the wanted state of the resoure is that it should be absent (i.e. to be deleted).
def deleting?
obj = @parameters[:ensure] and obj.should == :absent
end
# Creates a new property value holder for the resource if it is valid and does not already exist
# @return [Boolean] true if a new parameter was added, false otherwise
def add_property_parameter(prop_name)
if self.class.validproperty?(prop_name) && !@parameters[prop_name]
self.newattr(prop_name)
return true
end
false
end
# @return [Symbol, Boolean] Returns the name of the namevar if there is only one or false otherwise.
# @comment This is really convoluted and part of the support for multiple namevars (?).
# If there is only one namevar, the produced value is naturally this namevar, but if there are several?
# The logic caches the name of the namevar if it is a single name, but otherwise always
# calls key_attributes, and then caches the first if there was only one, otherwise it returns
# false and caches this (which is then subsequently returned as a cache hit).
#
def name_var
return @name_var_cache unless @name_var_cache.nil?
key_attributes = self.class.key_attributes
@name_var_cache = (key_attributes.length == 1) && key_attributes.first
end
# Gets the 'should' (wanted state) value of a parameter or property by name.
# To explicitly get the 'is' (current state) value use `o.is(:name)`, and to explicitly get the 'should' value
# use `o.should(:name)`
# @param name [String] the name of the attribute to obtain the 'should' value for.
# @return [Object] 'should'/wanted value of the given attribute
def [](name)
name = name.intern
fail("Invalid parameter #{name}(#{name.inspect})") unless self.class.validattr?(name)
if name == :name && nv = name_var
name = nv
end
if obj = @parameters[name]
# Note that if this is a property, then the value is the "should" value,
# not the current value.
obj.value
else
return nil
end
end
# Sets the 'should' (wanted state) value of a property, or the value of a parameter.
# @return
# @raise [Puppet::Error] if the setting of the value fails, or if the given name is nil.
# @raise [Puppet::ResourceError] when the parameter validation raises Puppet::Error or
# ArgumentError
def []=(name,value)
name = name.intern
fail("Invalid parameter #{name}") unless self.class.validattr?(name)
if name == :name && nv = name_var
name = nv
end
raise Puppet::Error.new("Got nil value for #{name}") if value.nil?
property = self.newattr(name)
if property
begin
# make sure the parameter doesn't have any errors
property.value = value
rescue Puppet::Error, ArgumentError => detail
error = Puppet::ResourceError.new("Parameter #{name} failed on #{ref}: #{detail}")
adderrorcontext(error, detail)
raise error
end
end
nil
end
# Removes a property from the object; useful in testing or in cleanup
# when an error has been encountered
# @todo Incomprehensible - the comment says "Remove a property", the code refers to @parameters, and
# the method parameter is called "attr" - What is it, property, parameter, both (i.e an attribute) or what?
# @todo Don't know what the attr is (name or Property/Parameter?). Guessing it is a String name...
# @todo Is it possible to delete a meta-parameter?
# @todo What does delete mean? Is it deleted from the type or is its value state 'is'/'should' deleted?
# @param attr [String] the attribute to delete from this object. WHAT IS THE TYPE?
# @raise [Puppet::DecError] when an attempt is made to delete an attribute that does not exists.
#
def delete(attr)
attr = attr.intern
if @parameters.has_key?(attr)
@parameters.delete(attr)
else
raise Puppet::DevError.new("Undefined attribute '#{attr}' in #{self}")
end
end
# Iterates over the existing properties.
# @todo what does this mean? As opposed to iterating over the "non existing properties" ??? Is it an
# iteration over those properties that have state? CONFUSING.
# @yieldparam property [Puppet::Property] each property
# @return [void]
def eachproperty
# properties is a private method
properties.each { |property|
yield property
}
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
# values in this object. For possible options to pass (if any ????) see {Puppet::Transaction::Event}.
# @todo Needs a better explanation "Why should I care who is calling this method?", What do I need to know
# about events and how they work? Where can I read about them?
# @param options [Hash] options merged with a fixed set of options defined by this method, passed on to {Puppet::Transaction::Event}.
# @return [Puppet::Transaction::Event] the created event
def event(options = {})
Puppet::Transaction::Event.new({:resource => self, :file => file, :line => line, :tags => tags}.merge(options))
end
# @return [Object, nil] Returns the 'should' (wanted state) value for a specified property, or nil if the
# given attribute name is not a property (i.e. if it is a parameter, meta-parameter, or does not exist).
def should(name)
name = name.intern
(prop = @parameters[name] and prop.is_a?(Puppet::Property)) ? prop.should : nil
end
# Creates an instance to represent/manage the given attribute.
# Requires either the attribute name or class as the first argument, then an optional hash of
# attributes to set during initialization.
# @todo The original comment is just wrong - the method does not accept a hash of options
# @todo Detective work required; this method interacts with provider to ask if it supports a parameter of
# the given class. it then returns the parameter if it exists, otherwise creates a parameter
# with its :resource => self.
# @overload newattr(name)
# @param name [String] Unclear what name is (probably a symbol) - Needs investigation.
# @overload newattr(klass)
# @param klass [Class] a class supported as an attribute class - Needs clarification what that means.
# @return [???] Probably returns a new instance of the class - Needs investigation.
#
def newattr(name)
if name.is_a?(Class)
klass = name
name = klass.name
end
unless klass = self.class.attrclass(name)
raise Puppet::Error, "Resource type #{self.class.name} does not support parameter #{name}"
end
if provider and ! provider.class.supports_parameter?(klass)
missing = klass.required_features.find_all { |f| ! provider.class.feature?(f) }
debug "Provider %s does not support features %s; not managing attribute %s" % [provider.class.name, missing.join(", "), name]
return nil
end
return @parameters[name] if @parameters.include?(name)
@parameters[name] = klass.new(:resource => self)
end
# Returns a string representation of the resource's containment path in
# the catalog.
# @return [String]
def path
@path ||= '/' + pathbuilder.join('/')
end
# Returns the value of this object's parameter given by name
# @param name [String] the name of the parameter
# @return [Object] the value
def parameter(name)
@parameters[name.to_sym]
end
# Returns a shallow copy of this object's hash of parameters.
# @todo Add that this is not only "parameters", but also "properties" and "meta-parameters" ?
# Changes to the contained parameters will have an effect on the parameters of this type, but changes to
# the returned hash does not.
# @return [Hash{String => Puppet:???Parameter}] a new hash being a shallow copy of the parameters map name to parameter
def parameters
@parameters.dup
end
# @return [Boolean] Returns whether the property given by name is defined or not.
# @todo what does it mean to be defined?
def propertydefined?(name)
name = name.intern unless name.is_a? Symbol
@parameters.include?(name)
end
# Returns a {Puppet::Property} instance by name.
# To return the value, use 'resource[param]'
# @todo LAK:NOTE(20081028) Since the 'parameter' method is now a superset of this method,
# this one should probably go away at some point. - Does this mean it should be deprecated ?
# @return [Puppet::Property] the property with the given name, or nil if not a property or does not exist.
def property(name)
(obj = @parameters[name.intern] and obj.is_a?(Puppet::Property)) ? obj : nil
end
# @todo comment says "For any parameters or properties that have defaults and have not yet been
# set, set them now. This method can be handed a list of attributes,
# and if so it will only set defaults for those attributes."
# @todo Needs a better explanation, and investigation about the claim an array can be passed (it is passed
# to self.class.attrclass to produce a class on which a check is made if it has a method class :default (does
# not seem to support an array...
# @return [void]
#
def set_default(attr)
return unless klass = self.class.attrclass(attr)
return unless klass.method_defined?(:default)
return if @parameters.include?(klass.name)
return unless parameter = newattr(klass.name)
if value = parameter.default and ! value.nil?
parameter.value = value
else
@parameters.delete(parameter.name)
end
end
# @todo the comment says: "Convert our object to a hash. This just includes properties."
# @todo this is confused, again it is the @parameters instance variable that is consulted, and
# each value is copied - does it contain "properties" and "parameters" or both? Does it contain
# meta-parameters?
#
# @return [Hash{ ??? => ??? }] a hash of WHAT?. The hash is a shallow copy, any changes to the
# objects returned in this hash will be reflected in the original resource having these attributes.
#
def to_hash
rethash = {}
@parameters.each do |name, obj|
rethash[name] = obj.value
end
rethash
end
# @return [String] the name of this object's class
# @todo Would that be "file" for the "File" resource type? of "File" or something else?
#
def type
self.class.name
end
# @todo Comment says "Return a specific value for an attribute.", as opposed to what "An upspecific value"???
# @todo is this the 'is' or the 'should' value?
# @todo why is the return restricted to things that respond to :value? (Only non structural basic data types
# supported?
#
# @return [Object, nil] the value of the attribute having the given name, or nil if the given name is not
# an attribute, or the referenced attribute does not respond to `:value`.
def value(name)
name = name.intern
(obj = @parameters[name] and obj.respond_to?(:value)) ? obj.value : nil
end
# @todo What is this used for? Needs a better explanation.
# @return [???] the version of the catalog or 0 if there is no catalog.
def version
return 0 unless catalog
catalog.version
end
# @return [Array<Puppet::Property>] Returns all of the property objects, in the order specified in the
# class.
# @todo "what does the 'order specified in the class' mean? The order the properties where added in the
# ruby file adding a new type with new properties?
#
def properties
self.class.properties.collect { |prop| @parameters[prop.name] }.compact
end
# Returns true if the type's notion of name is the identity of a resource.
# See the overview of this class for a longer explanation of the concept _isomorphism_.
# Defaults to true.
#
# @return [Boolan] true, if this type's name is isomorphic with the object
def self.isomorphic?
if defined?(@isomorphic)
return @isomorphic
else
return true
end
end
# @todo check that this gets documentation (it is at the class level as well as instance).
# (see isomorphic?)
def isomorphic?
self.class.isomorphic?
end
# Returns true if the instance is a managed instance.
# A 'yes' here means that the instance was created from the language, vs. being created
# in order resolve other questions, such as finding a package in a list.
# @note An object that is managed always stays managed, but an object that is not managed
# may become managed later in its lifecycle.
# @return [Boolean] true if the object is managed
def managed?
# Once an object is managed, it always stays managed; but an object
# that is listed as unmanaged might become managed later in the process,
# so we have to check that every time
if @managed
return @managed
else
@managed = false
properties.each { |property|
s = property.should
if s and ! property.class.unmanaged
@managed = true
break
end
}
return @managed
end
end
###############################
# Code related to the container behaviour.
# Returns true if the search should be done in depth-first order.
# This implementation always returns false.
# @todo What is this used for?
#
# @return [Boolean] true if the search should be done in depth first order.
#
def depthfirst?
false
end
# Removes this object (FROM WHERE?)
# @todo removes if from where?
# @overload remove(rmdeps)
# @deprecated Use remove()
# @param rmdeps [Boolean] intended to indicate that all subscriptions should also be removed, ignored.
# @overload remove()
# @return [void]
#
def remove(rmdeps = true)
# This is hackish (mmm, cut and paste), but it works for now, and it's
# better than warnings.
@parameters.each do |name, obj|
obj.remove
end
@parameters.clear
@parent = nil
# Remove the reference to the provider.
if self.provider
@provider.clear
@provider = nil
end
end
###############################
# Code related to evaluating the resources.
# Returns the ancestors - WHAT?
# This implementation always returns an empty list.
# @todo WHAT IS THIS ?
# @return [Array<???>] returns a list of ancestors.
def ancestors
[]
end
# Flushes the provider if supported by the provider, else no action.
# This is called by the transaction.
# @todo What does Flushing the provider mean? Why is it interesting to know that this is
# called by the transaction? (It is not explained anywhere what a transaction is).
#
# @return [???, nil] WHAT DOES IT RETURN? GUESS IS VOID
def flush
self.provider.flush if self.provider and self.provider.respond_to?(:flush)
end
# Returns true if all contained objects are in sync.
# @todo "contained in what?" in the given "in" parameter?
#
# @todo deal with the comment _"FIXME I don't think this is used on the type instances any more,
# it's really only used for testing"_
# @return [Boolean] true if in sync, false otherwise.
#
def insync?(is)
insync = true
if property = @parameters[:ensure]
unless is.include? property
raise Puppet::DevError,
"The is value is not in the is array for '#{property.name}'"
end
ensureis = is[property]
if property.safe_insync?(ensureis) and property.should == :absent
return true
end
end
properties.each { |property|
unless is.include? property
raise Puppet::DevError,
"The is value is not in the is array for '#{property.name}'"
end
propis = is[property]
unless property.safe_insync?(propis)
property.debug("Not in sync: #{propis.inspect} vs #{property.should.inspect}")
insync = false
#else
# property.debug("In sync")
end
}
#self.debug("#{self} sync status is #{insync}")
insync
end
# Retrieves the current value of all contained properties.
# Parameters and meta-parameters are not included in the result.
# @todo As oposed to all non contained properties? How is this different than any of the other
# methods that also "gets" properties/parameters/etc. ?
# @return [Puppet::Resource] array of all property values (mix of types)
# @raise [fail???] if there is a provider and it is not suitable for the host this is evaluated for.
def retrieve
fail "Provider #{provider.class.name} is not functional on this host" if self.provider.is_a?(Puppet::Provider) and ! provider.class.suitable?
result = Puppet::Resource.new(type, title)
# Provide the name, so we know we'll always refer to a real thing
result[:name] = self[:name] unless self[:name] == title
if ensure_prop = property(:ensure) or (self.class.validattr?(:ensure) and ensure_prop = newattr(:ensure))
result[:ensure] = ensure_state = ensure_prop.retrieve
else
ensure_state = nil
end
properties.each do |property|
next if property.name == :ensure
if ensure_state == :absent
result[property] = :absent
else
result[property] = property.retrieve
end
end
result
end
# Retrieve the current state of the system as a Puppet::Resource. For
# the base Puppet::Type this does the same thing as #retrieve, but
# specific types are free to implement #retrieve as returning a hash,
# and this will call #retrieve and convert the hash to a resource.
# This is used when determining when syncing a resource.
#
# @return [Puppet::Resource] A resource representing the current state
# of the system.
#
# @api private
def retrieve_resource
resource = retrieve
resource = Resource.new(type, title, :parameters => resource) if resource.is_a? Hash
resource
end
# Given the hash of current properties, should this resource be treated as if it
# currently exists on the system. May need to be overridden by types that offer up
# more than just :absent and :present.
def present?(current_values)
current_values[:ensure] != :absent
end
# Returns a hash of the current properties and their values.
# If a resource is absent, its value is the symbol `:absent`
# @return [Hash{Puppet::Property => Object}] mapping of property instance to its value
#
def currentpropvalues
# It's important to use the 'properties' method here, as it follows the order
# in which they're defined in the class. It also guarantees that 'ensure'
# is the first property, which is important for skipping 'retrieve' on
# all the properties if the resource is absent.
ensure_state = false
return properties.inject({}) do | prophash, property|
if property.name == :ensure
ensure_state = property.retrieve
prophash[property] = ensure_state
else
if ensure_state == :absent
prophash[property] = :absent
else
prophash[property] = property.retrieve
end
end
prophash
end
end
# Returns the `noop` run mode status of this.
# @return [Boolean] true if running in noop mode.
def noop?
# If we're not a host_config, we're almost certainly part of
# Settings, and we want to ignore 'noop'
return false if catalog and ! catalog.host_config?
if defined?(@noop)
@noop
else
Puppet[:noop]
end
end
# (see #noop?)
def noop
noop?
end
# Retrieves all known instances.
# @todo Retrieves them from where? Known to whom?
# Either requires providers or must be overridden.
# @raise [Puppet::DevError] when there are no providers and the implementation has not overridded this method.
def self.instances
raise Puppet::DevError, "#{self.name} has no providers and has not overridden 'instances'" if provider_hash.empty?
# Put the default provider first, then the rest of the suitable providers.
provider_instances = {}
providers_by_source.collect do |provider|
self.properties.find_all do |property|
provider.supports_parameter?(property)
end.collect do |property|
property.name
end
provider.instances.collect do |instance|
# We always want to use the "first" provider instance we find, unless the resource
# is already managed and has a different provider set
if other = provider_instances[instance.name]
Puppet.debug "%s %s found in both %s and %s; skipping the %s version" %
[self.name.to_s.capitalize, instance.name, other.class.name, instance.class.name, instance.class.name]
next
end
provider_instances[instance.name] = instance
result = new(:name => instance.name, :provider => instance)
properties.each { |name| result.newattr(name) }
result
end
end.flatten.compact
end
# Returns a list of one suitable provider per source, with the default provider first.
# @todo Needs better explanation; what does "source" mean in this context?
# @return [Array<Puppet::Provider>] list of providers
#
def self.providers_by_source
# Put the default provider first (can be nil), then the rest of the suitable providers.
sources = []
[defaultprovider, suitableprovider].flatten.uniq.collect do |provider|
next if provider.nil?
next if sources.include?(provider.source)
sources << provider.source
provider
end.compact
end
# Converts a simple hash into a Resource instance.
# @todo as opposed to a complex hash? Other raised exceptions?
# @param [Hash{Symbol, String => Object}] hash resource attribute to value map to initialize the created resource from
# @return [Puppet::Resource] the resource created from the hash
# @raise [Puppet::Error] if a title is missing in the given hash
def self.hash2resource(hash)
hash = hash.inject({}) { |result, ary| result[ary[0].to_sym] = ary[1]; result }
title = hash.delete(:title)
title ||= hash[:name]
title ||= hash[key_attributes.first] if key_attributes.length == 1
raise Puppet::Error, "Title or name must be provided" unless title
# Now create our resource.
resource = Puppet::Resource.new(self.name, title)
resource.catalog = hash.delete(:catalog)
+ resource.resource_type = self
hash.each do |param, value|
resource[param] = value
end
resource
end
# Returns an array of strings representing the containment heirarchy
# (types/classes) that make up the path to the resource from the root
# of the catalog. This is mostly used for logging purposes.
#
# @api private
def pathbuilder
if p = parent
[p.pathbuilder, self.ref].flatten
else
[self.ref]
end
end
###############################
# Add all of the meta-parameters.
newmetaparam(:noop) do
desc "Whether to apply this resource in noop mode.
When applying a resource in noop mode, Puppet will check whether it is in sync,
like it does when running normally. However, if a resource attribute is not in
the desired state (as declared in the catalog), Puppet will take no
action, and will instead report the changes it _would_ have made. These
simulated changes will appear in the report sent to the puppet master, or
be shown on the console if running puppet agent or puppet apply in the
foreground. The simulated changes will not send refresh events to any
subscribing or notified resources, although Puppet will log that a refresh
event _would_ have been sent.
**Important note:**
[The `noop` setting](http://docs.puppetlabs.com/references/latest/configuration.html#noop)
allows you to globally enable or disable noop mode, but it will _not_ override
the `noop` metaparameter on individual resources. That is, the value of the
global `noop` setting will _only_ affect resources that do not have an explicit
value set for their `noop` attribute."
newvalues(:true, :false)
munge do |value|
case value
when true, :true, "true"; @resource.noop = true
when false, :false, "false"; @resource.noop = false
end
end
end
newmetaparam(:schedule) do
desc "A schedule to govern when Puppet is allowed to manage this resource.
The value of this metaparameter must be the `name` of a `schedule`
resource. This means you must declare a schedule resource, then
refer to it by name; see
[the docs for the `schedule` type](http://docs.puppetlabs.com/references/latest/type.html#schedule)
for more info.
schedule { 'everyday':
period => daily,
range => \"2-4\"
}
exec { \"/usr/bin/apt-get update\":
schedule => 'everyday'
}
Note that you can declare the schedule resource anywhere in your
manifests, as long as it ends up in the final compiled catalog."
end
newmetaparam(:audit) do
desc "Marks a subset of this resource's unmanaged attributes for auditing. Accepts an
attribute name, an array of attribute names, or `all`.
Auditing a resource attribute has two effects: First, whenever a catalog
is applied with puppet apply or puppet agent, Puppet will check whether
that attribute of the resource has been modified, comparing its current
value to the previous run; any change will be logged alongside any actions
performed by Puppet while applying the catalog.
Secondly, marking a resource attribute for auditing will include that
attribute in inspection reports generated by puppet inspect; see the
puppet inspect documentation for more details.
Managed attributes for a resource can also be audited, but note that
changes made by Puppet will be logged as additional modifications. (I.e.
if a user manually edits a file whose contents are audited and managed,
puppet agent's next two runs will both log an audit notice: the first run
will log the user's edit and then revert the file to the desired state,
and the second run will log the edit made by Puppet.)"
validate do |list|
list = Array(list).collect {|p| p.to_sym}
unless list == [:all]
list.each do |param|
next if @resource.class.validattr?(param)
fail "Cannot audit #{param}: not a valid attribute for #{resource}"
end
end
end
munge do |args|
properties_to_audit(args).each do |param|
next unless resource.class.validproperty?(param)
resource.newattr(param)
end
end
def all_properties
resource.class.properties.find_all do |property|
resource.provider.nil? or resource.provider.class.supports_parameter?(property)
end.collect do |property|
property.name
end
end
def properties_to_audit(list)
if !list.kind_of?(Array) && list.to_sym == :all
list = all_properties
else
list = Array(list).collect { |p| p.to_sym }
end
end
end
newmetaparam(:loglevel) do
desc "Sets the level that information will be logged.
The log levels have the biggest impact when logs are sent to
syslog (which is currently the default)."
defaultto :notice
newvalues(*Puppet::Util::Log.levels)
newvalues(:verbose)
munge do |loglevel|
val = super(loglevel)
if val == :verbose
val = :info
end
val
end
end
newmetaparam(:alias) do
desc %q{Creates an alias for the resource. Puppet uses this internally when you
provide a symbolic title and an explicit namevar value:
file { 'sshdconfig':
path => $operatingsystem ? {
solaris => '/usr/local/etc/ssh/sshd_config',
default => '/etc/ssh/sshd_config',
},
source => '...'
}
service { 'sshd':
subscribe => File['sshdconfig'],
}
When you use this feature, the parser sets `sshdconfig` as the title,
and the library sets that as an alias for the file so the dependency
lookup in `Service['sshd']` works. You can use this metaparameter yourself,
but note that aliases generally only work for creating relationships; anything
else that refers to an existing resource (such as amending or overriding
resource attributes in an inherited class) must use the resource's exact
title. For example, the following code will not work:
file { '/etc/ssh/sshd_config':
owner => root,
group => root,
alias => 'sshdconfig',
}
File['sshdconfig'] {
mode => 644,
}
There's no way here for the Puppet parser to know that these two stanzas
should be affecting the same file.
}
munge do |aliases|
aliases = [aliases] unless aliases.is_a?(Array)
raise(ArgumentError, "Cannot add aliases without a catalog") unless @resource.catalog
aliases.each do |other|
if obj = @resource.catalog.resource(@resource.class.name, other)
unless obj.object_id == @resource.object_id
self.fail("#{@resource.title} can not create alias #{other}: object already exists")
end
next
end
# Newschool, add it to the catalog.
@resource.catalog.alias(@resource, other)
end
end
end
newmetaparam(:tag) do
desc "Add the specified tags to the associated resource. While all resources
are automatically tagged with as much information as possible
(e.g., each class and definition containing the resource), it can
be useful to add your own tags to a given resource.
Multiple tags can be specified as an array:
file {'/etc/hosts':
ensure => file,
source => 'puppet:///modules/site/hosts',
mode => 0644,
tag => ['bootstrap', 'minimumrun', 'mediumrun'],
}
Tags are useful for things like applying a subset of a host's configuration
with [the `tags` setting](/references/latest/configuration.html#tags)
(e.g. `puppet agent --test --tags bootstrap`) or filtering alerts with
[the `tagmail` report processor](http://docs.puppetlabs.com/references/latest/report.html#tagmail)."
munge do |tags|
tags = [tags] unless tags.is_a? Array
tags.each do |tag|
@resource.tag(tag)
end
end
end
# RelationshipMetaparam is an implementation supporting the meta-parameters `:require`, `:subscribe`,
# `:notify`, and `:before`.
#
#
class RelationshipMetaparam < Puppet::Parameter
class << self
attr_accessor :direction, :events, :callback, :subclasses
end
@subclasses = []
def self.inherited(sub)
@subclasses << sub
end
# @return [Array<Puppet::Resource>] turns attribute value(s) into list of resources
def munge(references)
references = [references] unless references.is_a?(Array)
references.collect do |ref|
if ref.is_a?(Puppet::Resource)
ref
else
Puppet::Resource.new(ref)
end
end
end
# Checks each reference to assert that what it references exists in the catalog.
#
# @raise [???fail] if the referenced resource can not be found
# @return [void]
def validate_relationship
@value.each do |ref|
unless @resource.catalog.resource(ref.to_s)
description = self.class.direction == :in ? "dependency" : "dependent"
fail ResourceError, "Could not find #{description} #{ref} for #{resource.ref}"
end
end
end
# Creates edges for all relationships.
# The `:in` relationships are specified by the event-receivers, and `:out`
# relationships are specified by the event generator.
# @todo references to "event-receivers" and "event generator" means in this context - are those just
# the resources at the two ends of the relationship?
# This way 'source' and 'target' are consistent terms in both edges
# and events, i.e. an event targets edges whose source matches
# the event's source. The direction of the relationship determines
# which resource is applied first and which resource is considered
# to be the event generator.
# @return [Array<Puppet::Relationship>]
# @raise [???fail] when a reference can not be resolved
#
def to_edges
@value.collect do |reference|
reference.catalog = resource.catalog
# Either of the two retrieval attempts could have returned
# nil.
unless related_resource = reference.resolve
self.fail "Could not retrieve dependency '#{reference}' of #{@resource.ref}"
end
# Are we requiring them, or vice versa? See the method docs
# for futher info on this.
if self.class.direction == :in
source = related_resource
target = @resource
else
source = @resource
target = related_resource
end
if method = self.class.callback
subargs = {
:event => self.class.events,
:callback => method
}
self.debug("subscribes to #{related_resource.ref}")
else
# If there's no callback, there's no point in even adding
# a label.
subargs = nil
self.debug("requires #{related_resource.ref}")
end
Puppet::Relationship.new(source, target, subargs)
end
end
end
# @todo document this, have no clue what this does... it retuns "RelationshipMetaparam.subclasses"
#
def self.relationship_params
RelationshipMetaparam.subclasses
end
# Note that the order in which the relationships params is defined
# matters. The labelled params (notify and subcribe) must be later,
# so that if both params are used, those ones win. It's a hackish
# solution, but it works.
newmetaparam(:require, :parent => RelationshipMetaparam, :attributes => {:direction => :in, :events => :NONE}) do
desc "One or more resources that this resource depends on, expressed as
[resource references](http://docs.puppetlabs.com/puppet/latest/reference/lang_datatypes.html#resource-references).
Multiple resources can be specified as an array of references. When this
attribute is present:
* The required resource(s) will be applied **before** this resource.
This is one of the four relationship metaparameters, along with
`before`, `notify`, and `subscribe`. For more context, including the
alternate chaining arrow (`->` and `~>`) syntax, see
[the language page on relationships](http://docs.puppetlabs.com/puppet/latest/reference/lang_relationships.html)."
end
newmetaparam(:subscribe, :parent => RelationshipMetaparam, :attributes => {:direction => :in, :events => :ALL_EVENTS, :callback => :refresh}) do
desc "One or more resources that this resource depends on, expressed as
[resource references](http://docs.puppetlabs.com/puppet/latest/reference/lang_datatypes.html#resource-references).
Multiple resources can be specified as an array of references. When this
attribute is present:
* The subscribed resource(s) will be applied _before_ this resource.
* If Puppet makes changes to any of the subscribed resources, it will cause
this resource to _refresh._ (Refresh behavior varies by resource
type: services will restart, mounts will unmount and re-mount, etc. Not
all types can refresh.)
This is one of the four relationship metaparameters, along with
`before`, `require`, and `notify`. For more context, including the
alternate chaining arrow (`->` and `~>`) syntax, see
[the language page on relationships](http://docs.puppetlabs.com/puppet/latest/reference/lang_relationships.html)."
end
newmetaparam(:before, :parent => RelationshipMetaparam, :attributes => {:direction => :out, :events => :NONE}) do
desc "One or more resources that depend on this resource, expressed as
[resource references](http://docs.puppetlabs.com/puppet/latest/reference/lang_datatypes.html#resource-references).
Multiple resources can be specified as an array of references. When this
attribute is present:
* This resource will be applied _before_ the dependent resource(s).
This is one of the four relationship metaparameters, along with
`require`, `notify`, and `subscribe`. For more context, including the
alternate chaining arrow (`->` and `~>`) syntax, see
[the language page on relationships](http://docs.puppetlabs.com/puppet/latest/reference/lang_relationships.html)."
end
newmetaparam(:notify, :parent => RelationshipMetaparam, :attributes => {:direction => :out, :events => :ALL_EVENTS, :callback => :refresh}) do
desc "One or more resources that depend on this resource, expressed as
[resource references](http://docs.puppetlabs.com/puppet/latest/reference/lang_datatypes.html#resource-references).
Multiple resources can be specified as an array of references. When this
attribute is present:
* This resource will be applied _before_ the notified resource(s).
* If Puppet makes changes to this resource, it will cause all of the
notified resources to _refresh._ (Refresh behavior varies by resource
type: services will restart, mounts will unmount and re-mount, etc. Not
all types can refresh.)
This is one of the four relationship metaparameters, along with
`before`, `require`, and `subscribe`. For more context, including the
alternate chaining arrow (`->` and `~>`) syntax, see
[the language page on relationships](http://docs.puppetlabs.com/puppet/latest/reference/lang_relationships.html)."
end
newmetaparam(:stage) do
desc %{Which run stage this class should reside in.
**Note: This metaparameter can only be used on classes,** and only when
declaring them with the resource-like syntax. It cannot be used on normal
resources or on classes declared with `include`.
By default, all classes are declared in the `main` stage. To assign a class
to a different stage, you must:
* Declare the new stage as a [`stage` resource](http://docs.puppetlabs.com/references/latest/type.html#stage).
* Declare an order relationship between the new stage and the `main` stage.
* Use the resource-like syntax to declare the class, and set the `stage`
metaparameter to the name of the desired stage.
For example:
stage { 'pre':
before => Stage['main'],
}
class { 'apt-updates':
stage => 'pre',
}
}
end
###############################
# All of the provider plumbing for the resource types.
require 'puppet/provider'
require 'puppet/util/provider_features'
# Add the feature handling module.
extend Puppet::Util::ProviderFeatures
# The provider that has been selected for the instance of the resource type.
# @return [Puppet::Provider,nil] the selected provider or nil, if none has been selected
#
attr_reader :provider
# the Type class attribute accessors
class << self
# The loader of providers to use when loading providers from disk.
# Although it looks like this attribute provides a way to operate with different loaders of
# providers that is not the case; the attribute is written when a new type is created,
# and should not be changed thereafter.
# @api private
#
attr_accessor :providerloader
# @todo Don't know if this is a name, or a reference to a Provider instance (now marked up as an instance
# of Provider.
# @return [Puppet::Provider, nil] The default provider for this type, or nil if non is defines
#
attr_writer :defaultprovider
end
# The default provider, or the most suitable provider if no default provider was set.
# @note a warning will be issued if no default provider has been configured and a search for the most
# suitable provider returns more than one equally suitable provider.
# @return [Puppet::Provider, nil] the default or most suitable provider, or nil if no provider was found
#
def self.defaultprovider
return @defaultprovider if @defaultprovider
suitable = suitableprovider
# Find which providers are a default for this system.
defaults = suitable.find_all { |provider| provider.default? }
# If we don't have any default we use suitable providers
defaults = suitable if defaults.empty?
max = defaults.collect { |provider| provider.specificity }.max
defaults = defaults.find_all { |provider| provider.specificity == max }
if defaults.length > 1
Puppet.warning(
"Found multiple default providers for #{self.name}: #{defaults.collect { |i| i.name.to_s }.join(", ")}; using #{defaults[0].name}"
)
end
@defaultprovider = defaults.shift unless defaults.empty?
end
# @return [Hash{??? => Puppet::Provider}] Returns a hash of WHAT EXACTLY for the given type
# @todo what goes into this hash?
def self.provider_hash_by_type(type)
@provider_hashes ||= {}
@provider_hashes[type] ||= {}
end
# @return [Hash{ ??? => Puppet::Provider}] Returns a hash of WHAT EXACTLY for this type.
# @see provider_hash_by_type method to get the same for some other type
def self.provider_hash
Puppet::Type.provider_hash_by_type(self.name)
end
# Returns the provider having the given name.
# This will load a provider if it is not already loaded. The returned provider is the first found provider
# having the given name, where "first found" semantics is defined by the {providerloader} in use.
#
# @param name [String] the name of the provider to get
# @return [Puppet::Provider, nil] the found provider, or nil if no provider of the given name was found
#
def self.provider(name)
name = name.intern
# If we don't have it yet, try loading it.
@providerloader.load(name) unless provider_hash.has_key?(name)
provider_hash[name]
end
# Returns a list of loaded providers by name.
# This method will not load/search for available providers.
# @return [Array<String>] list of loaded provider names
#
def self.providers
provider_hash.keys
end
# Returns true if the given name is a reference to a provider and if this is a suitable provider for
# this type.
# @todo How does the provider know if it is suitable for the type? Is it just suitable for the platform/
# environment where this method is executing?
# @param name [String] the name of the provider for which validity is checked
# @return [Boolean] true if the given name references a provider that is suitable
#
def self.validprovider?(name)
name = name.intern
(provider_hash.has_key?(name) && provider_hash[name].suitable?)
end
# Creates a new provider of a type.
# This method must be called directly on the type that it's implementing.
# @todo Fix Confusing Explanations!
# Is this a new provider of a Type (metatype), or a provider of an instance of Type (a resource), or
# a Provider (the implementation of a Type's behavior). CONFUSED. It calls magically named methods like
# "providify" ...
# @param name [String, Symbol] the name of the WHAT? provider? type?
# @param options [Hash{Symbol => Object}] a hash of options, used by this method, and passed on to {#genclass}, (see
# it for additional options to pass).
# @option options [Puppet::Provider] :parent the parent provider (what is this?)
# @option options [Puppet::Type] :resource_type the resource type, defaults to this type if unspecified
# @return [Puppet::Provider] a provider ???
# @raise [Puppet::DevError] when the parent provider could not be found.
#
def self.provide(name, options = {}, &block)
name = name.intern
if unprovide(name)
Puppet.debug "Reloading #{name} #{self.name} provider"
end
parent = if pname = options[:parent]
options.delete(:parent)
if pname.is_a? Class
pname
else
if provider = self.provider(pname)
provider
else
raise Puppet::DevError,
"Could not find parent provider #{pname} of #{name}"
end
end
else
Puppet::Provider
end
options[:resource_type] ||= self
self.providify
provider = genclass(
name,
:parent => parent,
:hash => provider_hash,
:prefix => "Provider",
:block => block,
:include => feature_module,
:extend => feature_module,
:attributes => options
)
provider
end
# Ensures there is a `:provider` parameter defined.
# Should only be called if there are providers.
# @return [void]
def self.providify
return if @paramhash.has_key? :provider
newparam(:provider) do
# We're using a hacky way to get the name of our type, since there doesn't
# seem to be a correct way to introspect this at the time this code is run.
# We expect that the class in which this code is executed will be something
# like Puppet::Type::Ssh_authorized_key::ParameterProvider.
desc <<-EOT
The specific backend to use for this `#{self.to_s.split('::')[2].downcase}`
resource. You will seldom need to specify this --- Puppet will usually
discover the appropriate provider for your platform.
EOT
# This is so we can refer back to the type to get a list of
# providers for documentation.
class << self
# The reference to a parent type for the parameter `:provider` used to get a list of
# providers for documentation purposes.
#
attr_accessor :parenttype
end
# Provides the ability to add documentation to a provider.
#
def self.doc
# Since we're mixing @doc with text from other sources, we must normalize
# its indentation with scrub. But we don't need to manually scrub the
# provider's doc string, since markdown_definitionlist sanitizes its inputs.
scrub(@doc) + "Available providers are:\n\n" + parenttype.providers.sort { |a,b|
a.to_s <=> b.to_s
}.collect { |i|
markdown_definitionlist( i, scrub(parenttype().provider(i).doc) )
}.join
end
# @todo this does what? where and how?
# @return [String] the name of the provider
defaultto {
prov = @resource.class.defaultprovider
prov.name if prov
}
validate do |provider_class|
provider_class = provider_class[0] if provider_class.is_a? Array
provider_class = provider_class.class.name if provider_class.is_a?(Puppet::Provider)
unless @resource.class.provider(provider_class)
raise ArgumentError, "Invalid #{@resource.class.name} provider '#{provider_class}'"
end
end
munge do |provider|
provider = provider[0] if provider.is_a? Array
provider = provider.intern if provider.is_a? String
@resource.provider = provider
if provider.is_a?(Puppet::Provider)
provider.class.name
else
provider
end
end
end.parenttype = self
end
# @todo this needs a better explanation
# Removes the implementation class of a given provider.
# @return [Object] returns what {Puppet::Util::ClassGen#rmclass} returns
def self.unprovide(name)
if @defaultprovider and @defaultprovider.name == name
@defaultprovider = nil
end
rmclass(name, :hash => provider_hash, :prefix => "Provider")
end
# Returns a list of suitable providers for the given type.
# A call to this method will load all providers if not already loaded and ask each if it is
# suitable - those that are are included in the result.
# @note This method also does some special processing which rejects a provider named `:fake` (for testing purposes).
# @return [Array<Puppet::Provider>] Returns an array of all suitable providers.
#
def self.suitableprovider
providerloader.loadall if provider_hash.empty?
provider_hash.find_all { |name, provider|
provider.suitable?
}.collect { |name, provider|
provider
}.reject { |p| p.name == :fake } # For testing
end
# @return [Boolean] Returns true if this is something else than a `:provider`, or if it
# is a provider and it is suitable, or if there is a default provider. Otherwise, false is returned.
#
def suitable?
# If we don't use providers, then we consider it suitable.
return true unless self.class.paramclass(:provider)
# We have a provider and it is suitable.
return true if provider && provider.class.suitable?
# We're using the default provider and there is one.
if !provider and self.class.defaultprovider
self.provider = self.class.defaultprovider.name
return true
end
# We specified an unsuitable provider, or there isn't any suitable
# provider.
false
end
# Sets the provider to the given provider/name.
# @overload provider=(name)
# Sets the provider to the result of resolving the name to an instance of Provider.
# @param name [String] the name of the provider
# @overload provider=(provider)
# Sets the provider to the given instances of Provider.
# @param provider [Puppet::Provider] the provider to set
# @return [Puppet::Provider] the provider set
# @raise [ArgumentError] if the provider could not be found/resolved.
#
def provider=(name)
if name.is_a?(Puppet::Provider)
@provider = name
@provider.resource = self
elsif klass = self.class.provider(name)
@provider = klass.new(self)
else
raise ArgumentError, "Could not find #{name} provider of #{self.class.name}"
end
end
###############################
# All of the relationship code.
# Adds a block producing a single name (or list of names) of the given resource type name to autorequire.
# @example Autorequire the files File['foo', 'bar']
# autorequire( 'file', {|| ['foo', 'bar'] })
#
# @todo original = _"Specify a block for generating a list of objects to autorequire.
# This makes it so that you don't have to manually specify things that you clearly require."_
# @param name [String] the name of a type of which one or several resources should be autorequired e.g. "file"
# @yield [ ] a block returning list of names of given type to auto require
# @yieldreturn [String, Array<String>] one or several resource names for the named type
# @return [void]
# @dsl type
# @api public
#
def self.autorequire(name, &block)
@autorequires ||= {}
@autorequires[name] = block
end
# Provides iteration over added auto-requirements (see {autorequire}).
# @yieldparam type [String] the name of the type to autoriquire an instance of
# @yieldparam block [Proc] a block producing one or several dependencies to auto require (see {autorequire}).
# @yieldreturn [void]
# @return [void]
def self.eachautorequire
@autorequires ||= {}
@autorequires.each { |type, block|
yield(type, block)
}
end
# Adds dependencies to the catalog from added autorequirements.
# See {autorequire} for how to add an auto-requirement.
# @todo needs details - see the param rel_catalog, and type of this param
# @param rel_catalog [Puppet::Resource::Catalog, nil] the catalog to
# add dependencies to. Defaults to the current catalog (set when the
# type instance was added to a catalog)
# @raise [Puppet::DevError] if there is no catalog
#
def autorequire(rel_catalog = nil)
rel_catalog ||= catalog
raise(Puppet::DevError, "You cannot add relationships without a catalog") unless rel_catalog
reqs = []
self.class.eachautorequire { |type, block|
# Ignore any types we can't find, although that would be a bit odd.
next unless Puppet::Type.type(type)
# Retrieve the list of names from the block.
next unless list = self.instance_eval(&block)
list = [list] unless list.is_a?(Array)
# Collect the current prereqs
list.each { |dep|
# Support them passing objects directly, to save some effort.
unless dep.is_a? Puppet::Type
# Skip autorequires that we aren't managing
unless dep = rel_catalog.resource(type, dep)
next
end
end
reqs << Puppet::Relationship.new(dep, self)
}
}
reqs
end
# Builds the dependencies associated with an individual object.
# @todo Which object is the "individual object", as opposed to "object as a group?" or should it simply
# be "this object" as in "this resource" ?
# @todo Does this method "build dependencies" or "build what it depends on" ... CONFUSING
#
# @return [Array<???>] list of WHAT? resources? edges?
def builddepends
# Handle the requires
self.class.relationship_params.collect do |klass|
if param = @parameters[klass.name]
param.to_edges
end
end.flatten.reject { |r| r.nil? }
end
# Sets the initial list of tags...
# @todo The initial list of tags, that ... that what?
# @return [void] ???
def tags=(list)
tag(self.class.name)
tag(*list)
end
# @comment - these two comments were floating around here, and turned up as documentation
# for the attribute "title", much to my surprise and amusement. Clearly these comments
# are orphaned ... I think they can just be removed as what they say should be covered
# by the now added yardoc. <irony>(Yo! to quote some of the other actual awsome specific comments applicable
# to objects called from elsewhere, or not. ;-)</irony>
#
# @comment Types (which map to resources in the languages) are entirely composed of
# attribute value pairs. Generally, Puppet calls any of these things an
# 'attribute', but these attributes always take one of three specific
# forms: parameters, metaparams, or properties.
# @comment In naming methods, I have tried to consistently name the method so
# that it is clear whether it operates on all attributes (thus has 'attr' in
# the method name, or whether it operates on a specific type of attributes.
# The title attribute of WHAT ???
# @todo Figure out what this is the title attribute of (it appears on line 1926 currently).
# @return [String] the title
attr_writer :title
# The noop attribute of WHAT ??? does WHAT???
# @todo Figure out what this is the noop attribute of (it appears on line 1931 currently).
# @return [???] the noop WHAT ??? (mode? if so of what, or noop for an instance of the type, or for all
# instances of a type, or for what???
#
attr_writer :noop
include Enumerable
# class methods dealing with Type management
public
# The Type class attribute accessors
class << self
# @return [String] the name of the resource type; e.g., "File"
#
attr_reader :name
# @return [Boolean] true if the type should send itself a refresh event on change.
#
attr_accessor :self_refresh
include Enumerable, Puppet::Util::ClassGen
include Puppet::MetaType::Manager
include Puppet::Util
include Puppet::Util::Logging
end
# Initializes all of the variables that must be initialized for each subclass.
# @todo Does the explanation make sense?
# @return [void]
def self.initvars
# all of the instances of this class
@objects = Hash.new
@aliases = Hash.new
@defaults = {}
@parameters ||= []
@validproperties = {}
@properties = []
@parameters = []
@paramhash = {}
@paramdoc = Hash.new { |hash,key|
key = key.intern if key.is_a?(String)
if hash.include?(key)
hash[key]
else
"Param Documentation for #{key} not found"
end
}
@doc ||= ""
end
# Returns the name of this type (if specified) or the parent type #to_s.
# The returned name is on the form "Puppet::Type::<name>", where the first letter of name is
# capitalized.
# @return [String] the fully qualified name Puppet::Type::<name> where the first letter of name is captialized
#
def self.to_s
if defined?(@name)
"Puppet::Type::#{@name.to_s.capitalize}"
else
super
end
end
# Creates a `validate` method that is used to validate a resource before it is operated on.
# The validation should raise exceptions if the validation finds errors. (It is not recommended to
# issue warnings as this typically just ends up in a logfile - you should fail if a validation fails).
# The easiest way to raise an appropriate exception is to call the method {Puppet::Util::Errors.fail} with
# the message as an argument.
#
# @yield [ ] a required block called with self set to the instance of a Type class representing a resource.
# @return [void]
# @dsl type
# @api public
#
def self.validate(&block)
define_method(:validate, &block)
end
# @return [String] The file from which this type originates from
attr_accessor :file
# @return [Integer] The line in {#file} from which this type originates from
attr_accessor :line
# @todo what does this mean "this resource" (sounds like this if for an instance of the type, not the meta Type),
# but not sure if this is about the catalog where the meta Type is included)
# @return [??? TODO] The catalog that this resource is stored in.
attr_accessor :catalog
# @return [Boolean] Flag indicating if this type is exported
attr_accessor :exported
# @return [Boolean] Flag indicating if the type is virtual (it should not be).
attr_accessor :virtual
# Creates a log entry with the given message at the log level specified by the parameter `loglevel`
# @return [void]
#
def log(msg)
Puppet::Util::Log.create(
:level => @parameters[:loglevel].value,
:message => msg,
:source => self
)
end
# instance methods related to instance intrinsics
# e.g., initialize and name
public
# @return [Hash] hash of parameters originally defined
# @api private
attr_reader :original_parameters
# Creates an instance of Type from a hash or a {Puppet::Resource}.
# @todo Unclear if this is a new Type or a new instance of a given type (the initialization ends
# with calling validate - which seems like validation of an instance of a given type, not a new
# meta type.
#
# @todo Explain what the Hash and Resource are. There seems to be two different types of
# resources; one that causes the title to be set to resource.title, and one that
# causes the title to be resource.ref ("for components") - what is a component?
#
# @overload initialize(hash)
# @param [Hash] hash
# @raise [Puppet::ResourceError] when the type validation raises
# Puppet::Error or ArgumentError
# @overload initialize(resource)
# @param resource [Puppet:Resource]
# @raise [Puppet::ResourceError] when the type validation raises
# Puppet::Error or ArgumentError
#
def initialize(resource)
resource = self.class.hash2resource(resource) unless resource.is_a?(Puppet::Resource)
# The list of parameter/property instances.
@parameters = {}
# Set the title first, so any failures print correctly.
if resource.type.to_s.downcase.to_sym == self.class.name
self.title = resource.title
else
# This should only ever happen for components
self.title = resource.ref
end
[:file, :line, :catalog, :exported, :virtual].each do |getter|
setter = getter.to_s + "="
if val = resource.send(getter)
self.send(setter, val)
end
end
@tags = resource.tags
@original_parameters = resource.to_hash
set_name(@original_parameters)
set_default(:provider)
set_parameters(@original_parameters)
begin
self.validate if self.respond_to?(:validate)
rescue Puppet::Error, ArgumentError => detail
error = Puppet::ResourceError.new("Validation of #{ref} failed: #{detail}")
adderrorcontext(error, detail)
raise error
end
end
private
# Sets the name of the resource from a hash containing a mapping of `name_var` to value.
# Sets the value of the property/parameter appointed by the `name_var` (if it is defined). The value set is
# given by the corresponding entry in the given hash - e.g. if name_var appoints the name `:path` the value
# of `:path` is set to the value at the key `:path` in the given hash. As a side effect this key/value is then
# removed from the given hash.
#
# @note This method mutates the given hash by removing the entry with a key equal to the value
# returned from name_var!
# @param hash [Hash] a hash of what
# @return [void]
def set_name(hash)
self[name_var] = hash.delete(name_var) if name_var
end
# Sets parameters from the given hash.
# Values are set in _attribute order_ i.e. higher priority attributes before others, otherwise in
# the order they were specified (as opposed to just setting them in the order they happen to appear in
# when iterating over the given hash).
#
# Attributes that are not included in the given hash are set to their default value.
#
# @todo Is this description accurate? Is "ensure" an example of such a higher priority attribute?
# @return [void]
# @raise [Puppet::DevError] when impossible to set the value due to some problem
# @raise [ArgumentError, TypeError, Puppet::Error] when faulty arguments have been passed
#
def set_parameters(hash)
# Use the order provided by allattrs, but add in any
# extra attributes from the resource so we get failures
# on invalid attributes.
no_values = []
(self.class.allattrs + hash.keys).uniq.each do |attr|
begin
# Set any defaults immediately. This is mostly done so
# that the default provider is available for any other
# property validation.
if hash.has_key?(attr)
self[attr] = hash[attr]
else
no_values << attr
end
rescue ArgumentError, Puppet::Error, TypeError
raise
rescue => detail
error = Puppet::DevError.new( "Could not set #{attr} on #{self.class.name}: #{detail}")
error.set_backtrace(detail.backtrace)
raise error
end
end
no_values.each do |attr|
set_default(attr)
end
end
public
# Finishes any outstanding processing.
# This method should be called as a final step in setup,
# to allow the parameters that have associated auto-require needs to be processed.
#
# @todo what is the expected sequence here - who is responsible for calling this? When?
# Is the returned type correct?
# @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|
if param = @parameters[klass.name]
param.validate_relationship
end
end.flatten.reject { |r| r.nil? }
end
# @comment For now, leave the 'name' method functioning like it used to. Once 'title'
# works everywhere, I'll switch it.
# Returns the resource's name
# @todo There is a comment in source that this is not quite the same as ':title' and that a switch should
# be made...
# @return [String] the name of a resource
def name
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 [Puppet::Type, nil] the
# containing resource or nil if there is no catalog or no containing
# resource.
def parent
return nil unless catalog
@parent ||=
if parents = catalog.adjacent(self, :direction => :in)
parents.shift
else
nil
end
end
# Returns a reference to this as a string in "Type[name]" format.
# @return [String] a reference to this object on the form 'Type[name]'
#
def ref
# memoizing this is worthwhile ~ 3 percent of calls are the "first time
# around" in an average run of Puppet. --daniel 2012-07-17
@ref ||= "#{self.class.name.to_s.capitalize}[#{self.title}]"
end
# (see self_refresh)
# @todo check that meaningful yardoc is produced - this method delegates to "self.class.self_refresh"
# @return [Boolean] - ??? returns true when ... what?
#
def self_refresh?
self.class.self_refresh
end
# Marks the object as "being purged".
# This method is used by transactions to forbid deletion when there are dependencies.
# @todo what does this mean; "mark that we are purging" (purging what from where). How to use/when?
# Is this internal API in transactions?
# @see purging?
def purging
@purging = true
end
# Returns whether this resource is being purged or not.
# This method is used by transactions to forbid deletion when there are dependencies.
# @return [Boolean] the current "purging" state
#
def purging?
if defined?(@purging)
@purging
else
false
end
end
# Returns the title of this object, or its name if title was not explicetly set.
# If the title is not already set, it will be computed by looking up the {#name_var} and using
# that value as the title.
# @todo it is somewhat confusing that if the name_var is a valid parameter, it is assumed to
# be the name_var called :name, but if it is a property, it uses the name_var.
# It is further confusing as Type in some respects supports multiple namevars.
#
# @return [String] Returns the title of this object, or its name if title was not explicetly set.
# @raise [??? devfail] if title is not set, and name_var can not be found.
def title
unless @title
if self.class.validparameter?(name_var)
@title = self[:name]
elsif self.class.validproperty?(name_var)
@title = self.should(name_var)
else
self.devfail "Could not find namevar #{name_var} for #{self.class.name}"
end
end
@title
end
# Produces a reference to this in reference format.
# @see #ref
#
def to_s
self.ref
end
# @todo What to resource? Which one of the resource forms is prroduced? returned here?
# @return [??? Resource] a resource that WHAT???
#
def to_resource
resource = self.retrieve_resource
resource.tag(*self.tags)
@parameters.each do |name, param|
# Avoid adding each instance name twice
next if param.class.isnamevar? and param.value == self.title
# We've already got property values
next if param.is_a?(Puppet::Property)
resource[name] = param.value
end
resource
end
# @return [Boolean] Returns whether the resource is virtual or not
def virtual?; !!@virtual; end
# @return [Boolean] Returns whether the resource is exported or not
def exported?; !!@exported; end
# @return [Boolean] Returns whether the resource is applicable to `:device`
# Returns true if a resource of this type can be evaluated on a 'network device' kind
# of hosts.
# @api private
def appliable_to_device?
self.class.can_apply_to(:device)
end
# @return [Boolean] Returns whether the resource is applicable to `:host`
# Returns true if a resource of this type can be evaluated on a regular generalized computer (ie not an appliance like a network device)
# @api private
def appliable_to_host?
self.class.can_apply_to(:host)
end
end
end
require 'puppet/provider'
-
-# Always load these types.
-Puppet::Type.type(:component)
diff --git a/lib/puppet/type/augeas.rb b/lib/puppet/type/augeas.rb
index 930235f0c..47cb58f17 100644
--- a/lib/puppet/type/augeas.rb
+++ b/lib/puppet/type/augeas.rb
@@ -1,188 +1,189 @@
#
# Copyright 2011 Bryan Kearney <bkearney@redhat.com>
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
Puppet::Type.newtype(:augeas) do
include Puppet::Util
feature :parse_commands, "Parse the command string"
feature :need_to_run?, "If the command should run"
feature :execute_changes, "Actually make the changes"
@doc = <<-'EOT'
Apply a change or an array of changes to the filesystem
using the augeas tool.
Requires:
- [Augeas](http://www.augeas.net)
- The ruby-augeas bindings
Sample usage with a string:
augeas{"test1" :
context => "/files/etc/sysconfig/firstboot",
changes => "set RUN_FIRSTBOOT YES",
onlyif => "match other_value size > 0",
}
Sample usage with an array and custom lenses:
augeas{"jboss_conf":
context => "/files",
changes => [
"set etc/jbossas/jbossas.conf/JBOSS_IP $ipaddress",
"set etc/jbossas/jbossas.conf/JAVA_HOME /usr",
],
load_path => "$/usr/share/jbossas/lenses",
}
EOT
newparam (:name) do
desc "The name of this task. Used for uniqueness."
isnamevar
end
newparam (:context) do
desc "Optional context path. This value is prepended to the paths of all
changes if the path is relative. If the `incl` parameter is set,
defaults to `/files + incl`; otherwise, defaults to the empty string."
defaultto ""
munge do |value|
if value.empty? and resource[:incl]
"/files" + resource[:incl]
else
value
end
end
end
newparam (:onlyif) do
desc "Optional augeas command and comparisons to control the execution of this type.
Supported onlyif syntax:
* `get <AUGEAS_PATH> <COMPARATOR> <STRING>`
* `match <MATCH_PATH> size <COMPARATOR> <INT>`
* `match <MATCH_PATH> include <STRING>`
* `match <MATCH_PATH> not_include <STRING>`
* `match <MATCH_PATH> == <AN_ARRAY>`
* `match <MATCH_PATH> != <AN_ARRAY>`
where:
* `AUGEAS_PATH` is a valid path scoped by the context
* `MATCH_PATH` is a valid match synatx scoped by the context
* `COMPARATOR` is one of `>, >=, !=, ==, <=,` or `<`
* `STRING` is a string
* `INT` is a number
* `AN_ARRAY` is in the form `['a string', 'another']`"
defaultto ""
end
newparam(:changes) 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
If the `context` parameter is set, that value is prepended to any relative `PATH`s."
end
newparam(:root) do
desc "A file system path; all files loaded by Augeas are loaded underneath `root`."
defaultto "/"
end
newparam(:load_path) do
desc "Optional colon-separated list or array of directories; these directories are searched for schema definitions. The agent's `$libdir/augeas/lenses` path will always be added to support pluginsync."
defaultto ""
end
newparam(:force) do
desc "Optional command to force the augeas type to execute even if it thinks changes
will not be made. This does not overide the `onlyif` parameter."
defaultto false
end
newparam(:type_check) do
desc "Whether augeas should perform typechecking. Defaults to false."
newvalues(:true, :false)
defaultto :false
end
newparam(:lens) do
desc "Use a specific lens, e.g. `Hosts.lns`. When this parameter is set, you
- must also set the `incl` parameter to indicate which file to load."
+ must also set the `incl` parameter to indicate which file to load.
+ The Augeas documentation includes [a list of available lenses](http://augeas.net/stock_lenses.html)."
end
newparam(:incl) do
desc "Load only a specific file, e.g. `/etc/hosts`. This can greatly speed
up the execution the resource. When this parameter is set, you must also
set the `lens` parameter to indicate which lens to use."
end
validate do
has_lens = !self[:lens].nil?
has_incl = !self[:incl].nil?
self.fail "You must specify both the lens and incl parameters, or neither." if has_lens != has_incl
end
# This is the actual meat of the code. It forces
# augeas to be run and fails or not based on the augeas return
# code.
newproperty(:returns) do |property|
include Puppet::Util
desc "The expected return code from the augeas command. Should not be set."
defaultto 0
# Make output a bit prettier
def change_to_s(currentvalue, newvalue)
"executed successfully"
end
# if the onlyif resource is provided, then the value is parsed.
# a return value of 0 will stop exection because it matches the
# default value.
def retrieve
if @resource.provider.need_to_run?()
:need_to_run
else
0
end
end
# Actually execute the command.
def sync
@resource.provider.execute_changes
end
end
end
diff --git a/lib/puppet/type/cron.rb b/lib/puppet/type/cron.rb
index 198d6c171..f7a49b57d 100644
--- a/lib/puppet/type/cron.rb
+++ b/lib/puppet/type/cron.rb
@@ -1,444 +1,474 @@
require 'etc'
require 'facter'
require 'puppet/util/filetype'
Puppet::Type.newtype(:cron) do
@doc = <<-'EOT'
Installs and manages cron jobs. Every cron resource requires a command
and user attribute, as well as at least one periodic attribute (hour,
minute, month, monthday, weekday, or special). While the name of the cron
- job is not part of the actual job, it is used by Puppet to store and
- retrieve it.
+ job is not part of the actual job, the name is stored in a comment beginning with
+ `# Puppet Name: `. These comments are used to match crontab entries created by
+ Puppet with cron resources.
- If you specify a cron resource that duplicates the scheduling and command
- used by an existing crontab entry, then Puppet will take no action and
- defers to the existing crontab entry. If the duplicate cron resource
- specifies `ensure => absent`, all existing duplicated crontab entries will
- be removed. Specifying multiple duplicate cron resources with different
- `ensure` states will result in undefined behavior.
+ If an existing crontab entry happens to match the scheduling and command of a
+ cron resource that has never been synched, Puppet will defer to the existing
+ crontab entry and will not create a new entry tagged with the `# Puppet Name: `
+ comment.
Example:
cron { logrotate:
command => "/usr/sbin/logrotate",
user => root,
hour => 2,
minute => 0
}
Note that all periodic attributes can be specified as an array of values:
cron { logrotate:
command => "/usr/sbin/logrotate",
user => root,
hour => [2, 4]
}
...or using ranges or the step syntax `*/2` (although there's no guarantee
that your `cron` daemon supports these):
cron { logrotate:
command => "/usr/sbin/logrotate",
user => root,
hour => ['2-4'],
minute => '*/10'
}
An important note: _the Cron type will not reset parameters that are
removed from a manifest_. For example, removing a `minute => 10` parameter
will not reset the minute component of the associated cronjob to `*`.
These changes must be expressed by setting the parameter to
`minute => absent` because Puppet only manages parameters that are out of
sync with manifest entries.
EOT
ensurable
# A base class for all of the Cron parameters, since they all have
# similar argument checking going on.
class CronParam < Puppet::Property
class << self
attr_accessor :boundaries, :default
end
# We have to override the parent method, because we consume the entire
# "should" array
def insync?(is)
self.is_to_s(is) == self.should_to_s
end
# A method used to do parameter input handling. Converts integers
# in string form to actual integers, and returns the value if it's
# an integer or false if it's just a normal string.
def numfix(num)
if num =~ /^\d+$/
return num.to_i
elsif num.is_a?(Integer)
return num
else
return false
end
end
# Verify that a number is within the specified limits. Return the
# number if it is, or false if it is not.
def limitcheck(num, lower, upper)
(num >= lower and num <= upper) && num
end
# Verify that a value falls within the specified array. Does case
# insensitive matching, and supports matching either the entire word
# or the first three letters of the word.
def alphacheck(value, ary)
tmp = value.downcase
# If they specified a shortened version of the name, then see
# if we can lengthen it (e.g., mon => monday).
if tmp.length == 3
ary.each_with_index { |name, index|
if tmp.upcase == name[0..2].upcase
return index
end
}
else
return ary.index(tmp) if ary.include?(tmp)
end
false
end
def should_to_s(newvalue = @should)
if newvalue
newvalue = [newvalue] unless newvalue.is_a?(Array)
if self.name == :command or newvalue[0].is_a? Symbol
newvalue[0]
else
newvalue.join(",")
end
else
nil
end
end
def is_to_s(currentvalue = @is)
if currentvalue
return currentvalue unless currentvalue.is_a?(Array)
if self.name == :command or currentvalue[0].is_a? Symbol
currentvalue[0]
else
currentvalue.join(",")
end
else
nil
end
end
def should
if @should and @should[0] == :absent
:absent
else
@should
end
end
def should=(ary)
super
@should.flatten!
end
# The method that does all of the actual parameter value
# checking; called by all of the +param<name>=+ methods.
# Requires the value, type, and bounds, and optionally supports
# a boolean of whether to do alpha checking, and if so requires
# the ary against which to do the checking.
munge do |value|
# Support 'absent' as a value, so that they can remove
# a value
if value == "absent" or value == :absent
return :absent
end
# Allow the */2 syntax
if value =~ /^\*\/[0-9]+$/
return value
end
# Allow ranges
if value =~ /^[0-9]+-[0-9]+$/
return value
end
# Allow ranges + */2
if value =~ /^[0-9]+-[0-9]+\/[0-9]+$/
return value
end
if value == "*"
return :absent
end
return value unless self.class.boundaries
lower, upper = self.class.boundaries
retval = nil
if num = numfix(value)
retval = limitcheck(num, lower, upper)
elsif respond_to?(:alpha)
# If it has an alpha method defined, then we check
# to see if our value is in that list and if so we turn
# it into a number
retval = alphacheck(value, alpha)
end
if retval
return retval.to_s
else
self.fail "#{value} is not a valid #{self.class.name}"
end
end
end
# Somewhat uniquely, this property does not actually change anything -- it
# just calls +@resource.sync+, which writes out the whole cron tab for
# the user in question. There is no real way to change individual cron
# jobs without rewriting the entire cron file.
#
# Note that this means that managing many cron jobs for a given user
# could currently result in multiple write sessions for that user.
newproperty(:command, :parent => CronParam) do
desc "The command to execute in the cron job. The environment
provided to the command varies by local system rules, and it is
best to always provide a fully qualified command. The user's
profile is not sourced when the command is run, so if the
user's environment is desired it should be sourced manually.
All cron parameters support `absent` as a value; this will
remove any existing values for that field."
def retrieve
return_value = super
return_value = return_value[0] if return_value && return_value.is_a?(Array)
return_value
end
def should
if @should
if @should.is_a? Array
@should[0]
else
devfail "command is not an array"
end
else
nil
end
end
def munge(value)
- value.sub!(/^\s+/, '')
- value.sub!(/\s+$/, '')
- value
+ value.strip
end
end
newproperty(:special) do
desc "A special value such as 'reboot' or 'annually'.
Only available on supported systems such as Vixie Cron.
Overrides more specific time of day/week settings.
Set to 'absent' to make puppet revert to a plain numeric schedule."
def specials
%w{reboot yearly annually monthly weekly daily midnight hourly absent} +
[ :absent ]
end
validate do |value|
raise ArgumentError, "Invalid special schedule #{value.inspect}" unless specials.include?(value)
end
def munge(value)
# Support value absent so that a schedule can be
# forced to change to numeric.
if value == "absent" or value == :absent
return :absent
end
value
end
end
newproperty(:minute, :parent => CronParam) do
self.boundaries = [0, 59]
desc "The minute at which to run the cron job.
Optional; if specified, must be between 0 and 59, inclusive."
end
newproperty(:hour, :parent => CronParam) do
self.boundaries = [0, 23]
desc "The hour at which to run the cron job. Optional;
if specified, must be between 0 and 23, inclusive."
end
newproperty(:weekday, :parent => CronParam) do
def alpha
%w{sunday monday tuesday wednesday thursday friday saturday}
end
self.boundaries = [0, 7]
desc "The weekday on which to run the command.
Optional; if specified, must be between 0 and 7, inclusive, with
0 (or 7) being Sunday, or must be the name of the day (e.g., Tuesday)."
end
newproperty(:month, :parent => CronParam) do
def alpha
%w{january february march april may june july
august september october november december}
end
self.boundaries = [1, 12]
desc "The month of the year. Optional; if specified
must be between 1 and 12 or the month name (e.g., December)."
end
newproperty(:monthday, :parent => CronParam) do
self.boundaries = [1, 31]
desc "The day of the month on which to run the
command. Optional; if specified, must be between 1 and 31."
end
newproperty(:environment) do
desc "Any environment settings associated with this cron job. They
will be stored between the header and the job in the crontab. There
can be no guarantees that other, earlier settings will not also
affect a given cron job.
Also, Puppet cannot automatically determine whether an existing,
unmanaged environment setting is associated with a given cron
job. If you already have cron jobs with environment settings,
then Puppet will keep those settings in the same place in the file,
but will not associate them with a specific job.
Settings should be specified exactly as they should appear in
the crontab, e.g., `PATH=/bin:/usr/bin:/usr/sbin`."
validate do |value|
unless value =~ /^\s*(\w+)\s*=\s*(.*)\s*$/ or value == :absent or value == "absent"
raise ArgumentError, "Invalid environment setting #{value.inspect}"
end
end
def insync?(is)
if is.is_a? Array
return is.sort == @should.sort
else
return is == @should
end
end
def is_to_s(newvalue)
if newvalue
if newvalue.is_a?(Array)
newvalue.join(",")
else
newvalue
end
else
nil
end
end
def should
@should
end
def should_to_s(newvalue = @should)
if newvalue
newvalue.join(",")
else
nil
end
end
end
newparam(:name) do
desc "The symbolic name of the cron job. This name
is used for human reference only and is generated automatically
for cron jobs found on the system. This generally won't
matter, as Puppet will do its best to match existing cron jobs
against specified jobs (and Puppet adds a comment to cron jobs it adds),
but it is at least possible that converting from unmanaged jobs to
managed jobs might require manual intervention."
isnamevar
end
newproperty(:user) do
desc "The user to run the command as. This user must
be allowed to run cron jobs, which is not currently checked by
Puppet.
The user defaults to whomever Puppet is running as."
defaultto {
struct = Etc.getpwuid(Process.uid)
struct.respond_to?(:name) && struct.name or 'root'
}
end
# Autorequire the owner of the crontab entry.
autorequire(:user) do
self[:user]
end
newproperty(:target) do
desc "The username that will own the cron entry. Defaults to
the value of $USER for the shell that invoked Puppet, or root if $USER
is empty."
defaultto {
if provider.is_a?(@resource.class.provider(:crontab))
if val = @resource.should(:user)
val
else
raise ArgumentError,
"You must provide a username with crontab entries"
end
elsif provider.class.ancestors.include?(Puppet::Provider::ParsedFile)
provider.class.default_target
else
nil
end
}
end
+ validate do
+ return true unless self[:special]
+ return true if self[:special] == :absent
+ # there is a special schedule in @should, so we don't want to see
+ # any numeric should values
+ [ :minute, :hour, :weekday, :monthday, :month ].each do |field|
+ next unless self[field]
+ next if self[field] == :absent
+ raise ArgumentError, "#{self.ref} cannot specify both a special schedule and a value for #{field}"
+ end
+ end
+
# We have to reorder things so that :provide is before :target
attr_accessor :uid
+ # Marks the resource as "being purged".
+ #
+ # @api public
+ #
+ # @note This overrides the Puppet::Type method in order to handle
+ # an edge case that has so far been observed during testig only.
+ # Without forcing the should-value for the user property to be
+ # identical to the original cron file, purging from a fixture
+ # will not work, because the user property defaults to the user
+ # running the test. It is not clear whether this scenario can apply
+ # during normal operation.
+ #
+ # @note Also, when not forcing the should-value for the target
+ # property, unpurged file content (such as comments) can end up
+ # being written to the default target (i.e. the current login name).
+ def purging
+ self[:target] = provider.property_hash[:target]
+ self[:user] = provider.property_hash[:target]
+ super
+ end
+
def value(name)
name = name.intern
ret = nil
if obj = @parameters[name]
ret = obj.should
ret ||= obj.retrieve
if ret == :absent
ret = nil
end
end
unless ret
case name
when :command
devfail "No command, somehow" unless @parameters[:ensure].value == :absent
when :special
# nothing
else
#ret = (self.class.validproperty?(name).default || "*").to_s
ret = "*"
end
end
ret
end
end
diff --git a/lib/puppet/type/exec.rb b/lib/puppet/type/exec.rb
index 24d6289ee..732a0c1c5 100644
--- a/lib/puppet/type/exec.rb
+++ b/lib/puppet/type/exec.rb
@@ -1,564 +1,564 @@
module Puppet
newtype(:exec) do
include Puppet::Util::Execution
require 'timeout'
@doc = "Executes external commands.
Any command in an `exec` resource **must** be able to run multiple times
without causing harm --- that is, it must be *idempotent*. There are three
main ways for an exec to be idempotent:
* The command itself is already idempotent. (For example, `apt-get update`.)
* The exec has an `onlyif`, `unless`, or `creates` attribute, which prevents
Puppet from running the command unless some condition is met.
* The exec has `refreshonly => true`, which only allows Puppet to run the
command when some other resource is changed. (See the notes on refreshing
below.)
A caution: There's a widespread tendency to use collections of execs to
manage resources that aren't covered by an existing resource type. This
works fine for simple tasks, but once your exec pile gets complex enough
that you really have to think to understand what's happening, you should
consider developing a custom resource type instead, as it will be much
more predictable and maintainable.
**Refresh:** `exec` resources can respond to refresh events (via
`notify`, `subscribe`, or the `~>` arrow). The refresh behavior of execs
is non-standard, and can be affected by the `refresh` and
`refreshonly` attributes:
* If `refreshonly` is set to true, the exec will _only_ run when it receives an
event. This is the most reliable way to use refresh with execs.
* If the exec already would have run and receives an event, it will run its
command **up to two times.** (If an `onlyif`, `unless`, or `creates` condition
is no longer met after the first run, the second run will not occur.)
* If the exec already would have run, has a `refresh` command, and receives an
event, it will run its normal command, then run its `refresh` command
(as long as any `onlyif`, `unless`, or `creates` conditions are still met
after the normal command finishes).
* If the exec would **not** have run (due to an `onlyif`, `unless`, or `creates`
attribute) and receives an event, it still will not run.
* If the exec has `noop => true`, would otherwise have run, and receives
an event from a non-noop resource, it will run once (or run its `refresh`
command instead, if it has one).
In short: If there's a possibility of your exec receiving refresh events,
it becomes doubly important to make sure the run conditions are restricted.
**Autorequires:** If Puppet is managing an exec's cwd or the executable
file used in an exec's command, the exec resource will autorequire those
files. If Puppet is managing the user that an exec should run as, the
exec resource will autorequire that user."
# Create a new check mechanism. It's basically just a parameter that
# provides one extra 'check' method.
def self.newcheck(name, options = {}, &block)
@checks ||= {}
check = newparam(name, options, &block)
@checks[name] = check
end
def self.checks
@checks.keys
end
newproperty(:returns, :array_matching => :all, :event => :executed_command) do |property|
include Puppet::Util::Execution
munge do |value|
value.to_s
end
def event_name
:executed_command
end
defaultto "0"
attr_reader :output
desc "The expected return code(s). An error will be returned if the
executed command returns something else. Defaults to 0. Can be
specified as an array of acceptable return codes or a single value."
# Make output a bit prettier
def change_to_s(currentvalue, newvalue)
"executed successfully"
end
# First verify that all of our checks pass.
def retrieve
# We need to return :notrun to trigger evaluation; when that isn't
# true, we *LIE* about what happened and return a "success" for the
# value, which causes us to be treated as in_sync?, which means we
# don't actually execute anything. I think. --daniel 2011-03-10
if @resource.check_all_attributes
return :notrun
else
return self.should
end
end
# Actually execute the command.
def sync
event = :executed_command
tries = self.resource[:tries]
try_sleep = self.resource[:try_sleep]
begin
tries.times do |try|
# Only add debug messages for tries > 1 to reduce log spam.
debug("Exec try #{try+1}/#{tries}") if tries > 1
@output, @status = provider.run(self.resource[:command])
break if self.should.include?(@status.exitstatus.to_s)
if try_sleep > 0 and tries > 1
debug("Sleeping for #{try_sleep} seconds between tries")
sleep try_sleep
end
end
rescue Timeout::Error
- self.fail "Command exceeded timeout" % value.inspect
+ self.fail Puppet::Error, "Command exceeded timeout", $!
end
if log = @resource[:logoutput]
case log
when :true
log = @resource[:loglevel]
when :on_failure
unless self.should.include?(@status.exitstatus.to_s)
log = @resource[:loglevel]
else
log = :false
end
end
unless log == :false
@output.split(/\n/).each { |line|
self.send(log, line)
}
end
end
unless self.should.include?(@status.exitstatus.to_s)
self.fail("#{self.resource[:command]} returned #{@status.exitstatus} instead of one of [#{self.should.join(",")}]")
end
event
end
end
newparam(:command) do
isnamevar
desc "The actual command to execute. Must either be fully qualified
or a search path for the command must be provided. If the command
succeeds, any output produced will be logged at the instance's
normal log level (usually `notice`), but if the command fails
(meaning its return code does not match the specified code) then
any output is logged at the `err` log level."
validate do |command|
raise ArgumentError, "Command must be a String, got value of class #{command.class}" unless command.is_a? String
end
end
newparam(:path) do
desc "The search path used for command execution.
Commands must be fully qualified if no path is specified. Paths
can be specified as an array or as a '#{File::PATH_SEPARATOR}' separated list."
# Support both arrays and colon-separated fields.
def value=(*values)
@value = values.flatten.collect { |val|
val.split(File::PATH_SEPARATOR)
}.flatten
end
end
newparam(:user) do
desc "The user to run the command as. Note that if you
use this then any error output is not currently captured. This
is because of a bug within Ruby. If you are using Puppet to
create this user, the exec will automatically require the user,
as long as it is specified by name.
Please note that the $HOME environment variable is not automatically set
when using this attribute."
# Most validation is handled by the SUIDManager class.
validate do |user|
self.fail "Only root can execute commands as other users" unless Puppet.features.root?
self.fail "Unable to execute commands as other users on Windows" if Puppet.features.microsoft_windows?
end
end
newparam(:group) do
desc "The group to run the command as. This seems to work quite
haphazardly on different platforms -- it is a platform issue
not a Ruby or Puppet one, since the same variety exists when
running commands as different users in the shell."
# Validation is handled by the SUIDManager class.
end
newparam(:cwd, :parent => Puppet::Parameter::Path) do
desc "The directory from which to run the command. If
this directory does not exist, the command will fail."
end
newparam(:logoutput) do
desc "Whether to log command output in addition to logging the
exit code. Defaults to `on_failure`, which only logs the output
when the command has an exit code that does not match any value
- specified by the `returns` attribute. In addition to the values
- below, you may set this attribute to any legal log level."
+ specified by the `returns` attribute. As with any resource type,
+ the log level can be controlled with the `loglevel` metaparameter."
defaultto :on_failure
newvalues(:true, :false, :on_failure)
end
newparam(:refresh) do
desc "How to refresh this command. By default, the exec is just
called again when it receives an event from another resource,
but this parameter allows you to define a different command
for refreshing."
validate do |command|
provider.validatecmd(command)
end
end
newparam(:environment) do
desc "Any additional environment variables you want to set for a
command. Note that if you use this to set PATH, it will override
the `path` attribute. Multiple environment variables should be
specified as an array."
validate do |values|
values = [values] unless values.is_a? Array
values.each do |value|
unless value =~ /\w+=/
raise ArgumentError, "Invalid environment setting '#{value}'"
end
end
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
and will be stopped. The timeout is specified in seconds. The default
timeout is 300 seconds and you can set it to 0 to disable the timeout."
munge do |value|
value = value.shift if value.is_a?(Array)
begin
value = Float(value)
rescue ArgumentError
- raise ArgumentError, "The timeout must be a number."
+ raise ArgumentError, "The timeout must be a number.", $!.backtrace
end
[value, 0.0].max
end
defaultto 300
end
newparam(:tries) do
desc "The number of times execution of the command should be tried.
Defaults to '1'. This many attempts will be made to execute
the command until an acceptable return code is returned.
Note that the timeout paramater applies to each try rather than
to the complete set of tries."
munge do |value|
if value.is_a?(String)
unless value =~ /^[\d]+$/
raise ArgumentError, "Tries must be an integer"
end
value = Integer(value)
end
raise ArgumentError, "Tries must be an integer >= 1" if value < 1
value
end
defaultto 1
end
newparam(:try_sleep) do
desc "The time to sleep in seconds between 'tries'."
munge do |value|
if value.is_a?(String)
unless value =~ /^[-\d.]+$/
raise ArgumentError, "try_sleep must be a number"
end
value = Float(value)
end
raise ArgumentError, "try_sleep cannot be a negative number" if value < 0
value
end
defaultto 0
end
newcheck(:refreshonly) do
desc <<-'EOT'
The command should only be run as a
refresh mechanism for when a dependent object is changed. It only
makes sense to use this option when this command depends on some
other object; it is useful for triggering an action:
# Pull down the main aliases file
file { "/etc/aliases":
source => "puppet://server/module/aliases"
}
# Rebuild the database, but only when the file changes
exec { newaliases:
path => ["/usr/bin", "/usr/sbin"],
subscribe => File["/etc/aliases"],
refreshonly => true
}
Note that only `subscribe` and `notify` can trigger actions, not `require`,
so it only makes sense to use `refreshonly` with `subscribe` or `notify`.
EOT
newvalues(:true, :false)
# We always fail this test, because we're only supposed to run
# on refresh.
def check(value)
# We have to invert the values.
if value == :true
false
else
true
end
end
end
newcheck(:creates, :parent => Puppet::Parameter::Path) do
desc <<-'EOT'
A file to look for before running the command. The command will
only run if the file **doesn't exist.**
This parameter doesn't cause Puppet to create a file; it is only
useful if **the command itself** creates a file.
exec { "tar -xf /Volumes/nfs02/important.tar":
cwd => "/var/tmp",
creates => "/var/tmp/myfile",
path => ["/usr/bin", "/usr/sbin"]
}
In this example, `myfile` is assumed to be a file inside
`important.tar`. If it is ever deleted, the exec will bring it
back by re-extracting the tarball. If `important.tar` does **not**
actually contain `myfile`, the exec will keep running every time
Puppet runs.
EOT
accept_arrays
# If the file exists, return false (i.e., don't run the command),
# else return true
def check(value)
- ! Puppet::FileSystem::File.exist?(value)
+ ! Puppet::FileSystem.exist?(value)
end
end
newcheck(:unless) do
desc <<-'EOT'
If this parameter is set, then this `exec` will run unless
the command returns 0. For example:
exec { "/bin/echo root >> /usr/lib/cron/cron.allow":
path => "/usr/bin:/usr/sbin:/bin",
unless => "grep root /usr/lib/cron/cron.allow 2>/dev/null"
}
This would add `root` to the cron.allow file (on Solaris) unless
`grep` determines it's already there.
Note that this command follows the same rules as the main command,
which is to say that it must be fully qualified if the path is not set.
EOT
validate do |cmds|
cmds = [cmds] unless cmds.is_a? Array
cmds.each do |command|
provider.validatecmd(command)
end
end
# Return true if the command does not return 0.
def check(value)
begin
output, status = provider.run(value, true)
rescue Timeout::Error
err "Check #{value.inspect} exceeded timeout"
return false
end
output.split(/\n/).each { |line|
self.debug(line)
}
status.exitstatus != 0
end
end
newcheck(:onlyif) do
desc <<-'EOT'
If this parameter is set, then this `exec` will only run if
the command returns 0. For example:
exec { "logrotate":
path => "/usr/bin:/usr/sbin:/bin",
onlyif => "test `du /var/log/messages | cut -f1` -gt 100000"
}
This would run `logrotate` only if that test returned true.
Note that this command follows the same rules as the main command,
which is to say that it must be fully qualified if the path is not set.
Also note that onlyif can take an array as its value, e.g.:
onlyif => ["test -f /tmp/file1", "test -f /tmp/file2"]
This will only run the exec if _all_ conditions in the array return true.
EOT
validate do |cmds|
cmds = [cmds] unless cmds.is_a? Array
cmds.each do |command|
provider.validatecmd(command)
end
end
# Return true if the command returns 0.
def check(value)
begin
output, status = provider.run(value, true)
rescue Timeout::Error
err "Check #{value.inspect} exceeded timeout"
return false
end
output.split(/\n/).each { |line|
self.debug(line)
}
status.exitstatus == 0
end
end
# Exec names are not isomorphic with the objects.
@isomorphic = false
validate do
provider.validatecmd(self[:command])
end
# FIXME exec should autorequire any exec that 'creates' our cwd
autorequire(:file) do
reqs = []
# Stick the cwd in there if we have it
reqs << self[:cwd] if self[:cwd]
file_regex = Puppet.features.microsoft_windows? ? %r{^([a-zA-Z]:[\\/]\S+)} : %r{^(/\S+)}
self[:command].scan(file_regex) { |str|
reqs << str
}
self[:command].scan(/^"([^"]+)"/) { |str|
reqs << str
}
[:onlyif, :unless].each { |param|
next unless tmp = self[param]
tmp = [tmp] unless tmp.is_a? Array
tmp.each do |line|
# And search the command line for files, adding any we
# find. This will also catch the command itself if it's
# fully qualified. It might not be a bad idea to add
# unqualified files, but, well, that's a bit more annoying
# to do.
reqs += line.scan(file_regex)
end
}
# For some reason, the += isn't causing a flattening
reqs.flatten!
reqs
end
autorequire(:user) do
# Autorequire users if they are specified by name
if user = self[:user] and user !~ /^\d+$/
user
end
end
def self.instances
[]
end
# Verify that we pass all of the checks. The argument determines whether
# we skip the :refreshonly check, which is necessary because we now check
# within refresh
def check_all_attributes(refreshing = false)
self.class.checks.each { |check|
next if refreshing and check == :refreshonly
if @parameters.include?(check)
val = @parameters[check].value
val = [val] unless val.is_a? Array
val.each do |value|
return false unless @parameters[check].check(value)
end
end
}
true
end
def output
if self.property(:returns).nil?
return nil
else
return self.property(:returns).output
end
end
# Run the command, or optionally run a separately-specified command.
def refresh
if self.check_all_attributes(true)
if cmd = self[:refresh]
provider.run(cmd)
else
self.property(:returns).sync
end
end
end
end
end
diff --git a/lib/puppet/type/file.rb b/lib/puppet/type/file.rb
index 58a004821..b49452c11 100644
--- a/lib/puppet/type/file.rb
+++ b/lib/puppet/type/file.rb
@@ -1,853 +1,891 @@
require 'digest/md5'
require 'cgi'
require 'etc'
require 'uri'
require 'fileutils'
require 'enumerator'
require 'pathname'
require 'puppet/parameter/boolean'
require 'puppet/util/diff'
require 'puppet/util/checksums'
require 'puppet/util/backups'
require 'puppet/util/symbolic_file_mode'
Puppet::Type.newtype(:file) do
include Puppet::Util::MethodHelper
include Puppet::Util::Checksums
include Puppet::Util::Backups
include Puppet::Util::SymbolicFileMode
@doc = "Manages files, including their content, ownership, and permissions.
The `file` type can manage normal files, directories, and symlinks; the
type should be specified in the `ensure` attribute. Note that symlinks cannot
be managed on Windows systems.
File contents can be managed directly with the `content` attribute, or
downloaded from a remote source using the `source` attribute; the latter
can also be used to recursively serve directories (when the `recurse`
attribute is set to `true` or `local`). On Windows, note that file
contents are managed in binary mode; Puppet never automatically translates
line endings.
**Autorequires:** If Puppet is managing the user or group that owns a
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
newparam(:path) do
desc <<-'EOT'
The path to the file to manage. Must be fully qualified.
On Windows, the path should include the drive letter and should use `/` as
the separator character (rather than `\\`).
EOT
isnamevar
validate do |value|
unless Puppet::Util.absolute_path?(value)
fail Puppet::Error, "File paths must be fully qualified, not '#{value}'"
end
end
munge do |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
newparam(:backup) do
desc <<-EOT
Whether (and how) file content should be backed up before being replaced.
This attribute works best as a resource default in the site manifest
(`File { backup => main }`), so it can affect all file resources.
* If set to `false`, file content won't be backed up.
* If set to a string beginning with `.` (e.g., `.puppet-bak`), Puppet will
use copy the file in the same directory with that value as the extension
of the backup. (A value of `true` is a synonym for `.puppet-bak`.)
* If set to any other string, Puppet will try to back up to a filebucket
with that title. See the `filebucket` resource type for more details.
(This is the preferred method for backup, since it can be centralized
and queried.)
Default value: `puppet`, which backs up to a filebucket of the same name.
(Puppet automatically creates a **local** filebucket named `puppet` if one
doesn't already exist.)
Backing up to a local filebucket isn't particularly useful. If you want
to make organized use of backups, you will generally want to use the
puppet master server's filebucket service. This requires declaring a
filebucket resource and a resource default for the `backup` attribute
in site.pp:
# /etc/puppet/manifests/site.pp
filebucket { 'main':
path => false, # This is required for remote filebuckets.
server => 'puppet.example.com', # Optional; defaults to the configured puppet master.
}
File { backup => main, }
If you are using multiple puppet master servers, you will want to
centralize the contents of the filebucket. Either configure your load
balancer to direct all filebucket traffic to a single master, or use
something like an out-of-band rsync task to synchronize the content on all
masters.
EOT
defaultto "puppet"
munge do |value|
# I don't really know how this is happening.
value = value.shift if value.is_a?(Array)
case value
when false, "false", :false
false
when true, "true", ".puppet-bak", :true
".puppet-bak"
when String
value
else
self.fail "Invalid backup type #{value.inspect}"
end
end
end
newparam(:recurse) do
desc "Whether and how to do recursive file management. Options are:
* `inf,true` --- Regular style recursion on both remote and local
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.
+ This can only be used when a source parameter is specified.
* `false` --- Default of no recursion.
"
newvalues(:true, :false, :inf, :remote)
validate { |arg| }
munge do |value|
newval = super(value)
case newval
when :true, :inf; true
when :false; false
when :remote; :remote
else
self.fail "Invalid recurse value #{value.inspect}"
end
end
end
newparam(:recurselimit) do
desc "How deeply to do recursive management."
newvalues(/^[0-9]+$/)
munge do |value|
newval = super(value)
case newval
when Integer, Fixnum, Bignum; value
when /^\d+$/; Integer(value)
else
self.fail "Invalid recurselimit value #{value.inspect}"
end
end
end
newparam(:replace, :boolean => true, :parent => Puppet::Parameter::Boolean) do
desc "Whether to replace a file or symlink that already exists on the local system but
whose content doesn't match what the `source` or `content` attribute
specifies. Setting this to false allows file resources to initialize files
without overwriting future changes. Note that this only affects content;
Puppet will still manage ownership and permissions. Defaults to `true`."
defaultto :true
end
newparam(:force, :boolean => true, :parent => Puppet::Parameter::Boolean) do
desc "Perform the file operation even if it will destroy one or more directories.
You must use `force` in order to:
* `purge` subdirectories
* Replace directories with files or links
* Remove a directory when `ensure => absent`"
defaultto false
end
newparam(:ignore) do
desc "A parameter which omits action on files matching
specified patterns during recursion. Uses Ruby's builtin globbing
engine, so shell metacharacters are fully supported, e.g. `[a-z]*`.
Matches that would descend into the directory structure are ignored,
e.g., `*/*`."
validate do |value|
unless value.is_a?(Array) or value.is_a?(String) or value == false
self.devfail "Ignore must be a string or an Array"
end
end
end
newparam(:links) do
desc "How to handle links during file actions. During file copying,
`follow` will copy the target file instead of the link, `manage`
will copy the link itself, and `ignore` will just pass it by.
When not copying, `manage` and `ignore` behave equivalently
(because you cannot really ignore links entirely during local
recursion), and `follow` will manage the file to which the link points."
newvalues(:follow, :manage)
defaultto :manage
end
newparam(:purge, :boolean => true, :parent => Puppet::Parameter::Boolean) do
desc "Whether unmanaged files should be purged. This option only makes
sense when managing directories with `recurse => true`.
* When recursively duplicating an entire directory with the `source`
attribute, `purge => true` will automatically purge any files
that are not in the source directory.
* When managing files in a directory as individual resources,
setting `purge => true` will purge any files that aren't being
specifically managed.
If you have a filebucket configured, the purged files will be uploaded,
but if you do not, this will destroy data."
defaultto :false
end
newparam(:sourceselect) do
desc "Whether to copy all valid sources, or just the first one. This parameter
only affects recursive directory copies; by default, the first valid
source is the only one used, but if this parameter is set to `all`, then
all valid sources will have all of their contents copied to the local
system. If a given file exists in more than one source, the version from
the earliest source in the list will be used."
defaultto :first
newvalues(:first, :all)
end
newparam(:show_diff, :boolean => true, :parent => Puppet::Parameter::Boolean) do
desc "Whether to display differences when the file changes, defaulting to
true. This parameter is useful for files that may contain passwords or
other secret data, which might otherwise be included in Puppet reports or
- other insecure outputs. If the global ``show_diff` configuration parameter
+ other insecure outputs. If the global ``show_diff` setting
is false, then no diffs will be shown even if this parameter is true."
defaultto :true
end
+ newparam(:validate_cmd) do
+ desc "A command for validating the file's syntax before replacing it. If
+ Puppet would need to rewrite a file due to new `source` or `content`, it
+ will check the new content's validity first. If validation fails, the file
+ resource will fail.
+
+ This command must have a fully qualified path, and should contain a
+ percent (`%`) token where it would expect an input file. It must exit `0`
+ if the syntax is correct, and non-zero otherwise. The command will be
+ run on the target system while applying the catalog, not on the puppet master.
+
+ Example:
+
+ file { '/etc/apache2/apache2.conf':
+ content => 'example',
+ validate_cmd => '/usr/sbin/apache2 -t -f %',
+ }
+
+ This would replace apache2.conf only if the test returned true.
+
+ Note that if a validation command requires a `%` as part of its text,
+ you can specify a different placeholder token with the
+ `validate_replacement` attribute."
+ end
+
+ newparam(:validate_replacement) do
+ desc "The replacement string in a `validate_cmd` that will be replaced
+ with an input file name. Defaults to: `%`"
+
+ defaultto '%'
+ end
+
# Autorequire the nearest ancestor directory found in the catalog.
autorequire(:file) do
req = []
path = Pathname.new(self[:path])
if !path.root?
# Start at our parent, to avoid autorequiring ourself
parents = path.parent.enum_for(:ascend)
if found = parents.find { |p| catalog.resource(:file, p.to_s) }
req << found.to_s
end
end
# if the resource is a link, make sure the target is created first
req << self[:target] if self[:target]
req
end
# Autorequire the owner and group of the file.
{:user => :owner, :group => :group}.each do |type, property|
autorequire(type) do
if @parameters.include?(property)
# The user/group property automatically converts to IDs
next unless should = @parameters[property].shouldorig
val = should[0]
if val.is_a?(Integer) or val =~ /^\d+$/
nil
else
val
end
end
end
end
CREATORS = [:content, :source, :target]
SOURCE_ONLY_CHECKSUMS = [:none, :ctime, :mtime]
validate do
creator_count = 0
CREATORS.each do |param|
creator_count += 1 if self.should(param)
end
creator_count += 1 if @parameters.include?(:source)
self.fail "You cannot specify more than one of #{CREATORS.collect { |p| p.to_s}.join(", ")}" if creator_count > 1
self.fail "You cannot specify a remote recursion without a source" if !self[:source] and self[:recurse] == :remote
self.fail "You cannot specify source when using checksum 'none'" if self[:checksum] == :none && !self[:source].nil?
SOURCE_ONLY_CHECKSUMS.each do |checksum_type|
self.fail "You cannot specify content when using checksum '#{checksum_type}'" if self[:checksum] == checksum_type && !self[:content].nil?
end
self.warning "Possible error: recurselimit is set but not recurse, no recursion will happen" if !self[:recurse] and self[:recurselimit]
provider.validate if provider.respond_to?(:validate)
end
def self.[](path)
return nil unless path
super(path.gsub(/\/+/, '/').sub(/\/$/, ''))
end
def self.instances
return []
end
# Determine the user to write files as.
def asuser
if self.should(:owner) and ! self.should(:owner).is_a?(Symbol)
writeable = Puppet::Util::SUIDManager.asuser(self.should(:owner)) {
FileTest.writable?(::File.dirname(self[:path]))
}
# If the parent directory is writeable, then we execute
# as the user in question. Otherwise we'll rely on
# the 'owner' property to do things.
asuser = self.should(:owner) if writeable
end
asuser
end
def bucket
return @bucket if @bucket
backup = self[:backup]
return nil unless backup
return nil if backup =~ /^\./
unless catalog or backup == "puppet"
fail "Can not find filebucket for backups without a catalog"
end
unless catalog and filebucket = catalog.resource(:filebucket, backup) or backup == "puppet"
fail "Could not find filebucket #{backup} specified in backup"
end
return default_bucket unless filebucket
@bucket = filebucket.bucket
@bucket
end
def default_bucket
Puppet::Type.type(:filebucket).mkdefaultbucket.bucket
end
# Does the file currently exist? Just checks for whether
# we have a stat
def exist?
stat ? true : false
end
def present?(current_values)
super && current_values[:ensure] != :false
end
# We have to do some extra finishing, to retrieve our bucket if
# there is one.
def finish
# Look up our bucket, if there is one
bucket
super
end
# Create any children via recursion or whatever.
def eval_generate
return [] unless self.recurse?
recurse
end
def ancestors
ancestors = Pathname.new(self[:path]).enum_for(:ascend).map(&:to_s)
ancestors.delete(self[:path])
ancestors
end
def flush
# We want to make sure we retrieve metadata anew on each transaction.
@parameters.each do |name, param|
param.flush if param.respond_to?(:flush)
end
@stat = :needs_stat
end
def initialize(hash)
# Used for caching clients
@clients = {}
super
# If they've specified a source, we get our 'should' values
# from it.
unless self[:ensure]
if self[:target]
self[:ensure] = :link
elsif self[:content]
self[:ensure] = :file
end
end
@stat = :needs_stat
end
# Configure discovered resources to be purged.
def mark_children_for_purging(children)
children.each do |name, child|
next if child[:source]
child[:ensure] = :absent
end
end
# Create a new file or directory object as a child to the current
# object.
def newchild(path)
full_path = ::File.join(self[:path], path)
# Add some new values to our original arguments -- these are the ones
# set at initialization. We specifically want to exclude any param
# values set by the :source property or any default values.
# LAK:NOTE This is kind of silly, because the whole point here is that
# the values set at initialization should live as long as the resource
# but values set by default or by :source should only live for the transaction
# or so. Unfortunately, we don't have a straightforward way to manage
# the different lifetimes of this data, so we kludge it like this.
# The right-side hash wins in the merge.
options = @original_parameters.merge(:path => full_path).reject { |param, value| value.nil? }
# These should never be passed to our children.
[:parent, :ensure, :recurse, :recurselimit, :target, :alias, :source].each do |param|
options.delete(param) if options.include?(param)
end
self.class.new(options)
end
# Files handle paths specially, because they just lengthen their
# path names, rather than including the full parent's title each
# time.
def pathbuilder
# We specifically need to call the method here, so it looks
# up our parent in the catalog graph.
if parent = parent()
# We only need to behave specially when our parent is also
# a file
if parent.is_a?(self.class)
# Remove the parent file name
list = parent.pathbuilder
list.pop # remove the parent's path info
return list << self.ref
else
return super
end
else
return [self.ref]
end
end
# Recursively generate a list of file resources, which will
# be used to copy remote files, manage local files, and/or make links
# to map to another directory.
def recurse
children = (self[:recurse] == :remote) ? {} : recurse_local
if self[:target]
recurse_link(children)
elsif self[:source]
recurse_remote(children)
end
# If we're purging resources, then delete any resource that isn't on the
# remote system.
mark_children_for_purging(children) if self.purge?
# REVISIT: sort_by is more efficient?
result = children.values.sort { |a, b| a[:path] <=> b[:path] }
remove_less_specific_files(result)
end
# This is to fix bug #2296, where two files recurse over the same
# set of files. It's a rare case, and when it does happen you're
# not likely to have many actual conflicts, which is good, because
# this is a pretty inefficient implementation.
def remove_less_specific_files(files)
# REVISIT: is this Windows safe? AltSeparator?
mypath = self[:path].split(::File::Separator)
other_paths = catalog.vertices.
select { |r| r.is_a?(self.class) and r[:path] != self[:path] }.
collect { |r| r[:path].split(::File::Separator) }.
select { |p| p[0,mypath.length] == mypath }
return files if other_paths.empty?
files.reject { |file|
path = file[:path].split(::File::Separator)
other_paths.any? { |p| path[0,p.length] == p }
}
end
# A simple method for determining whether we should be recursing.
def recurse?
self[:recurse] == true or self[:recurse] == :remote
end
# Recurse the target of the link.
def recurse_link(children)
perform_recursion(self[:target]).each do |meta|
if meta.relative_path == "."
self[:ensure] = :directory
next
end
children[meta.relative_path] ||= newchild(meta.relative_path)
if meta.ftype == "directory"
children[meta.relative_path][:ensure] = :directory
else
children[meta.relative_path][:ensure] = :link
children[meta.relative_path][:target] = meta.full_path
end
end
children
end
# Recurse the file itself, returning a Metadata instance for every found file.
def recurse_local
result = perform_recursion(self[:path])
return {} unless result
result.inject({}) do |hash, meta|
next hash if meta.relative_path == "."
hash[meta.relative_path] = newchild(meta.relative_path)
hash
end
end
# Recurse against our remote file.
def recurse_remote(children)
sourceselect = self[:sourceselect]
total = self[:source].collect do |source|
next unless result = perform_recursion(source)
return if top = result.find { |r| r.relative_path == "." } and top.ftype != "directory"
result.each { |data| data.source = "#{source}/#{data.relative_path}" }
break result if result and ! result.empty? and sourceselect == :first
result
end.flatten.compact
# This only happens if we have sourceselect == :all
unless sourceselect == :first
found = []
total.reject! do |data|
result = found.include?(data.relative_path)
found << data.relative_path unless found.include?(data.relative_path)
result
end
end
total.each do |meta|
if meta.relative_path == "."
parameter(:source).metadata = meta
next
end
children[meta.relative_path] ||= newchild(meta.relative_path)
children[meta.relative_path][:source] = meta.source
children[meta.relative_path][:checksum] = :md5 if meta.ftype == "file"
children[meta.relative_path].parameter(:source).metadata = meta
end
children
end
def perform_recursion(path)
Puppet::FileServing::Metadata.indirection.search(
path,
:links => self[:links],
:recurse => (self[:recurse] == :remote ? true : self[:recurse]),
:recurselimit => self[:recurselimit],
:ignore => self[:ignore],
:checksum_type => (self[:source] || self[:content]) ? self[:checksum] : :none,
:environment => catalog.environment
)
end
# Back up and remove the file or directory at `self[:path]`.
#
# @param [Symbol] should The file type replacing the current content.
# @return [Boolean] True if the file was removed, else False
# @raises [fail???] If the current file isn't one of %w{file link directory} and can't be removed.
def remove_existing(should)
wanted_type = should.to_s
current_type = read_current_type
if current_type.nil?
return false
end
if can_backup?(current_type)
backup_existing
end
if wanted_type != "link" and current_type == wanted_type
return false
end
case current_type
when "directory"
return remove_directory(wanted_type)
when "link", "file"
return remove_file(current_type, wanted_type)
else
self.fail "Could not back up files of type #{current_type}"
end
end
def retrieve
if source = parameter(:source)
source.copy_source_values
end
super
end
# Set the checksum, from another property. There are multiple
# properties that modify the contents of a file, and they need the
# ability to make sure that the checksum value is in sync.
def setchecksum(sum = nil)
if @parameters.include? :checksum
if sum
@parameters[:checksum].checksum = sum
else
# If they didn't pass in a sum, then tell checksum to
# figure it out.
currentvalue = @parameters[:checksum].retrieve
@parameters[:checksum].checksum = currentvalue
end
end
end
# Should this thing be a normal file? This is a relatively complex
# way of determining whether we're trying to create a normal file,
# and it's here so that the logic isn't visible in the content property.
def should_be_file?
return true if self[:ensure] == :file
# I.e., it's set to something like "directory"
return false if e = self[:ensure] and e != :present
# The user doesn't really care, apparently
if self[:ensure] == :present
return true unless s = stat
return(s.ftype == "file" ? true : false)
end
# If we've gotten here, then :ensure isn't set
return true if self[:content]
return true if stat and stat.ftype == "file"
false
end
# Stat our file. Depending on the value of the 'links' attribute, we
# use either 'stat' or 'lstat', and we expect the properties to use the
# resulting stat object accordingly (mostly by testing the 'ftype'
# value).
#
# We use the initial value :needs_stat to ensure we only stat the file once,
# but can also keep track of a failed stat (@stat == nil). This also allows
# us to re-stat on demand by setting @stat = :needs_stat.
def stat
return @stat unless @stat == :needs_stat
method = :stat
# Files are the only types that support links
if (self.class.name == :file and self[:links] != :follow) or self.class.name == :tidy
method = :lstat
end
@stat = begin
- Puppet::FileSystem::File.new(self[:path]).send(method)
+ Puppet::FileSystem.send(method, self[:path])
rescue Errno::ENOENT => error
nil
rescue Errno::ENOTDIR => error
nil
rescue Errno::EACCES => error
warning "Could not stat; permission denied"
nil
end
end
def to_resource
resource = super
resource.delete(:target) if resource[:target] == :notlink
resource
end
# Write out the file. Requires the property name for logging.
# Write will be done by the content property, along with checksum computation
def write(property)
remove_existing(:file)
mode = self.should(:mode) # might be nil
mode_int = mode ? symbolic_mode_to_int(mode, Puppet::Util::DEFAULT_POSIX_MODE) : nil
if write_temporary_file?
Puppet::Util.replace_file(self[:path], mode_int) do |file|
file.binmode
content_checksum = write_content(file)
file.flush
fail_if_checksum_is_wrong(file.path, content_checksum) if validate_checksum?
+ if self[:validate_cmd]
+ output = Puppet::Util::Execution.execute(self[:validate_cmd].gsub(self[:validate_replacement], file.path), :failonfail => true, :combine => true)
+ output.split(/\n/).each { |line|
+ self.debug(line)
+ }
+ end
end
else
umask = mode ? 000 : 022
Puppet::Util.withumask(umask) { ::File.open(self[:path], 'wb', mode_int ) { |f| write_content(f) } }
end
# make sure all of the modes are actually correct
property_fix
end
private
# @return [String] The type of the current file, cast to a string.
def read_current_type
stat_info = stat
if stat_info
stat_info.ftype.to_s
else
nil
end
end
# @return [Boolean] If the current file can be backed up and needs to be backed up.
def can_backup?(type)
if type == "directory" and not force?
# (#18110) Directories cannot be removed without :force, so it doesn't
# make sense to back them up.
false
else
true
end
end
# @return [Boolean] True if the directory was removed
# @api private
def remove_directory(wanted_type)
if force?
debug "Removing existing directory for replacement with #{wanted_type}"
FileUtils.rmtree(self[:path])
stat_needed
true
else
notice "Not removing directory; use 'force' to override"
false
end
end
# @return [Boolean] if the file was removed (which is always true currently)
# @api private
def remove_file(current_type, wanted_type)
debug "Removing existing #{current_type} for replacement with #{wanted_type}"
- Puppet::FileSystem::File.unlink(self[:path])
+ Puppet::FileSystem.unlink(self[:path])
stat_needed
true
end
def stat_needed
@stat = :needs_stat
end
# Back up the existing file at a given prior to it being removed
# @api private
# @raise [Puppet::Error] if the file backup failed
# @return [void]
def backup_existing
unless perform_backup
raise Puppet::Error, "Could not back up; will not replace"
end
end
# Should we validate the checksum of the file we're writing?
def validate_checksum?
self[:checksum] !~ /time/
end
# Make sure the file we wrote out is what we think it is.
def fail_if_checksum_is_wrong(path, content_checksum)
newsum = parameter(:checksum).sum_file(path)
return if [:absent, nil, content_checksum].include?(newsum)
self.fail "File written to disk did not match checksum; discarding changes (#{content_checksum} vs #{newsum})"
end
# write the current content. Note that if there is no content property
# simply opening the file with 'w' as done in write is enough to truncate
# or write an empty length file.
def write_content(file)
(content = property(:content)) && content.write(file)
end
def write_temporary_file?
# unfortunately we don't know the source file size before fetching it
# so let's assume the file won't be empty
(c = property(:content) and c.length) || @parameters[:source]
end
# There are some cases where all of the work does not get done on
# file creation/modification, so we have to do some extra checking.
def property_fix
properties.each do |thing|
next unless [:mode, :owner, :group, :seluser, :selrole, :seltype, :selrange].include?(thing.name)
# Make sure we get a new stat objct
@stat = :needs_stat
currentvalue = thing.retrieve
thing.sync unless thing.safe_insync?(currentvalue)
end
end
end
# We put all of the properties in separate files, because there are so many
# of them. The order these are loaded is important, because it determines
# the order they are in the property lit.
require 'puppet/type/file/checksum'
require 'puppet/type/file/content' # can create the file
require 'puppet/type/file/source' # can create the file
require 'puppet/type/file/target' # creates a different type of file
require 'puppet/type/file/ensure' # can create the file
require 'puppet/type/file/owner'
require 'puppet/type/file/group'
require 'puppet/type/file/mode'
require 'puppet/type/file/type'
require 'puppet/type/file/selcontext' # SELinux file context
require 'puppet/type/file/ctime'
require 'puppet/type/file/mtime'
diff --git a/lib/puppet/type/file/content.rb b/lib/puppet/type/file/content.rb
index 6a2eef676..bdeeedc32 100644
--- a/lib/puppet/type/file/content.rb
+++ b/lib/puppet/type/file/content.rb
@@ -1,240 +1,239 @@
require 'net/http'
require 'uri'
require 'tempfile'
require 'puppet/util/checksums'
-require 'puppet/network/http/api/v1'
+require 'puppet/network/http'
require 'puppet/network/http/compression'
module Puppet
Puppet::Type.type(:file).newproperty(:content) do
include Puppet::Util::Diff
include Puppet::Util::Checksums
- include Puppet::Network::HTTP::API::V1
include Puppet::Network::HTTP::Compression.module
attr_reader :actual_content
desc <<-'EOT'
The desired contents of a file, as a string. This attribute is mutually
exclusive with `source` and `target`.
Newlines and tabs can be specified in double-quoted strings using
standard escaped syntax --- \n for a newline, and \t for a tab.
With very small files, you can construct content strings directly in
the manifest...
define resolve(nameserver1, nameserver2, domain, search) {
$str = "search $search
domain $domain
nameserver $nameserver1
nameserver $nameserver2
"
file { "/etc/resolv.conf":
content => "$str",
}
}
...but for larger files, this attribute is more useful when combined with the
[template](http://docs.puppetlabs.com/references/latest/function.html#template)
function.
EOT
# Store a checksum as the value, rather than the actual content.
# Simplifies everything.
munge do |value|
if value == :absent
value
elsif checksum?(value)
# XXX This is potentially dangerous because it means users can't write a file whose
# entire contents are a plain checksum
value
else
@actual_content = value
resource.parameter(:checksum).sum(value)
end
end
# Checksums need to invert how changes are printed.
def change_to_s(currentvalue, newvalue)
# Our "new" checksum value is provided by the source.
if source = resource.parameter(:source) and tmp = source.checksum
newvalue = tmp
end
if currentvalue == :absent
return "defined content as '#{newvalue}'"
elsif newvalue == :absent
return "undefined content from '#{currentvalue}'"
else
return "content changed '#{currentvalue}' to '#{newvalue}'"
end
end
def checksum_type
if source = resource.parameter(:source)
result = source.checksum
else
result = resource[:checksum]
end
if result =~ /^\{(\w+)\}.+/
return $1.to_sym
else
return result
end
end
def length
(actual_content and actual_content.length) || 0
end
def content
self.should
end
# Override this method to provide diffs if asked for.
# Also, fix #872: when content is used, and replace is true, the file
# should be insync when it exists
def insync?(is)
if resource.should_be_file?
return false if is == :absent
else
if resource[:ensure] == :present and resource[:content] and s = resource.stat
resource.warning "Ensure set to :present but file type is #{s.ftype} so no content will be synced"
end
return true
end
return true if ! @resource.replace?
result = super
if ! result and Puppet[:show_diff] and resource.show_diff?
write_temporarily do |path|
- notice "\n" + diff(@resource[:path], path)
+ send @resource[:loglevel], "\n" + diff(@resource[:path], path)
end
end
result
end
def retrieve
return :absent unless stat = @resource.stat
ftype = stat.ftype
# Don't even try to manage the content on directories or links
return nil if ["directory","link"].include?(ftype)
begin
resource.parameter(:checksum).sum_file(resource[:path])
rescue => detail
- raise Puppet::Error, "Could not read #{ftype} #{@resource.title}: #{detail}"
+ raise Puppet::Error, "Could not read #{ftype} #{@resource.title}: #{detail}", detail.backtrace
end
end
# 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
# Just write our content out to disk.
def sync
return_event = @resource.stat ? :file_changed : :file_created
# We're safe not testing for the 'source' if there's no 'should'
# because we wouldn't have gotten this far if there weren't at least
# one valid value somewhere.
@resource.write(:content)
return_event
end
def write_temporarily
tempfile = Tempfile.new("puppet-file")
tempfile.open
write(tempfile)
tempfile.close
yield tempfile.path
tempfile.delete
end
def write(file)
resource.parameter(:checksum).sum_stream { |sum|
each_chunk_from(actual_content || resource.parameter(:source)) { |chunk|
sum << chunk
file.print chunk
}
}
end
# the content is munged so if it's a checksum source_or_content is nil
# unless the checksum indirectly comes from source
def each_chunk_from(source_or_content)
if source_or_content.is_a?(String)
yield source_or_content
elsif content_is_really_a_checksum? && source_or_content.nil?
yield read_file_from_filebucket
elsif source_or_content.nil?
yield ''
elsif Puppet[:default_file_terminus] == :file_server
yield source_or_content.content
elsif source_or_content.local?
chunk_file_from_disk(source_or_content) { |chunk| yield chunk }
else
chunk_file_from_source(source_or_content) { |chunk| yield chunk }
end
end
private
def content_is_really_a_checksum?
checksum?(should)
end
def chunk_file_from_disk(source_or_content)
File.open(source_or_content.full_path, "rb") do |src|
while chunk = src.read(8192)
yield chunk
end
end
end
def get_from_source(source_or_content, &block)
request = Puppet::Indirector::Request.new(:file_content, :find, source_or_content.full_path.sub(/^\//,''), nil, :environment => resource.catalog.environment)
request.do_request(:fileserver) do |req|
connection = Puppet::Network::HttpPool.http_instance(req.server, req.port)
- connection.request_get(indirection2uri(req), add_accept_encoding({"Accept" => "raw"}), &block)
+ connection.request_get(Puppet::Network::HTTP::API::V1.indirection2uri(req), add_accept_encoding({"Accept" => "raw"}), &block)
end
end
def chunk_file_from_source(source_or_content)
get_from_source(source_or_content) do |response|
case response.code
when /^2/; uncompress(response) { |uncompressor| response.read_body { |chunk| yield uncompressor.uncompress(chunk) } }
else
# Raise the http error if we didn't get a 'success' of some kind.
message = "Error #{response.code} on SERVER: #{(response.body||'').empty? ? response.message : uncompress_body(response)}"
raise Net::HTTPError.new(message, response)
end
end
end
def read_file_from_filebucket
raise "Could not get filebucket from file" unless dipper = resource.bucket
sum = should.sub(/\{\w+\}/, '')
dipper.getfile(sum)
rescue => detail
- fail "Could not retrieve content for #{should} from filebucket: #{detail}"
+ self.fail Puppet::Error, "Could not retrieve content for #{should} from filebucket: #{detail}", detail
end
end
end
diff --git a/lib/puppet/type/file/ensure.rb b/lib/puppet/type/file/ensure.rb
index d75c8f6ac..77ecc57db 100644
--- a/lib/puppet/type/file/ensure.rb
+++ b/lib/puppet/type/file/ensure.rb
@@ -1,189 +1,189 @@
module Puppet
Puppet::Type.type(:file).ensurable do
require 'etc'
require 'puppet/util/symbolic_file_mode'
include Puppet::Util::SymbolicFileMode
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:
file { "/etc/inetd.conf":
ensure => "/etc/inet/inetd.conf",
}
file { "/etc/inetd.conf":
ensure => link,
target => "/etc/inet/inetd.conf",
}
However, we recommend using `link` and `target` explicitly, since this
behavior can be harder to read.
EOT
# Most 'ensure' properties have a default, but with files we, um, don't.
nodefault
newvalue(:absent) do
- Puppet::FileSystem::File.unlink(@resource[:path])
+ Puppet::FileSystem.unlink(@resource[:path])
end
aliasvalue(:false, :absent)
newvalue(:file, :event => :file_created) do
# Make sure we're not managing the content some other way
if property = @resource.property(:content)
property.sync
else
@resource.write(:ensure)
@resource.should(:mode)
end
end
#aliasvalue(:present, :file)
newvalue(:present, :event => :file_created) do
# Make a file if they want something, but this will match almost
# anything.
set_file
end
newvalue(:directory, :event => :directory_created) do
mode = @resource.should(:mode)
parent = File.dirname(@resource[:path])
- unless Puppet::FileSystem::File.exist? parent
+ unless Puppet::FileSystem.exist? parent
raise Puppet::Error,
"Cannot create #{@resource[:path]}; parent directory #{parent} does not exist"
end
if mode
Puppet::Util.withumask(000) do
Dir.mkdir(@resource[:path], symbolic_mode_to_int(mode, 0755, true))
end
else
Dir.mkdir(@resource[:path])
end
@resource.send(:property_fix)
return :directory_created
end
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
end
# Symlinks.
newvalue(/./) do
# This code never gets executed. We need the regex to support
# specifying it, but the work is done in the 'symlink' code block.
end
munge do |value|
value = super(value)
value,resource[:target] = :link,value unless value.is_a? Symbol
resource[:links] = :manage if value == :link and resource[:links] != :follow
value
end
def change_to_s(currentvalue, newvalue)
return super unless newvalue.to_s == "file"
return super unless property = @resource.property(:content)
# We know that content is out of sync if we're here, because
# it's essentially equivalent to 'ensure' in the transaction.
if source = @resource.parameter(:source)
should = source.checksum
else
should = property.should
end
if should == :absent
is = property.retrieve
else
is = :absent
end
property.change_to_s(is, should)
end
# Check that we can actually create anything
def check
basedir = File.dirname(@resource[:path])
- if ! Puppet::FileSystem::File.exist?(basedir)
+ if ! Puppet::FileSystem.exist?(basedir)
raise Puppet::Error,
"Can not create #{@resource.title}; parent directory does not exist"
elsif ! FileTest.directory?(basedir)
raise Puppet::Error,
"Can not create #{@resource.title}; #{dirname} is not a directory"
end
end
# We have to treat :present specially, because it works with any
# type of file.
def insync?(currentvalue)
unless currentvalue == :absent or resource.replace?
return true
end
if self.should == :present
return !(currentvalue.nil? or currentvalue == :absent)
else
return super(currentvalue)
end
end
def retrieve
if stat = @resource.stat
return stat.ftype.intern
else
if self.should == :false
return :false
else
return :absent
end
end
end
def sync
@resource.remove_existing(self.should)
if self.should == :absent
return :file_removed
end
event = super
event
end
end
end
diff --git a/lib/puppet/type/file/source.rb b/lib/puppet/type/file/source.rb
index 7f88e692a..7826ddf3d 100644
--- a/lib/puppet/type/file/source.rb
+++ b/lib/puppet/type/file/source.rb
@@ -1,250 +1,256 @@
require 'puppet/file_serving/content'
require 'puppet/file_serving/metadata'
module Puppet
# Copy files from a local or remote source. This state *only* does any work
# when the remote file is an actual file; in that case, this state copies
# the file down. If the remote file is a dir or a link or whatever, then
# this state, during retrieval, modifies the appropriate other states
# so that things get taken care of appropriately.
Puppet::Type.type(:file).newparam(:source) do
include Puppet::Util::Diff
attr_accessor :source, :local
desc <<-'EOT'
A source file, which will be copied into place on the local system.
Values can be URIs pointing to remote files, or fully qualified paths to
files available on the local system (including files on NFS shares or
Windows mapped drives). This attribute is mutually exclusive with
`content` and `target`.
The available URI schemes are *puppet* and *file*. *Puppet*
URIs will retrieve files from Puppet's built-in file server, and are
usually formatted as:
`puppet:///modules/name_of_module/filename`
This will fetch a file from a module on the puppet master (or from a
local module when using puppet apply). Given a `modulepath` of
`/etc/puppetlabs/puppet/modules`, the example above would resolve to
`/etc/puppetlabs/puppet/modules/name_of_module/files/filename`.
Unlike `content`, the `source` attribute can be used to recursively copy
directories if the `recurse` attribute is set to `true` or `remote`. If
a source directory contains symlinks, use the `links` attribute to
specify whether to recreate links or follow them.
Multiple `source` values can be specified as an array, and Puppet will
use the first source that exists. This can be used to serve different
files to different system types:
file { "/etc/nfs.conf":
source => [
"puppet:///modules/nfs/conf.$host",
"puppet:///modules/nfs/conf.$operatingsystem",
"puppet:///modules/nfs/conf"
]
}
Alternately, when serving directories recursively, multiple sources can
be combined by setting the `sourceselect` attribute to `all`.
EOT
validate do |sources|
sources = [sources] unless sources.is_a?(Array)
sources.each do |source|
next if Puppet::Util.absolute_path?(source)
begin
uri = URI.parse(URI.escape(source))
rescue => detail
- self.fail "Could not understand source #{source}: #{detail}"
+ self.fail Puppet::Error, "Could not understand source #{source}: #{detail}", detail
end
self.fail "Cannot use relative URLs '#{source}'" unless uri.absolute?
self.fail "Cannot use opaque URLs '#{source}'" unless uri.hierarchical?
self.fail "Cannot use URLs of type '#{uri.scheme}' as source for fileserving" unless %w{file puppet}.include?(uri.scheme)
end
end
SEPARATOR_REGEX = [Regexp.escape(File::SEPARATOR.to_s), Regexp.escape(File::ALT_SEPARATOR.to_s)].join
munge do |sources|
sources = [sources] unless sources.is_a?(Array)
sources.map do |source|
source = source.sub(/[#{SEPARATOR_REGEX}]+$/, '')
if Puppet::Util.absolute_path?(source)
URI.unescape(Puppet::Util.path_to_uri(source).to_s)
else
source
end
end
end
def change_to_s(currentvalue, newvalue)
# newvalue = "{md5}#{@metadata.checksum}"
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}"
end
end
def checksum
metadata && metadata.checksum
end
# Look up (if necessary) and return remote content.
def content
return @content if @content
raise Puppet::DevError, "No source for content was stored with the metadata" unless metadata.source
unless tmp = Puppet::FileServing::Content.indirection.find(metadata.source, :environment => resource.catalog.environment, :links => resource[:links])
- fail "Could not find any content at %s" % metadata.source
+ self.fail "Could not find any content at %s" % metadata.source
end
@content = tmp.content
end
# Copy the values from the source to the resource. Yay.
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].each do |metadata_method|
next if metadata_method == :owner and !Puppet.features.root?
next if metadata_method == :group and !Puppet.features.root?
if Puppet.features.microsoft_windows?
# 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
case resource[:source_permissions]
when :ignore
next
when :use_when_creating
- next if Puppet::FileSystem::File.exist?(resource[:path])
+ next if Puppet::FileSystem.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
resource[:ensure] = :present
else
resource[:ensure] = "link"
resource[:target] = metadata.destination
end
end
attr_writer :metadata
# Provide, and retrieve if necessary, the metadata for this file. Fail
# if we can't find data about this host, and fail if there are any
# problems in our query.
def metadata
return @metadata if @metadata
return nil unless value
value.each do |source|
begin
- if data = Puppet::FileServing::Metadata.indirection.find(source, :environment => resource.catalog.environment, :links => resource[:links])
+ options = {
+ :environment => resource.catalog.environment,
+ :links => resource[:links],
+ :source_permissions => resource[:source_permissions]
+ }
+
+ if data = Puppet::FileServing::Metadata.indirection.find(source, options)
@metadata = data
@metadata.source = source
break
end
rescue => detail
- fail detail, "Could not retrieve file metadata for #{source}: #{detail}"
+ self.fail Puppet::Error, "Could not retrieve file metadata for #{source}: #{detail}", detail
end
end
- fail "Could not retrieve information from environment #{resource.catalog.environment} source(s) #{value.join(", ")}" unless @metadata
+ self.fail "Could not retrieve information from environment #{resource.catalog.environment} source(s) #{value.join(", ")}" unless @metadata
@metadata
end
def local?
found? and scheme == "file"
end
def full_path
Puppet::Util.uri_to_path(uri) if found?
end
def server?
uri and uri.host
end
def server
(uri and uri.host) or Puppet.settings[:server]
end
def port
(uri and uri.port) or Puppet.settings[:masterport]
end
private
def scheme
(uri and uri.scheme)
end
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 08a2a97df..12b21d185 100644
--- a/lib/puppet/type/file/target.rb
+++ b/lib/puppet/type/file/target.rb
@@ -1,87 +1,87 @@
module Puppet
Puppet::Type.type(:file).newproperty(:target) do
desc "The target for creating a link. Currently, symlinks are the
only type supported. This attribute is mutually exclusive with `source`
and `content`.
Symlink targets can be relative, as well as absolute:
# (Useful on Solaris)
file { \"/etc/inetd.conf\":
ensure => link,
target => \"inet/inetd.conf\",
}
Directories of symlinks can be served recursively by instead using the
`source` attribute, setting `ensure` to `directory`, and setting the
`links` attribute to `manage`."
newvalue(:notlink) do
# We do nothing if the value is absent
return :nochange
end
# Anything else, basically
newvalue(/./) do
@resource[:ensure] = :link if ! @resource.should(:ensure)
# Only call mklink if ensure didn't call us in the first place.
currentensure = @resource.property(:ensure).retrieve
mklink if @resource.property(:ensure).safe_insync?(currentensure)
end
# Create our link.
def mklink
raise Puppet::Error, "Cannot symlink on this platform version" if !provider.feature?(:manages_symlinks)
target = self.should
# Clean up any existing objects. The argument is just for logging,
# it doesn't determine what's removed.
@resource.remove_existing(target)
- raise Puppet::Error, "Could not remove existing file" if Puppet::FileSystem::File.exist?(@resource[:path])
+ raise Puppet::Error, "Could not remove existing file" if Puppet::FileSystem.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
- Puppet::FileSystem::File.new(target).symlink(@resource[:path])
+ Puppet::FileSystem.symlink(target, @resource[:path])
end
else
- Puppet::FileSystem::File.new(target).symlink(@resource[:path])
+ Puppet::FileSystem.symlink(target, @resource[:path])
end
end
@resource.send(:property_fix)
:link_created
end
end
def insync?(currentvalue)
if [:nochange, :notlink].include?(self.should) or @resource.recurse?
return true
- elsif ! @resource.replace? and Puppet::FileSystem::File.exist?(@resource[:path])
+ elsif ! @resource.replace? and Puppet::FileSystem.exist?(@resource[:path])
return true
else
return super(currentvalue)
end
end
def retrieve
if stat = @resource.stat
if stat.ftype == "link"
- return Puppet::FileSystem::File.new(@resource[:path]).readlink
+ return Puppet::FileSystem.readlink(@resource[:path])
else
return :notlink
end
else
return :absent
end
end
end
end
diff --git a/lib/puppet/type/k5login.rb b/lib/puppet/type/k5login.rb
index a87b3e7d8..554d7c204 100644
--- a/lib/puppet/type/k5login.rb
+++ b/lib/puppet/type/k5login.rb
@@ -1,88 +1,88 @@
# Plug-in type for handling k5login files
require 'puppet/util'
Puppet::Type.newtype(:k5login) do
@doc = "Manage the `.k5login` file for a user. Specify the full path to
the `.k5login` file as the name, and an array of principals as the
`principals` attribute."
ensurable
# Principals that should exist in the file
newproperty(:principals, :array_matching => :all) do
desc "The principals present in the `.k5login` file. This should be specified as an array."
end
# The path/name of the k5login file
newparam(:path) do
isnamevar
desc "The path to the `.k5login` file to manage. Must be fully qualified."
validate do |value|
unless absolute_path?(value)
raise Puppet::Error, "File paths must be fully qualified."
end
end
end
# To manage the mode of the file
newproperty(:mode) do
desc "The desired permissions mode of the `.k5login` file. Defaults to `644`."
defaultto { "644" }
end
provide(:k5login) do
desc "The k5login provider is the only provider for the k5login
type."
# Does this file exist?
def exists?
- Puppet::FileSystem::File.exist?(@resource[:name])
+ Puppet::FileSystem.exist?(@resource[:name])
end
# create the file
def create
write(@resource.should(:principals))
should_mode = @resource.should(:mode)
unless self.mode == should_mode
self.mode = should_mode
end
end
# remove the file
def destroy
- Puppet::FileSystem::File.unlink(@resource[:name])
+ Puppet::FileSystem.unlink(@resource[:name])
end
# Return the principals
def principals(dummy_argument=:work_arround_for_ruby_GC_bug)
- if Puppet::FileSystem::File.exist?(@resource[:name])
+ if Puppet::FileSystem.exist?(@resource[:name])
File.readlines(@resource[:name]).collect { |line| line.chomp }
else
:absent
end
end
# Write the principals out to the k5login file
def principals=(value)
write(value)
end
# Return the mode as an octal string, not as an integer
def mode
- "%o" % (Puppet::FileSystem::File.new(@resource[:name]).stat.mode & 007777)
+ "%o" % (Puppet::FileSystem.stat(@resource[:name]).mode & 007777)
end
# Set the file mode, converting from a string to an integer.
def mode=(value)
File.chmod(Integer("0#{value}"), @resource[:name])
end
private
def write(value)
Puppet::Util.replace_file(@resource[:name], 0644) do |f|
f.puts value
end
end
end
end
diff --git a/lib/puppet/type/mount.rb b/lib/puppet/type/mount.rb
index fceb5b631..8111a0d4a 100644
--- a/lib/puppet/type/mount.rb
+++ b/lib/puppet/type/mount.rb
@@ -1,288 +1,288 @@
require 'puppet/property/boolean'
module Puppet
# We want the mount to refresh when it changes.
newtype(:mount, :self_refresh => true) do
@doc = "Manages mounted filesystems, including putting mount
information into the mount table. The actual behavior depends
on the value of the 'ensure' parameter.
**Refresh:** `mount` resources can respond to refresh events (via
`notify`, `subscribe`, or the `~>` arrow). If a `mount` receives an event
from another resource **and** its `ensure` attribute is set to `mounted`,
Puppet will try to unmount then remount that filesystem.
**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]
# Use the normal parent class, because we actually want to
# call code when sync is called.
newproperty(:ensure) do
desc "Control what to do with this mount. Set this attribute to
`unmounted` to make sure the filesystem is in the filesystem table
but not mounted (if the filesystem is currently mounted, it will be
unmounted). Set it to `absent` to unmount (if necessary) and remove
the filesystem from the fstab. Set to `mounted` to add it to the
fstab and mount it. Set to `present` to add to fstab but not change
mount/unmount status."
# IS -> SHOULD In Sync Action
# ghost -> present NO create
# absent -> present NO create
# (mounted -> present YES)
# (unmounted -> present YES)
newvalue(:defined) do
provider.create
return :mount_created
end
aliasvalue :present, :defined
# IS -> SHOULD In Sync Action
# ghost -> unmounted NO create, unmount
# absent -> unmounted NO create
# mounted -> unmounted NO unmount
newvalue(:unmounted) do
case self.retrieve
when :ghost # (not in fstab but mounted)
provider.create
@resource.flush
provider.unmount
return :mount_unmounted
when nil, :absent # (not in fstab and not mounted)
provider.create
return :mount_created
when :mounted # (in fstab and mounted)
provider.unmount
syncothers # I guess it's more likely that the mount was originally mounted with
# the wrong attributes so I sync AFTER the umount
return :mount_unmounted
else
raise Puppet::Error, "Unexpected change from #{current_value} to unmounted}"
end
end
# IS -> SHOULD In Sync Action
# ghost -> absent NO unmount
# mounted -> absent NO provider.destroy AND unmount
# unmounted -> absent NO provider.destroy
newvalue(:absent, :event => :mount_deleted) do
current_value = self.retrieve
provider.unmount if provider.mounted?
provider.destroy unless current_value == :ghost
end
# IS -> SHOULD In Sync Action
# ghost -> mounted NO provider.create
# absent -> mounted NO provider.create AND mount
# unmounted -> mounted NO mount
newvalue(:mounted, :event => :mount_mounted) do
# Create the mount point if it does not already exist.
current_value = self.retrieve
currently_mounted = provider.mounted?
provider.create if [nil, :absent, :ghost].include?(current_value)
syncothers
# The fs can be already mounted if it was absent but mounted
provider.property_hash[:needs_mount] = true unless currently_mounted
end
# insync: mounted -> present
# unmounted -> present
def insync?(is)
if should == :defined and [:mounted,:unmounted].include?(is)
true
else
super
end
end
def syncothers
# We have to flush any changes to disk.
currentvalues = @resource.retrieve_resource
# Determine if there are any out-of-sync properties.
oos = @resource.send(:properties).find_all do |prop|
unless currentvalues.include?(prop)
raise Puppet::DevError, "Parent has property %s but it doesn't appear in the current values", [prop.name]
end
if prop.name == :ensure
false
else
! prop.safe_insync?(currentvalues[prop])
end
end.each { |prop| prop.sync }.length
@resource.flush if oos > 0
end
end
newproperty(:device) do
desc "The device providing the mount. This can be whatever
device is supporting by the mount, including network
devices or devices specified by UUID rather than device
path, depending on the operating system."
validate do |value|
raise Puppet::Error, "device must not contain whitespace: #{value}" if value =~ /\s/
end
end
# Solaris specifies two devices, not just one.
newproperty(:blockdevice) do
desc "The device to fsck. This is property is only valid
on Solaris, and in most cases will default to the correct
value."
# Default to the device but with "dsk" replaced with "rdsk".
defaultto do
if Facter.value(:osfamily) == "Solaris"
if device = resource[:device] and device =~ %r{/dsk/}
device.sub(%r{/dsk/}, "/rdsk/")
elsif fstype = resource[:fstype] and fstype == 'nfs'
'-'
else
nil
end
else
nil
end
end
validate do |value|
raise Puppet::Error, "blockdevice must not contain whitespace: #{value}" if value =~ /\s/
end
end
newproperty(:fstype) do
desc "The mount type. Valid values depend on the
operating system. This is a required option."
validate do |value|
raise Puppet::Error, "fstype must not contain whitespace: #{value}" if value =~ /\s/
end
end
newproperty(:options) do
desc "Mount options for the mounts, as they would
appear in the fstab."
validate do |value|
raise Puppet::Error, "option must not contain whitespace: #{value}" if value =~ /\s/
end
end
newproperty(:pass) do
desc "The pass in which the mount is checked."
defaultto {
if @resource.managed?
if Facter.value(:osfamily) == 'Solaris'
'-'
else
0
end
end
}
end
newproperty(:atboot, :parent => Puppet::Property::Boolean) do
desc "Whether to mount the mount at boot. Not all platforms
support this."
def munge(value)
munged = super
if munged
:yes
else
:no
end
end
end
newproperty(:dump) do
desc "Whether to dump the mount. Not all platform support this.
Valid values are `1` or `0`. or `2` on FreeBSD, Default is `0`."
if Facter.value(:operatingsystem) == "FreeBSD"
newvalue(%r{(0|1|2)})
else
newvalue(%r{(0|1)})
end
newvalue(%r{(0|1)})
defaultto {
0 if @resource.managed?
}
end
newproperty(:target) do
desc "The file in which to store the mount table. Only used by
those providers that write to disk."
defaultto { if @resource.class.defaultprovider.ancestors.include?(Puppet::Provider::ParsedFile)
@resource.class.defaultprovider.default_target
else
nil
end
}
end
newparam(:name) do
desc "The mount path for the mount."
isnamevar
validate do |value|
raise Puppet::Error, "name must not contain whitespace: #{value}" if value =~ /\s/
end
munge do |value|
value.gsub(/^(.+?)\/*$/, '\1')
end
end
newparam(:remounts) do
desc "Whether the mount can be remounted `mount -o remount`. If
this is false, then the filesystem will be unmounted and remounted
manually, which is prone to failure."
newvalues(:true, :false)
defaultto do
case Facter.value(:operatingsystem)
- when "FreeBSD", "Darwin", "AIX", "DragonFly"
+ when "FreeBSD", "Darwin", "AIX", "DragonFly", "OpenBSD"
false
else
true
end
end
end
def refresh
# Only remount if we're supposed to be mounted.
provider.remount if self.should(:fstype) != "swap" and provider.mounted?
end
def value(name)
name = name.intern
if property = @parameters[name]
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 50356cedf..bc2babdf8 100644
--- a/lib/puppet/type/package.rb
+++ b/lib/puppet/type/package.rb
@@ -1,384 +1,460 @@
# Define the different packaging systems. Each package system is implemented
# in a module, which then gets used to individually extend each package object.
# This allows packages to exist on the same machine using different packaging
# systems.
require 'puppet/parameter/package_options'
module Puppet
newtype(:package) do
@doc = "Manage packages. There is a basic dichotomy in package
support right now: Some package types (e.g., yum and apt) can
retrieve their own package files, while others (e.g., rpm and sun)
cannot. For those package formats that cannot retrieve their own files,
you can use the `source` parameter to point to the correct file.
Puppet will automatically guess the packaging format that you are
using based on the platform you are on, but you can override it
using the `provider` parameter; each provider defines what it
requires in order to function, and you must meet those requirements
to use a given provider.
**Autorequires:** If Puppet is managing the files specified as a
package's `adminfile`, `responsefile`, or `source`, the package
resource will autorequire those files."
feature :installable, "The provider can install packages.",
:methods => [:install]
feature :uninstallable, "The provider can uninstall packages.",
:methods => [:uninstall]
feature :upgradeable, "The provider can upgrade to the latest version of a
package. This feature is used by specifying `latest` as the
desired value for the package.",
:methods => [:update, :latest]
feature :purgeable, "The provider can purge packages. This generally means
that all traces of the package are removed, including
existing configuration files. This feature is thus destructive
and should be used with the utmost care.",
:methods => [:purge]
feature :versionable, "The provider is capable of interrogating the
package database for installed version(s), and can select
which out of a set of available versions of a package to
install if asked."
feature :holdable, "The provider is capable of placing packages on hold
such that they are not automatically upgraded as a result of
other package dependencies unless explicit action is taken by
a user or another package. Held is considered a superset of
installed.",
:methods => [:hold]
feature :install_options, "The provider accepts options to be
passed to the installer command."
feature :uninstall_options, "The provider accepts options to be
passed to the uninstaller command."
+ feature :package_settings, "The provider accepts package_settings to be
+ ensured for the given package. The meaning and format of these settings is
+ provider-specific.",
+ :methods => [:package_settings_insync?, :package_settings, :package_settings=]
+
ensurable do
desc <<-EOT
What state the package should be in. On packaging systems that can
retrieve new packages on their own, you can choose which package to
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. This defaults to `installed`.
EOT
attr_accessor :latest
newvalue(:present, :event => :package_installed) do
provider.install
end
newvalue(:absent, :event => :package_removed) do
provider.uninstall
end
newvalue(:purged, :event => :package_purged, :required_features => :purgeable) do
provider.purge
end
newvalue(:held, :event => :package_held, :required_features => :holdable) do
provider.hold
end
# Alias the 'present' value.
aliasvalue(:installed, :present)
newvalue(:latest, :required_features => :upgradeable) do
# Because yum always exits with a 0 exit code, there's a retrieve
# in the "install" method. So, check the current state now,
# to compare against later.
current = self.retrieve
begin
provider.update
rescue => detail
- self.fail "Could not update: #{detail}"
+ self.fail Puppet::Error, "Could not update: #{detail}", detail
end
if current == :absent
:package_installed
else
:package_changed
end
end
newvalue(/./, :required_features => :versionable) do
begin
provider.install
rescue => detail
- self.fail "Could not update: #{detail}"
+ self.fail Puppet::Error, "Could not update: #{detail}", detail
end
if self.retrieve == :absent
:package_installed
else
:package_changed
end
end
defaultto :installed
# Override the parent method, because we've got all kinds of
# funky definitions of 'in sync'.
def insync?(is)
@lateststamp ||= (Time.now.to_i - 1000)
# Iterate across all of the should values, and see how they
# turn out.
@should.each { |should|
case should
when :present
return true unless [:absent, :purged, :held].include?(is)
when :latest
# Short-circuit packages that are not present
return false if is == :absent or is == :purged
# Don't run 'latest' more than about every 5 minutes
if @latest and ((Time.now.to_i - @lateststamp) / 60) < 5
#self.debug "Skipping latest check"
else
begin
@latest = provider.latest
@lateststamp = Time.now.to_i
rescue => detail
error = Puppet::Error.new("Could not get latest version: #{detail}")
error.set_backtrace(detail.backtrace)
raise error
end
end
case
when is.is_a?(Array) && is.include?(@latest)
return true
when is == @latest
return true
when is == :present
# This will only happen on retarded packaging systems
# that can't query versions.
return true
else
self.debug "#{@resource.name} #{is.inspect} is installed, latest is #{@latest.inspect}"
end
when :absent
return true if is == :absent or is == :purged
when :purged
return true if is == :purged
# this handles version number matches and
# supports providers that can have multiple versions installed
when *Array(is)
return true
end
}
false
end
# This retrieves the current state. LAK: I think this method is unused.
def retrieve
provider.properties[:ensure]
end
# Provide a bit more information when logging upgrades.
def should_to_s(newvalue = @should)
if @latest
@latest.to_s
else
super(newvalue)
end
end
end
newparam(:name) do
desc "The package name. This is the name that the packaging
system uses internally, which is sometimes (especially on Solaris)
a name that is basically useless to humans. If you want to
abstract package installation, then you can use aliases to provide
a common name to packages:
# In the 'openssl' class
$ssl = $operatingsystem ? {
solaris => SMCossl,
default => openssl
}
# It is not an error to set an alias to the same value as the
# object name.
package { $ssl:
ensure => installed,
alias => openssl
}
. etc. .
$ssh = $operatingsystem ? {
solaris => SMCossh,
default => openssh
}
# Use the alias to specify a dependency, rather than
# having another selector to figure it out again.
package { $ssh:
ensure => installed,
alias => openssh,
require => Package[openssl]
}
"
isnamevar
validate do |value|
if !value.is_a?(String)
raise ArgumentError, "Name must be a String not #{value.class}"
end
end
end
+ newproperty(:package_settings, :required_features=>:package_settings) do
+ desc "Package settings. The definition of package settings is provider
+ specific. In general, these are certain properties which alter contents
+ of a package being installed. An example of package settings are the
+ FreeBSD ports options.
+
+ The package_settings attribute is a property. This means that the options
+ can be enforced during package installation and verified/retrieved
+ for packages that are already installed.
+
+ For example, ports provider on FreeBSD implements the package settings
+ as port build options (the ones you normally set with make config).
+ There is a simple usage example for this particular provider:
+
+ package { 'www/apache22':
+ package_settings => { 'SUEXEC' => false }
+ }
+
+ The above manifest ensures, that apache22 is compiled without SUEXEC
+ module.
+
+ Despite the package_settings are provider specific, the typical
+ behavior, when you change package's package_settings in your manifest,
+ is to reinstall package with new settings.
+ "
+
+ validate do |value|
+ if provider.respond_to?(:package_settings_validate)
+ provider.package_settings_validate(value)
+ else
+ super(value)
+ end
+ end
+
+ munge do |value|
+ if provider.respond_to?(:package_settings_munge)
+ provider.package_settings_munge(value)
+ else
+ super(value)
+ end
+ end
+
+ def insync?(is)
+ provider.package_settings_insync?(should, is)
+ end
+
+ def should_to_s(newvalue)
+ if provider.respond_to?(:package_settings_should_to_s)
+ provider.package_settings_should_to_s(should, newvalue)
+ else
+ super(newvalue)
+ end
+ end
+
+ def is_to_s(currentvalue)
+ if provider.respond_to?(:package_settings_is_to_s)
+ provider.package_settings_is_to_s(should, currentvalue)
+ else
+ super(currentvalue)
+ end
+ end
+
+ def change_to_s(currentvalue, newvalue)
+ if provider.respond_to?(:package_settings_change_to_s)
+ provider.package_settings_change_to_s(currentvalue, newvalue)
+ else
+ super(currentvalue,newvalue)
+ end
+ end
+ end
+
newparam(:source) do
- desc "Where to find the actual package. This must be a local file
+ desc "Where to find the actual package. This must be a local file
(or on a network file system) or a URL that your specific
packaging type understands; Puppet will not retrieve files for you,
although you can manage packages as `file` resources."
validate do |value|
provider.validate_source(value)
end
end
newparam(:instance) do
desc "A read-only parameter set by the package."
end
newparam(:status) do
desc "A read-only parameter set by the package."
end
newparam(:adminfile) do
desc "A file containing package defaults for installing packages.
This is currently only used on Solaris. The value will be
validated according to system rules, which in the case of
Solaris means that it should either be a fully qualified path
or it should be in `/var/sadm/install/admin`."
end
newparam(:responsefile) do
desc "A file containing any necessary answers to questions asked by
the package. This is currently used on Solaris and Debian. The
value will be validated according to system rules, but it should
generally be a fully qualified path."
end
newparam(:configfiles) do
desc "Whether configfiles should be kept or replaced. Most packages
types do not support this parameter. Defaults to `keep`."
defaultto :keep
newvalues(:keep, :replace)
end
newparam(:category) do
desc "A read-only parameter set by the package."
end
newparam(:platform) do
desc "A read-only parameter set by the package."
end
newparam(:root) do
desc "A read-only parameter set by the package."
end
newparam(:vendor) do
desc "A read-only parameter set by the package."
end
newparam(:description) do
desc "A read-only parameter set by the package."
end
newparam(:allowcdrom) do
desc "Tells apt to allow cdrom sources in the sources.list file.
Normally apt will bail if you try this."
newvalues(:true, :false)
end
newparam(:flavor) do
desc "OpenBSD supports 'flavors', which are further specifications for
which type of package you want."
end
newparam(:install_options, :parent => Puppet::Parameter::PackageOptions, :required_features => :install_options) do
desc <<-EOT
An array of additional options to pass when installing a package. These
options are package-specific, and should be documented by the software
vendor. One commonly implemented option is `INSTALLDIR`:
package { 'mysql':
ensure => installed,
source => 'N:/packages/mysql-5.5.16-winx64.msi',
install_options => [ '/S', { 'INSTALLDIR' => 'C:\\mysql-5.5' } ],
}
Each option in the array can either be a string or a hash, where each
key and value pair are interpreted in a provider specific way. Each
option will automatically be quoted when passed to the install command.
On Windows, this is the **only** place in Puppet where backslash
separators should be used. Note that backslashes in double-quoted
strings _must_ be double-escaped and backslashes in single-quoted
strings _may_ be double-escaped.
EOT
end
newparam(:uninstall_options, :parent => Puppet::Parameter::PackageOptions, :required_features => :uninstall_options) do
desc <<-EOT
An array of additional options to pass when uninstalling a package. These
options are package-specific, and should be documented by the software
vendor. For example:
package { 'VMware Tools':
ensure => absent,
uninstall_options => [ { 'REMOVE' => 'Sync,VSS' } ],
}
Each option in the array can either be a string or a hash, where each
key and value pair are interpreted in a provider specific way. Each
option will automatically be quoted when passed to the uninstall
command.
On Windows, this is the **only** place in Puppet where backslash
separators should be used. Note that backslashes in double-quoted
strings _must_ be double-escaped and backslashes in single-quoted
strings _may_ be double-escaped.
EOT
end
autorequire(:file) do
autos = []
[:responsefile, :adminfile].each { |param|
if val = self[param]
autos << val
end
}
if source = self[:source] and absolute_path?(source)
autos << source
end
autos
end
# This only exists for testing.
def clear
if obj = @parameters[:ensure]
obj.latest = nil
end
end
# The 'query' method returns a hash of info if the package
# exists and returns nil if it does not.
def exists?
@provider.get(:ensure) != :absent
end
def present?(current_values)
super && current_values[:ensure] != :purged
end
end
end
diff --git a/lib/puppet/type/port.rb b/lib/puppet/type/port.rb
deleted file mode 100644
index e19988515..000000000
--- a/lib/puppet/type/port.rb
+++ /dev/null
@@ -1,119 +0,0 @@
-#module Puppet
-# newtype(:port) do
-# @doc = "Installs and manages port entries. For most systems, these
-# entries will just be in /etc/services, but some systems (notably OS X)
-# will have different solutions."
-#
-# ensurable
-#
-# newproperty(:protocols) do
-# desc "The protocols the port uses. Valid values are *udp* and *tcp*.
-# Most services have both protocols, but not all. If you want
-# both protocols, you must specify that; Puppet replaces the
-# current values, it does not merge with them. If you specify
-# multiple protocols they must be as an array."
-#
-# def is=(value)
-# case value
-# when String
-# @is = value.split(/\s+/)
-# else
-# @is = value
-# end
-# end
-#
-# def is
-# @is
-# end
-#
-# # We actually want to return the whole array here, not just the first
-# # value.
-# def should
-# if defined?(@should)
-# if @should[0] == :absent
-# return :absent
-# else
-# return @should
-# end
-# else
-# return nil
-# end
-# end
-#
-# validate do |value|
-# valids = ["udp", "tcp", "ddp", :absent]
-# unless valids.include? value
-# raise Puppet::Error,
-# "Protocols can be either 'udp' or 'tcp', not #{value}"
-# end
-# end
-# end
-#
-# newproperty(:number) do
-# desc "The port number."
-# end
-#
-# newproperty(:description) do
-# desc "The port description."
-# end
-#
-# newproperty(:port_aliases) do
-# desc 'Any aliases the port might have. Multiple values must be
-# specified as an array. Note that this property is not the same as
-# the "alias" metaparam; use this property to add aliases to a port
-# in the services file, and "alias" to aliases for use in your Puppet
-# scripts.'
-#
-# # We actually want to return the whole array here, not just the first
-# # value.
-# def should
-# if defined?(@should)
-# if @should[0] == :absent
-# return :absent
-# else
-# return @should
-# end
-# else
-# return nil
-# end
-# end
-#
-# validate do |value|
-# if value.is_a? String and value =~ /\s/
-# raise Puppet::Error,
-# "Aliases cannot have whitespace in them: %s" %
-# value.inspect
-# end
-# end
-#
-# munge do |value|
-# unless value == "absent" or value == :absent
-# # Add the :alias metaparam in addition to the property
-# @resource.newmetaparam(
-# @resource.class.metaparamclass(:alias), value
-# )
-# end
-# value
-# end
-# end
-#
-# newproperty(:target) do
-# desc "The file in which to store service information. Only used by
-# those providers that write to disk."
-#
-# defaultto { if @resource.class.defaultprovider.ancestors.include?(Puppet::Provider::ParsedFile)
-# @resource.class.defaultprovider.default_target
-# else
-# nil
-# end
-# }
-# end
-#
-# newparam(:name) do
-# desc "The port name."
-#
-# isnamevar
-# end
-# end
-#end
-
diff --git a/lib/puppet/type/resources.rb b/lib/puppet/type/resources.rb
index 12bd3cac5..c4da057d7 100644
--- a/lib/puppet/type/resources.rb
+++ b/lib/puppet/type/resources.rb
@@ -1,132 +1,161 @@
require 'puppet'
require 'puppet/parameter/boolean'
Puppet::Type.newtype(:resources) do
@doc = "This is a metatype that can manage other resource types. Any
metaparams specified here will be passed on to any generated resources,
so you can purge umanaged resources but set `noop` to true so the
purging is only logged and does not actually happen."
newparam(:name) do
desc "The name of the type to be managed."
validate do |name|
raise ArgumentError, "Could not find resource type '#{name}'" unless Puppet::Type.type(name)
end
munge { |v| v.to_s }
end
newparam(:purge, :boolean => true, :parent => Puppet::Parameter::Boolean) do
desc "Purge unmanaged resources. This will delete any resource
that is not specified in your configuration
and is not required by any specified resources."
defaultto :false
validate do |value|
if munge(value)
unless @resource.resource_type.respond_to?(:instances)
raise ArgumentError, "Purging resources of type #{@resource[:name]} is not supported, since they cannot be queried from the system"
end
raise ArgumentError, "Purging is only supported on types that accept 'ensure'" unless @resource.resource_type.validproperty?(:ensure)
end
end
end
newparam(:unless_system_user) do
desc "This keeps system users from being purged. By default, it
does not purge users whose UIDs are less than or equal to 500, but you can specify
a different UID as the inclusive limit."
newvalues(:true, :false, /^\d+$/)
munge do |value|
case value
when /^\d+/
Integer(value)
when :true, true
500
when :false, false
false
when Integer; value
else
raise ArgumentError, "Invalid value #{value.inspect}"
end
end
defaultto {
if @resource[:name] == "user"
500
else
nil
end
}
end
+ newparam(:unless_uid) do
+ desc "This keeps specific uids or ranges of uids from being purged when purge is true.
+ Accepts ranges, integers and (mixed) arrays of both."
+
+ munge do |value|
+ case value
+ when /^\d+/
+ [Integer(value)]
+ when Integer
+ [value]
+ when Range
+ [value]
+ when Array
+ value
+ when /^\[\d+/
+ value.split(',').collect{|x| x.include?('..') ? Integer(x.split('..')[0])..Integer(x.split('..')[1]) : Integer(x) }
+ else
+ raise ArgumentError, "Invalid value #{value.inspect}"
+ end
+ end
+ end
+
def check(resource)
@checkmethod ||= "#{self[:name]}_check"
@hascheck ||= respond_to?(@checkmethod)
if @hascheck
return send(@checkmethod, resource)
else
return true
end
end
def able_to_ensure_absent?(resource)
resource[:ensure] = :absent
rescue ArgumentError, Puppet::Error
err "The 'ensure' attribute on #{self[:name]} resources does not accept 'absent' as a value"
false
end
# Generate any new resources we need to manage. This is pretty hackish
# right now, because it only supports purging.
def generate
return [] unless self.purge?
resource_type.instances.
reject { |r| catalog.resource_refs.include? r.ref }.
select { |r| check(r) }.
select { |r| r.class.validproperty?(:ensure) }.
select { |r| able_to_ensure_absent?(r) }.
each { |resource|
@parameters.each do |name, param|
resource[name] = param.value if param.metaparam?
end
# Mark that we're purging, so transactions can handle relationships
# correctly
resource.purging
}
end
def resource_type
unless defined?(@resource_type)
unless type = Puppet::Type.type(self[:name])
raise Puppet::DevError, "Could not find resource type"
end
@resource_type = type
end
@resource_type
end
- # Make sure we don't purge users below a certain uid, if the check
- # is enabled.
+ # Make sure we don't purge users with specific uids
def user_check(resource)
return true unless self[:name] == "user"
return true unless self[:unless_system_user]
-
resource[:audit] = :uid
+ current_values = resource.retrieve_resource
+ current_uid = current_values[resource.property(:uid)]
+ unless_uids = self[:unless_uid]
return false if system_users.include?(resource[:name])
- current_values = resource.retrieve_resource
- current_values[resource.property(:uid)] > self[:unless_system_user]
+ if unless_uids && unless_uids.length > 0
+ unless_uids.each do |unless_uid|
+ return false if unless_uid == current_uid
+ return false if unless_uid.respond_to?('include?') && unless_uid.include?(current_uid)
+ end
+ end
+
+ current_uid > self[:unless_system_user]
end
def system_users
%w{root nobody bin noaccess daemon sys}
end
end
diff --git a/lib/puppet/type/selboolean.rb b/lib/puppet/type/selboolean.rb
index 204b89056..eb30742a5 100644
--- a/lib/puppet/type/selboolean.rb
+++ b/lib/puppet/type/selboolean.rb
@@ -1,26 +1,26 @@
module Puppet
newtype(:selboolean) do
@doc = "Manages SELinux booleans on systems with SELinux support. The supported booleans
are any of the ones found in `/selinux/booleans/`."
newparam(:name) do
desc "The name of the SELinux boolean to be managed."
isnamevar
end
newproperty(:value) do
- desc "Whether the the SELinux boolean should be enabled or disabled."
+ desc "Whether the SELinux boolean should be enabled or disabled."
newvalue(:on)
newvalue(:off)
end
newparam(:persistent) do
desc "If set true, SELinux booleans will be written to disk and persist accross reboots.
The default is `false`."
defaultto :false
newvalues(:true, :false)
end
end
end
diff --git a/lib/puppet/type/selmodule.rb b/lib/puppet/type/selmodule.rb
index 70ef60581..23bf235de 100644
--- a/lib/puppet/type/selmodule.rb
+++ b/lib/puppet/type/selmodule.rb
@@ -1,59 +1,59 @@
#
# Simple module for managing SELinux policy modules
#
Puppet::Type.newtype(:selmodule) do
@doc = "Manages loading and unloading of SELinux policy modules
on the system. Requires SELinux support. See man semodule(8)
for more information on SELinux policy modules.
**Autorequires:** If Puppet is managing the file containing this SELinux
policy module (which is either explicitly specified in the `selmodulepath`
attribute or will be found at {`selmoduledir`}/{`name`}.pp), the selmodule
resource will autorequire that file."
ensurable
newparam(:name) do
desc "The name of the SELinux policy to be managed. You should not
include the customary trailing .pp extension."
isnamevar
end
newparam(:selmoduledir) do
desc "The directory to look for the compiled pp module file in.
Currently defaults to `/usr/share/selinux/targeted`. If the
`selmodulepath` attribute is not specified, Puppet will expect to find
the module in `<selmoduledir>/<name>.pp`, where `name` is the value of the
`name` parameter."
defaultto "/usr/share/selinux/targeted"
end
newparam(:selmodulepath) do
desc "The full path to the compiled .pp policy module. You only need to use
this if the module file is not in the `selmoduledir` directory."
end
newproperty(:syncversion) do
desc "If set to `true`, the policy will be reloaded if the
version found in the on-disk file differs from the loaded
- version. If set to `false` (the default) the the only check
+ version. If set to `false` (the default) the only check
that will be made is if the policy is loaded at all or not."
newvalue(:true)
newvalue(:false)
end
autorequire(:file) do
if self[:selmodulepath]
[self[:selmodulepath]]
else
["#{self[:selmoduledir]}/#{self[:name]}.pp"]
end
end
end
diff --git a/lib/puppet/type/ssh_authorized_key.rb b/lib/puppet/type/ssh_authorized_key.rb
index 1bdb5863b..12c8294b1 100644
--- a/lib/puppet/type/ssh_authorized_key.rb
+++ b/lib/puppet/type/ssh_authorized_key.rb
@@ -1,114 +1,115 @@
module Puppet
newtype(:ssh_authorized_key) do
@doc = "Manages SSH authorized keys. Currently only type 2 keys are
supported.
**Autorequires:** If Puppet is managing the user account in which this
SSH key should be installed, the `ssh_authorized_key` resource will autorequire
that user."
ensurable
newparam(:name) do
desc "The SSH key comment. This attribute is currently used as a
system-wide primary key and therefore has to be unique."
isnamevar
end
newproperty(:type) do
desc "The encryption type used: ssh-dss or ssh-rsa."
- newvalues :'ssh-dss', :'ssh-rsa', :'ecdsa-sha2-nistp256', :'ecdsa-sha2-nistp384', :'ecdsa-sha2-nistp521'
+ newvalues :'ssh-dss', :'ssh-rsa', :'ecdsa-sha2-nistp256', :'ecdsa-sha2-nistp384', :'ecdsa-sha2-nistp521', :'ssh-ed25519'
aliasvalue(:dsa, :'ssh-dss')
+ aliasvalue(:ed25519, :'ssh-ed25519')
aliasvalue(:rsa, :'ssh-rsa')
end
newproperty(:key) do
desc "The public key itself; generally a long string of hex characters. The key attribute
may not contain whitespace: Omit key headers (e.g. 'ssh-rsa') and key identifiers
(e.g. 'joe@joescomputer.local') found in the public key file."
validate do |value|
raise Puppet::Error, "Key must not contain whitespace: #{value}" if value =~ /\s/
end
end
newproperty(:user) do
desc "The user account in which the SSH key should be installed.
The resource will automatically depend on this user."
end
newproperty(:target) do
desc "The absolute filename in which to store the SSH key. This
property is optional and should only be used in cases where keys
are stored in a non-standard location (i.e.` not in
`~user/.ssh/authorized_keys`)."
defaultto :absent
def should
return super if defined?(@should) and @should[0] != :absent
return nil unless user = resource[:user]
begin
return File.expand_path("~#{user}/.ssh/authorized_keys")
rescue
Puppet.debug "The required user is not yet present on the system"
return nil
end
end
def insync?(is)
is == should
end
end
newproperty(:options, :array_matching => :all) do
desc "Key options, see sshd(8) for possible values. Multiple values
should be specified as an array."
defaultto do :absent end
def is_to_s(value)
if value == :absent or value.include?(:absent)
super
else
value.join(",")
end
end
def should_to_s(value)
if value == :absent or value.include?(:absent)
super
else
value.join(",")
end
end
validate do |value|
unless value == :absent or value =~ /^[-a-z0-9A-Z_]+(?:=\".*?\")?$/
raise Puppet::Error, "Option #{value} is not valid. A single option must either be of the form 'option' or 'option=\"value\". Multiple options must be provided as an array"
end
end
end
autorequire(:user) do
should(:user) if should(:user)
end
validate do
# Go ahead if target attribute is defined
return if @parameters[:target].shouldorig[0] != :absent
# Go ahead if user attribute is defined
return if @parameters.include?(:user)
# If neither target nor user is defined, this is an error
raise Puppet::Error, "Attribute 'user' or 'target' is mandatory"
end
end
end
diff --git a/lib/puppet/type/sshkey.rb b/lib/puppet/type/sshkey.rb
index 41948ed98..db3c52c85 100644
--- a/lib/puppet/type/sshkey.rb
+++ b/lib/puppet/type/sshkey.rb
@@ -1,72 +1,73 @@
module Puppet
newtype(:sshkey) do
@doc = "Installs and manages ssh host keys. At this point, this type
only knows how to install keys into `/etc/ssh/ssh_known_hosts`. See
the `ssh_authorized_key` type to manage authorized keys."
ensurable
newproperty(:type) do
desc "The encryption type used. Probably ssh-dss or ssh-rsa."
- newvalues :'ssh-dss', :'ssh-rsa', :'ecdsa-sha2-nistp256', :'ecdsa-sha2-nistp384', :'ecdsa-sha2-nistp521'
+ newvalues :'ssh-dss', :'ssh-ed25519', :'ssh-rsa', :'ecdsa-sha2-nistp256', :'ecdsa-sha2-nistp384', :'ecdsa-sha2-nistp521'
aliasvalue(:dsa, :'ssh-dss')
+ aliasvalue(:ed25519, :'ssh-ed25519')
aliasvalue(:rsa, :'ssh-rsa')
end
newproperty(:key) do
desc "The key itself; generally a long string of hex digits."
end
# FIXME This should automagically check for aliases to the hosts, just
# to see if we can automatically glean any aliases.
newproperty(:host_aliases) do
desc 'Any aliases the host might have. Multiple values must be
specified as an array.'
attr_accessor :meta
def insync?(is)
is == @should
end
# We actually want to return the whole array here, not just the first
# value.
def should
defined?(@should) ? @should : nil
end
validate do |value|
if value =~ /\s/
raise Puppet::Error, "Aliases cannot include whitespace"
end
if value =~ /,/
raise Puppet::Error, "Aliases must be provided as an array, not a comma-separated list"
end
end
end
newparam(:name) do
desc "The host name that the key is associated with."
isnamevar
validate do |value|
raise Puppet::Error, "Resourcename cannot include whitespaces" if value =~ /\s/
raise Puppet::Error, "No comma in resourcename allowed. If you want to specify aliases use the host_aliases property" if value.include?(',')
end
end
newproperty(:target) do
desc "The file in which to store the ssh key. Only used by
the `parsed` provider."
defaultto { if @resource.class.defaultprovider.ancestors.include?(Puppet::Provider::ParsedFile)
@resource.class.defaultprovider.default_target
else
nil
end
}
end
end
end
diff --git a/lib/puppet/type/tidy.rb b/lib/puppet/type/tidy.rb
index 83ac3322d..aecb1e609 100644
--- a/lib/puppet/type/tidy.rb
+++ b/lib/puppet/type/tidy.rb
@@ -1,324 +1,324 @@
require 'puppet/parameter/boolean'
Puppet::Type.newtype(:tidy) do
require 'puppet/file_serving/fileset'
require 'puppet/file_bucket/dipper'
@doc = "Remove unwanted files based on specific criteria. Multiple
criteria are OR'd together, so a file that is too large but is not
old enough will still get tidied.
If you don't specify either `age` or `size`, then all files will
be removed.
This resource type works by generating a file resource for every file
that should be deleted and then letting that resource perform the
actual deletion.
"
newparam(:path) do
desc "The path to the file or directory to manage. Must be fully
qualified."
isnamevar
end
newparam(:recurse) do
desc "If target is a directory, recursively descend
into the directory looking for files to tidy."
newvalues(:true, :false, :inf, /^[0-9]+$/)
# Replace the validation so that we allow numbers in
# addition to string representations of them.
validate { |arg| }
munge do |value|
newval = super(value)
case newval
when :true, :inf; true
when :false; false
when Integer, Fixnum, Bignum; value
when /^\d+$/; Integer(value)
else
raise ArgumentError, "Invalid recurse value #{value.inspect}"
end
end
end
newparam(:matches) do
desc <<-'EOT'
One or more (shell type) file glob patterns, which restrict
the list of files to be tidied to those whose basenames match
at least one of the patterns specified. Multiple patterns can
be specified using an array.
Example:
tidy { "/tmp":
age => "1w",
recurse => 1,
matches => [ "[0-9]pub*.tmp", "*.temp", "tmpfile?" ]
}
This removes files from `/tmp` if they are one week old or older,
are not in a subdirectory and match one of the shell globs given.
Note that the patterns are matched against the basename of each
file -- that is, your glob patterns should not have any '/'
characters in them, since you are only specifying against the last
bit of the file.
Finally, note that you must now specify a non-zero/non-false value
for recurse if matches is used, as matches only apply to files found
by recursion (there's no reason to use static patterns match against
a statically determined path). Requiering explicit recursion clears
up a common source of confusion.
EOT
# Make sure we convert to an array.
munge do |value|
fail "Tidy can't use matches with recurse 0, false, or undef" if "#{@resource[:recurse]}" =~ /^(0|false|)$/
[value].flatten
end
# Does a given path match our glob patterns, if any? Return true
# if no patterns have been provided.
def tidy?(path, stat)
basename = File.basename(path)
flags = File::FNM_DOTMATCH | File::FNM_PATHNAME
return(value.find {|pattern| File.fnmatch(pattern, basename, flags) } ? true : false)
end
end
newparam(:backup) do
desc "Whether tidied files should be backed up. Any values are passed
directly to the file resources used for actual file deletion, so consult
the `file` type's backup documentation to determine valid values."
end
newparam(:age) do
desc "Tidy files whose age is equal to or greater than
the specified time. You can choose seconds, minutes,
hours, days, or weeks by specifying the first letter of any
of those words (e.g., '1w').
Specifying 0 will remove all files."
AgeConvertors = {
:s => 1,
:m => 60,
:h => 60 * 60,
:d => 60 * 60 * 24,
:w => 60 * 60 * 24 * 7,
}
def convert(unit, multi)
if num = AgeConvertors[unit]
return num * multi
else
self.fail "Invalid age unit '#{unit}'"
end
end
def tidy?(path, stat)
# If the file's older than we allow, we should get rid of it.
(Time.now.to_i - stat.send(resource[:type]).to_i) > value
end
munge do |age|
unit = multi = nil
case age
when /^([0-9]+)(\w)\w*$/
multi = Integer($1)
unit = $2.downcase.intern
when /^([0-9]+)$/
multi = Integer($1)
unit = :d
else
self.fail "Invalid tidy age #{age}"
end
convert(unit, multi)
end
end
newparam(:size) do
desc "Tidy files whose size is equal to or greater than
the specified size. Unqualified values are in kilobytes, but
*b*, *k*, *m*, *g*, and *t* can be appended to specify *bytes*,
*kilobytes*, *megabytes*, *gigabytes*, and *terabytes*, respectively.
Only the first character is significant, so the full word can also
be used."
def convert(unit, multi)
if num = { :b => 0, :k => 1, :m => 2, :g => 3, :t => 4 }[unit]
result = multi
num.times do result *= 1024 end
return result
else
self.fail "Invalid size unit '#{unit}'"
end
end
def tidy?(path, stat)
stat.size >= value
end
munge do |size|
case size
when /^([0-9]+)(\w)\w*$/
multi = Integer($1)
unit = $2.downcase.intern
when /^([0-9]+)$/
multi = Integer($1)
unit = :k
else
self.fail "Invalid tidy size #{age}"
end
convert(unit, multi)
end
end
newparam(:type) do
desc "Set the mechanism for determining age. Default: atime."
newvalues(:atime, :mtime, :ctime)
defaultto :atime
end
newparam(:rmdirs, :boolean => true, :parent => Puppet::Parameter::Boolean) do
desc "Tidy directories in addition to files; that is, remove
directories whose age is older than the specified criteria.
This will only remove empty directories, so all contained
files must also be tidied before a directory gets removed."
end
# Erase PFile's validate method
validate do
end
def self.instances
[]
end
def depthfirst?
true
end
def initialize(hash)
super
# only allow backing up into filebuckets
self[:backup] = false unless self[:backup].is_a? Puppet::FileBucket::Dipper
end
# Make a file resource to remove a given file.
def mkfile(path)
# Force deletion, so directories actually get deleted.
Puppet::Type.type(:file).new :path => path, :backup => self[:backup], :ensure => :absent, :force => true
end
def retrieve
# Our ensure property knows how to retrieve everything for us.
if obj = @parameters[:ensure]
return obj.retrieve
else
return {}
end
end
# Hack things a bit so we only ever check the ensure property.
def properties
[]
end
def generate
return [] unless stat(self[:path])
case self[:recurse]
when Integer, Fixnum, Bignum, /^\d+$/
parameter = { :recurse => true, :recurselimit => self[:recurse] }
when true, :true, :inf
parameter = { :recurse => true }
end
if parameter
files = Puppet::FileServing::Fileset.new(self[:path], parameter).files.collect do |f|
f == "." ? self[:path] : ::File.join(self[:path], f)
end
else
files = [self[:path]]
end
result = files.find_all { |path| tidy?(path) }.collect { |path| mkfile(path) }.each { |file| notice "Tidying #{file.ref}" }.sort { |a,b| b[:path] <=> a[:path] }
# No need to worry about relationships if we don't have rmdirs; there won't be
# any directories.
return result unless rmdirs?
# Now make sure that all directories require the files they contain, if all are available,
# so that a directory is emptied before we try to remove it.
files_by_name = result.inject({}) { |hash, file| hash[file[:path]] = file; hash }
files_by_name.keys.sort { |a,b| b <=> b }.each do |path|
dir = ::File.dirname(path)
next unless resource = files_by_name[dir]
if resource[:require]
resource[:require] << Puppet::Resource.new(:file, path)
else
resource[:require] = [Puppet::Resource.new(:file, path)]
end
end
result
end
# Does a given path match our glob patterns, if any? Return true
# if no patterns have been provided.
def matches?(path)
return true unless self[:matches]
basename = File.basename(path)
flags = File::FNM_DOTMATCH | File::FNM_PATHNAME
if self[:matches].find {|pattern| File.fnmatch(pattern, basename, flags) }
return true
else
debug "No specified patterns match #{path}, not tidying"
return false
end
end
# Should we remove the specified file?
def tidy?(path)
return false unless stat = self.stat(path)
return false if stat.ftype == "directory" and ! rmdirs?
# The 'matches' parameter isn't OR'ed with the other tests --
# it's just used to reduce the list of files we can match.
return false if param = parameter(:matches) and ! param.tidy?(path, stat)
tested = false
[:age, :size].each do |name|
next unless param = parameter(name)
tested = true
return true if param.tidy?(path, stat)
end
# If they don't specify either, then the file should always be removed.
return true unless tested
false
end
def stat(path)
begin
- Puppet::FileSystem::File.new(path).lstat
+ Puppet::FileSystem.lstat(path)
rescue Errno::ENOENT => error
info "File does not exist"
return nil
rescue Errno::EACCES => error
warning "Could not stat; permission denied"
return nil
end
end
end
diff --git a/lib/puppet/type/user.rb b/lib/puppet/type/user.rb
index 5f41d1ac8..f56289256 100644
--- a/lib/puppet/type/user.rb
+++ b/lib/puppet/type/user.rb
@@ -1,569 +1,572 @@
require 'etc'
require 'facter'
require 'puppet/parameter/boolean'
require 'puppet/property/list'
require 'puppet/property/ordered_list'
require 'puppet/property/keyvalue'
module Puppet
newtype(:user) do
@doc = "Manage users. This type is mostly built to manage system
users, so it is lacking some features useful for managing normal
users.
This resource type uses the prescribed native tools for creating
groups and generally uses POSIX APIs for retrieving information
about them. It does not directly modify `/etc/passwd` or anything.
**Autorequires:** If Puppet is managing the user's primary group (as
provided in the `gid` attribute), the user resource will autorequire
that group. If Puppet is managing any role accounts corresponding to the
user's roles, the user resource will autorequire those role accounts."
feature :allows_duplicates,
"The provider supports duplicate users with the same UID."
feature :manages_homedir,
"The provider can create and remove home directories."
feature :manages_passwords,
"The provider can modify user passwords, by accepting a password
hash."
feature :manages_password_age,
"The provider can set age requirements and restrictions for
passwords."
feature :manages_password_salt,
"The provider can set a password salt. This is for providers that
implement PBKDF2 passwords with salt properties."
feature :manages_solaris_rbac,
"The provider can manage roles and normal users"
feature :manages_expiry,
"The provider can manage the expiry date for a user."
feature :system_users,
"The provider allows you to create system users with lower UIDs."
feature :manages_aix_lam,
"The provider can manage AIX Loadable Authentication Module (LAM) system."
feature :libuser,
"Allows local users to be managed on systems that also use some other
remote NSS method of managing accounts."
+ feature :manages_shell,
+ "The provider allows for setting shell and validates if possible"
+
newproperty(:ensure, :parent => Puppet::Property::Ensure) do
newvalue(:present, :event => :user_created) do
provider.create
end
newvalue(:absent, :event => :user_removed) do
provider.delete
end
newvalue(:role, :event => :role_created, :required_features => :manages_solaris_rbac) do
provider.create_role
end
desc "The basic state that the object should be in."
# If they're talking about the thing at all, they generally want to
# say it should exist.
defaultto do
if @resource.managed?
:present
else
nil
end
end
def retrieve
if provider.exists?
if provider.respond_to?(:is_role?) and provider.is_role?
return :role
else
return :present
end
else
return :absent
end
end
end
newproperty(:home) do
desc "The home directory of the user. The directory must be created
separately and is not currently checked for existence."
end
newproperty(:uid) do
desc "The user ID; must be specified numerically. If no user ID is
specified when creating a new user, then one will be chosen
automatically. This will likely result in the same user having
different UIDs on different systems, which is not recommended. This is
especially noteworthy when managing the same user on both Darwin and
other platforms, since Puppet does UID generation on Darwin, but
the underlying tools do so on other platforms.
On Windows, this property is read-only and will return the user's
security identifier (SID)."
munge do |value|
case value
when String
if value =~ /^[-0-9]+$/
value = Integer(value)
end
end
return value
end
end
newproperty(:gid) do
desc "The user's primary group. Can be specified numerically or by name.
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]+$/
Integer(value)
else
value
end
end
def insync?(is)
# We know the 'is' is a number, so we need to convert the 'should' to a number,
# too.
@should.each do |value|
return true if number = Puppet::Util.gid(value) and is == number
end
false
end
def sync
found = false
@should.each do |value|
if number = Puppet::Util.gid(value)
provider.gid = number
found = true
break
end
end
fail "Could not find group(s) #{@should.join(",")}" unless found
# Use the default event.
end
end
newproperty(:comment) do
desc "A description of the user. Generally the user's full name."
munge do |v|
v.respond_to?(:force_encoding) ? v.force_encoding(Encoding::ASCII_8BIT) : v
end
end
- newproperty(:shell) do
+ newproperty(:shell, :required_features => :manages_shell) do
desc "The user's login shell. The shell must exist and be
executable.
This attribute cannot be managed on Windows systems."
end
newproperty(:password, :required_features => :manages_passwords) do
desc %q{The user's password, in whatever encrypted format the local
system requires.
* Most modern Unix-like systems use salted SHA1 password hashes. You can use
Puppet's built-in `sha1` function to generate a hash from a password.
* Mac OS X 10.5 and 10.6 also use salted SHA1 hashes.
* Mac OS X 10.7 (Lion) uses salted SHA512 hashes. The Puppet Labs [stdlib][]
module contains a `str2saltedsha512` function which can generate password
hashes for Lion.
* Windows passwords can only be managed in cleartext, as there is no Windows API
for setting the password hash.
[stdlib]: https://github.com/puppetlabs/puppetlabs-stdlib/
Be sure to enclose any value that includes a dollar sign ($) in single
quotes (') to avoid accidental variable interpolation.}
validate do |value|
raise ArgumentError, "Passwords cannot include ':'" if value.is_a?(String) and value.include?(":")
end
def change_to_s(currentvalue, newvalue)
if currentvalue == :absent
return "created password"
else
return "changed password"
end
end
def is_to_s( currentvalue )
return '[old password hash redacted]'
end
def should_to_s( newvalue )
return '[new password hash redacted]'
end
end
newproperty(:password_min_age, :required_features => :manages_password_age) do
desc "The minimum number of days a password must be used before it may be changed."
munge do |value|
case value
when String
Integer(value)
else
value
end
end
validate do |value|
if value.to_s !~ /^-?\d+$/
raise ArgumentError, "Password minimum age must be provided as a number."
end
end
end
newproperty(:password_max_age, :required_features => :manages_password_age) do
desc "The maximum number of days a password may be used before it must be changed."
munge do |value|
case value
when String
Integer(value)
else
value
end
end
validate do |value|
if value.to_s !~ /^-?\d+$/
raise ArgumentError, "Password maximum age must be provided as a number."
end
end
end
newproperty(:groups, :parent => Puppet::Property::List) do
desc "The groups to which the user belongs. The primary group should
not be listed, and groups should be identified by name rather than by
GID. Multiple groups should be specified as an array."
validate do |value|
if value =~ /^\d+$/
raise ArgumentError, "Group names must be provided, not GID numbers."
end
raise ArgumentError, "Group names must be provided as an array, not a comma-separated list." if value.include?(",")
raise ArgumentError, "Group names must not be empty. If you want to specify \"no groups\" pass an empty array" if value.empty?
end
end
newparam(:name) do
desc "The user name. While naming limitations vary by operating system,
it is advisable to restrict names to the lowest common denominator,
which is a maximum of 8 characters beginning with a letter.
Note that Puppet considers user names to be case-sensitive, regardless
of the platform's own rules; be sure to always use the same case when
referring to a given user."
isnamevar
end
newparam(:membership) do
desc "Whether specified groups should be considered the **complete list**
(`inclusive`) or the **minimum list** (`minimum`) of groups to which
the user belongs. Defaults to `minimum`."
newvalues(:inclusive, :minimum)
defaultto :minimum
end
newparam(:system, :boolean => true, :parent => Puppet::Parameter::Boolean) do
desc "Whether the user is a system user, according to the OS's criteria;
on most platforms, a UID less than or equal to 500 indicates a system
user. Defaults to `false`."
defaultto false
end
newparam(:allowdupe, :boolean => true, :parent => Puppet::Parameter::Boolean) do
desc "Whether to allow duplicate UIDs. Defaults to `false`."
defaultto false
end
newparam(:managehome, :boolean => true, :parent => Puppet::Parameter::Boolean) do
desc "Whether to manage the home directory when managing the user.
This will create the home directory when `ensure => present`, and
delete the home directory when `ensure => absent`. Defaults to `false`."
defaultto false
validate do |val|
if munge(val)
raise ArgumentError, "User provider #{provider.class.name} can not manage home directories" if provider and not provider.class.manages_homedir?
end
end
end
newproperty(:expiry, :required_features => :manages_expiry) do
desc "The expiry date for this user. Must be provided in
a zero-padded YYYY-MM-DD format --- e.g. 2010-02-19.
If you want to make sure the user account does never
expire, you can pass the special value `absent`."
newvalues :absent
newvalues /^\d{4}-\d{2}-\d{2}$/
validate do |value|
if value.intern != :absent and value !~ /^\d{4}-\d{2}-\d{2}$/
raise ArgumentError, "Expiry dates must be YYYY-MM-DD or the string \"absent\""
end
end
end
# Autorequire the group, if it's around
autorequire(:group) do
autos = []
if obj = @parameters[:gid] and groups = obj.shouldorig
groups = groups.collect { |group|
if group =~ /^\d+$/
Integer(group)
else
group
end
}
groups.each { |group|
case group
when Integer
if resource = catalog.resources.find { |r| r.is_a?(Puppet::Type.type(:group)) and r.should(:gid) == group }
autos << resource
end
else
autos << group
end
}
end
if obj = @parameters[:groups] and groups = obj.should
autos += groups.split(",")
end
autos
end
# This method has been exposed for puppet to manage users and groups of
# files in its settings and should not be considered available outside of
# puppet.
#
# (see Puppet::Settings#service_user_available?)
#
# @return [Boolean] if the user exists on the system
# @api private
def exists?
provider.exists?
end
def retrieve
absent = false
properties.inject({}) { |prophash, property|
current_value = :absent
if absent
prophash[property] = :absent
else
current_value = property.retrieve
prophash[property] = current_value
end
if property.name == :ensure and current_value == :absent
absent = true
end
prophash
}
end
newproperty(:roles, :parent => Puppet::Property::List, :required_features => :manages_solaris_rbac) do
desc "The roles the user has. Multiple roles should be
specified as an array."
def membership
:role_membership
end
validate do |value|
if value =~ /^\d+$/
raise ArgumentError, "Role names must be provided, not numbers"
end
raise ArgumentError, "Role names must be provided as an array, not a comma-separated list" if value.include?(",")
end
end
#autorequire the roles that the user has
autorequire(:user) do
reqs = []
if roles_property = @parameters[:roles] and roles = roles_property.should
reqs += roles.split(',')
end
reqs
end
newparam(:role_membership) do
desc "Whether specified roles should be considered the **complete list**
(`inclusive`) or the **minimum list** (`minimum`) of roles the user
has. Defaults to `minimum`."
newvalues(:inclusive, :minimum)
defaultto :minimum
end
newproperty(:auths, :parent => Puppet::Property::List, :required_features => :manages_solaris_rbac) do
desc "The auths the user has. Multiple auths should be
specified as an array."
def membership
:auth_membership
end
validate do |value|
if value =~ /^\d+$/
raise ArgumentError, "Auth names must be provided, not numbers"
end
raise ArgumentError, "Auth names must be provided as an array, not a comma-separated list" if value.include?(",")
end
end
newparam(:auth_membership) do
desc "Whether specified auths should be considered the **complete list**
(`inclusive`) or the **minimum list** (`minimum`) of auths the user
has. Defaults to `minimum`."
newvalues(:inclusive, :minimum)
defaultto :minimum
end
newproperty(:profiles, :parent => Puppet::Property::OrderedList, :required_features => :manages_solaris_rbac) do
desc "The profiles the user has. Multiple profiles should be
specified as an array."
def membership
:profile_membership
end
validate do |value|
if value =~ /^\d+$/
raise ArgumentError, "Profile names must be provided, not numbers"
end
raise ArgumentError, "Profile names must be provided as an array, not a comma-separated list" if value.include?(",")
end
end
newparam(:profile_membership) do
desc "Whether specified roles should be treated as the **complete list**
(`inclusive`) or the **minimum list** (`minimum`) of roles
of which the user is a member. Defaults to `minimum`."
newvalues(:inclusive, :minimum)
defaultto :minimum
end
newproperty(:keys, :parent => Puppet::Property::KeyValue, :required_features => :manages_solaris_rbac) do
desc "Specify user attributes in an array of key = value pairs."
def membership
:key_membership
end
validate do |value|
raise ArgumentError, "Key/value pairs must be separated by an =" unless value.include?("=")
end
end
newparam(:key_membership) do
desc "Whether specified key/value pairs should be considered the
**complete list** (`inclusive`) or the **minimum list** (`minimum`) of
the user's attributes. Defaults to `minimum`."
newvalues(:inclusive, :minimum)
defaultto :minimum
end
newproperty(:project, :required_features => :manages_solaris_rbac) do
desc "The name of the project associated with a user."
end
newparam(:ia_load_module, :required_features => :manages_aix_lam) do
desc "The name of the I&A module to use to manage this user."
end
newproperty(:attributes, :parent => Puppet::Property::KeyValue, :required_features => :manages_aix_lam) do
desc "Specify AIX attributes for the user in an array of attribute = value pairs."
def membership
:attribute_membership
end
def delimiter
" "
end
validate do |value|
raise ArgumentError, "Attributes value pairs must be separated by an =" unless value.include?("=")
end
end
newparam(:attribute_membership) do
desc "Whether specified attribute value pairs should be treated as the
**complete list** (`inclusive`) or the **minimum list** (`minimum`) of
attribute/value pairs for the user. Defaults to `minimum`."
newvalues(:inclusive, :minimum)
defaultto :minimum
end
newproperty(:salt, :required_features => :manages_password_salt) do
desc "This is the 32 byte salt used to generate the PBKDF2 password used in
OS X"
end
newproperty(:iterations, :required_features => :manages_password_salt) do
desc "This is the number of iterations of a chained computation of the
password hash (http://en.wikipedia.org/wiki/PBKDF2). This parameter
is used in OS X"
munge do |value|
if value.is_a?(String) and value =~/^[-0-9]+$/
Integer(value)
else
value
end
end
end
newparam(:forcelocal, :boolean => true,
:required_features => :libuser,
:parent => Puppet::Parameter::Boolean) do
desc "Forces the mangement of local accounts when accounts are also
being managed by some other NSS"
defaultto false
end
end
end
diff --git a/lib/puppet/type/yumrepo.rb b/lib/puppet/type/yumrepo.rb
index f71736045..d173155dc 100644
--- a/lib/puppet/type/yumrepo.rb
+++ b/lib/puppet/type/yumrepo.rb
@@ -1,395 +1,270 @@
-require 'puppet/util/inifile'
-
-module Puppet
- # A property for one entry in a .ini-style file
- class IniProperty < Puppet::Property
- def insync?(is)
- # A should property of :absent is the same as nil
- if is.nil? && should == :absent
- return true
- end
- super(is)
- end
+require 'uri'
- def sync
- if safe_insync?(retrieve)
- result = nil
- else
- result = set(self.should)
- if should == :absent
- resource.section[inikey] = nil
- else
- resource.section[inikey] = should
- end
- end
- result
- end
+Puppet::Type.newtype(:yumrepo) do
+ @doc = "The client-side description of a yum repository. Repository
+ configurations are found by parsing `/etc/yum.conf` and
+ the files indicated by the `reposdir` option in that file
+ (see `yum.conf(5)` for details).
- def retrieve
- resource.section[inikey]
- end
+ Most parameters are identical to the ones documented
+ in the `yum.conf(5)` man page.
- def inikey
- name.to_s
- end
+ Continuation lines that yum supports (for the `baseurl`, for example)
+ are not supported. This type does not attempt to read or verify the
+ exinstence of files listed in the `include` attribute."
- # Set the key associated with this property to KEY, instead
- # of using the property's NAME
- def self.inikey(key)
- # Override the inikey instance method
- # Is there a way to do this without resorting to strings ?
- # Using a block fails because the block can't access
- # the variable 'key' in the outer scope
- self.class_eval("def inikey ; \"#{key.to_s}\" ; end")
- end
+ # Ensure yumrepos can be removed too.
+ ensurable
+ # Doc string for properties that can be made 'absent'
+ ABSENT_DOC="Set this to `absent` to remove it from the file completely."
+ # False can be false/0/no and True can be true/1/yes in yum.
+ YUM_BOOLEAN=/(True|False|0|1|No|Yes)/
+ YUM_BOOLEAN_DOC="Valid values are: False/0/No or True/1/Yes."
+ newparam(:name, :namevar => true) do
+ desc "The name of the repository. This corresponds to the
+ `repositoryid` parameter in `yum.conf(5)`."
end
- # Doc string for properties that can be made 'absent'
- ABSENT_DOC="Set this to `absent` to remove it from the file completely."
+ newparam(:target) do
+ desc "The filename to write the yum repository to."
- newtype(:yumrepo) do
- @doc = "The client-side description of a yum repository. Repository
- configurations are found by parsing `/etc/yum.conf` and
- the files indicated by the `reposdir` option in that file
- (see `yum.conf(5)` for details).
-
- Most parameters are identical to the ones documented
- in the `yum.conf(5)` man page.
-
- Continuation lines that yum supports (for the `baseurl`, for example)
- are not supported. This type does not attempt to read or verify the
- exinstence of files listed in the `include` attribute."
-
- class << self
- attr_accessor :filetype
- # The writer is only used for testing, there should be no need
- # to change yumconf or inifile in any other context
- attr_accessor :yumconf
- attr_writer :inifile
- end
+ defaultto :absent
+ end
- self.filetype = Puppet::Util::FileType.filetype(:flat)
-
- @inifile = nil
-
- @yumconf = "/etc/yum.conf"
-
- # Where to put files for brand new sections
- @defaultrepodir = nil
-
- def self.instances
- l = []
- check = validproperties
- clear
- inifile.each_section do |s|
- next if s.name == "main"
- obj = new(:name => s.name, :audit => check)
- current_values = obj.retrieve
- obj.eachproperty do |property|
- if current_values[property].nil?
- obj.delete(property.name)
- else
- property.should = current_values[property]
- end
- end
- obj.delete(:audit)
- l << obj
- end
- l
- end
+ newproperty(:descr) do
+ desc "A human-readable description of the repository.
+ This corresponds to the name parameter in `yum.conf(5)`.
+ #{ABSENT_DOC}"
- # Return the Puppet::Util::IniConfig::File for the whole yum config
- def self.inifile
- if @inifile.nil?
- @inifile = read
- main = @inifile['main']
- raise Puppet::Error, "File #{yumconf} does not contain a main section" if main.nil?
- reposdir = main['reposdir']
- reposdir ||= "/etc/yum.repos.d, /etc/yum/repos.d"
- reposdir.gsub!(/[\n,]/, " ")
- reposdir.split.each do |dir|
- Dir::glob("#{dir}/*.repo").each do |file|
- @inifile.read(file) if ::File.file?(file)
- end
- end
- reposdir.split.each do |dir|
- if ::File.directory?(dir) && ::File.writable?(dir)
- @defaultrepodir = dir
- break
- end
- end
- end
- @inifile
- end
+ newvalues(/.*/, :absent)
+ end
- # Parse the yum config files. Only exposed for the tests
- # Non-test code should use self.inifile to get at the
- # underlying file
- def self.read
- result = Puppet::Util::IniConfig::File.new
- result.read(yumconf)
- main = result['main']
- raise Puppet::Error, "File #{yumconf} does not contain a main section" if main.nil?
- reposdir = main['reposdir']
- reposdir ||= "/etc/yum.repos.d, /etc/yum/repos.d"
- reposdir.gsub!(/[\n,]/, " ")
- reposdir.split.each do |dir|
- Dir::glob("#{dir}/*.repo").each do |file|
- result.read(file) if ::File.file?(file)
- end
- end
- if @defaultrepodir.nil?
- reposdir.split.each do |dir|
- if ::File.directory?(dir) && ::File.writable?(dir)
- @defaultrepodir = dir
- break
- end
- end
- end
- result
- end
+ newproperty(:mirrorlist) do
+ desc "The URL that holds the list of mirrors for this repository.
+ #{ABSENT_DOC}"
- # Return the Puppet::Util::IniConfig::Section with name NAME
- # from the yum config
- def self.section(name)
- result = inifile[name]
- if result.nil?
- # Brand new section
- path = yumconf
- path = ::File.join(@defaultrepodir, "#{name}.repo") unless @defaultrepodir.nil?
- Puppet::info "create new repo #{name} in file #{path}"
- result = inifile.add_section(name, path)
- end
- result
+ newvalues(/.*/, :absent)
+ validate do |value|
+ parsed = URI.parse(value)
+ fail("Must be a valid URL") unless ['file', 'http', 'https', 'ftp'].include?(parsed.scheme)
end
+ end
- # Store all modifications back to disk
- def self.store
- inifile.store
- unless Puppet[:noop]
- target_mode = 0644 # FIXME: should be configurable
- inifile.each_file do |file|
- 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)
- end
- end
- end
- end
+ newproperty(:baseurl) do
+ desc "The URL for this repository. #{ABSENT_DOC}"
- # This is only used during testing.
- def self.clear
- @inifile = nil
- @yumconf = "/etc/yum.conf"
- @defaultrepodir = nil
+ newvalues(/.*/, :absent)
+ validate do |value|
+ parsed = URI.parse(value)
+ fail("Must be a valid URL") unless ['file', 'http', 'https', 'ftp'].include?(parsed.scheme)
end
+ end
- # Return the Puppet::Util::IniConfig::Section for this yumrepo resource
- def section
- self.class.section(self[:name])
- end
+ newproperty(:enabled) do
+ desc "Whether this repository is enabled.
+ #{YUM_BOOLEAN_DOC}
+ #{ABSENT_DOC}"
- # Store modifications to this yumrepo resource back to disk
- def flush
- self.class.store
- end
+ newvalues(YUM_BOOLEAN, :absent)
+ end
- newparam(:name) do
- desc "The name of the repository. This corresponds to the
- `repositoryid` parameter in `yum.conf(5)`."
- isnamevar
- end
+ newproperty(:gpgcheck) do
+ desc "Whether to check the GPG signature on packages installed
+ from this repository.
+ #{YUM_BOOLEAN_DOC}
+ #{ABSENT_DOC}"
- newproperty(:descr, :parent => Puppet::IniProperty) do
- desc "A human-readable description of the repository.
- This corresponds to the name parameter in `yum.conf(5)`.
- #{ABSENT_DOC}"
- newvalue(:absent) { self.should = :absent }
- newvalue(/.*/) { }
- inikey "name"
- end
+ newvalues(YUM_BOOLEAN, :absent)
+ end
- newproperty(:mirrorlist, :parent => Puppet::IniProperty) do
- desc "The URL that holds the list of mirrors for this repository.
- #{ABSENT_DOC}"
- newvalue(:absent) { self.should = :absent }
- # Should really check that it's a valid URL
- newvalue(/.*/) { }
- end
+ newproperty(:repo_gpgcheck) do
+ desc "Whether to check the GPG signature on repodata.
+ #{YUM_BOOLEAN_DOC}
+ #{ABSENT_DOC}"
- newproperty(:baseurl, :parent => Puppet::IniProperty) do
- desc "The URL for this repository. #{ABSENT_DOC}"
- newvalue(:absent) { self.should = :absent }
- # Should really check that it's a valid URL
- newvalue(/.*/) { }
- end
+ newvalues(YUM_BOOLEAN, :absent)
+ end
- newproperty(:enabled, :parent => Puppet::IniProperty) do
- desc "Whether this repository is enabled, as represented by a
- `0` or `1`. #{ABSENT_DOC}"
- newvalue(:absent) { self.should = :absent }
- newvalue(/^(0|1)$/) { }
- end
+ newproperty(:gpgkey) do
+ desc "The URL for the GPG key with which packages from this
+ repository are signed. #{ABSENT_DOC}"
- newproperty(:gpgcheck, :parent => Puppet::IniProperty) do
- desc "Whether to check the GPG signature on packages installed
- from this repository, as represented by a `0` or `1`.
- #{ABSENT_DOC}"
- newvalue(:absent) { self.should = :absent }
- newvalue(/^(0|1)$/) { }
+ newvalues(/.*/, :absent)
+ validate do |value|
+ parsed = URI.parse(value)
+ fail("Must be a valid URL") unless ['file', 'http', 'https', 'ftp'].include?(parsed.scheme)
end
+ end
- newproperty(:gpgkey, :parent => Puppet::IniProperty) do
- desc "The URL for the GPG key with which packages from this
- repository are signed. #{ABSENT_DOC}"
- newvalue(:absent) { self.should = :absent }
- # Should really check that it's a valid URL
- newvalue(/.*/) { }
- end
+ newproperty(:include) do
+ desc "The URL of a remote file containing additional yum configuration
+ settings. Puppet does not check for this file's existence or validity.
+ #{ABSENT_DOC}"
- newproperty(:include, :parent => Puppet::IniProperty) do
- desc "The URL of a remote file containing additional yum configuration
- settings. Puppet does not check for this file's existence or validity.
- #{ABSENT_DOC}"
- newvalue(:absent) { self.should = :absent }
- # Should really check that it's a valid URL
- newvalue(/.*/) { }
+ newvalues(/.*/, :absent)
+ validate do |value|
+ parsed = URI.parse(value)
+ fail("Must be a valid URL") unless ['file', 'http', 'https', 'ftp'].include?(parsed.scheme)
end
+ end
- newproperty(:exclude, :parent => Puppet::IniProperty) do
- desc "List of shell globs. Matching packages will never be
- considered in updates or installs for this repo.
- #{ABSENT_DOC}"
- newvalue(:absent) { self.should = :absent }
- newvalue(/.*/) { }
- end
+ newproperty(:exclude) do
+ desc "List of shell globs. Matching packages will never be
+ considered in updates or installs for this repo.
+ #{ABSENT_DOC}"
- newproperty(:includepkgs, :parent => Puppet::IniProperty) do
- desc "List of shell globs. If this is set, only packages
- matching one of the globs will be considered for
- update or install from this repo. #{ABSENT_DOC}"
- newvalue(:absent) { self.should = :absent }
- newvalue(/.*/) { }
- end
+ newvalues(/.*/, :absent)
+ end
- newproperty(:enablegroups, :parent => Puppet::IniProperty) do
- desc "Whether yum will allow the use of package groups for this
- repository, as represented by a `0` or `1`. #{ABSENT_DOC}"
- newvalue(:absent) { self.should = :absent }
- newvalue(/^(0|1)$/) { }
- end
+ newproperty(:includepkgs) do
+ desc "List of shell globs. If this is set, only packages
+ matching one of the globs will be considered for
+ update or install from this repo. #{ABSENT_DOC}"
- newproperty(:failovermethod, :parent => Puppet::IniProperty) do
- desc "The failover methode for this repository; should be either
- `roundrobin` or `priority`. #{ABSENT_DOC}"
- newvalue(:absent) { self.should = :absent }
- newvalue(%r{roundrobin|priority}) { }
- end
+ newvalues(/.*/, :absent)
+ end
- newproperty(:keepalive, :parent => Puppet::IniProperty) do
- desc "Whether HTTP/1.1 keepalive should be used with this repository, as
- represented by a `0` or `1`. #{ABSENT_DOC}"
- newvalue(:absent) { self.should = :absent }
- newvalue(/^(0|1)$/) { }
- end
+ newproperty(:enablegroups) do
+ desc "Whether yum will allow the use of package groups for this
+ repository.
+ #{YUM_BOOLEAN_DOC}
+ #{ABSENT_DOC}"
- newproperty(:http_caching, :parent => Puppet::IniProperty) do
- desc "What to cache from this repository. #{ABSENT_DOC}"
- newvalue(:absent) { self.should = :absent }
- newvalue(%r(packages|all|none)) { }
- end
-
- newproperty(:timeout, :parent => Puppet::IniProperty) do
- desc "Number of seconds to wait for a connection before timing
- out. #{ABSENT_DOC}"
- newvalue(:absent) { self.should = :absent }
- newvalue(%r{[0-9]+}) { }
- end
+ newvalues(YUM_BOOLEAN, :absent)
+ end
- newproperty(:metadata_expire, :parent => Puppet::IniProperty) do
- desc "Number of seconds after which the metadata will expire.
- #{ABSENT_DOC}"
- newvalue(:absent) { self.should = :absent }
- newvalue(%r{[0-9]+}) { }
- end
+ newproperty(:failovermethod) do
+ desc "The failover method for this repository; should be either
+ `roundrobin` or `priority`. #{ABSENT_DOC}"
- newproperty(:protect, :parent => Puppet::IniProperty) do
- desc "Enable or disable protection for this repository. Requires
- that the `protectbase` plugin is installed and enabled.
- #{ABSENT_DOC}"
- newvalue(:absent) { self.should = :absent }
- newvalue(/^(0|1)$/) { }
- end
+ newvalues(/roundrobin|priority/, :absent)
+ end
- newproperty(:priority, :parent => Puppet::IniProperty) do
- desc "Priority of this repository from 1-99. Requires that
- the `priorities` plugin is installed and enabled.
- #{ABSENT_DOC}"
- newvalue(:absent) { self.should = :absent }
- newvalue(%r{[1-9][0-9]?}) { }
- end
+ newproperty(:keepalive) do
+ desc "Whether HTTP/1.1 keepalive should be used with this repository.
+ #{YUM_BOOLEAN_DOC}
+ #{ABSENT_DOC}"
- newproperty(:cost, :parent => Puppet::IniProperty) do
- desc "Cost of this repository. #{ABSENT_DOC}"
- newvalue(:absent) { self.should = :absent }
- newvalue(%r{\d+}) { }
- end
+ newvalues(YUM_BOOLEAN, :absent)
+ end
- newproperty(:proxy, :parent => Puppet::IniProperty) do
- desc "URL to the proxy server for this repository. #{ABSENT_DOC}"
- newvalue(:absent) { self.should = :absent }
- # Should really check that it's a valid URL
- newvalue(/.*/) { }
- end
+ newproperty(:http_caching) do
+ desc "What to cache from this repository. #{ABSENT_DOC}"
- newproperty(:proxy_username, :parent => Puppet::IniProperty) do
- desc "Username for this proxy. #{ABSENT_DOC}"
- newvalue(:absent) { self.should = :absent }
- newvalue(/.*/) { }
- end
+ newvalues(/(packages|all|none)/, :absent)
+ end
- newproperty(:proxy_password, :parent => Puppet::IniProperty) do
- desc "Password for this proxy. #{ABSENT_DOC}"
- newvalue(:absent) { self.should = :absent }
- newvalue(/.*/) { }
- end
+ newproperty(:timeout) do
+ desc "Number of seconds to wait for a connection before timing
+ out. #{ABSENT_DOC}"
- newproperty(:s3_enabled, :parent => Puppet::IniProperty) do
- desc "Access the repo via S3. #{ABSENT_DOC}"
- newvalue(:absent) { self.should = :absent }
- newvalue(/^(0|1)$/) { }
- end
+ newvalues(/[0-9]+/, :absent)
+ end
- 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.
- #{ABSENT_DOC}"
- newvalue(:absent) { self.should = :absent }
- newvalue(/.*/) { }
- end
+ newproperty(:metadata_expire) do
+ desc "Number of seconds after which the metadata will expire.
+ #{ABSENT_DOC}"
- newproperty(:sslverify, :parent => Puppet::IniProperty) do
- desc "Should yum verify SSL certificates/hosts at all.
- Possible values are 'True' or 'False'.
- #{ABSENT_DOC}"
- newvalue(:absent) { self.should = :absent }
- newvalue(%r(True|False)) { }
+ newvalues(/[0-9]+/, :absent)
+ end
+
+ newproperty(:protect) do
+ desc "Enable or disable protection for this repository. Requires
+ that the `protectbase` plugin is installed and enabled.
+ #{YUM_BOOLEAN_DOC}
+ #{ABSENT_DOC}"
+
+ newvalues(YUM_BOOLEAN, :absent)
+ end
+
+ newproperty(:priority) do
+ desc "Priority of this repository from 1-99. Requires that
+ the `priorities` plugin is installed and enabled.
+ #{ABSENT_DOC}"
+
+ newvalues(/.*/, :absent)
+ validate do |value|
+ unless value == :absent or (1..99).include?(value.to_i)
+ fail("Must be within range 1-99")
+ end
end
+ end
+
+ newproperty(:cost) do
+ desc "Cost of this repository. #{ABSENT_DOC}"
+
+ newvalues(/\d+/, :absent)
+ end
+
+ newproperty(:proxy) do
+ desc "URL to the proxy server for this repository. #{ABSENT_DOC}"
- newproperty(:sslclientcert, :parent => Puppet::IniProperty) do
- desc "Path to the SSL client certificate yum should use to connect
- to repos/remote sites. #{ABSENT_DOC}"
- newvalue(:absent) { self.should = :absent }
- newvalue(/.*/) { }
+ newvalues(/.*/, :absent)
+ validate do |value|
+ parsed = URI.parse(value)
+ fail("Must be a valid URL") unless ['file', 'http', 'https', 'ftp'].include?(parsed.scheme)
end
+ end
+
+ newproperty(:proxy_username) do
+ desc "Username for this proxy. #{ABSENT_DOC}"
+
+ newvalues(/.*/, :absent)
+ end
+
+ newproperty(:proxy_password) do
+ desc "Password for this proxy. #{ABSENT_DOC}"
+
+ newvalues(/.*/, :absent)
+ end
+
+ newproperty(:s3_enabled) do
+ desc "Access the repo via S3.
+ #{YUM_BOOLEAN_DOC}
+ #{ABSENT_DOC}"
+
+ newvalues(YUM_BOOLEAN, :absent)
+ end
+
+ newproperty(:sslcacert) do
+ desc "Path to the directory containing the databases of the
+ certificate authorities yum should use to verify SSL certificates.
+ #{ABSENT_DOC}"
+
+ newvalues(/.*/, :absent)
+ end
+
+ newproperty(:sslverify) do
+ desc "Should yum verify SSL certificates/hosts at all.
+ #{YUM_BOOLEAN_DOC}
+ #{ABSENT_DOC}"
+
+ newvalues(YUM_BOOLEAN, :absent)
+ end
+
+ newproperty(:sslclientcert) do
+ desc "Path to the SSL client certificate yum should use to connect
+ to repos/remote sites. #{ABSENT_DOC}"
+
+ newvalues(/.*/, :absent)
+ end
+
+ newproperty(:sslclientkey) do
+ desc "Path to the SSL client key yum should use to connect
+ to repos/remote sites. #{ABSENT_DOC}"
+
+ newvalues(/.*/, :absent)
+ end
+
+ newproperty(:metalink) do
+ desc "Metalink for mirrors. #{ABSENT_DOC}"
- newproperty(:sslclientkey, :parent => Puppet::IniProperty) do
- desc "Path to the SSL client key yum should use to connect
- to repos/remote sites. #{ABSENT_DOC}"
- newvalue(:absent) { self.should = :absent }
- newvalue(/.*/) { }
+ newvalues(/.*/, :absent)
+ validate do |value|
+ parsed = URI.parse(value)
+ fail("Must be a valid URL") unless ['file', 'http', 'https', 'ftp'].include?(parsed.scheme)
end
end
+
end
diff --git a/lib/puppet/type/zone.rb b/lib/puppet/type/zone.rb
index 04c807383..09572bdb6 100644
--- a/lib/puppet/type/zone.rb
+++ b/lib/puppet/type/zone.rb
@@ -1,375 +1,385 @@
require 'puppet/property/list'
Puppet::Type.newtype(:zone) do
@doc = "Manages Solaris zones.
**Autorequires:** If Puppet is managing the directory specified as the root of
the zone's filesystem (with the `path` attribute), the zone resource will
autorequire that directory."
+module Puppet::Zone
class StateMachine
# A silly little state machine.
def initialize
@state = {}
@sequence = []
@state_aliases = {}
@default = nil
end
# The order of calling insert_state is important
def insert_state(name, transitions)
@sequence << name
@state[name] = transitions
end
def alias_state(state, salias)
@state_aliases[state] = salias
end
def name(n)
@state_aliases[n.to_sym] || n.to_sym
end
def index(state)
@sequence.index(name(state))
end
# return all states between fs and ss excluding fs
def sequence(fs, ss)
fi = index(fs)
si= index(ss)
(if fi > si
then @sequence[si .. fi].map{|i| @state[i]}.reverse
else @sequence[fi .. si].map{|i| @state[i]}
end)[1..-1]
end
def cmp?(a,b)
index(a) < index(b)
end
end
+end
ensurable do
desc "The running state of the zone. The valid states directly reflect
the states that `zoneadm` provides. The states are linear,
in that a zone must be `configured`, then `installed`, and
only then can be `running`. Note also that `halt` is currently
used to stop zones."
def self.fsm
return @fsm if @fsm
- @fsm = StateMachine.new
+ @fsm = Puppet::Zone::StateMachine.new
end
def self.alias_state(values)
values.each do |k,v|
fsm.alias_state(k,v)
end
end
def self.seqvalue(name, hash)
fsm.insert_state(name, hash)
self.newvalue name
end
# This is seq value because the order of declaration is important.
# i.e we go linearly from :absent -> :configured -> :installed -> :running
seqvalue :absent, :down => :destroy
seqvalue :configured, :up => :configure, :down => :uninstall
seqvalue :installed, :up => :install, :down => :stop
seqvalue :running, :up => :start
alias_state :incomplete => :installed, :ready => :installed, :shutting_down => :running
defaultto :running
def self.state_sequence(first, second)
fsm.sequence(first, second)
end
# Why override it? because property/ensure.rb has a default retrieve method
# that knows only about :present and :absent. That method just calls
# provider.exists? and returns :present if a result was returned.
def retrieve
provider.properties[:ensure]
end
def provider_sync_send(method)
warned = false
while provider.processing?
next if warned
info "Waiting for zone to finish processing"
warned = true
sleep 1
end
provider.send(method)
provider.flush()
end
def sync
method = nil
direction = up? ? :up : :down
# We need to get the state we're currently in and just call
# everything between it and us.
self.class.state_sequence(self.retrieve, self.should).each do |state|
method = state[direction]
raise Puppet::DevError, "Cannot move #{direction} from #{st[:name]}" unless method
provider_sync_send(method)
end
("zone_#{self.should}").intern
end
# Are we moving up the property tree?
def up?
self.class.fsm.cmp?(self.retrieve, self.should)
end
end
newparam(:name) do
desc "The name of the zone."
isnamevar
end
newparam(:id) do
desc "The numerical ID of the zone. This number is autogenerated
and cannot be changed."
end
newparam(:clone) do
desc "Instead of installing the zone, clone it from another zone.
If the zone root resides on a zfs file system, a snapshot will be
used to create the clone; if it resides on a ufs filesystem, a copy of the
zone will be used. The zone from which you clone must not be running."
end
newproperty(:ip, :parent => Puppet::Property::List) do
require 'ipaddr'
- desc "The IP address of the zone. IP addresses must be specified
- with the interface, separated by a colon, e.g.: bge0:192.168.0.1.
- For multiple interfaces, specify them in an array."
+ desc "The IP address of the zone. IP addresses **must** be specified
+ with an interface, and may optionally be specified with a default router
+ (sometimes called a defrouter). The interface, IP address, and default
+ router should be separated by colons to form a complete IP address string.
+ For example: `bge0:192.168.178.200` would be a valid IP address string
+ without a default router, and `bge0:192.168.178.200:192.168.178.1` adds a
+ default router to it.
+
+ For zones with multiple interfaces, the value of this attribute should be
+ an array of IP address strings (each of which must include an interface
+ and may include a default router)."
# The default action of list should is to lst.join(' '). By specifying
# @should, we ensure the should remains an array. If we override should, we
# should also override insync?() -- property/list.rb
def should
@should
end
# overridden so that we match with self.should
def insync?(is)
return true unless is
is = [] if is == :absent
is.sort == self.should.sort
end
end
newproperty(:iptype) do
desc "The IP stack type of the zone."
defaultto :shared
newvalue :shared
newvalue :exclusive
end
newproperty(:autoboot, :boolean => true) do
desc "Whether the zone should automatically boot."
defaultto true
newvalues(:true, :false)
end
newproperty(:path) do
desc "The root of the zone's filesystem. Must be a fully qualified
file name. If you include `%s` in the path, then it will be
replaced with the zone's name. Currently, you cannot use
Puppet to move a zone. Consequently this is a readonly property."
validate do |value|
raise ArgumentError, "The zone base must be fully qualified" unless value =~ /^\//
end
munge do |value|
if value =~ /%s/
value % @resource[:name]
else
value
end
end
end
newproperty(:pool) do
desc "The resource pool for this zone."
end
newproperty(:shares) do
desc "Number of FSS CPU shares allocated to the zone."
end
newproperty(:dataset, :parent => Puppet::Property::List ) do
desc "The list of datasets delegated to the non-global zone from the
global zone. All datasets must be zfs filesystem names which are
different from the mountpoint."
def should
@should
end
# overridden so that we match with self.should
def insync?(is)
return true unless is
is = [] if is == :absent
is.sort == self.should.sort
end
validate do |value|
unless value !~ /^\//
raise ArgumentError, "Datasets must be the name of a zfs filesystem"
end
end
end
newproperty(:inherit, :parent => Puppet::Property::List) do
desc "The list of directories that the zone inherits from the global
zone. All directories must be fully qualified."
def should
@should
end
# overridden so that we match with self.should
def insync?(is)
return true unless is
is = [] if is == :absent
is.sort == self.should.sort
end
validate do |value|
unless value =~ /^\//
raise ArgumentError, "Inherited filesystems must be fully qualified"
end
end
end
# Specify the sysidcfg file. This is pretty hackish, because it's
# only used to boot the zone the very first time.
newparam(:sysidcfg) do
desc %{The text to go into the `sysidcfg` file when the zone is first
booted. The best way is to use a template:
# $confdir/modules/site/templates/sysidcfg.erb
system_locale=en_US
timezone=GMT
terminal=xterms
security_policy=NONE
root_password=<%= password %>
timeserver=localhost
name_service=DNS {domain_name=<%= domain %> name_server=<%= nameserver %>}
network_interface=primary {hostname=<%= realhostname %>
ip_address=<%= ip %>
netmask=<%= netmask %>
protocol_ipv6=no
default_route=<%= defaultroute %>}
nfs4_domain=dynamic
And then call that:
zone { myzone:
ip => "bge0:192.168.0.23",
sysidcfg => template("site/sysidcfg.erb"),
path => "/opt/zones/myzone",
realhostname => "fully.qualified.domain.name"
}
The `sysidcfg` only matters on the first booting of the zone,
so Puppet only checks for it at that time.}
end
newparam(:create_args) do
desc "Arguments to the `zonecfg` create command. This can be used to create branded zones."
end
newparam(:install_args) do
desc "Arguments to the `zoneadm` install command. This can be used to create branded zones."
end
newparam(:realhostname) do
desc "The actual hostname of the zone."
end
# If Puppet is also managing the base dir or its parent dir, list them
# both as prerequisites.
autorequire(:file) do
if @parameters.include? :path
[@parameters[:path].value, ::File.dirname(@parameters[:path].value)]
else
nil
end
end
# If Puppet is also managing the zfs filesystem which is the zone dataset
# then list it as a prerequisite. Zpool's get autorequired by the zfs
# type. We just need to autorequire the dataset zfs itself as the zfs type
# will autorequire all of the zfs parents and zpool.
autorequire(:zfs) do
# Check if we have datasets in our zone configuration and autorequire each dataset
self[:dataset] if @parameters.include? :dataset
end
def validate_ip(ip, name)
IPAddr.new(ip) if ip
rescue ArgumentError
- self.fail "'#{ip}' is an invalid #{name}"
+ self.fail Puppet::Error, "'#{ip}' is an invalid #{name}", $!
end
def validate_exclusive(interface, address, router)
return if !interface.nil? and address.nil?
self.fail "only interface may be specified when using exclusive IP stack: #{interface}:#{address}"
end
def validate_shared(interface, address, router)
self.fail "ip must contain interface name and ip address separated by a \":\"" if interface.nil? or address.nil?
[address, router].each do |ip|
validate_ip(address, "IP address") unless ip.nil?
end
end
validate do
return unless self[:ip]
# self[:ip] reflects the type passed from proeprty:ip.should. If we
# override it and pass @should, then we get an array here back.
self[:ip].each do |ip|
interface, address, router = ip.split(':')
if self[:iptype] == :shared
validate_shared(interface, address, router)
else
validate_exclusive(interface, address, router)
end
end
end
def retrieve
provider.flush
hash = provider.properties
return setstatus(hash) unless hash.nil? or hash[:ensure] == :absent
# Return all properties as absent.
return Hash[properties.map{|p| [p, :absent]} ]
end
# Take the results of a listing and set everything appropriately.
def setstatus(hash)
prophash = {}
hash.each do |param, value|
next if param == :name
case self.class.attrtype(param)
when :property
# Only try to provide values for the properties we're managing
prop = self.property(param)
prophash[prop] = value if prop
else
self[param] = value
end
end
prophash
end
end
diff --git a/lib/puppet/util.rb b/lib/puppet/util.rb
index 0249c9c0d..a8b78da7c 100644
--- a/lib/puppet/util.rb
+++ b/lib/puppet/util.rb
@@ -1,544 +1,545 @@
# A module to collect utility functions.
require 'English'
require 'puppet/error'
require 'puppet/util/execution_stub'
require 'uri'
require 'tempfile'
require 'pathname'
require 'ostruct'
require 'puppet/util/platform'
require 'puppet/util/symbolic_file_mode'
require 'securerandom'
module Puppet
module Util
require 'puppet/util/monkey_patches'
require 'benchmark'
# These are all for backward compatibility -- these are methods that used
# to be in Puppet::Util but have been moved into external modules.
require 'puppet/util/posix'
extend Puppet::Util::POSIX
extend Puppet::Util::SymbolicFileMode
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)
else
0
end
end
# Run some code with a specific environment. Resets the environment back to
# what it was at the end of the code.
def self.withenv(hash)
saved = ENV.to_hash
hash.each do |name, val|
ENV[name.to_s] = val
end
yield
ensure
ENV.clear
saved.each do |name, val|
ENV[name] = val
end
end
# Execute a given chunk of code with a new umask.
def self.withumask(mask)
cur = File.umask(mask)
begin
yield
ensure
File.umask(cur)
end
end
# Change the process to a different user
def self.chuser
if group = Puppet[:group]
begin
Puppet::Util::SUIDManager.change_group(group, true)
rescue => detail
Puppet.warning "could not change to group #{group.inspect}: #{detail}"
$stderr.puts "could not change to group #{group.inspect}"
# Don't exit on failed group changes, since it's
# not fatal
#exit(74)
end
end
if user = Puppet[:user]
begin
Puppet::Util::SUIDManager.change_user(user, true)
rescue => detail
$stderr.puts "Could not change to user #{user}: #{detail}"
exit(74)
end
end
end
# Create instance methods for each of the log levels. This allows
# the messages to be a little richer. Most classes will be calling this
# method.
def self.logmethods(klass, useself = true)
Puppet::Util::Log.eachlevel { |level|
klass.send(:define_method, level, proc { |args|
args = args.join(" ") if args.is_a?(Array)
if useself
Puppet::Util::Log.create(
:level => level,
:source => self,
:message => args
)
else
Puppet::Util::Log.create(
:level => level,
:message => args
)
end
})
}
end
# Proxy a bunch of methods to another object.
def self.classproxy(klass, objmethod, *methods)
classobj = class << klass; self; end
methods.each do |method|
classobj.send(:define_method, method) do |*args|
obj = self.send(objmethod)
obj.send(method, *args)
end
end
end
# Proxy a bunch of methods to another object.
def self.proxy(klass, objmethod, *methods)
methods.each do |method|
klass.send(:define_method, method) do |*args|
obj = self.send(objmethod)
obj.send(method, *args)
end
end
end
def benchmark(*args)
msg = args.pop
level = args.pop
object = nil
if args.empty?
if respond_to?(level)
object = self
else
object = Puppet
end
else
object = args.pop
end
raise Puppet::DevError, "Failed to provide level to :benchmark" unless level
unless level == :none or object.respond_to? level
raise Puppet::DevError, "Benchmarked object does not respond to #{level}"
end
# Only benchmark if our log level is high enough
if level != :none and Puppet::Util::Log.sendlevel?(level)
seconds = Benchmark.realtime {
yield
}
object.send(level, msg + (" in %0.2f seconds" % seconds))
return seconds
else
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`
# environment variable.
#
# @api public
# @param bin [String] the name of the executable to find.
# @return [String] the absolute path to the found executable.
def which(bin)
if absolute_path?(bin)
return bin if FileTest.file? bin and FileTest.executable? bin
else
ENV['PATH'].split(File::PATH_SEPARATOR).each do |dir|
begin
dest = File.expand_path(File.join(dir, bin))
rescue ArgumentError => e
# if the user's PATH contains a literal tilde (~) character and HOME is not set, we may get
# an ArgumentError here. Let's check to see if that is the case; if not, re-raise whatever error
# was thrown.
if e.to_s =~ /HOME/ and (ENV['HOME'].nil? || ENV['HOME'] == "")
# if we get here they have a tilde in their PATH. We'll issue a single warning about this and then
# ignore this path element and carry on with our lives.
Puppet::Util::Warnings.warnonce("PATH contains a ~ character, and HOME is not set; ignoring PATH element '#{dir}'.")
elsif e.to_s =~ /doesn't exist|can't find user/
# ...otherwise, we just skip the non-existent entry, and do nothing.
Puppet::Util::Warnings.warnonce("Couldn't expand PATH containing a ~ character; ignoring PATH element '#{dir}'.")
else
raise
end
else
if Puppet.features.microsoft_windows? && File.extname(dest).empty?
exts = ENV['PATHEXT']
exts = exts ? exts.split(File::PATH_SEPARATOR) : %w[.COM .EXE .BAT .CMD]
exts.each do |ext|
destext = File.expand_path(dest + ext)
return destext if FileTest.file? destext and FileTest.executable? destext
end
end
return dest if FileTest.file? dest and FileTest.executable? dest
end
end
end
nil
end
module_function :which
# Determine in a platform-specific way whether a path is absolute. This
# defaults to the local platform if none is specified.
#
# Escape once for the string literal, and once for the regex.
slash = '[\\\\/]'
label = '[^\\\\/]+'
AbsolutePathWindows = %r!^(?:(?:[A-Z]:#{slash})|(?:#{slash}#{slash}#{label}#{slash}#{label})|(?:#{slash}#{slash}\?#{slash}#{label}))!io
AbsolutePathPosix = %r!^/!
def absolute_path?(path, platform=nil)
# Ruby only sets File::ALT_SEPARATOR on Windows and the Ruby standard
# library uses that to test what platform it's on. Normally in Puppet we
# would use Puppet.features.microsoft_windows?, but this method needs to
# be called during the initialization of features so it can't depend on
# that.
platform ||= Puppet::Util::Platform.windows? ? :windows : :posix
regex = case platform
when :windows
AbsolutePathWindows
when :posix
AbsolutePathPosix
else
raise Puppet::DevError, "unknown platform #{platform} in absolute_path"
end
!! (path =~ regex)
end
module_function :absolute_path?
# Convert a path to a file URI
def path_to_uri(path)
return unless path
params = { :scheme => 'file' }
if Puppet.features.microsoft_windows?
path = path.gsub(/\\/, '/')
if unc = /^\/\/([^\/]+)(\/.+)/.match(path)
params[:host] = unc[1]
path = unc[2]
elsif path =~ /^[a-z]:\//i
path = '/' + path
end
end
params[:path] = URI.escape(path)
begin
URI::Generic.build(params)
rescue => detail
- raise Puppet::Error, "Failed to convert '#{path}' to URI: #{detail}"
+ raise Puppet::Error, "Failed to convert '#{path}' to URI: #{detail}", detail.backtrace
end
end
module_function :path_to_uri
# Get the path component of a URI
def uri_to_path(uri)
return unless uri.is_a?(URI)
path = URI.unescape(uri.path)
if Puppet.features.microsoft_windows? and uri.scheme == 'file'
if uri.host
path = "//#{uri.host}" + path # UNC
else
path.sub!(/^\//, '')
end
end
path
end
module_function :uri_to_path
def safe_posix_fork(stdin=$stdin, stdout=$stdout, stderr=$stderr, &block)
child_pid = Kernel.fork do
$stdin.reopen(stdin)
$stdout.reopen(stdout)
$stderr.reopen(stderr)
3.upto(256){|fd| IO::new(fd).close rescue nil}
block.call if block
end
child_pid
end
module_function :safe_posix_fork
def memory
unless defined?(@pmap)
@pmap = which('pmap')
end
if @pmap
%x{#{@pmap} #{Process.pid}| grep total}.chomp.sub(/^\s*total\s+/, '').sub(/K$/, '').to_i
else
0
end
end
def symbolizehash(hash)
newhash = {}
hash.each do |name, val|
name = name.intern if name.respond_to? :intern
newhash[name] = val
end
newhash
end
module_function :symbolizehash
# Just benchmark, with no logging.
def thinmark
seconds = Benchmark.realtime {
yield
}
seconds
end
module_function :memory, :thinmark
# 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
# utility method to get the current call stack and format it to a human-readable string (which some IDEs/editors
# will recognize as links to the line numbers in the trace)
def self.pretty_backtrace(backtrace = caller(1))
backtrace.collect do |line|
_, path, rest = /^(.*):(\d+.*)$/.match(line).to_a
# If the path doesn't exist - like in one test, and like could happen in
# the world - we should just tolerate it and carry on. --daniel 2012-09-05
# Also, if we don't match, just include the whole line.
if path
path = Pathname(path).realpath rescue path
"#{path}:#{rest}"
else
line
end
end.join("\n")
end
# Replace a file, securely. This takes a block, and passes it the file
# handle of a file open for writing. Write the replacement content inside
# the block and it will safely replace the target file.
#
# This method will make no changes to the target file until the content is
# successfully written and the block returns without raising an error.
#
# As far as possible the state of the existing file, such as mode, is
# preserved. This works hard to avoid loss of any metadata, but will result
# in an inode change for the file.
#
# Arguments: `filename`, `default_mode`
#
# The filename is the file we are going to replace.
#
# The default_mode is the mode to use when the target file doesn't already
# exist; if the file is present we copy the existing mode/owner/group values
# across. The default_mode can be expressed as an octal integer, a numeric string (ie '0664')
# or a symbolic file mode.
DEFAULT_POSIX_MODE = 0644
DEFAULT_WINDOWS_MODE = nil
def replace_file(file, default_mode, &block)
raise Puppet::DevError, "replace_file requires a block" unless block_given?
if default_mode
unless valid_symbolic_mode?(default_mode)
raise Puppet::DevError, "replace_file default_mode: #{default_mode} is invalid"
end
mode = symbolic_mode_to_int(normalize_symbolic_mode(default_mode))
else
if Puppet.features.microsoft_windows?
mode = DEFAULT_WINDOWS_MODE
else
mode = DEFAULT_POSIX_MODE
end
end
- file = Puppet::FileSystem::File.new(file)
- tempfile = Tempfile.new(file.basename, file.dir.to_s)
+ file = Puppet::FileSystem.pathname(file)
+ tempfile = Tempfile.new(Puppet::FileSystem.basename_string(file), Puppet::FileSystem.dir_string(file))
# Set properties of the temporary file before we write the content, because
# Tempfile doesn't promise to be safe from reading by other people, just
# that it avoids races around creating the file.
#
# Our Windows emulation is pretty limited, and so we have to carefully
# and specifically handle the platform, which has all sorts of magic.
# So, unlike Unix, we don't pre-prep security; we use the default "quite
# secure" tempfile permissions instead. Magic happens later.
if !Puppet.features.microsoft_windows?
# Grab the current file mode, and fall back to the defaults.
- if file.exist?
- stat = file.path.lstat
+ effective_mode =
+ if Puppet::FileSystem.exist?(file)
+ stat = Puppet::FileSystem.lstat(file)
tempfile.chown(stat.uid, stat.gid)
- effective_mode = stat.mode
+ stat.mode
else
- effective_mode = mode
+ mode
end
if effective_mode
# We only care about the bottom four slots, which make the real mode,
# and not the rest of the platform stat call fluff and stuff.
tempfile.chmod(effective_mode & 07777)
end
end
# OK, now allow the caller to write the content of the file.
yield tempfile
# Now, make sure the data (which includes the mode) is safe on disk.
tempfile.flush
begin
tempfile.fsync
rescue NotImplementedError
# fsync may not be implemented by Ruby on all platforms, but
# there is absolutely no recovery path if we detect that. So, we just
# ignore the return code.
#
# However, don't be fooled: that is accepting that we are running in
# an unsafe fashion. If you are porting to a new platform don't stub
# that out.
end
tempfile.close
if Puppet.features.microsoft_windows?
# Windows ReplaceFile needs a file to exist, so touch handles this
- if !file.exist?
- file.touch
+ if !Puppet::FileSystem.exist?(file)
+ Puppet::FileSystem.touch(file)
if mode
- Puppet::Util::Windows::Security.set_mode(mode, file.path.to_s)
+ Puppet::Util::Windows::Security.set_mode(mode, Puppet::FileSystem.path_string(file))
end
end
# Yes, the arguments are reversed compared to the rename in the rest
# of the world.
- Puppet::Util::Windows::File.replace_file(file.path, tempfile.path)
+ Puppet::Util::Windows::File.replace_file(FileSystem.path_string(file), tempfile.path)
else
- File.rename(tempfile.path, file.path.to_s)
+ File.rename(tempfile.path, Puppet::FileSystem.path_string(file))
end
# Ideally, we would now fsync the directory as well, but Ruby doesn't
# have support for that, and it doesn't matter /that/ much...
# Return something true, and possibly useful.
- file.path
+ file
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.
#
# @api public
# @param [String] message a message to log if the block fails
# @param [Integer] code the exit code that the ruby interpreter should return if the block fails
# @yield
def exit_on_fail(message, code = 1)
yield
# First, we need to check and see if we are catching a SystemExit error. These will be raised
# when we daemonize/fork, and they do not necessarily indicate a failure case.
rescue SystemExit => err
raise err
# Now we need to catch *any* other kind of exception, because we may be calling third-party
# code (e.g. webrick), and we have no idea what they might throw.
rescue Exception => err
## NOTE: when debugging spec failures, these two lines can be very useful
#puts err.inspect
#puts Puppet::Util.pretty_backtrace(err.backtrace)
Puppet.log_exception(err, "Could not #{message}: #{err}")
Puppet::Util::Log.force_flushqueue()
exit(code)
end
module_function :exit_on_fail
def deterministic_rand(seed,max)
if defined?(Random) == 'constant' && Random.class == Class
Random.new(seed).rand(max).to_s
else
srand(seed)
result = rand(max).to_s
srand()
result
end
end
module_function :deterministic_rand
#######################################################################################################
# Deprecated methods relating to process execution; these have been moved to Puppet::Util::Execution
#######################################################################################################
def execpipe(command, failonfail = true, &block)
Puppet.deprecation_warning("Puppet::Util.execpipe is deprecated; please use Puppet::Util::Execution.execpipe")
Puppet::Util::Execution.execpipe(command, failonfail, &block)
end
module_function :execpipe
def execfail(command, exception)
Puppet.deprecation_warning("Puppet::Util.execfail is deprecated; please use Puppet::Util::Execution.execfail")
Puppet::Util::Execution.execfail(command, exception)
end
module_function :execfail
def execute(*args)
Puppet.deprecation_warning("Puppet::Util.execute is deprecated; please use Puppet::Util::Execution.execute")
Puppet::Util::Execution.execute(*args)
end
module_function :execute
end
end
require 'puppet/util/errors'
require 'puppet/util/methodhelper'
require 'puppet/util/metaid'
require 'puppet/util/classgen'
require 'puppet/util/docs'
require 'puppet/util/execution'
require 'puppet/util/logging'
require 'puppet/util/package'
require 'puppet/util/warnings'
diff --git a/lib/puppet/util/adsi.rb b/lib/puppet/util/adsi.rb
index 98639eabd..4c30e18c2 100644
--- a/lib/puppet/util/adsi.rb
+++ b/lib/puppet/util/adsi.rb
@@ -1,352 +1,368 @@
module Puppet::Util::ADSI
class << self
def connectable?(uri)
begin
!! connect(uri)
rescue
false
end
end
def connect(uri)
begin
WIN32OLE.connect(uri)
rescue Exception => e
- raise Puppet::Error.new( "ADSI connection error: #{e}" )
+ raise Puppet::Error.new( "ADSI connection error: #{e}", e )
end
end
def create(name, resource_type)
Puppet::Util::ADSI.connect(computer_uri).Create(resource_type, name)
end
def delete(name, resource_type)
Puppet::Util::ADSI.connect(computer_uri).Delete(resource_type, name)
end
def computer_name
unless @computer_name
buf = " " * 128
Win32API.new('kernel32', 'GetComputerName', ['P','P'], 'I').call(buf, buf.length.to_s)
@computer_name = buf.unpack("A*")[0]
end
@computer_name
end
def computer_uri(host = '.')
"WinNT://#{host}"
end
def wmi_resource_uri( host = '.' )
"winmgmts:{impersonationLevel=impersonate}!//#{host}/root/cimv2"
end
+ # @api private
+ def sid_uri_safe(sid)
+ return sid_uri(sid) if sid.kind_of?(Win32::Security::SID)
+
+ begin
+ sid = Win32::Security::SID.new(Win32::Security::SID.string_to_sid(sid))
+ sid_uri(sid)
+ rescue Win32::Security::SID::Error
+ return nil
+ end
+ 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
def wmi_connection
connect(wmi_resource_uri)
end
def execquery(query)
wmi_connection.execquery(query)
end
def sid_for_account(name)
Puppet.deprecation_warning "Puppet::Util::ADSI.sid_for_account is deprecated and will be removed in 3.0, use Puppet::Util::Windows::SID.name_to_sid instead."
Puppet::Util::Windows::Security.name_to_sid(name)
end
end
class User
extend Enumerable
attr_accessor :native_user
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(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 = '.')
+ if sid_uri = Puppet::Util::ADSI.sid_uri_safe(name) then return sid_uri end
+
host = '.' if ['NT AUTHORITY', 'BUILTIN', Socket.gethostname].include?(host)
Puppet::Util::ADSI.uri(name, 'user', host)
end
def uri
self.class.uri(sid.account, sid.domain)
end
def self.logon(name, password)
Puppet::Util::Windows::User.password_is?(name, password)
end
def [](attribute)
native_user.Get(attribute)
end
def []=(attribute, value)
native_user.Put(attribute, value)
end
def commit
begin
native_user.SetInfo unless native_user.nil?
rescue Exception => e
- raise Puppet::Error.new( "User update failed: #{e}" )
+ raise Puppet::Error.new( "User update failed: #{e}", e )
end
self
end
def password_is?(password)
self.class.logon(name, password)
end
def add_flag(flag_name, value)
flag = native_user.Get(flag_name) rescue 0
native_user.Put(flag_name, flag | value)
commit
end
def password=(password)
native_user.SetPassword(password)
commit
fADS_UF_DONT_EXPIRE_PASSWD = 0x10000
add_flag("UserFlags", fADS_UF_DONT_EXPIRE_PASSWD)
end
def groups
# WIN32OLE objects aren't enumerable, so no map
groups = []
native_user.Groups.each {|g| groups << g.Name} rescue nil
groups
end
def add_to_groups(*group_names)
group_names.each do |group_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_sids(sid)
end
end
alias remove_from_group remove_from_groups
def set_groups(desired_groups, minimum = true)
return if desired_groups.nil? or desired_groups.empty?
desired_groups = desired_groups.split(',').map(&:strip)
current_groups = self.groups
# First we add the user to all the groups it should be in but isn't
groups_to_add = desired_groups - current_groups
add_to_groups(*groups_to_add)
# Then we remove the user from all groups it is in but shouldn't be, if
# that's been requested
groups_to_remove = current_groups - desired_groups
remove_from_groups(*groups_to_remove) unless minimum
end
def self.create(name)
# Windows error 1379: The specified local group already exists.
raise Puppet::Error.new( "Cannot create user if group '#{name}' exists." ) if Puppet::Util::ADSI::Group.exists? name
new(name, Puppet::Util::ADSI.create(name, 'user'))
end
def self.exists?(name)
Puppet::Util::ADSI::connectable?(User.uri(*User.parse_name(name)))
end
def self.delete(name)
Puppet::Util::ADSI.delete(name, 'user')
end
def self.each(&block)
wql = Puppet::Util::ADSI.execquery('select name from win32_useraccount where localaccount = "TRUE"')
users = []
wql.each do |u|
users << new(u.name)
end
users.each(&block)
end
end
class UserProfile
def self.delete(sid)
begin
Puppet::Util::ADSI.wmi_connection.Delete("Win32_UserProfile.SID='#{sid}'")
rescue => e
# http://social.technet.microsoft.com/Forums/en/ITCG/thread/0f190051-ac96-4bf1-a47f-6b864bfacee5
# Prior to Vista SP1, there's no builtin way to programmatically
# delete user profiles (except for delprof.exe). So try to delete
# but warn if we fail
raise e unless e.message.include?('80041010')
Puppet.warning "Cannot delete user profile for '#{sid}' prior to Vista SP1"
end
end
end
class Group
extend Enumerable
attr_accessor :native_group
attr_reader :name
def initialize(name, native_group = nil)
@name = name
@native_group = native_group
end
def uri
self.class.uri(name)
end
def self.uri(name, host = '.')
+ if sid_uri = Puppet::Util::ADSI.sid_uri_safe(name) then return sid_uri end
+
Puppet::Util::ADSI.uri(name, 'group', host)
end
def native_group
@native_group ||= Puppet::Util::ADSI.connect(uri)
end
def commit
begin
native_group.SetInfo unless native_group.nil?
rescue Exception => e
- raise Puppet::Error.new( "Group update failed: #{e}" )
+ raise Puppet::Error.new( "Group update failed: #{e}", e )
end
self
end
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)
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 = []
native_group.Members.each {|m| members << m.Name}
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_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_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_hash.keys - desired_hash.keys).map { |sid| current_hash[sid] }
remove_member_sids(*members_to_remove)
end
def self.create(name)
# Windows error 2224: The account already exists.
raise Puppet::Error.new( "Cannot create group if user '#{name}' exists." ) if Puppet::Util::ADSI::User.exists? name
new(name, Puppet::Util::ADSI.create(name, 'group'))
end
def self.exists?(name)
Puppet::Util::ADSI.connectable?(Group.uri(name))
end
def self.delete(name)
Puppet::Util::ADSI.delete(name, 'group')
end
def self.each(&block)
wql = Puppet::Util::ADSI.execquery( 'select name from win32_group where localaccount = "TRUE"' )
groups = []
wql.each do |g|
groups << new(g.name)
end
groups.each(&block)
end
end
end
diff --git a/lib/puppet/util/autoload.rb b/lib/puppet/util/autoload.rb
index 5c3d3dea2..f8f7c260c 100644
--- a/lib/puppet/util/autoload.rb
+++ b/lib/puppet/util/autoload.rb
@@ -1,224 +1,227 @@
require 'pathname'
require 'puppet/util/rubygems'
require 'puppet/util/warnings'
require 'puppet/util/methodhelper'
# Autoload paths, either based on names or all at once.
class Puppet::Util::Autoload
include Puppet::Util::MethodHelper
@autoloaders = {}
@loaded = {}
class << self
attr_reader :autoloaders
attr_accessor :loaded
private :autoloaders, :loaded
def gem_source
@gem_source ||= Puppet::Util::RubyGems::Source.new
end
# Has a given path been loaded? This is used for testing whether a
# changed file should be loaded or just ignored. This is only
# used in network/client/master, when downloading plugins, to
# see if a given plugin is currently loaded and thus should be
# reloaded.
def loaded?(path)
path = cleanpath(path).chomp('.rb')
loaded.include?(path)
end
# Save the fact that a given path has been loaded. This is so
# we can load downloaded plugins if they've already been loaded
# into memory.
def mark_loaded(name, file)
name = cleanpath(name).chomp('.rb')
ruby_file = name + ".rb"
$LOADED_FEATURES << ruby_file unless $LOADED_FEATURES.include?(ruby_file)
loaded[name] = [file, File.mtime(file)]
end
def changed?(name)
name = cleanpath(name).chomp('.rb')
return true unless loaded.include?(name)
file, old_mtime = loaded[name]
- return true unless file == get_file(name)
+ environment = Puppet.lookup(:environments).get(Puppet[:environment])
+ return true unless file == get_file(name, environment)
begin
old_mtime.to_i != File.mtime(file).to_i
rescue Errno::ENOENT
true
end
end
# Load a single plugin by name. We use 'load' here so we can reload a
# given plugin.
- def load_file(name, env=nil)
+ def load_file(name, env)
file = get_file(name.to_s, env)
return false unless file
begin
mark_loaded(name, file)
Kernel.load file, @wrap
return true
rescue SystemExit,NoMemoryError
raise
rescue Exception => detail
message = "Could not autoload #{name}: #{detail}"
Puppet.log_exception(detail, message)
- raise Puppet::Error, message
+ raise Puppet::Error, message, detail.backtrace
end
end
def loadall(path)
# Load every instance of everything we can find.
files_to_load(path).each do |file|
name = file.chomp(".rb")
- load_file(name) unless loaded?(name)
+ load_file(name, nil) unless loaded?(name)
end
end
def reload_changed
- loaded.keys.each { |file| load_file(file) if changed?(file) }
+ loaded.keys.each { |file| load_file(file, nil) if changed?(file) }
end
# Get the correct file to load for a given path
# returns nil if no file is found
- def get_file(name, env=nil)
+ def get_file(name, env)
name = name + '.rb' unless name =~ /\.rb$/
- path = search_directories(env).find { |dir| Puppet::FileSystem::File.exist?(File.join(dir, name)) }
+ path = search_directories(env).find { |dir| Puppet::FileSystem.exist?(File.join(dir, name)) }
path and File.join(path, name)
end
def files_to_load(path)
- search_directories.map {|dir| files_in_dir(dir, path) }.flatten.uniq
+ search_directories(nil).map {|dir| files_in_dir(dir, path) }.flatten.uniq
end
def files_in_dir(dir, path)
dir = Pathname.new(File.expand_path(dir))
Dir.glob(File.join(dir, path, "*.rb")).collect do |file|
Pathname.new(file).relative_path_from(dir).to_s
end
end
- def module_directories(env=nil)
- # We have to require this late in the process because otherwise we might
- # have load order issues. Since require is much slower than defined?, we
- # can skip that - and save some 2,155 invocations of require in my real
- # world testing. --daniel 2012-07-10
- require 'puppet/node/environment' unless defined?(Puppet::Node::Environment)
-
- real_env = Puppet::Node::Environment.new(env)
-
+ def module_directories(env)
# 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.
$env_module_directories ||= {}
# This is a little bit of a hack. Basically, the autoloader is being
# called indirectly during application bootstrapping when we do things
# such as check "features". However, during bootstrapping, we haven't
# yet parsed all of the command line parameters nor the config files,
# and thus we don't yet know with certainty what the module path is.
# This should be irrelevant during bootstrapping, because anything that
# we are attempting to load during bootstrapping should be something
# that we ship with puppet, and thus the module path is irrelevant.
#
# In the long term, I think the way that we want to handle this is to
# have the autoloader ignore the module path in all cases where it is
# not specifically requested (e.g., by a constructor param or
# something)... because there are very few cases where we should
# actually be loading code from the module path. However, until that
# happens, we at least need a way to prevent the autoloader from
# attempting to access the module path before it is initialized. For
# now we are accomplishing that by calling the
# "app_defaults_initialized?" method on the main puppet Settings object.
# --cprice 2012-03-16
if Puppet.settings.app_defaults_initialized?
+ env ||= Puppet.lookup(:environments).get(Puppet[:environment])
+
# if the app defaults have been initialized then it should be safe to access the module path setting.
- $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|
+ $env_module_directories[env] ||= env.modulepath.collect do |dir|
+ Dir.entries(dir).reject { |f| f =~ /^\./ }.collect { |f| File.join(dir, f, "lib") }
+ end.flatten.find_all do |d|
FileTest.directory?(d)
end
else
# if we get here, the app defaults have not been initialized, so we basically use an empty module path.
[]
end
end
def libdirs()
# See the comments in #module_directories above. Basically, we need to be careful not to try to access the
# libdir before we know for sure that all of the settings have been initialized (e.g., during bootstrapping).
if (Puppet.settings.app_defaults_initialized?)
Puppet[:libdir].split(File::PATH_SEPARATOR)
else
[]
end
end
def gem_directories
gem_source.directories
end
- def search_directories(env=nil)
+ def search_directories(env)
[gem_directories, module_directories(env), libdirs(), $LOAD_PATH].flatten
end
# Normalize a path. This converts ALT_SEPARATOR to SEPARATOR on Windows
# and eliminates unnecessary parts of a path.
def cleanpath(path)
# There are two cases here because cleanpath does not handle absolute
# paths correctly on windows (c:\ and c:/ are treated as distinct) but
# we don't want to convert relative paths to absolute
if Puppet::Util.absolute_path?(path)
File.expand_path(path)
else
Pathname.new(path).cleanpath.to_s
end
end
end
# Send [] and []= to the @autoloaders hash
Puppet::Util.classproxy self, :autoloaders, "[]", "[]="
attr_accessor :object, :path, :objwarn, :wrap
def initialize(obj, path, options = {})
@path = path.to_s
raise ArgumentError, "Autoload paths cannot be fully qualified" if Puppet::Util.absolute_path?(@path)
@object = obj
self.class[obj] = self
set_options(options)
@wrap = true unless defined?(@wrap)
end
- def load(name, env=nil)
+ def load(name, env = nil)
self.class.load_file(expand(name), env)
end
- # Load all instances that we can. This uses require, rather than load,
- # so that already-loaded files don't get reloaded unnecessarily.
+ # Load all instances from a path of Autoload.search_directories matching the
+ # relative path this Autoloader was initialized with. For example, if we
+ # have created a Puppet::Util::Autoload for Puppet::Type::User with a path of
+ # 'puppet/provider/user', the search_directories path will be searched for
+ # all ruby files matching puppet/provider/user/*.rb and they will then be
+ # loaded from the first directory in the search path providing them. So
+ # earlier entries in the search path may shadow later entries.
+ #
+ # This uses require, rather than load, so that already-loaded files don't get
+ # reloaded unnecessarily.
def loadall
self.class.loadall(@path)
end
def loaded?(name)
self.class.loaded?(expand(name))
end
def changed?(name)
self.class.changed?(expand(name))
end
def files_to_load
self.class.files_to_load(@path)
end
def expand(name)
::File.join(@path, name.to_s)
end
end
diff --git a/lib/puppet/util/backups.rb b/lib/puppet/util/backups.rb
index 8ae14c190..249d27861 100644
--- a/lib/puppet/util/backups.rb
+++ b/lib/puppet/util/backups.rb
@@ -1,86 +1,86 @@
require 'find'
require 'fileutils'
module Puppet::Util::Backups
# Deal with backups.
def perform_backup(file = nil)
# if they specifically don't want a backup, then just say
# we're good
return true unless self[:backup]
# let the path be specified
file ||= self[:path]
- return true unless Puppet::FileSystem::File.exist?(file)
+ return true unless Puppet::FileSystem.exist?(file)
return(self.bucket ? perform_backup_with_bucket(file) : perform_backup_with_backuplocal(file, self[:backup]))
end
private
def perform_backup_with_bucket(fileobj)
file = (fileobj.class == String) ? fileobj : fileobj.name
- case Puppet::FileSystem::File.new(file).lstat.ftype
+ case Puppet::FileSystem.lstat(file).ftype
when "directory"
# we don't need to backup directories when recurse is on
return true if self[:recurse]
info "Recursively backing up to filebucket"
Find.find(self[:path]) { |f| backup_file_with_filebucket(f) if File.file?(f) }
when "file"; backup_file_with_filebucket(file)
when "link";
end
true
end
def perform_backup_with_backuplocal(fileobj, backup)
file = (fileobj.class == String) ? fileobj : fileobj.name
newfile = file + backup
remove_backup(newfile)
begin
bfile = file + backup
# N.B. cp_r works on both files and directories
FileUtils.cp_r(file, bfile, :preserve => true)
return true
rescue => detail
# since they said they want a backup, let's error out
# if we couldn't make one
- self.fail "Could not back #{file} up: #{detail.message}"
+ self.fail Puppet::Error, "Could not back #{file} up: #{detail.message}", detail
end
end
def remove_backup(newfile)
if self.class.name == :file and self[:links] != :follow
method = :lstat
else
method = :stat
end
begin
- stat = Puppet::FileSystem::File.new(newfile).send(method)
+ stat = Puppet::FileSystem.send(method, newfile)
rescue Errno::ENOENT
return
end
if stat.ftype == "directory"
raise Puppet::Error, "Will not remove directory backup #{newfile}; use a filebucket"
end
info "Removing old backup of type #{stat.ftype}"
begin
- Puppet::FileSystem::File.unlink(newfile)
+ Puppet::FileSystem.unlink(newfile)
rescue => detail
message = "Could not remove old backup: #{detail}"
self.log_exception(detail, message)
- self.fail message
+ self.fail Puppet::Error, message, detail
end
end
def backup_file_with_filebucket(f)
sum = self.bucket.backup(f)
self.info "Filebucketed #{f} to #{self.bucket.name} with sum #{sum}"
return sum
end
end
diff --git a/lib/puppet/util/checksums.rb b/lib/puppet/util/checksums.rb
index 1f28fe8a7..76772802f 100644
--- a/lib/puppet/util/checksums.rb
+++ b/lib/puppet/util/checksums.rb
@@ -1,143 +1,143 @@
require 'digest/md5'
require 'digest/sha1'
# A stand-alone module for calculating checksums
# in a generic way.
module Puppet::Util::Checksums
class FakeChecksum
def <<(*args)
self
end
end
# Is the provided string a checksum?
def checksum?(string)
string =~ /^\{(\w{3,5})\}\S+/
end
# Strip the checksum type from an existing checksum
def sumdata(checksum)
checksum =~ /^\{(\w+)\}(.+)/ ? $2 : nil
end
# Strip the checksum type from an existing checksum
def sumtype(checksum)
checksum =~ /^\{(\w+)\}/ ? $1 : nil
end
# Calculate a checksum using Digest::MD5.
def md5(content)
Digest::MD5.hexdigest(content)
end
# Calculate a checksum of the first 500 chars of the content using Digest::MD5.
def md5lite(content)
md5(content[0..511])
end
# Calculate a checksum of a file's content using Digest::MD5.
def md5_file(filename, lite = false)
digest = Digest::MD5.new
checksum_file(digest, filename, lite)
end
# Calculate a checksum of the first 500 chars of a file's content using Digest::MD5.
def md5lite_file(filename)
md5_file(filename, true)
end
def md5_stream(&block)
digest = Digest::MD5.new
yield digest
digest.hexdigest
end
alias :md5lite_stream :md5_stream
# Return the :mtime timestamp of a file.
def mtime_file(filename)
- Puppet::FileSystem::File.new(filename).stat.send(:mtime)
+ Puppet::FileSystem.stat(filename).send(:mtime)
end
# by definition this doesn't exist
# but we still need to execute the block given
def mtime_stream
noop_digest = FakeChecksum.new
yield noop_digest
nil
end
def mtime(content)
""
end
# Calculate a checksum using Digest::SHA1.
def sha1(content)
Digest::SHA1.hexdigest(content)
end
# Calculate a checksum of the first 500 chars of the content using Digest::SHA1.
def sha1lite(content)
sha1(content[0..511])
end
# Calculate a checksum of a file's content using Digest::SHA1.
def sha1_file(filename, lite = false)
digest = Digest::SHA1.new
checksum_file(digest, filename, lite)
end
# Calculate a checksum of the first 500 chars of a file's content using Digest::SHA1.
def sha1lite_file(filename)
sha1_file(filename, true)
end
def sha1_stream
digest = Digest::SHA1.new
yield digest
digest.hexdigest
end
alias :sha1lite_stream :sha1_stream
# Return the :ctime of a file.
def ctime_file(filename)
- Puppet::FileSystem::File.new(filename).stat.send(:ctime)
+ Puppet::FileSystem.stat(filename).send(:ctime)
end
alias :ctime_stream :mtime_stream
def ctime(content)
""
end
# Return a "no checksum"
def none_file(filename)
""
end
def none_stream
noop_digest = FakeChecksum.new
yield noop_digest
""
end
def none(content)
""
end
private
# Perform an incremental checksum on a file.
def checksum_file(digest, filename, lite = false)
buffer = lite ? 512 : 4096
File.open(filename, 'rb') do |file|
while content = file.read(buffer)
digest << content
break if lite
end
end
digest.hexdigest
end
end
diff --git a/lib/puppet/util/classgen.rb b/lib/puppet/util/classgen.rb
index ee1ea5f46..78e8732b4 100644
--- a/lib/puppet/util/classgen.rb
+++ b/lib/puppet/util/classgen.rb
@@ -1,236 +1,236 @@
require 'puppet/util/methodhelper'
module Puppet
class ConstantAlreadyDefined < Error; end
class SubclassAlreadyDefined < Error; end
end
# This is a utility module for generating classes.
# @api public
#
module Puppet::Util::ClassGen
include Puppet::Util::MethodHelper
include Puppet::Util
# Create a new class.
# @param name [String] the name of the generated class
# @param options [Hash] a hash of options
# @option options [Array<Class>] :array if specified, the generated class is appended to this array
# @option options [Hash<{String => Object}>] :attributes a hash that is applied to the generated class
# by calling setter methods corresponding to this hash's keys/value pairs. This is done before the given
# block is evaluated.
# @option options [Proc] :block a block to evaluate in the context of the class (this block can be provided
# this way, or as a normal yield block).
# @option options [String] :constant (name with first letter capitalized) what to set the constant that references
# the generated class to.
# @option options [Hash] :hash a hash of existing classes that this class is appended to (name => class).
# This hash must be specified if the `:overwrite` option is set to `true`.
# @option options [Boolean] :overwrite whether an overwrite of an existing class should be allowed (requires also
# defining the `:hash` with existing classes as the test is based on the content of this hash).
# @option options [Class] :parent (self) the parent class of the generated class.
# @option options [String] ('') :prefix the constant prefix to prepend to the constant name referencing the
# generated class.
# @return [Class] the generated class
#
def genclass(name, options = {}, &block)
genthing(name, Class, options, block)
end
# Creates a new module.
# @param name [String] the name of the generated module
# @param options [Hash] hash with options
# @option options [Array<Class>] :array if specified, the generated class is appended to this array
# @option options [Hash<{String => Object}>] :attributes a hash that is applied to the generated class
# by calling setter methods corresponding to this hash's keys/value pairs. This is done before the given
# block is evaluated.
# @option options [Proc] :block a block to evaluate in the context of the class (this block can be provided
# this way, or as a normal yield block).
# @option options [String] :constant (name with first letter capitalized) what to set the constant that references
# the generated class to.
# @option options [Hash] :hash a hash of existing classes that this class is appended to (name => class).
# This hash must be specified if the `:overwrite` option is set to `true`.
# @option options [Boolean] :overwrite whether an overwrite of an existing class should be allowed (requires also
# defining the `:hash` with existing classes as the test is based on the content of this hash).
# the capitalized name is appended and the result is set as the constant.
# @option options [String] ('') :prefix the constant prefix to prepend to the constant name referencing the
# generated class.
# @return [Module] the generated Module
def genmodule(name, options = {}, &block)
genthing(name, Module, options, block)
end
# Removes an existing class.
# @param name [String] the name of the class to remove
# @param options [Hash] options
# @option options [Hash] :hash a hash of existing classes from which the class to be removed is also removed
# @return [Boolean] whether the class was removed or not
#
def rmclass(name, options)
options = symbolize_options(options)
const = genconst_string(name, options)
retval = false
if is_constant_defined?(const)
remove_const(const)
retval = true
end
if hash = options[:hash] and hash.include? name
hash.delete(name)
retval = true
end
# Let them know whether we did actually delete a subclass.
retval
end
private
# Generates the constant to create or remove.
# @api private
def genconst_string(name, options)
unless const = options[:constant]
prefix = options[:prefix] || ""
const = prefix + name2const(name)
end
const
end
# This does the actual work of creating our class or module. It's just a
# slightly abstract version of genclass.
# @api private
def genthing(name, type, options, block)
options = symbolize_options(options)
name = name.to_s.downcase.intern
if type == Module
#evalmethod = :module_eval
evalmethod = :class_eval
# Create the class, with the correct name.
klass = Module.new do
class << self
attr_reader :name
end
@name = name
end
else
options[:parent] ||= self
evalmethod = :class_eval
# Create the class, with the correct name.
klass = Class.new(options[:parent]) do
@name = name
end
end
# Create the constant as appropriation.
handleclassconst(klass, name, options)
# Initialize any necessary variables.
initclass(klass, options)
block ||= options[:block]
# Evaluate the passed block if there is one. This should usually
# define all of the work.
klass.send(evalmethod, &block) if block
klass.postinit if klass.respond_to? :postinit
# Store the class in hashes or arrays or whatever.
storeclass(klass, name, options)
klass
end
# const_defined? in Ruby 1.9 behaves differently in terms
# of which class hierarchy it polls for nested namespaces
#
# See http://redmine.ruby-lang.org/issues/show/1915
# @api private
#
def is_constant_defined?(const)
- if ::RUBY_VERSION =~ /^1.8/
+ if ::RUBY_VERSION =~ /^1\.8/
const_defined?(const)
else
const_defined?(const, false)
end
end
# Handle the setting and/or removing of the associated constant.
# @api private
#
def handleclassconst(klass, name, options)
const = genconst_string(name, options)
if is_constant_defined?(const)
if options[:overwrite]
Puppet.info "Redefining #{name} in #{self}"
remove_const(const)
else
raise Puppet::ConstantAlreadyDefined,
"Class #{const} is already defined in #{self}"
end
end
const_set(const, klass)
const
end
# Perform the initializations on the class.
# @api private
#
def initclass(klass, options)
klass.initvars if klass.respond_to? :initvars
if attrs = options[:attributes]
attrs.each do |param, value|
method = param.to_s + "="
klass.send(method, value) if klass.respond_to? method
end
end
[:include, :extend].each do |method|
if set = options[method]
set = [set] unless set.is_a?(Array)
set.each do |mod|
klass.send(method, mod)
end
end
end
klass.preinit if klass.respond_to? :preinit
end
# Convert our name to a constant.
# @api private
def name2const(name)
name.to_s.capitalize
end
# Store the class in the appropriate places.
# @api private
def storeclass(klass, klassname, options)
if hash = options[:hash]
if hash.include? klassname and ! options[:overwrite]
raise Puppet::SubclassAlreadyDefined,
"Already a generated class named #{klassname}"
end
hash[klassname] = klass
end
# If we were told to stick it in a hash, then do so
if array = options[:array]
if (klass.respond_to? :name and
array.find { |c| c.name == klassname } and
! options[:overwrite])
raise Puppet::SubclassAlreadyDefined,
"Already a generated class named #{klassname}"
end
array << klass
end
end
end
diff --git a/lib/puppet/util/colors.rb b/lib/puppet/util/colors.rb
index a10e240a1..622c3b583 100644
--- a/lib/puppet/util/colors.rb
+++ b/lib/puppet/util/colors.rb
@@ -1,183 +1,175 @@
require 'puppet/util/platform'
module Puppet::Util::Colors
BLACK = {:console => "\e[0;30m", :html => "color: #FFA0A0" }
RED = {:console => "\e[0;31m", :html => "color: #FFA0A0" }
GREEN = {:console => "\e[0;32m", :html => "color: #00CD00" }
YELLOW = {:console => "\e[0;33m", :html => "color: #FFFF60" }
BLUE = {:console => "\e[0;34m", :html => "color: #80A0FF" }
MAGENTA = {:console => "\e[0;35m", :html => "color: #FFA500" }
CYAN = {:console => "\e[0;36m", :html => "color: #40FFFF" }
WHITE = {:console => "\e[0;37m", :html => "color: #FFFFFF" }
HBLACK = {:console => "\e[1;30m", :html => "color: #FFA0A0" }
HRED = {:console => "\e[1;31m", :html => "color: #FFA0A0" }
HGREEN = {:console => "\e[1;32m", :html => "color: #00CD00" }
HYELLOW = {:console => "\e[1;33m", :html => "color: #FFFF60" }
HBLUE = {:console => "\e[1;34m", :html => "color: #80A0FF" }
HMAGENTA = {:console => "\e[1;35m", :html => "color: #FFA500" }
HCYAN = {:console => "\e[1;36m", :html => "color: #40FFFF" }
HWHITE = {:console => "\e[1;37m", :html => "color: #FFFFFF" }
BG_RED = {:console => "\e[0;41m", :html => "background: #FFA0A0"}
BG_GREEN = {:console => "\e[0;42m", :html => "background: #00CD00"}
BG_YELLOW = {:console => "\e[0;43m", :html => "background: #FFFF60"}
BG_BLUE = {:console => "\e[0;44m", :html => "background: #80A0FF"}
BG_MAGENTA = {:console => "\e[0;45m", :html => "background: #FFA500"}
BG_CYAN = {:console => "\e[0;46m", :html => "background: #40FFFF"}
BG_WHITE = {:console => "\e[0;47m", :html => "background: #FFFFFF"}
BG_HRED = {:console => "\e[1;41m", :html => "background: #FFA0A0"}
BG_HGREEN = {:console => "\e[1;42m", :html => "background: #00CD00"}
BG_HYELLOW = {:console => "\e[1;43m", :html => "background: #FFFF60"}
BG_HBLUE = {:console => "\e[1;44m", :html => "background: #80A0FF"}
BG_HMAGENTA = {:console => "\e[1;45m", :html => "background: #FFA500"}
BG_HCYAN = {:console => "\e[1;46m", :html => "background: #40FFFF"}
BG_HWHITE = {:console => "\e[1;47m", :html => "background: #FFFFFF"}
RESET = {:console => "\e[0m", :html => "" }
Colormap = {
:debug => WHITE,
:info => GREEN,
:notice => CYAN,
:warning => YELLOW,
:err => HMAGENTA,
:alert => RED,
:emerg => HRED,
:crit => HRED,
:black => BLACK,
:red => RED,
:green => GREEN,
:yellow => YELLOW,
:blue => BLUE,
:magenta => MAGENTA,
:cyan => CYAN,
:white => WHITE,
:hblack => HBLACK,
:hred => HRED,
:hgreen => HGREEN,
:hyellow => HYELLOW,
:hblue => HBLUE,
:hmagenta => HMAGENTA,
:hcyan => HCYAN,
:hwhite => HWHITE,
:bg_red => BG_RED,
:bg_green => BG_GREEN,
:bg_yellow => BG_YELLOW,
:bg_blue => BG_BLUE,
:bg_magenta => BG_MAGENTA,
:bg_cyan => BG_CYAN,
:bg_white => BG_WHITE,
:bg_hred => BG_HRED,
:bg_hgreen => BG_HGREEN,
:bg_hyellow => BG_HYELLOW,
:bg_hblue => BG_HBLUE,
:bg_hmagenta => BG_HMAGENTA,
:bg_hcyan => BG_HCYAN,
:bg_hwhite => BG_HWHITE,
:reset => { :console => "\e[m", :html => "" }
}
# We define console_has_color? at load time since it's checking the
# underlying platform which will not change, and we don't want to perform
# the check every time we use logging
if Puppet::Util::Platform.windows?
# We're on windows, need win32console for color to work
begin
require 'Win32API'
require 'win32console'
- require 'windows/wide_string'
+ require 'puppet/util/windows/string'
# The win32console gem uses ANSI functions for writing to the console
# which doesn't work for unicode strings, e.g. module tool. Ruby 1.9
# does the same thing, but doesn't account for ANSI escape sequences
class WideConsole < Win32::Console
WriteConsole = Win32API.new( "kernel32", "WriteConsoleW", ['l', 'p', 'l', 'p', 'p'], 'l' )
WriteConsoleOutputCharacter = Win32API.new( "kernel32", "WriteConsoleOutputCharacterW", ['l', 'p', 'l', 'l', 'p'], 'l' )
def initialize(t = nil)
super(t)
end
def WriteChar(str, col, row)
dwWriteCoord = (row << 16) + col
lpNumberOfCharsWritten = ' ' * 4
utf16, nChars = string_encode(str)
WriteConsoleOutputCharacter.call(@handle, utf16, nChars, dwWriteCoord, lpNumberOfCharsWritten)
lpNumberOfCharsWritten.unpack('L')
end
def Write(str)
written = 0.chr * 4
reserved = 0.chr * 4
utf16, nChars = string_encode(str)
WriteConsole.call(@handle, utf16, nChars, written, reserved)
end
- if String.method_defined?("encode")
- def string_encode(str)
- wstr = str.encode('UTF-16LE')
- [wstr, wstr.length]
- end
- else
- require 'iconv'
- def string_encode(str)
- wstr = Iconv.conv('UTF-16LE', 'UTF-8', str)
- [wstr, wstr.length/2]
- end
+ def string_encode(str)
+ wstr = Puppet::Util::Windows::String.wide_string(str)
+ [wstr, wstr.length - 1]
end
end
# Override the win32console's IO class so we can supply
# our own Console class
class WideIO < Win32::Console::ANSI::IO
def initialize(fd_std = :stdout)
super(fd_std)
handle = FD_STD_MAP[fd_std][1]
@Out = WideConsole.new(handle)
end
end
$stdout = WideIO.new(:stdout)
$stderr = WideIO.new(:stderr)
rescue LoadError
def console_has_color?
false
end
else
def console_has_color?
true
end
end
else
# On a posix system we can just enable it
def console_has_color?
true
end
end
def colorize(color, str)
case Puppet[:color]
when true, :ansi, "ansi", "yes"
if console_has_color?
console_color(color, str)
else
str
end
when :html, "html"
html_color(color, str)
else
str
end
end
def console_color(color, str)
Colormap[color][:console] +
str.gsub(RESET[:console], Colormap[color][:console]) +
RESET[:console]
end
def html_color(color, str)
span = '<span style="%s">' % Colormap[color][:html]
"#{span}%s</span>" % str.gsub(/<span .*?<\/span>/, "</span>\\0#{span}")
end
end
diff --git a/lib/puppet/util/command_line.rb b/lib/puppet/util/command_line.rb
index a334ef8db..35a38f5c3 100644
--- a/lib/puppet/util/command_line.rb
+++ b/lib/puppet/util/command_line.rb
@@ -1,182 +1,182 @@
# Bundler and rubygems maintain a set of directories from which to
# load gems. If Bundler is loaded, let it determine what can be
# loaded. If it's not loaded, then use rubygems. But do this before
# loading any puppet code, so that our gem loading system is sane.
if not defined? ::Bundler
begin
require 'rubygems'
rescue LoadError
end
end
require 'puppet'
require 'puppet/util'
require "puppet/util/plugins"
require "puppet/util/rubygems"
require "puppet/util/limits"
module Puppet
module Util
# This is the main entry point for all puppet applications / faces; it
# 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
# @param argv [Array<String>] the arguments passed on the command line
# @param stdin [IO] (unused)
def initialize(zero = $0, argv = ARGV, stdin = STDIN)
@command = File.basename(zero, '.rb')
@argv = argv
Puppet::Plugins.on_commandline_initialization(:command_line_object => self)
end
# @return [String] name of the subcommand is being executed
# @api public
def subcommand_name
return @command if @command != 'puppet'
if @argv.first =~ OPTION_OR_MANIFEST_FILE
nil
else
@argv.first
end
end
# @return [Array<String>] the command line arguments being passed to the subcommand
# @api public
def args
return @argv if @command != 'puppet'
if subcommand_name.nil?
@argv
else
@argv[1..-1]
end
end
# @api private
# @deprecated
def self.available_subcommands
Puppet.deprecation_warning('Puppet::Util::CommandLine.available_subcommands is deprecated; please use Puppet::Application.available_application_names instead.')
Puppet::Application.available_application_names
end
# available_subcommands was previously an instance method, not a class
# method, and we have an unknown number of user-implemented applications
# that depend on that behaviour. Forwarding allows us to preserve a
# backward compatible API. --daniel 2011-04-11
# @api private
# @deprecated
def available_subcommands
Puppet.deprecation_warning('Puppet::Util::CommandLine#available_subcommands is deprecated; please use Puppet::Application.available_application_names instead.')
Puppet::Application.available_application_names
end
# Run the puppet subcommand. If the subcommand is determined to be an
# external executable, this method will never return and the current
# process will be replaced via {Kernel#exec}.
#
# @return [void]
def execute
- Puppet::Util.exit_on_fail("intialize global default settings") do
+ Puppet::Util.exit_on_fail("initialize global default settings") do
Puppet.initialize_settings(args)
end
setpriority(Puppet[:priority])
find_subcommand.run
end
# @api private
def external_subcommand
Puppet::Util.which("puppet-#{subcommand_name}")
end
private
def find_subcommand
if subcommand_name.nil?
NilSubcommand.new(self)
elsif Puppet::Application.available_application_names.include?(subcommand_name)
ApplicationSubcommand.new(subcommand_name, self)
elsif path_to_subcommand = external_subcommand
ExternalSubcommand.new(path_to_subcommand, self)
else
UnknownSubcommand.new(subcommand_name, self)
end
end
# @api private
class ApplicationSubcommand
def initialize(subcommand_name, command_line)
@subcommand_name = subcommand_name
@command_line = command_line
end
def run
# For most applications, we want to be able to load code from the modulepath,
# such as apply, describe, resource, and faces.
# For agent, we only want to load pluginsync'ed code from libdir.
# For master, we shouldn't ever be loading per-enviroment code into the master's
# ruby process, but that requires fixing (#17210, #12173, #8750). So for now
# we try to restrict to only code that can be autoloaded from the node's
# environment.
if @subcommand_name != 'master' and @subcommand_name != 'agent'
- Puppet::Node::Environment.new.each_plugin_directory do |dir|
+ Puppet.lookup(:environments).get(Puppet[:environment]).each_plugin_directory do |dir|
$LOAD_PATH << dir unless $LOAD_PATH.include?(dir)
end
end
app = Puppet::Application.find(@subcommand_name).new(@command_line)
Puppet::Plugins.on_application_initialization(:application_object => @command_line)
app.run
end
end
# @api private
class ExternalSubcommand
def initialize(path_to_subcommand, command_line)
@path_to_subcommand = path_to_subcommand
@command_line = command_line
end
def run
Kernel.exec(@path_to_subcommand, *@command_line.args)
end
end
# @api private
class NilSubcommand
def initialize(command_line)
@command_line = command_line
end
def run
if @command_line.args.include? "--version" or @command_line.args.include? "-V"
puts Puppet.version
else
puts "See 'puppet help' for help on available puppet subcommands"
end
end
end
# @api private
class UnknownSubcommand < NilSubcommand
def initialize(subcommand_name, command_line)
@subcommand_name = subcommand_name
super(command_line)
end
def run
puts "Error: Unknown Puppet subcommand '#{@subcommand_name}'"
super
end
end
end
end
end
diff --git a/lib/puppet/util/command_line/trollop.rb b/lib/puppet/util/command_line/trollop.rb
index ed1246c0e..25f21d90e 100644
--- a/lib/puppet/util/command_line/trollop.rb
+++ b/lib/puppet/util/command_line/trollop.rb
@@ -1,824 +1,824 @@
## lib/trollop.rb -- trollop command-line processing library
## Author:: William Morgan (mailto: wmorgan-trollop@masanjin.net)
## Copyright:: Copyright 2007 William Morgan
## License:: the same terms as ruby itself
##
## 2012-03: small changes made by cprice (chris@puppetlabs.com);
## patch submitted for upstream consideration:
## https://gitorious.org/trollop/mainline/merge_requests/9
## 2012-08: namespace changes made by Jeff McCune (jeff@puppetlabs.com)
## moved Trollop into Puppet::Util::CommandLine to prevent monkey
## patching the upstream trollop library if also loaded.
require 'date'
module Puppet
module Util
class CommandLine
module Trollop
VERSION = "1.16.2"
## Thrown by Parser in the event of a commandline error. Not needed if
## you're using the Trollop::options entry.
class CommandlineError < StandardError; end
## Thrown by Parser if the user passes in '-h' or '--help'. Handled
## automatically by Trollop#options.
class HelpNeeded < StandardError; end
## Thrown by Parser if the user passes in '-h' or '--version'. Handled
## automatically by Trollop#options.
class VersionNeeded < StandardError; end
## Regex for floating point numbers
FLOAT_RE = /^-?((\d+(\.\d+)?)|(\.\d+))([eE][-+]?[\d]+)?$/
## Regex for parameters
PARAM_RE = /^-(-|\.$|[^\d\.])/
## The commandline parser. In typical usage, the methods in this class
## will be handled internally by Trollop::options. In this case, only the
## #opt, #banner and #version, #depends, and #conflicts methods will
## typically be called.
##
## If you want to instantiate this class yourself (for more complicated
## argument-parsing logic), call #parse to actually produce the output hash,
## and consider calling it from within
## Trollop::with_standard_exception_handling.
class Parser
## The set of values that indicate a flag option when passed as the
## +:type+ parameter of #opt.
FLAG_TYPES = [:flag, :bool, :boolean]
## The set of values that indicate a single-parameter (normal) option when
## passed as the +:type+ parameter of #opt.
##
## A value of +io+ corresponds to a readable IO resource, including
## a filename, URI, or the strings 'stdin' or '-'.
SINGLE_ARG_TYPES = [:int, :integer, :string, :double, :float, :io, :date]
## The set of values that indicate a multiple-parameter option (i.e., that
## takes multiple space-separated values on the commandline) when passed as
## the +:type+ parameter of #opt.
MULTI_ARG_TYPES = [:ints, :integers, :strings, :doubles, :floats, :ios, :dates]
## The complete set of legal values for the +:type+ parameter of #opt.
TYPES = FLAG_TYPES + SINGLE_ARG_TYPES + MULTI_ARG_TYPES
INVALID_SHORT_ARG_REGEX = /[\d-]/ #:nodoc:
## The values from the commandline that were not interpreted by #parse.
attr_reader :leftovers
## The complete configuration hashes for each option. (Mainly useful
## for testing.)
attr_reader :specs
## A flag that determines whether or not to attempt to automatically generate "short" options if they are not
## explicitly specified.
attr_accessor :create_default_short_options
## A flag that determines whether or not to raise an error if the parser is passed one or more
## options that were not registered ahead of time. If 'true', then the parser will simply
## ignore options that it does not recognize.
attr_accessor :ignore_invalid_options
## A flag indicating whether or not the parser should attempt to handle "--help" and
## "--version" specially. If 'false', it will treat them just like any other option.
attr_accessor :handle_help_and_version
## Initializes the parser, and instance-evaluates any block given.
def initialize *a, &b
@version = nil
@leftovers = []
@specs = {}
@long = {}
@short = {}
@order = []
@constraints = []
@stop_words = []
@stop_on_unknown = false
#instance_eval(&b) if b # can't take arguments
cloaker(&b).bind(self).call(*a) if b
end
## Define an option. +name+ is the option name, a unique identifier
## for the option that you will use internally, which should be a
## symbol or a string. +desc+ is a string description which will be
## displayed in help messages.
##
## Takes the following optional arguments:
##
## [+:long+] Specify the long form of the argument, i.e. the form with two dashes. If unspecified, will be automatically derived based on the argument name by turning the +name+ option into a string, and replacing any _'s by -'s.
## [+:short+] Specify the short form of the argument, i.e. the form with one dash. If unspecified, will be automatically derived from +name+.
## [+:type+] Require that the argument take a parameter or parameters of type +type+. For a single parameter, the value can be a member of +SINGLE_ARG_TYPES+, or a corresponding Ruby class (e.g. +Integer+ for +:int+). For multiple-argument parameters, the value can be any member of +MULTI_ARG_TYPES+ constant. If unset, the default argument type is +:flag+, meaning that the argument does not take a parameter. The specification of +:type+ is not necessary if a +:default+ is given.
- ## [+:default+] Set the default value for an argument. Without a default value, the hash returned by #parse (and thus Trollop::options) will have a +nil+ value for this key unless the argument is given on the commandline. The argument type is derived automatically from the class of the default value given, so specifying a +:type+ is not necessary if a +:default+ is given. (But see below for an important caveat when +:multi+: is specified too.) If the argument is a flag, and the default is set to +true+, then if it is specified on the the commandline the value will be +false+.
+ ## [+:default+] Set the default value for an argument. Without a default value, the hash returned by #parse (and thus Trollop::options) will have a +nil+ value for this key unless the argument is given on the commandline. The argument type is derived automatically from the class of the default value given, so specifying a +:type+ is not necessary if a +:default+ is given. (But see below for an important caveat when +:multi+: is specified too.) If the argument is a flag, and the default is set to +true+, then if it is specified on the commandline the value will be +false+.
## [+:required+] If set to +true+, the argument must be provided on the commandline.
## [+:multi+] If set to +true+, allows multiple occurrences of the option on the commandline. Otherwise, only a single instance of the option is allowed. (Note that this is different from taking multiple parameters. See below.)
##
## Note that there are two types of argument multiplicity: an argument
## can take multiple values, e.g. "--arg 1 2 3". An argument can also
## be allowed to occur multiple times, e.g. "--arg 1 --arg 2".
##
## Arguments that take multiple values should have a +:type+ parameter
## drawn from +MULTI_ARG_TYPES+ (e.g. +:strings+), or a +:default:+
## value of an array of the correct type (e.g. [String]). The
## value of this argument will be an array of the parameters on the
## commandline.
##
## Arguments that can occur multiple times should be marked with
## +:multi+ => +true+. The value of this argument will also be an array.
## In contrast with regular non-multi options, if not specified on
## the commandline, the default value will be [], not nil.
##
## These two attributes can be combined (e.g. +:type+ => +:strings+,
## +:multi+ => +true+), in which case the value of the argument will be
## an array of arrays.
##
## There's one ambiguous case to be aware of: when +:multi+: is true and a
## +:default+ is set to an array (of something), it's ambiguous whether this
## is a multi-value argument as well as a multi-occurrence argument.
## In thise case, Trollop assumes that it's not a multi-value argument.
## If you want a multi-value, multi-occurrence argument with a default
## value, you must specify +:type+ as well.
def opt name, desc="", opts={}
raise ArgumentError, "you already have an argument named '#{name}'" if @specs.member? name
## fill in :type
opts[:type] = # normalize
case opts[:type]
when :boolean, :bool; :flag
when :integer; :int
when :integers; :ints
when :double; :float
when :doubles; :floats
when Class
case opts[:type].name
when 'TrueClass', 'FalseClass'; :flag
when 'String'; :string
when 'Integer'; :int
when 'Float'; :float
when 'IO'; :io
when 'Date'; :date
else
raise ArgumentError, "unsupported argument type '#{opts[:type].class.name}'"
end
when nil; nil
else
raise ArgumentError, "unsupported argument type '#{opts[:type]}'" unless TYPES.include?(opts[:type])
opts[:type]
end
## for options with :multi => true, an array default doesn't imply
## a multi-valued argument. for that you have to specify a :type
## as well. (this is how we disambiguate an ambiguous situation;
## see the docs for Parser#opt for details.)
disambiguated_default =
if opts[:multi] && opts[:default].is_a?(Array) && !opts[:type]
opts[:default].first
else
opts[:default]
end
type_from_default =
case disambiguated_default
when Integer; :int
when Numeric; :float
when TrueClass, FalseClass; :flag
when String; :string
when IO; :io
when Date; :date
when Array
if opts[:default].empty?
raise ArgumentError, "multiple argument type cannot be deduced from an empty array for '#{opts[:default][0].class.name}'"
end
case opts[:default][0] # the first element determines the types
when Integer; :ints
when Numeric; :floats
when String; :strings
when IO; :ios
when Date; :dates
else
raise ArgumentError, "unsupported multiple argument type '#{opts[:default][0].class.name}'"
end
when nil; nil
else
raise ArgumentError, "unsupported argument type '#{opts[:default].class.name}'"
end
raise ArgumentError, ":type specification and default type don't match (default type is #{type_from_default})" if opts[:type] && type_from_default && opts[:type] != type_from_default
opts[:type] = opts[:type] || type_from_default || :flag
## fill in :long
opts[:long] = opts[:long] ? opts[:long].to_s : name.to_s.gsub("_", "-")
opts[:long] =
case opts[:long]
when /^--([^-].*)$/
$1
when /^[^-]/
opts[:long]
else
raise ArgumentError, "invalid long option name #{opts[:long].inspect}"
end
raise ArgumentError, "long option name #{opts[:long].inspect} is already taken; please specify a (different) :long" if @long[opts[:long]]
## fill in :short
opts[:short] = opts[:short].to_s if opts[:short] unless opts[:short] == :none
opts[:short] = case opts[:short]
when /^-(.)$/; $1
when nil, :none, /^.$/; opts[:short]
else raise ArgumentError, "invalid short option name '#{opts[:short].inspect}'"
end
if opts[:short]
raise ArgumentError, "short option name #{opts[:short].inspect} is already taken; please specify a (different) :short" if @short[opts[:short]]
raise ArgumentError, "a short option name can't be a number or a dash" if opts[:short] =~ INVALID_SHORT_ARG_REGEX
end
## fill in :default for flags
opts[:default] = false if opts[:type] == :flag && opts[:default].nil?
## autobox :default for :multi (multi-occurrence) arguments
opts[:default] = [opts[:default]] if opts[:default] && opts[:multi] && !opts[:default].is_a?(Array)
## fill in :multi
opts[:multi] ||= false
opts[:desc] ||= desc
@long[opts[:long]] = name
@short[opts[:short]] = name if opts[:short] && opts[:short] != :none
@specs[name] = opts
@order << [:opt, name]
end
## Sets the version string. If set, the user can request the version
## on the commandline. Should probably be of the form "<program name>
## <version number>".
def version s=nil; @version = s if s; @version end
## Adds text to the help display. Can be interspersed with calls to
## #opt to build a multi-section help page.
def banner s; @order << [:text, s] end
alias :text :banner
## Marks two (or more!) options as requiring each other. Only handles
## undirected (i.e., mutual) dependencies. Directed dependencies are
## better modeled with Trollop::die.
def depends *syms
syms.each { |sym| raise ArgumentError, "unknown option '#{sym}'" unless @specs[sym] }
@constraints << [:depends, syms]
end
## Marks two (or more!) options as conflicting.
def conflicts *syms
syms.each { |sym| raise ArgumentError, "unknown option '#{sym}'" unless @specs[sym] }
@constraints << [:conflicts, syms]
end
## Defines a set of words which cause parsing to terminate when
## encountered, such that any options to the left of the word are
## parsed as usual, and options to the right of the word are left
## intact.
##
## A typical use case would be for subcommand support, where these
## would be set to the list of subcommands. A subsequent Trollop
## invocation would then be used to parse subcommand options, after
## shifting the subcommand off of ARGV.
def stop_on *words
@stop_words = [*words].flatten
end
## Similar to #stop_on, but stops on any unknown word when encountered
## (unless it is a parameter for an argument). This is useful for
## cases where you don't know the set of subcommands ahead of time,
## i.e., without first parsing the global options.
def stop_on_unknown
@stop_on_unknown = true
end
## Parses the commandline. Typically called by Trollop::options,
## but you can call it directly if you need more control.
##
## throws CommandlineError, HelpNeeded, and VersionNeeded exceptions.
def parse cmdline=ARGV
vals = {}
required = {}
if handle_help_and_version
opt :version, "Print version and exit" if @version unless @specs[:version] || @long["version"]
opt :help, "Show this message" unless @specs[:help] || @long["help"]
end
@specs.each do |sym, opts|
required[sym] = true if opts[:required]
vals[sym] = opts[:default]
vals[sym] = [] if opts[:multi] && !opts[:default] # multi arguments default to [], not nil
end
resolve_default_short_options if create_default_short_options
## resolve symbols
given_args = {}
@leftovers = each_arg cmdline do |arg, params|
sym = case arg
when /^-([^-])$/
@short[$1]
when /^--no-([^-]\S*)$/
@long["[no-]#{$1}"]
when /^--([^-]\S*)$/
@long[$1] ? @long[$1] : @long["[no-]#{$1}"]
else
raise CommandlineError, "invalid argument syntax: '#{arg}'"
end
unless sym
next 0 if ignore_invalid_options
raise CommandlineError, "unknown argument '#{arg}'" unless sym
end
if given_args.include?(sym) && !@specs[sym][:multi]
raise CommandlineError, "option '#{arg}' specified multiple times"
end
given_args[sym] ||= {}
given_args[sym][:arg] = arg
given_args[sym][:params] ||= []
# The block returns the number of parameters taken.
num_params_taken = 0
unless params.nil?
if SINGLE_ARG_TYPES.include?(@specs[sym][:type])
given_args[sym][:params] << params[0, 1] # take the first parameter
num_params_taken = 1
elsif MULTI_ARG_TYPES.include?(@specs[sym][:type])
given_args[sym][:params] << params # take all the parameters
num_params_taken = params.size
end
end
num_params_taken
end
if handle_help_and_version
## check for version and help args
raise VersionNeeded if given_args.include? :version
raise HelpNeeded if given_args.include? :help
end
## check constraint satisfaction
@constraints.each do |type, syms|
constraint_sym = syms.find { |sym| given_args[sym] }
next unless constraint_sym
case type
when :depends
syms.each { |sym| raise CommandlineError, "--#{@specs[constraint_sym][:long]} requires --#{@specs[sym][:long]}" unless given_args.include? sym }
when :conflicts
syms.each { |sym| raise CommandlineError, "--#{@specs[constraint_sym][:long]} conflicts with --#{@specs[sym][:long]}" if given_args.include?(sym) && (sym != constraint_sym) }
end
end
required.each do |sym, val|
raise CommandlineError, "option --#{@specs[sym][:long]} must be specified" unless given_args.include? sym
end
## parse parameters
given_args.each do |sym, given_data|
arg = given_data[:arg]
params = given_data[:params]
opts = @specs[sym]
raise CommandlineError, "option '#{arg}' needs a parameter" if params.empty? && opts[:type] != :flag
vals["#{sym}_given".intern] = true # mark argument as specified on the commandline
case opts[:type]
when :flag
if arg =~ /^--no-/ and sym.to_s =~ /^--\[no-\]/
vals[sym] = opts[:default]
else
vals[sym] = !opts[:default]
end
when :int, :ints
vals[sym] = params.map { |pg| pg.map { |p| parse_integer_parameter p, arg } }
when :float, :floats
vals[sym] = params.map { |pg| pg.map { |p| parse_float_parameter p, arg } }
when :string, :strings
vals[sym] = params.map { |pg| pg.map { |p| p.to_s } }
when :io, :ios
vals[sym] = params.map { |pg| pg.map { |p| parse_io_parameter p, arg } }
when :date, :dates
vals[sym] = params.map { |pg| pg.map { |p| parse_date_parameter p, arg } }
end
if SINGLE_ARG_TYPES.include?(opts[:type])
unless opts[:multi] # single parameter
vals[sym] = vals[sym][0][0]
else # multiple options, each with a single parameter
vals[sym] = vals[sym].map { |p| p[0] }
end
elsif MULTI_ARG_TYPES.include?(opts[:type]) && !opts[:multi]
vals[sym] = vals[sym][0] # single option, with multiple parameters
end
# else: multiple options, with multiple parameters
opts[:callback].call(vals[sym]) if opts.has_key?(:callback)
end
## modify input in place with only those
## arguments we didn't process
cmdline.clear
@leftovers.each { |l| cmdline << l }
## allow openstruct-style accessors
class << vals
def method_missing(m, *args)
self[m] || self[m.to_s]
end
end
vals
end
def parse_date_parameter param, arg #:nodoc:
begin
begin
time = Chronic.parse(param)
rescue NameError
# chronic is not available
end
time ? Date.new(time.year, time.month, time.day) : Date.parse(param)
rescue ArgumentError
- raise CommandlineError, "option '#{arg}' needs a date"
+ raise CommandlineError, "option '#{arg}' needs a date", $!.backtrace
end
end
## Print the help message to +stream+.
def educate stream=$stdout
width # just calculate it now; otherwise we have to be careful not to
# call this unless the cursor's at the beginning of a line.
left = {}
@specs.each do |name, spec|
left[name] = "--#{spec[:long]}" +
(spec[:short] && spec[:short] != :none ? ", -#{spec[:short]}" : "") +
case spec[:type]
when :flag; ""
when :int; " <i>"
when :ints; " <i+>"
when :string; " <s>"
when :strings; " <s+>"
when :float; " <f>"
when :floats; " <f+>"
when :io; " <filename/uri>"
when :ios; " <filename/uri+>"
when :date; " <date>"
when :dates; " <date+>"
end
end
leftcol_width = left.values.map { |s| s.length }.max || 0
rightcol_start = leftcol_width + 6 # spaces
unless @order.size > 0 && @order.first.first == :text
stream.puts "#@version\n" if @version
stream.puts "Options:"
end
@order.each do |what, opt|
if what == :text
stream.puts wrap(opt)
next
end
spec = @specs[opt]
stream.printf " %#{leftcol_width}s: ", left[opt]
desc = spec[:desc] + begin
default_s = case spec[:default]
when $stdout; "<stdout>"
when $stdin; "<stdin>"
when $stderr; "<stderr>"
when Array
spec[:default].join(", ")
else
spec[:default].to_s
end
if spec[:default]
if spec[:desc] =~ /\.$/
" (Default: #{default_s})"
else
" (default: #{default_s})"
end
else
""
end
end
stream.puts wrap(desc, :width => width - rightcol_start - 1, :prefix => rightcol_start)
end
end
def width #:nodoc:
@width ||= if $stdout.tty?
begin
require 'curses'
Curses::init_screen
x = Curses::cols
Curses::close_screen
x
rescue Exception
80
end
else
80
end
end
def wrap str, opts={} # :nodoc:
if str == ""
[""]
else
str.split("\n").map { |s| wrap_line s, opts }.flatten
end
end
## The per-parser version of Trollop::die (see that for documentation).
def die arg, msg
if msg
$stderr.puts "Error: argument --#{@specs[arg][:long]} #{msg}."
else
$stderr.puts "Error: #{arg}."
end
$stderr.puts "Try --help for help."
exit(-1)
end
private
## yield successive arg, parameter pairs
def each_arg args
remains = []
i = 0
until i >= args.length
if @stop_words.member? args[i]
remains += args[i .. -1]
return remains
end
case args[i]
when /^--$/ # arg terminator
remains += args[(i + 1) .. -1]
return remains
when /^--(\S+?)=(.*)$/ # long argument with equals
yield "--#{$1}", [$2]
i += 1
when /^--(\S+)$/ # long argument
params = collect_argument_parameters(args, i + 1)
unless params.empty?
num_params_taken = yield args[i], params
unless num_params_taken
if @stop_on_unknown
remains += args[i + 1 .. -1]
return remains
else
remains += params
end
end
i += 1 + num_params_taken
else # long argument no parameter
yield args[i], nil
i += 1
end
when /^-(\S+)$/ # one or more short arguments
shortargs = $1.split(//)
shortargs.each_with_index do |a, j|
if j == (shortargs.length - 1)
params = collect_argument_parameters(args, i + 1)
unless params.empty?
num_params_taken = yield "-#{a}", params
unless num_params_taken
if @stop_on_unknown
remains += args[i + 1 .. -1]
return remains
else
remains += params
end
end
i += 1 + num_params_taken
else # argument no parameter
yield "-#{a}", nil
i += 1
end
else
yield "-#{a}", nil
end
end
else
if @stop_on_unknown
remains += args[i .. -1]
return remains
else
remains << args[i]
i += 1
end
end
end
remains
end
def parse_integer_parameter param, arg
raise CommandlineError, "option '#{arg}' needs an integer" unless param =~ /^\d+$/
param.to_i
end
def parse_float_parameter param, arg
raise CommandlineError, "option '#{arg}' needs a floating-point number" unless param =~ FLOAT_RE
param.to_f
end
def parse_io_parameter param, arg
case param
when /^(stdin|-)$/i; $stdin
else
require 'open-uri'
begin
open param
rescue SystemCallError => e
- raise CommandlineError, "file or url for option '#{arg}' cannot be opened: #{e.message}"
+ raise CommandlineError, "file or url for option '#{arg}' cannot be opened: #{e.message}", e.backtrace
end
end
end
def collect_argument_parameters args, start_at
params = []
pos = start_at
while args[pos] && args[pos] !~ PARAM_RE && !@stop_words.member?(args[pos]) do
params << args[pos]
pos += 1
end
params
end
def resolve_default_short_options
@order.each do |type, name|
next unless type == :opt
opts = @specs[name]
next if opts[:short]
c = opts[:long].split(//).find { |d| d !~ INVALID_SHORT_ARG_REGEX && !@short.member?(d) }
if c # found a character to use
opts[:short] = c
@short[c] = name
end
end
end
def wrap_line str, opts={}
prefix = opts[:prefix] || 0
width = opts[:width] || (self.width - 1)
start = 0
ret = []
until start > str.length
nextt =
if start + width >= str.length
str.length
else
x = str.rindex(/\s/, start + width)
x = str.index(/\s/, start) if x && x < start
x || str.length
end
ret << (ret.empty? ? "" : " " * prefix) + str[start ... nextt]
start = nextt + 1
end
ret
end
## instance_eval but with ability to handle block arguments
## thanks to why: http://redhanded.hobix.com/inspect/aBlockCostume.html
def cloaker &b
(class << self; self; end).class_eval do
define_method :cloaker_, &b
meth = instance_method :cloaker_
remove_method :cloaker_
meth
end
end
end
## The easy, syntactic-sugary entry method into Trollop. Creates a Parser,
## passes the block to it, then parses +args+ with it, handling any errors or
## requests for help or version information appropriately (and then exiting).
## Modifies +args+ in place. Returns a hash of option values.
##
## The block passed in should contain zero or more calls to +opt+
## (Parser#opt), zero or more calls to +text+ (Parser#text), and
## probably a call to +version+ (Parser#version).
##
## The returned block contains a value for every option specified with
## +opt+. The value will be the value given on the commandline, or the
## default value if the option was not specified on the commandline. For
## every option specified on the commandline, a key "<option
## name>_given" will also be set in the hash.
##
## Example:
##
## require 'trollop'
## opts = Trollop::options do
## opt :monkey, "Use monkey mode" # a flag --monkey, defaulting to false
## opt :goat, "Use goat mode", :default => true # a flag --goat, defaulting to true
## opt :num_limbs, "Number of limbs", :default => 4 # an integer --num-limbs <i>, defaulting to 4
## opt :num_thumbs, "Number of thumbs", :type => :int # an integer --num-thumbs <i>, defaulting to nil
## end
##
## ## if called with no arguments
## p opts # => { :monkey => false, :goat => true, :num_limbs => 4, :num_thumbs => nil }
##
## ## if called with --monkey
## p opts # => {:monkey_given=>true, :monkey=>true, :goat=>true, :num_limbs=>4, :help=>false, :num_thumbs=>nil}
##
## See more examples at http://trollop.rubyforge.org.
def options args=ARGV, *a, &b
@last_parser = Parser.new(*a, &b)
with_standard_exception_handling(@last_parser) { @last_parser.parse args }
end
## If Trollop::options doesn't do quite what you want, you can create a Parser
## object and call Parser#parse on it. That method will throw CommandlineError,
## HelpNeeded and VersionNeeded exceptions when necessary; if you want to
## have these handled for you in the standard manner (e.g. show the help
## and then exit upon an HelpNeeded exception), call your code from within
## a block passed to this method.
##
## Note that this method will call System#exit after handling an exception!
##
## Usage example:
##
## require 'trollop'
## p = Trollop::Parser.new do
## opt :monkey, "Use monkey mode" # a flag --monkey, defaulting to false
## opt :goat, "Use goat mode", :default => true # a flag --goat, defaulting to true
## end
##
## opts = Trollop::with_standard_exception_handling p do
## o = p.parse ARGV
## raise Trollop::HelpNeeded if ARGV.empty? # show help screen
## o
## end
##
## Requires passing in the parser object.
def with_standard_exception_handling parser
begin
yield
rescue CommandlineError => e
$stderr.puts "Error: #{e.message}."
$stderr.puts "Try --help for help."
exit(-1)
rescue HelpNeeded
parser.educate
exit
rescue VersionNeeded
puts parser.version
exit
end
end
## Informs the user that their usage of 'arg' was wrong, as detailed by
## 'msg', and dies. Example:
##
## options do
## opt :volume, :default => 0.0
## end
##
## die :volume, "too loud" if opts[:volume] > 10.0
## die :volume, "too soft" if opts[:volume] < 0.1
##
## In the one-argument case, simply print that message, a notice
## about -h, and die. Example:
##
## options do
## opt :whatever # ...
## end
##
## Trollop::die "need at least one filename" if ARGV.empty?
def die arg, msg=nil
if @last_parser
@last_parser.die arg, msg
else
raise ArgumentError, "Trollop::die can only be called after Trollop::options"
end
end
module_function :options, :die, :with_standard_exception_handling
end # module
end
end
end
diff --git a/lib/puppet/util/docs.rb b/lib/puppet/util/docs.rb
index 0c854b76c..3f0a2fba0 100644
--- a/lib/puppet/util/docs.rb
+++ b/lib/puppet/util/docs.rb
@@ -1,128 +1,128 @@
# Some simple methods for helping manage automatic documentation generation.
module Puppet::Util::Docs
# Specify the actual doc string.
def desc(str)
@doc = str
end
# Add a new autodoc block. We have to define these as class methods,
# rather than just sticking them in a hash, because otherwise they're
# too difficult to do inheritance with.
def dochook(name, &block)
method = "dochook_#{name}"
meta_def method, &block
end
attr_writer :doc
# Generate the full doc string.
def doc
extra = methods.find_all { |m| m.to_s =~ /^dochook_.+/ }.sort.collect { |m|
self.send(m)
}.delete_if {|r| r.nil? }.collect {|r| "* #{r}"}.join("\n")
if @doc
scrub(@doc) + (extra.empty? ? '' : "\n\n#{extra}")
else
extra
end
end
# Build a table
def doctable(headers, data)
str = "\n\n"
lengths = []
# Figure out the longest field for all columns
data.each do |name, values|
[name, values].flatten.each_with_index do |value, i|
lengths[i] ||= 0
lengths[i] = value.to_s.length if value.to_s.length > lengths[i]
end
end
# The headers could also be longest
headers.each_with_index do |value, i|
lengths[i] = value.to_s.length if value.to_s.length > lengths[i]
end
# Add the header names
str += headers.zip(lengths).collect { |value, num| pad(value, num) }.join(" | ") + " |" + "\n"
# And the header row
str += lengths.collect { |num| "-" * num }.join(" | ") + " |" + "\n"
# Now each data row
data.sort { |a, b| a[0].to_s <=> b[0].to_s }.each do |name, rows|
str += [name, rows].flatten.zip(lengths).collect do |value, length|
pad(value, length)
end.join(" | ") + " |" + "\n"
end
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
end
# Pad a field with spaces
def pad(value, length)
value.to_s + (" " * (length - value.to_s.length))
end
HEADER_LEVELS = [nil, "#", "##", "###", "####", "#####"]
def markdown_header(name, level)
"#{HEADER_LEVELS[level]} #{name}\n\n"
end
def markdown_definitionlist(term, definition)
lines = scrub(definition).split("\n")
str = "#{term}\n: #{lines.shift}\n"
lines.each do |line|
str << " " if line =~ /\S/
str << "#{line}\n"
end
str << "\n"
end
# 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)
- # One-liners are easy!
- return text.strip if text !~ /\n/
+ # One-liners are easy! (One-liners may be buffered with extra newlines.)
+ return text.strip if text.strip !~ /\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
end
diff --git a/lib/puppet/util/errors.rb b/lib/puppet/util/errors.rb
index a03e3a8bb..d10336dc7 100644
--- a/lib/puppet/util/errors.rb
+++ b/lib/puppet/util/errors.rb
@@ -1,99 +1,106 @@
# Some helper methods for throwing and populating errors.
#
# @api public
module Puppet::Util::Errors
# Throw a Puppet::DevError with the specified message. Used for unknown or
# internal application failures.
#
# @param msg [String] message used in raised error
# @raise [Puppet::DevError] always raised with the supplied message
def devfail(msg)
self.fail(Puppet::DevError, msg)
end
# Add line and file info to the supplied exception if info is available from
# this object, is appropriately populated and the supplied exception supports
# it. When other is supplied, the backtrace will be copied to the error
# object.
#
# @param error [Exception] exception that is populated with info
# @param other [Exception] original exception, source of backtrace info
# @return [Exception] error parameter
def adderrorcontext(error, other = nil)
error.line ||= self.line if error.respond_to?(:line=) and self.respond_to?(:line) and self.line
error.file ||= self.file if error.respond_to?(:file=) and self.respond_to?(:file) and self.file
error.original ||= other if error.respond_to?(:original=)
error.set_backtrace(other.backtrace) if other and other.respond_to?(:backtrace)
error
end
# Return a human-readable string of this object's file and line attributes,
# if set.
#
# @return [String] description of file and line
def error_context
if file and line
" at #{file}:#{line}"
elsif line
" at line #{line}"
elsif file
" in #{file}"
else
""
end
end
# Wrap a call in such a way that we always throw the right exception and keep
# as much context as possible.
#
# @param options [Hash<Symbol,Object>] options used to create error
# @option options [Class] :type error type to raise, defaults to
# Puppet::DevError
# @option options [String] :message message to use in error, default mentions
# the name of this class
# @raise [Puppet::Error] re-raised with extra context if the block raises it
# @raise [Error] of type options[:type], when the block raises other
# exceptions
def exceptwrap(options = {})
options[:type] ||= Puppet::DevError
begin
return yield
rescue Puppet::Error => detail
raise adderrorcontext(detail)
rescue => detail
message = options[:message] || "#{self.class} failed with error #{detail.class}: #{detail}"
error = options[:type].new(message)
# We can't use self.fail here because it always expects strings,
# not exceptions.
raise adderrorcontext(error, detail)
end
retval
end
# Throw an error, defaulting to a Puppet::Error.
#
# @overload fail(message, ..)
# Throw a Puppet::Error with a message concatenated from the given
# arguments.
# @param [String] message error message(s)
# @overload fail(error_klass, message, ..)
# Throw an exception of type error_klass with a message concatenated from
# the given arguments.
# @param [Class] type of error
# @param [String] message error message(s)
+ # @overload fail(error_klass, message, ..)
+ # Throw an exception of type error_klass with a message concatenated from
+ # the given arguments.
+ # @param [Class] type of error
+ # @param [String] message error message(s)
+ # @param [Exception] original exception, source of backtrace info
def fail(*args)
if args[0].is_a?(Class)
type = args.shift
else
type = Puppet::Error
end
- error = adderrorcontext(type.new(args.join(" ")))
+ other = args.count > 1 ? args.pop : nil
+ error = adderrorcontext(type.new(args.join(" ")), other)
raise error
end
end
diff --git a/lib/puppet/util/execution.rb b/lib/puppet/util/execution.rb
index e931816f6..3c792cf40 100644
--- a/lib/puppet/util/execution.rb
+++ b/lib/puppet/util/execution.rb
@@ -1,306 +1,313 @@
module Puppet
require 'rbconfig'
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 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.
# This allows data to be fed to the subprocess.
#
# The command can be a simple string, which is executed as-is, or an Array,
# which is treated as a set of command arguments to pass through.
#
# In all cases this is passed directly to the shell, and STDOUT and STDERR
# are connected together during execution.
# @param command [String, Array<String>] the command to execute as one string, or as parts in an array.
# the parts of the array are joined with one separating space between each entry when converting to
# the command line string to execute.
# @param failonfail [Boolean] (true) if the execution should fail with Exception on failure or not.
# @yield [pipe] to a block executing a subprocess
# @yieldparam pipe [IO] the opened pipe
# @yieldreturn [String] the output to return
# @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)
# 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.
#
# Having two spaces is really not a big drama, since this passes to the
# 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
+ # force the run of the command with
+ # the user/system locale to "C" (via environment variables LANG and LC_*)
+ # it enables to have non localized output for some commands and therefore
+ # a predictable output
+ english_env = ENV.to_hash.merge( {'LANG' => 'C', 'LC_ALL' => 'C'} )
+ output = Puppet::Util.withenv(english_env) do
+ open("| #{command_str} 2>&1") do |pipe|
+ yield pipe
+ end
end
if failonfail
unless $CHILD_STATUS == 0
raise Puppet::ExecutionFailure, output
end
end
output
end
# 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 Puppet::ExecutionFailure
- raise exception, output
+ raise exception, output, exception.backtrace
end
# Default empty options for {execute}
NoOptionsSpecified = {}
# Executes the desired command, and return the status and output.
# def execute(command, options)
# @param command [Array<String>, String] the command to execute. If it is
# an Array the first element should be the executable and the rest of the
# elements should be the individual arguments to that executable.
# @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 [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.
# @option options [Boolean] :squelch (true) if true, ignore stdout / stderr completely.
# @option options [Boolean] :override_locale (true) by default (and if this option is set to true), we will temporarily override
# the user/system locale to "C" (via environment variables LANG and LC_*) while we are executing the command.
# This ensures that the output of the command will be formatted consistently, making it predictable for parsing.
# 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 [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
# set of overrides without affecting the default values for options that they don't pass in
default_options = {
:failonfail => NoOptionsSpecified.equal?(options),
:uid => nil,
:gid => nil,
:combine => NoOptionsSpecified.equal?(options),
:stdinfile => nil,
:squelch => false,
:override_locale => true,
:custom_environment => {},
}
options = default_options.merge(options)
if command.is_a?(Array)
command = command.flatten.map(&:to_s)
str = command.join(" ")
elsif command.is_a?(String)
str = command
end
if respond_to? :debug
debug "Executing '#{str}'"
else
Puppet.debug "Executing '#{str}'"
end
null_file = Puppet.features.microsoft_windows? ? 'NUL' : '/dev/null'
stdin = File.open(options[:stdinfile] || null_file, 'r')
stdout = options[:squelch] ? File.open(null_file, 'w') : Tempfile.new('puppet')
stderr = options[:combine] ? stdout : File.open(null_file, 'w')
exec_args = [command, options, stdin, stdout, stderr]
if execution_stub = Puppet::Util::ExecutionStub.current_value
return execution_stub.call(*exec_args)
elsif Puppet.features.posix?
child_pid = execute_posix(*exec_args)
exit_status = Process.waitpid2(child_pid).last.exitstatus
elsif Puppet.features.microsoft_windows?
process_info = execute_windows(*exec_args)
begin
exit_status = Puppet::Util::Windows::Process.wait_process(process_info.process_handle)
ensure
Puppet::Util::Windows::Process.CloseHandle(process_info.process_handle)
Puppet::Util::Windows::Process.CloseHandle(process_info.thread_handle)
end
end
[stdin, stdout, stderr].each {|io| io.close rescue nil}
# read output in if required
unless options[:squelch]
output = wait_for_output(stdout)
Puppet.warning "Could not get output" unless output
end
if options[:failonfail] and exit_status != 0
- raise Puppet::ExecutionFailure, "Execution of '#{str}' returned #{exit_status}: #{output}"
+ raise Puppet::ExecutionFailure, "Execution of '#{str}' returned #{exit_status}: #{output.strip}"
end
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'],
RbConfig::CONFIG['ruby_install_name'] + RbConfig::CONFIG['EXEEXT']).
sub(/.*\s.*/m, '"\&"')
end
# Because some modules provide their own version of this method.
class << self
alias util_execute execute
end
# This is private method.
# @comment see call to private_class_method after method definition
# @api private
#
def self.execute_posix(command, options, stdin, stdout, stderr)
child_pid = Puppet::Util.safe_posix_fork(stdin, stdout, stderr) do
# We can't just call Array(command), and rely on it returning
# things like ['foo'], when passed ['foo'], because
# Array(command) will call command.to_a internally, which when
# given a string can end up doing Very Bad Things(TM), such as
# turning "/tmp/foo;\r\n /bin/echo" into ["/tmp/foo;\r\n", " /bin/echo"]
command = [command].flatten
Process.setsid
begin
Puppet::Util::SUIDManager.change_privileges(options[:uid], options[:gid], true)
# if the caller has requested that we override locale environment variables,
if (options[:override_locale]) then
# loop over them and clear them
Puppet::Util::POSIX::LOCALE_ENV_VARS.each { |name| ENV.delete(name) }
# set LANG and LC_ALL to 'C' so that the command will have consistent, predictable output
# it's OK to manipulate these directly rather than, e.g., via "withenv", because we are in
# a forked process.
ENV['LANG'] = 'C'
ENV['LC_ALL'] = 'C'
end
# unset all of the user-related environment variables so that different methods of starting puppet
# (automatic start during boot, via 'service', via /etc/init.d, etc.) won't have unexpected side
# effects relating to user / home dir environment vars.
# it's OK to manipulate these directly rather than, e.g., via "withenv", because we are in
# a forked process.
Puppet::Util::POSIX::USER_ENV_VARS.each { |name| ENV.delete(name) }
options[:custom_environment] ||= {}
Puppet::Util.withenv(options[:custom_environment]) do
Kernel.exec(*command)
end
rescue => detail
Puppet.log_exception(detail, "Could not execute posix command: #{detail}")
exit!(1)
end
end
child_pid
end
private_class_method :execute_posix
# This is private method.
# @comment see call to private_class_method after method definition
# @api private
#
def self.execute_windows(command, options, stdin, stdout, stderr)
command = command.map do |part|
part.include?(' ') ? %Q["#{part.gsub(/"/, '\"')}"] : part
end.join(" ") if command.is_a?(Array)
options[:custom_environment] ||= {}
Puppet::Util.withenv(options[:custom_environment]) do
Puppet::Util::Windows::Process.execute(command, options, stdin, stdout, stderr)
end
end
private_class_method :execute_windows
# This is private method.
# @comment see call to private_class_method after method definition
# @api private
#
def self.wait_for_output(stdout)
# Make sure the file's actually been written. This is basically a race
# condition, and is probably a horrible way to handle it, but, well, oh
# well.
# (If this method were treated as private / inaccessible from outside of this file, we shouldn't have to worry
# 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 Puppet::FileSystem::File.exist?(stdout.path)
+ if Puppet::FileSystem.exist?(stdout.path)
stdout.open
begin
return stdout.read
ensure
stdout.close
stdout.unlink
end
else
time_to_sleep = try / 2.0
Puppet.warning "Waiting for output; will sleep #{time_to_sleep} seconds"
sleep(time_to_sleep)
end
end
nil
end
private_class_method :wait_for_output
end
diff --git a/lib/puppet/util/filetype.rb b/lib/puppet/util/filetype.rb
index 7f43f8c10..9fc3b289a 100644
--- a/lib/puppet/util/filetype.rb
+++ b/lib/puppet/util/filetype.rb
@@ -1,299 +1,299 @@
# Basic classes for reading, writing, and emptying files. Not much
# to see here.
require 'puppet/util/selinux'
require 'tempfile'
require 'fileutils'
class Puppet::Util::FileType
attr_accessor :loaded, :path, :synced
class FileReadError < Puppet::Error; end
include Puppet::Util::SELinux
class << self
attr_accessor :name
include Puppet::Util::ClassGen
end
# Create a new filetype.
def self.newfiletype(name, &block)
@filetypes ||= {}
klass = genclass(
name,
:block => block,
:prefix => "FileType",
:hash => @filetypes
)
# Rename the read and write methods, so that we're sure they
# maintain the stats.
klass.class_eval do
# Rename the read method
define_method(:real_read, instance_method(:read))
define_method(:read) do
begin
val = real_read
@loaded = Time.now
if val
return val.gsub(/# HEADER.*\n/,'')
else
return ""
end
rescue Puppet::Error => detail
raise
rescue => detail
message = "#{self.class} could not read #{@path}: #{detail}"
Puppet.log_exception(detail, message)
- raise Puppet::Error, message
+ raise Puppet::Error, message, detail.backtrace
end
end
# And then the write method
define_method(:real_write, instance_method(:write))
define_method(:write) do |text|
begin
val = real_write(text)
@synced = Time.now
return val
rescue Puppet::Error => detail
raise
rescue => detail
message = "#{self.class} could not write #{@path}: #{detail}"
Puppet.log_exception(detail, message)
- raise Puppet::Error, message
+ raise Puppet::Error, message, detail.backtrace
end
end
end
end
def self.filetype(type)
@filetypes[type]
end
# Pick or create a filebucket to use.
def bucket
@bucket ||= Puppet::Type.type(:filebucket).mkdefaultbucket.bucket
end
def initialize(path)
raise ArgumentError.new("Path is nil") if path.nil?
@path = path
end
# Arguments that will be passed to the execute method. Will set the uid
# to the target user if the target user and the current user are not
# the same
def cronargs
if uid = Puppet::Util.uid(@path) and uid == Puppet::Util::SUIDManager.uid
{:failonfail => true, :combine => true}
else
{:failonfail => true, :combine => true, :uid => @path}
end
end
# Operate on plain files.
newfiletype(:flat) do
# Back the file up before replacing it.
def backup
- bucket.backup(@path) if Puppet::FileSystem::File.exist?(@path)
+ bucket.backup(@path) if Puppet::FileSystem.exist?(@path)
end
# Read the file.
def read
- if Puppet::FileSystem::File.exist?(@path)
+ if Puppet::FileSystem.exist?(@path)
File.read(@path)
else
return nil
end
end
# Remove the file.
def remove
- Puppet::FileSystem::File.unlink(@path) if Puppet::FileSystem::File.exist?(@path)
+ Puppet::FileSystem.unlink(@path) if Puppet::FileSystem.exist?(@path)
end
# Overwrite the file.
def write(text)
tf = Tempfile.new("puppet")
tf.print text; tf.flush
FileUtils.cp(tf.path, @path)
tf.close
# If SELinux is present, we need to ensure the file has its expected context
set_selinux_default_context(@path)
end
end
# Operate on plain files.
newfiletype(:ram) do
@@tabs = {}
def self.clear
@@tabs.clear
end
def initialize(path)
super
@@tabs[@path] ||= ""
end
# Read the file.
def read
Puppet.info "Reading #{@path} from RAM"
@@tabs[@path]
end
# Remove the file.
def remove
Puppet.info "Removing #{@path} from RAM"
@@tabs[@path] = ""
end
# Overwrite the file.
def write(text)
Puppet.info "Writing #{@path} to RAM"
@@tabs[@path] = text
end
end
# Handle Linux-style cron tabs.
newfiletype(:crontab) do
def initialize(user)
self.path = user
end
def path=(user)
begin
@uid = Puppet::Util.uid(user)
rescue Puppet::Error => detail
raise FileReadError, "Could not retrieve user #{user}: #{detail}", detail.backtrace
end
# XXX We have to have the user name, not the uid, because some
# systems *cough*linux*cough* require it that way
@path = user
end
# Read a specific @path's cron tab.
def read
%x{#{cmdbase} -l 2>/dev/null}
end
# Remove a specific @path's cron tab.
def remove
if %w{Darwin FreeBSD DragonFly}.include?(Facter.value("operatingsystem"))
%x{/bin/echo yes | #{cmdbase} -r 2>/dev/null}
else
%x{#{cmdbase} -r 2>/dev/null}
end
end
# Overwrite a specific @path's cron tab; must be passed the @path name
# and the text with which to create the cron tab.
def write(text)
IO.popen("#{cmdbase()} -", "w") { |p|
p.print text
}
end
private
# Only add the -u flag when the @path is different. Fedora apparently
# does not think I should be allowed to set the @path to my own user name
def cmdbase
if @uid == Puppet::Util::SUIDManager.uid || Facter.value(:operatingsystem) == "HP-UX"
return "crontab"
else
return "crontab -u #{@path}"
end
end
end
# SunOS has completely different cron commands; this class implements
# its versions.
newfiletype(:suntab) do
# Read a specific @path's cron tab.
def read
Puppet::Util::Execution.execute(%w{crontab -l}, cronargs)
rescue => detail
case detail.to_s
when /can't open your crontab/
return ""
when /you are not authorized to use cron/
raise FileReadError, "User #{@path} not authorized to use cron", detail.backtrace
else
raise FileReadError, "Could not read crontab for #{@path}: #{detail}", detail.backtrace
end
end
# Remove a specific @path's cron tab.
def remove
Puppet::Util::Execution.execute(%w{crontab -r}, cronargs)
rescue => detail
raise FileReadError, "Could not remove crontab for #{@path}: #{detail}", detail.backtrace
end
# Overwrite a specific @path's cron tab; must be passed the @path name
# and the text with which to create the cron tab.
def write(text)
output_file = Tempfile.new("puppet_suntab")
begin
output_file.print text
output_file.close
# We have to chown the stupid file to the user.
File.chown(Puppet::Util.uid(@path), nil, output_file.path)
Puppet::Util::Execution.execute(["crontab", output_file.path], cronargs)
rescue => detail
raise FileReadError, "Could not write crontab for #{@path}: #{detail}", detail.backtrace
ensure
output_file.close
output_file.unlink
end
end
end
# Support for AIX crontab with output different than suntab's crontab command.
newfiletype(:aixtab) do
# Read a specific @path's cron tab.
def read
Puppet::Util::Execution.execute(%w{crontab -l}, cronargs)
rescue => detail
case detail.to_s
when /Cannot open a file in the .* directory/
return ""
when /You are not authorized to use the cron command/
raise FileReadError, "User #{@path} not authorized to use cron", detail.backtrace
else
raise FileReadError, "Could not read crontab for #{@path}: #{detail}", detail.backtrace
end
end
# Remove a specific @path's cron tab.
def remove
Puppet::Util::Execution.execute(%w{crontab -r}, cronargs)
rescue => detail
raise FileReadError, "Could not remove crontab for #{@path}: #{detail}", detail.backtrace
end
# Overwrite a specific @path's cron tab; must be passed the @path name
# and the text with which to create the cron tab.
def write(text)
output_file = Tempfile.new("puppet_aixtab")
begin
output_file.print text
output_file.close
# We have to chown the stupid file to the user.
File.chown(Puppet::Util.uid(@path), nil, output_file.path)
Puppet::Util::Execution.execute(["crontab", output_file.path], cronargs)
rescue => detail
raise FileReadError, "Could not write crontab for #{@path}: #{detail}", detail.backtrace
ensure
output_file.close
output_file.unlink
end
end
end
end
diff --git a/lib/puppet/util/inifile.rb b/lib/puppet/util/inifile.rb
index 82f074d53..df3c59850 100644
--- a/lib/puppet/util/inifile.rb
+++ b/lib/puppet/util/inifile.rb
@@ -1,203 +1,218 @@
# Module Puppet::IniConfig
# A generic way to parse .ini style files and manipulate them in memory
# One 'file' can be made up of several physical files. Changes to sections
# on the file are tracked so that only the physical files in which
# something has changed are written back to disk
# Great care is taken to preserve comments and blank lines from the original
# files
#
# The parsing tries to stay close to python's ConfigParser
require 'puppet/util/filetype'
module Puppet::Util::IniConfig
# A section in a .ini file
class Section
- attr_reader :name, :file
+ attr_reader :name, :file, :entries
+ attr_writer :destroy
def initialize(name, file)
@name = name
@file = file
@dirty = false
@entries = []
+ @destroy = false
end
# Has this section been modified since it's been read in
# or written back to disk
def dirty?
@dirty
end
# Should only be used internally
def mark_clean
@dirty = false
end
+ # Should the file be destroyed?
+ def destroy?
+ @destroy
+ end
+
# Add a line of text (e.g., a comment) Such lines
# will be written back out in exactly the same
# place they were read in
def add_line(line)
@entries << line
end
# Set the entry 'key=value'. If no entry with the
# given key exists, one is appended to teh end of the section
def []=(key, value)
entry = find_entry(key)
@dirty = true
if entry.nil?
@entries << [key, value]
else
entry[1] = value
end
end
# Return the value associated with KEY. If no such entry
# exists, return nil
def [](key)
entry = find_entry(key)
return(entry.nil? ? nil : entry[1])
end
# Format the section as text in the way it should be
# written to file
def format
text = "[#{name}]\n"
@entries.each do |entry|
if entry.is_a?(Array)
key, value = entry
text << "#{key}=#{value}\n" unless value.nil?
else
text << entry
end
end
text
end
private
def find_entry(key)
@entries.each do |entry|
return entry if entry.is_a?(Array) && entry[0] == key
end
nil
end
end
# A logical .ini-file that can be spread across several physical
# files. For each physical file, call #read with the filename
class File
def initialize
@files = {}
end
# Add the contents of the file with name FILE to the
# already existing sections
def read(file)
text = Puppet::Util::FileType.filetype(:flat).new(file).read
raise "Could not find #{file}" if text.nil?
section = nil # The name of the current section
optname = nil # The name of the last option in section
line = 0
@files[file] = []
text.each_line do |l|
line += 1
if l.strip.empty? || "#;".include?(l[0,1]) ||
(l.split(nil, 2)[0].downcase == "rem" && l[0,1].downcase == "r")
# Whitespace or comment
if section.nil?
@files[file] << l
else
section.add_line(l)
end
elsif " \t\r\n\f".include?(l[0,1]) && section && optname
# continuation line
section[optname] += "\n#{l.chomp}"
elsif l =~ /^\[([^\]]+)\]/
# section heading
section.mark_clean unless section.nil?
section = add_section($1, file)
optname = nil
elsif l =~ /^\s*([^\s=]+)\s*\=(.*)$/
# We allow space around the keys, but not the values
# For the values, we don't know if space is significant
if section.nil?
raise "#{file}:#{line}:Key/value pair outside of a section for key #{$1}"
else
section[$1] = $2
optname = $1
end
else
raise "#{file}:#{line}: Can't parse '#{l.chomp}'"
end
end
section.mark_clean unless section.nil?
end
# Store all modifications made to sections in this file back
# to the physical files. If no modifications were made to
# a physical file, nothing is written
def store
@files.each do |file, lines|
text = ""
dirty = false
+ destroy = false
lines.each do |l|
if l.is_a?(Section)
+ destroy ||= l.destroy?
dirty ||= l.dirty?
text << l.format
l.mark_clean
else
text << l
end
end
- if dirty
- Puppet::Util::FileType.filetype(:flat).new(file).write(text)
- return file
+ # We delete the file and then remove it from the list of files.
+ if destroy
+ ::File.unlink(file)
+ @files.delete(file)
+ else
+ if dirty
+ Puppet::Util::FileType.filetype(:flat).new(file).write(text)
+ return file
+ end
end
end
end
# Execute BLOCK, passing each section in this file
# as an argument
def each_section(&block)
@files.each do |file, list|
list.each do |entry|
yield(entry) if entry.is_a?(Section)
end
end
end
# Execute BLOCK, passing each file constituting this inifile
# as an argument
def each_file(&block)
@files.keys.each do |file|
yield(file)
end
end
# Return the Section with the given name or nil
def [](name)
name = name.to_s
each_section do |section|
return section if section.name == name
end
nil
end
# Return true if the file contains a section with name NAME
def include?(name)
! self[name].nil?
end
# Add a section to be stored in FILE when store is called
def add_section(name, file)
raise "A section with name #{name} already exists" if include?(name)
result = Section.new(name, file)
@files[file] ||= []
@files[file] << result
result
end
end
end
diff --git a/lib/puppet/util/instrumentation/data.rb b/lib/puppet/util/instrumentation/data.rb
index 48e595432..34d978cec 100644
--- a/lib/puppet/util/instrumentation/data.rb
+++ b/lib/puppet/util/instrumentation/data.rb
@@ -1,41 +1,46 @@
require 'puppet/indirector'
require 'puppet/util/instrumentation'
# This is just a transport class to be used through the instrumentation_data
# indirection. All the data resides in the real underlying listeners which this
# class delegates to.
class Puppet::Util::Instrumentation::Data
extend Puppet::Indirector
indirects :instrumentation_data, :terminus_class => :local
attr_reader :listener
def initialize(listener_name)
@listener = Puppet::Util::Instrumentation[listener_name]
raise "Listener #{listener_name} wasn't registered" unless @listener
end
def name
@listener.name
end
def to_data_hash
{ :name => name }.merge(@listener.respond_to?(:data) ? @listener.data : {})
end
def to_pson_data_hash
{
'document_type' => "Puppet::Util::Instrumentation::Data",
'data' => to_data_hash,
}
end
def to_pson(*args)
to_pson_data_hash.to_pson(*args)
end
+ def self.from_data_hash(data)
+ data
+ end
+
def self.from_pson(data)
+ Puppet.deprecation_warning("from_pson is being removed in favour of from_data_hash.")
data
end
end
diff --git a/lib/puppet/util/instrumentation/indirection_probe.rb b/lib/puppet/util/instrumentation/indirection_probe.rb
index 237d8dbc8..afaccd303 100644
--- a/lib/puppet/util/instrumentation/indirection_probe.rb
+++ b/lib/puppet/util/instrumentation/indirection_probe.rb
@@ -1,36 +1,41 @@
require 'puppet/indirector'
require 'puppet/util/instrumentation'
# We need to use a class other than Probe for the indirector because
# the Indirection class might declare some probes, and this would be a huge unbreakable
# dependency cycle.
class Puppet::Util::Instrumentation::IndirectionProbe
extend Puppet::Indirector
indirects :instrumentation_probe, :terminus_class => :local
attr_reader :probe_name
def initialize(probe_name)
@probe_name = probe_name
end
def to_data_hash
{ :name => probe_name }
end
def to_pson_data_hash
{
:document_type => "Puppet::Util::Instrumentation::IndirectionProbe",
:data => to_data_hash,
}
end
def to_pson(*args)
to_pson_data_hash.to_pson(*args)
end
- def self.from_pson(data)
+ def self.from_data_hash(data)
self.new(data["name"])
end
+
+ def self.from_pson(data)
+ Puppet.deprecation_warning("from_pson is being removed in favour of from_data_hash.")
+ self.from_data_hash(data)
+ end
end
diff --git a/lib/puppet/util/instrumentation/listener.rb b/lib/puppet/util/instrumentation/listener.rb
index b965e976f..cb25b2925 100644
--- a/lib/puppet/util/instrumentation/listener.rb
+++ b/lib/puppet/util/instrumentation/listener.rb
@@ -1,67 +1,72 @@
require 'puppet/indirector'
require 'puppet/util/instrumentation'
require 'puppet/util/instrumentation/data'
class Puppet::Util::Instrumentation::Listener
include Puppet::Util
include Puppet::Util::Warnings
extend Puppet::Indirector
indirects :instrumentation_listener, :terminus_class => :local
attr_reader :pattern, :listener
attr_accessor :enabled
def initialize(listener, pattern = nil, enabled = false)
@pattern = pattern.is_a?(Symbol) ? pattern.to_s : pattern
raise "Listener isn't a correct listener (it doesn't provide the notify method)" unless listener.respond_to?(:notify)
@listener = listener
@enabled = enabled
end
def notify(label, event, data)
listener.notify(label, event, data)
rescue => e
warnonce("Error during instrumentation notification: #{e}")
end
def listen_to?(label)
enabled? and (!@pattern || @pattern === label.to_s)
end
def enabled?
!!@enabled
end
def name
@listener.name.to_s
end
def data
{ :data => @listener.data }
end
def to_data_hash
{
:name => name,
:pattern => pattern,
:enabled => enabled?
}
end
def to_pson_data_hash
{
:document_type => "Puppet::Util::Instrumentation::Listener",
:data => to_data_hash,
}
end
def to_pson(*args)
to_pson_data_hash.to_pson(*args)
end
- def self.from_pson(data)
+ def self.from_data_hash(data)
result = Puppet::Util::Instrumentation[data["name"]]
self.new(result.listener, result.pattern, data["enabled"])
end
+
+ def self.from_pson(data)
+ Puppet.deprecation_warning("from_pson is being removed in favour of from_data_hash.")
+ self.from_data_hash(data)
+ end
end
diff --git a/lib/puppet/util/json_lockfile.rb b/lib/puppet/util/json_lockfile.rb
index ee2603c4e..df67b170f 100644
--- a/lib/puppet/util/json_lockfile.rb
+++ b/lib/puppet/util/json_lockfile.rb
@@ -1,41 +1,44 @@
require 'puppet/util/lockfile'
# This class provides a simple API for managing a lock file
# whose contents are a serialized JSON object. In addition
# to querying the basic state (#locked?) of the lock, managing
# the lock (#lock, #unlock), the contents can be retrieved at
# any time while the lock is held (#lock_data). This can be
# used to store structured data (state messages, etc.) about
# the lock.
#
# @see Puppet::Util::Lockfile
class Puppet::Util::JsonLockfile < Puppet::Util::Lockfile
# Lock the lockfile. You may optionally pass a data object, which will be
# retrievable for the duration of time during which the file is locked.
#
# @param [Hash] lock_data an optional Hash of data to associate with the lock.
# This may be used to store pids, descriptive messages, etc. The
# data may be retrieved at any time while the lock is held by
# calling the #lock_data method. <b>NOTE</b> that the JSON serialization
# does NOT support Symbol objects--if you pass them in, they will be
# serialized as Strings, so you should plan accordingly.
# @return [boolean] true if lock is successfully acquired, false otherwise.
def lock(lock_data = nil)
return false if locked?
super(lock_data.to_pson)
end
# Retrieve the (optional) lock data that was specified at the time the file
# was locked.
# @return [Object] the data object. Remember that the serialization does not
# support Symbol objects, so if your data Object originally contained symbols,
# they will be converted to Strings.
def lock_data
return nil unless file_locked?
file_contents = super
- return nil if file_contents.nil?
+ return nil if file_contents.nil? or file_contents.empty?
PSON.parse(file_contents)
+ rescue PSON::ParserError => e
+ Puppet.warning "Unable to read lockfile data from #{@file_path}: not in PSON"
+ nil
end
end
diff --git a/lib/puppet/util/ldap/connection.rb b/lib/puppet/util/ldap/connection.rb
index 9f951ce10..c5110921c 100644
--- a/lib/puppet/util/ldap/connection.rb
+++ b/lib/puppet/util/ldap/connection.rb
@@ -1,71 +1,71 @@
require 'puppet/util/ldap'
require 'puppet/util/methodhelper'
class Puppet::Util::Ldap::Connection
include Puppet::Util::MethodHelper
attr_accessor :host, :port, :user, :password, :reset, :ssl
attr_reader :connection
# Return a default connection, using our default settings.
def self.instance
ssl = if Puppet[:ldaptls]
:tls
elsif Puppet[:ldapssl]
true
else
false
end
options = {}
options[:ssl] = ssl
if user = Puppet.settings[:ldapuser] and user != ""
options[:user] = user
if pass = Puppet.settings[:ldappassword] and pass != ""
options[:password] = pass
end
end
new(Puppet[:ldapserver], Puppet[:ldapport], options)
end
def close
connection.unbind if connection.bound?
end
def initialize(host, port, options = {})
raise Puppet::Error, "Could not set up LDAP Connection: Missing ruby/ldap libraries" unless Puppet.features.ldap?
@host, @port = host, port
set_options(options)
end
# Create a per-connection unique name.
def name
[host, port, user, password, ssl].collect { |p| p.to_s }.join("/")
end
# Should we reset the connection?
def reset?
reset
end
# Start our ldap connection.
def start
case ssl
when :tls
@connection = LDAP::SSLConn.new(host, port, true)
when true
@connection = LDAP::SSLConn.new(host, port)
else
@connection = LDAP::Conn.new(host, port)
end
@connection.set_option(LDAP::LDAP_OPT_PROTOCOL_VERSION, 3)
@connection.set_option(LDAP::LDAP_OPT_REFERRALS, LDAP::LDAP_OPT_ON)
@connection.simple_bind(user, password)
rescue => detail
- raise Puppet::Error, "Could not connect to LDAP: #{detail}"
+ raise Puppet::Error, "Could not connect to LDAP: #{detail}", detail.backtrace
end
end
diff --git a/lib/puppet/util/lockfile.rb b/lib/puppet/util/lockfile.rb
index 4f1ad5716..9771f389e 100644
--- a/lib/puppet/util/lockfile.rb
+++ b/lib/puppet/util/lockfile.rb
@@ -1,62 +1,66 @@
# This class provides a simple API for managing a lock file
# whose contents are an (optional) String. In addition
# to querying the basic state (#locked?) of the lock, managing
# the lock (#lock, #unlock), the contents can be retrieved at
# any time while the lock is held (#lock_data). This can be
# used to store pids, messages, etc.
#
# @see Puppet::Util::JsonLockfile
class Puppet::Util::Lockfile
attr_reader :file_path
def initialize(file_path)
@file_path = file_path
end
# Lock the lockfile. You may optionally pass a data object, which will be
# retrievable for the duration of time during which the file is locked.
#
# @param [String] lock_data an optional String data object to associate
# with the lock. This may be used to store pids, descriptive messages,
# etc. The data may be retrieved at any time while the lock is held by
# calling the #lock_data method.
# @return [boolean] true if lock is successfully acquired, false otherwise.
def lock(lock_data = nil)
- return false if locked?
-
- File.open(@file_path, 'w') { |fd| fd.print(lock_data) }
- true
+ begin
+ Puppet::FileSystem.exclusive_create(@file_path, nil) do |fd|
+ fd.print(lock_data)
+ end
+ true
+ rescue Errno::EEXIST
+ false
+ end
end
def unlock
if locked?
- Puppet::FileSystem::File.unlink(@file_path)
+ Puppet::FileSystem.unlink(@file_path)
true
else
false
end
end
def locked?
# delegate logic to a more explicit private method
file_locked?
end
# Retrieve the (optional) lock data that was specified at the time the file
# was locked.
# @return [String] the data object.
def lock_data
return File.read(@file_path) if file_locked?
end
# Private, internal utility method for encapsulating the logic about
# whether or not the file is locked. This method can be called
# by other methods in this class without as much risk of accidentally
# being overridden by child classes.
# @return [boolean] true if the file is locked, false if it is not.
def file_locked?()
- Puppet::FileSystem::File.exist? @file_path
+ Puppet::FileSystem.exist? @file_path
end
private :file_locked?
end
diff --git a/lib/puppet/util/log.rb b/lib/puppet/util/log.rb
index 808e0631d..05f0b3fad 100644
--- a/lib/puppet/util/log.rb
+++ b/lib/puppet/util/log.rb
@@ -1,338 +1,343 @@
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
# multiple destinations, one of which is a remote server.
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
@desttypes = {}
# Create a new destination type.
def self.newdesttype(name, options = {}, &block)
dest = genclass(
name,
:parent => Puppet::Util::Log::Destination,
:prefix => "Dest",
:block => block,
:hash => @desttypes,
:attributes => options
)
dest.match(dest.name)
dest
end
require 'puppet/util/log/destination'
require 'puppet/util/log/destinations'
@destinations = {}
@queued = []
class << self
include Puppet::Util
include Puppet::Util::ClassGen
attr_reader :desttypes
end
# Reset log to basics. Basically just flushes and closes files and
# undefs other objects.
def Log.close(destination)
if @destinations.include?(destination)
@destinations[destination].flush if @destinations[destination].respond_to?(:flush)
@destinations[destination].close if @destinations[destination].respond_to?(:close)
@destinations.delete(destination)
end
end
def self.close_all
destinations.keys.each { |dest|
close(dest)
}
raise Puppet::DevError.new("Log.close_all failed to close #{@destinations.keys.inspect}") if !@destinations.empty?
end
# Flush any log destinations that support such operations.
def Log.flush
@destinations.each { |type, dest|
dest.flush if dest.respond_to?(:flush)
}
end
def Log.autoflush=(v)
@destinations.each do |type, dest|
dest.autoflush = v if dest.respond_to?(:autoflush=)
end
end
# Create a new log message. The primary role of this method is to
# avoid creating log messages below the loglevel.
def Log.create(hash)
raise Puppet::DevError, "Logs require a level" unless hash.include?(:level)
raise Puppet::DevError, "Invalid log level #{hash[:level]}" unless @levels.index(hash[:level])
@levels.index(hash[:level]) >= @loglevel ? Puppet::Util::Log.new(hash) : nil
end
def Log.destinations
@destinations
end
# Yield each valid level in turn
def Log.eachlevel
@levels.each { |level| yield level }
end
# Return the current log level.
def Log.level
@levels[@loglevel]
end
# Set the current log level.
def Log.level=(level)
level = level.intern unless level.is_a?(Symbol)
raise Puppet::DevError, "Invalid loglevel #{level}" unless @levels.include?(level)
@loglevel = @levels.index(level)
end
def Log.levels
@levels.dup
end
# Create a new log destination.
def Log.newdestination(dest)
# Each destination can only occur once.
if @destinations.find { |name, obj| obj.name == dest }
return
end
name, type = @desttypes.find do |name, klass|
klass.match?(dest)
end
if type.respond_to?(:suitable?) and not type.suitable?(dest)
return
end
raise Puppet::DevError, "Unknown destination type #{dest}" unless type
begin
if type.instance_method(:initialize).arity == 1
@destinations[dest] = type.new(dest)
else
@destinations[dest] = type.new
end
flushqueue
@destinations[dest]
rescue => detail
Puppet.log_exception(detail)
# If this was our only destination, then add the console back in.
newdestination(:console) if @destinations.empty? and (dest != :console and dest != "console")
end
end
def Log.with_destination(destination, &block)
if @destinations.include?(destination)
yield
else
newdestination(destination)
begin
yield
ensure
close(destination)
end
end
end
# Route the actual message. FIXME There are lots of things this method
# should do, like caching and a bit more. It's worth noting that there's
# a potential for a loop here, if the machine somehow gets the destination set as
# itself.
def Log.newmessage(msg)
return if @levels.index(msg.level) < @loglevel
queuemessage(msg) if @destinations.length == 0
@destinations.each do |name, dest|
dest.handle(msg)
end
end
def Log.queuemessage(msg)
@queued.push(msg)
end
def Log.flushqueue
return unless @destinations.size >= 1
@queued.each do |msg|
Log.newmessage(msg)
end
@queued.clear
end
# Flush the logging queue. If there are no destinations available,
# adds in a console logger before flushing the queue.
# This is mainly intended to be used as a last-resort attempt
# to ensure that logging messages are not thrown away before
# the program is about to exit--most likely in a horrific
# error scenario.
# @return nil
def Log.force_flushqueue()
if (@destinations.empty? and !(@queued.empty?))
newdestination(:console)
end
flushqueue
end
def Log.sendlevel?(level)
@levels.index(level) >= @loglevel
end
# Reopen all of our logs.
def Log.reopen
Puppet.notice "Reopening log files"
types = @destinations.keys
@destinations.each { |type, dest|
dest.close if dest.respond_to?(:close)
}
@destinations.clear
# We need to make sure we always end up with some kind of destination
begin
types.each { |type|
Log.newdestination(type)
}
rescue => detail
if @destinations.empty?
Log.setup_default
Puppet.err detail.to_s
end
end
end
def self.setup_default
Log.newdestination(
(Puppet.features.syslog? ? :syslog :
(Puppet.features.eventlog? ? :eventlog : Puppet[:puppetdlog])))
end
# Is the passed level a valid log level?
def self.validlevel?(level)
@levels.include?(level)
end
- def self.from_pson(data)
+ def self.from_data_hash(data)
obj = allocate
obj.initialize_from_hash(data)
obj
end
+ def self.from_pson(data)
+ Puppet.deprecation_warning("from_pson is being removed in favour of from_data_hash.")
+ self.from_data_hash(data)
+ end
+
attr_accessor :time, :remote, :file, :line, :source
attr_reader :level, :message
def initialize(args)
self.level = args[:level]
self.message = args[:message]
self.source = args[:source] || "Puppet"
@time = Time.now
if tags = args[:tags]
tags.each { |t| self.tag(t) }
end
[:file, :line].each do |attr|
next unless value = args[attr]
send(attr.to_s + "=", value)
end
Log.newmessage(self)
end
def initialize_from_hash(data)
@level = data['level'].intern
@message = data['message']
@source = data['source']
@tags = Puppet::Util::TagSet.new(data['tags'])
@time = data['time']
if @time.is_a? String
@time = Time.parse(@time)
end
@file = data['file'] if data['file']
@line = data['line'] if data['line']
end
def to_hash
self.to_data_hash
end
def to_data_hash
{
'level' => @level,
'message' => @message,
'source' => @source,
'tags' => @tags,
'time' => @time.iso8601(9),
'file' => @file,
'line' => @line,
}
end
def to_pson(*args)
to_data_hash.to_pson(*args)
end
def message=(msg)
raise ArgumentError, "Puppet::Util::Log requires a message" unless msg
@message = msg.to_s
end
def level=(level)
raise ArgumentError, "Puppet::Util::Log requires a log level" unless level
raise ArgumentError, "Puppet::Util::Log requires a symbol or string" unless level.respond_to? "to_sym"
@level = level.to_sym
raise ArgumentError, "Invalid log level #{@level}" unless self.class.validlevel?(@level)
# Tag myself with my log level
tag(level)
end
# If they pass a source in to us, we make sure it is a string, and
# we retrieve any tags we can.
def source=(source)
if source.respond_to?(:path)
@source = source.path
source.tags.each { |t| tag(t) }
self.file = source.file
self.line = source.line
else
@source = source.to_s
end
end
def to_report
"#{time} #{source} (#{level}): #{to_s}"
end
def to_s
message
end
end
# This is for backward compatibility from when we changed the constant to Puppet::Util::Log
# because the reports include the constant name. Apparently the alias was created in
# March 2007, should could probably be removed soon.
Puppet::Log = Puppet::Util::Log
diff --git a/lib/puppet/util/log/destinations.rb b/lib/puppet/util/log/destinations.rb
index ecb19c66b..932007099 100644
--- a/lib/puppet/util/log/destinations.rb
+++ b/lib/puppet/util/log/destinations.rb
@@ -1,228 +1,228 @@
Puppet::Util::Log.newdesttype :syslog do
def self.suitable?(obj)
Puppet.features.syslog?
end
def close
Syslog.close
end
def initialize
Syslog.close if Syslog.opened?
name = "puppet-#{Puppet.run_mode.name}"
options = Syslog::LOG_PID | Syslog::LOG_NDELAY
# XXX This should really be configurable.
str = Puppet[:syslogfacility]
begin
facility = Syslog.const_get("LOG_#{str.upcase}")
rescue NameError
- raise Puppet::Error, "Invalid syslog facility #{str}"
+ raise Puppet::Error, "Invalid syslog facility #{str}", $!.backtrace
end
@syslog = Syslog.open(name, options, facility)
end
def handle(msg)
# XXX Syslog currently has a bug that makes it so you
# cannot log a message with a '%' in it. So, we get rid
# of them.
if msg.source == "Puppet"
msg.to_s.split("\n").each do |line|
@syslog.send(msg.level, line.gsub("%", '%%'))
end
else
msg.to_s.split("\n").each do |line|
@syslog.send(msg.level, "(%s) %s" % [msg.source.to_s.gsub("%", ""),
line.gsub("%", '%%')
]
)
end
end
end
end
Puppet::Util::Log.newdesttype :file do
require 'fileutils'
def self.match?(obj)
Puppet::Util.absolute_path?(obj)
end
def close
if defined?(@file)
@file.close
@file = nil
end
end
def flush
@file.flush if defined?(@file)
end
attr_accessor :autoflush
def initialize(path)
@name = path
# first make sure the directory exists
# We can't just use 'Config.use' here, because they've
# specified a "special" destination.
- unless Puppet::FileSystem::File.exist?(File.dirname(path))
+ unless Puppet::FileSystem.exist?(Puppet::FileSystem.dir(path))
FileUtils.mkdir_p(File.dirname(path), :mode => 0755)
Puppet.info "Creating log directory #{File.dirname(path)}"
end
# create the log file, if it doesn't already exist
file = File.open(path, File::WRONLY|File::CREAT|File::APPEND)
# Give ownership to the user and group puppet will run as
begin
FileUtils.chown(Puppet[:user], Puppet[:group], path) unless Puppet::Util::Platform.windows?
rescue ArgumentError, Errno::EPERM
Puppet.err "Unable to set ownership of log file"
end
@file = file
@autoflush = Puppet[:autoflush]
end
def handle(msg)
@file.puts("#{msg.time} #{msg.source} (#{msg.level}): #{msg}")
@file.flush if @autoflush
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
def initialize
# Flush output immediately.
$stderr.sync = true
$stdout.sync = true
end
def handle(msg)
levels = {
:emerg => { :name => 'Emergency', :color => :hred, :stream => $stderr },
:alert => { :name => 'Alert', :color => :hred, :stream => $stderr },
:crit => { :name => 'Critical', :color => :hred, :stream => $stderr },
:err => { :name => 'Error', :color => :hred, :stream => $stderr },
:warning => { :name => 'Warning', :color => :hred, :stream => $stderr },
:notice => { :name => 'Notice', :color => :reset, :stream => $stdout },
:info => { :name => 'Info', :color => :green, :stream => $stdout },
:debug => { :name => 'Debug', :color => :cyan, :stream => $stdout },
}
str = msg.respond_to?(:multiline) ? msg.multiline : msg.to_s
str = msg.source == "Puppet" ? str : "#{msg.source}: #{str}"
level = levels[msg.level]
level[:stream].puts colorize(level[:color], "#{level[:name]}: #{str}")
end
end
# Log to a transaction report.
Puppet::Util::Log.newdesttype :report do
attr_reader :report
match "Puppet::Transaction::Report"
def initialize(report)
@report = report
end
def handle(msg)
@report << msg
end
end
# Log to an array, just for testing.
module Puppet::Test
class LogCollector
def initialize(logs)
@logs = logs
end
def <<(value)
@logs << value
end
end
end
Puppet::Util::Log.newdesttype :array do
match "Puppet::Test::LogCollector"
def initialize(messages)
@messages = messages
end
def handle(msg)
@messages << msg
end
end
Puppet::Util::Log.newdesttype :eventlog do
def self.suitable?(obj)
Puppet.features.eventlog?
end
def initialize
@eventlog = Win32::EventLog.open("Application")
end
def to_native(level)
case level
when :debug,:info,:notice
[Win32::EventLog::INFO, 0x01]
when :warning
[Win32::EventLog::WARN, 0x02]
when :err,:alert,:emerg,:crit
[Win32::EventLog::ERROR, 0x03]
end
end
def handle(msg)
native_type, native_id = to_native(msg.level)
@eventlog.report_event(
:source => "Puppet",
:event_type => native_type,
:event_id => native_id,
:data => (msg.source and msg.source != 'Puppet' ? "#{msg.source}: " : '') + msg.to_s
)
end
def close
if @eventlog
@eventlog.close
@eventlog = nil
end
end
end
diff --git a/lib/puppet/util/metric.rb b/lib/puppet/util/metric.rb
index f37bbcfea..4173fded6 100644
--- a/lib/puppet/util/metric.rb
+++ b/lib/puppet/util/metric.rb
@@ -1,205 +1,210 @@
# 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
attr_writer :basedir
- def self.from_pson(data)
+ def self.from_data_hash(data)
metric = new(data['name'], data['label'])
metric.values = data['values']
metric
end
+ def self.from_pson(data)
+ Puppet.deprecation_warning("from_pson is being removed in favour of from_data_hash.")
+ self.from_data_hash(data)
+ end
+
def to_data_hash
{
'name' => @name,
'label' => @label,
'values' => @values
}
end
def to_pson(*args)
to_data_hash.to_pson(*args)
end
# Return a specific value
def [](name)
if value = @values.find { |v| v[0] == name }
return value[2]
else
return 0
end
end
def basedir
if defined?(@basedir)
@basedir
else
Puppet[:rrddir]
end
end
def create(start = nil)
Puppet.settings.use(:main, :metrics)
start ||= Time.now.to_i - 5
args = []
if Puppet.features.rrd_legacy? && ! Puppet.features.rrd?
@rrd = RRDtool.new(self.path)
end
values.each { |value|
# the 7200 is the heartbeat -- this means that any data that isn't
# more frequently than every two hours gets thrown away
args.push "DS:#{value[0]}:GAUGE:7200:U:U"
}
args.push "RRA:AVERAGE:0.5:1:300"
begin
if Puppet.features.rrd_legacy? && ! Puppet.features.rrd?
@rrd.create( Puppet[:rrdinterval], start, args)
else
RRD.create( self.path, '-s', Puppet[:rrdinterval].to_s, '-b', start.to_i.to_s, *args)
end
rescue => detail
- raise "Could not create RRD file #{path}: #{detail}"
+ raise detail, "Could not create RRD file #{path}: #{detail}", detail.backtrace
end
end
def dump
if Puppet.features.rrd_legacy? && ! Puppet.features.rrd?
puts @rrd.info
else
puts RRD.info(self.path)
end
end
def graph(range = nil)
unless Puppet.features.rrd? || Puppet.features.rrd_legacy?
Puppet.warning "RRD library is missing; cannot graph metrics"
return
end
unit = 60 * 60 * 24
colorstack = %w{#00ff00 #ff0000 #0000ff #ffff00 #ff99ff #ff9966 #66ffff #990000 #099000 #000990 #f00990 #0f0f0f #555555 #333333 #ffffff}
{:daily => unit, :weekly => unit * 7, :monthly => unit * 30, :yearly => unit * 365}.each do |name, time|
file = self.path.sub(/\.rrd$/, "-#{name}.png")
args = [file]
args.push("--title",self.label)
args.push("--imgformat","PNG")
args.push("--interlace")
defs = []
lines = []
#p @values.collect { |s,l| s }
values.zip(colorstack).each { |value,color|
next if value.nil?
# this actually uses the data label
defs.push("DEF:#{value[0]}=#{self.path}:#{value[0]}:AVERAGE")
lines.push("LINE2:#{value[0]}#{color}:#{value[1]}")
}
args << defs
args << lines
args.flatten!
if range
if Puppet.features.rrd_legacy? && ! Puppet.features.rrd?
args.push("--start",range[0],"--end",range[1])
else
args.push("--start",range[0].to_i.to_s,"--end",range[1].to_i.to_s)
end
else
if Puppet.features.rrd_legacy? && ! Puppet.features.rrd?
args.push("--start", Time.now.to_i - time, "--end", Time.now.to_i)
else
args.push("--start", (Time.now.to_i - time).to_s, "--end", Time.now.to_i.to_s)
end
end
begin
#Puppet.warning "args = #{args}"
if Puppet.features.rrd_legacy? && ! Puppet.features.rrd?
RRDtool.graph( args )
else
RRD.graph( *args )
end
rescue => detail
Puppet.err "Failed to graph #{self.name}: #{detail}"
end
end
end
def initialize(name,label = nil)
@name = name.to_s
@label = label || self.class.labelize(name)
@values = []
end
def path
File.join(self.basedir, @name + ".rrd")
end
def newvalue(name,value,label = nil)
raise ArgumentError.new("metric name #{name.inspect} is not a string") unless name.is_a? String
label ||= self.class.labelize(name)
@values.push [name,label,value]
end
def store(time)
unless Puppet.features.rrd? || Puppet.features.rrd_legacy?
Puppet.warning "RRD library is missing; cannot store metrics"
return
end
- self.create(time - 5) unless Puppet::FileSystem::File.exist?(self.path)
+ self.create(time - 5) unless Puppet::FileSystem.exist?(self.path)
if Puppet.features.rrd_legacy? && ! Puppet.features.rrd?
@rrd ||= RRDtool.new(self.path)
end
# XXX this is not terribly error-resistant
args = [time]
temps = []
values.each { |value|
#Puppet.warning "value[0]: #{value[0]}; value[1]: #{value[1]}; value[2]: #{value[2]}; "
args.push value[2]
temps.push value[0]
}
arg = args.join(":")
template = temps.join(":")
begin
if Puppet.features.rrd_legacy? && ! Puppet.features.rrd?
@rrd.update( template, [ arg ] )
else
RRD.update( self.path, '-t', template, arg )
end
#system("rrdtool updatev #{self.path} '#{arg}'")
rescue => detail
- raise Puppet::Error, "Failed to update #{self.name}: #{detail}"
+ raise Puppet::Error, "Failed to update #{self.name}: #{detail}", detail.backtrace
end
end
def values
@values.sort { |a, b| a[1] <=> b[1] }
end
# Convert a name into a label.
def self.labelize(name)
name.to_s.capitalize.gsub("_", " ")
end
end
# This is necessary because we changed the class path in early 2007,
# and reports directly yaml-dump these metrics, so both client and server
# have to agree on the class name.
Puppet::Metric = Puppet::Util::Metric
diff --git a/lib/puppet/util/nagios_maker.rb b/lib/puppet/util/nagios_maker.rb
index 02e5d8bd7..61cf9248b 100644
--- a/lib/puppet/util/nagios_maker.rb
+++ b/lib/puppet/util/nagios_maker.rb
@@ -1,60 +1,85 @@
require 'puppet/external/nagios'
require 'puppet/external/nagios/base'
require 'puppet/provider/naginator'
module Puppet::Util::NagiosMaker
# Create a new nagios type, using all of the parameters
# from the parser.
def self.create_nagios_type(name)
name = name.to_sym
full_name = ("nagios_#{name}").to_sym
raise(Puppet::DevError, "No nagios type for #{name}") unless nagtype = Nagios::Base.type(name)
- type = Puppet::Type.newtype(full_name) {}
+ type = Puppet::Type.newtype(full_name) do
+
+ # Generate a file resource if necessary.
+ #
+ # @see Puppet::Type::File and its properties owner, group and mode.
+ def generate
+ return nil unless self[:owner] or self[:group] or self[:mode]
+ props = { :name => self[:target] }
+ [ :owner, :group, :mode ].each do |prop|
+ props[prop] = self[prop] if self[prop]
+ end
+ Puppet::Type.type(:file).new(props)
+ end
+
+ end
type.ensurable
type.newparam(nagtype.namevar, :namevar => true) do
desc "The name of this nagios_#{nagtype.name} resource."
end
+ [ :owner, :group, :mode].each do |fileprop|
+ type.newparam(fileprop) do
+ desc "The desired #{fileprop} of the config file for this nagios_#{nagtype.name} resource.
+
+ NOTE: If the target file is explicitly managed by a file resource in your manifest,
+ this parameter has no effect. If a parent directory of the target is managed by
+ a recursive file resource, this limitation does not apply (i.e., this parameter
+ takes precedence, and if purge is used, the target file is exempt)."
+ end
+ end
+
# We deduplicate the parameters because it makes sense to allow Naginator to have dupes.
nagtype.parameters.uniq.each do |param|
next if param == nagtype.namevar
# We can't turn these parameter names into constants, so at least for now they aren't
# supported.
next if param.to_s =~ /^[0-9]/
type.newproperty(param) do
desc "Nagios configuration file parameter."
end
end
type.newproperty(:target) do
desc 'The target.'
defaultto do
resource.class.defaultprovider.default_target
end
end
target = "/etc/nagios/#{full_name.to_s}.cfg"
provider = type.provide(:naginator, :parent => Puppet::Provider::Naginator, :default_target => target) {}
provider.nagios_type
type.desc "The Nagios type #{name.to_s}. This resource type is autogenerated using the
model developed in Naginator, and all of the Nagios types are generated using the
same code and the same library.
This type generates Nagios configuration statements in Nagios-parseable configuration
files. By default, the statements will be added to `#{target}`, but
you can send them to a different file by setting their `target` attribute.
You can purge Nagios resources using the `resources` type, but *only*
in the default file locations. This is an architectural limitation.
"
end
end
diff --git a/lib/puppet/util/network_device.rb b/lib/puppet/util/network_device.rb
index 2d0f94f4f..60da0363a 100644
--- a/lib/puppet/util/network_device.rb
+++ b/lib/puppet/util/network_device.rb
@@ -1,17 +1,17 @@
class Puppet::Util::NetworkDevice
class << self
attr_reader :current
end
def self.init(device)
require "puppet/util/network_device/#{device.provider}/device"
@current = Puppet::Util::NetworkDevice.const_get(device.provider.capitalize).const_get(:Device).new(device.url, device.options)
rescue => detail
- raise "Can't load #{device.provider} for #{device.name}: #{detail}"
+ raise detail, "Can't load #{device.provider} for #{device.name}: #{detail}", detail.backtrace
end
# Should only be used in tests
def self.teardown
@current = nil
end
end
diff --git a/lib/puppet/util/network_device/config.rb b/lib/puppet/util/network_device/config.rb
index fe355708a..2d6283c64 100644
--- a/lib/puppet/util/network_device/config.rb
+++ b/lib/puppet/util/network_device/config.rb
@@ -1,93 +1,93 @@
require 'ostruct'
require 'puppet/util/watched_file'
require 'puppet/util/network_device'
class Puppet::Util::NetworkDevice::Config
def self.main
@main ||= self.new
end
def self.devices
main.devices || []
end
attr_reader :devices
def exists?
- Puppet::FileSystem::File.exist?(@file)
+ Puppet::FileSystem.exist?(@file.to_str)
end
def initialize
@file = Puppet::Util::WatchedFile.new(Puppet[:deviceconfig])
@devices = {}
read(true) # force reading at start
end
# Read the configuration file.
def read(force = false)
return unless exists?
parse if force or @file.changed?
end
private
def parse
begin
devices = {}
device = nil
File.open(@file) { |f|
count = 1
f.each { |line|
case line
when /^\s*#/ # skip comments
count += 1
next
when /^\s*$/ # skip blank lines
count += 1
next
when /^\[([\w.-]+)\]\s*$/ # [device.fqdn]
name = $1
name.chomp!
raise Puppet::Error, "Duplicate device found at line #{count}, already found at #{device.line}" if devices.include?(name)
device = OpenStruct.new
device.name = name
device.line = count
device.options = { :debug => false }
Puppet.debug "found device: #{device.name} at #{device.line}"
devices[name] = device
when /^\s*(type|url|debug)(\s+(.+))*$/
parse_directive(device, $1, $3, count)
else
raise Puppet::Error, "Invalid line #{count}: #{line}"
end
count += 1
}
}
rescue Errno::EACCES => detail
Puppet.err "Configuration error: Cannot read #{@file}; cannot serve"
#raise Puppet::Error, "Cannot read #{@config}"
rescue Errno::ENOENT => detail
Puppet.err "Configuration error: '#{@file}' does not exit; cannot serve"
end
@devices = devices
end
def parse_directive(device, var, value, count)
case var
when "type"
device.provider = value
when "url"
device.url = value
when "debug"
device.options[:debug] = true
else
raise Puppet::Error, "Invalid argument '#{var}' at line #{count}"
end
end
end
diff --git a/lib/puppet/util/network_device/transport/ssh.rb b/lib/puppet/util/network_device/transport/ssh.rb
index cc613bda0..1c20eb822 100644
--- a/lib/puppet/util/network_device/transport/ssh.rb
+++ b/lib/puppet/util/network_device/transport/ssh.rb
@@ -1,122 +1,122 @@
require 'puppet/util/network_device'
require 'puppet/util/network_device/transport'
require 'puppet/util/network_device/transport/base'
# This is an adaptation/simplification of gem net-ssh-telnet, which aims to have
# a sane interface to Net::SSH. Credits goes to net-ssh-telnet authors
class Puppet::Util::NetworkDevice::Transport::Ssh < Puppet::Util::NetworkDevice::Transport::Base
attr_accessor :buf, :ssh, :channel
def initialize(verbose = false)
super()
@verbose = verbose
unless Puppet.features.ssh?
raise 'Connecting with ssh to a network device requires the \'net/ssh\' ruby library'
end
end
def handles_login?
true
end
def eof?
!! @eof
end
def connect(&block)
@output = []
@channel_data = ""
begin
Puppet.debug("connecting to #{host} as #{user}")
@ssh = Net::SSH.start(host, user, :port => port, :password => password, :timeout => timeout)
rescue TimeoutError
- raise TimeoutError, "timed out while opening an ssh connection to the host"
+ raise TimeoutError, "timed out while opening an ssh connection to the host", $!.backtrace
rescue Net::SSH::AuthenticationFailed
- raise Puppet::Error, "SSH authentication failure connecting to #{host} as #{user}"
+ raise Puppet::Error, "SSH authentication failure connecting to #{host} as #{user}", $!.backtrace
rescue Net::SSH::Exception
- raise Puppet::Error, "SSH connection failure to #{host}"
+ raise Puppet::Error, "SSH connection failure to #{host}", $!.backtrace
end
@buf = ""
@eof = false
@channel = nil
@ssh.open_channel do |channel|
channel.request_pty { |ch,success| raise "failed to open pty" unless success }
channel.send_channel_request("shell") do |ch, success|
raise "failed to open ssh shell channel" unless success
ch.on_data { |ch,data| @buf << data }
ch.on_extended_data { |ch,type,data| @buf << data if type == 1 }
ch.on_close { @eof = true }
@channel = ch
expect(default_prompt, &block)
# this is a little bit unorthodox, we're trying to escape
# the ssh loop there while still having the ssh connection up
# otherwise we wouldn't be able to return ssh stdout/stderr
# for a given call of command.
return
end
end
@ssh.loop
end
def close
@channel.close if @channel
@channel = nil
@ssh.close if @ssh
end
def expect(prompt)
line = ''
sock = @ssh.transport.socket
while not @eof
break if line =~ prompt and @buf == ''
break if sock.closed?
IO::select([sock], [sock], nil, nil)
process_ssh
# at this point we have accumulated some data in @buf
# or the channel has been closed
if @buf != ""
line += @buf.gsub(/\r\n/no, "\n")
@buf = ''
yield line if block_given?
elsif @eof
# channel has been closed
break if line =~ prompt
if line == ''
line = nil
yield nil if block_given?
end
break
end
end
Puppet.debug("ssh: expected #{line}") if @verbose
line
end
def send(line)
Puppet.debug("ssh: send #{line}") if @verbose
@channel.send_data(line + "\n")
end
def process_ssh
while @buf == "" and not eof?
begin
@channel.connection.process(0.1)
rescue IOError
@eof = true
end
end
end
end
diff --git a/lib/puppet/util/plugins.rb b/lib/puppet/util/plugins.rb
index dde496d35..c39f6b138 100644
--- a/lib/puppet/util/plugins.rb
+++ b/lib/puppet/util/plugins.rb
@@ -1,82 +1,82 @@
#
# This system manages an extensible set of metadata about plugins which it
# collects by searching for files named "plugin_init.rb" in a series of
# directories. Initially, these are simply the $LOAD_PATH.
#
# The contents of each file found is executed in the context of a Puppet::Plugins
# object (and thus scoped). An example file might contain:
#
# -------------------------------------------------------
# @name = "Greet the CA"
#
# @description = %q{
# This plugin causes a friendly greeting to print out on a master
# that is operating as the CA, after it has been set up but before
# it does anything.
# }
#
# def after_application_setup(options)
# if options[:application_object].is_a?(Puppet::Application::Master) && Puppet::SSL::CertificateAuthority.ca?
# puts "Hey, this is the CA!"
# end
# end
# -------------------------------------------------------
#
# Note that the instance variables are local to this Puppet::Plugin (and so may be used
# for maintaining state, etc.) but the plugin system does not provide any thread safety
# assurances, so they may not be adequate for some complex use cases.
module Puppet
class Plugins
Paths = [] # Where we might find plugin initialization code
Loaded = [] # Code we have found (one-to-one with paths once searched)
#
# Return all the Puppet::Plugins we know about, searching any new paths
#
def self.known
Paths[Loaded.length...Paths.length].each { |path|
file = File.join(path,'plugin_init.rb')
- Loaded << (Puppet::FileSystem::File.exist?(file) && new(file))
+ Loaded << (Puppet::FileSystem.exist?(file) && new(file))
}
Loaded.compact
end
#
# Add more places to look for plugins without adding duplicates or changing the
# order of ones we've already found.
#
def self.look_in(*paths)
Paths.replace Paths | paths.flatten.collect { |path| File.expand_path(path) }
end
#
# Initially just look in $LOAD_PATH
#
look_in $LOAD_PATH
#
# Calling methods (hooks) on the class calls the method of the same name on
# all plugins that use that hook, passing in the same arguments to each
# and returning an array containing the results returned by each plugin as
# an array of [plugin_name,result] pairs.
#
def self.method_missing(hook,*args,&block)
known.
select { |p| p.respond_to? hook }.
collect { |p| [p.name,p.send(hook,*args,&block)] }
end
#
#
#
attr_reader :path,:name
def initialize(path)
@name = @path = path
class << self
private
def define_hooks
eval File.read(path),nil,path,1
end
end
define_hooks
end
end
end
diff --git a/lib/puppet/util/profiler.rb b/lib/puppet/util/profiler.rb
index c8d1762f5..4246181b4 100644
--- a/lib/puppet/util/profiler.rb
+++ b/lib/puppet/util/profiler.rb
@@ -1,33 +1,45 @@
require 'benchmark'
# A simple profiling callback system.
#
-# @api private
+# @api public
module Puppet::Util::Profiler
require 'puppet/util/profiler/wall_clock'
require 'puppet/util/profiler/object_counts'
require 'puppet/util/profiler/none'
NONE = Puppet::Util::Profiler::None.new
# Reset the profiling system to the original state
+ #
+ # @api private
def self.clear
@profiler = nil
end
# @return This thread's configured profiler
+ # @api private
def self.current
@profiler || NONE
end
# @param profiler [#profile] A profiler for the current thread
+ # @api private
def self.current=(profiler)
@profiler = profiler
end
+ # Profile a block of code and log the time it took to execute.
+ #
+ # This outputs logs entries to the Puppet masters logging destination
+ # providing the time it took, a message describing the profiled code
+ # and a leaf location marking where the profile method was called
+ # in the profiled hierachy.
+ #
# @param message [String] A description of the profiled event
# @param block [Block] The segment of code to profile
+ # @api public
def self.profile(message, &block)
current.profile(message, &block)
end
end
diff --git a/lib/puppet/util/pson.rb b/lib/puppet/util/pson.rb
index 1441069c0..b796cb111 100644
--- a/lib/puppet/util/pson.rb
+++ b/lib/puppet/util/pson.rb
@@ -1,13 +1,13 @@
# A simple module to provide consistency between how we use PSON and how
# ruby expects it to be used. Basically, we don't want to require
# that the sender specify a class.
# Ruby wants everyone to provide a 'type' field, and the PSON support
# requires such a field to track the class down. Because we use our URL to
# figure out what class we're working on, we don't need that, and we don't want
# our consumers and producers to need to know anything about our internals.
module Puppet::Util::Pson
def pson_create(pson)
raise ArgumentError, "No data provided in pson data" unless pson['data']
- from_pson(pson['data'])
+ from_data_hash(pson['data'])
end
end
diff --git a/lib/puppet/util/queue/stomp.rb b/lib/puppet/util/queue/stomp.rb
index 4a7081bc7..4ed91e1e9 100644
--- a/lib/puppet/util/queue/stomp.rb
+++ b/lib/puppet/util/queue/stomp.rb
@@ -1,60 +1,60 @@
require 'puppet/util/queue'
require 'stomp'
require 'uri'
# Implements the Ruby Stomp client as a queue type within the
# Puppet::Indirector::Queue::Client registry, for use with the <tt>:queue</tt>
# indirection terminus type.
#
# Looks to <tt>Puppet[:queue_source]</tt> for the sole argument to the
# underlying Stomp::Client constructor; consequently, for this client to work,
# <tt>Puppet[:queue_source]</tt> must use the Stomp::Client URL-like syntax
# for identifying the Stomp message broker: <em>login:pass@host.port</em>
class Puppet::Util::Queue::Stomp
attr_accessor :stomp_client
def initialize
begin
uri = URI.parse(Puppet[:queue_source])
rescue => detail
- raise ArgumentError, "Could not create Stomp client instance - queue source #{Puppet[:queue_source]} is invalid: #{detail}"
+ raise ArgumentError, "Could not create Stomp client instance - queue source #{Puppet[:queue_source]} is invalid: #{detail}", detail.backtrace
end
unless uri.scheme == "stomp"
raise ArgumentError, "Could not create Stomp client instance - queue source #{Puppet[:queue_source]} is not a Stomp URL: #{detail}"
end
begin
self.stomp_client = Stomp::Client.new(uri.user, uri.password, uri.host, uri.port, true)
rescue => detail
- raise ArgumentError, "Could not create Stomp client instance with queue source #{Puppet[:queue_source]}: got internal Stomp client error #{detail}"
+ raise ArgumentError, "Could not create Stomp client instance with queue source #{Puppet[:queue_source]}: got internal Stomp client error #{detail}", detail.backtrace
end
# Identify the supported method for sending messages.
@method =
case
when stomp_client.respond_to?(:publish)
:publish
when stomp_client.respond_to?(:send)
:send
else
raise ArgumentError, "STOMP client does not respond to either publish or send"
end
end
def publish_message(target, msg)
stomp_client.__send__(@method, stompify_target(target), msg, :persistent => true)
end
def subscribe(target)
stomp_client.subscribe(stompify_target(target), :ack => :client) do |stomp_message|
yield(stomp_message.body)
stomp_client.acknowledge(stomp_message)
end
end
def stompify_target(target)
'/queue/' + target.to_s
end
Puppet::Util::Queue.register_queue_type(self, :stomp)
end
diff --git a/lib/puppet/util/rdoc.rb b/lib/puppet/util/rdoc.rb
index 49784956b..9119784e7 100644
--- a/lib/puppet/util/rdoc.rb
+++ b/lib/puppet/util/rdoc.rb
@@ -1,89 +1,89 @@
require 'puppet/util'
module Puppet::Util::RDoc
module_function
# launch a rdoc documenation process
# with the files/dir passed in +files+
def rdoc(outputdir, files, charset = nil)
Puppet[:ignoreimport] = true
# then rdoc
require 'rdoc/rdoc'
require 'rdoc/options'
# load our parser
require 'puppet/util/rdoc/parser'
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"
)
end
# 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 ]
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
def manifestdoc(files)
Puppet[:ignoreimport] = true
files.select { |f| FileTest.file?(f) }.each do |f|
- parser = Puppet::Parser::Parser.new(Puppet::Node::Environment.new(Puppet[:environment]))
+ parser = Puppet::Parser::Parser.new(Puppet.lookup(:environments).get(Puppet[:environment]))
parser.file = f
ast = parser.parse
output(f, ast)
end
end
# Ouputs to the console the documentation
# of a manifest
def output(file, ast)
astobj = []
ast.instantiate('').each do |resource_type|
astobj << resource_type if resource_type.file == file
end
astobj.sort! {|a,b| a.line <=> b.line }.each do |k|
output_astnode_doc(k)
end
end
def output_astnode_doc(ast)
puts ast.doc if !ast.doc.nil? and !ast.doc.empty?
if Puppet.settings[:document_all]
# scan each underlying resources to produce documentation
code = ast.code.children if ast.code.is_a?(Puppet::Parser::AST::ASTArray)
code ||= ast.code
output_resource_doc(code) unless code.nil?
end
end
def output_resource_doc(code)
code.sort { |a,b| a.line <=> b.line }.each do |stmt|
output_resource_doc(stmt.children) if stmt.is_a?(Puppet::Parser::AST::ASTArray)
if stmt.is_a?(Puppet::Parser::AST::Resource)
puts stmt.doc if !stmt.doc.nil? and !stmt.doc.empty?
end
end
end
end
diff --git a/lib/puppet/util/rdoc/generators/puppet_generator.rb b/lib/puppet/util/rdoc/generators/puppet_generator.rb
index 142124769..2ad15c264 100644
--- a/lib/puppet/util/rdoc/generators/puppet_generator.rb
+++ b/lib/puppet/util/rdoc/generators/puppet_generator.rb
@@ -1,910 +1,910 @@
require 'rdoc/generators/html_generator'
require 'puppet/util/rdoc/code_objects'
require 'digest/md5'
module Generators
# This module holds all the classes needed to generate the HTML documentation
# of a bunch of puppet manifests.
#
# It works by traversing all the code objects defined by the Puppet RDoc::Parser
# and produces HTML counterparts objects that in turns are used by RDoc template engine
# to produce the final HTML.
#
# It is also responsible of creating the whole directory hierarchy, and various index
# files.
#
# It is to be noted that the whole system is built on top of ruby RDoc. As such there
# is an implicit mapping of puppet entities to ruby entitites:
#
# Puppet => Ruby
# ------------------------
# Module Module
# Class Class
# Definition Method
# Resource
# Node
# Plugin
# Fact
MODULE_DIR = "modules"
NODE_DIR = "nodes"
PLUGIN_DIR = "plugins"
# We're monkey patching RDoc markup to allow
# lowercase class1::class2::class3 crossref hyperlinking
module MarkUp
alias :old_markup :markup
def new_markup(str, remove_para=false)
first = @markup.nil?
res = old_markup(str, remove_para)
if first and not @markup.nil?
@markup.add_special(/\b([a-z]\w+(::\w+)*)/,:CROSSREF)
# we need to call it again, since we added a rule
res = old_markup(str, remove_para)
end
res
end
alias :markup :new_markup
end
# This is a specialized HTMLGenerator tailored to Puppet manifests
class PuppetGenerator < HTMLGenerator
def PuppetGenerator.for(options)
AllReferences::reset
HtmlMethod::reset
if options.all_one_file
PuppetGeneratorInOne.new(options)
else
PuppetGenerator.new(options)
end
end
def initialize(options) #:not-new:
@options = options
load_html_template
end
# loads our own html template file
def load_html_template
require 'puppet/util/rdoc/generators/template/puppet/puppet'
extend RDoc::Page
rescue LoadError
$stderr.puts "Could not find Puppet template '#{template}'"
exit 99
end
def gen_method_index
# we don't generate an all define index
# as the presentation is per module/per class
end
# This is the central method, it generates the whole structures
# along with all the indices.
def generate_html
super
gen_into(@nodes)
gen_into(@plugins)
end
##
# Generate:
# the list of modules
# the list of classes and definitions of a specific module
# the list of all classes
# the list of nodes
# the list of resources
def build_indices
@allfiles = []
@nodes = []
@plugins = []
# contains all the seen modules
@modules = {}
@allclasses = {}
# remove unknown toplevels
# it can happen that RDoc triggers a different parser for some files (ie .c, .cc or .h)
# in this case RDoc generates a RDoc::TopLevel which we do not support in this generator
# So let's make sure we don't generate html for those.
@toplevels = @toplevels.select { |tl| tl.is_a? RDoc::PuppetTopLevel }
# build the modules, classes and per modules classes and define list
@toplevels.each do |toplevel|
next unless toplevel.document_self
file = HtmlFile.new(toplevel, @options, FILE_DIR)
classes = []
methods = []
modules = []
nodes = []
# find all classes of this toplevel
# store modules if we find one
toplevel.each_classmodule do |k|
generate_class_list(classes, modules, k, toplevel, CLASS_DIR)
end
# find all defines belonging to this toplevel
HtmlMethod.all_methods.each do |m|
# find parent module, check this method is not already
# defined.
if m.context.parent.toplevel === toplevel
methods << m
end
end
classes.each do |k|
@allclasses[k.index_name] = k if !@allclasses.has_key?(k.index_name)
end
# generate nodes and plugins found
classes.each do |k|
if k.context.is_module?
k.context.each_node do |name,node|
nodes << HTMLPuppetNode.new(node, toplevel, NODE_DIR, @options)
@nodes << nodes.last
end
k.context.each_plugin do |plugin|
@plugins << HTMLPuppetPlugin.new(plugin, toplevel, PLUGIN_DIR, @options)
end
k.context.each_fact do |fact|
@plugins << HTMLPuppetPlugin.new(fact, toplevel, PLUGIN_DIR, @options)
end
end
end
@files << file
@allfiles << { "file" => file, "modules" => modules, "classes" => classes, "methods" => methods, "nodes" => nodes }
end
# scan all classes to create the childs references
@allclasses.values.each do |klass|
if superklass = klass.context.superclass
if superklass = AllReferences[superklass] and (superklass.is_a?(HTMLPuppetClass) or superklass.is_a?(HTMLPuppetNode))
superklass.context.add_child(klass.context)
end
end
end
@classes = @allclasses.values
end
# produce a class/module list of HTMLPuppetModule/HTMLPuppetClass
# based on the code object traversal.
def generate_class_list(classes, modules, from, html_file, class_dir)
if from.is_module? and !@modules.has_key?(from.name)
k = HTMLPuppetModule.new(from, html_file, class_dir, @options)
classes << k
@modules[from.name] = k
modules << @modules[from.name]
elsif from.is_module?
modules << @modules[from.name]
elsif !from.is_module?
k = HTMLPuppetClass.new(from, html_file, class_dir, @options)
classes << k
end
from.each_classmodule do |mod|
generate_class_list(classes, modules, mod, html_file, class_dir)
end
end
# generate all the subdirectories, modules, classes and files
def gen_sub_directories
super
File.makedirs(MODULE_DIR)
File.makedirs(NODE_DIR)
File.makedirs(PLUGIN_DIR)
rescue
$stderr.puts $ERROR_INFO.message
exit 1
end
# generate the index of modules
def gen_file_index
gen_top_index(@modules.values, 'All Modules', RDoc::Page::TOP_INDEX, "fr_modules_index.html")
end
# generate a top index
def gen_top_index(collection, title, template, filename)
template = TemplatePage.new(RDoc::Page::FR_INDEX_BODY, template)
res = []
collection.sort.each do |f|
if f.document_self
res << { "classlist" => CGI.escapeHTML("#{MODULE_DIR}/fr_#{f.index_name}.html"), "module" => CGI.escapeHTML("#{CLASS_DIR}/#{f.index_name}.html"),"name" => CGI.escapeHTML(f.index_name) }
end
end
values = {
"entries" => res,
'list_title' => CGI.escapeHTML(title),
'index_url' => main_url,
'charset' => @options.charset,
'style_url' => style_url('', @options.css),
}
File.open(filename, "w") do |f|
template.write_html_on(f, values)
end
end
# generate the all classes index file and the combo index
def gen_class_index
gen_an_index(@classes, 'All Classes', RDoc::Page::CLASS_INDEX, "fr_class_index.html")
@allfiles.each do |file|
unless file['file'].context.file_relative_name =~ /\.rb$/
gen_composite_index(
file,
RDoc::Page::COMBO_INDEX,
"#{MODULE_DIR}/fr_#{file["file"].context.module_name}.html")
end
end
end
def gen_composite_index(collection, template, filename)\
- return if Puppet::FileSystem::File.exist?(filename)
+ return if Puppet::FileSystem.exist?(filename)
template = TemplatePage.new(RDoc::Page::FR_INDEX_BODY, template)
res1 = []
collection['classes'].sort.each do |f|
if f.document_self
res1 << { "href" => "../"+CGI.escapeHTML(f.path), "name" => CGI.escapeHTML(f.index_name) } unless f.context.is_module?
end
end
res2 = []
collection['methods'].sort.each do |f|
res2 << { "href" => "../#{f.path}", "name" => f.index_name.sub(/\(.*\)$/,'') } if f.document_self
end
module_name = []
res3 = []
res4 = []
collection['modules'].sort.each do |f|
module_name << { "href" => "../"+CGI.escapeHTML(f.path), "name" => CGI.escapeHTML(f.index_name) }
unless f.facts.nil?
f.facts.each do |fact|
res3 << {"href" => "../"+CGI.escapeHTML(AllReferences["PLUGIN(#{fact.name})"].path), "name" => CGI.escapeHTML(fact.name)}
end
end
unless f.plugins.nil?
f.plugins.each do |plugin|
res4 << {"href" => "../"+CGI.escapeHTML(AllReferences["PLUGIN(#{plugin.name})"].path), "name" => CGI.escapeHTML(plugin.name)}
end
end
end
res5 = []
collection['nodes'].sort.each do |f|
res5 << { "href" => "../"+CGI.escapeHTML(f.path), "name" => CGI.escapeHTML(f.name) } if f.document_self
end
values = {
"module" => module_name,
"classes" => res1,
'classes_title' => CGI.escapeHTML("Classes"),
'defines_title' => CGI.escapeHTML("Defines"),
'facts_title' => CGI.escapeHTML("Custom Facts"),
'plugins_title' => CGI.escapeHTML("Plugins"),
'nodes_title' => CGI.escapeHTML("Nodes"),
'index_url' => main_url,
'charset' => @options.charset,
'style_url' => style_url('', @options.css),
}
values["defines"] = res2 if res2.size>0
values["facts"] = res3 if res3.size>0
values["plugins"] = res4 if res4.size>0
values["nodes"] = res5 if res5.size>0
File.open(filename, "w") do |f|
template.write_html_on(f, values)
end
end
# returns the initial_page url
def main_url
main_page = @options.main_page
ref = nil
if main_page
ref = AllReferences[main_page]
if ref
ref = ref.path
else
$stderr.puts "Could not find main page #{main_page}"
end
end
unless ref
for file in @files
if file.document_self and file.context.global
ref = CGI.escapeHTML("#{CLASS_DIR}/#{file.context.module_name}.html")
break
end
end
end
unless ref
for file in @files
if file.document_self and !file.context.global
ref = CGI.escapeHTML("#{CLASS_DIR}/#{file.context.module_name}.html")
break
end
end
end
unless ref
$stderr.puts "Couldn't find anything to document"
$stderr.puts "Perhaps you've used :stopdoc: in all classes"
exit(1)
end
ref
end
end
# This module is used to generate a referenced full name list of ContextUser
module ReferencedListBuilder
def build_referenced_list(list)
res = []
list.each do |i|
ref = AllReferences[i.name] || @context.find_symbol(i.name)
ref = ref.viewer if ref and ref.respond_to?(:viewer)
name = i.respond_to?(:full_name) ? i.full_name : i.name
h_name = CGI.escapeHTML(name)
if ref and ref.document_self
path = url(ref.path)
res << { "name" => h_name, "aref" => path }
else
res << { "name" => h_name }
end
end
res
end
end
# This module is used to hold/generate a list of puppet resources
# this is used in HTMLPuppetClass and HTMLPuppetNode
module ResourceContainer
def collect_resources
list = @context.resource_list
@resources = list.collect {|m| HTMLPuppetResource.new(m, self, @options) }
end
def build_resource_summary_list(path_prefix='')
collect_resources unless @resources
resources = @resources.sort
res = []
resources.each do |r|
res << {
"name" => CGI.escapeHTML(r.name),
"aref" => CGI.escape(path_prefix)+"\#"+CGI.escape(r.aref)
}
end
res
end
def build_resource_detail_list(section)
outer = []
resources = @resources.sort
resources.each do |r|
row = {}
if r.section == section and r.document_self
row["name"] = CGI.escapeHTML(r.name)
desc = r.description.strip
row["m_desc"] = desc unless desc.empty?
row["aref"] = r.aref
row["params"] = r.params
outer << row
end
end
outer
end
end
class HTMLPuppetClass < HtmlClass
include ResourceContainer, ReferencedListBuilder
def value_hash
super
rl = build_resource_summary_list
@values["resources"] = rl unless rl.empty?
@context.sections.each do |section|
secdata = @values["sections"].select { |secdata| secdata["secsequence"] == section.sequence }
if secdata.size == 1
secdata = secdata[0]
rdl = build_resource_detail_list(section)
secdata["resource_list"] = rdl unless rdl.empty?
end
end
rl = build_require_list(@context)
@values["requires"] = rl unless rl.empty?
rl = build_realize_list(@context)
@values["realizes"] = rl unless rl.empty?
cl = build_child_list(@context)
@values["childs"] = cl unless cl.empty?
@values
end
def build_require_list(context)
build_referenced_list(context.requires)
end
def build_realize_list(context)
build_referenced_list(context.realizes)
end
def build_child_list(context)
build_referenced_list(context.childs)
end
end
class HTMLPuppetNode < ContextUser
include ResourceContainer, ReferencedListBuilder
attr_reader :path
def initialize(context, html_file, prefix, options)
super(context, options)
@html_file = html_file
@is_module = context.is_module?
@values = {}
context.viewer = self
if options.all_one_file
@path = context.full_name
else
@path = http_url(context.full_name, prefix)
end
AllReferences.add("NODE(#{@context.full_name})", self)
end
def name
@context.name
end
# return the relative file name to store this class in,
# which is also its url
def http_url(full_name, prefix)
path = full_name.dup
path.gsub!(/<<\s*(\w*)/) { "from-#$1" } if path['<<']
File.join(prefix, path.split("::").collect { |p| Digest::MD5.hexdigest(p) }) + ".html"
end
def parent_name
@context.parent.full_name
end
def index_name
name
end
def write_on(f)
value_hash
template = TemplatePage.new(
RDoc::Page::BODYINC,
RDoc::Page::NODE_PAGE,
RDoc::Page::METHOD_LIST)
template.write_html_on(f, @values)
end
def value_hash
class_attribute_values
add_table_of_sections
@values["charset"] = @options.charset
@values["style_url"] = style_url(path, @options.css)
d = markup(@context.comment)
@values["description"] = d unless d.empty?
ml = build_method_summary_list
@values["methods"] = ml unless ml.empty?
rl = build_resource_summary_list
@values["resources"] = rl unless rl.empty?
il = build_include_list(@context)
@values["includes"] = il unless il.empty?
rl = build_require_list(@context)
@values["requires"] = rl unless rl.empty?
rl = build_realize_list(@context)
@values["realizes"] = rl unless rl.empty?
cl = build_child_list(@context)
@values["childs"] = cl unless cl.empty?
@values["sections"] = @context.sections.map do |section|
secdata = {
"sectitle" => section.title,
"secsequence" => section.sequence,
"seccomment" => markup(section.comment)
}
al = build_alias_summary_list(section)
secdata["aliases"] = al unless al.empty?
co = build_constants_summary_list(section)
secdata["constants"] = co unless co.empty?
al = build_attribute_list(section)
secdata["attributes"] = al unless al.empty?
cl = build_class_list(0, @context, section)
secdata["classlist"] = cl unless cl.empty?
mdl = build_method_detail_list(section)
secdata["method_list"] = mdl unless mdl.empty?
rdl = build_resource_detail_list(section)
secdata["resource_list"] = rdl unless rdl.empty?
secdata
end
@values
end
def build_attribute_list(section)
atts = @context.attributes.sort
res = []
atts.each do |att|
next unless att.section == section
if att.visibility == :public || att.visibility == :protected || @options.show_all
entry = {
"name" => CGI.escapeHTML(att.name),
"rw" => att.rw,
"a_desc" => markup(att.comment, true)
}
unless att.visibility == :public || att.visibility == :protected
entry["rw"] << "-"
end
res << entry
end
end
res
end
def class_attribute_values
h_name = CGI.escapeHTML(name)
@values["classmod"] = "Node"
@values["title"] = CGI.escapeHTML("#{@values['classmod']}: #{h_name}")
c = @context
c = c.parent while c and !c.diagram
@values["diagram"] = diagram_reference(c.diagram) if c && c.diagram
@values["full_name"] = h_name
parent_class = @context.superclass
if parent_class
@values["parent"] = CGI.escapeHTML(parent_class)
if parent_name
lookup = parent_name + "::#{parent_class}"
else
lookup = parent_class
end
lookup = "NODE(#{lookup})"
parent_url = AllReferences[lookup] || AllReferences[parent_class]
@values["par_url"] = aref_to(parent_url.path) if parent_url and parent_url.document_self
end
files = []
@context.in_files.each do |f|
res = {}
full_path = CGI.escapeHTML(f.file_absolute_name)
res["full_path"] = full_path
res["full_path_url"] = aref_to(f.viewer.path) if f.document_self
res["cvsurl"] = cvs_url( @options.webcvs, full_path ) if @options.webcvs
files << res
end
@values['infiles'] = files
end
def build_require_list(context)
build_referenced_list(context.requires)
end
def build_realize_list(context)
build_referenced_list(context.realizes)
end
def build_child_list(context)
build_referenced_list(context.childs)
end
def <=>(other)
self.name <=> other.name
end
end
class HTMLPuppetModule < HtmlClass
def initialize(context, html_file, prefix, options)
super(context, html_file, prefix, options)
end
def value_hash
@values = super
fl = build_facts_summary_list
@values["facts"] = fl unless fl.empty?
pl = build_plugins_summary_list
@values["plugins"] = pl unless pl.empty?
nl = build_nodes_list(0, @context)
@values["nodelist"] = nl unless nl.empty?
@values
end
def build_nodes_list(level, context)
res = ""
prefix = "&nbsp;&nbsp;::" * level;
context.nodes.sort.each do |node|
if node.document_self
res <<
prefix <<
"Node " <<
href(url(node.viewer.path), "link", node.full_name) <<
"<br />\n"
end
end
res
end
def build_facts_summary_list
potentially_referenced_list(context.facts) {|fn| ["PLUGIN(#{fn})"] }
end
def build_plugins_summary_list
potentially_referenced_list(context.plugins) {|fn| ["PLUGIN(#{fn})"] }
end
def facts
@context.facts
end
def plugins
@context.plugins
end
end
class HTMLPuppetPlugin < ContextUser
attr_reader :path
def initialize(context, html_file, prefix, options)
super(context, options)
@html_file = html_file
@is_module = false
@values = {}
context.viewer = self
if options.all_one_file
@path = context.full_name
else
@path = http_url(context.full_name, prefix)
end
AllReferences.add("PLUGIN(#{@context.full_name})", self)
end
def name
@context.name
end
# return the relative file name to store this class in,
# which is also its url
def http_url(full_name, prefix)
path = full_name.dup
path.gsub!(/<<\s*(\w*)/) { "from-#$1" } if path['<<']
File.join(prefix, path.split("::")) + ".html"
end
def parent_name
@context.parent.full_name
end
def index_name
name
end
def write_on(f)
value_hash
template = TemplatePage.new(
RDoc::Page::BODYINC,
RDoc::Page::PLUGIN_PAGE,
RDoc::Page::PLUGIN_LIST)
template.write_html_on(f, @values)
end
def value_hash
attribute_values
add_table_of_sections
@values["charset"] = @options.charset
@values["style_url"] = style_url(path, @options.css)
d = markup(@context.comment)
@values["description"] = d unless d.empty?
if context.is_fact?
unless context.confine.empty?
res = {}
res["type"] = context.confine[:type]
res["value"] = context.confine[:value]
@values["confine"] = [res]
end
else
@values["type"] = context.type
end
@values["sections"] = @context.sections.map do |section|
secdata = {
"sectitle" => section.title,
"secsequence" => section.sequence,
"seccomment" => markup(section.comment)
}
secdata
end
@values
end
def attribute_values
h_name = CGI.escapeHTML(name)
if @context.is_fact?
@values["classmod"] = "Fact"
else
@values["classmod"] = "Plugin"
end
@values["title"] = "#{@values['classmod']}: #{h_name}"
@values["full_name"] = h_name
files = []
@context.in_files.each do |f|
res = {}
full_path = CGI.escapeHTML(f.file_absolute_name)
res["full_path"] = full_path
res["full_path_url"] = aref_to(f.viewer.path) if f.document_self
res["cvsurl"] = cvs_url( @options.webcvs, full_path ) if @options.webcvs
files << res
end
@values['infiles'] = files
end
def <=>(other)
self.name <=> other.name
end
end
class HTMLPuppetResource
include MarkUp
attr_reader :context
@@seq = "R000000"
def initialize(context, html_class, options)
@context = context
@html_class = html_class
@options = options
@@seq = @@seq.succ
@seq = @@seq
context.viewer = self
AllReferences.add(name, self)
end
def as_href(from_path)
if @options.all_one_file
"##{path}"
else
HTMLGenerator.gen_url(from_path, path)
end
end
def name
@context.name
end
def section
@context.section
end
def index_name
"#{@context.name}"
end
def params
@context.params
end
def parent_name
if @context.parent.parent
@context.parent.parent.full_name
else
nil
end
end
def aref
@seq
end
def path
if @options.all_one_file
aref
else
@html_class.path + "##{aref}"
end
end
def description
markup(@context.comment)
end
def <=>(other)
@context <=> other.context
end
def document_self
@context.document_self
end
def find_symbol(symbol, method=nil)
res = @context.parent.find_symbol(symbol, method)
res &&= res.viewer
end
end
class PuppetGeneratorInOne < HTMLGeneratorInOne
def gen_method_index
gen_an_index(HtmlMethod.all_methods, 'Defines')
end
end
end
diff --git a/lib/puppet/util/rdoc/parser/puppet_parser_core.rb b/lib/puppet/util/rdoc/parser/puppet_parser_core.rb
index dd7d03caf..baff91e98 100644
--- a/lib/puppet/util/rdoc/parser/puppet_parser_core.rb
+++ b/lib/puppet/util/rdoc/parser/puppet_parser_core.rb
@@ -1,477 +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)
+ environment = Puppet.lookup(:environments).get(Puppet[:environment])
+ known_resource_types = environment.known_resource_types
+ unless 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
+ known_resource_types.add type
end
end
end
- scan_top_level(@top_level)
+ scan_top_level(@top_level, environment)
@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)
+ def split_module(path, environment)
# 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|
+ environment.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|
+ environment.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)
+ def scan_top_level(container, environment)
# 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)
+ name = split_module(@input_file_name, environment)
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)
+ parse_elements(mod, environment.known_resource_types)
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}"
+ raise Puppet::ParseError, "impossible to parse resource in #{stmt.file} at line #{stmt.line}: #{detail}", detail.backtrace
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}"
+ raise Puppet::ParseError, "impossible to parse class '#{name}' in #{klass.file} at line #{klass.line}: #{detail}", detail.backtrace
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}"
+ raise Puppet::ParseError, "impossible to parse node '#{name}' in #{node.file} at line #{node.line}: #{detail}", detail.backtrace
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}"
+ raise Puppet::ParseError, "impossible to parse definition '#{name}' in #{define.file} at line #{define.line}: #{detail}", detail.backtrace
end
# Traverse the AST tree and produce code-objects node
# that contains the documentation
- def parse_elements(container)
+ def parse_elements(container, known_resource_types)
Puppet.debug "rdoc: scanning manifest"
- @known_resource_types.hostclasses.values.sort { |a,b| a.name <=> b.name }.each do |klass|
+ 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|
+ 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|
+ 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/reference.rb b/lib/puppet/util/reference.rb
index 491e39b09..81d3af0d5 100644
--- a/lib/puppet/util/reference.rb
+++ b/lib/puppet/util/reference.rb
@@ -1,124 +1,124 @@
require 'puppet/util/instance_loader'
require 'puppet/util/methodhelper'
require 'fileutils'
# Manage Reference Documentation.
class Puppet::Util::Reference
include Puppet::Util
include Puppet::Util::MethodHelper
include Puppet::Util::Docs
extend Puppet::Util::InstanceLoader
instance_load(:reference, 'puppet/reference')
def self.footer
"\n\n----------------\n\n*This page autogenerated on #{Time.now}*\n"
end
def self.modes
%w{pdf text}
end
def self.newreference(name, options = {}, &block)
ref = self.new(name, options, &block)
instance_hash(:reference)[name.intern] = ref
ref
end
def self.page(*sections)
depth = 4
# Use the minimum depth
sections.each do |name|
section = reference(name) or raise "Could not find section #{name}"
depth = section.depth if section.depth < depth
end
end
def self.pdf(text)
puts "creating pdf"
rst2latex = which('rst2latex') || which('rst2latex.py') ||
raise("Could not find rst2latex")
cmd = %{#{rst2latex} /tmp/puppetdoc.txt > /tmp/puppetdoc.tex}
Puppet::Util.replace_file("/tmp/puppetdoc.txt") {|f| f.puts text }
# 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:
- Puppet::FileSystem::File.unlink('/tmp/puppetdoc.tex') rescue nil
+ Puppet::FileSystem.unlink('/tmp/puppetdoc.tex') rescue nil
output = %x{#{cmd}}
unless $CHILD_STATUS == 0
$stderr.puts "rst2latex failed"
$stderr.puts output
exit(1)
end
$stderr.puts output
# Now convert to pdf
Dir.chdir("/tmp") do
%x{texi2pdf puppetdoc.tex >/dev/null 2>/dev/null}
end
end
def self.references
instance_loader(:reference).loadall
loaded_instances(:reference).sort { |a,b| a.to_s <=> b.to_s }
end
attr_accessor :page, :depth, :header, :title, :dynamic
attr_writer :doc
def doc
if defined?(@doc)
return "#{@name} - #{@doc}"
else
return @title
end
end
def dynamic?
self.dynamic
end
def initialize(name, options = {}, &block)
@name = name
set_options(options)
meta_def(:generate, &block)
# Now handle the defaults
@title ||= "#{@name.to_s.capitalize} Reference"
@page ||= @title.gsub(/\s+/, '')
@depth ||= 2
@header ||= ""
end
# Indent every line in the chunk except those which begin with '..'.
def indent(text, tab)
text.gsub(/(^|\A)/, tab).gsub(/^ +\.\./, "..")
end
def option(name, value)
":#{name.to_s.capitalize}: #{value}\n"
end
def text
puts output
end
def to_markdown(withcontents = true)
# First the header
text = markdown_header(@title, 1)
text << "\n\n**This page is autogenerated; any changes will get overwritten** *(last generated on #{Time.now.to_s})*\n\n"
text << @header
text << generate
text << self.class.footer if withcontents
text
end
end
diff --git a/lib/puppet/util/resource_template.rb b/lib/puppet/util/resource_template.rb
index bed585b21..d401c4b55 100644
--- a/lib/puppet/util/resource_template.rb
+++ b/lib/puppet/util/resource_template.rb
@@ -1,61 +1,61 @@
require 'puppet/util'
require 'puppet/util/logging'
require 'erb'
# A template wrapper that evaluates a template in the
# context of a resource, allowing the resource attributes
# to be looked up from within the template.
# This provides functionality essentially equivalent to
# the language's template() function. You pass your file
# path and the resource you want to use into the initialization
# method, then call result on the instance, and you get back
# a chunk of text.
# The resource's parameters are available as instance variables
# (as opposed to the language, where we use a method_missing trick).
# For example, say you have a resource that generates a file. You would
# need to implement the following style of `generate` method:
#
# def generate
# template = Puppet::Util::ResourceTemplate.new("/path/to/template", self)
#
# return Puppet::Type.type(:file).new :path => "/my/file",
# :content => template.evaluate
# end
#
# This generated file gets added to the catalog (which is what `generate` does),
# and its content is the result of the template. You need to use instance
# variables in your template, so if your template just needs to have the name
# of the generating resource, it would just have:
#
# <%= @name %>
#
# Since the ResourceTemplate class sets as instance variables all of the resource's
# parameters.
#
# Note that this example uses the generating resource as its source of
# parameters, which is generally most useful, since it allows you to configure
# the generated resource via the generating resource.
class Puppet::Util::ResourceTemplate
include Puppet::Util::Logging
def evaluate
set_resource_variables
ERB.new(File.read(@file), 0, "-").result(binding)
end
def initialize(file, resource)
- raise ArgumentError, "Template #{file} does not exist" unless Puppet::FileSystem::File.exist?(file)
+ raise ArgumentError, "Template #{file} does not exist" unless Puppet::FileSystem.exist?(file)
@file = file
@resource = resource
end
private
def set_resource_variables
@resource.to_hash.each do |param, value|
var = "@#{param.to_s}"
instance_variable_set(var, value)
end
end
end
diff --git a/lib/puppet/util/retryaction.rb b/lib/puppet/util/retryaction.rb
index bd578c147..a50ceb784 100644
--- a/lib/puppet/util/retryaction.rb
+++ b/lib/puppet/util/retryaction.rb
@@ -1,47 +1,47 @@
module Puppet::Util::RetryAction
class RetryException < Exception; end
class RetryException::NoBlockGiven < RetryException; end
class RetryException::NoRetriesGiven < RetryException;end
class RetryException::RetriesExceeded < RetryException; end
def self.retry_action( parameters = { :retry_exceptions => nil, :retries => nil } )
# Retry actions for a specified amount of time. This method will allow the final
# retry to complete even if that extends beyond the timeout period.
unless block_given?
raise RetryException::NoBlockGiven
end
raise RetryException::NoRetriesGiven if parameters[:retries].nil?
parameters[:retry_exceptions] ||= Hash.new
failures = 0
begin
yield
rescue Exception => e
# If we were giving exceptions to catch,
# catch the excptions we care about and retry.
# All others fail hard
- raise RetryException::RetriesExceeded if parameters[:retries] == 0
+ raise RetryException::RetriesExceeded, "#{parameters[:retries]} exceeded", e.backtrace if parameters[:retries] == 0
if (not parameters[:retry_exceptions].keys.empty?) and parameters[:retry_exceptions].keys.include?(e.class)
Puppet.info("Caught exception #{e.class}:#{e}")
Puppet.info(parameters[:retry_exceptions][e.class])
elsif (not parameters[:retry_exceptions].keys.empty?)
# If the exceptions is not in the list of retry_exceptions re-raise.
raise e
end
failures += 1
parameters[:retries] -= 1
# Increase the amount of time that we sleep after every
# failed retry attempt.
sleep (((2 ** failures) -1) * 0.1)
retry
end
end
end
diff --git a/lib/puppet/util/selinux.rb b/lib/puppet/util/selinux.rb
index 78c3c4dfa..feb5c3266 100644
--- a/lib/puppet/util/selinux.rb
+++ b/lib/puppet/util/selinux.rb
@@ -1,222 +1,222 @@
# Provides utility functions to help interface Puppet to SELinux.
#
# This requires the very new SELinux Ruby bindings. These bindings closely
# mirror the SELinux C library interface.
#
# Support for the command line tools is not provided because the performance
# was abysmal. At this time (2008-11-02) the only distribution providing
# these Ruby SELinux bindings which I am aware of is Fedora (in libselinux-ruby).
Puppet.features.selinux? # check, but continue even if it's not
require 'pathname'
module Puppet::Util::SELinux
def selinux_support?
return false unless defined?(Selinux)
if Selinux.is_selinux_enabled == 1
return true
end
false
end
# Retrieve and return the full context of the file. If we don't have
# SELinux support or if the SELinux call fails then return nil.
def get_selinux_current_context(file)
return nil unless selinux_support?
retval = Selinux.lgetfilecon(file)
if retval == -1
return nil
end
retval[1]
end
# Retrieve and return the default context of the file. If we don't have
# SELinux support or if the SELinux call fails to file a default then return nil.
def get_selinux_default_context(file)
return nil unless selinux_support?
# If the filesystem has no support for SELinux labels, return a default of nil
# instead of what matchpathcon would return
return nil unless selinux_label_support?(file)
# If the file exists we should pass the mode to matchpathcon for the most specific
# matching. If not, we can pass a mode of 0.
begin
filestat = file_lstat(file)
mode = filestat.mode
rescue Errno::EACCES, Errno::ENOENT
mode = 0
end
retval = Selinux.matchpathcon(file, mode)
if retval == -1
return nil
end
retval[1]
end
# Take the full SELinux context returned from the tools and parse it
# out to the three (or four) component parts. Supports :seluser, :selrole,
# :seltype, and on systems with range support, :selrange.
def parse_selinux_context(component, context)
if context.nil? or context == "unlabeled"
return nil
end
unless context =~ /^([a-z0-9_]+):([a-z0-9_]+):([a-zA-Z0-9_]+)(?::([a-zA-Z0-9:,._-]+))?/
raise Puppet::Error, "Invalid context to parse: #{context}"
end
ret = {
:seluser => $1,
:selrole => $2,
:seltype => $3,
:selrange => $4,
}
ret[component]
end
# This updates the actual SELinux label on the file. You can update
# only a single component or update the entire context.
# The caveat is that since setting a partial context makes no sense the
# file has to already exist. Puppet (via the File resource) will always
# just try to set components, even if all values are specified by the manifest.
# I believe that the OS should always provide at least a fall-through context
# though on any well-running system.
def set_selinux_context(file, value, component = false)
return nil unless selinux_support? && selinux_label_support?(file)
if component
# Must first get existing context to replace a single component
context = Selinux.lgetfilecon(file)[1]
if context == -1
# We can't set partial context components when no context exists
# unless/until we can find a way to make Puppet call this method
# once for all selinux file label attributes.
Puppet.warning "Can't set SELinux context on file unless the file already has some kind of context"
return nil
end
context = context.split(':')
case component
when :seluser
context[0] = value
when :selrole
context[1] = value
when :seltype
context[2] = value
when :selrange
context[3] = value
else
raise ArguementError, "set_selinux_context component must be one of :seluser, :selrole, :seltype, or :selrange"
end
context = context.join(':')
else
context = value
end
retval = Selinux.lsetfilecon(file, context)
if retval == 0
return true
else
Puppet.warning "Failed to set SELinux context #{context} on #{file}"
return false
end
end
# Since this call relies on get_selinux_default_context it also needs a
# full non-relative path to the file. Fortunately, that seems to be all
# Puppet uses. This will set the file's SELinux context to the policy's
# default context (if any) if it differs from the context currently on
# the file.
def set_selinux_default_context(file)
new_context = get_selinux_default_context(file)
return nil unless new_context
cur_context = get_selinux_current_context(file)
if new_context != cur_context
set_selinux_context(file, new_context)
return new_context
end
nil
end
########################################################################
# Internal helper methods from here on in, kids. Don't fiddle.
private
# Check filesystem a path resides on for SELinux support against
# whitelist of known-good filesystems.
# Returns true if the filesystem can support SELinux labels and
# false if not.
def selinux_label_support?(file)
fstype = find_fs(file)
return false if fstype.nil?
- filesystems = ['ext2', 'ext3', 'ext4', 'gfs', 'gfs2', 'xfs', 'jfs']
+ filesystems = ['ext2', 'ext3', 'ext4', 'gfs', 'gfs2', 'xfs', 'jfs', 'btrfs']
filesystems.include?(fstype)
end
# Internal helper function to read and parse /proc/mounts
def read_mounts
mounts = ""
begin
if File.method_defined? "read_nonblock"
# If possible we use read_nonblock in a loop rather than read to work-
# a linux kernel bug. See ticket #1963 for details.
mountfh = File.open("/proc/mounts")
mounts += mountfh.read_nonblock(1024) while true
else
# Otherwise we shell out and let cat do it for us
mountfh = IO.popen("/bin/cat /proc/mounts")
mounts = mountfh.read
end
rescue EOFError
# that's expected
rescue
return nil
ensure
mountfh.close if mountfh
end
mntpoint = {}
# Read all entries in /proc/mounts. The second column is the
# mountpoint and the third column is the filesystem type.
# We skip rootfs because it is always mounted at /
mounts.each_line do |line|
params = line.split(' ')
next if params[2] == 'rootfs'
mntpoint[params[1]] = params[2]
end
mntpoint
end
# Internal helper function to return which type of filesystem a given file
# path resides on
def find_fs(path)
return nil unless mounts = read_mounts
# cleanpath eliminates useless parts of the path (like '.', or '..', or
# multiple slashes), without touching the filesystem, and without
# following symbolic links. This gives the right (logical) tree to follow
# while we try and figure out what file-system the target lives on.
path = Pathname(path).cleanpath
unless path.absolute?
raise Puppet::DevError, "got a relative path in SELinux find_fs: #{path}"
end
# Now, walk up the tree until we find a match for that path in the hash.
path.ascend do |segment|
return mounts[segment.to_s] if mounts.has_key?(segment.to_s)
end
# Should never be reached...
return mounts['/']
end
##
# file_lstat is an internal, private method to allow precise stubbing and
# mocking without affecting the rest of the system.
#
# @return [File::Stat] File.lstat result
def file_lstat(path)
- Puppet::FileSystem::File.new(path).lstat
+ Puppet::FileSystem.lstat(path)
end
private :file_lstat
end
diff --git a/lib/puppet/util/storage.rb b/lib/puppet/util/storage.rb
index 9df1cb501..0568243b9 100644
--- a/lib/puppet/util/storage.rb
+++ b/lib/puppet/util/storage.rb
@@ -1,89 +1,89 @@
require 'yaml'
require 'sync'
require 'singleton'
require 'puppet/util/yaml'
# a class for storing state
class Puppet::Util::Storage
include Singleton
include Puppet::Util
def self.state
@@state
end
def initialize
self.class.load
end
# Return a hash that will be stored to disk. It's worth noting
# here that we use the object's full path, not just the name/type
# combination. At the least, this is useful for those non-isomorphic
# types like exec, but it also means that if an object changes locations
# in the configuration it will lose its cache.
def self.cache(object)
if object.is_a?(Symbol)
name = object
else
name = object.to_s
end
@@state[name] ||= {}
end
def self.clear
@@state.clear
end
def self.init
@@state = {}
end
self.init
def self.load
Puppet.settings.use(:main) unless FileTest.directory?(Puppet[:statedir])
filename = Puppet[:statefile]
- unless Puppet::FileSystem::File.exist?(filename)
+ unless Puppet::FileSystem.exist?(filename)
self.init if @@state.nil?
return
end
unless File.file?(filename)
Puppet.warning("Checksumfile #{filename} is not a file, ignoring")
return
end
Puppet::Util.benchmark(:debug, "Loaded state") do
begin
@@state = Puppet::Util::Yaml.load_file(filename)
rescue Puppet::Util::Yaml::YamlLoadError => detail
Puppet.err "Checksumfile #{filename} is corrupt (#{detail}); replacing"
begin
File.rename(filename, filename + ".bad")
rescue
- raise Puppet::Error, "Could not rename corrupt #{filename}; remove manually"
+ raise Puppet::Error, "Could not rename corrupt #{filename}; remove manually", detail.backtrace
end
end
end
unless @@state.is_a?(Hash)
Puppet.err "State got corrupted"
self.init
end
end
def self.stateinspect
@@state.inspect
end
def self.store
Puppet.debug "Storing state"
- Puppet.info "Creating state file #{Puppet[:statefile]}" unless Puppet::FileSystem::File.exist?(Puppet[:statefile])
+ Puppet.info "Creating state file #{Puppet[:statefile]}" unless Puppet::FileSystem.exist?(Puppet[:statefile])
Puppet::Util.benchmark(:debug, "Stored state") do
Puppet::Util::Yaml.dump(@@state, Puppet[:statefile])
end
end
end
diff --git a/lib/puppet/util/symbolic_file_mode.rb b/lib/puppet/util/symbolic_file_mode.rb
index 970dededc..69248190c 100644
--- a/lib/puppet/util/symbolic_file_mode.rb
+++ b/lib/puppet/util/symbolic_file_mode.rb
@@ -1,144 +1,144 @@
require 'puppet/util'
module Puppet
module Util
module SymbolicFileMode
SetUIDBit = ReadBit = 4
SetGIDBit = WriteBit = 2
StickyBit = ExecBit = 1
SymbolicMode = { 'x' => ExecBit, 'w' => WriteBit, 'r' => ReadBit }
SymbolicSpecialToBit = {
't' => { 'u' => StickyBit, 'g' => StickyBit, 'o' => StickyBit },
's' => { 'u' => SetUIDBit, 'g' => SetGIDBit, 'o' => StickyBit }
}
def valid_symbolic_mode?(value)
value = normalize_symbolic_mode(value)
return true if value =~ /^0?[0-7]{1,4}$/
return true if value =~ /^([ugoa]*[-=+][-=+rstwxXugo]*)(,[ugoa]*[-=+][-=+rstwxXugo]*)*$/
return false
end
def normalize_symbolic_mode(value)
return nil if value.nil?
# We need to treat integers as octal numbers.
if value.is_a? Numeric then
return value.to_s(8)
elsif value =~ /^0?[0-7]{1,4}$/ then
return value.to_i(8).to_s(8)
else
return value
end
end
def symbolic_mode_to_int(modification, to_mode = 0, is_a_directory = false)
if modification.nil? or modification == '' then
raise Puppet::Error, "An empty mode string is illegal"
end
if modification =~ /^[0-7]+$/ then return modification.to_i(8) end
if modification =~ /^\d+$/ then
raise Puppet::Error, "Numeric modes must be in octal, not decimal!"
end
fail "non-numeric current mode (#{to_mode.inspect})" unless to_mode.is_a?(Numeric)
original_mode = {
's' => (to_mode & 07000) >> 9,
'u' => (to_mode & 00700) >> 6,
'g' => (to_mode & 00070) >> 3,
'o' => (to_mode & 00007) >> 0,
# Are there any execute bits set in the original mode?
'any x?' => (to_mode & 00111) != 0
}
final_mode = {
's' => original_mode['s'],
'u' => original_mode['u'],
'g' => original_mode['g'],
'o' => original_mode['o'],
}
modification.split(/\s*,\s*/).each do |part|
begin
_, to, dsl = /^([ugoa]*)([-+=].*)$/.match(part).to_a
if dsl.nil? then raise Puppet::Error, 'Missing action' end
to = "a" unless to and to.length > 0
# We want a snapshot of the mode before we start messing with it to
# make actions like 'a-g' atomic. Various parts of the DSL refer to
# the original mode, the final mode, or the current snapshot of the
# mode, for added fun.
snapshot_mode = {}
final_mode.each {|k,v| snapshot_mode[k] = v }
to.gsub('a', 'ugo').split('').uniq.each do |who|
value = snapshot_mode[who]
action = '!'
actions = {
'!' => lambda {|_,_| raise Puppet::Error, 'Missing operation (-, =, or +)' },
'=' => lambda {|m,v| m | v },
'+' => lambda {|m,v| m | v },
'-' => lambda {|m,v| m & ~v },
}
dsl.split('').each do |op|
case op
when /[-+=]/ then
action = op
# Clear all bits, if this is assignment
value = 0 if op == '='
when /[ugo]/ then
value = actions[action].call(value, snapshot_mode[op])
when /[rwx]/ then
value = actions[action].call(value, SymbolicMode[op])
when 'X' then
# Only meaningful in combination with "set" actions.
if action != '+' then
raise Puppet::Error, "X only works with the '+' operator"
end
# As per the BSD manual page, set if this is a directory, or if
# any execute bit is set on the original (unmodified) mode.
# Ignored otherwise; it is "add if", not "add or clear".
if is_a_directory or original_mode['any x?'] then
value = actions[action].call(value, ExecBit)
end
when /[st]/ then
bit = SymbolicSpecialToBit[op][who] or fail "internal error"
final_mode['s'] = actions[action].call(final_mode['s'], bit)
else
raise Puppet::Error, 'Unknown operation'
end
end
# Now, assign back the value.
final_mode[who] = value
end
rescue Puppet::Error => e
if part.inspect != modification.inspect then
rest = " at #{part.inspect}"
else
rest = ''
end
- raise Puppet::Error, "#{e}#{rest} in symbolic mode #{modification.inspect}"
+ raise Puppet::Error, "#{e}#{rest} in symbolic mode #{modification.inspect}", e.backtrace
end
end
result =
final_mode['s'] << 9 |
final_mode['u'] << 6 |
final_mode['g'] << 3 |
final_mode['o'] << 0
return result
end
end
end
end
diff --git a/lib/puppet/util/tag_set.rb b/lib/puppet/util/tag_set.rb
index bd1029040..6f83ff870 100644
--- a/lib/puppet/util/tag_set.rb
+++ b/lib/puppet/util/tag_set.rb
@@ -1,29 +1,41 @@
require 'set'
+require 'puppet/network/format_support'
class Puppet::Util::TagSet < Set
+ include Puppet::Network::FormatSupport
+
def self.from_yaml(yaml)
self.new(YAML.load(yaml))
end
def to_yaml
@hash.keys.to_yaml
end
- def self.from_pson(data)
+ def self.from_data_hash(data)
self.new(data)
end
+ def self.from_pson(data)
+ Puppet.deprecation_warning("from_pson is being removed in favour of from_data_hash.")
+ self.from_data_hash(data)
+ end
+
+ def to_data_hash
+ to_a
+ end
+
def to_pson(*args)
- to_a.to_pson
+ to_data_hash.to_pson
end
# this makes puppet serialize it as an array for backwards
# compatibility
def to_zaml(z)
- to_a.to_zaml(z)
+ to_data_hash.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 2e788279b..031b27f93 100644
--- a/lib/puppet/util/tagging.rb
+++ b/lib/puppet/util/tagging.rb
@@ -1,60 +1,55 @@
require 'puppet/util/tag_set'
module Puppet::Util::Tagging
+ ValidTagRegex = /^\w[-\w:.]*$/
+
# 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 ||= new_tags
- qualified = []
-
- ary.collect { |tag| tag.to_s.downcase }.each do |tag|
- fail(Puppet::ParseError, "Invalid tag #{tag.inspect}") unless valid_tag?(tag)
- qualified << tag if tag.include?("::")
- @tags << tag unless @tags.include?(tag)
+ ary.each do |tag|
+ name = tag.to_s.downcase
+ if name =~ ValidTagRegex
+ @tags << name
+ name.split("::").each do |section|
+ @tags << section
+ end
+ else
+ fail(Puppet::ParseError, "Invalid tag #{name}")
+ end
end
-
- handle_qualified_tags( qualified )
end
# Are we tagged with the provided tag?
def tagged?(*tags)
not ( self.tags & tags.flatten.collect { |t| t.to_s } ).empty?
end
# Return a copy of the tag list, so someone can't ask for our tags
# and then modify them.
def tags
@tags ||= new_tags
@tags.dup
end
def tags=(tags)
@tags = new_tags
return if tags.nil? or tags == ""
tags = tags.strip.split(/\s*,\s*/) if tags.is_a?(String)
tags.each {|t| tag(t) }
end
private
- def handle_qualified_tags(qualified)
- qualified.each do |name|
- name.split("::").each do |tag|
- @tags << tag unless @tags.include?(tag)
- end
- end
- end
-
- ValidTagRegex = /^\w[-\w:.]*$/
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 3e1195700..8396b55ac 100644
--- a/lib/puppet/util/watched_file.rb
+++ b/lib/puppet/util/watched_file.rb
@@ -1,37 +1,37 @@
require 'puppet/util/watcher'
# Monitor a given file for changes on a periodic interval. Changes are detected
# by looking for a change in the file ctime.
class Puppet::Util::WatchedFile
# @!attribute [r] filename
# @return [String] The fully qualified path to the file.
attr_reader :filename
# @param filename [String] The fully qualified path to the file.
# @param file_timeout [Integer] The polling interval for checking for file
# changes. Setting the timeout to a negative value will treat the file as
# always changed. Defaults to `Puppet[:filetimeout]`
def initialize(filename, timer = Puppet::Util::Watcher::Timer.new(Puppet[:filetimeout]))
@filename = filename
@timer = timer
@info = Puppet::Util::Watcher::PeriodicWatcher.new(
Puppet::Util::Watcher::Common.file_ctime_change_watcher(@filename),
timer)
end
# @return [true, false] If the file has changed since it was last checked.
def changed?
@info.changed?
end
# Allow this to be used as the name of the file being watched in various
- # other methods (such as Puppet::FileSystem::File.exist?)
+ # other methods (such as Puppet::FileSystem.exist?)
def to_str
@filename
end
def to_s
"<WatchedFile: filename = #{@filename}, timeout = #{@timer.timeout}>"
end
end
diff --git a/lib/puppet/util/watcher.rb b/lib/puppet/util/watcher.rb
index 547c24c9e..a65788bd5 100644
--- a/lib/puppet/util/watcher.rb
+++ b/lib/puppet/util/watcher.rb
@@ -1,17 +1,17 @@
module Puppet::Util::Watcher
require 'puppet/util/watcher/timer'
require 'puppet/util/watcher/change_watcher'
require 'puppet/util/watcher/periodic_watcher'
module Common
def self.file_ctime_change_watcher(filename)
Puppet::Util::Watcher::ChangeWatcher.watch(lambda do
begin
- Puppet::FileSystem::File.new(filename).stat.ctime
+ Puppet::FileSystem.stat(filename).ctime
rescue Errno::ENOENT, Errno::ENOTDIR
:absent
end
end)
end
end
end
diff --git a/lib/puppet/util/windows/error.rb b/lib/puppet/util/windows/error.rb
index 2893b6a42..c6088fa7a 100644
--- a/lib/puppet/util/windows/error.rb
+++ b/lib/puppet/util/windows/error.rb
@@ -1,16 +1,16 @@
require 'puppet/util/windows'
# represents an error resulting from a Win32 error code
class Puppet::Util::Windows::Error < Puppet::Error
require 'windows/error'
include ::Windows::Error
attr_reader :code
- def initialize(message, code = GetLastError.call)
- super(message + ": #{get_last_error(code)}")
+ def initialize(message, code = GetLastError.call, original = nil)
+ super(message + ": #{get_last_error(code)}", original)
@code = code
end
end
diff --git a/lib/puppet/util/windows/file.rb b/lib/puppet/util/windows/file.rb
index 5f21ff197..15a6ef469 100644
--- a/lib/puppet/util/windows/file.rb
+++ b/lib/puppet/util/windows/file.rb
@@ -1,263 +1,279 @@
require 'puppet/util/windows'
module Puppet::Util::Windows::File
require 'ffi'
require 'windows/api'
def replace_file(target, source)
target_encoded = Puppet::Util::Windows::String.wide_string(target.to_s)
source_encoded = Puppet::Util::Windows::String.wide_string(source.to_s)
flags = 0x1
backup_file = nil
result = API.replace_file(
target_encoded,
source_encoded,
backup_file,
flags,
0,
0
)
return true if result
raise Puppet::Util::Windows::Error.new("ReplaceFile(#{target}, #{source})")
end
module_function :replace_file
MoveFileEx = Windows::API.new('MoveFileExW', 'PPL', 'B')
def move_file_ex(source, target, flags = 0)
result = MoveFileEx.call(Puppet::Util::Windows::String.wide_string(source.to_s),
Puppet::Util::Windows::String.wide_string(target.to_s),
flags)
return true unless result == 0
raise Puppet::Util::Windows::Error.
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
# http://msdn.microsoft.com/en-us/library/windows/desktop/aa365512(v=vs.85).aspx
# BOOL WINAPI ReplaceFile(
# _In_ LPCTSTR lpReplacedFileName,
# _In_ LPCTSTR lpReplacementFileName,
# _In_opt_ LPCTSTR lpBackupFileName,
# _In_ DWORD dwReplaceFlags - 0x1 REPLACEFILE_WRITE_THROUGH,
# 0x2 REPLACEFILE_IGNORE_MERGE_ERRORS,
# 0x4 REPLACEFILE_IGNORE_ACL_ERRORS
# _Reserved_ LPVOID lpExclude,
# _Reserved_ LPVOID lpReserved
# );
attach_function :replace_file, :ReplaceFileW,
[:buffer_in, :buffer_in, :buffer_in, :uint, :uint, :uint], :bool
# 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(Puppet::Util::Windows::String.wide_string(symlink.to_s),
Puppet::Util::Windows::String.wide_string(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(Puppet::Util::Windows::String.wide_string(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(Puppet::Util::Windows::String.wide_string(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}")
+ "DeviceIoControl(#{handle}, #{io_control_code}, " +
+ "#{in_buffer}, #{in_buffer ? in_buffer.size : ''}, " +
+ "#{out_buffer}, #{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(
Puppet::Util::Windows::String.wide_string(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)
+ singleton_class = class << stat; self; end
+ target_path = file_name
+
if symlink?(file_name)
- link_ftype = File.stat(readlink(file_name)).ftype
+ target_path = readlink(file_name)
+ link_ftype = File.stat(target_path).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
+
+ singleton_class.send(:define_method, :mode) do
+ Puppet::Util::Windows::Security.get_mode(target_path)
+ 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)
+
+ singleton_class = class << stat; self; end
+ singleton_class.send(:define_method, :mode) do
+ Puppet::Util::Windows::Security.get_mode(file_name)
+ end
+
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/registry.rb b/lib/puppet/util/windows/registry.rb
index a8feb3dce..13b931ec0 100644
--- a/lib/puppet/util/windows/registry.rb
+++ b/lib/puppet/util/windows/registry.rb
@@ -1,70 +1,70 @@
require 'puppet/util/windows'
module Puppet::Util::Windows
module Registry
# http://msdn.microsoft.com/en-us/library/windows/desktop/aa384129(v=vs.85).aspx
KEY64 = 0x100
KEY32 = 0x200
KEY_READ = 0x20019
KEY_WRITE = 0x20006
KEY_ALL_ACCESS = 0x2003f
def root(name)
Win32::Registry.const_get(name)
rescue NameError
- raise Puppet::Error, "Invalid registry key '#{name}'"
+ raise Puppet::Error, "Invalid registry key '#{name}'", $!.backtrace
end
def open(name, path, mode = KEY_READ | KEY64, &block)
hkey = root(name)
begin
hkey.open(path, mode) do |subkey|
return yield subkey
end
rescue Win32::Registry::Error => error
- raise Puppet::Util::Windows::Error.new("Failed to open registry key '#{hkey.keyname}\\#{path}'", error.code)
+ raise Puppet::Util::Windows::Error.new("Failed to open registry key '#{hkey.keyname}\\#{path}'", error.code, error)
end
end
def values(subkey)
values = {}
subkey.each_value do |name, type, data|
case type
when Win32::Registry::REG_MULTI_SZ
data.each { |str| force_encoding(str) }
when Win32::Registry::REG_SZ, Win32::Registry::REG_EXPAND_SZ
force_encoding(data)
end
values[name] = data
end
values
end
if defined?(Encoding)
def force_encoding(str)
if @encoding.nil?
# See https://bugs.ruby-lang.org/issues/8943
# Ruby uses ANSI versions of Win32 APIs to read values from the
# registry. The encoding of these strings depends on the active
# code page. However, ruby incorrectly sets the string
# encoding to US-ASCII. So we must force the encoding to the
# correct value.
require 'windows/national'
begin
cp = Windows::National::GetACP.call
@encoding = Encoding.const_get("CP#{cp}")
rescue
@encoding = Encoding.default_external
end
end
str.force_encoding(@encoding)
end
else
def force_encoding(str, enc)
end
end
private :force_encoding
end
end
diff --git a/lib/puppet/vendor/safe_yaml/CHANGES.md b/lib/puppet/vendor/safe_yaml/CHANGES.md
index 1efd80cba..4dff37d5c 100644
--- a/lib/puppet/vendor/safe_yaml/CHANGES.md
+++ b/lib/puppet/vendor/safe_yaml/CHANGES.md
@@ -1,104 +1,104 @@
0.9.2
-----
- fixed error w/ parsing "!" when whitelisting tags
- fixed parsing of the number 0 (d'oh!)
0.9.1
-----
- added Yecht support (JRuby)
- more bug fixes
0.9.0
-----
- added `whitelist!` method for easily whitelisting tags
- added support for call-specific options
- removed deprecated methods
0.8.6
-----
- fixed bug in float matcher
0.8.5
-----
- performance improvements
- made less verbose by default
- bug fixes
0.8.4
-----
- enhancements to parsing of integers, floats, and dates
- updated built-in whitelist
- more bug fixes
0.8.3
-----
- fixed exception on parsing empty document
- fixed handling of octal & hexadecimal numbers
0.8.2
-----
- bug fixes
0.8.1
-----
- added `:raise_on_unknown_tag` option
- renamed `reset_defaults!` to `restore_defaults!`
0.8
---
- added tag whitelisting
- more API changes
0.7
---
- separated YAML engine support from Ruby version
- added support for binary scalars
- numerous bug fixes and enhancements
0.6
---
- several API changes
- added `SafeYAML::OPTIONS` for specifying default behavior
0.5
---
Added support for dates
0.4
---
- efficiency improvements
- made `YAML.load` use `YAML.safe_load` by default
- made symbol deserialization optional
0.3
---
Added Syck support
0.2
---
Added support for:
- anchors & aliases
- booleans
- nils
0.1
---
-Initial release
\ No newline at end of file
+Initial release
diff --git a/lib/puppetx.rb b/lib/puppetx.rb
index b5f30fc69..ad779add1 100644
--- a/lib/puppetx.rb
+++ b/lib/puppetx.rb
@@ -1,109 +1,89 @@
# The Puppet Extensions Module.
#
# Submodules of this module should be named after the publisher (e.g. 'user' part of a Puppet Module name).
# The submodule {Puppetx::Puppet} contains the puppet extension points.
#
# This module also contains constants that are used when defining extensions.
#
# @api public
#
module Puppetx
# The lookup **key** for the multibind containing syntax checkers used to syntax check embedded string in non
# puppet DSL syntax.
# @api public
SYNTAX_CHECKERS = 'puppetx::puppet::syntaxcheckers'
# The lookup **type** for the multibind containing syntax checkers used to syntax check embedded string in non
# puppet DSL syntax.
# @api public
SYNTAX_CHECKERS_TYPE = 'Puppetx::Puppet::SyntaxChecker'
# The lookup **key** for the multibind containing a map from scheme name to scheme handler class for bindings schemes.
# @api public
BINDINGS_SCHEMES = 'puppetx::puppet::bindings::schemes'
# The lookup **type** for the multibind containing a map from scheme name to scheme handler class for bindings schemes.
# @api public
BINDINGS_SCHEMES_TYPE = 'Puppetx::Puppet::BindingsSchemeHandler'
- # The lookup **key** for the multibind containing a map from hiera-2 backend name to class implementing the backend.
- # @api public
- HIERA2_BACKENDS = 'puppetx::puppet::hiera2::backends'
-
- # The lookup **type** for the multibind containing a map from hiera-2 backend name to class implementing the backend.
- # @api public
- HIERA2_BACKENDS_TYPE = 'Puppetx::Puppet::Hiera2Backend'
-
# This module is the name space for extension points
# @api public
module Puppet
if ::Puppet[:binder] || ::Puppet[:parser] == 'future'
# Extension-points are registered here:
# - If in a Ruby submodule it is best to create it here
# - The class does not have to be required; it will be auto required when the binder
# needs it.
# - If the extension is a multibind, it can be registered here; either with a required
# class or a class reference in string form.
# Register extension points
# -------------------------
system_bindings = ::Puppet::Pops::Binder::SystemBindings
extensions = system_bindings.extensions()
extensions.multibind(SYNTAX_CHECKERS).name(SYNTAX_CHECKERS).hash_of(SYNTAX_CHECKERS_TYPE)
extensions.multibind(BINDINGS_SCHEMES).name(BINDINGS_SCHEMES).hash_of(BINDINGS_SCHEMES_TYPE)
- extensions.multibind(HIERA2_BACKENDS).name(HIERA2_BACKENDS).hash_of(HIERA2_BACKENDS_TYPE)
# Register injector boot bindings
# -------------------------------
boot_bindings = system_bindings.injector_boot_bindings()
# Register the default bindings scheme handlers
require 'puppetx/puppet/bindings_scheme_handler'
{ 'module' => 'ModuleScheme',
'confdir' => 'ConfdirScheme',
- 'module-hiera' => 'ModuleHieraScheme',
- 'confdir-hiera' => 'ConfdirHieraScheme'
}.each do |scheme, class_name|
boot_bindings.bind.name(scheme).instance_of(BINDINGS_SCHEMES_TYPE).in_multibind(BINDINGS_SCHEMES).
to_instance("Puppet::Pops::Binder::SchemeHandler::#{class_name}")
end
-
- # Register the default hiera2 backends
- require 'puppetx/puppet/hiera2_backend'
- { 'json' => 'JsonBackend',
- 'yaml' => 'YamlBackend'
- }.each do |symbolic, class_name|
- boot_bindings.bind.name(symbolic).instance_of(HIERA2_BACKENDS_TYPE).in_multibind(HIERA2_BACKENDS).
- to_instance("Puppet::Pops::Binder::Hiera2::#{class_name}")
- end
end
end
# Module with implementations of various extensions
# @api public
module Puppetlabs
# Default extensions delivered in Puppet Core are included here
# @api public
module SyntaxCheckers
if ::Puppet[:binder] || ::Puppet[:parser] == 'future'
# Classes in this name-space are lazily loaded as they may be overridden and/or never used
# (Lazy loading is done by binding to the name of a class instead of a Class instance).
# Register extensions
# -------------------
system_bindings = ::Puppet::Pops::Binder::SystemBindings
bindings = system_bindings.default_bindings()
bindings.bind do
name('json')
instance_of(SYNTAX_CHECKERS_TYPE)
in_multibind(SYNTAX_CHECKERS)
to_instance('Puppetx::Puppetlabs::SyntaxCheckers::Json')
end
end
end
end
-end
\ No newline at end of file
+end
diff --git a/lib/puppetx/puppet/hiera2_backend.rb b/lib/puppetx/puppet/hiera2_backend.rb
deleted file mode 100644
index d4985d602..000000000
--- a/lib/puppetx/puppet/hiera2_backend.rb
+++ /dev/null
@@ -1,31 +0,0 @@
-module Puppetx::Puppet
-
- # Hiera2Backend is a Puppet Extension Point for the purpose of extending Puppet with a hiera data compatible
- # backend. The intended use is to create a class derived from this class and then register it with the
- # Puppet Binder under a backend name in the `binder_config.yaml` file to map symbolic name to class name.
- #
- # The responsibility of a Hiera2 backend is minimal. It should read the given file (with some extesion(s) determined by
- # the backend, and return a hash of the content. If the directory does not exist, or the file does not exist an empty
- # hash should be produced.
- #
- # @abstract
- # @api public
- #
- class Hiera2Backend
- # Produces a hash with data read from the file in the given
- # directory having the given file_name (with extensions appended under the discretion of this
- # backend).
- #
- # Should return an empty hash if the directory or the file does not exist. May raise exception on other types of errors, but
- # not return nil.
- #
- # @param directory [String] the path to the directory containing the file to read
- # @param file_name [String] the file name (without extension) that should be read
- # @return [Hash<String, Object>, Hash<Symbol, Object>] the produced hash with data, may be empty if there was no file
- # @api public
- #
- def read_data(directory, file_name)
- raise NotImplementedError, "The class #{self.class.name} should have implemented the method 'read_data(directory, file_name)'"
- end
- end
-end
diff --git a/lib/puppetx/puppet/syntax_checker.rb b/lib/puppetx/puppet/syntax_checker.rb
index 6baa1479d..d46ac8fc8 100644
--- a/lib/puppetx/puppet/syntax_checker.rb
+++ b/lib/puppetx/puppet/syntax_checker.rb
@@ -1,91 +1,91 @@
module Puppetx::Puppet
# SyntaxChecker is a Puppet Extension Point for the purpose of extending Puppet with syntax checkers.
# The intended use is to create a class derived from this class and then register it with the
# Puppet Binder.
#
# Creating the Extension Class
# ----------------------------
# As an example, a class for checking custom xml (aware of some custom schemes) may be authored in
# say a puppet module called 'exampleorg/xmldata'. The name of the class should start with `Puppetx::<user>::<module>`,
# e.g. 'Puppetx::Exampleorg::XmlData::XmlChecker" and
# be located in `lib/puppetx/exampleorg/xml_data/xml_checker.rb`. The Puppet Binder will auto-load this file when it
# has a binding to the class `Puppetx::Exampleorg::XmlData::XmlChecker'
# The Ruby Module `Puppetx` is created by Puppet, the remaining modules should be created by the loaded logic - e.g.:
#
# @example Defining an XmlChecker
# module Puppetx::Exampleorg
# module XmlData
# class XmlChecker < Puppetx::Puppetlabs::SyntaxCheckers::SyntaxChecker
# def check(text, syntax_identifier, acceptor, location_hash)
# # do the checking
# end
# end
# end
# end
#
# Implementing the check method
# -----------------------------
# The implementation of the {#check} method should naturally perform syntax checking of the given text/string and
# produce found issues on the given `acceptor`. These can be warnings or errors. The method should return `false` if
# any warnings or errors were produced (it is up to the caller to check for error/warning conditions and report them
# to the user).
#
# Issues are reported by calling the given `acceptor`, which takes a severity (e.g. `:error`,
# or `:warning), an {Puppet::Pops::Issues::Issue} instance, and a {Puppet::Pops::Adapters::SourcePosAdapter}
# (which describes details about linenumber, position, and length of the problem area). Note that the
# `location_info` given to the check method holds information about the location of the string in its *container*
# (e.g. the source position of a Heredoc); this information can be used if more detailed information is not
# available, or combined if there are more details (relative to the start of the checked string).
#
# @example Reporting an issue
# # create an issue with a symbolic name (that can serve as a reference to more details about the problem),
# # make the name unique
# issue = Puppet::Pops::Issues::issue(:EXAMPLEORG_XMLDATA_ILLEGAL_XML) { "syntax error found in xml text" }
# source_pos = Puppet::Pops::Adapters::SourcePosAdapter.new()
# source_pos.line = info[:line] # use this if there is no detail from the used parser
# source_pos.pos = info[:pos] # use this pos if there is no detail from used parser
#
# # report it
# acceptor.accept(Puppet::Pops::Validation::Diagnostic.new(:error, issue, info[:file], source_pos, {}))
#
# There is usually a cap on the number of errors/warnings that are presented to the user, this is handled by the
# reporting logic, but care should be taken to not generate too many as the issues are kept in memory until
# the checker returns. The acceptor may set a limit and simply ignore issues past a certain (high) number of reported
# issues (this number is typically higher than the cap on issues reported to the user).
#
# The `syntax_identifier`
# -----------------------
# The extension makes use of a syntax identifier written in mime-style. This identifier can be something simple
# as 'xml', or 'json', but can also consist of several segments joined with '+' where the most specific syntax variant
# is placed first. When searching for a syntax checker; say for JSON having some special traits, say 'userdata', the
# author of the text may indicate this as the text having the syntax "userdata+json" - when a checker is looked up it is
# first checked if there is a checker for "userdata+json", if none is found, a lookup is made for "json" (since the text
# must at least be valid json). The given identifier is passed to the checker (to allow the same checker to check for
# several dialects/specializations).
#
# Use in Puppet DSL
# -----------------
# The Puppet DSL Heredoc support and Puppet Templates makes use of the syntax checker extension. A user of a
# heredoc can specify the syntax in the heredoc tag, e.g.`@(END:userdata+json)`.
#
#
# @abstract
#
class SyntaxChecker
# Checks the text for syntax issues and reports them to the given acceptor.
# This implementation is abstract, it raises {NotImplementedError} since a subclass should have implemented the
# method.
#
# @param text [String] The text to check
# @param syntax_identifier [String] The syntax identifier in mime style (e.g. 'json', 'json-patch+json', 'xml', 'myapp+xml'
# @option location_info [String] :file The filename where the string originates
# @option location_info [Integer] :line The line number identifying the location where the string is being used/checked
# @option location_info [Integer] :position The position on the line identifying the location where the string is being used/checked
# @return [Boolean] Whether the checked string had issues (warnings and/or errors) or not.
# @api public
#
def check(text, syntax_identifier, acceptor, location_info)
raise NotImplementedError, "The class #{self.class.name} should have implemented the method check()"
end
end
-end
\ No newline at end of file
+end
diff --git a/lib/puppetx/puppetlabs/syntax_checkers/json.rb b/lib/puppetx/puppetlabs/syntax_checkers/json.rb
index e31f52688..c12e0e52e 100644
--- a/lib/puppetx/puppetlabs/syntax_checkers/json.rb
+++ b/lib/puppetx/puppetlabs/syntax_checkers/json.rb
@@ -1,39 +1,37 @@
# A syntax checker for JSON.
# @api public
+require 'puppetx/puppet/syntax_checker'
class Puppetx::Puppetlabs::SyntaxCheckers::Json < Puppetx::Puppet::SyntaxChecker
# Checks the text for JSON syntax issues and reports them to the given acceptor.
# This implementation is abstract, it raises {NotImplementedError} since a subclass should have implemented the
# method.
#
+ # Error messages from the checker are capped at 100 chars from the source text.
+ #
# @param text [String] The text to check
# @param syntax [String] The syntax identifier in mime style (e.g. 'json', 'json-patch+json', 'xml', 'myapp+xml'
- # @option location_info [String] :file The filename where the string originates
- # @option location_info [Integer] :line The line number identifying the location where the string is being used/checked
- # @option location_info [Integer] :position The position on the line identifying the location where the string is being used/checked
- # @return [Boolean] Whether the checked string had issues (warnings and/or errors) or not.
+ # @param acceptor [#accept] A Diagnostic acceptor
+ # @param source_pos [Puppet::Pops::Adapters::SourcePosAdapter] A source pos adapter with location information
# @api public
#
- def check(text, syntax, acceptor, location_info={})
+ def check(text, syntax, acceptor, source_pos)
raise ArgumentError.new("Json syntax checker: the text to check must be a String.") unless text.is_a?(String)
raise ArgumentError.new("Json syntax checker: the syntax identifier must be a String, e.g. json, data+json") unless syntax.is_a?(String)
raise ArgumentError.new("Json syntax checker: invalid Acceptor, got: '#{acceptor.class.name}'.") unless acceptor.is_a?(Puppet::Pops::Validation::Acceptor)
- raise ArgumentError.new("Json syntax checker: location_info must be a Hash") unless info.is_a?(Hash)
+ #raise ArgumentError.new("Json syntax checker: location_info must be a Hash") unless location_info.is_a?(Hash)
begin
JSON.parse(text)
rescue => e
# Cap the message to 100 chars and replace newlines
- msg = "Json syntax checker:: Cannot parse invalid JSON string. \"#{e.message().slice(0,100).gsub(/\r?\n/, "\\n")}\""
+ msg = "JSON syntax checker: Cannot parse invalid JSON string. \"#{e.message().slice(0,100).gsub(/\r?\n/, "\\n")}\""
# TODO: improve the pops API to allow simpler diagnostic creation while still maintaining capabilities
# and the issue code. (In this case especially, where there is only a single error message being issued).
#
issue = Puppet::Pops::Issues::issue(:ILLEGAL_JSON) { msg }
- source_pos = Puppet::Pops::Adapters::SourcePosAdapter.new()
- source_pos.line = location_info[:line]
- source_pos.pos = location_info[:pos]
- acceptor.accept(Puppet::Pops::Validation::Diagnostic.new(:error, issue, location_info[:file], source_pos, {}))
+ acceptor.accept(Puppet::Pops::Validation::Diagnostic.new(:error, issue, source_pos.locator.file, source_pos, {}))
end
end
end
diff --git a/spec/fixtures/integration/node/environment/sitedir/00_a.pp b/spec/fixtures/integration/node/environment/sitedir/00_a.pp
new file mode 100644
index 000000000..508992fc3
--- /dev/null
+++ b/spec/fixtures/integration/node/environment/sitedir/00_a.pp
@@ -0,0 +1,2 @@
+class a {}
+$a = 10
\ No newline at end of file
diff --git a/spec/fixtures/integration/node/environment/sitedir/01_b.pp b/spec/fixtures/integration/node/environment/sitedir/01_b.pp
new file mode 100644
index 000000000..25e339b4c
--- /dev/null
+++ b/spec/fixtures/integration/node/environment/sitedir/01_b.pp
@@ -0,0 +1,6 @@
+class b {}
+
+# if the files are evaluated in the wrong order, the file 'b' has a reference
+# to $a (set in file 'a') and with strict variable lookup should raise an error
+# and fail this test.
+$b = $a # error if $a not set in strict mode
diff --git a/spec/fixtures/unit/pops/binder/hiera2/yaml_backend/empty/common.yaml b/spec/fixtures/integration/node/environment/sitedir/03_empty.pp
similarity index 100%
rename from spec/fixtures/unit/pops/binder/hiera2/yaml_backend/empty/common.yaml
rename to spec/fixtures/integration/node/environment/sitedir/03_empty.pp
diff --git a/spec/fixtures/integration/node/environment/sitedir/04_include.pp b/spec/fixtures/integration/node/environment/sitedir/04_include.pp
new file mode 100644
index 000000000..293067a7f
--- /dev/null
+++ b/spec/fixtures/integration/node/environment/sitedir/04_include.pp
@@ -0,0 +1,2 @@
+include a, b
+notify { "variables": message => "a: $a, b: $b" }
diff --git a/spec/fixtures/integration/provider/cron/crontab/purged b/spec/fixtures/integration/provider/cron/crontab/purged
new file mode 100644
index 000000000..b302836b0
--- /dev/null
+++ b/spec/fixtures/integration/provider/cron/crontab/purged
@@ -0,0 +1,8 @@
+# HEADER: some simple
+# HEADER: header
+
+# commend with blankline above and below
+
+
+# Puppet Name: only managed entry
+* * * * * /bin/true
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 f0cbc446b..9d8c12983 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
@@ -1,21 +1,21 @@
Puppet::Type.type(:a2mod).provide(:debian) do
desc "Manage Apache 2 modules on Debian-like OSes (e.g. Ubuntu)"
commands :encmd => "a2enmod"
commands :discmd => "a2dismod"
defaultfor :operatingsystem => [:debian, :ubuntu]
def create
encmd resource[:name]
end
def destroy
discmd resource[:name]
end
def exists?
mod= "/etc/apache2/mods-enabled/" + resource[:name] + ".load"
- Puppet::FileSystem::File.exist?(mod)
+ Puppet::FileSystem.exist?(mod)
end
end
diff --git a/spec/fixtures/unit/pops/binder/bindings_composer/hiera1config/binder_config.yaml b/spec/fixtures/unit/pops/binder/bindings_composer/hiera1config/binder_config.yaml
deleted file mode 100644
index 9088193a4..000000000
--- a/spec/fixtures/unit/pops/binder/bindings_composer/hiera1config/binder_config.yaml
+++ /dev/null
@@ -1,18 +0,0 @@
----
-version: 1
-layers:
- [{name: site, include: 'confdir-hiera:/'},
- {name: modules, include: ['module-hiera:/*/', 'module:/*::default'] }
- ]
-categories:
- [['node', '$fqdn'],
- ['environment', '${environment}'],
- ['osfamily', '${osfamily}'],
- ['common', 'true']
- ]
-#extensions:
-# scheme_handlers:
-# echo: 'Puppetx::Awesome::EchoSchemeHandler'
-#
-# hiera_backends:
-# echo: 'Puppetx::Awesome::EchoBackend'
diff --git a/spec/fixtures/unit/pops/binder/bindings_composer/hiera1config/hiera.yaml b/spec/fixtures/unit/pops/binder/bindings_composer/hiera1config/hiera.yaml
deleted file mode 100644
index 717eacc4b..000000000
--- a/spec/fixtures/unit/pops/binder/bindings_composer/hiera1config/hiera.yaml
+++ /dev/null
@@ -1,8 +0,0 @@
----
-hierarchy:
- - '%fqdn'
- - 'common'
-
-backends:
- - yaml
- - json
diff --git a/spec/fixtures/unit/pops/binder/bindings_composer/hiera1config/modules/good/common.yaml b/spec/fixtures/unit/pops/binder/bindings_composer/hiera1config/modules/good/common.yaml
deleted file mode 100644
index 38494c2d5..000000000
--- a/spec/fixtures/unit/pops/binder/bindings_composer/hiera1config/modules/good/common.yaml
+++ /dev/null
@@ -1 +0,0 @@
-the_meaning_of_life: 300
\ No newline at end of file
diff --git a/spec/fixtures/unit/pops/binder/bindings_composer/hiera1config/modules/good/hiera.yaml b/spec/fixtures/unit/pops/binder/bindings_composer/hiera1config/modules/good/hiera.yaml
deleted file mode 100644
index 5d3ea65a0..000000000
--- a/spec/fixtures/unit/pops/binder/bindings_composer/hiera1config/modules/good/hiera.yaml
+++ /dev/null
@@ -1,10 +0,0 @@
----
-version: 2
-hierarchy:
- [ ['node', '${fqdn}', '${fqdn}' ],
- ['common', 'true', 'common' ]
- ]
-
-backends:
- - yaml
- - json
diff --git a/spec/fixtures/unit/pops/binder/bindings_composer/ok/binder_config.yaml b/spec/fixtures/unit/pops/binder/bindings_composer/ok/binder_config.yaml
index 46148bca6..f7a6f8c4d 100644
--- a/spec/fixtures/unit/pops/binder/bindings_composer/ok/binder_config.yaml
+++ b/spec/fixtures/unit/pops/binder/bindings_composer/ok/binder_config.yaml
@@ -1,19 +1,10 @@
---
version: 1
layers:
- [{name: site, include: 'confdir-hiera:/'},
+ [{name: site, include: 'confdir:/confdirtest'},
{name: test, include: 'echo:/quick/brown/fox'},
- {name: modules, include: ['module-hiera:/*/', 'module:/*::default'], exclude: 'module-hiera:/bad/' }
- ]
-categories:
- [['node', '$fqdn'],
- ['environment', '${environment}'],
- ['osfamily', '${osfamily}'],
- ['common', 'true']
+ {name: modules, include: ['module:/*::default'], exclude: 'module:/bad::default/' }
]
extensions:
scheme_handlers:
- echo: 'Puppetx::Awesome::EchoSchemeHandler'
-
- hiera_backends:
- echo: 'Puppetx::Awesome::EchoBackend'
+ echo: 'Puppetx::Awesome2::EchoSchemeHandler'
diff --git a/spec/fixtures/unit/pops/binder/bindings_composer/ok/common.yaml b/spec/fixtures/unit/pops/binder/bindings_composer/ok/common.yaml
deleted file mode 100644
index e412898ba..000000000
--- a/spec/fixtures/unit/pops/binder/bindings_composer/ok/common.yaml
+++ /dev/null
@@ -1 +0,0 @@
-has_funny_hat: 'the pope'
\ No newline at end of file
diff --git a/spec/fixtures/unit/pops/binder/bindings_composer/ok/hiera.yaml b/spec/fixtures/unit/pops/binder/bindings_composer/ok/hiera.yaml
deleted file mode 100644
index a56580ac6..000000000
--- a/spec/fixtures/unit/pops/binder/bindings_composer/ok/hiera.yaml
+++ /dev/null
@@ -1,11 +0,0 @@
----
-version: 2
-
-hierarchy:
- [ ['node', '${fqdn}', '${fqdn}' ],
- ['common', 'true', 'common' ]
- ]
-
-backends:
- - yaml
- - json
diff --git a/spec/fixtures/unit/pops/binder/bindings_composer/ok/lib/puppet/bindings/confdirtest.rb b/spec/fixtures/unit/pops/binder/bindings_composer/ok/lib/puppet/bindings/confdirtest.rb
new file mode 100644
index 000000000..096d1d938
--- /dev/null
+++ b/spec/fixtures/unit/pops/binder/bindings_composer/ok/lib/puppet/bindings/confdirtest.rb
@@ -0,0 +1,10 @@
+Puppet::Bindings.newbindings('confdirtest') do |scope|
+ bind {
+ name 'has_funny_hat'
+ to 'the pope'
+ }
+ bind {
+ name 'the_meaning_of_life'
+ to 42
+ }
+end
\ No newline at end of file
diff --git a/spec/fixtures/unit/pops/binder/bindings_composer/ok/localhost.yaml b/spec/fixtures/unit/pops/binder/bindings_composer/ok/localhost.yaml
deleted file mode 100644
index 1976ccec8..000000000
--- a/spec/fixtures/unit/pops/binder/bindings_composer/ok/localhost.yaml
+++ /dev/null
@@ -1 +0,0 @@
-the_meaning_of_life: 42
\ No newline at end of file
diff --git a/spec/fixtures/unit/pops/binder/bindings_composer/ok/modules/awesome/common.yaml b/spec/fixtures/unit/pops/binder/bindings_composer/ok/modules/awesome/common.yaml
deleted file mode 100644
index 4bdf31981..000000000
--- a/spec/fixtures/unit/pops/binder/bindings_composer/ok/modules/awesome/common.yaml
+++ /dev/null
@@ -1,3 +0,0 @@
-awesome_x: 'golden'
-the_meaning_of_life: 100
-has_funny_hat: 'kkk'
\ No newline at end of file
diff --git a/spec/fixtures/unit/pops/binder/bindings_composer/ok/modules/awesome/hiera.yaml b/spec/fixtures/unit/pops/binder/bindings_composer/ok/modules/awesome/hiera.yaml
deleted file mode 100644
index f83cb1194..000000000
--- a/spec/fixtures/unit/pops/binder/bindings_composer/ok/modules/awesome/hiera.yaml
+++ /dev/null
@@ -1,13 +0,0 @@
----
-version: 2
-
-hierarchy:
- [ ['node', '${fqdn}', '${fqdn}' ],
-# ['osfamily', '${osfamily}', 'osfamily' ]
- ['common', 'true', 'common' ]
- ]
-
-backends:
- - yaml
- - json
- - echo
diff --git a/spec/fixtures/unit/pops/binder/bindings_composer/ok/modules/awesome/lib/puppet/bindings/awesome/default.rb b/spec/fixtures/unit/pops/binder/bindings_composer/ok/modules/awesome/lib/puppet/bindings/awesome/default.rb
deleted file mode 100644
index 2517202aa..000000000
--- a/spec/fixtures/unit/pops/binder/bindings_composer/ok/modules/awesome/lib/puppet/bindings/awesome/default.rb
+++ /dev/null
@@ -1,4 +0,0 @@
-Puppet::Bindings.newbindings('awesome::default') do |scope|
- bind.name('all your base').to('are belong to us')
- bind.name('env_meaning_of_life').to(puppet_string("$environment thinks it is 42", __FILE__))
-end
\ No newline at end of file
diff --git a/spec/fixtures/unit/pops/binder/bindings_composer/ok/modules/awesome/lib/puppetx/awesome/echo_backend.rb b/spec/fixtures/unit/pops/binder/bindings_composer/ok/modules/awesome/lib/puppetx/awesome/echo_backend.rb
deleted file mode 100644
index 7de067a9e..000000000
--- a/spec/fixtures/unit/pops/binder/bindings_composer/ok/modules/awesome/lib/puppetx/awesome/echo_backend.rb
+++ /dev/null
@@ -1,11 +0,0 @@
-require 'puppetx/puppet/hiera2_backend'
-
-module Puppetx
- module Awesome
- class EchoBackend < Puppetx::Puppet::Hiera2Backend
- def read_data(directory, file_name)
- {"echo::#{file_name}" => "echo... #{File.basename(directory)}/#{file_name}"}
- end
- end
- end
-end
\ No newline at end of file
diff --git a/spec/fixtures/unit/pops/binder/bindings_composer/ok/modules/awesome/localhost.yaml b/spec/fixtures/unit/pops/binder/bindings_composer/ok/modules/awesome/localhost.yaml
deleted file mode 100644
index 01aba4d93..000000000
--- a/spec/fixtures/unit/pops/binder/bindings_composer/ok/modules/awesome/localhost.yaml
+++ /dev/null
@@ -1 +0,0 @@
-good_x: 'golden'
\ No newline at end of file
diff --git a/spec/fixtures/unit/pops/binder/bindings_composer/ok/modules/awesome2/lib/puppet/bindings/awesome2/default.rb b/spec/fixtures/unit/pops/binder/bindings_composer/ok/modules/awesome2/lib/puppet/bindings/awesome2/default.rb
new file mode 100644
index 000000000..eb686a8da
--- /dev/null
+++ b/spec/fixtures/unit/pops/binder/bindings_composer/ok/modules/awesome2/lib/puppet/bindings/awesome2/default.rb
@@ -0,0 +1,20 @@
+Puppet::Bindings.newbindings('awesome2::default') do |scope|
+ bind.name('all your base').to('are belong to us')
+ bind.name('env_meaning_of_life').to(puppet_string("$environment thinks it is 42", __FILE__))
+ bind {
+ name 'awesome_x'
+ to 'golden'
+ }
+ bind {
+ name 'the_meaning_of_life'
+ to 100
+ }
+ bind {
+ name 'has_funny_hat'
+ to 'kkk'
+ }
+ bind {
+ name 'good_x'
+ to 'golden'
+ }
+end
\ No newline at end of file
diff --git a/spec/fixtures/unit/pops/binder/bindings_composer/ok/modules/awesome/lib/puppetx/awesome/echo_scheme_handler.rb b/spec/fixtures/unit/pops/binder/bindings_composer/ok/modules/awesome2/lib/puppetx/awesome2/echo_scheme_handler.rb
similarity index 95%
rename from spec/fixtures/unit/pops/binder/bindings_composer/ok/modules/awesome/lib/puppetx/awesome/echo_scheme_handler.rb
rename to spec/fixtures/unit/pops/binder/bindings_composer/ok/modules/awesome2/lib/puppetx/awesome2/echo_scheme_handler.rb
index 3f97a89ab..8d51ab7ad 100644
--- a/spec/fixtures/unit/pops/binder/bindings_composer/ok/modules/awesome/lib/puppetx/awesome/echo_scheme_handler.rb
+++ b/spec/fixtures/unit/pops/binder/bindings_composer/ok/modules/awesome2/lib/puppetx/awesome2/echo_scheme_handler.rb
@@ -1,18 +1,18 @@
require 'puppetx/puppet/bindings_scheme_handler'
module Puppetx
- module Awesome
+ module Awesome2
# A binding scheme that echos its path
# 'echo:/quick/brown/fox' becomes key '::quick::brown::fox' => 'echo: quick brown fox'.
# (silly class for testing loading of extension)
#
class EchoSchemeHandler < Puppetx::Puppet::BindingsSchemeHandler
def contributed_bindings(uri, scope, composer)
factory = ::Puppet::Pops::Binder::BindingsFactory
bindings = factory.named_bindings("echo")
bindings.bind.name(uri.path.gsub(/\//, '::')).to("echo: #{uri.path.gsub(/\//, ' ').strip!}")
- result = factory.contributed_bindings("echo", bindings.model, nil)
+ result = factory.contributed_bindings("echo", bindings.model) ### , nil)
end
end
end
end
\ No newline at end of file
diff --git a/spec/fixtures/unit/pops/binder/bindings_composer/ok/modules/bad/common.yaml b/spec/fixtures/unit/pops/binder/bindings_composer/ok/modules/bad/common.yaml
deleted file mode 100644
index bcb9f61ce..000000000
--- a/spec/fixtures/unit/pops/binder/bindings_composer/ok/modules/bad/common.yaml
+++ /dev/null
@@ -1,3 +0,0 @@
-bad_x: 'rotten'
-the_meaning_of_life: 200
-has_funny_hat: 'the syldavians'
\ No newline at end of file
diff --git a/spec/fixtures/unit/pops/binder/bindings_composer/ok/modules/bad/hiera_config.yaml b/spec/fixtures/unit/pops/binder/bindings_composer/ok/modules/bad/hiera_config.yaml
deleted file mode 100644
index bb00d74f9..000000000
--- a/spec/fixtures/unit/pops/binder/bindings_composer/ok/modules/bad/hiera_config.yaml
+++ /dev/null
@@ -1,9 +0,0 @@
-# BROKEN ON PURPOSE
----
-hierarchyyyyyy:
- - ['node', '${fqdn}', '${fqdn}'
- - ['common', 'true', 'common'
-
-backendsssssss:
- - yaml
- - json
diff --git a/spec/fixtures/unit/pops/binder/bindings_composer/ok/modules/bad/lib/puppet/bindings/bad/default.rb b/spec/fixtures/unit/pops/binder/bindings_composer/ok/modules/bad/lib/puppet/bindings/bad/default.rb
new file mode 100644
index 000000000..aa9a1fef3
--- /dev/null
+++ b/spec/fixtures/unit/pops/binder/bindings_composer/ok/modules/bad/lib/puppet/bindings/bad/default.rb
@@ -0,0 +1,5 @@
+nil + nil + nil # broken on purpose, this file should never be loaded
+
+Puppet::Bindings.newbindings('bad::default') do |scope|
+ nil + nil + nil # broken on purpose, this should never be evaluated
+end
\ No newline at end of file
diff --git a/spec/fixtures/unit/pops/binder/bindings_composer/ok/modules/good/common.yaml b/spec/fixtures/unit/pops/binder/bindings_composer/ok/modules/good/common.yaml
deleted file mode 100644
index ad5a5d9aa..000000000
--- a/spec/fixtures/unit/pops/binder/bindings_composer/ok/modules/good/common.yaml
+++ /dev/null
@@ -1,2 +0,0 @@
-good_x: 'decent'
-the_meaning_of_life: 300
\ No newline at end of file
diff --git a/spec/fixtures/unit/pops/binder/bindings_composer/ok/modules/good/hiera.yaml b/spec/fixtures/unit/pops/binder/bindings_composer/ok/modules/good/hiera.yaml
deleted file mode 100644
index a56580ac6..000000000
--- a/spec/fixtures/unit/pops/binder/bindings_composer/ok/modules/good/hiera.yaml
+++ /dev/null
@@ -1,11 +0,0 @@
----
-version: 2
-
-hierarchy:
- [ ['node', '${fqdn}', '${fqdn}' ],
- ['common', 'true', 'common' ]
- ]
-
-backends:
- - yaml
- - json
diff --git a/spec/fixtures/unit/pops/binder/bindings_composer/ok/modules/good/lib/puppet/bindings/good/default.rb b/spec/fixtures/unit/pops/binder/bindings_composer/ok/modules/good/lib/puppet/bindings/good/default.rb
new file mode 100644
index 000000000..76b6cc410
--- /dev/null
+++ b/spec/fixtures/unit/pops/binder/bindings_composer/ok/modules/good/lib/puppet/bindings/good/default.rb
@@ -0,0 +1,6 @@
+Puppet::Bindings.newbindings('good::default') do |scope|
+ bind {
+ name 'the_meaning_of_life'
+ to 300
+ }
+end
\ No newline at end of file
diff --git a/spec/fixtures/unit/pops/binder/config/binder_config/ok/binder_config.yaml b/spec/fixtures/unit/pops/binder/config/binder_config/ok/binder_config.yaml
index c51ee4c01..4ad3555ea 100644
--- a/spec/fixtures/unit/pops/binder/config/binder_config/ok/binder_config.yaml
+++ b/spec/fixtures/unit/pops/binder/config/binder_config/ok/binder_config.yaml
@@ -1,9 +1,9 @@
---
version: 1
layers:
- - {name: site, include: 'confdir-hiera:/'}
- - {name: modules, include: 'module-hiera:/*/', exclude: 'module-hiera:/bad/' }
+ - {name: site, include: 'confdir:/'}
+ - {name: modules, include: 'module:/*::test/', exclude: 'module:/bad::test/' }
categories:
- ['node', '$fqn']
- ['environment', '$environment']
- ['common', 'true']
\ No newline at end of file
diff --git a/spec/fixtures/unit/pops/binder/hiera2/bindings_provider/ok/hiera.yaml b/spec/fixtures/unit/pops/binder/hiera2/bindings_provider/ok/hiera.yaml
deleted file mode 100644
index 4ea7358a4..000000000
--- a/spec/fixtures/unit/pops/binder/hiera2/bindings_provider/ok/hiera.yaml
+++ /dev/null
@@ -1,9 +0,0 @@
----
-version: 2
-
-hierarchy:
- - ['node', '${node}', '${node}' ]
-
-backends:
- - yaml
- - json
diff --git a/spec/fixtures/unit/pops/binder/hiera2/bindings_provider/ok/node.example.com.json b/spec/fixtures/unit/pops/binder/hiera2/bindings_provider/ok/node.example.com.json
deleted file mode 100644
index 5b4a104d5..000000000
--- a/spec/fixtures/unit/pops/binder/hiera2/bindings_provider/ok/node.example.com.json
+++ /dev/null
@@ -1,9 +0,0 @@
-{
- "a_json_number": 142,
- "a_string": "don't want this to override",
- "a_json_string": "one hundred and forty two",
- "a_json_eval": "the answer from \"json\" is ${a} and \\${a}.",
- "a_json_eval2": "the answer\nfrom \\\"json\\\" is $a and \\$a",
- "a_json_array": ["a", "b", 100],
- "a_json_hash": { "a": 1, "b": 2 }
-}
diff --git a/spec/fixtures/unit/pops/binder/hiera2/bindings_provider/ok/node.example.com.yaml b/spec/fixtures/unit/pops/binder/hiera2/bindings_provider/ok/node.example.com.yaml
deleted file mode 100644
index 01340d6c7..000000000
--- a/spec/fixtures/unit/pops/binder/hiera2/bindings_provider/ok/node.example.com.yaml
+++ /dev/null
@@ -1,5 +0,0 @@
-a_number: 42
-a_string: forty two
-an_eval: "the answer from \"yaml\" is ${a}."
-an_eval2: "the answer\nfrom \\\"yaml\\\" is $a and \\$a"
-
diff --git a/spec/fixtures/unit/pops/binder/hiera2/config/bad_syntax/hiera.yaml b/spec/fixtures/unit/pops/binder/hiera2/config/bad_syntax/hiera.yaml
deleted file mode 100644
index 3c1c2d811..000000000
--- a/spec/fixtures/unit/pops/binder/hiera2/config/bad_syntax/hiera.yaml
+++ /dev/null
@@ -1,10 +0,0 @@
----
-version: 2
-
-hierarchy
- os:
- - ${osfamily}
- - for_${osfamily}
-
-backends:
- - yaml
diff --git a/spec/fixtures/unit/pops/binder/hiera2/config/malformed_hierarchy/hiera.yaml b/spec/fixtures/unit/pops/binder/hiera2/config/malformed_hierarchy/hiera.yaml
deleted file mode 100644
index 13869783f..000000000
--- a/spec/fixtures/unit/pops/binder/hiera2/config/malformed_hierarchy/hiera.yaml
+++ /dev/null
@@ -1,8 +0,0 @@
----
-version: 2
-
-hierarchy:
- ['os', '${osfamily}' ]
-
-backends:
- - yaml
diff --git a/spec/fixtures/unit/pops/binder/hiera2/config/missing/foo.txt b/spec/fixtures/unit/pops/binder/hiera2/config/missing/foo.txt
deleted file mode 100644
index e5ce6c87a..000000000
--- a/spec/fixtures/unit/pops/binder/hiera2/config/missing/foo.txt
+++ /dev/null
@@ -1 +0,0 @@
-# Do not delete
diff --git a/spec/fixtures/unit/pops/binder/hiera2/config/no_backends/hiera.yaml b/spec/fixtures/unit/pops/binder/hiera2/config/no_backends/hiera.yaml
deleted file mode 100644
index f1e9f4251..000000000
--- a/spec/fixtures/unit/pops/binder/hiera2/config/no_backends/hiera.yaml
+++ /dev/null
@@ -1,7 +0,0 @@
----
-version: 2
-hierarchy:
- os:
- - ${osfamily}
- - for_${osfamily}
-
diff --git a/spec/fixtures/unit/pops/binder/hiera2/config/no_hierarchy/hiera.yaml b/spec/fixtures/unit/pops/binder/hiera2/config/no_hierarchy/hiera.yaml
deleted file mode 100644
index 238752dd0..000000000
--- a/spec/fixtures/unit/pops/binder/hiera2/config/no_hierarchy/hiera.yaml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-version: 2
-backends:
- - yaml
diff --git a/spec/fixtures/unit/pops/binder/hiera2/config/not_a_hash/hiera.yaml b/spec/fixtures/unit/pops/binder/hiera2/config/not_a_hash/hiera.yaml
deleted file mode 100644
index a9e37cff5..000000000
--- a/spec/fixtures/unit/pops/binder/hiera2/config/not_a_hash/hiera.yaml
+++ /dev/null
@@ -1,2 +0,0 @@
-- first
-- second
diff --git a/spec/fixtures/unit/pops/binder/hiera2/config/ok/hiera.yaml b/spec/fixtures/unit/pops/binder/hiera2/config/ok/hiera.yaml
deleted file mode 100644
index 4e2f493d1..000000000
--- a/spec/fixtures/unit/pops/binder/hiera2/config/ok/hiera.yaml
+++ /dev/null
@@ -1,8 +0,0 @@
----
-version: 2
-
-hierarchy:
- - ['os', '${osfamily}', 'for_${osfamily}' ]
-
-backends:
- - yaml
diff --git a/spec/fixtures/unit/pops/binder/hiera2/yaml_backend/invalid/common.yaml b/spec/fixtures/unit/pops/binder/hiera2/yaml_backend/invalid/common.yaml
deleted file mode 100644
index ed97d539c..000000000
--- a/spec/fixtures/unit/pops/binder/hiera2/yaml_backend/invalid/common.yaml
+++ /dev/null
@@ -1 +0,0 @@
----
diff --git a/spec/fixtures/unit/pops/binder/hiera2/yaml_backend/ok/common.yaml b/spec/fixtures/unit/pops/binder/hiera2/yaml_backend/ok/common.yaml
deleted file mode 100644
index 0aa0b6bcb..000000000
--- a/spec/fixtures/unit/pops/binder/hiera2/yaml_backend/ok/common.yaml
+++ /dev/null
@@ -1,2 +0,0 @@
----
-brillig: slithy
diff --git a/spec/fixtures/unit/provider/service/systemd/list_units b/spec/fixtures/unit/provider/service/systemd/list_units_services
similarity index 92%
rename from spec/fixtures/unit/provider/service/systemd/list_units
rename to spec/fixtures/unit/provider/service/systemd/list_units_services
index 4998a08e4..c2784d854 100644
--- a/spec/fixtures/unit/provider/service/systemd/list_units
+++ b/spec/fixtures/unit/provider/service/systemd/list_units_services
@@ -1,18 +1,17 @@
UNIT LOAD ACTIVE SUB DESCRIPTION
auditd.service loaded active running Security Auditing Service
crond.service loaded active running Command Scheduler
dbus.service loaded active running D-Bus System Message Bus
display-manager.service error inactive dead display-manager.service
ebtables.service loaded inactive dead SYSV: Ethernet Bridge filtering tables
fedora-readonly.service loaded active exited Configure read-only root support
initrd-switch-root.service loaded inactive dead Switch Root
ip6tables.service error inactive dead ip6tables.service
puppet.service loaded inactive dead SYSV: Enables periodic system configuration checks through puppet.
-sshd.service loaded failed failed OpenSSH server daemon
LOAD = Reflects whether the unit definition was properly loaded.
ACTIVE = The high-level unit activation state, i.e. generalization of SUB.
SUB = The low-level unit activation state, values depend on unit type.
155 loaded units listed.
To show all installed unit files use 'systemctl list-unit-files'.
diff --git a/spec/integration/agent/logging_spec.rb b/spec/integration/agent/logging_spec.rb
index 284928ed7..c686397c8 100755
--- a/spec/integration/agent/logging_spec.rb
+++ b/spec/integration/agent/logging_spec.rb
@@ -1,178 +1,178 @@
#! /usr/bin/env ruby
require 'spec_helper'
require 'puppet'
require 'puppet/daemon'
require 'puppet/application/agent'
# The command line flags affecting #20900 and #20919:
#
# --onetime
# --daemonize
# --no-daemonize
# --logdest
# --verbose
# --debug
# (no flags) (-)
#
# d and nd are mutally exclusive
#
# Combinations without logdest, verbose or debug:
#
# --onetime --daemonize
# --onetime --no-daemonize
# --onetime
# --daemonize
# --no-daemonize
# -
#
# 6 cases X [--logdest=console, --logdest=syslog, --logdest=/some/file, <nothing added>]
# = 24 cases to test
#
# X [--verbose, --debug, <nothing added>]
# = 72 cases to test
#
# Expectations of behavior are defined in the expected_loggers, expected_level methods,
# so adapting to a change in logging behavior should hopefully be mostly a matter of
# adjusting the logic in those methods to define new behavior.
#
# Note that this test does not have anything to say about what happens to logging after
# daemonizing.
describe 'agent logging' do
ONETIME = '--onetime'
DAEMONIZE = '--daemonize'
NO_DAEMONIZE = '--no-daemonize'
LOGDEST_FILE = '--logdest=/dev/null/foo'
LOGDEST_SYSLOG = '--logdest=syslog'
LOGDEST_CONSOLE = '--logdest=console'
VERBOSE = '--verbose'
DEBUG = '--debug'
DEFAULT_LOG_LEVEL = :notice
INFO_LEVEL = :info
DEBUG_LEVEL = :debug
CONSOLE = :console
SYSLOG = :syslog
EVENTLOG = :eventlog
FILE = :file
ONETIME_DAEMONIZE_ARGS = [
[ONETIME],
[ONETIME, DAEMONIZE],
[ONETIME, NO_DAEMONIZE],
[DAEMONIZE],
[NO_DAEMONIZE],
[],
]
LOG_DEST_ARGS = [LOGDEST_FILE, LOGDEST_SYSLOG, LOGDEST_CONSOLE, nil]
LOG_LEVEL_ARGS = [VERBOSE, DEBUG, nil]
shared_examples "an agent" do |argv, expected|
before(:each) do
# Don't actually run the agent, bypassing cert checks, forking and the puppet run itself
Puppet::Application::Agent.any_instance.stubs(:run_command)
end
def double_of_bin_puppet_agent_call(argv)
argv.unshift('agent')
command_line = Puppet::Util::CommandLine.new('puppet', argv)
command_line.execute
end
if Puppet.features.microsoft_windows? && argv.include?(DAEMONIZE)
it "should exit on a platform which cannot daemonize if the --daemonize flag is set" do
expect { double_of_bin_puppet_agent_call(argv) }.to raise_error(SystemExit)
end
else
it "when evoked with #{argv}, logs to #{expected[:loggers].inspect} at level #{expected[:level]}" do
# This logger is created by the Puppet::Settings object which creates and
# applies a catalog to ensure that configuration files and users are in
# place.
#
# It's not something we are specifically testing here since it occurs
# regardless of user flags.
- Puppet::Util::Log.expects(:newdestination).with(instance_of(Puppet::Transaction::Report)).once
+ Puppet::Util::Log.expects(:newdestination).with(instance_of(Puppet::Transaction::Report)).at_least_once
expected[:loggers].each do |logclass|
Puppet::Util::Log.expects(:newdestination).with(logclass).at_least_once
end
double_of_bin_puppet_agent_call(argv)
Puppet::Util::Log.level.should == expected[:level]
end
end
end
def self.no_log_dest_set_in(argv)
([LOGDEST_SYSLOG, LOGDEST_CONSOLE, LOGDEST_FILE] & argv).empty?
end
def self.verbose_or_debug_set_in_argv(argv)
!([VERBOSE, DEBUG] & argv).empty?
end
def self.log_dest_is_set_to(argv, log_dest)
argv.include?(log_dest)
end
# @param argv Array of commandline flags
# @return Set<Symbol> of expected loggers
def self.expected_loggers(argv)
loggers = Set.new
loggers << CONSOLE if verbose_or_debug_set_in_argv(argv)
loggers << 'console' if log_dest_is_set_to(argv, LOGDEST_CONSOLE)
loggers << '/dev/null/foo' if log_dest_is_set_to(argv, LOGDEST_FILE)
if Puppet.features.microsoft_windows?
# an explicit call to --logdest syslog on windows is swallowed silently with no
# logger created (see #suitable() of the syslog Puppet::Util::Log::Destination subclass)
# however Puppet::Util::Log.newdestination('syslog') does get called...so we have
# to set an expectation
loggers << 'syslog' if log_dest_is_set_to(argv, LOGDEST_SYSLOG)
loggers << EVENTLOG if no_log_dest_set_in(argv)
else
# posix
loggers << 'syslog' if log_dest_is_set_to(argv, LOGDEST_SYSLOG)
loggers << SYSLOG if no_log_dest_set_in(argv)
end
return loggers
end
# @param argv Array of commandline flags
# @return Symbol of the expected log level
def self.expected_level(argv)
case
when argv.include?(VERBOSE) then INFO_LEVEL
when argv.include?(DEBUG) then DEBUG_LEVEL
else DEFAULT_LOG_LEVEL
end
end
# @param argv Array of commandline flags
# @return Hash of expected loggers and the expected log level
def self.with_expectations_based_on(argv)
{
:loggers => expected_loggers(argv),
:level => expected_level(argv),
}
end
# For running a single spec (by line number): rspec -l150 spec/integration/agent/logging_spec.rb
# debug_argv = []
# it_should_behave_like( "an agent", [debug_argv], with_expectations_based_on([debug_argv]))
ONETIME_DAEMONIZE_ARGS.each do |onetime_daemonize_args|
LOG_LEVEL_ARGS.each do |log_level_args|
LOG_DEST_ARGS.each do |log_dest_args|
argv = (onetime_daemonize_args + [log_level_args, log_dest_args]).flatten.compact
describe "for #{argv}" do
it_should_behave_like( "an agent", argv, with_expectations_based_on(argv))
end
end
end
end
end
diff --git a/spec/integration/application/apply_spec.rb b/spec/integration/application/apply_spec.rb
index 43fb3cd66..9847a14dc 100755
--- a/spec/integration/application/apply_spec.rb
+++ b/spec/integration/application/apply_spec.rb
@@ -1,33 +1,108 @@
#! /usr/bin/env ruby
require 'spec_helper'
require 'puppet_spec/files'
require 'puppet/application/apply'
describe "apply" do
include PuppetSpec::Files
before :each do
Puppet[:reports] = "none"
end
describe "when applying provided catalogs" do
- it "should be able to apply catalogs provided in a file in pson" do
+ it "can apply catalogs provided in a file in pson" do
file_to_create = tmpfile("pson_catalog")
catalog = Puppet::Resource::Catalog.new
resource = Puppet::Resource.new(:file, file_to_create, :parameters => {:content => "my stuff"})
catalog.add_resource resource
manifest = tmpfile("manifest")
File.open(manifest, "w") { |f| f.print catalog.to_pson }
puppet = Puppet::Application[:apply]
puppet.options[:catalog] = manifest
puppet.apply
- Puppet::FileSystem::File.exist?(file_to_create).should be_true
- File.read(file_to_create).should == "my stuff"
+ expect(Puppet::FileSystem.exist?(file_to_create)).to be_true
+ expect(File.read(file_to_create)).to eq("my stuff")
end
end
+
+ it "applies a given file even when a directory environment is specified" do
+ manifest = tmpfile("manifest.pp")
+ File.open(manifest, "w") do |f|
+ f.puts <<-EOF
+ notice('it was applied')
+ EOF
+ end
+
+ special = Puppet::Node::Environment.create(:special, [], '')
+ Puppet.override(:current_environment => special) do
+ Puppet[:environment] = 'special'
+ puppet = Puppet::Application[:apply]
+ puppet.stubs(:command_line).returns(stub('command_line', :args => [manifest]))
+ expect { puppet.run_command }.to exit_with(0)
+ end
+
+ expect(@logs.map(&:to_s)).to include('it was applied')
+ end
+
+ context "with a module" do
+ let(:modulepath) { tmpdir('modulepath') }
+ let(:execute) { 'include amod' }
+ let(:args) { ['-e', execute, '--modulepath', modulepath] }
+
+ before(:each) do
+ Puppet::FileSystem.mkpath("#{modulepath}/amod/manifests")
+ File.open("#{modulepath}/amod/manifests/init.pp", "w") do |f|
+ f.puts <<-EOF
+ class amod{
+ notice('amod class included')
+ }
+ EOF
+ end
+ create_default_directory_environment
+ end
+
+ def create_default_directory_environment
+ Puppet::FileSystem.mkpath("#{Puppet[:environmentpath]}/#{Puppet[:environment]}")
+ end
+
+ def init_cli_args_and_apply_app(args, execute)
+ Puppet.initialize_settings(args)
+ puppet = Puppet::Application.find(:apply).new(stub('command_line', :subcommand_name => :apply, :args => args))
+ puppet.options[:code] = execute
+ return puppet
+ end
+
+ it "looks in --modulepath even when the default directory environment exists" do
+ apply = init_cli_args_and_apply_app(args, execute)
+
+ expect do
+ expect { apply.run }.to exit_with(0)
+ end.to have_printed('amod class included')
+ end
+
+ it "looks in --modulepath even when given a specific directory --environment" do
+ args << '--environment' << 'production'
+ apply = init_cli_args_and_apply_app(args, execute)
+
+ expect do
+ expect { apply.run }.to exit_with(0)
+ end.to have_printed('amod class included')
+ end
+
+ it "looks in --modulepath when given multiple paths in --modulepath" do
+ args = ['-e', execute, '--modulepath', [tmpdir('notmodulepath'), modulepath].join(File::PATH_SEPARATOR)]
+ apply = init_cli_args_and_apply_app(args, execute)
+
+ expect do
+ expect { apply.run }.to exit_with(0)
+ end.to have_printed('amod class included')
+ end
+ end
+
end
diff --git a/spec/integration/application/doc_spec.rb b/spec/integration/application/doc_spec.rb
index 97ef026e7..77fc38625 100755
--- a/spec/integration/application/doc_spec.rb
+++ b/spec/integration/application/doc_spec.rb
@@ -1,55 +1,56 @@
#! /usr/bin/env ruby
require 'spec_helper'
require 'puppet_spec/files'
require 'puppet/application/doc'
describe Puppet::Application::Doc do
include PuppetSpec::Files
- it "should not generate an error when module dir overlaps parent of site.pp (#4798)", :if => Puppet.features.rdoc1?, :unless => Puppet.features.microsoft_windows? do
+ it "should not generate an error when module dir overlaps parent of site.pp (#4798)",
+ :if => (Puppet.features.rdoc1? and not Puppet.features.microsoft_windows?) do
begin
# Note: the directory structure below is more complex than it
# needs to be, but it's representative of the directory structure
# used in bug #4798.
old_dir = Dir.getwd # Note: can't use chdir with a block because it will generate bogus warnings
tmpdir = tmpfile('doc_spec')
Dir.mkdir(tmpdir)
Dir.chdir(tmpdir)
site_file = 'site.pp'
File.open(site_file, 'w') do |f|
f.puts '# A comment'
end
modules_dir = 'modules'
Dir.mkdir(modules_dir)
rt_dir = File.join(modules_dir, 'rt')
Dir.mkdir(rt_dir)
manifests_dir = File.join(rt_dir, 'manifests')
Dir.mkdir(manifests_dir)
rt_file = File.join(manifests_dir, 'rt.pp')
File.open(rt_file, 'w') do |f|
f.puts '# A class'
f.puts 'class foo { }'
f.puts '# A definition'
f.puts 'define bar { }'
end
puppet = Puppet::Application[:doc]
Puppet[:modulepath] = modules_dir
Puppet[:manifest] = site_file
puppet.options[:mode] = :rdoc
expect { puppet.run_command }.to exit_with 0
- Puppet::FileSystem::File.exist?('doc').should be_true
+ Puppet::FileSystem.exist?('doc').should be_true
ensure
Dir.chdir(old_dir)
end
end
it "should respect the -o option" do
puppetdoc = Puppet::Application[:doc]
puppetdoc.command_line.stubs(:args).returns(['foo', '-o', 'bar'])
puppetdoc.parse_options
puppetdoc.options[:outputdir].should == 'bar'
end
end
diff --git a/spec/integration/configurer_spec.rb b/spec/integration/configurer_spec.rb
index ddb044a0b..99eaeaefd 100755
--- a/spec/integration/configurer_spec.rb
+++ b/spec/integration/configurer_spec.rb
@@ -1,79 +1,81 @@
#! /usr/bin/env ruby
require 'spec_helper'
require 'puppet/configurer'
describe Puppet::Configurer do
include PuppetSpec::Files
describe "when downloading plugins" do
it "should use the :pluginsignore setting, split on whitespace, for ignoring remote files" do
+ Puppet.settings.stubs(:use)
resource = Puppet::Type.type(:notify).new :name => "yay"
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
configurer.download_plugins
end
end
describe "when running" do
before(:each) do
@catalog = Puppet::Resource::Catalog.new
@catalog.add_resource(Puppet::Type.type(:notify).new(:title => "testing"))
- # Make sure we don't try to persist the local state after the transaction ran,
+ # Make sure we don't try to persist the local state after the transaction ran,
# because it will fail during test (the state file is in a not-existing directory)
# and we need the transaction to be successful to be able to produce a summary report
@catalog.host_config = false
@configurer = Puppet::Configurer.new
end
it "should send a transaction report with valid data" do
@configurer.stubs(:save_last_run_summary)
Puppet::Transaction::Report.indirection.expects(:save).with do |report, x|
report.time.class == Time and report.logs.length > 0
end
Puppet[:report] = true
@configurer.run :catalog => @catalog
end
it "should save a correct last run summary" do
report = Puppet::Transaction::Report.new("apply")
Puppet::Transaction::Report.indirection.stubs(:save)
Puppet[:lastrunfile] = tmpfile("lastrunfile")
Puppet.settings.setting(:lastrunfile).mode = 0666
Puppet[:report] = true
# We only record integer seconds in the timestamp, and truncate
# backwards, so don't use a more accurate timestamp in the test.
# --daniel 2011-03-07
t1 = Time.now.tv_sec
@configurer.run :catalog => @catalog, :report => report
t2 = Time.now.tv_sec
- file_mode = Puppet.features.microsoft_windows? ? '100644' : '100666'
+ # sticky bit only applies to directories in windows
+ file_mode = Puppet.features.microsoft_windows? ? '666' : '100666'
- Puppet::FileSystem::File.new(Puppet[:lastrunfile]).stat.mode.to_s(8).should == file_mode
+ Puppet::FileSystem.stat(Puppet[:lastrunfile]).mode.to_s(8).should == file_mode
summary = nil
File.open(Puppet[:lastrunfile], "r") do |fd|
summary = YAML.load(fd.read)
end
summary.should be_a(Hash)
%w{time changes events resources}.each do |key|
summary.should be_key(key)
end
summary["time"].should be_key("notify")
summary["time"]["last_run"].should be_between(t1, t2)
end
end
end
diff --git a/spec/integration/directory_environments_spec.rb b/spec/integration/directory_environments_spec.rb
new file mode 100644
index 000000000..71b82a987
--- /dev/null
+++ b/spec/integration/directory_environments_spec.rb
@@ -0,0 +1,50 @@
+require 'spec_helper'
+
+describe "directory environments" do
+ let(:args) { ['--configprint', 'modulepath', '--environment', 'direnv'] }
+ let(:puppet) do
+ app = Puppet::Application[:apply]
+ app.stubs(:command_line).returns(stub('command_line', :args => []))
+ app
+ end
+
+ context "with a single directory environmentpath" do
+ before(:each) do
+ environmentdir = Puppet[:environmentpath].split(File::PATH_SEPARATOR).first
+ FileUtils.mkdir_p(environmentdir + "/direnv/modules")
+ end
+
+ it "config prints the environments modulepath" do
+ Puppet.settings.initialize_global_settings(args)
+ expect do
+ expect { puppet.run }.to exit_with(0)
+ end.to have_printed('direnv/modules')
+ end
+
+ it "config prints the cli --modulepath despite environment" do
+ args << '--modulepath' << 'completely/different'
+ Puppet.settings.initialize_global_settings(args)
+ expect do
+ expect { puppet.run }.to exit_with(0)
+ end.to have_printed('completely/different')
+ end
+ end
+
+ context "with an environmentpath having multiple directories" do
+ let(:args) { ['--configprint', 'modulepath', '--environment', 'otherdirenv'] }
+
+ before(:each) do
+ envdir1 = File.join(Puppet[:confdir], 'env1')
+ envdir2 = File.join(Puppet[:confdir], 'env2')
+ Puppet[:environmentpath] = [envdir1, envdir2].join(File::PATH_SEPARATOR)
+ FileUtils.mkdir_p(envdir2 + "/otherdirenv/modules")
+ end
+
+ it "config prints a directory environment modulepath" do
+ Puppet.settings.initialize_global_settings(args)
+ expect do
+ expect { puppet.run }.to exit_with(0)
+ end.to have_printed('otherdirenv/modules')
+ end
+ end
+end
diff --git a/spec/integration/indirector/direct_file_server_spec.rb b/spec/integration/indirector/direct_file_server_spec.rb
index 3a6b96250..cd0b32b74 100755
--- a/spec/integration/indirector/direct_file_server_spec.rb
+++ b/spec/integration/indirector/direct_file_server_spec.rb
@@ -1,67 +1,67 @@
#! /usr/bin/env ruby
require 'spec_helper'
require 'puppet/indirector/file_content/file'
describe Puppet::Indirector::DirectFileServer, " when interacting with the filesystem and the model" do
include PuppetSpec::Files
before do
# We just test a subclass, since it's close enough.
@terminus = Puppet::Indirector::FileContent::File.new
@filepath = make_absolute("/path/to/my/file")
end
it "should return an instance of the model" do
pending("porting to Windows", :if => Puppet.features.microsoft_windows?) do
- Puppet::FileSystem::File.expects(:exist?).with(@filepath).returns(true)
+ Puppet::FileSystem.expects(:exist?).with(@filepath).returns(true)
@terminus.find(@terminus.indirection.request(:find, "file://host#{@filepath}", nil)).should be_instance_of(Puppet::FileServing::Content)
end
end
it "should return an instance capable of returning its content" do
pending("porting to Windows", :if => Puppet.features.microsoft_windows?) do
filename = file_containing("testfile", "my content")
instance = @terminus.find(@terminus.indirection.request(:find, "file://host#{filename}", nil))
instance.content.should == "my content"
end
end
end
describe Puppet::Indirector::DirectFileServer, " when interacting with FileServing::Fileset and the model" do
include PuppetSpec::Files
let(:path) { tmpdir('direct_file_server_testing') }
before do
@terminus = Puppet::Indirector::FileContent::File.new
File.open(File.join(path, "one"), "w") { |f| f.print "one content" }
File.open(File.join(path, "two"), "w") { |f| f.print "two content" }
@request = @terminus.indirection.request(:search, "file:///#{path}", nil, :recurse => true)
end
it "should return an instance for every file in the fileset" do
result = @terminus.search(@request)
result.should be_instance_of(Array)
result.length.should == 3
result.each { |r| r.should be_instance_of(Puppet::FileServing::Content) }
end
it "should return instances capable of returning their content" do
@terminus.search(@request).each do |instance|
case instance.full_path
when /one/; instance.content.should == "one content"
when /two/; instance.content.should == "two content"
when path
else
raise "No valid key for #{instance.path.inspect}"
end
end
end
end
diff --git a/spec/integration/indirector/file_content/file_server_spec.rb b/spec/integration/indirector/file_content/file_server_spec.rb
index 44e2fabee..d35d60b44 100755
--- a/spec/integration/indirector/file_content/file_server_spec.rb
+++ b/spec/integration/indirector/file_content/file_server_spec.rb
@@ -1,90 +1,89 @@
#! /usr/bin/env ruby
require 'spec_helper'
require 'puppet/indirector/file_content/file_server'
require 'shared_behaviours/file_server_terminus'
require 'puppet_spec/files'
describe Puppet::Indirector::FileContent::FileServer, " when finding files" do
it_should_behave_like "Puppet::Indirector::FileServerTerminus"
include PuppetSpec::Files
before do
@terminus = Puppet::Indirector::FileContent::FileServer.new
@test_class = Puppet::FileServing::Content
Puppet::FileServing::Configuration.instance_variable_set(:@configuration, nil)
end
it "should find plugin file content in the environment specified in the request" do
path = tmpfile("file_content_with_env")
Dir.mkdir(path)
modpath = File.join(path, "mod")
FileUtils.mkdir_p(File.join(modpath, "lib"))
file = File.join(modpath, "lib", "file.rb")
File.open(file, "wb") { |f| f.write "1\r\n" }
Puppet.settings[:modulepath] = "/no/such/file"
- env = Puppet::Node::Environment.new("foo")
- env.stubs(:modulepath).returns [path]
+ env = Puppet::Node::Environment.create(:foo, [path], '')
- result = Puppet::FileServing::Content.indirection.search("plugins", :environment => "foo", :recurse => true)
+ result = Puppet::FileServing::Content.indirection.search("plugins", :environment => env, :recurse => true)
result.should_not be_nil
result.length.should == 2
result.map {|x| x.should be_instance_of(Puppet::FileServing::Content) }
result.find {|x| x.relative_path == 'file.rb' }.content.should == "1\r\n"
end
it "should find file content in modules" do
path = tmpfile("file_content")
Dir.mkdir(path)
modpath = File.join(path, "mymod")
FileUtils.mkdir_p(File.join(modpath, "files"))
file = File.join(modpath, "files", "myfile")
File.open(file, "wb") { |f| f.write "1\r\n" }
Puppet.settings[:modulepath] = path
result = Puppet::FileServing::Content.indirection.find("modules/mymod/myfile")
result.should_not be_nil
result.should be_instance_of(Puppet::FileServing::Content)
result.content.should == "1\r\n"
end
it "should find file content in files when node name expansions are used" do
- Puppet::FileSystem::File.stubs(:exist?).returns true
- Puppet::FileSystem::File.stubs(:exist?).with(Puppet[:fileserverconfig]).returns(true)
+ Puppet::FileSystem.stubs(:exist?).returns true
+ Puppet::FileSystem.stubs(:exist?).with(Puppet[:fileserverconfig]).returns(true)
@path = tmpfile("file_server_testing")
Dir.mkdir(@path)
subdir = File.join(@path, "mynode")
Dir.mkdir(subdir)
File.open(File.join(subdir, "myfile"), "wb") { |f| f.write "1\r\n" }
# Use a real mount, so the integration is a bit deeper.
@mount1 = Puppet::FileServing::Configuration::Mount::File.new("one")
@mount1.stubs(:allowed?).returns true
@mount1.path = File.join(@path, "%h")
@parser = stub 'parser', :changed? => false
@parser.stubs(:parse).returns("one" => @mount1)
Puppet::FileServing::Configuration::Parser.stubs(:new).returns(@parser)
path = File.join(@path, "myfile")
result = Puppet::FileServing::Content.indirection.find("one/myfile", :environment => "foo", :node => "mynode")
result.should_not be_nil
result.should be_instance_of(Puppet::FileServing::Content)
result.content.should == "1\r\n"
end
end
diff --git a/spec/integration/network/authconfig_spec.rb b/spec/integration/network/authconfig_spec.rb
index 9db0c1099..cdd9a2559 100644
--- a/spec/integration/network/authconfig_spec.rb
+++ b/spec/integration/network/authconfig_spec.rb
@@ -1,257 +1,257 @@
require 'spec_helper'
require 'puppet/network/authconfig'
require 'puppet/network/auth_config_parser'
RSpec::Matchers.define :allow do |params|
match do |auth|
begin
- auth.check_authorization(params[0], params[1], params[2], params[3])
+ auth.check_authorization(*params)
true
rescue Puppet::Network::AuthorizationError
false
end
end
failure_message_for_should do |instance|
- "expected #{params[3][:node]}/#{params[3][:ip]} to be allowed"
+ "expected #{params[2][:node]}/#{params[2][:ip]} to be allowed"
end
failure_message_for_should_not do |instance|
- "expected #{params[3][:node]}/#{params[3][:ip]} to be forbidden"
+ "expected #{params[2][:node]}/#{params[2][:ip]} to be forbidden"
end
end
describe Puppet::Network::AuthConfig do
include PuppetSpec::Files
def add_rule(rule)
parser = Puppet::Network::AuthConfigParser.new(
"path /test\n#{rule}\n"
)
@auth = parser.parse
end
def add_regex_rule(regex, rule)
parser = Puppet::Network::AuthConfigParser.new(
"path ~ #{regex}\n#{rule}\n"
)
@auth = parser.parse
end
def add_raw_stanza(stanza)
parser = Puppet::Network::AuthConfigParser.new(
stanza
)
@auth = parser.parse
end
def request(args = {})
args = {
:key => 'key',
:node => 'host.domain.com',
:ip => '10.1.1.1',
:authenticated => true
}.merge(args)
- ['test', :find, args[:key], args]
+ [:find, "/test/#{args[:key]}", args]
end
describe "allow" do
it "should not match IP addresses" do
add_rule("allow 10.1.1.1")
@auth.should_not allow(request)
end
it "should not accept CIDR IPv4 address" do
expect {
add_rule("allow 10.0.0.0/8")
}.to raise_error Puppet::ConfigurationError, /Invalid pattern 10\.0\.0\.0\/8/
end
it "should not match wildcard IPv4 address" do
expect {
add_rule("allow 10.1.1.*")
}.to raise_error Puppet::ConfigurationError, /Invalid pattern 10\.1\.1\.*/
end
it "should not match IPv6 address" do
expect {
add_rule("allow 2001:DB8::8:800:200C:417A")
}.to raise_error Puppet::ConfigurationError, /Invalid pattern 2001/
end
it "should support hostname" do
add_rule("allow host.domain.com")
@auth.should allow(request)
end
it "should support wildcard host" do
add_rule("allow *.domain.com")
@auth.should allow(request)
end
it 'should warn about missing path before allow_ip in stanza' do
expect {
add_raw_stanza("allow_ip 10.0.0.1\n")
}.to raise_error Puppet::ConfigurationError, /Missing or invalid 'path' before right directive at line.*/
end
it 'should warn about missing path before allow in stanza' do
expect {
add_raw_stanza("allow host.domain.com\n")
}.to raise_error Puppet::ConfigurationError, /Missing or invalid 'path' before right directive at line.*/
end
it "should support hostname backreferences" do
add_regex_rule('^/test/([^/]+)$', "allow $1.domain.com")
@auth.should allow(request(:key => 'host'))
end
it "should support opaque strings" do
add_rule("allow this-is-opaque@or-not")
@auth.should allow(request(:node => 'this-is-opaque@or-not'))
end
it "should support opaque strings and backreferences" do
add_regex_rule('^/test/([^/]+)$', "allow $1")
@auth.should allow(request(:key => 'this-is-opaque@or-not', :node => 'this-is-opaque@or-not'))
end
it "should support hostname ending with '.'" do
pending('bug #7589')
add_rule("allow host.domain.com.")
@auth.should allow(request(:node => 'host.domain.com.'))
end
it "should support hostname ending with '.' and backreferences" do
pending('bug #7589')
add_regex_rule('^/test/([^/]+)$',"allow $1")
@auth.should allow(request(:node => 'host.domain.com.'))
end
it "should support trailing whitespace" do
add_rule('allow host.domain.com ')
@auth.should allow(request)
end
it "should support inlined comments" do
add_rule('allow host.domain.com # will it work?')
@auth.should allow(request)
end
it "should deny non-matching host" do
add_rule("allow inexistant")
@auth.should_not allow(request)
end
end
describe "allow_ip" do
it "should not warn when matches against IP addresses fail" do
add_rule("allow_ip 10.1.1.2")
@auth.should_not allow(request)
@logs.should_not be_any {|log| log.level == :warning and log.message =~ /Authentication based on IP address is deprecated/}
end
it "should support IPv4 address" do
add_rule("allow_ip 10.1.1.1")
@auth.should allow(request)
end
it "should support CIDR IPv4 address" do
add_rule("allow_ip 10.0.0.0/8")
@auth.should allow(request)
end
it "should support wildcard IPv4 address" do
add_rule("allow_ip 10.1.1.*")
@auth.should allow(request)
end
it "should support IPv6 address" do
add_rule("allow_ip 2001:DB8::8:800:200C:417A")
@auth.should allow(request(:ip => '2001:DB8::8:800:200C:417A'))
end
it "should support hostname" do
expect {
add_rule("allow_ip host.domain.com")
}.to raise_error Puppet::ConfigurationError, /Invalid IP pattern host.domain.com/
end
end
describe "deny" do
it "should deny denied hosts" do
add_rule <<-EOALLOWRULE
deny host.domain.com
allow *.domain.com
EOALLOWRULE
@auth.should_not allow(request)
end
it "denies denied hosts after allowing them" do
add_rule <<-EOALLOWRULE
allow *.domain.com
deny host.domain.com
EOALLOWRULE
@auth.should_not allow(request)
end
it "should not deny based on IP" do
add_rule <<-EOALLOWRULE
deny 10.1.1.1
allow host.domain.com
EOALLOWRULE
@auth.should allow(request)
end
it "should not deny based on IP (ordering #2)" do
add_rule <<-EOALLOWRULE
allow host.domain.com
deny 10.1.1.1
EOALLOWRULE
@auth.should allow(request)
end
end
describe "deny_ip" do
it "should deny based on IP" do
add_rule <<-EOALLOWRULE
deny_ip 10.1.1.1
allow host.domain.com
EOALLOWRULE
@auth.should_not allow(request)
end
it "should deny based on IP (ordering #2)" do
add_rule <<-EOALLOWRULE
allow host.domain.com
deny_ip 10.1.1.1
EOALLOWRULE
@auth.should_not allow(request)
end
end
end
diff --git a/spec/integration/network/formats_spec.rb b/spec/integration/network/formats_spec.rb
index 2834e33f0..686961338 100755
--- a/spec/integration/network/formats_spec.rb
+++ b/spec/integration/network/formats_spec.rb
@@ -1,91 +1,91 @@
#! /usr/bin/env ruby
require 'spec_helper'
require 'puppet/network/formats'
class PsonIntTest
attr_accessor :string
def ==(other)
other.class == self.class and string == other.string
end
- def self.from_pson(data)
+ def self.from_data_hash(data)
new(data[0])
end
def initialize(string)
@string = string
end
def to_pson(*args)
{
'type' => self.class.name,
'data' => [@string]
}.to_pson(*args)
end
def self.canonical_order(s)
s.gsub(/\{"data":\[(.*?)\],"type":"PsonIntTest"\}/,'{"type":"PsonIntTest","data":[\1]}')
end
end
describe Puppet::Network::FormatHandler.format(:s) do
before do
@format = Puppet::Network::FormatHandler.format(:s)
end
it "should support certificates" do
@format.should be_supported(Puppet::SSL::Certificate)
end
it "should not support catalogs" do
@format.should_not be_supported(Puppet::Resource::Catalog)
end
end
describe Puppet::Network::FormatHandler.format(:pson) do
before do
@pson = Puppet::Network::FormatHandler.format(:pson)
end
it "should be able to render an instance to pson" do
instance = PsonIntTest.new("foo")
PsonIntTest.canonical_order(@pson.render(instance)).should == PsonIntTest.canonical_order('{"type":"PsonIntTest","data":["foo"]}' )
end
it "should be able to render arrays to pson" do
@pson.render([1,2]).should == '[1,2]'
end
it "should be able to render arrays containing hashes to pson" do
@pson.render([{"one"=>1},{"two"=>2}]).should == '[{"one":1},{"two":2}]'
end
it "should be able to render multiple instances to pson" do
one = PsonIntTest.new("one")
two = PsonIntTest.new("two")
PsonIntTest.canonical_order(@pson.render([one,two])).should == PsonIntTest.canonical_order('[{"type":"PsonIntTest","data":["one"]},{"type":"PsonIntTest","data":["two"]}]')
end
it "should be able to intern pson into an instance" do
@pson.intern(PsonIntTest, '{"type":"PsonIntTest","data":["foo"]}').should == PsonIntTest.new("foo")
end
it "should be able to intern pson with no class information into an instance" do
@pson.intern(PsonIntTest, '["foo"]').should == PsonIntTest.new("foo")
end
it "should be able to intern multiple instances from pson" do
@pson.intern_multiple(PsonIntTest, '[{"type": "PsonIntTest", "data": ["one"]},{"type": "PsonIntTest", "data": ["two"]}]').should == [
PsonIntTest.new("one"), PsonIntTest.new("two")
]
end
it "should be able to intern multiple instances from pson with no class information" do
@pson.intern_multiple(PsonIntTest, '[["one"],["two"]]').should == [
PsonIntTest.new("one"), PsonIntTest.new("two")
]
end
end
diff --git a/spec/integration/node/environment_spec.rb b/spec/integration/node/environment_spec.rb
index f41a5988a..d193367ee 100755
--- a/spec/integration/node/environment_spec.rb
+++ b/spec/integration/node/environment_spec.rb
@@ -1,57 +1,109 @@
#! /usr/bin/env ruby
require 'spec_helper'
require 'puppet_spec/files'
+require 'puppet_spec/scope'
+require 'matchers/resource'
describe Puppet::Node::Environment do
include PuppetSpec::Files
+ include Matchers::Resource
+
+ def a_module_in(name, dir)
+ Dir.mkdir(dir)
+ moddir = File.join(dir, name)
+ Dir.mkdir(moddir)
+ moddir
+ end
it "should be able to return each module from its environment with the environment, name, and path set correctly" do
base = tmpfile("env_modules")
Dir.mkdir(base)
dirs = []
mods = {}
%w{1 2}.each do |num|
dir = File.join(base, "dir#{num}")
dirs << dir
- Dir.mkdir(dir)
- mod = "mod#{num}"
- moddir = File.join(dir, mod)
- mods[mod] = moddir
- Dir.mkdir(moddir)
+
+ mods["mod#{num}"] = a_module_in("mod#{num}", dir)
end
- environment = Puppet::Node::Environment.new("foo")
- environment.stubs(:modulepath).returns dirs
+ environment = Puppet::Node::Environment.create(:foo, dirs, '')
environment.modules.each do |mod|
mod.environment.should == environment
mod.path.should == mods[mod.name]
end
end
it "should not yield the same module from different module paths" do
base = tmpfile("env_modules")
Dir.mkdir(base)
dirs = []
- mods = {}
%w{1 2}.each do |num|
dir = File.join(base, "dir#{num}")
dirs << dir
- Dir.mkdir(dir)
- mod = "mod"
- moddir = File.join(dir, mod)
- mods[mod] = moddir
- Dir.mkdir(moddir)
+
+ a_module_in("mod", dir)
end
- environment = Puppet::Node::Environment.new("foo")
- environment.stubs(:modulepath).returns dirs
+ environment = Puppet::Node::Environment.create(:foo, dirs, '')
mods = environment.modules
mods.length.should == 1
mods[0].path.should == File.join(base, "dir1", "mod")
end
+
+ shared_examples_for "the environment's initial import" do |settings|
+ it "a manifest referring to a directory invokes parsing of all its files in sorted order" do
+ settings.each do |name, value|
+ Puppet[name] = value
+ end
+
+ # fixture has three files 00_a.pp, 01_b.pp, and 02_c.pp. The 'b' file
+ # depends on 'a' being evaluated first. The 'c' file is empty (to ensure
+ # empty things do not break the directory import).
+ #
+ dirname = my_fixture('sitedir')
+
+ # Set the manifest to the directory to make it parse and combine them when compiling
+ node = Puppet::Node.new('testnode',
+ :environment => Puppet::Node::Environment.create(:testing, [], dirname))
+
+ catalog = Puppet::Parser::Compiler.compile(node)
+
+ expect(catalog).to have_resource('Class[a]')
+ expect(catalog).to have_resource('Class[b]')
+ expect(catalog).to have_resource('Notify[variables]').with_parameter(:message, "a: 10, b: 10")
+ end
+ end
+
+ describe 'using classic parser' do
+ it_behaves_like "the environment's initial import",
+ :parser => 'current',
+ # fixture uses variables that are set in a particular order (this ensures
+ # that files are parsed and combined in the right order or an error will
+ # be raised if 'b' is evaluated before 'a').
+ :strict_variables => true
+ end
+
+ describe 'using future parser' do
+ it_behaves_like "the environment's initial import",
+ :parser => 'future',
+ :evaluator => 'future',
+ # Turned off because currently future parser turns on the binder which
+ # causes lookup of facts that are uninitialized and it will fail with
+ # errors for 'osfamily' etc. This can be turned back on when the binder
+ # is taken out of the equation.
+ :strict_variables => false
+
+ context 'and evaluator current' do
+ it_behaves_like "the environment's initial import",
+ :parser => 'future',
+ :evaluator => 'current',
+ :strict_variables => false
+ end
+ end
end
diff --git a/spec/integration/node/facts_spec.rb b/spec/integration/node/facts_spec.rb
index fa9fd4905..cf465b797 100755
--- a/spec/integration/node/facts_spec.rb
+++ b/spec/integration/node/facts_spec.rb
@@ -1,41 +1,41 @@
#! /usr/bin/env ruby
require 'spec_helper'
describe Puppet::Node::Facts do
describe "when using the indirector" do
it "should expire any cached node instances when it is saved" do
Puppet::Node::Facts.indirection.stubs(:terminus_class).returns :yaml
Puppet::Node::Facts.indirection.terminus(:yaml).should equal(Puppet::Node::Facts.indirection.terminus(:yaml))
terminus = Puppet::Node::Facts.indirection.terminus(:yaml)
terminus.stubs :save
Puppet::Node.indirection.expects(:expire).with("me", optionally(instance_of(Hash)))
facts = Puppet::Node::Facts.new("me")
Puppet::Node::Facts.indirection.save(facts)
end
it "should be able to delegate to the :yaml terminus" do
Puppet::Node::Facts.indirection.stubs(:terminus_class).returns :yaml
# Load now, before we stub the exists? method.
terminus = Puppet::Node::Facts.indirection.terminus(:yaml)
terminus.expects(:path).with("me").returns "/my/yaml/file"
- Puppet::FileSystem::File.expects(:exist?).with("/my/yaml/file").returns false
+ Puppet::FileSystem.expects(:exist?).with("/my/yaml/file").returns false
Puppet::Node::Facts.indirection.find("me").should be_nil
end
it "should be able to delegate to the :facter terminus" do
Puppet::Node::Facts.indirection.stubs(:terminus_class).returns :facter
Facter.expects(:to_hash).returns "facter_hash"
facts = Puppet::Node::Facts.new("me")
Puppet::Node::Facts.expects(:new).with("me", "facter_hash").returns facts
Puppet::Node::Facts.indirection.find("me").should equal(facts)
end
end
end
diff --git a/spec/integration/node_spec.rb b/spec/integration/node_spec.rb
index ef862ef28..8337c2a66 100755
--- a/spec/integration/node_spec.rb
+++ b/spec/integration/node_spec.rb
@@ -1,82 +1,82 @@
#! /usr/bin/env ruby
require 'spec_helper'
require 'puppet/node'
describe Puppet::Node do
describe "when delegating indirection calls" do
before do
Puppet::Node.indirection.reset_terminus_class
Puppet::Node.indirection.cache_class = nil
@name = "me"
@node = Puppet::Node.new(@name)
end
it "should be able to use the yaml terminus" do
Puppet::Node.indirection.stubs(:terminus_class).returns :yaml
# Load now, before we stub the exists? method.
terminus = Puppet::Node.indirection.terminus(:yaml)
terminus.expects(:path).with(@name).returns "/my/yaml/file"
- Puppet::FileSystem::File.expects(:exist?).with("/my/yaml/file").returns false
+ Puppet::FileSystem.expects(:exist?).with("/my/yaml/file").returns false
Puppet::Node.indirection.find(@name).should be_nil
end
it "should have an ldap terminus" do
Puppet::Node.indirection.terminus(:ldap).should_not be_nil
end
it "should be able to use the plain terminus" do
Puppet::Node.indirection.stubs(:terminus_class).returns :plain
# Load now, before we stub the exists? method.
Puppet::Node.indirection.terminus(:plain)
Puppet::Node.expects(:new).with(@name).returns @node
Puppet::Node.indirection.find(@name).should equal(@node)
end
describe "and using the memory terminus" do
before do
@name = "me"
@terminus = Puppet::Node.indirection.terminus(:memory)
Puppet::Node.indirection.stubs(:terminus).returns @terminus
@node = Puppet::Node.new(@name)
end
after do
@terminus.instance_variable_set(:@instances, {})
end
it "should find no nodes by default" do
Puppet::Node.indirection.find(@name).should be_nil
end
it "should be able to find nodes that were previously saved" do
Puppet::Node.indirection.save(@node)
Puppet::Node.indirection.find(@name).should equal(@node)
end
it "should replace existing saved nodes when a new node with the same name is saved" do
Puppet::Node.indirection.save(@node)
two = Puppet::Node.new(@name)
Puppet::Node.indirection.save(two)
Puppet::Node.indirection.find(@name).should equal(two)
end
it "should be able to remove previously saved nodes" do
Puppet::Node.indirection.save(@node)
Puppet::Node.indirection.destroy(@node.name)
Puppet::Node.indirection.find(@name).should be_nil
end
it "should fail when asked to destroy a node that does not exist" do
proc { Puppet::Node.indirection.destroy(@node) }.should raise_error(ArgumentError)
end
end
end
end
diff --git a/spec/integration/parser/catalog_spec.rb b/spec/integration/parser/catalog_spec.rb
index c11e251dd..e37eb591a 100644
--- a/spec/integration/parser/catalog_spec.rb
+++ b/spec/integration/parser/catalog_spec.rb
@@ -1,85 +1,125 @@
require 'spec_helper'
require 'matchers/include_in_order'
require 'puppet_spec/compiler'
require 'puppet/indirector/catalog/compiler'
-describe "Transmission of the catalog to the agent" do
+describe "A catalog" do
include PuppetSpec::Compiler
- it "preserves the order in which the resources are added to the catalog" do
- resources_in_declaration_order = ["Class[First]",
- "Second[position]",
- "Class[Third]",
- "Fourth[position]"]
-
- master_catalog, agent_catalog = master_and_agent_catalogs_for(<<-EOM)
- define fourth() { }
- class third { }
-
- define second() {
- fourth { "position": }
- }
-
- class first {
- second { "position": }
- class { "third": }
- }
-
- include first
- EOM
-
- expect(resources_in(master_catalog)).
- to include_in_order(*resources_in_declaration_order)
- expect(resources_in(agent_catalog)).
- to include_in_order(*resources_in_declaration_order)
+ shared_examples_for "when compiled" do
+ context "when transmitted to the agent" do
+
+ it "preserves the order in which the resources are added to the catalog" do
+ resources_in_declaration_order = ["Class[First]",
+ "Second[position]",
+ "Class[Third]",
+ "Fourth[position]"]
+
+ master_catalog, agent_catalog = master_and_agent_catalogs_for(<<-EOM)
+ define fourth() { }
+ class third { }
+
+ define second() {
+ fourth { "position": }
+ }
+
+ class first {
+ second { "position": }
+ class { "third": }
+ }
+
+ include first
+ EOM
+
+ expect(resources_in(master_catalog)).
+ to include_in_order(*resources_in_declaration_order)
+ expect(resources_in(agent_catalog)).
+ to include_in_order(*resources_in_declaration_order)
+ end
+
+ it "does not contain unrealized, virtual resources" do
+ virtual_resources = ["Unrealized[unreal]", "Class[Unreal]"]
+
+ master_catalog, agent_catalog = master_and_agent_catalogs_for(<<-EOM)
+ class unreal { }
+ define unrealized() { }
+
+ class real {
+ @unrealized { "unreal": }
+ @class { "unreal": }
+ }
+
+ include real
+ EOM
+
+ expect(resources_in(master_catalog)).to_not include(*virtual_resources)
+ expect(resources_in(agent_catalog)).to_not include(*virtual_resources)
+ end
+
+ it "does not contain unrealized, exported resources" do
+ exported_resources = ["Unrealized[unreal]", "Class[Unreal]"]
+
+ master_catalog, agent_catalog = master_and_agent_catalogs_for(<<-EOM)
+ class unreal { }
+ define unrealized() { }
+
+ class real {
+ @@unrealized { "unreal": }
+ @@class { "unreal": }
+ }
+
+ include real
+ EOM
+
+ expect(resources_in(master_catalog)).to_not include(*exported_resources)
+ expect(resources_in(agent_catalog)).to_not include(*exported_resources)
+ end
+ end
+
+ it "compiles resource creation from appended array as two separate resources" do
+ # moved here from acceptance test "jeff_append_to_array.rb"
+ master_catalog = master_catalog_for(<<-EOM)
+ class parent {
+ $arr1 = [ "parent array element" ]
+ }
+ class parent::child inherits parent {
+ $arr1 += ["child array element"]
+ notify { $arr1: }
+ }
+ include parent::child
+ EOM
+ expect(resources_in(master_catalog)).to include('Notify[parent array element]', 'Notify[child array element]')
+ end
end
- it "does not contain unrealized, virtual resources" do
- virtual_resources = ["Unrealized[unreal]", "Class[Unreal]"]
-
- master_catalog, agent_catalog = master_and_agent_catalogs_for(<<-EOM)
- class unreal { }
- define unrealized() { }
-
- class real {
- @unrealized { "unreal": }
- @class { "unreal": }
- }
-
- include real
- EOM
-
- expect(resources_in(master_catalog)).to_not include(*virtual_resources)
- expect(resources_in(agent_catalog)).to_not include(*virtual_resources)
+ describe 'using classic parser' do
+ before :each do
+ Puppet[:parser] = 'current'
+ end
+ it_behaves_like 'when compiled' do
+ end
end
- it "does not contain unrealized, exported resources" do
- exported_resources = ["Unrealized[unreal]", "Class[Unreal]"]
-
- master_catalog, agent_catalog = master_and_agent_catalogs_for(<<-EOM)
- class unreal { }
- define unrealized() { }
-
- class real {
- @@unrealized { "unreal": }
- @@class { "unreal": }
- }
-
- include real
- EOM
+ describe 'using future parser' do
+ before :each do
+ Puppet[:parser] = 'future'
+ end
+ it_behaves_like 'when compiled' do
+ end
+ end
- expect(resources_in(master_catalog)).to_not include(*exported_resources)
- expect(resources_in(agent_catalog)).to_not include(*exported_resources)
+ def master_catalog_for(manifest)
+ master_catalog = Puppet::Resource::Catalog::Compiler.new.filter(compile_to_catalog(manifest))
end
def master_and_agent_catalogs_for(manifest)
master_catalog = Puppet::Resource::Catalog::Compiler.new.filter(compile_to_catalog(manifest))
agent_catalog = Puppet::Resource::Catalog.convert_from(:pson, master_catalog.render(:pson))
[master_catalog, agent_catalog]
end
def resources_in(catalog)
catalog.resources.map(&:ref)
end
end
diff --git a/spec/integration/parser/compiler_spec.rb b/spec/integration/parser/compiler_spec.rb
index eb87d03ed..f10ce1adc 100755
--- a/spec/integration/parser/compiler_spec.rb
+++ b/spec/integration/parser/compiler_spec.rb
@@ -1,424 +1,513 @@
#! /usr/bin/env ruby
require 'spec_helper'
require 'puppet/parser/parser_factory'
require 'puppet_spec/compiler'
+require 'matchers/resource'
describe "Puppet::Parser::Compiler" do
include PuppetSpec::Compiler
+ include Matchers::Resource
before :each do
@node = Puppet::Node.new "testnode"
@scope_resource = stub 'scope_resource', :builtin? => true, :finish => nil, :ref => 'Class[main]'
@scope = stub 'scope', :resource => @scope_resource, :source => mock("source")
end
after do
Puppet.settings.clear
end
# shared because tests are invoked both for classic and future parser
#
shared_examples_for "the compiler" do
it "should be able to determine the configuration version from a local version control repository" do
pending("Bug #14071 about semantics of Puppet::Util::Execute on Windows", :if => Puppet.features.microsoft_windows?) do
# This should always work, because we should always be
# in the puppet repo when we run this.
version = %x{git rev-parse HEAD}.chomp
Puppet.settings[:config_version] = 'git rev-parse HEAD'
@parser = Puppet::Parser::ParserFactory.parser "development"
@compiler = Puppet::Parser::Compiler.new(@node)
@compiler.catalog.version.should == version
end
end
it "should not create duplicate resources when a class is referenced both directly and indirectly by the node classifier (4792)" do
Puppet[:code] = <<-PP
class foo
{
notify { foo_notify: }
include bar
}
class bar
{
notify { bar_notify: }
}
PP
@node.stubs(:classes).returns(['foo', 'bar'])
catalog = Puppet::Parser::Compiler.compile(@node)
catalog.resource("Notify[foo_notify]").should_not be_nil
catalog.resource("Notify[bar_notify]").should_not be_nil
end
describe "when resolving class references" do
it "should favor local scope, even if there's an included class in topscope" do
Puppet[:code] = <<-PP
class experiment {
class baz {
}
notify {"x" : require => Class[Baz] }
}
class baz {
}
include baz
include experiment
include experiment::baz
PP
catalog = Puppet::Parser::Compiler.compile(Puppet::Node.new("mynode"))
notify_resource = catalog.resource( "Notify[x]" )
notify_resource[:require].title.should == "Experiment::Baz"
end
it "should favor local scope, even if there's an unincluded class in topscope" do
Puppet[:code] = <<-PP
class experiment {
class baz {
}
notify {"x" : require => Class[Baz] }
}
class baz {
}
include experiment
include experiment::baz
PP
catalog = Puppet::Parser::Compiler.compile(Puppet::Node.new("mynode"))
notify_resource = catalog.resource( "Notify[x]" )
notify_resource[:require].title.should == "Experiment::Baz"
end
end
describe "(ticket #13349) when explicitly specifying top scope" do
["class {'::bar::baz':}", "include ::bar::baz"].each do |include|
describe "with #{include}" do
it "should find the top level class" do
Puppet[:code] = <<-MANIFEST
class { 'foo::test': }
class foo::test {
#{include}
}
class bar::baz {
notify { 'good!': }
}
class foo::bar::baz {
notify { 'bad!': }
}
MANIFEST
catalog = Puppet::Parser::Compiler.compile(Puppet::Node.new("mynode"))
catalog.resource("Class[Bar::Baz]").should_not be_nil
catalog.resource("Notify[good!]").should_not be_nil
catalog.resource("Class[Foo::Bar::Baz]").should be_nil
catalog.resource("Notify[bad!]").should be_nil
end
end
end
end
it "should recompute the version after input files are re-parsed" do
Puppet[:code] = 'class foo { }'
Time.stubs(:now).returns(1)
node = Puppet::Node.new('mynode')
Puppet::Parser::Compiler.compile(node).version.should == 1
Time.stubs(:now).returns(2)
Puppet::Parser::Compiler.compile(node).version.should == 1 # no change because files didn't change
Puppet::Resource::TypeCollection.any_instance.stubs(:stale?).returns(true).then.returns(false) # pretend change
Puppet::Parser::Compiler.compile(node).version.should == 2
end
['class', 'define', 'node'].each do |thing|
- it "should not allow #{thing} inside evaluated conditional constructs" do
+ it "should not allow '#{thing}' inside evaluated conditional constructs" do
Puppet[:code] = <<-PP
if true {
#{thing} foo {
}
notify { decoy: }
}
PP
begin
Puppet::Parser::Compiler.compile(Puppet::Node.new("mynode"))
raise "compilation should have raised Puppet::Error"
rescue Puppet::Error => e
e.message.should =~ /at line 2/
end
end
end
it "should not allow classes inside unevaluated conditional constructs" do
Puppet[:code] = <<-PP
if false {
class foo {
}
}
PP
lambda { Puppet::Parser::Compiler.compile(Puppet::Node.new("mynode")) }.should raise_error(Puppet::Error)
end
describe "when defining relationships" do
def extract_name(ref)
ref.sub(/File\[(\w+)\]/, '\1')
end
let(:node) { Puppet::Node.new('mynode') }
let(:code) do
<<-MANIFEST
file { [a,b,c]:
mode => 0644,
}
file { [d,e]:
mode => 0755,
}
MANIFEST
end
let(:expected_relationships) { [] }
let(:expected_subscriptions) { [] }
before :each do
Puppet[:code] = code
end
after :each do
catalog = Puppet::Parser::Compiler.compile(node)
resources = catalog.resources.select { |res| res.type == 'File' }
actual_relationships, actual_subscriptions = [:before, :notify].map do |relation|
resources.map do |res|
dependents = Array(res[relation])
dependents.map { |ref| [res.title, extract_name(ref)] }
end.inject(&:concat)
end
actual_relationships.should =~ expected_relationships
actual_subscriptions.should =~ expected_subscriptions
end
it "should create a relationship" do
code << "File[a] -> File[b]"
expected_relationships << ['a','b']
end
it "should create a subscription" do
code << "File[a] ~> File[b]"
expected_subscriptions << ['a', 'b']
end
it "should create relationships using title arrays" do
code << "File[a,b] -> File[c,d]"
expected_relationships.concat [
['a', 'c'],
['b', 'c'],
['a', 'd'],
['b', 'd'],
]
end
it "should create relationships using collection expressions" do
code << "File <| mode == 0644 |> -> File <| mode == 0755 |>"
expected_relationships.concat [
['a', 'd'],
['b', 'd'],
['c', 'd'],
['a', 'e'],
['b', 'e'],
['c', 'e'],
]
end
it "should create relationships using resource names" do
code << "'File[a]' -> 'File[b]'"
expected_relationships << ['a', 'b']
end
it "should create relationships using variables" do
code << <<-MANIFEST
$var = File[a]
$var -> File[b]
MANIFEST
expected_relationships << ['a', 'b']
end
it "should create relationships using case statements" do
code << <<-MANIFEST
$var = 10
case $var {
10: {
file { s1: }
}
12: {
file { s2: }
}
}
->
case $var + 2 {
10: {
file { t1: }
}
12: {
file { t2: }
}
}
MANIFEST
expected_relationships << ['s1', 't2']
end
it "should create relationships using array members" do
code << <<-MANIFEST
$var = [ [ [ File[a], File[b] ] ] ]
$var[0][0][0] -> $var[0][0][1]
MANIFEST
expected_relationships << ['a', 'b']
end
it "should create relationships using hash members" do
code << <<-MANIFEST
$var = {'foo' => {'bar' => {'source' => File[a], 'target' => File[b]}}}
$var[foo][bar][source] -> $var[foo][bar][target]
MANIFEST
expected_relationships << ['a', 'b']
end
it "should create relationships using resource declarations" do
code << "file { l: } -> file { r: }"
expected_relationships << ['l', 'r']
end
it "should chain relationships" do
code << "File[a] -> File[b] ~> File[c] <- File[d] <~ File[e]"
expected_relationships << ['a', 'b'] << ['d', 'c']
expected_subscriptions << ['b', 'c'] << ['e', 'd']
end
end
+ context 'when working with immutable node data' do
+ context 'and have opted in to immutable_node_data' do
+ before :each do
+ Puppet[:immutable_node_data] = true
+ end
+
+ def node_with_facts(facts)
+ Puppet[:facts_terminus] = :memory
+ Puppet::Node::Facts.indirection.save(Puppet::Node::Facts.new("testing", facts))
+ node = Puppet::Node.new("testing")
+ node.fact_merge
+ node
+ end
+
+ matcher :fail_compile_with do |node, message_regex|
+ match do |manifest|
+ @error = nil
+ begin
+ compile_to_catalog(manifest, node)
+ false
+ rescue Puppet::Error => e
+ @error = e
+ message_regex.match(e.message)
+ end
+ end
+
+ failure_message_for_should do
+ if @error
+ "failed with #{@error}\n#{@error.backtrace}"
+ else
+ "did not fail"
+ end
+ end
+ end
+
+ it 'should make $facts available' do
+ node = node_with_facts('the_facts' => 'straight')
+
+ catalog = compile_to_catalog(<<-MANIFEST, node)
+ notify { 'test': message => $facts[the_facts] }
+ MANIFEST
+
+ catalog.resource("Notify[test]")[:message].should == "straight"
+ end
+
+ it 'should make $facts reserved' do
+ node = node_with_facts('the_facts' => 'straight')
+
+ expect('$facts = {}').to fail_compile_with(node, /assign to a reserved variable name: 'facts'/)
+ expect('class a { $facts = {} } include a').to fail_compile_with(node, /assign to a reserved variable name: 'facts'/)
+ end
+
+ it 'should make $facts immutable' do
+ node = node_with_facts('string' => 'value', 'array' => ['string'], 'hash' => { 'a' => 'string' }, 'number' => 1, 'boolean' => true)
+
+ expect('$i=inline_template("<% @facts[%q{new}] = 2 %>")').to fail_compile_with(node, /frozen Hash/i)
+ expect('$i=inline_template("<% @facts[%q{string}].chop! %>")').to fail_compile_with(node, /frozen String/i)
+
+ expect('$i=inline_template("<% @facts[%q{array}][0].chop! %>")').to fail_compile_with(node, /frozen String/i)
+ expect('$i=inline_template("<% @facts[%q{array}][1] = 2 %>")').to fail_compile_with(node, /frozen Array/i)
+
+ expect('$i=inline_template("<% @facts[%q{hash}][%q{a}].chop! %>")').to fail_compile_with(node, /frozen String/i)
+ expect('$i=inline_template("<% @facts[%q{hash}][%q{b}] = 2 %>")').to fail_compile_with(node, /frozen Hash/i)
+ end
+
+ it 'should make $facts available even if there are no facts' do
+ Puppet[:facts_terminus] = :memory
+ node = Puppet::Node.new("testing2")
+ node.fact_merge
+
+ catalog = compile_to_catalog(<<-MANIFEST, node)
+ notify { 'test': message => $facts }
+ MANIFEST
+
+ expect(catalog).to have_resource("Notify[test]").with_parameter(:message, {})
+ end
+ end
+
+ context 'and have not opted in to immutable_node_data' do
+ before :each do
+ Puppet[:immutable_node_data] = false
+ end
+
+ it 'should not make $facts available' do
+ Puppet[:facts_terminus] = :memory
+ facts = Puppet::Node::Facts.new("testing", 'the_facts' => 'straight')
+ Puppet::Node::Facts.indirection.save(facts)
+ node = Puppet::Node.new("testing")
+ node.fact_merge
+
+ catalog = compile_to_catalog(<<-MANIFEST, node)
+ notify { 'test': message => "An $facts space" }
+ MANIFEST
+
+ catalog.resource("Notify[test]")[:message].should == "An space"
+ end
+ 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
before :each do
Puppet[:parser] = 'current'
end
it_behaves_like 'the compiler' do
end
end
-
- describe 'using future parser' do
- # have absolutely no clue to why this is needed - if not required here (even if required by used classes)
- # the tests will fail with error that rgen/ecore/ruby_to_ecore cannot be found...
- # TODO: Solve this mystery !
- require 'rgen/metamodel_builder'
-
- before :each do
- Puppet[:parser] = 'future'
- end
- it_behaves_like 'the compiler'
- end
end
diff --git a/spec/integration/parser/compiler_spec.rb b/spec/integration/parser/future_compiler_spec.rb
old mode 100755
new mode 100644
similarity index 72%
copy from spec/integration/parser/compiler_spec.rb
copy to spec/integration/parser/future_compiler_spec.rb
index eb87d03ed..5e56a7459
--- a/spec/integration/parser/compiler_spec.rb
+++ b/spec/integration/parser/future_compiler_spec.rb
@@ -1,424 +1,416 @@
#! /usr/bin/env ruby
require 'spec_helper'
+require 'puppet/pops'
require 'puppet/parser/parser_factory'
require 'puppet_spec/compiler'
+require 'puppet_spec/pops'
+require 'puppet_spec/scope'
+require 'rgen/metamodel_builder'
+# Test compilation using the future evaluator
+#
describe "Puppet::Parser::Compiler" do
include PuppetSpec::Compiler
before :each do
- @node = Puppet::Node.new "testnode"
+ Puppet[:parser] = 'future'
+ # This is in the original test - what is this for? Does not seem to make a difference at all
@scope_resource = stub 'scope_resource', :builtin? => true, :finish => nil, :ref => 'Class[main]'
@scope = stub 'scope', :resource => @scope_resource, :source => mock("source")
end
+
after do
Puppet.settings.clear
end
- # shared because tests are invoked both for classic and future parser
- #
- shared_examples_for "the compiler" do
+ describe "the compiler when using future parser and evaluator" do
it "should be able to determine the configuration version from a local version control repository" do
pending("Bug #14071 about semantics of Puppet::Util::Execute on Windows", :if => Puppet.features.microsoft_windows?) do
# This should always work, because we should always be
# in the puppet repo when we run this.
version = %x{git rev-parse HEAD}.chomp
Puppet.settings[:config_version] = 'git rev-parse HEAD'
- @parser = Puppet::Parser::ParserFactory.parser "development"
- @compiler = Puppet::Parser::Compiler.new(@node)
-
- @compiler.catalog.version.should == version
+ parser = Puppet::Parser::ParserFactory.parser "development"
+ compiler = Puppet::Parser::Compiler.new(Puppet::Node.new("testnode"))
+ compiler.catalog.version.should == version
end
end
it "should not create duplicate resources when a class is referenced both directly and indirectly by the node classifier (4792)" do
Puppet[:code] = <<-PP
class foo
{
notify { foo_notify: }
include bar
}
class bar
{
notify { bar_notify: }
}
PP
- @node.stubs(:classes).returns(['foo', 'bar'])
-
- catalog = Puppet::Parser::Compiler.compile(@node)
-
+ node = Puppet::Node.new("testnodex")
+ node.classes = ['foo', 'bar']
+ catalog = Puppet::Parser::Compiler.compile(node)
+ node.classes = nil
catalog.resource("Notify[foo_notify]").should_not be_nil
catalog.resource("Notify[bar_notify]").should_not be_nil
end
describe "when resolving class references" do
it "should favor local scope, even if there's an included class in topscope" do
Puppet[:code] = <<-PP
class experiment {
class baz {
}
notify {"x" : require => Class[Baz] }
}
class baz {
}
include baz
include experiment
include experiment::baz
PP
catalog = Puppet::Parser::Compiler.compile(Puppet::Node.new("mynode"))
notify_resource = catalog.resource( "Notify[x]" )
notify_resource[:require].title.should == "Experiment::Baz"
end
it "should favor local scope, even if there's an unincluded class in topscope" do
Puppet[:code] = <<-PP
class experiment {
class baz {
}
notify {"x" : require => Class[Baz] }
}
class baz {
}
include experiment
include experiment::baz
PP
catalog = Puppet::Parser::Compiler.compile(Puppet::Node.new("mynode"))
notify_resource = catalog.resource( "Notify[x]" )
notify_resource[:require].title.should == "Experiment::Baz"
end
end
describe "(ticket #13349) when explicitly specifying top scope" do
["class {'::bar::baz':}", "include ::bar::baz"].each do |include|
describe "with #{include}" do
it "should find the top level class" do
Puppet[:code] = <<-MANIFEST
class { 'foo::test': }
class foo::test {
#{include}
}
class bar::baz {
notify { 'good!': }
}
class foo::bar::baz {
notify { 'bad!': }
}
MANIFEST
catalog = Puppet::Parser::Compiler.compile(Puppet::Node.new("mynode"))
catalog.resource("Class[Bar::Baz]").should_not be_nil
catalog.resource("Notify[good!]").should_not be_nil
catalog.resource("Class[Foo::Bar::Baz]").should be_nil
catalog.resource("Notify[bad!]").should be_nil
end
end
end
end
it "should recompute the version after input files are re-parsed" do
Puppet[:code] = 'class foo { }'
Time.stubs(:now).returns(1)
node = Puppet::Node.new('mynode')
Puppet::Parser::Compiler.compile(node).version.should == 1
Time.stubs(:now).returns(2)
Puppet::Parser::Compiler.compile(node).version.should == 1 # no change because files didn't change
Puppet::Resource::TypeCollection.any_instance.stubs(:stale?).returns(true).then.returns(false) # pretend change
Puppet::Parser::Compiler.compile(node).version.should == 2
end
- ['class', 'define', 'node'].each do |thing|
- it "should not allow #{thing} inside evaluated conditional constructs" do
+ ['define', 'class', 'node'].each do |thing|
+ it "'#{thing}' is not allowed inside evaluated conditional constructs" do
Puppet[:code] = <<-PP
if true {
#{thing} foo {
}
notify { decoy: }
}
PP
begin
- Puppet::Parser::Compiler.compile(Puppet::Node.new("mynode"))
+ catalog = Puppet::Parser::Compiler.compile(Puppet::Node.new("mynode"))
raise "compilation should have raised Puppet::Error"
rescue Puppet::Error => e
- e.message.should =~ /at line 2/
+ e.message.should =~ /Classes, definitions, and nodes may only appear at toplevel/
end
end
end
- it "should not allow classes inside unevaluated conditional constructs" do
- Puppet[:code] = <<-PP
- if false {
- class foo {
+ ['define', 'class', 'node'].each do |thing|
+ it "'#{thing}' is not allowed inside un-evaluated conditional constructs" do
+ Puppet[:code] = <<-PP
+ if false {
+ #{thing} foo {
+ }
+ notify { decoy: }
}
- }
- PP
+ PP
- lambda { Puppet::Parser::Compiler.compile(Puppet::Node.new("mynode")) }.should raise_error(Puppet::Error)
+ begin
+ catalog = Puppet::Parser::Compiler.compile(Puppet::Node.new("mynode"))
+ raise "compilation should have raised Puppet::Error"
+ rescue Puppet::Error => e
+ e.message.should =~ /Classes, definitions, and nodes may only appear at toplevel/
+ end
+ end
end
- describe "when defining relationships" do
+ describe "relationships can be formed" do
def extract_name(ref)
ref.sub(/File\[(\w+)\]/, '\1')
end
let(:node) { Puppet::Node.new('mynode') }
let(:code) do
<<-MANIFEST
file { [a,b,c]:
mode => 0644,
}
file { [d,e]:
mode => 0755,
}
MANIFEST
end
let(:expected_relationships) { [] }
let(:expected_subscriptions) { [] }
before :each do
+ Puppet[:parser] = 'future'
Puppet[:code] = code
end
after :each do
catalog = Puppet::Parser::Compiler.compile(node)
resources = catalog.resources.select { |res| res.type == 'File' }
actual_relationships, actual_subscriptions = [:before, :notify].map do |relation|
resources.map do |res|
dependents = Array(res[relation])
dependents.map { |ref| [res.title, extract_name(ref)] }
end.inject(&:concat)
end
actual_relationships.should =~ expected_relationships
actual_subscriptions.should =~ expected_subscriptions
end
- it "should create a relationship" do
+ it "of regular type" do
code << "File[a] -> File[b]"
expected_relationships << ['a','b']
end
- it "should create a subscription" do
+ it "of subscription type" do
code << "File[a] ~> File[b]"
expected_subscriptions << ['a', 'b']
end
- it "should create relationships using title arrays" do
+ it "between multiple resources expressed as resource with multiple titles" do
code << "File[a,b] -> File[c,d]"
expected_relationships.concat [
['a', 'c'],
['b', 'c'],
['a', 'd'],
['b', 'd'],
]
end
- it "should create relationships using collection expressions" do
+ it "between collection expressions" do
code << "File <| mode == 0644 |> -> File <| mode == 0755 |>"
expected_relationships.concat [
['a', 'd'],
['b', 'd'],
['c', 'd'],
['a', 'e'],
['b', 'e'],
['c', 'e'],
]
end
- it "should create relationships using resource names" do
+ it "between resources expressed as Strings" do
code << "'File[a]' -> 'File[b]'"
expected_relationships << ['a', 'b']
end
- it "should create relationships using variables" do
+ it "between resources expressed as variables" do
code << <<-MANIFEST
$var = File[a]
$var -> File[b]
MANIFEST
expected_relationships << ['a', 'b']
end
- it "should create relationships using case statements" do
+ it "between resources expressed as case statements" do
code << <<-MANIFEST
$var = 10
case $var {
10: {
file { s1: }
}
12: {
file { s2: }
}
}
->
case $var + 2 {
10: {
file { t1: }
}
12: {
file { t2: }
}
}
MANIFEST
expected_relationships << ['s1', 't2']
end
- it "should create relationships using array members" do
+ it "using deep access in array" do
code << <<-MANIFEST
$var = [ [ [ File[a], File[b] ] ] ]
$var[0][0][0] -> $var[0][0][1]
MANIFEST
expected_relationships << ['a', 'b']
end
- it "should create relationships using hash members" do
+ it "using deep access in hash" do
code << <<-MANIFEST
$var = {'foo' => {'bar' => {'source' => File[a], 'target' => File[b]}}}
$var[foo][bar][source] -> $var[foo][bar][target]
MANIFEST
expected_relationships << ['a', 'b']
end
- it "should create relationships using resource declarations" do
+ it "using resource declarations" do
code << "file { l: } -> file { r: }"
expected_relationships << ['l', 'r']
end
- it "should chain relationships" do
+ it "between entries in a chain of relationships" do
code << "File[a] -> File[b] ~> File[c] <- File[d] <~ File[e]"
expected_relationships << ['a', 'b'] << ['d', 'c']
expected_subscriptions << ['b', 'c'] << ['e', 'd']
end
end
+ context "when dealing with variable references" do
+ it 'an initial underscore in a variable name is ok' do
+ node = Puppet::Node.new("testing_x")
+ catalog = compile_to_catalog(<<-MANIFEST, node)
+ class a { $_a = 10}
+ include a
+ notify { 'test': message => $a::_a }
+ MANIFEST
+
+ catalog.resource("Notify[test]")[:message].should == 10
+ end
+
+ it 'an initial underscore in not ok if elsewhere than last segment' do
+ node = Puppet::Node.new("testing_x")
+ expect {
+ catalog = compile_to_catalog(<<-MANIFEST, node)
+ class a { $_a = 10}
+ include a
+ notify { 'test': message => $_a::_a }
+ MANIFEST
+ }.to raise_error(/Illegal variable name/)
+ end
+ end
+
context 'when working with the trusted data hash' do
- context 'and have opted in to trusted_node_data' do
+ context 'and have opted in to hashed_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
+ context 'and have not opted in to hashed_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 }
+ 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
- before :each do
- Puppet[:parser] = 'current'
- end
- it_behaves_like 'the compiler' do
- end
- end
-
- describe 'using future parser' do
- # have absolutely no clue to why this is needed - if not required here (even if required by used classes)
- # the tests will fail with error that rgen/ecore/ruby_to_ecore cannot be found...
- # TODO: Solve this mystery !
- require 'rgen/metamodel_builder'
-
- before :each do
- Puppet[:parser] = 'future'
- end
- it_behaves_like 'the compiler'
- end
end
diff --git a/spec/integration/parser/parser_spec.rb b/spec/integration/parser/parser_spec.rb
index 280fd040f..a6709fc31 100755
--- a/spec/integration/parser/parser_spec.rb
+++ b/spec/integration/parser/parser_spec.rb
@@ -1,268 +1,207 @@
#! /usr/bin/env ruby
require 'spec_helper'
require 'puppet/parser/parser_factory'
describe "Puppet::Parser::Parser" do
module ParseMatcher
class ParseAs
def initialize(klass)
@parser = Puppet::Parser::ParserFactory.parser("development")
@class = klass
end
def result_instance
@result.code[0]
end
def matches?(string)
@string = string
@result = @parser.parse(string)
result_instance.instance_of?(@class)
end
def description
"parse as a #{@class}"
end
def failure_message
" expected #{@string} to parse as #{@class} but was #{result_instance.class}"
end
def negative_failure_message
" expected #{@string} not to parse as #{@class}"
end
end
def parse_as(klass)
ParseAs.new(klass)
end
class ParseWith
def initialize(block)
@parser = Puppet::Parser::ParserFactory.parser("development")
@block = block
end
def result_instance
@result.code[0]
end
def matches?(string)
@string = string
@result = @parser.parse(string)
@block.call(result_instance)
end
def description
"parse with the block evaluating to true"
end
def failure_message
" expected #{@string} to parse with a true result in the block"
end
def negative_failure_message
" expected #{@string} not to parse with a true result in the block"
end
end
def parse_with(&block)
ParseWith.new(block)
end
end
include ParseMatcher
before :each do
@resource_type_collection = Puppet::Resource::TypeCollection.new("env")
@parser = Puppet::Parser::ParserFactory.parser("development")
# @parser = Puppet::Parser::Parser.new "development"
end
shared_examples_for 'a puppet parser' do
- describe "when parsing comments before statement" do
+ describe "when parsing comments before a statement" do
it "should associate the documentation to the statement AST node" do
if Puppet[:parser] == 'future'
pending "egrammar does not yet process comments"
end
ast = @parser.parse("""
# comment
- class test {}
+ class test {
+ $foo = {bar => 23}
+ $bar = [23, 42]
+ $x = 'argument'
+ # this comment should not be returned
+ some_function('with', {a => 'hash'},
+ ['and', 1, 'array', $argument],
+ ) # not?
+ }
""")
ast.code[0].should be_a(Puppet::Parser::AST::Hostclass)
ast.code[0].name.should == 'test'
ast.code[0].instantiate('')[0].doc.should == "comment\n"
end
+
+ { "an empty hash" => "{}",
+ "a simple hash" => "{ 'key' => 'value' }",
+ "a nested hash" => "{ 'first' => $x, 'second' => { a => 1, b => 2 } }"
+ }.each_pair do |hash_desc, hash_expr|
+ context "in the presence of #{hash_desc}" do
+ { "a parameter default" => "class test($param = #{hash_expr}) { }",
+ "a parameter value" => "foo { 'bar': options => #{hash_expr} }",
+ "an plusignment rvalue" => "Foo['bar'] { options +> #{hash_expr} }",
+ "an assignment rvalue" => "$x = #{hash_expr}",
+ "an inequality rvalue" => "if $x != #{hash_expr} { }",
+ "an function argument in parenthesis" => "flatten(#{hash_expr})",
+ "a second argument" => "merge($x, #{hash_expr})",
+ }.each_pair do |dsl_desc, dsl_expr|
+ context "as #{dsl_desc}" do
+ it "should associate the docstring to the container" do
+ ast = @parser.parse("# comment\nclass container { #{dsl_expr} }\n")
+ ast.code[0].instantiate('')[0].doc.should == "comment\n"
+ end
+ end
+ end
+ # Pending, these syntaxes are not yet supported in 3.x
+ #
+ # @todo Merge these into the test above after the migration to the new
+ # parser is complete.
+ { "a selector alternative" => "$opt ? { { 'a' => 1 } => true, default => false }",
+ "an argument without parenthesis" => "flatten { 'a' => 1 }",
+ }.each_pair do |dsl_desc, dsl_expr|
+ context "as #{dsl_desc}" do
+ it "should associate the docstring to the container"
+ end
+ end
+ end
+ end
end
describe "when parsing" do
it "should be able to parse normal left to right relationships" do
"Notify[foo] -> Notify[bar]".should parse_as(Puppet::Parser::AST::Relationship)
end
it "should be able to parse right to left relationships" do
"Notify[foo] <- Notify[bar]".should parse_as(Puppet::Parser::AST::Relationship)
end
it "should be able to parse normal left to right subscriptions" do
"Notify[foo] ~> Notify[bar]".should parse_as(Puppet::Parser::AST::Relationship)
end
it "should be able to parse right to left subscriptions" do
"Notify[foo] <~ Notify[bar]".should parse_as(Puppet::Parser::AST::Relationship)
end
it "should correctly set the arrow type of a relationship" do
"Notify[foo] <~ Notify[bar]".should parse_with { |rel| rel.arrow == "<~" }
end
it "should be able to parse deep hash access" do
%q{
$hash = { 'a' => { 'b' => { 'c' => 'it works' } } }
$out = $hash['a']['b']['c']
}.should parse_with { |v| v.value.is_a?(Puppet::Parser::AST::ASTHash) }
end
it "should fail if asked to parse '$foo::::bar'" do
expect { @parser.parse("$foo::::bar") }.to raise_error(Puppet::ParseError, /Syntax error at ':'/)
end
describe "function calls" do
it "should be able to pass an array to a function" do
"my_function([1,2,3])".should parse_with { |fun|
fun.is_a?(Puppet::Parser::AST::Function) &&
fun.arguments[0].evaluate(stub 'scope') == ['1','2','3']
}
end
it "should be able to pass a hash to a function" do
"my_function({foo => bar})".should parse_with { |fun|
fun.is_a?(Puppet::Parser::AST::Function) &&
fun.arguments[0].evaluate(stub 'scope') == {'foo' => 'bar'}
}
end
end
describe "collections" do
it "should find resources according to an expression" do
%q{ File <| mode == 0700 + 0050 + 0050 |> }.should parse_with { |coll|
coll.is_a?(Puppet::Parser::AST::Collection) &&
coll.query.evaluate(stub 'scope').first == ["mode", "==", 0700 + 0050 + 0050]
}
end
end
end
end
describe 'using classic parser' do
before :each do
Puppet[:parser] = 'current'
end
it_behaves_like 'a puppet parser'
end
- describe 'using future parser' do
- before :each do
- Puppet[:parser] = 'future'
- end
- it_behaves_like 'a puppet parser'
-
- context 'more detailed errors should be generated' do
- before :each do
- Puppet[:parser] = 'future'
- @resource_type_collection = Puppet::Resource::TypeCollection.new("env")
- @parser = Puppet::Parser::ParserFactory.parser("development")
- end
-
- it 'should flag illegal type references' do
- source = <<-SOURCE.gsub(/^ {8}/,'')
- 1+1 { "title": }
- SOURCE
- # This error message is currently produced by the parser, and is not as detailed as desired
- # It references position 16 at the closing '}'
- expect { @parser.parse(source) }.to raise_error(/Expression is not valid as a resource.*line 1:16/)
- end
-
- it 'should flag illegal type references and get position correct' do
- source = <<-SOURCE.gsub(/^ {8}/,'')
- 1+1 { "title":
- }
- SOURCE
- # This error message is currently produced by the parser, and is not as detailed as desired
- # It references position 16 at the closing '}'
- expect { @parser.parse(source) }.to raise_error(/Expression is not valid as a resource.*line 2:3/)
- end
-
- it 'should flag illegal use of non r-value producing if' do
- source = <<-SOURCE.gsub(/^ {8}/,'')
- $a = if true {
- false
- }
- SOURCE
- expect { @parser.parse(source) }.to raise_error(/An 'if' statement does not produce a value at line 1:6/)
- end
-
- it 'should flag illegal use of non r-value producing case' do
- source = <<-SOURCE.gsub(/^ {8}/,'')
- $a = case true {
- false :{ }
- }
- SOURCE
- expect { @parser.parse(source) }.to raise_error(/A 'case' statement 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(/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/)
- end
-
- it 'should flag illegal use of non r-value producing define' do
- Puppet.expects(:err).with("Invalid use of expression. A 'define' expression does not produce a value at line 1:6")
- Puppet.expects(:err).with("Classes, definitions, and nodes may only appear at toplevel or inside other classes at line 1:6")
- expect { @parser.parse("$a = define foo { }") }.to raise_error(/2 errors/)
- end
-
- it 'should flag illegal use of non r-value producing class' do
- Puppet.expects(:err).with("Invalid use of expression. A Host Class Definition does not produce a value at line 1:6")
- Puppet.expects(:err).with("Classes, definitions, and nodes may only appear at toplevel or inside other classes at line 1:6")
- expect { @parser.parse("$a = class foo { }") }.to raise_error(/2 errors/)
- end
-
- it 'unclosed quote should be flagged for start position of string' do
- source = <<-SOURCE.gsub(/^ {8}/,'')
- $a = "xx
- yyy
- SOURCE
- expect { @parser.parse(source) }.to raise_error(/Unclosed quote after '"' followed by 'xx\\nyy\.\.\.' at line 1:6/)
- end
-
- it 'can produce multiple errors and raise a summary exception' do
- source = <<-SOURCE.gsub(/^ {8}/,'')
- $a = node x { }
- SOURCE
- Puppet.expects(:err).with("Invalid use of expression. A Node Definition does not produce a value at line 1:6")
- Puppet.expects(:err).with("Classes, definitions, and nodes may only appear at toplevel or inside other classes at line 1:6")
- expect { @parser.parse(source) }.to raise_error(/2 errors/)
- end
-
- it 'can produce detailed error for a bad hostname' do
- source = <<-SOURCE.gsub(/^ {8}/,'')
- node 'macbook+owned+by+name' { }
- SOURCE
- expect { @parser.parse(source) }.to raise_error(/The hostname 'macbook\+owned\+by\+name' contains illegal characters.*at line 1:6/)
- end
-
- it 'can produce detailed error for a hostname with interpolation' do
- source = <<-SOURCE.gsub(/^ {8}/,'')
- $name = 'fred'
- node "macbook-owned-by$name" { }
- SOURCE
- expect { @parser.parse(source) }.to raise_error(/An interpolated expression is not allowed in a hostname of a node at line 2:24/)
- end
- end
- end
end
diff --git a/spec/integration/parser/scope_spec.rb b/spec/integration/parser/scope_spec.rb
index 8df54393b..6158b09ff 100644
--- a/spec/integration/parser/scope_spec.rb
+++ b/spec/integration/parser/scope_spec.rb
@@ -1,671 +1,757 @@
require 'spec_helper'
require 'puppet_spec/compiler'
describe "Two step scoping for variables" do
include PuppetSpec::Compiler
def expect_the_message_to_be(message, node = Puppet::Node.new('the node'))
catalog = compile_to_catalog(yield, node)
catalog.resource('Notify', 'something')[:message].should == message
end
before :each do
Puppet.expects(:deprecation_warning).never
end
- describe "fully qualified variable names" do
- it "keeps nodescope separate from topscope" do
- expect_the_message_to_be('topscope') do <<-MANIFEST
- $c = "topscope"
- node default {
- $c = "nodescope"
- notify { 'something': message => $::c }
- }
- MANIFEST
+ context 'using current parser' do
+ describe "using plussignment to change in a new scope" do
+ it "does not change a string in the parent scope" do
+ # Expects to be able to concatenate string using +=
+ expect_the_message_to_be('top_msg') do <<-MANIFEST
+ $var = "top_msg"
+ class override {
+ $var += "override"
+ include foo
+ }
+ class foo {
+ notify { 'something': message => $var, }
+ }
+
+ include override
+ MANIFEST
+ end
end
end
end
- describe "when colliding class and variable names" do
- it "finds a topscope variable with the same name as a class" do
- expect_the_message_to_be('topscope') do <<-MANIFEST
- $c = "topscope"
- class c { }
- node default {
- include c
- notify { 'something': message => $c }
- }
- MANIFEST
- end
+ context 'using future parser' do
+ before(:each) do
+ Puppet[:parser] = 'future'
end
- it "finds a node scope variable with the same name as a class" do
- expect_the_message_to_be('nodescope') do <<-MANIFEST
- class c { }
- node default {
- $c = "nodescope"
- include c
- notify { 'something': message => $c }
- }
- MANIFEST
- end
- end
+ describe "using plussignment to change in a new scope" do
+ it "does not change a string in the parent scope" do
+ # Expects to be able to concatenate string using +=
+ expect do
+ catalog = compile_to_catalog(<<-MANIFEST, Puppet::Node.new('the node'))
+ $var = "top_msg"
+ class override {
+ $var += "override"
+ include foo
+ }
+ class foo {
+ notify { 'something': message => $var, }
+ }
- it "finds a class variable when the class collides with a nodescope variable" do
- expect_the_message_to_be('class') do <<-MANIFEST
- class c { $b = "class" }
- node default {
- $c = "nodescope"
- include c
- notify { 'something': message => $c::b }
- }
- MANIFEST
+ include override
+ MANIFEST
+ end.to raise_error(/The value 'top_msg' cannot be converted to Numeric/)
end
end
- it "finds a class variable when the class collides with a topscope variable" do
- expect_the_message_to_be('class') do <<-MANIFEST
- $c = "topscope"
- class c { $b = "class" }
+ it "when using a template ignores the dynamic value of the var when using the @varname syntax" do
+ expect_the_message_to_be('node_msg') do <<-MANIFEST
node default {
- include c
- notify { 'something': message => $::c::b }
- }
- MANIFEST
- end
- end
- end
-
- describe "when using shadowing and inheritance" do
- it "finds value define in the inherited node" do
- expect_the_message_to_be('parent_msg') do <<-MANIFEST
- $var = "top_msg"
- node parent {
- $var = "parent_msg"
- }
- node default inherits parent {
+ $var = "node_msg"
include foo
}
class foo {
- notify { 'something': message => $var, }
+ $var = "foo_msg"
+ include bar
+ }
+ class bar {
+ notify { 'something': message => inline_template("<%= @var %>"), }
}
MANIFEST
end
end
+ end
- it "finds top scope when the class is included before the node defines the var" do
- expect_the_message_to_be('top_msg') do <<-MANIFEST
- $var = "top_msg"
- node parent {
- include foo
- }
- node default inherits parent {
- $var = "default_msg"
- }
- class foo {
- notify { 'something': message => $var, }
- }
- MANIFEST
+ shared_examples_for "the scope" do
+
+ describe "fully qualified variable names" do
+ it "keeps nodescope separate from topscope" do
+ expect_the_message_to_be('topscope') do <<-MANIFEST
+ $c = "topscope"
+ node default {
+ $c = "nodescope"
+ notify { 'something': message => $::c }
+ }
+ MANIFEST
+ end
end
end
- it "finds top scope when the class is included before the node defines the var" do
- expect_the_message_to_be('top_msg') do <<-MANIFEST
- $var = "top_msg"
- node parent {
- include foo
- }
- node default inherits parent {
- $var = "default_msg"
- }
- class foo {
- notify { 'something': message => $var, }
- }
- MANIFEST
+ describe "when colliding class and variable names" do
+ it "finds a topscope variable with the same name as a class" do
+ expect_the_message_to_be('topscope') do <<-MANIFEST
+ $c = "topscope"
+ class c { }
+ node default {
+ include c
+ notify { 'something': message => $c }
+ }
+ MANIFEST
+ end
end
- end
- it "finds values in its local scope" do
- expect_the_message_to_be('local_msg') do <<-MANIFEST
- node default {
- include baz
- }
- class foo {
- }
- class bar inherits foo {
- $var = "local_msg"
- notify { 'something': message => $var, }
- }
- class baz {
- include bar
- }
- MANIFEST
+ it "finds a node scope variable with the same name as a class" do
+ expect_the_message_to_be('nodescope') do <<-MANIFEST
+ class c { }
+ node default {
+ $c = "nodescope"
+ include c
+ notify { 'something': message => $c }
+ }
+ MANIFEST
+ end
end
- end
- it "finds values in its inherited scope" do
- expect_the_message_to_be('foo_msg') do <<-MANIFEST
- node default {
- include baz
- }
- class foo {
- $var = "foo_msg"
- }
- class bar inherits foo {
- notify { 'something': message => $var, }
- }
- class baz {
- include bar
- }
- MANIFEST
+ it "finds a class variable when the class collides with a nodescope variable" do
+ expect_the_message_to_be('class') do <<-MANIFEST
+ class c { $b = "class" }
+ node default {
+ $c = "nodescope"
+ include c
+ notify { 'something': message => $c::b }
+ }
+ MANIFEST
+ end
end
- end
- it "prefers values in its local scope over values in the inherited scope" do
- expect_the_message_to_be('local_msg') do <<-MANIFEST
- include bar
+ it "finds a class variable when the class collides with a topscope variable" do
+ expect_the_message_to_be('class') do <<-MANIFEST
+ $c = "topscope"
+ class c { $b = "class" }
+ node default {
+ include c
+ notify { 'something': message => $::c::b }
+ }
+ MANIFEST
+ end
+ end
+ end
- class foo {
- $var = "inherited"
- }
+ describe "when using shadowing and inheritance" do
+ it "finds value define in the inherited node" do
+ expect_the_message_to_be('parent_msg') do <<-MANIFEST
+ $var = "top_msg"
+ node parent {
+ $var = "parent_msg"
+ }
+ node default inherits parent {
+ include foo
+ }
+ class foo {
+ notify { 'something': message => $var, }
+ }
+ MANIFEST
+ end
+ end
- class bar inherits foo {
- $var = "local_msg"
- notify { 'something': message => $var, }
- }
- MANIFEST
+ it "finds top scope when the class is included before the node defines the var" do
+ expect_the_message_to_be('top_msg') do <<-MANIFEST
+ $var = "top_msg"
+ node parent {
+ include foo
+ }
+ node default inherits parent {
+ $var = "default_msg"
+ }
+ class foo {
+ notify { 'something': message => $var, }
+ }
+ MANIFEST
+ end
end
- end
- it "finds a qualified variable by following parent scopes of the specified scope" do
- expect_the_message_to_be("from node") do <<-MANIFEST
- class c {
- notify { 'something': message => "$a::b" }
- }
+ it "finds top scope when the class is included before the node defines the var" do
+ expect_the_message_to_be('top_msg') do <<-MANIFEST
+ $var = "top_msg"
+ node parent {
+ include foo
+ }
+ node default inherits parent {
+ $var = "default_msg"
+ }
+ class foo {
+ notify { 'something': message => $var, }
+ }
+ MANIFEST
+ end
+ end
- class a { }
+ it "finds values in its local scope" do
+ expect_the_message_to_be('local_msg') do <<-MANIFEST
+ node default {
+ include baz
+ }
+ class foo {
+ }
+ class bar inherits foo {
+ $var = "local_msg"
+ notify { 'something': message => $var, }
+ }
+ class baz {
+ include bar
+ }
+ MANIFEST
+ end
+ end
- node default {
- $b = "from node"
- include a
- include c
- }
- MANIFEST
+ it "finds values in its inherited scope" do
+ expect_the_message_to_be('foo_msg') do <<-MANIFEST
+ node default {
+ include baz
+ }
+ class foo {
+ $var = "foo_msg"
+ }
+ class bar inherits foo {
+ notify { 'something': message => $var, }
+ }
+ class baz {
+ include bar
+ }
+ MANIFEST
+ end
end
- end
- it "finds values in its inherited scope when the inherited class is qualified to the top" do
- expect_the_message_to_be('foo_msg') do <<-MANIFEST
- node default {
- include baz
- }
- class foo {
- $var = "foo_msg"
- }
- class bar inherits ::foo {
- notify { 'something': message => $var, }
- }
- class baz {
+ it "prefers values in its local scope over values in the inherited scope" do
+ expect_the_message_to_be('local_msg') do <<-MANIFEST
include bar
- }
- MANIFEST
+
+ class foo {
+ $var = "inherited"
+ }
+
+ class bar inherits foo {
+ $var = "local_msg"
+ notify { 'something': message => $var, }
+ }
+ MANIFEST
+ end
end
- end
- it "prefers values in its local scope over values in the inherited scope when the inherited class is fully qualified" do
- expect_the_message_to_be('local_msg') do <<-MANIFEST
- include bar
+ it "finds a qualified variable by following parent scopes of the specified scope" do
+ expect_the_message_to_be("from node") do <<-MANIFEST
+ class c {
+ notify { 'something': message => "$a::b" }
+ }
- class foo {
- $var = "inherited"
- }
+ class a { }
- class bar inherits ::foo {
- $var = "local_msg"
- notify { 'something': message => $var, }
- }
- MANIFEST
+ node default {
+ $b = "from node"
+ include a
+ include c
+ }
+ MANIFEST
+ end
end
- end
- it "finds values in top scope when the inherited class is qualified to the top" do
- expect_the_message_to_be('top msg') do <<-MANIFEST
- $var = "top msg"
- class foo {
- }
+ it "finds values in its inherited scope when the inherited class is qualified to the top" do
+ expect_the_message_to_be('foo_msg') do <<-MANIFEST
+ node default {
+ include baz
+ }
+ class foo {
+ $var = "foo_msg"
+ }
+ class bar inherits ::foo {
+ notify { 'something': message => $var, }
+ }
+ class baz {
+ include bar
+ }
+ MANIFEST
+ end
+ end
- class bar inherits ::foo {
- notify { 'something': message => $var, }
- }
+ it "prefers values in its local scope over values in the inherited scope when the inherited class is fully qualified" do
+ expect_the_message_to_be('local_msg') do <<-MANIFEST
+ include bar
- include bar
- MANIFEST
+ class foo {
+ $var = "inherited"
+ }
+
+ class bar inherits ::foo {
+ $var = "local_msg"
+ notify { 'something': message => $var, }
+ }
+ MANIFEST
+ end
end
- end
- it "finds values in its inherited scope when the inherited class is a nested class that shadows another class at the top" do
- expect_the_message_to_be('inner baz') do <<-MANIFEST
- node default {
- include foo::bar
- }
- class baz {
- $var = "top baz"
- }
- class foo {
- class baz {
- $var = "inner baz"
+ it "finds values in top scope when the inherited class is qualified to the top" do
+ expect_the_message_to_be('top msg') do <<-MANIFEST
+ $var = "top msg"
+ class foo {
}
- class bar inherits baz {
+ class bar inherits ::foo {
notify { 'something': message => $var, }
}
- }
- MANIFEST
+
+ include bar
+ MANIFEST
+ end
end
- end
- it "finds values in its inherited scope when the inherited class is qualified to a nested class and qualified to the top" do
- expect_the_message_to_be('top baz') do <<-MANIFEST
- node default {
- include foo::bar
- }
- class baz {
- $var = "top baz"
- }
- class foo {
+ it "finds values in its inherited scope when the inherited class is a nested class that shadows another class at the top" do
+ expect_the_message_to_be('inner baz') do <<-MANIFEST
+ node default {
+ include foo::bar
+ }
+ class baz {
+ $var = "top baz"
+ }
+ class foo {
+ class baz {
+ $var = "inner baz"
+ }
+
+ class bar inherits baz {
+ notify { 'something': message => $var, }
+ }
+ }
+ MANIFEST
+ end
+ end
+
+ it "finds values in its inherited scope when the inherited class is qualified to a nested class and qualified to the top" do
+ expect_the_message_to_be('top baz') do <<-MANIFEST
+ node default {
+ include foo::bar
+ }
class baz {
- $var = "inner baz"
+ $var = "top baz"
+ }
+ class foo {
+ class baz {
+ $var = "inner baz"
+ }
+
+ class bar inherits ::baz {
+ notify { 'something': message => $var, }
+ }
}
+ MANIFEST
+ end
+ end
- class bar inherits ::baz {
+ it "finds values in its inherited scope when the inherited class is qualified" do
+ expect_the_message_to_be('foo_msg') do <<-MANIFEST
+ node default {
+ include bar
+ }
+ class foo {
+ class baz {
+ $var = "foo_msg"
+ }
+ }
+ class bar inherits foo::baz {
notify { 'something': message => $var, }
}
- }
- MANIFEST
+ MANIFEST
+ end
end
- end
- it "finds values in its inherited scope when the inherited class is qualified" do
- expect_the_message_to_be('foo_msg') do <<-MANIFEST
- node default {
- include bar
- }
- class foo {
- class baz {
+ it "prefers values in its inherited scope over those in the node (with intermediate inclusion)" do
+ expect_the_message_to_be('foo_msg') do <<-MANIFEST
+ node default {
+ $var = "node_msg"
+ include baz
+ }
+ class foo {
$var = "foo_msg"
}
- }
- class bar inherits foo::baz {
- notify { 'something': message => $var, }
- }
- MANIFEST
+ class bar inherits foo {
+ notify { 'something': message => $var, }
+ }
+ class baz {
+ include bar
+ }
+ MANIFEST
+ end
end
- end
- it "prefers values in its inherited scope over those in the node (with intermediate inclusion)" do
- expect_the_message_to_be('foo_msg') do <<-MANIFEST
- node default {
- $var = "node_msg"
- include baz
- }
- class foo {
- $var = "foo_msg"
- }
- class bar inherits foo {
- notify { 'something': message => $var, }
- }
- class baz {
- include bar
- }
- MANIFEST
+ it "prefers values in its inherited scope over those in the node (without intermediate inclusion)" do
+ expect_the_message_to_be('foo_msg') do <<-MANIFEST
+ node default {
+ $var = "node_msg"
+ include bar
+ }
+ class foo {
+ $var = "foo_msg"
+ }
+ class bar inherits foo {
+ notify { 'something': message => $var, }
+ }
+ MANIFEST
+ end
end
- end
- it "prefers values in its inherited scope over those in the node (without intermediate inclusion)" do
- expect_the_message_to_be('foo_msg') do <<-MANIFEST
- node default {
- $var = "node_msg"
- include bar
- }
- class foo {
- $var = "foo_msg"
- }
- class bar inherits foo {
- notify { 'something': message => $var, }
- }
- MANIFEST
+ it "prefers values in its inherited scope over those from where it is included" do
+ expect_the_message_to_be('foo_msg') do <<-MANIFEST
+ node default {
+ include baz
+ }
+ class foo {
+ $var = "foo_msg"
+ }
+ class bar inherits foo {
+ notify { 'something': message => $var, }
+ }
+ class baz {
+ $var = "baz_msg"
+ include bar
+ }
+ MANIFEST
+ end
end
- end
- it "prefers values in its inherited scope over those from where it is included" do
- expect_the_message_to_be('foo_msg') do <<-MANIFEST
- node default {
- include baz
- }
- class foo {
- $var = "foo_msg"
- }
- class bar inherits foo {
- notify { 'something': message => $var, }
- }
- class baz {
- $var = "baz_msg"
- include bar
- }
- MANIFEST
+ it "does not used variables from classes included in the inherited scope" do
+ expect_the_message_to_be('node_msg') do <<-MANIFEST
+ node default {
+ $var = "node_msg"
+ include bar
+ }
+ class quux {
+ $var = "quux_msg"
+ }
+ class foo inherits quux {
+ }
+ class baz {
+ include foo
+ }
+ class bar inherits baz {
+ notify { 'something': message => $var, }
+ }
+ MANIFEST
+ end
end
- end
- it "does not used variables from classes included in the inherited scope" do
- expect_the_message_to_be('node_msg') do <<-MANIFEST
- node default {
- $var = "node_msg"
- include bar
- }
- class quux {
- $var = "quux_msg"
- }
- class foo inherits quux {
- }
- class baz {
- include foo
- }
- class bar inherits baz {
- notify { 'something': message => $var, }
- }
- MANIFEST
+ it "does not use a variable from a scope lexically enclosing it" do
+ expect_the_message_to_be('node_msg') do <<-MANIFEST
+ node default {
+ $var = "node_msg"
+ include other::bar
+ }
+ class other {
+ $var = "other_msg"
+ class bar {
+ notify { 'something': message => $var, }
+ }
+ }
+ MANIFEST
+ end
end
- end
- it "does not use a variable from a scope lexically enclosing it" do
- expect_the_message_to_be('node_msg') do <<-MANIFEST
- node default {
- $var = "node_msg"
- include other::bar
- }
- class other {
- $var = "other_msg"
- class bar {
+ it "finds values in its node scope" do
+ expect_the_message_to_be('node_msg') do <<-MANIFEST
+ node default {
+ $var = "node_msg"
+ include baz
+ }
+ class foo {
+ }
+ class bar inherits foo {
notify { 'something': message => $var, }
}
- }
- MANIFEST
+ class baz {
+ include bar
+ }
+ MANIFEST
+ end
end
- end
- it "finds values in its node scope" do
- expect_the_message_to_be('node_msg') do <<-MANIFEST
- node default {
- $var = "node_msg"
- include baz
- }
- class foo {
- }
- class bar inherits foo {
- notify { 'something': message => $var, }
- }
- class baz {
- include bar
- }
- MANIFEST
+ it "finds values in its top scope" do
+ expect_the_message_to_be('top_msg') do <<-MANIFEST
+ $var = "top_msg"
+ node default {
+ include baz
+ }
+ class foo {
+ }
+ class bar inherits foo {
+ notify { 'something': message => $var, }
+ }
+ class baz {
+ include bar
+ }
+ MANIFEST
+ end
end
- end
- it "finds values in its top scope" do
- expect_the_message_to_be('top_msg') do <<-MANIFEST
- $var = "top_msg"
- node default {
- include baz
- }
- class foo {
- }
- class bar inherits foo {
- notify { 'something': message => $var, }
- }
- class baz {
- include bar
- }
- MANIFEST
+ it "prefers variables from the node over those in the top scope" do
+ expect_the_message_to_be('node_msg') do <<-MANIFEST
+ $var = "top_msg"
+ node default {
+ $var = "node_msg"
+ include foo
+ }
+ class foo {
+ notify { 'something': message => $var, }
+ }
+ MANIFEST
+ end
end
- end
- it "prefers variables from the node over those in the top scope" do
- expect_the_message_to_be('node_msg') do <<-MANIFEST
- $var = "top_msg"
- node default {
- $var = "node_msg"
- include foo
- }
- class foo {
- notify { 'something': message => $var, }
- }
- MANIFEST
+ it "finds top scope variables referenced inside a defined type" do
+ expect_the_message_to_be('top_msg') do <<-MANIFEST
+ $var = "top_msg"
+ node default {
+ foo { "testing": }
+ }
+ define foo() {
+ notify { 'something': message => $var, }
+ }
+ MANIFEST
+ end
end
- end
- it "finds top scope variables referenced inside a defined type" do
- expect_the_message_to_be('top_msg') do <<-MANIFEST
- $var = "top_msg"
- node default {
- foo { "testing": }
- }
- define foo() {
- notify { 'something': message => $var, }
- }
- MANIFEST
+ it "finds node scope variables referenced inside a defined type" do
+ expect_the_message_to_be('node_msg') do <<-MANIFEST
+ $var = "top_msg"
+ node default {
+ $var = "node_msg"
+ foo { "testing": }
+ }
+ define foo() {
+ notify { 'something': message => $var, }
+ }
+ MANIFEST
+ end
end
end
- it "finds node scope variables referenced inside a defined type" do
- expect_the_message_to_be('node_msg') do <<-MANIFEST
- $var = "top_msg"
- node default {
- $var = "node_msg"
- foo { "testing": }
- }
- define foo() {
- notify { 'something': message => $var, }
- }
- MANIFEST
+ describe "in situations that used to have dynamic lookup" do
+ it "ignores the dynamic value of the var" do
+ expect_the_message_to_be('node_msg') do <<-MANIFEST
+ node default {
+ $var = "node_msg"
+ include foo
+ }
+ class baz {
+ $var = "baz_msg"
+ include bar
+ }
+ class foo inherits baz {
+ }
+ class bar {
+ notify { 'something': message => $var, }
+ }
+ MANIFEST
+ end
end
- end
- end
- describe "in situations that used to have dynamic lookup" do
- it "ignores the dynamic value of the var" do
- expect_the_message_to_be('node_msg') do <<-MANIFEST
- node default {
- $var = "node_msg"
- include foo
- }
- class baz {
- $var = "baz_msg"
- include bar
- }
- class foo inherits baz {
- }
- class bar {
- notify { 'something': message => $var, }
- }
- MANIFEST
+ it "finds nil when the only set variable is in the dynamic scope" do
+ expect_the_message_to_be(nil) do <<-MANIFEST
+ node default {
+ include baz
+ }
+ class foo {
+ }
+ class bar inherits foo {
+ notify { 'something': message => $var, }
+ }
+ class baz {
+ $var = "baz_msg"
+ include bar
+ }
+ MANIFEST
+ end
end
- end
- it "finds nil when the only set variable is in the dynamic scope" do
- expect_the_message_to_be(nil) do <<-MANIFEST
- node default {
- include baz
- }
- class foo {
- }
- class bar inherits foo {
- notify { 'something': message => $var, }
- }
- class baz {
- $var = "baz_msg"
- include bar
- }
- MANIFEST
+ it "ignores the value in the dynamic scope for a defined type" do
+ expect_the_message_to_be('node_msg') do <<-MANIFEST
+ node default {
+ $var = "node_msg"
+ include foo
+ }
+ class foo {
+ $var = "foo_msg"
+ bar { "testing": }
+ }
+ define bar() {
+ notify { 'something': message => $var, }
+ }
+ MANIFEST
+ end
end
- end
- it "ignores the value in the dynamic scope for a defined type" do
- expect_the_message_to_be('node_msg') do <<-MANIFEST
- node default {
- $var = "node_msg"
- include foo
- }
- class foo {
- $var = "foo_msg"
- bar { "testing": }
- }
- define bar() {
- notify { 'something': message => $var, }
- }
- MANIFEST
+ it "when using a template ignores the dynamic value of the var when using scope.lookupvar" do
+ expect_the_message_to_be('node_msg') do <<-MANIFEST
+ node default {
+ $var = "node_msg"
+ include foo
+ }
+ class foo {
+ $var = "foo_msg"
+ include bar
+ }
+ class bar {
+ notify { 'something': message => inline_template("<%= scope.lookupvar('var') %>"), }
+ }
+ MANIFEST
+ end
end
end
- end
- describe "using plussignment to change in a new scope" do
- it "does not change a string in the parent scope" do
- expect_the_message_to_be('top_msg') do <<-MANIFEST
- $var = "top_msg"
- class override {
- $var += "override"
- include foo
- }
- class foo {
- notify { 'something': message => $var, }
- }
+ describe "using plussignment to change in a new scope" do
- include override
- MANIFEST
+ it "does not change an array in the parent scope" do
+ expect_the_message_to_be('top_msg') do <<-MANIFEST
+ $var = ["top_msg"]
+ class override {
+ $var += ["override"]
+ include foo
+ }
+ class foo {
+ notify { 'something': message => $var, }
+ }
+
+ include override
+ MANIFEST
+ end
end
- end
- it "does not change an array in the parent scope" do
- expect_the_message_to_be('top_msg') do <<-MANIFEST
- $var = ["top_msg"]
- class override {
- $var += ["override"]
- include foo
- }
- class foo {
- notify { 'something': message => $var, }
- }
+ it "concatenates two arrays" do
+ expect_the_message_to_be(['top_msg', 'override']) do <<-MANIFEST
+ $var = ["top_msg"]
+ class override {
+ $var += ["override"]
+ notify { 'something': message => $var, }
+ }
- include override
- MANIFEST
+ include override
+ MANIFEST
+ end
end
- end
- it "concatenates two arrays" do
- expect_the_message_to_be(['top_msg', 'override']) do <<-MANIFEST
- $var = ["top_msg"]
- class override {
- $var += ["override"]
- notify { 'something': message => $var, }
- }
+ it "leaves an array of arrays unflattened" do
+ expect_the_message_to_be([['top_msg'], ['override']]) do <<-MANIFEST
+ $var = [["top_msg"]]
+ class override {
+ $var += [["override"]]
+ notify { 'something': message => $var, }
+ }
- include override
- MANIFEST
+ include override
+ MANIFEST
+ end
end
- end
- it "leaves an array of arrays unflattened" do
- expect_the_message_to_be([['top_msg'], ['override']]) do <<-MANIFEST
- $var = [["top_msg"]]
- class override {
- $var += [["override"]]
- notify { 'something': message => $var, }
- }
+ it "does not change a hash in the parent scope" do
+ expect_the_message_to_be({"key"=>"top_msg"}) do <<-MANIFEST
+ $var = { "key" => "top_msg" }
+ class override {
+ $var += { "other" => "override" }
+ include foo
+ }
+ class foo {
+ notify { 'something': message => $var, }
+ }
- include override
- MANIFEST
+ include override
+ MANIFEST
+ end
end
- end
- it "does not change a hash in the parent scope" do
- expect_the_message_to_be({"key"=>"top_msg"}) do <<-MANIFEST
- $var = { "key" => "top_msg" }
- class override {
- $var += { "other" => "override" }
- include foo
- }
- class foo {
- notify { 'something': message => $var, }
- }
+ it "replaces a value of a key in the hash instead of merging the values" do
+ expect_the_message_to_be({"key"=>"override"}) do <<-MANIFEST
+ $var = { "key" => "top_msg" }
+ class override {
+ $var += { "key" => "override" }
+ notify { 'something': message => $var, }
+ }
- include override
- MANIFEST
+ include override
+ MANIFEST
+ end
end
end
- it "replaces a value of a key in the hash instead of merging the values" do
- expect_the_message_to_be({"key"=>"override"}) do <<-MANIFEST
- $var = { "key" => "top_msg" }
- class override {
- $var += { "key" => "override" }
+ describe "when using an enc" do
+ it "places enc parameters in top scope" do
+ enc_node = Puppet::Node.new("the node", { :parameters => { "var" => 'from_enc' } })
+
+ expect_the_message_to_be('from_enc', enc_node) do <<-MANIFEST
notify { 'something': message => $var, }
- }
+ MANIFEST
+ end
+ end
- include override
- MANIFEST
+ it "does not allow the enc to specify an existing top scope var" do
+ enc_node = Puppet::Node.new("the_node", { :parameters => { "var" => 'from_enc' } })
+
+ expect {
+ compile_to_catalog("$var = 'top scope'", enc_node)
+ }.to raise_error(
+ Puppet::Error,
+ /Cannot reassign variable var at line 1(\:6)? on node the_node/
+ )
end
- end
- end
- describe "when using an enc" do
- it "places enc parameters in top scope" do
- enc_node = Puppet::Node.new("the node", { :parameters => { "var" => 'from_enc' } })
+ it "evaluates enc classes in top scope when there is no node" do
+ enc_node = Puppet::Node.new("the node", { :classes => ['foo'], :parameters => { "var" => 'from_enc' } })
- expect_the_message_to_be('from_enc', enc_node) do <<-MANIFEST
- notify { 'something': message => $var, }
- MANIFEST
+ expect_the_message_to_be('from_enc', enc_node) do <<-MANIFEST
+ class foo {
+ notify { 'something': message => $var, }
+ }
+ MANIFEST
+ end
end
- end
- it "does not allow the enc to specify an existing top scope var" do
- enc_node = Puppet::Node.new("the_node", { :parameters => { "var" => 'from_enc' } })
+ it "evaluates enc classes in the node scope when there is a matching node" do
+ enc_node = Puppet::Node.new("the_node", { :classes => ['foo'] })
- expect {
- compile_to_catalog("$var = 'top scope'", enc_node)
- }.to raise_error(
- Puppet::Error,
- "Cannot reassign variable var at line 1 on node the_node"
- )
- end
+ expect_the_message_to_be('from matching node', enc_node) do <<-MANIFEST
+ node inherited {
+ $var = "from inherited"
+ }
- it "evaluates enc classes in top scope when there is no node" do
- enc_node = Puppet::Node.new("the node", { :classes => ['foo'], :parameters => { "var" => 'from_enc' } })
+ node the_node inherits inherited {
+ $var = "from matching node"
+ }
- expect_the_message_to_be('from_enc', enc_node) do <<-MANIFEST
- class foo {
- notify { 'something': message => $var, }
- }
- MANIFEST
+ class foo {
+ notify { 'something': message => $var, }
+ }
+ MANIFEST
+ end
end
end
+ end
- it "evaluates enc classes in the node scope when there is a matching node" do
- enc_node = Puppet::Node.new("the_node", { :classes => ['foo'] })
-
- expect_the_message_to_be('from matching node', enc_node) do <<-MANIFEST
- node inherited {
- $var = "from inherited"
- }
-
- node the_node inherits inherited {
- $var = "from matching node"
- }
+ describe 'using classic parser' do
+ before :each do
+ Puppet[:parser] = 'current'
+ end
+ it_behaves_like 'the scope' do
+ end
+ end
- class foo {
- notify { 'something': message => $var, }
- }
- MANIFEST
- end
+ describe 'using future parser' do
+ before :each do
+ Puppet[:parser] = 'future'
+ end
+ it_behaves_like 'the scope' do
end
end
+
end
diff --git a/spec/integration/provider/cron/crontab_spec.rb b/spec/integration/provider/cron/crontab_spec.rb
index 3c262c926..84ad4681c 100644
--- a/spec/integration/provider/cron/crontab_spec.rb
+++ b/spec/integration/provider/cron/crontab_spec.rb
@@ -1,196 +1,217 @@
#!/usr/bin/env ruby
require 'spec_helper'
require 'puppet/file_bucket/dipper'
describe Puppet::Type.type(:cron).provider(:crontab), '(integration)', :unless => Puppet.features.microsoft_windows? do
include PuppetSpec::Files
before :each do
Puppet::Type.type(:cron).stubs(:defaultprovider).returns described_class
Puppet::FileBucket::Dipper.any_instance.stubs(:backup) # Don't backup to filebucket
# 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 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)
end
after :each do
described_class.clear
end
let :crontab_user1 do
tmpfile('cron_integration_specs')
end
let :crontab_user2 do
tmpfile('cron_integration_specs')
end
def run_in_catalog(*resources)
catalog = Puppet::Resource::Catalog.new
catalog.host_config = false
resources.each do |resource|
resource.expects(:err).never
catalog.add_resource(resource)
end
+
+ # the resources are not properly contained and generated resources
+ # will end up with dangling edges without this stubbing:
+ catalog.stubs(:container_of).returns resources[0]
catalog.apply
end
def expect_output(fixture_name)
File.read(crontab_user1).should == File.read(my_fixture(fixture_name))
end
describe "when managing a cron entry" do
+
+ it "should be able to purge unmanaged entries" do
+ resource = Puppet::Type.type(:cron).new(
+ :name => 'only managed entry',
+ :ensure => :present,
+ :command => '/bin/true',
+ :target => crontab_user1,
+ :user => crontab_user1
+ )
+ resources = Puppet::Type.type(:resources).new(
+ :name => 'cron',
+ :purge => 'true'
+ )
+ run_in_catalog(resource, resources)
+ expect_output('purged')
+ end
+
describe "with ensure absent" do
it "should do nothing if entry already absent" do
resource = Puppet::Type.type(:cron).new(
:name => 'no_such_entry',
:ensure => :absent,
:target => crontab_user1,
:user => crontab_user1
)
run_in_catalog(resource)
expect_output('crontab_user1')
end
it "should remove the resource from crontab if present" do
resource = Puppet::Type.type(:cron).new(
:name => 'My daily failure',
:ensure => :absent,
:target => crontab_user1,
:user => crontab_user1
)
run_in_catalog(resource)
expect_output('remove_named_resource')
end
it "should remove a matching cronentry if present" do
resource = Puppet::Type.type(:cron).new(
:name => 'no_such_named_resource_in_crontab',
:ensure => :absent,
:minute => [ '17-19', '22' ],
:hour => [ '0-23/2' ],
:weekday => 'Tue',
:command => '/bin/unnamed_regular_command',
:target => crontab_user1,
:user => crontab_user1
)
run_in_catalog(resource)
expect_output('remove_unnamed_resource')
end
end
describe "with ensure present" do
it "should do nothing if entry already present" do
resource = Puppet::Type.type(:cron).new(
:name => 'My daily failure',
:special => 'daily',
:command => '/bin/false',
:target => crontab_user1,
:user => crontab_user1
)
run_in_catalog(resource)
expect_output('crontab_user1')
end
it "should do nothing if a matching entry already present" do
resource = Puppet::Type.type(:cron).new(
:name => 'no_such_named_resource_in_crontab',
:ensure => :present,
:minute => [ '17-19', '22' ],
:hour => [ '0-23/2' ],
:command => '/bin/unnamed_regular_command',
:target => crontab_user1,
:user => crontab_user1
)
run_in_catalog(resource)
expect_output('crontab_user1')
end
it "should add a new normal entry if currently absent" do
resource = Puppet::Type.type(:cron).new(
:name => 'new entry',
:ensure => :present,
:minute => '12',
:weekday => 'Tue',
:command => '/bin/new',
:environment => [
'MAILTO=""',
'SHELL=/bin/bash'
],
:target => crontab_user1,
:user => crontab_user1
)
run_in_catalog(resource)
expect_output('create_normal_entry')
end
it "should add a new special entry if currently absent" do
resource = Puppet::Type.type(:cron).new(
:name => 'new special entry',
:ensure => :present,
:special => 'reboot',
:command => 'echo "Booted" 1>&2',
:environment => 'MAILTO=bob@company.com',
:target => crontab_user1,
:user => crontab_user1
)
run_in_catalog(resource)
expect_output('create_special_entry')
end
it "should change existing entry if out of sync" do
resource = Puppet::Type.type(:cron).new(
:name => 'Monthly job',
:ensure => :present,
:special => 'monthly',
# :minute => ['22'],
:command => '/usr/bin/monthly',
:environment => [],
:target => crontab_user1,
:user => crontab_user1
)
run_in_catalog(resource)
expect_output('modify_entry')
end
it "should change a special schedule to numeric if requested" do
resource = Puppet::Type.type(:cron).new(
:name => 'My daily failure',
:special => 'absent',
:command => '/bin/false',
:target => crontab_user1,
:user => crontab_user1
)
run_in_catalog(resource)
expect_output('unspecialized')
end
it "should not try to move an entry from one file to another" do
# force the parsedfile provider to also parse user1's crontab
random_resource = Puppet::Type.type(:cron).new(
:name => 'foo',
:ensure => :absent,
:target => crontab_user1,
:user => crontab_user1
)
resource = Puppet::Type.type(:cron).new(
:name => 'My daily failure',
:special => 'daily',
:command => "/bin/false",
:target => crontab_user2,
:user => crontab_user2
)
run_in_catalog(resource)
File.read(crontab_user1).should == File.read(my_fixture('moved_cronjob_input1'))
File.read(crontab_user2).should == File.read(my_fixture('moved_cronjob_input2'))
end
end
end
end
diff --git a/spec/integration/provider/mount_spec.rb b/spec/integration/provider/mount_spec.rb
index 310b58d1f..8e8581153 100755
--- a/spec/integration/provider/mount_spec.rb
+++ b/spec/integration/provider/mount_spec.rb
@@ -1,156 +1,169 @@
require 'spec_helper'
require 'puppet/file_bucket/dipper'
describe "mount provider (integration)", :unless => Puppet.features.microsoft_windows? do
include PuppetSpec::Files
family = Facter.value(:osfamily)
def create_fake_fstab(initially_contains_entry)
File.open(@fake_fstab, 'w') do |f|
if initially_contains_entry
f.puts("/dev/disk1s1\t/Volumes/foo_disk\tmsdos\tlocal\t0\t0")
end
end
end
before :each do
@fake_fstab = tmpfile('fstab')
@current_options = "local"
@current_device = "/dev/disk1s1"
Puppet::Type.type(:mount).defaultprovider.stubs(:default_target).returns(@fake_fstab)
Facter.stubs(:value).with(:kernel).returns('Darwin')
Facter.stubs(:value).with(:operatingsystem).returns('Darwin')
Facter.stubs(:value).with(:osfamily).returns('Darwin')
Puppet::Util::ExecutionStub.set do |command, options|
case command[0]
when %r{/s?bin/mount}
if command.length == 1
if @mounted
"#{@current_device} on /Volumes/foo_disk (msdos, #{@current_options})\n"
else
''
end
else
command.length.should == 4
command[1].should == '-o'
+
+ # update is a special option, used on bsd's
+ # strip it out and track as a local bool here
+ update = false
+ tmp_options = command[2].split(",")
+
+ if tmp_options.include?("update")
+ update = true
+ tmp_options.delete("update")
+ end
+ @current_options = tmp_options.join(",")
+
+ if !update
+ @mounted.should == false # verify that we don't try to call "mount" redundantly
+ end
command[3].should == '/Volumes/foo_disk'
- @mounted.should == false # verify that we don't try to call "mount" redundantly
- @current_options = command[2]
@current_device = check_fstab(true)
@mounted = true
''
end
when %r{/s?bin/umount}
command.length.should == 2
command[1].should == '/Volumes/foo_disk'
@mounted.should == true # "umount" doesn't work when device not mounted (see #6632)
@mounted = false
''
else
fail "Unexpected command #{command.inspect} executed"
end
end
end
after :each do
Puppet::Type::Mount::ProviderParsed.clear # Work around bug #6628
end
def check_fstab(expected_to_be_present)
# Verify that the fake fstab has the expected data in it
fstab_contents = File.read(@fake_fstab).split("\n").reject { |x| x =~ /^#|^$/ }
if expected_to_be_present
fstab_contents.length().should == 1
device, rest_of_line = fstab_contents[0].split(/\t/,2)
rest_of_line.should == "/Volumes/foo_disk\tmsdos\t#{@desired_options}\t0\t0"
device
else
fstab_contents.length().should == 0
nil
end
end
def run_in_catalog(settings)
resource = Puppet::Type.type(:mount).new(settings.merge(:name => "/Volumes/foo_disk",
:device => "/dev/disk1s1", :fstype => "msdos"))
Puppet::FileBucket::Dipper.any_instance.stubs(:backup) # Don't backup to the filebucket
resource.expects(:err).never
catalog = Puppet::Resource::Catalog.new
catalog.host_config = false # Stop Puppet from doing a bunch of magic
catalog.add_resource resource
catalog.apply
end
[false, true].each do |initial_state|
describe "When initially #{initial_state ? 'mounted' : 'unmounted'}" do
before :each do
@mounted = initial_state
end
[false, true].each do |initial_fstab_entry|
describe "When there is #{initial_fstab_entry ? 'an' : 'no'} initial fstab entry" do
before :each do
create_fake_fstab(initial_fstab_entry)
end
[:defined, :present, :mounted, :unmounted, :absent].each do |ensure_setting|
expected_final_state = case ensure_setting
when :mounted
true
when :unmounted, :absent
false
when :defined, :present
initial_state
else
fail "Unknown ensure_setting #{ensure_setting}"
end
expected_fstab_data = (ensure_setting != :absent)
describe "When setting ensure => #{ensure_setting}" do
["local", "journaled"].each do |options_setting|
describe "When setting options => #{options_setting}" do
it "should leave the system in the #{expected_final_state ? 'mounted' : 'unmounted'} state, #{expected_fstab_data ? 'with' : 'without'} data in /etc/fstab" do
pending("Solaris: The mock :operatingsystem value does not get changed in lib/puppet/provider/mount/parsed.rb", :if => family == "Solaris")
@desired_options = options_setting
run_in_catalog(:ensure=>ensure_setting, :options => options_setting)
@mounted.should == expected_final_state
if expected_fstab_data
check_fstab(expected_fstab_data).should == "/dev/disk1s1"
else
check_fstab(expected_fstab_data).should == nil
end
if @mounted
if ![:defined, :present].include?(ensure_setting)
@current_options.should == @desired_options
elsif initial_fstab_entry
@current_options.should == @desired_options
else
@current_options.should == 'local' #Workaround for #6645
end
end
end
end
end
end
end
end
end
end
end
describe "When the wrong device is mounted" do
it "should remount the correct device" do
pending "Due to bug 6309"
@mounted = true
@current_device = "/dev/disk2s2"
create_fake_fstab(true)
@desired_options = "local"
run_in_catalog(:ensure=>:mounted, :options=>'local')
@current_device.should=="/dev/disk1s1"
@mounted.should==true
@current_options.should=='local'
check_fstab(true).should == "/dev/disk1s1"
end
end
end
diff --git a/spec/integration/resource/catalog_spec.rb b/spec/integration/resource/catalog_spec.rb
index 1469cc063..583792103 100755
--- a/spec/integration/resource/catalog_spec.rb
+++ b/spec/integration/resource/catalog_spec.rb
@@ -1,54 +1,54 @@
#! /usr/bin/env ruby
require 'spec_helper'
describe Puppet::Resource::Catalog do
it "should support pson" do
Puppet::Resource::Catalog.supported_formats.should be_include(:pson)
end
describe "when using the indirector" do
before do
# This is so the tests work w/out networking.
Facter.stubs(:to_hash).returns({"hostname" => "foo.domain.com"})
Facter.stubs(:value).returns("eh")
end
it "should be able to delegate to the :yaml terminus" do
Puppet::Resource::Catalog.indirection.stubs(:terminus_class).returns :yaml
# Load now, before we stub the exists? method.
terminus = Puppet::Resource::Catalog.indirection.terminus(:yaml)
terminus.expects(:path).with("me").returns "/my/yaml/file"
- Puppet::FileSystem::File.expects(:exist?).with("/my/yaml/file").returns false
+ Puppet::FileSystem.expects(:exist?).with("/my/yaml/file").returns false
Puppet::Resource::Catalog.indirection.find("me").should be_nil
end
it "should be able to delegate to the :compiler terminus" do
Puppet::Resource::Catalog.indirection.stubs(:terminus_class).returns :compiler
# Load now, before we stub the exists? method.
compiler = Puppet::Resource::Catalog.indirection.terminus(:compiler)
node = mock 'node'
node.stub_everything
Puppet::Node.indirection.expects(:find).returns(node)
compiler.expects(:compile).with(node).returns nil
Puppet::Resource::Catalog.indirection.find("me").should be_nil
end
it "should pass provided node information directly to the terminus" do
terminus = mock 'terminus'
Puppet::Resource::Catalog.indirection.stubs(:terminus).returns terminus
node = mock 'node'
terminus.stubs(:validate)
terminus.expects(:find).with { |request| request.options[:use_node] == node }
Puppet::Resource::Catalog.indirection.find("me", :use_node => node)
end
end
end
diff --git a/spec/integration/resource/type_collection_spec.rb b/spec/integration/resource/type_collection_spec.rb
index db2612ed0..6349460be 100755
--- a/spec/integration/resource/type_collection_spec.rb
+++ b/spec/integration/resource/type_collection_spec.rb
@@ -1,95 +1,94 @@
#! /usr/bin/env ruby
require 'spec_helper'
require 'puppet_spec/files'
require 'puppet/resource/type_collection'
describe Puppet::Resource::TypeCollection do
describe "when autoloading from modules" do
include PuppetSpec::Files
before do
@dir = tmpfile("autoload_testing")
- Puppet[:modulepath] = @dir
-
FileUtils.mkdir_p @dir
- @code = Puppet::Resource::TypeCollection.new("env")
- Puppet::Node::Environment.new("env").stubs(:known_resource_types).returns @code
+
+ environment = Puppet::Node::Environment.create(:env, [@dir], '')
+ @code = environment.known_resource_types
end
# Setup a module.
def mk_module(name, files = {})
mdir = File.join(@dir, name)
mandir = File.join(mdir, "manifests")
FileUtils.mkdir_p mandir
defs = files.delete(:define)
Dir.chdir(mandir) do
files.each do |file, classes|
File.open("#{file}.pp", "w") do |f|
classes.each { |klass|
if defs
f.puts "define #{klass} {}"
else
f.puts "class #{klass} {}"
end
}
end
end
end
end
it "should return nil when a class can't be found or loaded" do
@code.find_hostclass('', 'nosuchclass').should be_nil
end
it "should load the module's init file first" do
name = "simple"
mk_module(name, :init => [name])
@code.find_hostclass("", name).name.should == name
end
it "should load the module's init file even when searching from a different namespace" do
name = "simple"
mk_module(name, :init => [name])
@code.find_hostclass("other::ns", name).name.should == name
end
it "should be able to load definitions from the module base file" do
name = "simpdef"
mk_module(name, :define => true, :init => [name])
@code.find_definition("", name).name.should == name
end
it "should be able to load qualified classes from the module base file" do
modname = "both"
name = "sub"
mk_module(modname, :init => %w{both both::sub})
@code.find_hostclass("both", name).name.should == "both::sub"
end
it "should be able load classes from a separate file" do
modname = "separate"
name = "sub"
mk_module(modname, :init => %w{separate}, :sub => %w{separate::sub})
@code.find_hostclass("separate", name).name.should == "separate::sub"
end
it "should not fail when loading from a separate file if there is no module file" do
modname = "alone"
name = "sub"
mk_module(modname, :sub => %w{alone::sub})
lambda { @code.find_hostclass("alone", name) }.should_not raise_error
end
it "should be able to load definitions from their own file" do
name = "mymod"
mk_module(name, :define => true, :mydefine => ["mymod::mydefine"])
@code.find_definition("", "mymod::mydefine").name.should == "mymod::mydefine"
end
end
end
diff --git a/spec/integration/ssl/autosign_spec.rb b/spec/integration/ssl/autosign_spec.rb
index 003796ef0..2812d1fcc 100644
--- a/spec/integration/ssl/autosign_spec.rb
+++ b/spec/integration/ssl/autosign_spec.rb
@@ -1,130 +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
+ Puppet::FileSystem.touch(Puppet.settings[:csr_attributes])
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_revocation_list_spec.rb b/spec/integration/ssl/certificate_revocation_list_spec.rb
index 530f03ed9..06a69a741 100755
--- a/spec/integration/ssl/certificate_revocation_list_spec.rb
+++ b/spec/integration/ssl/certificate_revocation_list_spec.rb
@@ -1,37 +1,37 @@
#! /usr/bin/env ruby
require 'spec_helper'
require 'puppet/ssl/certificate_revocation_list'
describe Puppet::SSL::CertificateRevocationList do
include PuppetSpec::Files
before do
# Get a safe temporary file
dir = tmpdir("ca_integration_testing")
Puppet.settings[:confdir] = dir
Puppet.settings[:vardir] = dir
Puppet.settings[:group] = Process.gid
Puppet::SSL::Host.ca_location = :local
end
after {
Puppet::SSL::Host.ca_location = :none
Puppet.settings.clear
# This is necessary so the terminus instances don't lie around.
Puppet::SSL::Host.indirection.termini.clear
}
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 Puppet::FileSystem::File.exist?(Puppet[:hostcrl])
+ raise "CRL not created" unless Puppet::FileSystem.exist?(Puppet[:hostcrl])
crl = Puppet::SSL::CertificateRevocationList.new("crl_int_testing")
crl.read(Puppet[:hostcrl])
end
end
diff --git a/spec/integration/ssl/host_spec.rb b/spec/integration/ssl/host_spec.rb
index fb6b4e1e0..fbb108db7 100755
--- a/spec/integration/ssl/host_spec.rb
+++ b/spec/integration/ssl/host_spec.rb
@@ -1,84 +1,84 @@
#! /usr/bin/env ruby
require 'spec_helper'
require 'puppet/ssl/host'
describe Puppet::SSL::Host do
include PuppetSpec::Files
before do
# Get a safe temporary file
dir = tmpdir("host_integration_testing")
Puppet.settings[:confdir] = dir
Puppet.settings[:vardir] = dir
Puppet.settings[:group] = Process.gid
Puppet::SSL::Host.ca_location = :local
@host = Puppet::SSL::Host.new("luke.madstop.com")
@ca = Puppet::SSL::CertificateAuthority.new
end
after {
Puppet::SSL::Host.ca_location = :none
Puppet.settings.clear
}
it "should be considered a CA host if its name is equal to 'ca'" do
Puppet::SSL::Host.new(Puppet::SSL::CA_NAME).should be_ca
end
describe "when managing its key" do
it "should be able to generate and save a key" do
@host.generate_key
end
it "should save the key such that the Indirector can find it" do
@host.generate_key
Puppet::SSL::Key.indirection.find(@host.name).content.to_s.should == @host.key.to_s
end
it "should save the private key into the :privatekeydir" do
@host.generate_key
File.read(File.join(Puppet.settings[:privatekeydir], "luke.madstop.com.pem")).should == @host.key.to_s
end
end
describe "when managing its certificate request" do
it "should be able to generate and save a certificate request" do
@host.generate_certificate_request
end
it "should save the certificate request such that the Indirector can find it" do
@host.generate_certificate_request
Puppet::SSL::CertificateRequest.indirection.find(@host.name).content.to_s.should == @host.certificate_request.to_s
end
it "should save the private certificate request into the :privatekeydir" do
@host.generate_certificate_request
File.read(File.join(Puppet.settings[:requestdir], "luke.madstop.com.pem")).should == @host.certificate_request.to_s
end
end
describe "when the CA host" do
it "should never store its key in the :privatekeydir" do
Puppet.settings.use(:main, :ssl, :ca)
@ca = Puppet::SSL::Host.new(Puppet::SSL::Host.ca_name)
@ca.generate_key
- Puppet::FileSystem::File.exist?(File.join(Puppet[:privatekeydir], "ca.pem")).should be_false
+ Puppet::FileSystem.exist?(File.join(Puppet[:privatekeydir], "ca.pem")).should be_false
end
end
it "should pass the verification of its own SSL store", :unless => Puppet.features.microsoft_windows? do
@host.generate
@ca = Puppet::SSL::CertificateAuthority.new
@ca.sign(@host.name)
@host.ssl_store.verify(@host.certificate.content).should be_true
end
end
diff --git a/spec/integration/transaction_spec.rb b/spec/integration/transaction_spec.rb
index 851954dbe..fc1fff228 100755
--- a/spec/integration/transaction_spec.rb
+++ b/spec/integration/transaction_spec.rb
@@ -1,346 +1,346 @@
#! /usr/bin/env ruby
require 'spec_helper'
require 'puppet/transaction'
describe Puppet::Transaction do
include PuppetSpec::Files
before do
Puppet::Util::Storage.stubs(:store)
end
def mk_catalog(*resources)
catalog = Puppet::Resource::Catalog.new(Puppet::Node.new("mynode"))
resources.each { |res| catalog.add_resource res }
catalog
end
def usr_bin_touch(path)
Puppet.features.microsoft_windows? ? "#{ENV['windir']}/system32/cmd.exe /c \"type NUL >> \"#{path}\"\"" : "/usr/bin/touch #{path}"
end
def touch(path)
Puppet.features.microsoft_windows? ? "cmd.exe /c \"type NUL >> \"#{path}\"\"" : "touch #{path}"
end
it "should not apply generated resources if the parent resource fails" do
catalog = Puppet::Resource::Catalog.new
resource = Puppet::Type.type(:file).new :path => make_absolute("/foo/bar"), :backup => false
catalog.add_resource resource
child_resource = Puppet::Type.type(:file).new :path => make_absolute("/foo/bar/baz"), :backup => false
resource.expects(:eval_generate).returns([child_resource])
transaction = Puppet::Transaction.new(catalog, nil, Puppet::Graph::RandomPrioritizer.new)
resource.expects(:retrieve).raises "this is a failure"
resource.stubs(:err)
child_resource.expects(:retrieve).never
transaction.evaluate
end
it "should not apply virtual resources" do
catalog = Puppet::Resource::Catalog.new
resource = Puppet::Type.type(:file).new :path => make_absolute("/foo/bar"), :backup => false
resource.virtual = true
catalog.add_resource resource
transaction = Puppet::Transaction.new(catalog, nil, Puppet::Graph::RandomPrioritizer.new)
resource.expects(:evaluate).never
transaction.evaluate
end
it "should apply exported resources" do
catalog = Puppet::Resource::Catalog.new
path = tmpfile("exported_files")
resource = Puppet::Type.type(:file).new :path => path, :backup => false, :ensure => :file
resource.exported = true
catalog.add_resource resource
catalog.apply
- Puppet::FileSystem::File.exist?(path).should be_true
+ Puppet::FileSystem.exist?(path).should be_true
end
it "should not apply virtual exported resources" do
catalog = Puppet::Resource::Catalog.new
resource = Puppet::Type.type(:file).new :path => make_absolute("/foo/bar"), :backup => false
resource.exported = true
resource.virtual = true
catalog.add_resource resource
transaction = Puppet::Transaction.new(catalog, nil, Puppet::Graph::RandomPrioritizer.new)
resource.expects(:evaluate).never
transaction.evaluate
end
it "should not apply device resources on normal host" do
catalog = Puppet::Resource::Catalog.new
resource = Puppet::Type.type(:interface).new :name => "FastEthernet 0/1"
catalog.add_resource resource
transaction = Puppet::Transaction.new(catalog, nil, Puppet::Graph::RandomPrioritizer.new)
transaction.for_network_device = false
transaction.expects(:apply).never.with(resource, nil)
transaction.evaluate
transaction.resource_status(resource).should be_skipped
end
it "should not apply host resources on device" do
catalog = Puppet::Resource::Catalog.new
resource = Puppet::Type.type(:file).new :path => make_absolute("/foo/bar"), :backup => false
catalog.add_resource resource
transaction = Puppet::Transaction.new(catalog, nil, Puppet::Graph::RandomPrioritizer.new)
transaction.for_network_device = true
transaction.expects(:apply).never.with(resource, nil)
transaction.evaluate
transaction.resource_status(resource).should be_skipped
end
it "should apply device resources on device" do
catalog = Puppet::Resource::Catalog.new
resource = Puppet::Type.type(:interface).new :name => "FastEthernet 0/1"
catalog.add_resource resource
transaction = Puppet::Transaction.new(catalog, nil, Puppet::Graph::RandomPrioritizer.new)
transaction.for_network_device = true
transaction.expects(:apply).with(resource, nil)
transaction.evaluate
transaction.resource_status(resource).should_not be_skipped
end
it "should apply resources appliable on host and device on a device" do
catalog = Puppet::Resource::Catalog.new
resource = Puppet::Type.type(:schedule).new :name => "test"
catalog.add_resource resource
transaction = Puppet::Transaction.new(catalog, nil, Puppet::Graph::RandomPrioritizer.new)
transaction.for_network_device = true
transaction.expects(:apply).with(resource, nil)
transaction.evaluate
transaction.resource_status(resource).should_not be_skipped
end
# Verify that one component requiring another causes the contained
# resources in the requiring component to get refreshed.
it "should propagate events from a contained resource through its container to its dependent container's contained resources" do
transaction = nil
file = Puppet::Type.type(:file).new :path => tmpfile("event_propagation"), :ensure => :present
execfile = File.join(tmpdir("exec_event"), "exectestingness2")
exec = Puppet::Type.type(:exec).new :command => touch(execfile), :path => ENV['PATH']
catalog = mk_catalog(file)
fcomp = Puppet::Type.type(:component).new(:name => "Foo[file]")
catalog.add_resource fcomp
catalog.add_edge(fcomp, file)
ecomp = Puppet::Type.type(:component).new(:name => "Foo[exec]")
catalog.add_resource ecomp
catalog.add_resource exec
catalog.add_edge(ecomp, exec)
ecomp[:subscribe] = Puppet::Resource.new(:foo, "file")
exec[:refreshonly] = true
exec.expects(:refresh)
catalog.apply
end
# Make sure that multiple subscriptions get triggered.
it "should propagate events to all dependent resources" do
path = tmpfile("path")
file1 = tmpfile("file1")
file2 = tmpfile("file2")
file = Puppet::Type.type(:file).new(
:path => path,
:ensure => "file"
)
exec1 = Puppet::Type.type(:exec).new(
:path => ENV["PATH"],
:command => touch(file1),
:refreshonly => true,
:subscribe => Puppet::Resource.new(:file, path)
)
exec2 = Puppet::Type.type(:exec).new(
:path => ENV["PATH"],
:command => touch(file2),
:refreshonly => true,
:subscribe => Puppet::Resource.new(:file, path)
)
catalog = mk_catalog(file, exec1, exec2)
catalog.apply
- Puppet::FileSystem::File.exist?(file1).should be_true
- Puppet::FileSystem::File.exist?(file2).should be_true
+ Puppet::FileSystem.exist?(file1).should be_true
+ Puppet::FileSystem.exist?(file2).should be_true
end
it "should not let one failed refresh result in other refreshes failing" do
path = tmpfile("path")
newfile = tmpfile("file")
file = Puppet::Type.type(:file).new(
:path => path,
:ensure => "file"
)
exec1 = Puppet::Type.type(:exec).new(
:path => ENV["PATH"],
:command => touch(File.expand_path("/this/cannot/possibly/exist")),
:logoutput => true,
:refreshonly => true,
:subscribe => file,
:title => "one"
)
exec2 = Puppet::Type.type(:exec).new(
:path => ENV["PATH"],
:command => touch(newfile),
:logoutput => true,
:refreshonly => true,
:subscribe => [file, exec1],
:title => "two"
)
exec1.stubs(:err)
catalog = mk_catalog(file, exec1, exec2)
catalog.apply
- Puppet::FileSystem::File.exist?(newfile).should be_true
+ Puppet::FileSystem.exist?(newfile).should be_true
end
it "should still trigger skipped resources" do
catalog = mk_catalog
catalog.add_resource(*Puppet::Type.type(:schedule).mkdefaultschedules)
Puppet[:ignoreschedules] = false
file = Puppet::Type.type(:file).new(
:name => tmpfile("file"),
:ensure => "file",
:backup => false
)
fname = tmpfile("exec")
exec = Puppet::Type.type(:exec).new(
:name => touch(fname),
:path => Puppet.features.microsoft_windows? ? "#{ENV['windir']}/system32" : "/usr/bin:/bin",
:schedule => "monthly",
:subscribe => Puppet::Resource.new("file", file.name)
)
catalog.add_resource(file, exec)
# Run it once
catalog.apply
- Puppet::FileSystem::File.exist?(fname).should be_true
+ Puppet::FileSystem.exist?(fname).should be_true
# Now remove it, so it can get created again
- Puppet::FileSystem::File.unlink(fname)
+ Puppet::FileSystem.unlink(fname)
file[:content] = "some content"
catalog.apply
- Puppet::FileSystem::File.exist?(fname).should be_true
+ Puppet::FileSystem.exist?(fname).should be_true
# Now remove it, so it can get created again
- Puppet::FileSystem::File.unlink(fname)
+ Puppet::FileSystem.unlink(fname)
# And tag our exec
exec.tag("testrun")
# And our file, so it runs
file.tag("norun")
Puppet[:tags] = "norun"
file[:content] = "totally different content"
catalog.apply
- Puppet::FileSystem::File.exist?(fname).should be_true
+ Puppet::FileSystem.exist?(fname).should be_true
end
it "should not attempt to evaluate resources with failed dependencies" do
exec = Puppet::Type.type(:exec).new(
:command => "#{File.expand_path('/bin/mkdir')} /this/path/cannot/possibly/exist",
:title => "mkdir"
)
file1 = Puppet::Type.type(:file).new(
:title => "file1",
:path => tmpfile("file1"),
:require => exec,
:ensure => :file
)
file2 = Puppet::Type.type(:file).new(
:title => "file2",
:path => tmpfile("file2"),
:require => file1,
:ensure => :file
)
catalog = mk_catalog(exec, file1, file2)
catalog.apply
- Puppet::FileSystem::File.exist?(file1[:path]).should be_false
- Puppet::FileSystem::File.exist?(file2[:path]).should be_false
+ Puppet::FileSystem.exist?(file1[:path]).should be_false
+ Puppet::FileSystem.exist?(file2[:path]).should be_false
end
it "should not trigger subscribing resources on failure" do
file1 = tmpfile("file1")
file2 = tmpfile("file2")
create_file1 = Puppet::Type.type(:exec).new(
:command => usr_bin_touch(file1)
)
exec = Puppet::Type.type(:exec).new(
:command => "#{File.expand_path('/bin/mkdir')} /this/path/cannot/possibly/exist",
:title => "mkdir",
:notify => create_file1
)
create_file2 = Puppet::Type.type(:exec).new(
:command => usr_bin_touch(file2),
:subscribe => exec
)
catalog = mk_catalog(exec, create_file1, create_file2)
catalog.apply
- Puppet::FileSystem::File.exist?(file1).should be_false
- Puppet::FileSystem::File.exist?(file2).should be_false
+ Puppet::FileSystem.exist?(file1).should be_false
+ Puppet::FileSystem.exist?(file2).should be_false
end
# #801 -- resources only checked in noop should be rescheduled immediately.
it "should immediately reschedule noop resources" do
Puppet::Type.type(:schedule).mkdefaultschedules
resource = Puppet::Type.type(:notify).new(:name => "mymessage", :noop => true)
catalog = Puppet::Resource::Catalog.new
catalog.add_resource resource
trans = catalog.apply
trans.resource_harness.should be_scheduled(resource)
end
end
diff --git a/spec/integration/type/exec_spec.rb b/spec/integration/type/exec_spec.rb
index 2b044473f..1e39bdb9f 100755
--- a/spec/integration/type/exec_spec.rb
+++ b/spec/integration/type/exec_spec.rb
@@ -1,77 +1,77 @@
#! /usr/bin/env ruby
require 'spec_helper'
require 'puppet_spec/files'
describe Puppet::Type.type(:exec) do
include PuppetSpec::Files
let(:catalog) { Puppet::Resource::Catalog.new }
let(:path) { tmpfile('exec_provider') }
let(:command) { "ruby -e 'File.open(\"#{path}\", \"w\") { |f| f.print \"foo\" }'" }
before :each do
catalog.host_config = false
end
it "should execute the command" do
exec = described_class.new :command => command, :path => ENV['PATH']
catalog.add_resource exec
catalog.apply
File.read(path).should == 'foo'
end
it "should not execute the command if onlyif returns non-zero" do
exec = described_class.new(
:command => command,
:onlyif => "ruby -e 'exit 44'",
:path => ENV['PATH']
)
catalog.add_resource exec
catalog.apply
- Puppet::FileSystem::File.exist?(path).should be_false
+ Puppet::FileSystem.exist?(path).should be_false
end
it "should execute the command if onlyif returns zero" do
exec = described_class.new(
:command => command,
:onlyif => "ruby -e 'exit 0'",
:path => ENV['PATH']
)
catalog.add_resource exec
catalog.apply
File.read(path).should == 'foo'
end
it "should execute the command if unless returns non-zero" do
exec = described_class.new(
:command => command,
:unless => "ruby -e 'exit 45'",
:path => ENV['PATH']
)
catalog.add_resource exec
catalog.apply
File.read(path).should == 'foo'
end
it "should not execute the command if unless returns zero" do
exec = described_class.new(
:command => command,
:unless => "ruby -e 'exit 0'",
:path => ENV['PATH']
)
catalog.add_resource exec
catalog.apply
- Puppet::FileSystem::File.exist?(path).should be_false
+ Puppet::FileSystem.exist?(path).should be_false
end
end
diff --git a/spec/integration/type/file_spec.rb b/spec/integration/type/file_spec.rb
index a71d03dc4..f864fe06a 100755
--- a/spec/integration/type/file_spec.rb
+++ b/spec/integration/type/file_spec.rb
@@ -1,1302 +1,1375 @@
#! /usr/bin/env ruby
require 'spec_helper'
require 'puppet_spec/files'
if Puppet.features.microsoft_windows?
require 'puppet/util/windows'
class WindowsSecurity
extend Puppet::Util::Windows::Security
end
end
describe Puppet::Type.type(:file) do
include PuppetSpec::Files
let(:catalog) { Puppet::Resource::Catalog.new }
let(:path) 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, '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)
- Puppet::FileSystem::File.new(file).lstat.mode
+ Puppet::FileSystem.lstat(file).mode
end
def get_owner(file)
- Puppet::FileSystem::File.new(file).lstat.uid
+ Puppet::FileSystem.lstat(file).uid
end
def get_group(file)
- Puppet::FileSystem::File.new(file).lstat.gid
+ Puppet::FileSystem.lstat(file).gid
end
else
class SecurityHelper
extend Puppet::Util::Windows::Security
end
def set_mode(mode, file)
SecurityHelper.set_mode(mode, file)
end
def get_mode(file)
SecurityHelper.get_mode(file)
end
def get_owner(file)
SecurityHelper.get_owner(file)
end
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
# stub this to not try to create state.yaml
Puppet::Util::Storage.stubs(:store)
end
it "should not attempt to manage files that do not exist if no means of creating the file is specified" do
source = tmpfile('source')
catalog.add_resource described_class.new :path => source, :mode => 0755
status = catalog.apply.report.resource_statuses["File[#{source}]"]
status.should_not be_failed
status.should_not be_changed
- Puppet::FileSystem::File.exist?(source).should be_false
+ Puppet::FileSystem.exist?(source).should be_false
end
describe "when ensure is absent" do
it "should remove the file if present" do
FileUtils.touch(path)
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
- Puppet::FileSystem::File.exist?(path).should be_false
+ Puppet::FileSystem.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
- Puppet::FileSystem::File.exist?(path).should be_false
+ Puppet::FileSystem.exist?(path).should be_false
end
# issue #14599
it "should not fail if parts of path aren't directories" do
FileUtils.touch(path)
catalog.add_resource(described_class.new(:path => File.join(path,'no_such_file'), :ensure => :absent, :backup => :false))
report = catalog.apply.report
report.resource_statuses["File[#{File.join(path,'no_such_file')}]"].should_not be_failed
end
end
describe "when setting permissions" do
it "should set the owner" do
target = tmpfile_with_contents('target', '')
owner = get_owner(target)
catalog.add_resource described_class.new(
:name => target,
:owner => owner
)
catalog.apply
get_owner(target).should == owner
end
it "should set the group" do
target = tmpfile_with_contents('target', '')
group = get_group(target)
catalog.add_resource described_class.new(
:name => target,
:group => group
)
catalog.apply
get_group(target).should == group
end
describe "when setting mode" do
describe "for directories" do
let(:target) { tmpdir('dir_mode') }
it "should set executable bits for newly created directories" do
catalog.add_resource described_class.new(:path => target, :ensure => :directory, :mode => 0600)
catalog.apply
(get_mode(target) & 07777).should == 0700
end
it "should set executable bits for existing readable directories" do
set_mode(0600, target)
catalog.add_resource described_class.new(:path => target, :ensure => :directory, :mode => 0644)
catalog.apply
(get_mode(target) & 07777).should == 0755
end
it "should not set executable bits for unreadable directories" do
begin
catalog.add_resource described_class.new(:path => target, :ensure => :directory, :mode => 0300)
catalog.apply
(get_mode(target) & 07777).should == 0300
ensure
# so we can cleanup
set_mode(0700, target)
end
end
it "should set user, group, and other executable bits" do
catalog.add_resource described_class.new(:path => target, :ensure => :directory, :mode => 0664)
catalog.apply
(get_mode(target) & 07777).should == 0775
end
it "should set executable bits when overwriting a non-executable file" do
target_path = tmpfile_with_contents('executable', '')
set_mode(0444, target_path)
catalog.add_resource described_class.new(:path => target_path, :ensure => :directory, :mode => 0666, :backup => false)
catalog.apply
(get_mode(target_path) & 07777).should == 0777
File.should be_directory(target_path)
end
end
describe "for files" do
it "should not set executable bits" do
catalog.add_resource described_class.new(:path => path, :ensure => :file, :mode => 0666)
catalog.apply
(get_mode(path) & 07777).should == 0666
end
it "should not set executable bits when replacing an executable directory (#10365)" do
pending("bug #10365")
FileUtils.mkdir(path)
set_mode(0777, path)
catalog.add_resource described_class.new(:path => path, :ensure => :file, :mode => 0666, :backup => false, :force => true)
catalog.apply
(get_mode(path) & 07777).should == 0666
end
end
describe "for links", :if => described_class.defaultprovider.feature?(:manages_symlinks) do
let(:link) { tmpfile('link_mode') }
describe "when managing links" do
let(:link_target) { tmpfile('target') }
before :each do
FileUtils.touch(link_target)
File.chmod(0444, link_target)
- Puppet::FileSystem::File.new(link_target).symlink(link)
+ Puppet::FileSystem.symlink(link_target, link)
end
it "should not set the executable bit on the link nor the target" do
catalog.add_resource described_class.new(:path => link, :ensure => :link, :mode => 0666, :target => link_target, :links => :manage)
catalog.apply
- (Puppet::FileSystem::File.new(link).stat.mode & 07777) == 0666
- (Puppet::FileSystem::File.new(link_target).lstat.mode & 07777) == 0444
+ (Puppet::FileSystem.stat(link).mode & 07777) == 0666
+ (Puppet::FileSystem.lstat(link_target).mode & 07777) == 0444
end
it "should ignore dangling symlinks (#6856)" do
File.delete(link_target)
catalog.add_resource described_class.new(:path => link, :ensure => :link, :mode => 0666, :target => link_target, :links => :manage)
catalog.apply
- Puppet::FileSystem::File.exist?(link).should be_false
+ Puppet::FileSystem.exist?(link).should be_false
end
it "should create a link to the target if ensure is omitted" do
FileUtils.touch(link_target)
catalog.add_resource described_class.new(:path => link, :target => link_target)
catalog.apply
- 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
+ Puppet::FileSystem.exist?(link).should be_true
+ Puppet::FileSystem.lstat(link).ftype.should == 'link'
+ Puppet::FileSystem.readlink(link).should == link_target
end
end
describe "when following links" do
it "should ignore dangling symlinks (#6856)" do
target = tmpfile('dangling')
FileUtils.touch(target)
- Puppet::FileSystem::File.new(target).symlink(link)
+ Puppet::FileSystem.symlink(target, link)
File.delete(target)
catalog.add_resource described_class.new(:path => path, :source => link, :mode => 0600, :links => :follow)
catalog.apply
end
describe "to a directory" do
let(:link_target) { tmpdir('dir_target') }
before :each do
File.chmod(0600, link_target)
- Puppet::FileSystem::File.new(link_target).symlink(link)
+ Puppet::FileSystem.symlink(link_target, link)
end
after :each do
File.chmod(0750, link_target)
end
describe "that is readable" do
it "should set the executable bits when creating the destination (#10315)" do
catalog.add_resource described_class.new(:path => path, :source => link, :mode => 0666, :links => :follow)
catalog.apply
File.should be_directory(path)
(get_mode(path) & 07777).should == 0777
end
it "should set the executable bits when overwriting the destination (#10315)" do
FileUtils.touch(path)
catalog.add_resource described_class.new(:path => path, :source => link, :mode => 0666, :links => :follow, :backup => false)
catalog.apply
File.should be_directory(path)
(get_mode(path) & 07777).should == 0777
end
end
describe "that is not readable" do
before :each do
set_mode(0300, link_target)
end
# so we can cleanup
after :each do
set_mode(0700, link_target)
end
it "should set executable bits when creating the destination (#10315)" do
catalog.add_resource described_class.new(:path => path, :source => link, :mode => 0666, :links => :follow)
catalog.apply
File.should be_directory(path)
(get_mode(path) & 07777).should == 0777
end
it "should set executable bits when overwriting the destination" do
FileUtils.touch(path)
catalog.add_resource described_class.new(:path => path, :source => link, :mode => 0666, :links => :follow, :backup => false)
catalog.apply
File.should be_directory(path)
(get_mode(path) & 07777).should == 0777
end
end
end
describe "to a file" do
let(:link_target) { tmpfile('file_target') }
before :each do
FileUtils.touch(link_target)
- Puppet::FileSystem::File.new(link_target).symlink(link)
+ Puppet::FileSystem.symlink(link_target, link)
end
it "should create the file, not a symlink (#2817, #10315)" do
catalog.add_resource described_class.new(:path => path, :source => link, :mode => 0600, :links => :follow)
catalog.apply
File.should be_file(path)
(get_mode(path) & 07777).should == 0600
end
it "should overwrite the file" do
FileUtils.touch(path)
catalog.add_resource described_class.new(:path => path, :source => link, :mode => 0600, :links => :follow)
catalog.apply
File.should be_file(path)
(get_mode(path) & 07777).should == 0600
end
end
describe "to a link to a directory" do
let(:real_target) { tmpdir('real_target') }
let(:target) { tmpfile('target') }
before :each do
File.chmod(0666, real_target)
# link -> target -> real_target
- Puppet::FileSystem::File.new(real_target).symlink(target)
- Puppet::FileSystem::File.new(target).symlink(link)
+ Puppet::FileSystem.symlink(real_target, target)
+ Puppet::FileSystem.symlink(target, link)
end
after :each do
File.chmod(0750, real_target)
end
describe "when following all links" do
it "should create the destination and apply executable bits (#10315)" do
catalog.add_resource described_class.new(:path => path, :source => link, :mode => 0600, :links => :follow)
catalog.apply
File.should be_directory(path)
(get_mode(path) & 07777).should == 0700
end
it "should overwrite the destination and apply executable bits" do
FileUtils.mkdir(path)
catalog.add_resource described_class.new(:path => path, :source => link, :mode => 0600, :links => :follow)
catalog.apply
File.should be_directory(path)
(get_mode(path) & 0111).should == 0100
end
end
end
end
end
end
end
describe "when writing files" do
it "should backup files to a filebucket when one is configured" do
filebucket = Puppet::Type.type(:filebucket).new :path => tmpfile("filebucket"), :name => "mybucket"
file = described_class.new :path => path, :backup => "mybucket", :content => "foo"
catalog.add_resource file
catalog.add_resource filebucket
File.open(file[:path], "w") { |f| f.write("bar") }
md5 = Digest::MD5.hexdigest("bar")
catalog.apply
filebucket.bucket.getfile(md5).should == "bar"
end
it "should backup files in the local directory when a backup string is provided" do
file = described_class.new :path => path, :backup => ".bak", :content => "foo"
catalog.add_resource file
File.open(file[:path], "w") { |f| f.puts "bar" }
catalog.apply
backup = file[:path] + ".bak"
- Puppet::FileSystem::File.exist?(backup).should be_true
+ Puppet::FileSystem.exist?(backup).should be_true
File.read(backup).should == "bar\n"
end
it "should fail if no backup can be performed" do
dir = tmpdir("backups")
file = described_class.new :path => File.join(dir, "testfile"), :backup => ".bak", :content => "foo"
catalog.add_resource file
File.open(file[:path], 'w') { |f| f.puts "bar" }
# Create a directory where the backup should be so that writing to it fails
Dir.mkdir(File.join(dir, "testfile.bak"))
Puppet::Util::Log.stubs(:newmessage)
catalog.apply
File.read(file[:path]).should == "bar\n"
end
it "should not backup symlinks", :if => described_class.defaultprovider.feature?(:manages_symlinks) do
link = tmpfile("link")
dest1 = tmpfile("dest1")
dest2 = tmpfile("dest2")
bucket = Puppet::Type.type(:filebucket).new :path => tmpfile("filebucket"), :name => "mybucket"
file = described_class.new :path => link, :target => dest2, :ensure => :link, :backup => "mybucket"
catalog.add_resource file
catalog.add_resource bucket
File.open(dest1, "w") { |f| f.puts "whatever" }
- Puppet::FileSystem::File.new(dest1).symlink(link)
+ Puppet::FileSystem.symlink(dest1, link)
md5 = Digest::MD5.hexdigest(File.read(file[:path]))
catalog.apply
- Puppet::FileSystem::File.new(link).readlink().should == dest2
- Puppet::FileSystem::File.exist?(bucket[:path]).should be_false
+ Puppet::FileSystem.readlink(link).should == dest2
+ Puppet::FileSystem.exist?(bucket[:path]).should be_false
end
it "should backup directories to the local filesystem by copying the whole directory" do
file = described_class.new :path => path, :backup => ".bak", :content => "foo", :force => true
catalog.add_resource file
Dir.mkdir(path)
otherfile = File.join(path, "foo")
File.open(otherfile, "w") { |f| f.print "yay" }
catalog.apply
backup = "#{path}.bak"
FileTest.should be_directory(backup)
File.read(File.join(backup, "foo")).should == "yay"
end
it "should backup directories to filebuckets by backing up each file separately" do
bucket = Puppet::Type.type(:filebucket).new :path => tmpfile("filebucket"), :name => "mybucket"
file = described_class.new :path => tmpfile("bucket_backs"), :backup => "mybucket", :content => "foo", :force => true
catalog.add_resource file
catalog.add_resource bucket
Dir.mkdir(file[:path])
foofile = File.join(file[:path], "foo")
barfile = File.join(file[:path], "bar")
File.open(foofile, "w") { |f| f.print "fooyay" }
File.open(barfile, "w") { |f| f.print "baryay" }
foomd5 = Digest::MD5.hexdigest(File.read(foofile))
barmd5 = Digest::MD5.hexdigest(File.read(barfile))
catalog.apply
bucket.bucket.getfile(foomd5).should == "fooyay"
bucket.bucket.getfile(barmd5).should == "baryay"
end
end
describe "when recursing" do
def build_path(dir)
Dir.mkdir(dir)
File.chmod(0750, dir)
@dirs = [dir]
@files = []
%w{one two}.each do |subdir|
fdir = File.join(dir, subdir)
Dir.mkdir(fdir)
File.chmod(0750, fdir)
@dirs << fdir
%w{three}.each do |file|
ffile = File.join(fdir, file)
@files << ffile
File.open(ffile, "w") { |f| f.puts "test #{file}" }
File.chmod(0640, ffile)
end
end
end
it "should be able to recurse over a nonexistent file" do
@file = described_class.new(
:name => path,
:mode => 0644,
:recurse => true,
:backup => false
)
catalog.add_resource @file
lambda { @file.eval_generate }.should_not raise_error
end
it "should be able to recursively set properties on existing files" do
path = tmpfile("file_integration_tests")
build_path(path)
file = described_class.new(
:name => path,
:mode => 0644,
:recurse => true,
:backup => false
)
catalog.add_resource file
catalog.apply
@dirs.should_not be_empty
@dirs.each do |path|
(get_mode(path) & 007777).should == 0755
end
@files.should_not be_empty
@files.each do |path|
(get_mode(path) & 007777).should == 0644
end
end
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)
dest = tmpfile("file_link_integration_dest")
@file = described_class.new(:name => dest, :target => source, :recurse => true, :ensure => :link, :backup => false)
catalog.add_resource @file
catalog.apply
@dirs.each do |path|
link_path = path.sub(source, dest)
- Puppet::FileSystem::File.new(link_path).lstat.should be_directory
+ Puppet::FileSystem.lstat(link_path).should be_directory
end
@files.each do |path|
link_path = path.sub(source, dest)
- Puppet::FileSystem::File.new(link_path).lstat.ftype.should == "link"
+ Puppet::FileSystem.lstat(link_path).ftype.should == "link"
end
end
it "should be able to recursively copy files" do
source = tmpfile("file_source_integration_source")
build_path(source)
dest = tmpfile("file_source_integration_dest")
@file = described_class.new(:name => dest, :source => source, :recurse => true, :backup => false)
catalog.add_resource @file
catalog.apply
@dirs.each do |path|
newpath = path.sub(source, dest)
- Puppet::FileSystem::File.new(newpath).lstat.should be_directory
+ Puppet::FileSystem.lstat(newpath).should be_directory
end
@files.each do |path|
newpath = path.sub(source, dest)
- Puppet::FileSystem::File.new(newpath).lstat.ftype.should == "file"
+ Puppet::FileSystem.lstat(newpath).ftype.should == "file"
end
end
it "should not recursively manage files managed by a more specific explicit file" do
dir = tmpfile("recursion_vs_explicit_1")
subdir = File.join(dir, "subdir")
file = File.join(subdir, "file")
FileUtils.mkdir_p(subdir)
File.open(file, "w") { |f| f.puts "" }
base = described_class.new(:name => dir, :recurse => true, :backup => false, :mode => "755")
sub = described_class.new(:name => subdir, :recurse => true, :backup => false, :mode => "644")
catalog.add_resource base
catalog.add_resource sub
catalog.apply
(get_mode(file) & 007777).should == 0644
end
it "should recursively manage files even if there is an explicit file whose name is a prefix of the managed file" do
managed = File.join(path, "file")
generated = File.join(path, "file_with_a_name_starting_with_the_word_file")
managed_mode = 0700
FileUtils.mkdir_p(path)
FileUtils.touch(managed)
FileUtils.touch(generated)
catalog.add_resource described_class.new(:name => path, :recurse => true, :backup => false, :mode => managed_mode)
catalog.add_resource described_class.new(:name => managed, :recurse => true, :backup => false, :mode => "644")
catalog.apply
(get_mode(generated) & 007777).should == managed_mode
end
describe "when recursing remote directories" do
describe "when sourceselect first" do
describe "for a directory" do
it "should recursively copy the first directory that exists" do
one = File.expand_path('thisdoesnotexist')
two = tmpdir('two')
FileUtils.mkdir_p(File.join(two, 'three'))
FileUtils.touch(File.join(two, 'three', 'four'))
catalog.add_resource Puppet::Type.newfile(
:path => path,
:ensure => :directory,
:backup => false,
:recurse => true,
:sourceselect => :first,
:source => [one, two]
)
catalog.apply
File.should be_directory(path)
- Puppet::FileSystem::File.exist?(File.join(path, 'one')).should be_false
- Puppet::FileSystem::File.exist?(File.join(path, 'three', 'four')).should be_true
+ Puppet::FileSystem.exist?(File.join(path, 'one')).should be_false
+ Puppet::FileSystem.exist?(File.join(path, 'three', 'four')).should be_true
end
it "should recursively copy an empty directory" do
one = File.expand_path('thisdoesnotexist')
two = tmpdir('two')
three = tmpdir('three')
file_in_dir_with_contents(three, 'a', '')
catalog.add_resource Puppet::Type.newfile(
:path => path,
:ensure => :directory,
:backup => false,
:recurse => true,
:sourceselect => :first,
:source => [one, two, three]
)
catalog.apply
File.should be_directory(path)
- Puppet::FileSystem::File.exist?(File.join(path, 'a')).should be_false
+ Puppet::FileSystem.exist?(File.join(path, 'a')).should be_false
end
it "should only recurse one level" do
one = tmpdir('one')
FileUtils.mkdir_p(File.join(one, 'a', 'b'))
FileUtils.touch(File.join(one, 'a', 'b', 'c'))
two = tmpdir('two')
FileUtils.mkdir_p(File.join(two, 'z'))
FileUtils.touch(File.join(two, 'z', 'y'))
catalog.add_resource Puppet::Type.newfile(
:path => path,
:ensure => :directory,
:backup => false,
:recurse => true,
:recurselimit => 1,
:sourceselect => :first,
:source => [one, two]
)
catalog.apply
- 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
+ Puppet::FileSystem.exist?(File.join(path, 'a')).should be_true
+ Puppet::FileSystem.exist?(File.join(path, 'a', 'b')).should be_false
+ Puppet::FileSystem.exist?(File.join(path, 'z')).should be_false
end
end
describe "for a file" do
it "should copy the first file that exists" do
one = File.expand_path('thisdoesnotexist')
two = tmpfile_with_contents('two', 'yay')
three = tmpfile_with_contents('three', 'no')
catalog.add_resource Puppet::Type.newfile(
:path => path,
:ensure => :file,
:backup => false,
:sourceselect => :first,
:source => [one, two, three]
)
catalog.apply
File.read(path).should == 'yay'
end
it "should copy an empty file" do
one = File.expand_path('thisdoesnotexist')
two = tmpfile_with_contents('two', '')
three = tmpfile_with_contents('three', 'no')
catalog.add_resource Puppet::Type.newfile(
:path => path,
:ensure => :file,
:backup => false,
:sourceselect => :first,
:source => [one, two, three]
)
catalog.apply
File.read(path).should == ''
end
end
end
describe "when sourceselect all" do
describe "for a directory" do
it "should recursively copy all sources from the first valid source" do
dest = tmpdir('dest')
one = tmpdir('one')
two = tmpdir('two')
three = tmpdir('three')
four = tmpdir('four')
file_in_dir_with_contents(one, 'a', one)
file_in_dir_with_contents(two, 'a', two)
file_in_dir_with_contents(two, 'b', two)
file_in_dir_with_contents(three, 'a', three)
file_in_dir_with_contents(three, 'c', three)
obj = Puppet::Type.newfile(
:path => dest,
:ensure => :directory,
:backup => false,
:recurse => true,
:sourceselect => :all,
:source => [one, two, three, four]
)
catalog.add_resource obj
catalog.apply
File.read(File.join(dest, 'a')).should == one
File.read(File.join(dest, 'b')).should == two
File.read(File.join(dest, 'c')).should == three
end
it "should only recurse one level from each valid source" do
one = tmpdir('one')
FileUtils.mkdir_p(File.join(one, 'a', 'b'))
FileUtils.touch(File.join(one, 'a', 'b', 'c'))
two = tmpdir('two')
FileUtils.mkdir_p(File.join(two, 'z'))
FileUtils.touch(File.join(two, 'z', 'y'))
obj = Puppet::Type.newfile(
:path => path,
:ensure => :directory,
:backup => false,
:recurse => true,
:recurselimit => 1,
:sourceselect => :all,
:source => [one, two]
)
catalog.add_resource obj
catalog.apply
- 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
+ Puppet::FileSystem.exist?(File.join(path, 'a')).should be_true
+ Puppet::FileSystem.exist?(File.join(path, 'a', 'b')).should be_false
+ Puppet::FileSystem.exist?(File.join(path, 'z')).should be_true
+ Puppet::FileSystem.exist?(File.join(path, 'z', 'y')).should be_false
end
end
end
end
end
describe "when generating resources" do
before do
source = tmpdir("generating_in_catalog_source")
s1 = file_in_dir_with_contents(source, "one", "uno")
s2 = file_in_dir_with_contents(source, "two", "dos")
@file = described_class.new(
:name => path,
:source => source,
:recurse => true,
:backup => false
)
catalog.add_resource @file
end
it "should add each generated resource to the catalog" do
catalog.apply do |trans|
catalog.resource(:file, File.join(path, "one")).must be_a(described_class)
catalog.resource(:file, File.join(path, "two")).must be_a(described_class)
end
end
it "should have an edge to each resource in the relationship graph" do
catalog.apply do |trans|
one = catalog.resource(:file, File.join(path, "one"))
catalog.relationship_graph.should be_edge(@file, one)
two = catalog.resource(:file, File.join(path, "two"))
catalog.relationship_graph.should be_edge(@file, two)
end
end
end
describe "when copying files" do
it "should be able to copy files with pound signs in their names (#285)" do
source = tmpfile_with_contents("filewith#signs", "foo")
dest = tmpfile("destwith#signs")
catalog.add_resource described_class.new(:name => dest, :source => source)
catalog.apply
File.read(dest).should == "foo"
end
it "should be able to copy files with spaces in their names" do
dest = tmpfile("destwith spaces")
source = tmpfile_with_contents("filewith spaces", "foo")
- File.chmod(0755, source)
+
+ expected_mode = 0755
+ Puppet::FileSystem.chmod(expected_mode, source)
catalog.add_resource described_class.new(:path => dest, :source => source)
catalog.apply
- expected_mode = Puppet.features.microsoft_windows? ? 0644 : 0755
File.read(dest).should == "foo"
- (Puppet::FileSystem::File.new(dest).stat.mode & 007777).should == expected_mode
+ (Puppet::FileSystem.stat(dest).mode & 007777).should == expected_mode
end
it "should be able to copy individual files even if recurse has been specified" do
source = tmpfile_with_contents("source", "foo")
dest = tmpfile("dest")
catalog.add_resource described_class.new(:name => dest, :source => source, :recurse => true)
catalog.apply
File.read(dest).should == "foo"
end
end
it "should create a file with content if ensure is omitted" do
catalog.add_resource described_class.new(
:path => path,
:content => "this is some content, yo"
)
catalog.apply
File.read(path).should == "this is some content, yo"
end
it "should create files with content if both content and ensure are set" do
file = described_class.new(
:path => path,
:ensure => "file",
:content => "this is some content, yo"
)
catalog.add_resource file
catalog.apply
File.read(path).should == "this is some content, yo"
end
it "should delete files with sources but that are set for deletion" do
source = tmpfile_with_contents("source_source_with_ensure", "yay")
dest = tmpfile_with_contents("source_source_with_ensure", "boo")
file = described_class.new(
:path => dest,
:ensure => :absent,
:source => source,
:backup => false
)
catalog.add_resource file
catalog.apply
- Puppet::FileSystem::File.exist?(dest).should be_false
+ Puppet::FileSystem.exist?(dest).should be_false
end
describe "when sourcing" do
let(:source) { tmpfile_with_contents("source_default_values", "yay") }
it "should apply the source metadata values" do
set_mode(0770, source)
file = described_class.new(
:path => path,
:ensure => :file,
:source => source,
:backup => false
)
catalog.add_resource file
catalog.apply
get_owner(path).should == get_owner(source)
get_group(path).should == get_group(source)
(get_mode(path) & 07777).should == 0770
end
it "should override the default metadata values" do
set_mode(0770, source)
file = described_class.new(
:path => path,
:ensure => :file,
:source => source,
:backup => false,
:mode => 0440
)
catalog.add_resource file
catalog.apply
(get_mode(path) & 07777).should == 0440
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?).returns(false)
Puppet::Util::Windows::Security.stubs(:supports_acl?).with(source).returns false
file = described_class.new(
:path => path,
:ensure => :file,
:source => source,
:backup => false
)
catalog.add_resource file
catalog.apply
get_owner(path).should =~ /^S\-1\-5\-.*$/
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
+
+ describe "with :links set to :follow" do
+ it "should not fail to apply" do
+ # at minimal, we need an owner and/or group
+ @file[:owner] = @sids[:users]
+ @file[:links] = :follow
+
+ catalog.apply do |transaction|
+ if transaction.any_failed?
+ pretty_transaction_error(transaction)
+ end
+ end
+ end
+ end
end
describe "on directories" do
before :each do
@directory = described_class.new(
:path => dir,
:ensure => :directory
)
catalog.add_resource @directory
end
+ def grant_everyone_full_access(path)
+ sd = Puppet::Util::Windows::Security.get_security_descriptor(path)
+ sd.dacl.allow(
+ 'S-1-1-0', #everyone
+ Windows::File::FILE_ALL_ACCESS,
+ Windows::File::OBJECT_INHERIT_ACE | Windows::File::CONTAINER_INHERIT_ACE)
+ Puppet::Util::Windows::Security.set_security_descriptor(path, sd)
+ end
+
+ after :each do
+ grant_everyone_full_access(dir)
+ 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
+
+ describe "with :links set to :follow" do
+ it "should not fail to apply" do
+ # at minimal, we need an owner and/or group
+ @directory[:owner] = @sids[:users]
+ @directory[:links] = :follow
+
+ catalog.apply do |transaction|
+ if transaction.any_failed?
+ pretty_transaction_error(transaction)
+ end
+ end
+ end
+ end
end
end
end
end
end
describe "when purging files" do
before do
sourcedir = tmpdir("purge_source")
destdir = tmpdir("purge_dest")
sourcefile = File.join(sourcedir, "sourcefile")
@copiedfile = File.join(destdir, "sourcefile")
@localfile = File.join(destdir, "localfile")
@purgee = File.join(destdir, "to_be_purged")
File.open(@localfile, "w") { |f| f.print "oldtest" }
File.open(sourcefile, "w") { |f| f.print "funtest" }
# this file should get removed
File.open(@purgee, "w") { |f| f.print "footest" }
lfobj = Puppet::Type.newfile(
:title => "localfile",
:path => @localfile,
:content => "rahtest",
:ensure => :file,
:backup => false
)
destobj = Puppet::Type.newfile(
:title => "destdir",
:path => destdir,
:source => sourcedir,
:backup => false,
:purge => true,
:recurse => true
)
catalog.add_resource lfobj, destobj
catalog.apply
end
it "should still copy remote files" do
File.read(@copiedfile).should == 'funtest'
end
it "should not purge managed, local files" do
File.read(@localfile).should == 'rahtest'
end
it "should purge files that are neither remote nor otherwise managed" do
- Puppet::FileSystem::File.exist?(@purgee).should be_false
+ Puppet::FileSystem.exist?(@purgee).should be_false
+ end
+ end
+
+ describe "when using validate_cmd" do
+ it "should fail the file resource if command fails" do
+ catalog.add_resource(described_class.new(:path => path, :content => "foo", :validate_cmd => "/usr/bin/env false"))
+ Puppet::Util::Execution.expects(:execute).with("/usr/bin/env false", {:combine => true, :failonfail => true}).raises(Puppet::ExecutionFailure, "Failed")
+ report = catalog.apply.report
+ report.resource_statuses["File[#{path}]"].should be_failed
+ Puppet::FileSystem.exist?(path).should be_false
+ end
+
+ it "should succeed the file resource if command succeeds" do
+ catalog.add_resource(described_class.new(:path => path, :content => "foo", :validate_cmd => "/usr/bin/env true"))
+ Puppet::Util::Execution.expects(:execute).with("/usr/bin/env true", {:combine => true, :failonfail => true}).returns ''
+ report = catalog.apply.report
+ report.resource_statuses["File[#{path}]"].should_not be_failed
+ Puppet::FileSystem.exist?(path).should be_true
end
end
def tmpfile_with_contents(name, contents)
file = tmpfile(name)
File.open(file, "w") { |f| f.write contents }
file
end
def file_in_dir_with_contents(dir, name, contents)
full_name = File.join(dir, name)
File.open(full_name, "w") { |f| f.write contents }
full_name
end
+
+ def pretty_transaction_error(transaction)
+ report = transaction.report
+ status_failures = report.resource_statuses.values.select { |r| r.failed? }
+ status_fail_msg = status_failures.
+ collect(&:events).
+ flatten.
+ select { |event| event.status == 'failure' }.
+ collect { |event| "#{event.resource}: #{event.message}" }.join("; ")
+
+ raise "Got #{status_failures.length} failure(s) while applying: #{status_fail_msg}"
+ end
end
diff --git a/spec/integration/type/nagios_spec.rb b/spec/integration/type/nagios_spec.rb
new file mode 100644
index 000000000..818b61649
--- /dev/null
+++ b/spec/integration/type/nagios_spec.rb
@@ -0,0 +1,80 @@
+#!/usr/bin/env ruby
+
+require 'spec_helper'
+require 'puppet/file_bucket/dipper'
+
+describe "Nagios file creation" do
+ include PuppetSpec::Files
+
+ before :each do
+ FileUtils.touch(target_file)
+ File.chmod(0600, target_file)
+ Puppet::FileBucket::Dipper.any_instance.stubs(:backup) # Don't backup to filebucket
+ end
+
+ let :target_file do
+ tmpfile('nagios_integration_specs')
+ end
+
+ # Copied from the crontab integration spec.
+ #
+ # @todo This should probably live in the PuppetSpec module instead then.
+ def run_in_catalog(*resources)
+ catalog = Puppet::Resource::Catalog.new
+ catalog.host_config = false
+ resources.each do |resource|
+ resource.expects(:err).never
+ catalog.add_resource(resource)
+ end
+
+ # the resources are not properly contained and generated resources
+ # will end up with dangling edges without this stubbing:
+ catalog.stubs(:container_of).returns resources[0]
+ catalog.apply
+ end
+
+ # These three helpers are from file_spec.rb
+ #
+ # @todo Define those centrally as well?
+ def get_mode(file)
+ Puppet::FileSystem.stat(file).mode
+ end
+
+ context "when creating a nagios config file" do
+ context "which is not managed" do
+ it "should choose the file mode if requested" do
+ resource = Puppet::Type.type(:nagios_host).new(
+ :name => 'spechost',
+ :use => 'spectemplate',
+ :ensure => 'present',
+ :target => target_file,
+ :mode => '0640'
+ )
+ run_in_catalog(resource)
+ # sticky bit only applies to directories in Windows
+ mode = Puppet.features.microsoft_windows? ? "640" : "100640"
+ ( "%o" % get_mode(target_file) ).should == mode
+ end
+ end
+
+ context "which is managed" do
+ it "should not the mode" do
+ file_res = Puppet::Type.type(:file).new(
+ :name => target_file,
+ :ensure => :present
+ )
+ nag_res = Puppet::Type.type(:nagios_host).new(
+ :name => 'spechost',
+ :use => 'spectemplate',
+ :ensure => :present,
+ :target => target_file,
+ :mode => '0640'
+ )
+ run_in_catalog(file_res, nag_res)
+ ( "%o" % get_mode(target_file) ).should_not == "100640"
+ end
+ end
+
+ end
+
+end
diff --git a/spec/integration/type/tidy_spec.rb b/spec/integration/type/tidy_spec.rb
index 562ae17e3..9c044d703 100755
--- a/spec/integration/type/tidy_spec.rb
+++ b/spec/integration/type/tidy_spec.rb
@@ -1,31 +1,31 @@
#! /usr/bin/env ruby
require 'spec_helper'
require 'puppet_spec/files'
require 'puppet/file_bucket/dipper'
describe Puppet::Type.type(:tidy) do
include PuppetSpec::Files
before do
Puppet::Util::Storage.stubs(:store)
end
# Testing #355.
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)
- Puppet::FileSystem::File.new(target).symlink(link)
+ Puppet::FileSystem.symlink(target, link)
tidy = Puppet::Type.type(:tidy).new :path => dir, :recurse => true
catalog = Puppet::Resource::Catalog.new
catalog.add_resource(tidy)
catalog.apply
- Puppet::FileSystem::File.new(link).symlink?.should be_false
+ Puppet::FileSystem.symlink?(link).should be_false
end
end
diff --git a/spec/integration/util/execution_spec.rb b/spec/integration/util/execution_spec.rb
new file mode 100644
index 000000000..b14798236
--- /dev/null
+++ b/spec/integration/util/execution_spec.rb
@@ -0,0 +1,17 @@
+require 'spec_helper'
+
+describe Puppet::Util::Execution do
+ describe "#execpipe" do
+ it "should set LANG to C avoid localized output", :if => !Puppet.features.microsoft_windows? do
+ out = ""
+ Puppet::Util::Execution.execpipe('echo $LANG'){ |line| out << line.read.chomp }
+ expect(out).to eq("C")
+ end
+
+ it "should set LC_ALL to C avoid localized output", :if => !Puppet.features.microsoft_windows? do
+ out = ""
+ Puppet::Util::Execution.execpipe('echo $LC_ALL'){ |line| out << line.read.chomp }
+ expect(out).to eq("C")
+ end
+ end
+end
diff --git a/spec/integration/util/rdoc/parser_spec.rb b/spec/integration/util/rdoc/parser_spec.rb
index 58c0a882d..d3bbef45c 100755
--- a/spec/integration/util/rdoc/parser_spec.rb
+++ b/spec/integration/util/rdoc/parser_spec.rb
@@ -1,261 +1,261 @@
#! /usr/bin/env ruby
require 'spec_helper'
require 'puppet/util/rdoc'
describe "RDoc::Parser" do
require 'puppet_spec/files'
include PuppetSpec::Files
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
def write_file(file, content)
FileUtils.mkdir_p(File.dirname(file))
File.open(file, 'w') do |f|
f.puts(content)
end
end
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}")
+ Puppet::FileSystem.exist?(file).should(be_true, "Cannot find #{file}")
content_patterns.each do |pattern|
content = File.read(file)
content.should match(pattern)
end
end
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
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
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
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
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
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
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 0da0938e9..a76ade637 100755
--- a/spec/integration/util/settings_spec.rb
+++ b/spec/integration/util/settings_spec.rb
@@ -1,89 +1,89 @@
#! /usr/bin/env ruby
require 'spec_helper'
require 'puppet_spec/files'
describe Puppet::Settings do
include PuppetSpec::Files
def minimal_default_settings
{ :noop => {:default => false, :desc => "noop"} }
end
def define_settings(section, settings_hash)
settings.define_settings(section, minimal_default_settings.update(settings_hash))
end
let(:settings) { Puppet::Settings.new }
it "should be able to make needed directories" do
define_settings(:main,
:maindir => {
:default => tmpfile("main"),
:type => :directory,
:desc => "a",
}
)
settings.use(:main)
expect(File.directory?(settings[:maindir])).to be_true
end
it "should make its directories with the correct modes" do
define_settings(:main,
:maindir => {
:default => tmpfile("main"),
:type => :directory,
:desc => "a",
:mode => 0750
}
)
settings.use(:main)
- expect(Puppet::FileSystem::File.new(settings[:maindir]).stat.mode & 007777).to eq(Puppet.features.microsoft_windows? ? 0755 : 0750)
+ expect(Puppet::FileSystem.stat(settings[:maindir]).mode & 007777).to eq(0750)
end
it "reparses configuration if configuration file is touched", :if => !Puppet.features.microsoft_windows? do
config = tmpfile("config")
define_settings(:main,
:config => {
:type => :file,
:default => config,
:desc => "a"
},
:environment => {
:default => 'dingos',
:desc => 'test',
}
)
Puppet[:filetimeout] = '1s'
File.open(config, 'w') do |file|
file.puts <<-EOF
[main]
environment=toast
EOF
end
settings.initialize_global_settings
expect(settings[:environment]).to eq('toast')
# First reparse establishes WatchedFiles
settings.reparse_config_files
sleep 1
File.open(config, 'w') do |file|
file.puts <<-EOF
[main]
environment=bacon
EOF
end
# Second reparse if later than filetimeout, reparses if changed
settings.reparse_config_files
expect(settings[:environment]).to eq('bacon')
end
end
diff --git a/spec/integration/util/windows/security_spec.rb b/spec/integration/util/windows/security_spec.rb
index 99d866f7f..023aad800 100755
--- a/spec/integration/util/windows/security_spec.rb
+++ b/spec/integration/util/windows/security_spec.rb
@@ -1,821 +1,846 @@
#!/usr/bin/env ruby
require 'spec_helper'
require 'puppet/util/adsi'
if Puppet.features.microsoft_windows?
class WindowsSecurityTester
require 'puppet/util/windows/security'
include Puppet::Util::Windows::Security
end
end
describe "Puppet::Util::Windows::Security", :if => Puppet.features.microsoft_windows? do
include PuppetSpec::Files
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
}
+ # The TCP/IP NetBIOS Helper service (aka 'lmhosts') has ended up
+ # disabled on some VMs for reasons we couldn't track down. This
+ # condition causes tests which rely on resolving UNC style paths
+ # (like \\localhost) to fail with unhelpful error messages.
+ # Put a check for this upfront to aid debug should this strike again.
+ service = Puppet::Type.type(:service).new(:name => 'lmhosts')
+ service.provider.status.should == :running
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
+ def grant_everyone_full_access(path)
+ sd = winsec.get_security_descriptor(path)
+ everyone = 'S-1-1-0'
+ inherit = WindowsSecurityTester::OBJECT_INHERIT_ACE | WindowsSecurityTester::CONTAINER_INHERIT_ACE
+ sd.dacl.allow(everyone, Windows::File::FILE_ALL_ACCESS, inherit)
+ winsec.set_security_descriptor(path, sd)
+ end
+
shared_examples_for "only child owner" 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)
check_delete(path)
end
it "should deny parent owner" do
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
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
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
shared_examples_for "a securable object" do
describe "on a volume that doesn't support ACLs" do
[:owner, :group, :mode].each do |p|
it "should return nil #{p}" do
winsec.stubs(:supports_acl?).returns false
winsec.send("get_#{p}", path).should be_nil
end
end
end
describe "on a volume that supports ACLs" do
describe "for a normal user" do
before :each do
Puppet.features.stubs(:root?).returns(false)
end
after :each do
winsec.set_mode(WindowsSecurityTester::S_IRWXU, parent)
- winsec.set_mode(WindowsSecurityTester::S_IRWXU, path) if Puppet::FileSystem::File.exist?(path)
+ winsec.set_mode(WindowsSecurityTester::S_IRWXU, path) if Puppet::FileSystem.exist?(path)
end
describe "#supports_acl?" do
%w[c:/ c:\\ c:/windows/system32 \\\\localhost\\C$ \\\\127.0.0.1\\C$\\foo].each do |path|
it "should accept #{path}" do
winsec.should be_supports_acl(path)
end
end
it "should raise an exception if it cannot get volume information" do
expect {
winsec.supports_acl?('foobar')
}.to raise_error(Puppet::Error, /Failed to get volume information/)
end
end
describe "#owner=" do
it "should allow setting to the current user" do
winsec.set_owner(sids[:current_user], path)
end
it "should raise an exception when setting to a different user" do
lambda { winsec.set_owner(sids[:guest], path) }.should raise_error(Puppet::Error, /This security ID may not be assigned as the owner of this object./)
end
end
describe "#owner" do
it "it should not be empty" do
winsec.get_owner(path).should_not be_empty
end
it "should raise an exception if an invalid path is provided" do
lambda { winsec.get_owner("c:\\doesnotexist.txt") }.should raise_error(Puppet::Error, /The system cannot find the file specified./)
end
end
describe "#group=" do
it "should allow setting to a group the current owner is a member of" do
winsec.set_group(sids[:users], path)
end
# Unlike unix, if the user has permission to WRITE_OWNER, which the file owner has by default,
# then they can set the primary group to a group that the user does not belong to.
it "should allow setting to a group the current owner is not a member of" do
winsec.set_group(sids[:power_users], path)
end
end
describe "#group" do
it "should not be empty" do
winsec.get_group(path).should_not be_empty
end
it "should raise an exception if an invalid path is provided" do
lambda { winsec.get_group("c:\\doesnotexist.txt") }.should raise_error(Puppet::Error, /The system cannot find the file specified./)
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
winsec.set_mode(mode, path)
check_access(mode, path)
end
end
it "should round-trip all 128 modes that do not require deny ACEs" do
0.upto(1).each do |s|
0.upto(7).each do |u|
0.upto(u).each do |g|
0.upto(g).each do |o|
# if user is superset of group, and group superset of other, then
# no deny ace is required, and mode can be converted to win32
# access mask, and back to mode without loss of information
# (provided the owner and group are not the same)
next if ((u & g) != g) or ((g & o) != o)
mode = (s << 9 | u << 6 | g << 3 | o << 0)
winsec.set_mode(mode, path)
winsec.get_mode(path).to_s(8).should == mode.to_s(8)
end
end
end
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)
winsec.get_mode(path).to_s(8).should == "666"
end
it "should combine user and group modes when owner and group sids are equal" do
winsec.set_group(winsec.get_owner(path), path)
winsec.set_mode(0410, path)
winsec.get_mode(path).to_s(8).should == "550"
end
end
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
it "should make them writable if any sid has write permission" do
winsec.set_mode(WindowsSecurityTester::S_IWUSR, path)
(winsec.get_attributes(path) & WindowsSecurityTester::FILE_ATTRIBUTE_READONLY).should == 0
end
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
it "should raise an exception if an invalid path is provided" do
lambda { winsec.set_mode(sids[:guest], "c:\\doesnotexist.txt") }.should raise_error(Puppet::Error, /The system cannot find the file specified./)
end
end
describe "#mode" do
it "should report when extra aces are encounted" do
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 == WindowsSecurityTester::S_IEXTRA
end
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)
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
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
it "should raise an exception if an invalid path is provided" do
lambda { winsec.get_mode("c:\\doesnotexist.txt") }.should raise_error(Puppet::Error, /The system cannot find the file specified./)
end
end
describe "inherited access control entries" 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)
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
# add a bunch of aces to the parent with permission to add children
allow = WindowsSecurityTester::STANDARD_RIGHTS_ALL | WindowsSecurityTester::SPECIFIC_RIGHTS_ALL
inherit = WindowsSecurityTester::OBJECT_INHERIT_ACE | WindowsSecurityTester::CONTAINER_INHERIT_ACE
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)
(winsec.get_mode(path) & WindowsSecurityTester::S_IEXTRA).should == WindowsSecurityTester::S_IEXTRA
end
end
end
describe "for an administrator", :if => Puppet.features.root? do
before :each do
winsec.set_mode(WindowsSecurityTester::S_IRWXU | WindowsSecurityTester::S_IRWXG, 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 Puppet::FileSystem::File.exist?(path)
+ if Puppet::FileSystem.exist?(path)
winsec.set_owner(sids[:current_user], path)
winsec.set_mode(WindowsSecurityTester::S_IRWXU, path)
end
end
describe "#owner=" do
it "should accept a user sid" do
winsec.set_owner(sids[:admin], path)
winsec.get_owner(path).should == sids[:admin]
end
it "should accept a group sid" do
winsec.set_owner(sids[:power_users], path)
winsec.get_owner(path).should == sids[:power_users]
end
it "should raise an exception if an invalid sid is provided" do
lambda { winsec.set_owner("foobar", path) }.should raise_error(Puppet::Error, /Failed to convert string SID/)
end
it "should raise an exception if an invalid path is provided" do
lambda { winsec.set_owner(sids[:guest], "c:\\doesnotexist.txt") }.should raise_error(Puppet::Error, /The system cannot find the file specified./)
end
end
describe "#group=" do
it "should accept a group sid" do
winsec.set_group(sids[:power_users], path)
winsec.get_group(path).should == sids[:power_users]
end
it "should accept a user sid" do
winsec.set_group(sids[:admin], path)
winsec.get_group(path).should == sids[:admin]
end
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
# 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
lambda { winsec.set_group("foobar", path) }.should raise_error(Puppet::Error, /Failed to convert string SID/)
end
it "should raise an exception if an invalid path is provided" do
lambda { winsec.set_group(sids[:guest], "c:\\doesnotexist.txt") }.should raise_error(Puppet::Error, /The system cannot find the file specified./)
end
end
describe "when the sid is NULL" do
it "should retrieve an empty owner sid"
it "should retrieve an empty group sid"
end
describe "when the sid refers to a deleted trustee" do
it "should retrieve the user sid" do
sid = nil
user = Puppet::Util::ADSI::User.create("delete_me_user")
user.commit
begin
sid = Sys::Admin::get_user(user.name).sid
winsec.set_owner(sid, path)
winsec.set_mode(WindowsSecurityTester::S_IRWXU, path)
ensure
Puppet::Util::ADSI::User.delete(user.name)
end
winsec.get_owner(path).should == sid
winsec.get_mode(path).should == WindowsSecurityTester::S_IRWXU
end
it "should retrieve the group sid" do
sid = nil
group = Puppet::Util::ADSI::Group.create("delete_me_group")
group.commit
begin
sid = Sys::Admin::get_group(group.name).sid
winsec.set_group(sid, path)
winsec.set_mode(WindowsSecurityTester::S_IRWXG, path)
ensure
Puppet::Util::ADSI::Group.delete(group.name)
end
winsec.get_group(path).should == sid
winsec.get_mode(path).should == WindowsSecurityTester::S_IRWXG
end
end
describe "#mode" do
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 == WindowsSecurityTester::S_ISYSTEM_MISSING
end
# REMIND: ruby crashes when trying to set a NULL DACL
# it "should allow all when it is nil" do
# winsec.set_owner(sids[:current_user], path)
# winsec.open_file(path, WindowsSecurityTester::READ_CONTROL | WindowsSecurityTester::WRITE_DAC) do |handle|
# winsec.set_security_info(handle, WindowsSecurityTester::DACL_SECURITY_INFORMATION | WindowsSecurityTester::PROTECTED_DACL_SECURITY_INFORMATION, nil)
# end
# winsec.get_mode(path).to_s(8).should == "777"
# end
end
describe "when the parent directory" do
before :each do
winsec.set_owner(sids[:current_user], parent)
winsec.set_owner(sids[:current_user], path)
winsec.set_mode(0777, path, false)
end
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
it "should allow parent owner" do
winsec.set_owner(sids[:current_user], parent)
winsec.set_group(sids[:guest], parent)
winsec.set_mode(01700, parent)
winsec.set_owner(sids[:current_user], path)
winsec.set_group(sids[:guest], path)
winsec.set_mode(0700, path)
check_delete(path)
end
it "should deny group" do
winsec.set_owner(sids[:guest], parent)
winsec.set_group(sids[:current_user], parent)
winsec.set_mode(01770, parent)
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
winsec.set_owner(sids[:guest], parent)
winsec.set_group(sids[:current_user], parent)
winsec.set_mode(01777, parent)
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
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)
check_delete(path)
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)
winsec.set_owner(sids[:current_user], path)
winsec.set_group(sids[:guest], path)
winsec.set_mode(0700, path)
check_delete(path)
end
it "should allow group" do
winsec.set_owner(sids[:guest], parent)
winsec.set_group(sids[:current_user], parent)
winsec.set_mode(0770, parent)
winsec.set_owner(sids[:guest], path)
winsec.set_group(sids[:current_user], path)
winsec.set_mode(0700, path)
check_delete(path)
end
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
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
describe "is not executable" do
before :each do
winsec.set_group(sids[:current_user], parent)
winsec.set_mode(0666, parent)
end
it_behaves_like "only child owner"
end
end
end
end
end
describe "file" do
let (:parent) do
tmpdir('win_sec_test_file')
end
let (:path) do
path = File.join(parent, 'childfile')
File.new(path, 'w').close
path
end
+ after :each do
+ # allow temp files to be cleaned up
+ grant_everyone_full_access(parent)
+ end
+
it_behaves_like "a securable object" do
def check_access(mode, path)
if (mode & WindowsSecurityTester::S_IRUSR).nonzero?
check_read(path)
else
lambda { check_read(path) }.should raise_error(Errno::EACCES)
end
if (mode & WindowsSecurityTester::S_IWUSR).nonzero?
check_write(path)
else
lambda { check_write(path) }.should raise_error(Errno::EACCES)
end
if (mode & WindowsSecurityTester::S_IXUSR).nonzero?
lambda { check_execute(path) }.should raise_error(Errno::ENOEXEC)
else
lambda { check_execute(path) }.should raise_error(Errno::EACCES)
end
end
def check_read(path)
File.open(path, 'r').close
end
def check_write(path)
File.open(path, 'w').close
end
def check_execute(path)
Kernel.exec(path)
end
def check_delete(path)
File.delete(path)
end
end
describe "locked files" do
let (:explorer) { File.join(Dir::WINDOWS, "explorer.exe") }
it "should get the owner" do
winsec.get_owner(explorer).should match /^S-1-5-/
end
it "should get the group" do
winsec.get_group(explorer).should match /^S-1-5-/
end
it "should get the mode" do
winsec.get_mode(explorer).should == (WindowsSecurityTester::S_IRWXU | WindowsSecurityTester::S_IRWXG | WindowsSecurityTester::S_IEXTRA)
end
end
end
describe "directory" do
let (:parent) do
tmpdir('win_sec_test_dir')
end
let (:path) do
path = File.join(parent, 'childdir')
Dir.mkdir(path)
path
end
+ after :each do
+ # allow temp files to be cleaned up
+ grant_everyone_full_access(parent)
+ end
+
it_behaves_like "a securable object" do
def check_access(mode, path)
if (mode & WindowsSecurityTester::S_IRUSR).nonzero?
check_read(path)
else
lambda { check_read(path) }.should raise_error(Errno::EACCES)
end
if (mode & WindowsSecurityTester::S_IWUSR).nonzero?
check_write(path)
else
lambda { check_write(path) }.should raise_error(Errno::EACCES)
end
if (mode & WindowsSecurityTester::S_IXUSR).nonzero?
check_execute(path)
else
lambda { check_execute(path) }.should raise_error(Errno::EACCES)
end
end
def check_read(path)
Dir.entries(path)
end
def check_write(path)
Dir.mkdir(File.join(path, "subdir"))
end
def check_execute(path)
Dir.chdir(path) {}
end
def check_delete(path)
Dir.rmdir(path)
end
end
describe "inheritable aces" do
it "should be applied to child objects" do
mode640 = WindowsSecurityTester::S_IRUSR | WindowsSecurityTester::S_IWUSR | WindowsSecurityTester::S_IRGRP
winsec.set_mode(mode640, path)
newfile = File.join(path, "newfile.txt")
File.new(newfile, "w").close
newdir = File.join(path, "newdir")
Dir.mkdir(newdir)
[newfile, newdir].each do |p|
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/include.rb b/spec/lib/matchers/include.rb
new file mode 100644
index 000000000..c34725856
--- /dev/null
+++ b/spec/lib/matchers/include.rb
@@ -0,0 +1,27 @@
+module Matchers; module Include
+ extend RSpec::Matchers::DSL
+
+ matcher :include_in_any_order do |*matchers|
+ match do |enumerable|
+ @not_matched = []
+ expected.each do |matcher|
+ if enumerable.empty?
+ break
+ end
+
+ if found = enumerable.find { |elem| matcher.matches?(elem) }
+ enumerable = enumerable.reject { |elem| elem == found }
+ else
+ @not_matched << matcher
+ end
+ end
+
+
+ @not_matched.empty? && enumerable.empty?
+ end
+
+ failure_message_for_should do |enumerable|
+ "did not match #{@not_matched.collect(&:description).join(', ')} in #{enumerable.inspect}: <#{@not_matched.collect(&:failure_message_for_should).join('>, <')}>"
+ end
+ end
+end; end
diff --git a/spec/lib/matchers/include_spec.rb b/spec/lib/matchers/include_spec.rb
new file mode 100644
index 000000000..7a55e90f8
--- /dev/null
+++ b/spec/lib/matchers/include_spec.rb
@@ -0,0 +1,32 @@
+require 'spec_helper'
+require 'matchers/include'
+
+describe "include matchers" do
+ include Matchers::Include
+
+ context :include_in_any_order do
+ it "matches an empty list" do
+ expect([]).to include_in_any_order()
+ end
+
+ it "matches a list with a single element" do
+ expect([1]).to include_in_any_order(eq(1))
+ end
+
+ it "does not match when an expected element is missing" do
+ expect([1]).to_not include_in_any_order(eq(2))
+ end
+
+ it "matches a list with 2 elements in a different order from the expectation" do
+ expect([1, 2]).to include_in_any_order(eq(2), eq(1))
+ end
+
+ it "does not match when there are more than just the expected elements" do
+ expect([1, 2]).to_not include_in_any_order(eq(1))
+ end
+
+ it "matches multiple, equal elements when there are multiple, equal exepectations" do
+ expect([1, 1]).to include_in_any_order(eq(1), eq(1))
+ end
+ end
+end
diff --git a/spec/lib/matchers/json.rb b/spec/lib/matchers/json.rb
index 798e4cd21..dbeebf992 100644
--- a/spec/lib/matchers/json.rb
+++ b/spec/lib/matchers/json.rb
@@ -1,111 +1,167 @@
-RSpec::Matchers.define :set_json_attribute do |*attributes|
- def format
- @format ||= Puppet::Network::FormatHandler.format('pson')
- end
+module JSONMatchers
+ class SetJsonAttribute
+ def initialize(attributes)
+ @attributes = attributes
+ end
- chain :to do |value|
- @value = value
- end
+ def format
+ @format ||= Puppet::Network::FormatHandler.format('pson')
+ end
- def json(instance)
- PSON.parse(instance.to_pson)
- end
+ def json(instance)
+ PSON.parse(instance.to_pson)
+ end
- def attr_value(attrs, instance)
- attrs = attrs.dup
- hash = json(instance)['data']
- while attrs.length > 0
- name = attrs.shift
- hash = hash[name]
+ def attr_value(attrs, instance)
+ attrs = attrs.dup
+ hash = json(instance)['data']
+ while attrs.length > 0
+ name = attrs.shift
+ hash = hash[name]
+ end
+ hash
end
- hash
- end
- match do |instance|
- result = attr_value(attributes, instance)
- if @value
- result == @value
- else
- ! result.nil?
+ def to(value)
+ @value = value
+ self
end
- end
- failure_message_for_should do |instance|
- if @value
- "expected #{instance.inspect} to set #{attributes.inspect} to #{@value.inspect}; got #{attr_value(attributes, instance).inspect}"
- else
- "expected #{instance.inspect} to set #{attributes.inspect} but was nil"
+ def matches?(instance)
+ result = attr_value(@attributes, instance)
+ if @value
+ result == @value
+ else
+ ! result.nil?
+ end
end
- end
- failure_message_for_should_not do |instance|
- if @value
- "expected #{instance.inspect} not to set #{attributes.inspect} to #{@value.inspect}"
- else
- "expected #{instance.inspect} not to set #{attributes.inspect} to nil"
+ def failure_message_for_should(instance)
+ if @value
+ "expected #{instance.inspect} to set #{@attributes.inspect} to #{@value.inspect}; got #{attr_value(@attributes, instance).inspect}"
+ else
+ "expected #{instance.inspect} to set #{@attributes.inspect} but was nil"
+ end
end
- end
-end
-RSpec::Matchers.define :set_json_document_type_to do |type|
- def format
- @format ||= Puppet::Network::FormatHandler.format('pson')
+ def failure_message_for_should_not(instance)
+ if @value
+ "expected #{instance.inspect} not to set #{@attributes.inspect} to #{@value.inspect}"
+ else
+ "expected #{instance.inspect} not to set #{@attributes.inspect} to nil"
+ end
+ end
end
- match do |instance|
- json(instance)['document_type'] == type
- end
+ class SetJsonDocumentTypeTo
+ def initialize(type)
+ @type = type
+ end
- def json(instance)
- PSON.parse(instance.to_pson)
- end
+ def format
+ @format ||= Puppet::Network::FormatHandler.format('pson')
+ end
- failure_message_for_should do |instance|
- "expected #{instance.inspect} to set document_type to #{type.inspect}; got #{json(instance)['document_type'].inspect}"
- end
+ def matches?(instance)
+ json(instance)['document_type'] == @type
+ end
- failure_message_for_should_not do |instance|
- "expected #{instance.inspect} not to set document_type to #{type.inspect}"
- end
-end
+ def json(instance)
+ PSON.parse(instance.to_pson)
+ end
-RSpec::Matchers.define :read_json_attribute do |attribute|
- def format
- @format ||= Puppet::Network::FormatHandler.format('pson')
- end
+ def failure_message_for_should(instance)
+ "expected #{instance.inspect} to set document_type to #{@type.inspect}; got #{json(instance)['document_type'].inspect}"
+ end
- chain :from do |value|
- @json = value
+ def failure_message_for_should_not(instance)
+ "expected #{instance.inspect} not to set document_type to #{@type.inspect}"
+ end
end
- chain :as do |as|
- @value = as
- end
+ class ReadJsonAttribute
+ def initialize(attribute)
+ @attribute = attribute
+ end
- match do |klass|
- raise "Must specify json with 'from'" unless @json
+ def format
+ @format ||= Puppet::Network::FormatHandler.format('pson')
+ end
- @instance = format.intern(klass, @json)
- if @value
- @instance.send(attribute) == @value
- else
- ! @instance.send(attribute).nil?
+ def from(value)
+ @json = value
+ self
+ end
+
+ def as(as)
+ @value = as
+ self
+ end
+
+ def matches?(klass)
+ raise "Must specify json with 'from'" unless @json
+
+ @instance = format.intern(klass, @json)
+ if @value
+ @instance.send(@attribute) == @value
+ else
+ ! @instance.send(@attribute).nil?
+ end
+ end
+
+ def failure_message_for_should(klass)
+ if @value
+ "expected #{klass} to read #{@attribute} from #{@json} as #{@value.inspect}; got #{@instance.send(@attribute).inspect}"
+ else
+ "expected #{klass} to read #{@attribute} from #{@json} but was nil"
+ end
+ end
+
+ def failure_message_for_should_not(klass)
+ if @value
+ "expected #{klass} not to set #{@attribute} to #{@value}"
+ else
+ "expected #{klass} not to set #{@attribute} to nil"
+ end
end
end
- failure_message_for_should do |klass|
- if @value
- "expected #{klass} to read #{attribute} from #{@json} as #{@value.inspect}; got #{@instance.send(attribute).inspect}"
- else
- "expected #{klass} to read #{attribute} from #{@json} but was nil"
+ if !Puppet.features.microsoft_windows?
+ require 'json'
+ require 'json-schema'
+
+ class SchemaMatcher
+ JSON_META_SCHEMA = JSON.parse(File.read('api/schemas/json-meta-schema.json'))
+
+ def initialize(schema)
+ @schema = schema
+ end
+
+ def matches?(json)
+ JSON::Validator.validate!(JSON_META_SCHEMA, @schema)
+ JSON::Validator.validate!(@schema, json)
+ end
end
end
- failure_message_for_should_not do |klass|
- if @value
- "expected #{klass} not to set #{attribute} to #{@value}"
+ def validate_against(schema_file)
+ if Puppet.features.microsoft_windows?
+ pending("Schema checks cannot be done on windows because of json-schema problems")
else
- "expected #{klass} not to set #{attribute} to nil"
+ schema = JSON.parse(File.read(schema_file))
+ SchemaMatcher.new(schema)
end
end
+
+ def set_json_attribute(*attributes)
+ SetJsonAttribute.new(attributes)
+ end
+
+ def set_json_document_type_to(type)
+ SetJsonDocumentTypeTo.new(type)
+ end
+
+ def read_json_attribute(attribute)
+ ReadJsonAttribute.new(attribute)
+ end
end
diff --git a/spec/lib/matchers/match_tokens2.rb b/spec/lib/matchers/match_tokens2.rb
new file mode 100644
index 000000000..c1872e68f
--- /dev/null
+++ b/spec/lib/matchers/match_tokens2.rb
@@ -0,0 +1,74 @@
+# Matches tokens produced by lexer
+# The given exepected is one or more entries where an entry is one of
+# - a token symbol
+# - an Array with a token symbol and the text value
+# - an Array with a token symbol and a Hash specifying all attributes of the token
+# - nil (ignore)
+#
+RSpec::Matchers.define :match_tokens2 do | *expected |
+ match do | actual |
+ expected.zip(actual).all? do |e, a|
+ compare(e, a)
+ end
+ end
+
+ def failure_message_for_should
+ msg = ["Expected (#{expected.size}):"]
+ expected.each {|e| msg << e.to_s }
+
+ zipped = expected.zip(actual)
+ msg << "\nGot (#{actual.size}):"
+ actual.each_with_index do |e, idx|
+ if zipped[idx]
+ zipped_expected = zipped[idx][0]
+ zipped_actual = zipped[idx][1]
+
+ prefix = compare(zipped_expected, zipped_actual) ? ' ' : '*'
+ msg2 = ["#{prefix}[:"]
+ msg2 << e[0].to_s
+ msg2 << ', '
+ if e[1] == false
+ msg2 << 'false'
+ else
+ msg2 << e[1][:value].to_s.dump
+ end
+ # If expectation has options, output them
+ if zipped_expected.is_a?(Array) && zipped_expected[2] && zipped_expected[2].is_a?(Hash)
+ msg2 << ", {"
+ msg3 = []
+ zipped_expected[2].each do |k,v|
+ prefix = e[1][k] != v ? "*" : ''
+ msg3 << "#{prefix}:#{k}=>#{e[1][k]}"
+ end
+ msg2 << msg3.join(", ")
+ msg2 << "}"
+ end
+ msg2 << ']'
+ msg << msg2.join('')
+ end
+ end
+ msg.join("\n")
+ end
+
+ def compare(e, a)
+ # if expected ends before actual
+ return true if !e
+
+ # If actual ends before expected
+ return false if !a
+
+ # Simple - only expect token to match
+ return true if a[0] == e
+
+ # Expect value and optional attributes to match
+ if e.is_a? Array
+ # tokens must match
+ return false unless a[0] == e[0]
+ if e[2].is_a?(Hash)
+ e[2].each {|k,v| return false unless a[1][k] == v }
+ end
+ return (a[1] == e[1] || (a[1][:value] == e[1]))
+ end
+ false
+ end
+end
diff --git a/spec/lib/matchers/resource.rb b/spec/lib/matchers/resource.rb
new file mode 100644
index 000000000..3964a3e9e
--- /dev/null
+++ b/spec/lib/matchers/resource.rb
@@ -0,0 +1,35 @@
+module Matchers; module Resource
+ extend RSpec::Matchers::DSL
+
+ matcher :have_resource do |expected_resource|
+ @params = {}
+
+ match do |actual_catalog|
+ @mismatch = ""
+ if resource = actual_catalog.resource(expected_resource)
+ matched = true
+ failures = []
+ @params.each do |name, value|
+ if resource[name] != value
+ matched = false
+ failures << "expected #{name} to be '#{value}' but was '#{resource[name]}'"
+ end
+ end
+ @mismatch = failures.join("\n")
+
+ matched
+ else
+ @mismatch = "expected #{@actual.to_dot} to include #{@expected[0]}"
+ false
+ end
+ end
+
+ chain :with_parameter do |name, value|
+ @params[name] = value
+ end
+
+ def failure_message_for_should
+ @mismatch
+ end
+ end
+end; end
diff --git a/spec/lib/puppet/indirector/indirector_testing/memory.rb b/spec/lib/puppet/indirector/indirector_testing/memory.rb
new file mode 100644
index 000000000..edfe2b6f7
--- /dev/null
+++ b/spec/lib/puppet/indirector/indirector_testing/memory.rb
@@ -0,0 +1,7 @@
+require 'puppet/indirector/memory'
+
+class Puppet::IndirectorTesting::Memory < Puppet::Indirector::Memory
+ def supports_remote_requests?
+ true
+ end
+end
diff --git a/spec/lib/puppet/indirector/indirector_testing/msgpack.rb b/spec/lib/puppet/indirector/indirector_testing/msgpack.rb
new file mode 100644
index 000000000..c8ce89364
--- /dev/null
+++ b/spec/lib/puppet/indirector/indirector_testing/msgpack.rb
@@ -0,0 +1,6 @@
+require 'puppet/indirector_testing'
+require 'puppet/indirector/msgpack'
+
+class Puppet::IndirectorTesting::Msgpack < Puppet::Indirector::Msgpack
+ desc "Testing the MessagePack indirector"
+end
diff --git a/spec/lib/puppet/indirector_testing.rb b/spec/lib/puppet/indirector_testing.rb
index 883812158..0fd4ef044 100644
--- a/spec/lib/puppet/indirector_testing.rb
+++ b/spec/lib/puppet/indirector_testing.rb
@@ -1,27 +1,37 @@
require 'puppet/indirector'
require 'puppet/util/pson'
class Puppet::IndirectorTesting
extend Puppet::Indirector
indirects :indirector_testing
# We should have some way to identify if we got a valid object back with the
# current values, no?
attr_accessor :value
+ alias_method :name, :value
+ alias_method :name=, :value=
def initialize(value)
self.value = value
end
+ def self.from_raw(raw)
+ new(raw)
+ end
+
PSON.register_document_type('IndirectorTesting',self)
- def self.from_pson(data)
+ def self.from_data_hash(data)
new(data['value'])
end
+ def to_data_hash
+ { 'value' => value }
+ end
+
def to_pson
{
'document_type' => 'IndirectorTesting',
- 'data' => { 'value' => value },
+ 'data' => self.to_data_hash,
'metadata' => { 'api_version' => 1 }
}.to_pson
end
end
diff --git a/spec/lib/puppet_spec/files.rb b/spec/lib/puppet_spec/files.rb
index afe86fba2..65b04aab9 100755
--- a/spec/lib/puppet_spec/files.rb
+++ b/spec/lib/puppet_spec/files.rb
@@ -1,59 +1,60 @@
require 'fileutils'
require 'tempfile'
require 'tmpdir'
require 'pathname'
# A support module for testing files.
module PuppetSpec::Files
def self.cleanup
$global_tempfiles ||= []
while path = $global_tempfiles.pop do
begin
+ Dir.unstub(:entries)
FileUtils.rm_rf path, :secure => true
rescue Errno::ENOENT
# nothing to do
end
end
end
def make_absolute(path) PuppetSpec::Files.make_absolute(path) end
def self.make_absolute(path)
path = File.expand_path(path)
path[0] = 'c' if Puppet.features.microsoft_windows?
path
end
def tmpfile(name, dir = nil) PuppetSpec::Files.tmpfile(name, dir) end
def self.tmpfile(name, dir = nil)
# Generate a temporary file, just for the name...
source = dir ? Tempfile.new(name, dir) : Tempfile.new(name)
path = source.path
source.close!
record_tmp(File.expand_path(path))
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)
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/lib/puppet_spec/matchers.rb b/spec/lib/puppet_spec/matchers.rb
index 67a5f4779..448bd1811 100644
--- a/spec/lib/puppet_spec/matchers.rb
+++ b/spec/lib/puppet_spec/matchers.rb
@@ -1,115 +1,120 @@
require 'stringio'
########################################################################
# Backward compatibility for Jenkins outdated environment.
module RSpec
module Matchers
module BlockAliases
alias_method :to, :should unless method_defined? :to
alias_method :to_not, :should_not unless method_defined? :to_not
alias_method :not_to, :should_not unless method_defined? :not_to
end
end
end
########################################################################
# Custom matchers...
RSpec::Matchers.define :have_matching_element do |expected|
match do |actual|
actual.any? { |item| item =~ expected }
end
end
RSpec::Matchers.define :exit_with do |expected|
actual = nil
match do |block|
begin
block.call
rescue SystemExit => e
actual = e.status
end
actual and actual == expected
end
failure_message_for_should do |block|
"expected exit with code #{expected} but " +
(actual.nil? ? " exit was not called" : "we exited with #{actual} instead")
end
failure_message_for_should_not do |block|
"expected that exit would not be called with #{expected}"
end
description do
"expect exit with #{expected}"
end
end
class HavePrintedMatcher
attr_accessor :expected, :actual
def initialize(expected)
case expected
when String, Regexp
@expected = expected
else
@expected = expected.to_s
end
end
def matches?(block)
begin
$stderr = $stdout = StringIO.new
+ $stdout.set_encoding('UTF-8') if $stdout.respond_to?(:set_encoding)
block.call
$stdout.rewind
@actual = $stdout.read
ensure
$stdout = STDOUT
$stderr = STDERR
end
if @actual then
case @expected
when String
@actual.include? @expected
when Regexp
@expected.match @actual
end
else
false
end
end
def failure_message_for_should
if @actual.nil? then
"expected #{@expected.inspect}, but nothing was printed"
else
"expected #{@expected.inspect} to be printed; got:\n#{@actual}"
end
end
+ def failure_message_for_should_not
+ "expected #{@expected.inspect} to not be printed; got:\n#{@actual}"
+ end
+
def description
"expect #{@expected.inspect} to be printed"
end
end
def have_printed(what)
HavePrintedMatcher.new(what)
end
RSpec::Matchers.define :equal_attributes_of do |expected|
match do |actual|
actual.instance_variables.all? do |attr|
actual.instance_variable_get(attr) == expected.instance_variable_get(attr)
end
end
end
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
diff --git a/spec/lib/puppet_spec/modules.rb b/spec/lib/puppet_spec/modules.rb
index 1b75bb23a..6835e4434 100644
--- a/spec/lib/puppet_spec/modules.rb
+++ b/spec/lib/puppet_spec/modules.rb
@@ -1,26 +1,26 @@
module PuppetSpec::Modules
class << self
def create(name, dir, options = {})
module_dir = File.join(dir, name)
FileUtils.mkdir_p(module_dir)
- environment = Puppet::Node::Environment.new(options[:environment])
+ environment = options[:environment]
if metadata = options[:metadata]
metadata[:source] ||= 'github'
metadata[:author] ||= 'puppetlabs'
metadata[:version] ||= '9.9.9'
metadata[:license] ||= 'to kill'
metadata[:dependencies] ||= []
metadata[:name] = "#{metadata[:author]}/#{name}"
File.open(File.join(module_dir, 'metadata.json'), 'w') do |f|
f.write(metadata.to_pson)
end
end
Puppet::Module.new(name, module_dir, environment)
end
end
end
diff --git a/spec/lib/puppet_spec/scope.rb b/spec/lib/puppet_spec/scope.rb
new file mode 100644
index 000000000..c14ab4755
--- /dev/null
+++ b/spec/lib/puppet_spec/scope.rb
@@ -0,0 +1,14 @@
+
+module PuppetSpec::Scope
+ # Initialize a new scope suitable for testing.
+ #
+ def create_test_scope_for_node(node_name)
+ node = Puppet::Node.new(node_name)
+ compiler = Puppet::Parser::Compiler.new(node)
+ scope = Puppet::Parser::Scope.new(compiler)
+ scope.source = Puppet::Resource::Type.new(:node, node_name)
+ scope.parent = compiler.topscope
+ scope
+ end
+
+end
\ No newline at end of file
diff --git a/spec/shared_behaviours/file_server_terminus.rb b/spec/shared_behaviours/file_server_terminus.rb
index ff122f8ad..25d24682a 100755
--- a/spec/shared_behaviours/file_server_terminus.rb
+++ b/spec/shared_behaviours/file_server_terminus.rb
@@ -1,41 +1,41 @@
#! /usr/bin/env ruby
shared_examples_for "Puppet::Indirector::FileServerTerminus" do
# This only works if the shared behaviour is included before
# the 'before' block in the including context.
before do
Puppet::FileServing::Configuration.instance_variable_set(:@configuration, nil)
- Puppet::FileSystem::File.stubs(:exist?).returns true
- Puppet::FileSystem::File.stubs(:exist?).with(Puppet[:fileserverconfig]).returns(true)
+ Puppet::FileSystem.stubs(:exist?).returns true
+ Puppet::FileSystem.stubs(:exist?).with(Puppet[:fileserverconfig]).returns(true)
@path = Tempfile.new("file_server_testing")
path = @path.path
@path.close!
@path = path
Dir.mkdir(@path)
File.open(File.join(@path, "myfile"), "w") { |f| f.print "my content" }
# Use a real mount, so the integration is a bit deeper.
@mount1 = Puppet::FileServing::Configuration::Mount::File.new("one")
@mount1.path = @path
@parser = stub 'parser', :changed? => false
@parser.stubs(:parse).returns("one" => @mount1)
Puppet::FileServing::Configuration::Parser.stubs(:new).returns(@parser)
# Stub out the modules terminus
@modules = mock 'modules terminus'
@request = Puppet::Indirector::Request.new(:indirection, :method, "puppet://myhost/one/myfile", nil)
end
it "should use the file server configuration to find files" do
@modules.stubs(:find).returns(nil)
@terminus.indirection.stubs(:terminus).with(:modules).returns(@modules)
path = File.join(@path, "myfile")
@terminus.find(@request).should be_instance_of(@test_class)
end
end
diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb
index 49fa482cf..ee7b37d8a 100755
--- a/spec/spec_helper.rb
+++ b/spec/spec_helper.rb
@@ -1,163 +1,193 @@
# NOTE: a lot of the stuff in this file is duplicated in the "puppet_spec_helper" in the project
# puppetlabs_spec_helper. We should probably eat our own dog food and get rid of most of this from here,
# and have the puppet core itself use puppetlabs_spec_helper
dir = File.expand_path(File.dirname(__FILE__))
$LOAD_PATH.unshift File.join(dir, 'lib')
# Don't want puppet getting the command line arguments for rake or autotest
ARGV.clear
begin
require 'rubygems'
rescue LoadError
end
require 'puppet'
gem 'rspec', '>=2.0.0'
require 'rspec/expectations'
# So everyone else doesn't have to include this base constant.
module PuppetSpec
FIXTURE_DIR = File.join(dir = File.expand_path(File.dirname(__FILE__)), "fixtures") unless defined?(FIXTURE_DIR)
end
require 'pathname'
require 'tmpdir'
require 'fileutils'
require 'puppet_spec/verbose'
require 'puppet_spec/files'
require 'puppet_spec/settings'
require 'puppet_spec/fixtures'
require 'puppet_spec/matchers'
require 'puppet_spec/database'
require 'monkey_patches/alias_should_to_must'
require 'puppet/test/test_helper'
Pathname.glob("#{dir}/shared_contexts/*.rb") do |file|
require file.relative_path_from(Pathname.new(dir))
end
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
# Examples or groups can selectively tag themselves as broken.
# For example;
#
# rbv = "#{RUBY_VERSION}-p#{RbConfig::CONFIG['PATCHLEVEL']}"
# describe "mostly working", :broken => false unless rbv == "1.9.3-p327" do
# it "parses a valid IP" do
# IPAddr.new("::2:3:4:5:6:7:8")
# end
# end
- config.filter_run_excluding :broken => true
+ exclude_filters = {:broken => true}
+ exclude_filters[:benchmark] = true unless ENV['BENCHMARK']
+ config.filter_run_excluding exclude_filters
config.mock_with :mocha
tmpdir = Dir.mktmpdir("rspecrun")
oldtmpdir = Dir.tmpdir()
ENV['TMPDIR'] = tmpdir
if Puppet::Util::Platform.windows?
config.output_stream = $stdout
config.error_stream = $stderr
config.formatters.each do |f|
if not f.instance_variable_get(:@output).kind_of?(::File)
f.instance_variable_set(:@output, $stdout)
end
end
end
Puppet::Test::TestHelper.initialize
config.before :all do
Puppet::Test::TestHelper.before_all_tests()
+ if ENV['PROFILE'] == 'all'
+ require 'ruby-prof'
+ RubyProf.start
+ end
end
config.after :all do
+ if ENV['PROFILE'] == 'all'
+ require 'ruby-prof'
+ result = RubyProf.stop
+ printer = RubyProf::CallTreePrinter.new(result)
+ open(File.join(ENV['PROFILEOUT'],"callgrind.all.#{Time.now.to_i}.trace"), "w") do |f|
+ printer.print(f)
+ end
+ end
+
Puppet::Test::TestHelper.after_all_tests()
end
config.before :each do
# Disabling garbage collection inside each test, and only running it at
# the end of each block, gives us an ~ 15 percent speedup, and more on
# some platforms *cough* windows *cough* that are a little slower.
GC.disable
# REVISIT: I think this conceals other bad tests, but I don't have time to
# fully diagnose those right now. When you read this, please come tell me
# I suck for letting this float. --daniel 2011-04-21
Signal.stubs(:trap)
# TODO: in a more sane world, we'd move this logging redirection into our TestHelper class.
# Without doing so, external projects will all have to roll their own solution for
# redirecting logging, and for validating expected log messages. However, because the
# current implementation of this involves creating an instance variable "@logs" on
# EVERY SINGLE TEST CLASS, and because there are over 1300 tests that are written to expect
# this instance variable to be available--we can't easily solve this problem right now.
#
# redirecting logging away from console, because otherwise the test output will be
# obscured by all of the log output
@logs = []
Puppet::Util::Log.newdestination(Puppet::Test::LogCollector.new(@logs))
@log_level = Puppet::Util::Log.level
- Puppet::Test::TestHelper.before_each_test()
+ base = PuppetSpec::Files.tmpdir('tmp_settings')
+ Puppet[:vardir] = File.join(base, 'var')
+ Puppet[:confdir] = File.join(base, 'etc')
+ Puppet[:logdir] = "$vardir/log"
+ Puppet[:rundir] = "$vardir/run"
+ Puppet[:hiera_config] = File.join(base, 'hiera')
+ Puppet::Test::TestHelper.before_each_test()
end
config.after :each do
Puppet::Test::TestHelper.after_each_test()
# TODO: would like to move this into puppetlabs_spec_helper, but there are namespace issues at the moment.
PuppetSpec::Files.cleanup
# TODO: this should be abstracted in the future--see comments above the '@logs' block in the
# "before" code above.
#
# clean up after the logging changes that we made before each test.
@logs.clear
Puppet::Util::Log.close_all
Puppet::Util::Log.level = @log_level
# This will perform a GC between tests, but only if actually required. We
# experimented with forcing a GC run, and that was less efficient than
# just letting it run all the time.
GC.enable
end
config.after :suite do
# Log the spec order to a file, but only if the LOG_SPEC_ORDER environment variable is
# set. This should be enabled on Jenkins runs, as it can be used with Nick L.'s bisect
# script to help identify and debug order-dependent spec failures.
if ENV['LOG_SPEC_ORDER']
File.open("./spec_order.txt", "w") do |logfile|
config.instance_variable_get(:@files_to_run).each { |f| logfile.puts f }
end
end
- # Clean up switch of TMPDIR, don't know if needed after this, so needs to reset it
- # to old before removing it
+
+ # return to original tmpdir
ENV['TMPDIR'] = oldtmpdir
- FileUtils.rm_rf(tmpdir) if Puppet::FileSystem::File.exist?(tmpdir) && tmpdir.to_s.start_with?(oldtmpdir)
+ FileUtils.rm_rf(tmpdir)
+ end
+
+ if ENV['PROFILE']
+ require 'ruby-prof'
+
+ def profile
+ result = RubyProf.profile { yield }
+ name = example.metadata[:full_description].downcase.gsub(/[^a-z0-9_-]/, "-").gsub(/-+/, "-")
+ printer = RubyProf::CallTreePrinter.new(result)
+ open(File.join(ENV['PROFILEOUT'],"callgrind.#{name}.#{Time.now.to_i}.trace"), "w") do |f|
+ printer.print(f)
+ end
+ end
+
+ config.around(:each) do |example|
+ if ENV['PROFILE'] == 'each' or (example.metadata[:profile] and ENV['PROFILE'])
+ profile { example.run }
+ else
+ example.run
+ end
+ end
end
end
diff --git a/spec/unit/agent_spec.rb b/spec/unit/agent_spec.rb
index 38e53176d..68197dee8 100755
--- a/spec/unit/agent_spec.rb
+++ b/spec/unit/agent_spec.rb
@@ -1,320 +1,327 @@
#! /usr/bin/env ruby
require 'spec_helper'
require 'puppet/agent'
class AgentTestClient
def run
# no-op
end
def stop
# no-op
end
end
def without_warnings
flag = $VERBOSE
$VERBOSE = nil
yield
$VERBOSE = flag
end
describe Puppet::Agent do
before do
Puppet::Status.indirection.stubs(:find).returns Puppet::Status.new("version" => Puppet.version)
@agent = Puppet::Agent.new(AgentTestClient, false)
# So we don't actually try to hit the filesystem.
@agent.stubs(:lock).yields
# make Puppet::Application safe for stubbing; restore in an :after block; silence warnings for this.
without_warnings { Puppet::Application = Class.new(Puppet::Application) }
Puppet::Application.stubs(:clear?).returns(true)
Puppet::Application.class_eval do
class << self
def controlled_run(&block)
block.call
end
end
end
end
after do
# restore Puppet::Application from stub-safe subclass, and silence warnings
without_warnings { Puppet::Application = Puppet::Application.superclass }
end
it "should set its client class at initialization" do
Puppet::Agent.new("foo", false).client_class.should == "foo"
end
it "should include the Locker module" do
Puppet::Agent.ancestors.should be_include(Puppet::Agent::Locker)
end
it "should create an instance of its client class and run it when asked to run" do
client = mock 'client'
AgentTestClient.expects(:new).returns client
client.expects(:run)
@agent.stubs(:running?).returns false
@agent.stubs(:disabled?).returns false
@agent.run
end
it "should be considered running if the lock file is locked" do
lockfile = mock 'lockfile'
@agent.expects(:lockfile).returns(lockfile)
lockfile.expects(:locked?).returns true
@agent.should be_running
end
describe "when being run" do
before do
AgentTestClient.stubs(:lockfile_path).returns "/my/lock"
@agent.stubs(:running?).returns false
@agent.stubs(:disabled?).returns false
end
it "should splay" do
@agent.expects(:splay)
@agent.run
end
it "should do nothing if already running" do
@agent.expects(:running?).returns true
AgentTestClient.expects(:new).never
@agent.run
end
it "should do nothing if disabled" do
@agent.expects(:disabled?).returns(true)
AgentTestClient.expects(:new).never
@agent.run
end
it "(#11057) should notify the user about why a run is skipped" do
Puppet::Application.stubs(:controlled_run).returns(false)
Puppet::Application.stubs(:run_status).returns('MOCK_RUN_STATUS')
# This is the actual test that we inform the user why the run is skipped.
# We assume this information is contained in
# Puppet::Application.run_status
Puppet.expects(:notice).with(regexp_matches(/MOCK_RUN_STATUS/))
@agent.run
end
it "should display an informative message if the agent is administratively disabled" do
@agent.expects(:disabled?).returns true
@agent.expects(:disable_message).returns "foo"
Puppet.expects(:notice).with(regexp_matches(/Skipping run of .*; administratively disabled.*\(Reason: 'foo'\)/))
@agent.run
end
it "should use Puppet::Application.controlled_run to manage process state behavior" do
calls = sequence('calls')
Puppet::Application.expects(:controlled_run).yields.in_sequence(calls)
AgentTestClient.expects(:new).once.in_sequence(calls)
@agent.run
end
it "should not fail if a client class instance cannot be created" do
AgentTestClient.expects(:new).raises "eh"
Puppet.expects(:err)
@agent.run
end
it "should not fail if there is an exception while running its client" do
client = AgentTestClient.new
AgentTestClient.expects(:new).returns client
client.expects(:run).raises "eh"
Puppet.expects(:err)
@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
@agent.expects(:lock)
client.expects(:run).never # if it doesn't run, then we know our yield is what triggers it
@agent.run
end
it "should make its client instance available while running" do
client = AgentTestClient.new
AgentTestClient.expects(:new).returns client
client.expects(:run).with { @agent.client.should equal(client); true }
@agent.run
end
it "should run the client instance with any arguments passed to it" do
client = AgentTestClient.new
AgentTestClient.expects(:new).returns client
client.expects(:run).with(:pluginsync => true, :other => :options)
@agent.run(:other => :options)
end
it "should return the agent result" do
client = AgentTestClient.new
AgentTestClient.expects(:new).returns client
@agent.expects(:lock).returns(:result)
@agent.run.should == :result
end
- describe "when should_fork is true" do
+ describe "when should_fork is true", :as_platform => :posix do
before do
@agent = Puppet::Agent.new(AgentTestClient, true)
# So we don't actually try to hit the filesystem.
@agent.stubs(:lock).yields
Kernel.stubs(:fork)
Process.stubs(:waitpid2).returns [123, (stub 'process::status', :exitstatus => 0)]
@agent.stubs(:exit)
end
it "should run the agent in a forked process" do
client = AgentTestClient.new
AgentTestClient.expects(:new).returns client
client.expects(:run)
Kernel.expects(:fork).yields
@agent.run
end
it "should exit child process if child exit" do
client = AgentTestClient.new
AgentTestClient.expects(:new).returns client
client.expects(:run).raises(SystemExit)
Kernel.expects(:fork).yields
@agent.expects(:exit).with(-1)
@agent.run
end
it "should re-raise exit happening in the child" do
Process.stubs(:waitpid2).returns [123, (stub 'process::status', :exitstatus => -1)]
lambda { @agent.run }.should raise_error(SystemExit)
end
it "should re-raise NoMoreMemory happening in the child" do
Process.stubs(:waitpid2).returns [123, (stub 'process::status', :exitstatus => -2)]
lambda { @agent.run }.should raise_error(NoMemoryError)
end
it "should return the child exit code" do
Process.stubs(:waitpid2).returns [123, (stub 'process::status', :exitstatus => 777)]
@agent.run.should == 777
end
it "should return the block exit code as the child exit code" do
Kernel.expects(:fork).yields
@agent.expects(:exit).with(777)
@agent.run_in_fork {
777
}
end
end
+
+ describe "on Windows", :as_platform => :windows do
+ it "should never fork" do
+ agent = Puppet::Agent.new(AgentTestClient, true)
+ expect(agent.should_fork).to be_false
+ end
+ end
end
describe "when splaying" do
before do
Puppet[:splay] = true
Puppet[:splaylimit] = "10"
end
it "should do nothing if splay is disabled" do
Puppet[:splay] = false
@agent.expects(:sleep).never
@agent.splay
end
it "should do nothing if it has already splayed" do
@agent.expects(:splayed?).returns true
@agent.expects(:sleep).never
@agent.splay
end
it "should log that it is splaying" do
@agent.stubs :sleep
Puppet.expects :info
@agent.splay
end
it "should sleep for a random portion of the splaylimit plus 1" do
Puppet[:splaylimit] = "50"
@agent.expects(:rand).with(51).returns 10
@agent.expects(:sleep).with(10)
@agent.splay
end
it "should mark that it has splayed" do
@agent.stubs(:sleep)
@agent.splay
@agent.should be_splayed
end
end
describe "when checking execution state" do
describe 'with regular run status' do
before :each do
Puppet::Application.stubs(:restart_requested?).returns(false)
Puppet::Application.stubs(:stop_requested?).returns(false)
Puppet::Application.stubs(:interrupted?).returns(false)
Puppet::Application.stubs(:clear?).returns(true)
end
it 'should be false for :stopping?' do
@agent.stopping?.should be_false
end
it 'should be false for :needing_restart?' do
@agent.needing_restart?.should be_false
end
end
describe 'with a stop requested' do
before :each do
Puppet::Application.stubs(:clear?).returns(false)
Puppet::Application.stubs(:restart_requested?).returns(false)
Puppet::Application.stubs(:stop_requested?).returns(true)
Puppet::Application.stubs(:interrupted?).returns(true)
end
it 'should be true for :stopping?' do
@agent.stopping?.should be_true
end
it 'should be false for :needing_restart?' do
@agent.needing_restart?.should be_false
end
end
describe 'with a restart requested' do
before :each do
Puppet::Application.stubs(:clear?).returns(false)
Puppet::Application.stubs(:restart_requested?).returns(true)
Puppet::Application.stubs(:stop_requested?).returns(false)
Puppet::Application.stubs(:interrupted?).returns(true)
end
it 'should be false for :stopping?' do
@agent.stopping?.should be_false
end
it 'should be true for :needing_restart?' do
@agent.needing_restart?.should be_true
end
end
end
end
diff --git a/spec/unit/application/agent_spec.rb b/spec/unit/application/agent_spec.rb
index 4280a953c..c6fbba48f 100755
--- a/spec/unit/application/agent_spec.rb
+++ b/spec/unit/application/agent_spec.rb
@@ -1,655 +1,656 @@
#! /usr/bin/env ruby
require 'spec_helper'
require 'puppet/agent'
require 'puppet/application/agent'
require 'puppet/network/server'
require 'puppet/daemon'
describe Puppet::Application::Agent do
include PuppetSpec::Files
before :each do
@puppetd = Puppet::Application[:agent]
@daemon = Puppet::Daemon.new(nil)
@daemon.stubs(:daemonize)
@daemon.stubs(:start)
@daemon.stubs(:stop)
Puppet::Daemon.stubs(:new).returns(@daemon)
Puppet[:daemonize] = false
@agent = stub_everything 'agent'
Puppet::Agent.stubs(:new).returns(@agent)
@puppetd.preinit
Puppet::Util::Log.stubs(:newdestination)
@ssl_host = stub_everything 'ssl host'
Puppet::SSL::Host.stubs(:new).returns(@ssl_host)
Puppet::Node.indirection.stubs(:terminus_class=)
Puppet::Node.indirection.stubs(:cache_class=)
Puppet::Node::Facts.indirection.stubs(:terminus_class=)
$stderr.expects(:puts).never
+
+ Puppet.settings.stubs(:use)
end
it "should operate in agent run_mode" do
@puppetd.class.run_mode.name.should == :agent
end
it "should declare a main command" do
@puppetd.should respond_to(:main)
end
it "should declare a onetime command" do
@puppetd.should respond_to(:onetime)
end
it "should declare a fingerprint command" do
@puppetd.should respond_to(:fingerprint)
end
it "should declare a preinit block" do
@puppetd.should respond_to(:preinit)
end
describe "in preinit" do
it "should catch INT" do
Signal.expects(:trap).with { |arg,block| arg == :INT }
@puppetd.preinit
end
it "should init client to true" do
@puppetd.preinit
@puppetd.options[:client].should be_true
end
it "should init fqdn to nil" do
@puppetd.preinit
@puppetd.options[:fqdn].should be_nil
end
it "should init serve to []" do
@puppetd.preinit
@puppetd.options[:serve].should == []
end
it "should use SHA256 as default digest algorithm" do
@puppetd.preinit
@puppetd.options[:digest].should == 'SHA256'
end
it "should not fingerprint by default" do
@puppetd.preinit
@puppetd.options[:fingerprint].should be_false
end
it "should init waitforcert to nil" do
@puppetd.preinit
@puppetd.options[:waitforcert].should be_nil
end
end
describe "when handling options" do
before do
@puppetd.command_line.stubs(:args).returns([])
end
[:enable, :debug, :fqdn, :test, :verbose, :digest].each do |option|
it "should declare handle_#{option} method" do
@puppetd.should respond_to("handle_#{option}".to_sym)
end
it "should store argument value when calling handle_#{option}" do
@puppetd.send("handle_#{option}".to_sym, 'arg')
@puppetd.options[option].should == 'arg'
end
end
describe "when handling --disable" do
it "should set disable to true" do
@puppetd.handle_disable('')
@puppetd.options[:disable].should == true
end
it "should store disable message" do
@puppetd.handle_disable('message')
@puppetd.options[:disable_message].should == 'message'
end
end
it "should set client to false with --no-client" do
@puppetd.handle_no_client(nil)
@puppetd.options[:client].should be_false
end
it "should set waitforcert to 0 with --onetime and if --waitforcert wasn't given" do
@agent.stubs(:run).returns(2)
Puppet[:onetime] = true
@ssl_host.expects(:wait_for_cert).with(0)
expect { execute_agent }.to exit_with 0
end
it "should use supplied waitforcert when --onetime is specified" do
@agent.stubs(:run).returns(2)
Puppet[:onetime] = true
@puppetd.handle_waitforcert(60)
@ssl_host.expects(:wait_for_cert).with(60)
expect { execute_agent }.to exit_with 0
end
it "should use a default value for waitforcert when --onetime and --waitforcert are not specified" do
@ssl_host.expects(:wait_for_cert).with(120)
execute_agent
end
it "should use the waitforcert setting when checking for a signed certificate" do
Puppet[:waitforcert] = 10
@ssl_host.expects(:wait_for_cert).with(10)
execute_agent
end
it "should set the log destination with --logdest" do
Puppet::Log.expects(:newdestination).with("console")
@puppetd.handle_logdest("console")
end
it "should put the setdest options to true" do
@puppetd.handle_logdest("console")
@puppetd.options[:setdest].should == true
end
it "should parse the log destination from the command line" do
@puppetd.command_line.stubs(:args).returns(%w{--logdest /my/file})
Puppet::Util::Log.expects(:newdestination).with("/my/file")
@puppetd.parse_options
end
it "should store the waitforcert options with --waitforcert" do
@puppetd.handle_waitforcert("42")
@puppetd.options[:waitforcert].should == 42
end
end
describe "during setup" do
before :each do
Puppet.stubs(:info)
- 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=)
Puppet::Resource::Catalog.indirection.stubs(:terminus_class=)
Puppet::Resource::Catalog.indirection.stubs(:cache_class=)
Puppet::Node::Facts.indirection.stubs(:terminus_class=)
Puppet.stubs(:settraps)
end
describe "with --test" do
it "should call setup_test" do
@puppetd.options[:test] = true
@puppetd.expects(:setup_test)
@puppetd.setup
end
it "should set options[:verbose] to true" do
@puppetd.setup_test
@puppetd.options[:verbose].should == true
end
it "should set options[:onetime] to true" do
Puppet[:onetime] = false
@puppetd.setup_test
Puppet[:onetime].should == true
end
it "should set options[:detailed_exitcodes] to true" do
@puppetd.setup_test
@puppetd.options[:detailed_exitcodes].should == true
end
end
it "should call setup_logs" do
@puppetd.expects(:setup_logs)
@puppetd.setup
end
describe "when setting up logs" do
before :each do
Puppet::Util::Log.stubs(:newdestination)
end
it "should set log level to debug if --debug was passed" do
@puppetd.options[:debug] = true
@puppetd.setup_logs
Puppet::Util::Log.level.should == :debug
end
it "should set log level to info if --verbose was passed" do
@puppetd.options[:verbose] = true
@puppetd.setup_logs
Puppet::Util::Log.level.should == :info
end
[:verbose, :debug].each do |level|
it "should set console as the log destination with level #{level}" do
@puppetd.options[level] = true
Puppet::Util::Log.expects(:newdestination).at_least_once
Puppet::Util::Log.expects(:newdestination).with(:console).once
@puppetd.setup_logs
end
end
it "should set a default log destination if no --logdest" do
@puppetd.options[:setdest] = false
Puppet::Util::Log.expects(:setup_default)
@puppetd.setup_logs
end
end
it "should print puppet config if asked to in Puppet config" do
Puppet[:configprint] = "pluginsync"
Puppet.settings.expects(:print_configs).returns true
expect { execute_agent }.to exit_with 0
end
it "should exit after printing puppet config if asked to in Puppet config" do
path = make_absolute('/my/path')
Puppet[:modulepath] = path
Puppet[:configprint] = "modulepath"
Puppet::Settings.any_instance.expects(:puts).with(path)
expect { execute_agent }.to exit_with 0
end
it "should use :main, :puppetd, and :ssl" do
+ Puppet.settings.unstub(:use)
Puppet.settings.expects(:use).with(:main, :agent, :ssl)
@puppetd.setup
end
it "should install a remote ca location" do
Puppet::SSL::Host.expects(:ca_location=).with(:remote)
@puppetd.setup
end
it "should install a none ca location in fingerprint mode" do
@puppetd.options[:fingerprint] = true
Puppet::SSL::Host.expects(:ca_location=).with(:none)
@puppetd.setup
end
it "should tell the report handler to use REST" do
Puppet::Transaction::Report.indirection.expects(:terminus_class=).with(:rest)
@puppetd.setup
end
it "should tell the report handler to cache locally as yaml" do
Puppet::Transaction::Report.indirection.expects(:cache_class=).with(:yaml)
@puppetd.setup
end
it "should default catalog_terminus setting to 'rest'" do
@puppetd.initialize_app_defaults
Puppet[:catalog_terminus].should == :rest
end
it "should default node_terminus setting to 'rest'" do
@puppetd.initialize_app_defaults
Puppet[:node_terminus].should == :rest
end
it "has an application default :catalog_cache_terminus setting of 'json'" do
Puppet::Resource::Catalog.indirection.expects(:cache_class=).with(:json)
@puppetd.initialize_app_defaults
@puppetd.setup
end
it "should tell the catalog cache class based on the :catalog_cache_terminus setting" do
Puppet[:catalog_cache_terminus] = "yaml"
Puppet::Resource::Catalog.indirection.expects(:cache_class=).with(:yaml)
@puppetd.initialize_app_defaults
@puppetd.setup
end
it "should not set catalog cache class if :catalog_cache_terminus is explicitly nil" do
Puppet[:catalog_cache_terminus] = nil
Puppet::Resource::Catalog.indirection.expects(:cache_class=).never
@puppetd.initialize_app_defaults
@puppetd.setup
end
it "should default facts_terminus setting to 'facter'" do
@puppetd.initialize_app_defaults
Puppet[:facts_terminus].should == :facter
end
it "should create an agent" do
Puppet::Agent.stubs(:new).with(Puppet::Configurer)
@puppetd.setup
end
[:enable, :disable].each do |action|
it "should delegate to enable_disable_client if we #{action} the agent" do
@puppetd.options[action] = true
@puppetd.expects(:enable_disable_client).with(@agent)
@puppetd.setup
end
end
describe "when enabling or disabling agent" do
[:enable, :disable].each do |action|
it "should call client.#{action}" do
@puppetd.options[action] = true
@agent.expects(action)
expect { execute_agent }.to exit_with 0
end
end
it "should pass the disable message when disabling" do
@puppetd.options[:disable] = true
@puppetd.options[:disable_message] = "message"
@agent.expects(:disable).with("message")
expect { execute_agent }.to exit_with 0
end
it "should pass the default disable message when disabling without a message" do
@puppetd.options[:disable] = true
@puppetd.options[:disable_message] = nil
@agent.expects(:disable).with("reason not specified")
expect { execute_agent }.to exit_with 0
end
end
it "should inform the daemon about our agent if :client is set to 'true'" do
@puppetd.options[:client] = true
execute_agent
@daemon.agent.should == @agent
end
it "should not inform the daemon about our agent if :client is set to 'false'" do
@puppetd.options[:client] = false
execute_agent
@daemon.agent.should be_nil
end
it "should daemonize if needed" do
Puppet.features.stubs(:microsoft_windows?).returns false
Puppet[:daemonize] = true
@daemon.expects(:daemonize)
execute_agent
end
it "should wait for a certificate" do
@puppetd.options[:waitforcert] = 123
@ssl_host.expects(:wait_for_cert).with(123)
execute_agent
end
it "should not wait for a certificate in fingerprint mode" do
@puppetd.options[:fingerprint] = true
@puppetd.options[:waitforcert] = 123
@puppetd.options[:digest] = 'MD5'
certificate = mock 'certificate'
certificate.stubs(:digest).with('MD5').returns('ABCDE')
@ssl_host.stubs(:certificate).returns(certificate)
@ssl_host.expects(:wait_for_cert).never
@puppetd.expects(:puts).with('ABCDE')
execute_agent
end
describe "when setting up listen" do
before :each do
- Puppet::FileSystem::File.stubs(:exist?).with('auth').returns(true)
- Puppet::FileSystem::File.stubs(:exist?).returns(true)
+ Puppet::FileSystem.stubs(:exist?).with(Puppet[:rest_authconfig]).returns(true)
@puppetd.options[:serve] = []
@server = stub_everything 'server'
Puppet::Network::Server.stubs(:new).returns(@server)
end
it "should exit if no authorization file" do
Puppet[:listen] = true
Puppet.stubs(:err)
- Puppet::FileSystem::File.stubs(:exist?).with(Puppet[:rest_authconfig]).returns(false)
+ Puppet::FileSystem.stubs(:exist?).with(Puppet[:rest_authconfig]).returns(false)
expect do
execute_agent
end.to exit_with 14
end
it "should use puppet default port" do
Puppet[:puppetport] = 32768
Puppet[:listen] = true
Puppet::Network::Server.expects(:new).with(anything, 32768)
execute_agent
end
it "should issue a warning that listen is deprecated" do
Puppet[:listen] = true
Puppet.expects(:warning).with() { |msg| msg =~ /kick is deprecated/ }
execute_agent
end
end
describe "when setting up for fingerprint" do
before(:each) do
@puppetd.options[:fingerprint] = true
end
it "should not setup as an agent" do
@puppetd.expects(:setup_agent).never
@puppetd.setup
end
it "should not create an agent" do
Puppet::Agent.stubs(:new).with(Puppet::Configurer).never
@puppetd.setup
end
it "should not daemonize" do
@daemon.expects(:daemonize).never
@puppetd.setup
end
end
describe "when configuring agent for catalog run" do
it "should set should_fork as true when running normally" do
Puppet::Agent.expects(:new).with(anything, true)
@puppetd.setup
end
it "should not set should_fork as false for --onetime" do
Puppet[:onetime] = true
Puppet::Agent.expects(:new).with(anything, false)
@puppetd.setup
end
end
end
describe "when running" do
before :each do
@puppetd.options[:fingerprint] = false
end
it "should dispatch to fingerprint if --fingerprint is used" do
@puppetd.options[:fingerprint] = true
@puppetd.stubs(:fingerprint)
execute_agent
end
it "should dispatch to onetime if --onetime is used" do
@puppetd.options[:onetime] = true
@puppetd.stubs(:onetime)
execute_agent
end
it "should dispatch to main if --onetime and --fingerprint are not used" do
@puppetd.options[:onetime] = false
@puppetd.stubs(:main)
execute_agent
end
describe "with --onetime" do
before :each do
@agent.stubs(:run).returns(:report)
Puppet[:onetime] = true
@puppetd.options[:client] = :client
@puppetd.options[:detailed_exitcodes] = false
Puppet.stubs(:newservice)
end
it "should exit if no defined --client" do
@puppetd.options[:client] = nil
Puppet.expects(:err).with('onetime is specified but there is no client')
expect { execute_agent }.to exit_with 43
end
it "should setup traps" do
@daemon.expects(:set_signal_traps)
expect { execute_agent }.to exit_with 0
end
it "should let the agent run" do
@agent.expects(:run).returns(:report)
expect { execute_agent }.to exit_with 0
end
it "should stop the daemon" do
@daemon.expects(:stop).with(:exit => false)
expect { execute_agent }.to exit_with 0
end
describe "and --detailed-exitcodes" do
before :each do
@puppetd.options[:detailed_exitcodes] = true
end
it "should exit with agent computed exit status" do
Puppet[:noop] = false
@agent.stubs(:run).returns(666)
expect { execute_agent }.to exit_with 666
end
it "should exit with the agent's exit status, even if --noop is set." do
Puppet[:noop] = true
@agent.stubs(:run).returns(666)
expect { execute_agent }.to exit_with 666
end
end
end
describe "with --fingerprint" do
before :each do
@cert = mock 'cert'
@puppetd.options[:fingerprint] = true
@puppetd.options[:digest] = :MD5
end
it "should fingerprint the certificate if it exists" do
@ssl_host.stubs(:certificate).returns(@cert)
@cert.stubs(:digest).with('MD5').returns "fingerprint"
@puppetd.expects(:puts).with "fingerprint"
@puppetd.fingerprint
end
it "should fingerprint the certificate request if no certificate have been signed" do
@ssl_host.stubs(:certificate).returns(nil)
@ssl_host.stubs(:certificate_request).returns(@cert)
@cert.stubs(:digest).with('MD5').returns "fingerprint"
@puppetd.expects(:puts).with "fingerprint"
@puppetd.fingerprint
end
end
describe "without --onetime and --fingerprint" do
before :each do
Puppet.stubs(:notice)
@puppetd.options[:client] = nil
end
it "should start our daemon" do
@daemon.expects(:start)
execute_agent
end
end
end
def execute_agent
@puppetd.setup
@puppetd.run_command
end
end
diff --git a/spec/unit/application/apply_spec.rb b/spec/unit/application/apply_spec.rb
index d3cc54884..69cd284b4 100755
--- a/spec/unit/application/apply_spec.rb
+++ b/spec/unit/application/apply_spec.rb
@@ -1,436 +1,455 @@
#! /usr/bin/env ruby
require 'spec_helper'
require 'puppet/application/apply'
require 'puppet/file_bucket/dipper'
require 'puppet/configurer'
require 'fileutils'
describe Puppet::Application::Apply do
before :each do
@apply = Puppet::Application[:apply]
Puppet::Util::Log.stubs(:newdestination)
Puppet[:reports] = "none"
end
after :each do
Puppet::Node::Facts.indirection.reset_terminus_class
Puppet::Node::Facts.indirection.cache_class = nil
Puppet::Node.indirection.reset_terminus_class
Puppet::Node.indirection.cache_class = nil
end
- [:debug,:loadclasses,:verbose,:use_nodes,:detailed_exitcodes,:catalog, :write_catalog_summary].each do |option|
+ [:debug,:loadclasses,:test,:verbose,:use_nodes,:detailed_exitcodes,:catalog, :write_catalog_summary].each do |option|
it "should declare handle_#{option} method" do
@apply.should respond_to("handle_#{option}".to_sym)
end
it "should store argument value when calling handle_#{option}" do
@apply.options.expects(:[]=).with(option, 'arg')
@apply.send("handle_#{option}".to_sym, 'arg')
end
end
it "should set the code to the provided code when :execute is used" do
@apply.options.expects(:[]=).with(:code, 'arg')
@apply.send("handle_execute".to_sym, 'arg')
end
describe "when applying options" do
it "should set the log destination with --logdest" do
Puppet::Log.expects(:newdestination).with("console")
@apply.handle_logdest("console")
end
it "should set the setdest options to true" do
@apply.options.expects(:[]=).with(:setdest,true)
@apply.handle_logdest("console")
end
end
describe "during setup" do
before :each do
Puppet::Log.stubs(:newdestination)
Puppet::FileBucket::Dipper.stubs(:new)
STDIN.stubs(:read)
Puppet::Transaction::Report.indirection.stubs(:cache_class=)
+ end
+
+ describe "with --test" do
+ it "should call setup_test" do
+ @apply.options[:test] = true
+ @apply.expects(:setup_test)
+
+ @apply.setup
+ end
+
+ it "should set options[:verbose] to true" do
+ @apply.setup_test
- @apply.options.stubs(:[]).with(any_parameters)
+ @apply.options[:verbose].should == true
+ end
+ it "should set options[:show_diff] to true" do
+ Puppet.settings.override_default(:show_diff, false)
+ @apply.setup_test
+ Puppet[:show_diff].should == true
+ end
+ it "should set options[:detailed_exitcodes] to true" do
+ @apply.setup_test
+
+ @apply.options[:detailed_exitcodes].should == true
+ end
end
it "should set console as the log destination if logdest option wasn't provided" do
Puppet::Log.expects(:newdestination).with(:console)
@apply.setup
end
it "should set INT trap" do
Signal.expects(:trap).with(:INT)
@apply.setup
end
it "should set log level to debug if --debug was passed" do
- @apply.options.stubs(:[]).with(:debug).returns(true)
+ @apply.options[:debug] = true
@apply.setup
Puppet::Log.level.should == :debug
end
it "should set log level to info if --verbose was passed" do
- @apply.options.stubs(:[]).with(:verbose).returns(true)
+ @apply.options[:verbose] = true
@apply.setup
Puppet::Log.level.should == :info
end
it "should print puppet config if asked to in Puppet config" do
Puppet.settings.stubs(:print_configs?).returns true
Puppet.settings.expects(:print_configs).returns true
expect { @apply.setup }.to exit_with 0
end
it "should exit after printing puppet config if asked to in Puppet config" do
Puppet.settings.stubs(:print_configs?).returns(true)
expect { @apply.setup }.to exit_with 1
end
it "should tell the report handler to cache locally as yaml" do
Puppet::Transaction::Report.indirection.expects(:cache_class=).with(:yaml)
@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
end
describe "when executing" do
it "should dispatch to 'apply' if it was called with 'apply'" do
@apply.options[:catalog] = "foo"
@apply.expects(:apply)
@apply.run_command
end
it "should dispatch to main otherwise" do
@apply.stubs(:options).returns({})
@apply.expects(:main)
@apply.run_command
end
describe "the main command" do
include PuppetSpec::Files
before :each do
Puppet[:prerun_command] = ''
Puppet[:postrun_command] = ''
Puppet::Node::Facts.indirection.terminus_class = :memory
Puppet::Node::Facts.indirection.cache_class = :memory
Puppet::Node.indirection.terminus_class = :memory
Puppet::Node.indirection.cache_class = :memory
@facts = Puppet::Node::Facts.new(Puppet[:node_name_value])
Puppet::Node::Facts.indirection.save(@facts)
@node = Puppet::Node.new(Puppet[:node_name_value])
Puppet::Node.indirection.save(@node)
@catalog = Puppet::Resource::Catalog.new
@catalog.stubs(:to_ral).returns(@catalog)
Puppet::Resource::Catalog.indirection.stubs(:find).returns(@catalog)
STDIN.stubs(:read)
@transaction = stub('transaction')
@catalog.stubs(:apply).returns(@transaction)
Puppet::Util::Storage.stubs(:load)
Puppet::Configurer.any_instance.stubs(:save_last_run_summary) # to prevent it from trying to write files
end
after :each do
Puppet::Node::Facts.indirection.reset_terminus_class
Puppet::Node::Facts.indirection.cache_class = nil
end
+ around :each do |example|
+ Puppet.override(:current_environment =>
+ Puppet::Node::Environment.create(:production, [], '')) do
+ example.run
+ end
+ end
+
it "should set the code to run from --code" do
@apply.options[:code] = "code to run"
Puppet.expects(:[]=).with(:code,"code to run")
expect { @apply.main }.to exit_with 0
end
it "should set the code to run from STDIN if no arguments" do
@apply.command_line.stubs(:args).returns([])
STDIN.stubs(:read).returns("code to run")
Puppet.expects(:[]=).with(:code,"code to run")
expect { @apply.main }.to exit_with 0
end
- it "should set the manifest if a file is passed on command line and the file exists" do
- manifest = tmpfile('site.pp')
- FileUtils.touch(manifest)
- @apply.command_line.stubs(:args).returns([manifest])
-
- Puppet.expects(:[]=).with(:manifest,manifest)
-
- expect { @apply.main }.to exit_with 0
- end
-
it "should raise an error if a file is passed on command line and the file does not exist" do
noexist = tmpfile('noexist.pp')
@apply.command_line.stubs(:args).returns([noexist])
lambda { @apply.main }.should raise_error(RuntimeError, "Could not find file #{noexist}")
end
it "should set the manifest to the first file and warn other files will be skipped" do
manifest = tmpfile('starwarsIV')
FileUtils.touch(manifest)
@apply.command_line.stubs(:args).returns([manifest, 'starwarsI', 'starwarsII'])
- Puppet.expects(:[]=).with(:manifest,manifest)
expect { @apply.main }.to exit_with 0
msg = @logs.find {|m| m.message =~ /Only one file can be applied per run/ }
msg.message.should == 'Only one file can be applied per run. Skipping starwarsI, starwarsII'
msg.level.should == :warning
end
it "should raise an error if we can't find the node" do
Puppet::Node.indirection.expects(:find).returns(nil)
lambda { @apply.main }.should raise_error(RuntimeError, /Could not find node/)
end
it "should load custom classes if loadclasses" do
@apply.options[:loadclasses] = true
classfile = tmpfile('classfile')
File.open(classfile, 'w') { |c| c.puts 'class' }
Puppet[:classfile] = classfile
@node.expects(:classes=).with(['class'])
expect { @apply.main }.to exit_with 0
end
it "should compile the catalog" do
Puppet::Resource::Catalog.indirection.expects(:find).returns(@catalog)
expect { @apply.main }.to exit_with 0
end
it "should transform the catalog to ral" do
@catalog.expects(:to_ral).returns(@catalog)
expect { @apply.main }.to exit_with 0
end
it "should finalize the catalog" do
@catalog.expects(:finalize)
expect { @apply.main }.to exit_with 0
end
it "should not save the classes or resource file by default" do
@catalog.expects(:write_class_file).never
@catalog.expects(:write_resource_file).never
expect { @apply.main }.to exit_with 0
end
it "should save the classes and resources files when requested" do
@apply.options[:write_catalog_summary] = true
@catalog.expects(:write_class_file).once
@catalog.expects(:write_resource_file).once
expect { @apply.main }.to exit_with 0
end
it "should call the prerun and postrun commands on a Configurer instance" do
Puppet::Configurer.any_instance.expects(:execute_prerun_command).returns(true)
Puppet::Configurer.any_instance.expects(:execute_postrun_command).returns(true)
expect { @apply.main }.to exit_with 0
end
it "should apply the catalog" do
@catalog.expects(:apply).returns(stub_everything('transaction'))
expect { @apply.main }.to exit_with 0
end
it "should save the last run summary" do
Puppet[:noop] = false
report = Puppet::Transaction::Report.new("apply")
Puppet::Transaction::Report.stubs(:new).returns(report)
Puppet::Configurer.any_instance.expects(:save_last_run_summary).with(report)
expect { @apply.main }.to exit_with 0
end
describe "when using node_name_fact" do
before :each do
@facts = Puppet::Node::Facts.new(Puppet[:node_name_value], 'my_name_fact' => 'other_node_name')
Puppet::Node::Facts.indirection.save(@facts)
@node = Puppet::Node.new('other_node_name')
Puppet::Node.indirection.save(@node)
Puppet[:node_name_fact] = 'my_name_fact'
end
it "should set the facts name based on the node_name_fact" do
expect { @apply.main }.to exit_with 0
@facts.name.should == 'other_node_name'
end
it "should set the node_name_value based on the node_name_fact" do
expect { @apply.main }.to exit_with 0
Puppet[:node_name_value].should == 'other_node_name'
end
it "should merge in our node the loaded facts" do
@facts.values.merge!('key' => 'value')
expect { @apply.main }.to exit_with 0
@node.parameters['key'].should == 'value'
end
it "should raise an error if we can't find the facts" do
Puppet::Node::Facts.indirection.expects(:find).returns(nil)
lambda { @apply.main }.should raise_error
end
end
describe "with detailed_exitcodes" do
before :each do
@apply.options[:detailed_exitcodes] = true
end
it "should exit with report's computed exit status" do
Puppet[:noop] = false
Puppet::Transaction::Report.any_instance.stubs(:exit_status).returns(666)
expect { @apply.main }.to exit_with 666
end
it "should exit with report's computed exit status, even if --noop is set" do
Puppet[:noop] = true
Puppet::Transaction::Report.any_instance.stubs(:exit_status).returns(666)
expect { @apply.main }.to exit_with 666
end
it "should always exit with 0 if option is disabled" do
Puppet[:noop] = false
report = stub 'report', :exit_status => 666
@transaction.stubs(:report).returns(report)
expect { @apply.main }.to exit_with 0
end
it "should always exit with 0 if --noop" do
Puppet[:noop] = true
report = stub 'report', :exit_status => 666
@transaction.stubs(:report).returns(report)
expect { @apply.main }.to exit_with 0
end
end
end
describe "the 'apply' command" do
# We want this memoized, and to be able to adjust the content, so we
# have to do it ourselves.
def temporary_catalog(content = '"something"')
@tempfile = Tempfile.new('catalog.pson')
@tempfile.write(content)
@tempfile.close
@tempfile.path
end
it "should read the catalog in from disk if a file name is provided" do
@apply.options[:catalog] = temporary_catalog
Puppet::Resource::Catalog.stubs(:convert_from).
with(:pson,'"something"').returns(Puppet::Resource::Catalog.new)
@apply.apply
end
it "should read the catalog in from stdin if '-' is provided" do
@apply.options[:catalog] = "-"
$stdin.expects(:read).returns '"something"'
Puppet::Resource::Catalog.stubs(:convert_from).with(:pson,'"something"').returns Puppet::Resource::Catalog.new
@apply.apply
end
it "should deserialize the catalog from the default format" do
@apply.options[:catalog] = temporary_catalog
Puppet::Resource::Catalog.stubs(:default_format).returns :rot13_piglatin
Puppet::Resource::Catalog.stubs(:convert_from).with(:rot13_piglatin,'"something"').returns Puppet::Resource::Catalog.new
@apply.apply
end
it "should fail helpfully if deserializing fails" do
@apply.options[:catalog] = temporary_catalog('something syntactically invalid')
lambda { @apply.apply }.should raise_error(Puppet::Error)
end
it "should convert plain data structures into a catalog if deserialization does not do so" do
@apply.options[:catalog] = temporary_catalog
Puppet::Resource::Catalog.stubs(:convert_from).with(:pson,'"something"').returns({:foo => "bar"})
Puppet::Resource::Catalog.expects(:pson_create).with({:foo => "bar"}).returns(Puppet::Resource::Catalog.new)
@apply.apply
end
it "should convert the catalog to a RAL catalog and use a Configurer instance to apply it" do
@apply.options[:catalog] = temporary_catalog
catalog = Puppet::Resource::Catalog.new
Puppet::Resource::Catalog.stubs(:convert_from).with(:pson,'"something"').returns catalog
catalog.expects(:to_ral).returns "mycatalog"
configurer = stub 'configurer'
Puppet::Configurer.expects(:new).returns configurer
configurer.expects(:run).
with(:catalog => "mycatalog", :pluginsync => false)
@apply.apply
end
end
end
describe "apply_catalog" do
it "should call the configurer with the catalog" do
catalog = "I am a catalog"
Puppet::Configurer.any_instance.expects(:run).
with(:catalog => catalog, :pluginsync => false)
@apply.send(:apply_catalog, catalog)
end
end
end
diff --git a/spec/unit/application/device_spec.rb b/spec/unit/application/device_spec.rb
index bbbc011c1..25a4f83de 100755
--- a/spec/unit/application/device_spec.rb
+++ b/spec/unit/application/device_spec.rb
@@ -1,420 +1,415 @@
#! /usr/bin/env ruby
require 'spec_helper'
require 'puppet/application/device'
require 'puppet/util/network_device/config'
require 'ostruct'
require 'puppet/configurer'
describe Puppet::Application::Device do
include PuppetSpec::Files
before :each do
@device = Puppet::Application[:device]
@device.preinit
Puppet::Util::Log.stubs(:newdestination)
Puppet::Node.indirection.stubs(:terminus_class=)
Puppet::Node.indirection.stubs(:cache_class=)
Puppet::Node::Facts.indirection.stubs(:terminus_class=)
end
it "should operate in agent run_mode" do
@device.class.run_mode.name.should == :agent
end
it "should declare a main command" do
@device.should respond_to(:main)
end
it "should declare a preinit block" do
@device.should respond_to(:preinit)
end
describe "in preinit" do
before :each do
@device.stubs(:trap)
end
it "should catch INT" do
Signal.expects(:trap).with { |arg,block| arg == :INT }
@device.preinit
end
it "should init waitforcert to nil" do
@device.preinit
@device.options[:waitforcert].should be_nil
end
end
describe "when handling options" do
before do
@device.command_line.stubs(:args).returns([])
end
[:centrallogging, :debug, :verbose,].each do |option|
it "should declare handle_#{option} method" do
@device.should respond_to("handle_#{option}".to_sym)
end
it "should store argument value when calling handle_#{option}" do
@device.options.expects(:[]=).with(option, 'arg')
@device.send("handle_#{option}".to_sym, 'arg')
end
end
it "should set waitforcert to 0 with --onetime and if --waitforcert wasn't given" do
Puppet[:onetime] = true
Puppet::SSL::Host.any_instance.expects(:wait_for_cert).with(0)
@device.setup_host
end
it "should use supplied waitforcert when --onetime is specified" do
Puppet[:onetime] = true
@device.handle_waitforcert(60)
Puppet::SSL::Host.any_instance.expects(:wait_for_cert).with(60)
@device.setup_host
end
it "should use a default value for waitforcert when --onetime and --waitforcert are not specified" do
Puppet::SSL::Host.any_instance.expects(:wait_for_cert).with(120)
@device.setup_host
end
it "should use the waitforcert setting when checking for a signed certificate" do
Puppet[:waitforcert] = 10
Puppet::SSL::Host.any_instance.expects(:wait_for_cert).with(10)
@device.setup_host
end
it "should set the log destination with --logdest" do
@device.options.stubs(:[]=).with { |opt,val| opt == :setdest }
Puppet::Log.expects(:newdestination).with("console")
@device.handle_logdest("console")
end
it "should put the setdest options to true" do
@device.options.expects(:[]=).with(:setdest,true)
@device.handle_logdest("console")
end
it "should parse the log destination from the command line" do
@device.command_line.stubs(:args).returns(%w{--logdest /my/file})
Puppet::Util::Log.expects(:newdestination).with("/my/file")
@device.parse_options
end
it "should store the waitforcert options with --waitforcert" do
@device.options.expects(:[]=).with(:waitforcert,42)
@device.handle_waitforcert("42")
end
it "should set args[:Port] with --port" do
@device.handle_port("42")
@device.args[:Port].should == "42"
end
end
describe "during setup" do
before :each do
@device.options.stubs(:[])
Puppet.stubs(:info)
- 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=)
Puppet::Resource::Catalog.indirection.stubs(:terminus_class=)
Puppet::Resource::Catalog.indirection.stubs(:cache_class=)
Puppet::Node::Facts.indirection.stubs(:terminus_class=)
@host = stub_everything 'host'
Puppet::SSL::Host.stubs(:new).returns(@host)
Puppet.stubs(:settraps)
end
it "should call setup_logs" do
@device.expects(:setup_logs)
@device.setup
end
describe "when setting up logs" do
before :each do
Puppet::Util::Log.stubs(:newdestination)
end
it "should set log level to debug if --debug was passed" do
@device.options.stubs(:[]).with(:debug).returns(true)
@device.setup_logs
Puppet::Util::Log.level.should == :debug
end
it "should set log level to info if --verbose was passed" do
@device.options.stubs(:[]).with(:verbose).returns(true)
@device.setup_logs
Puppet::Util::Log.level.should == :info
end
[:verbose, :debug].each do |level|
it "should set console as the log destination with level #{level}" do
@device.options.stubs(:[]).with(level).returns(true)
Puppet::Util::Log.expects(:newdestination).with(:console)
@device.setup_logs
end
end
it "should set a default log destination if no --logdest" do
@device.options.stubs(:[]).with(:setdest).returns(false)
Puppet::Util::Log.expects(:setup_default)
@device.setup_logs
end
end
it "should set a central log destination with --centrallogs" do
@device.options.stubs(:[]).with(:centrallogs).returns(true)
Puppet[:server] = "puppet.reductivelabs.com"
Puppet::Util::Log.stubs(:newdestination).with(:syslog)
Puppet::Util::Log.expects(:newdestination).with("puppet.reductivelabs.com")
@device.setup
end
it "should use :main, :agent, :device and :ssl config" do
Puppet.settings.expects(:use).with(:main, :agent, :device, :ssl)
@device.setup
end
it "should install a remote ca location" do
Puppet::SSL::Host.expects(:ca_location=).with(:remote)
@device.setup
end
it "should tell the report handler to use REST" do
Puppet::Transaction::Report.indirection.expects(:terminus_class=).with(:rest)
@device.setup
end
it "should default the catalog_terminus setting to 'rest'" do
@device.initialize_app_defaults
Puppet[:catalog_terminus].should == :rest
end
it "should default the node_terminus setting to 'rest'" do
@device.initialize_app_defaults
Puppet[:node_terminus].should == :rest
end
it "has an application default :catalog_cache_terminus setting of 'json'" do
Puppet::Resource::Catalog.indirection.expects(:cache_class=).with(:json)
@device.initialize_app_defaults
@device.setup
end
it "should tell the catalog cache class based on the :catalog_cache_terminus setting" do
Puppet[:catalog_cache_terminus] = "yaml"
Puppet::Resource::Catalog.indirection.expects(:cache_class=).with(:yaml)
@device.initialize_app_defaults
@device.setup
end
it "should not set catalog cache class if :catalog_cache_terminus is explicitly nil" do
Puppet[:catalog_cache_terminus] = nil
Puppet::Resource::Catalog.indirection.expects(:cache_class=).never
@device.initialize_app_defaults
@device.setup
end
it "should default the facts_terminus setting to 'network_device'" do
@device.initialize_app_defaults
Puppet[:facts_terminus].should == :network_device
end
end
describe "when initializing each devices SSL" do
before(:each) do
@host = stub_everything 'host'
Puppet::SSL::Host.stubs(:new).returns(@host)
end
it "should create a new ssl host" do
Puppet::SSL::Host.expects(:new).returns(@host)
@device.setup_host
end
it "should wait for a certificate" do
@device.options.stubs(:[]).with(:waitforcert).returns(123)
@host.expects(:wait_for_cert).with(123)
@device.setup_host
end
end
describe "when running" do
before :each do
@device.options.stubs(:[]).with(:fingerprint).returns(false)
Puppet.stubs(:notice)
@device.options.stubs(:[]).with(:client)
Puppet::Util::NetworkDevice::Config.stubs(:devices).returns({})
end
it "should dispatch to main" do
@device.stubs(:main)
@device.run_command
end
it "should get the device list" do
device_hash = stub_everything 'device hash'
Puppet::Util::NetworkDevice::Config.expects(:devices).returns(device_hash)
@device.main
end
it "should exit if the device list is empty" do
expect { @device.main }.to exit_with 1
end
describe "for each device" do
before(:each) do
Puppet[:vardir] = make_absolute("/dummy")
Puppet[:confdir] = make_absolute("/dummy")
Puppet[:certname] = "certname"
@device_hash = {
"device1" => OpenStruct.new(:name => "device1", :url => "url", :provider => "cisco"),
"device2" => OpenStruct.new(:name => "device2", :url => "url", :provider => "cisco"),
}
Puppet::Util::NetworkDevice::Config.stubs(:devices).returns(@device_hash)
- Puppet.settings.stubs(:set_value)
+ Puppet.stubs(:[]=)
Puppet.settings.stubs(:use)
@device.stubs(:setup_host)
Puppet::Util::NetworkDevice.stubs(:init)
@configurer = stub_everything 'configurer'
Puppet::Configurer.stubs(:new).returns(@configurer)
end
it "should set vardir to the device vardir" do
- Puppet.settings.expects(:set_value).with(:vardir, make_absolute("/dummy/devices/device1"), :cli)
+ Puppet.expects(:[]=).with(:vardir, make_absolute("/dummy/devices/device1"))
@device.main
end
it "should set confdir to the device confdir" do
- Puppet.settings.expects(:set_value).with(:confdir, make_absolute("/dummy/devices/device1"), :cli)
+ Puppet.expects(:[]=).with(:confdir, make_absolute("/dummy/devices/device1"))
@device.main
end
it "should set certname to the device certname" do
- Puppet.settings.expects(:set_value).with(:certname, "device1", :cli)
- Puppet.settings.expects(:set_value).with(:certname, "device2", :cli)
+ Puppet.expects(:[]=).with(:certname, "device1")
+ Puppet.expects(:[]=).with(:certname, "device2")
@device.main
end
it "should make sure all the required folders and files are created" do
Puppet.settings.expects(:use).with(:main, :agent, :ssl).twice
@device.main
end
it "should initialize the device singleton" do
Puppet::Util::NetworkDevice.expects(:init).with(@device_hash["device1"]).then.with(@device_hash["device2"])
@device.main
end
it "should setup the SSL context" do
@device.expects(:setup_host).twice
@device.main
end
it "should launch a configurer for this device" do
@configurer.expects(:run).twice
@device.main
end
[:vardir, :confdir].each do |setting|
it "should cleanup the #{setting} setting after the run" do
all_devices = Set.new(@device_hash.keys.map do |device_name| make_absolute("/dummy/devices/#{device_name}") end)
found_devices = Set.new()
- # a block to use in a few places later to validate the arguments passed to "set_value"
- p = Proc.new do |my_setting, my_value, my_type|
- success =
- (my_setting == setting) &&
- (my_type == :cli) &&
- (all_devices.include?(my_value))
- found_devices.add(my_value) if success
- success
+ # a block to use in a few places later to validate the updated settings
+ p = Proc.new do |my_setting, my_value|
+ if my_setting == setting && all_devices.include?(my_value)
+ found_devices.add(my_value)
+ true
+ else
+ false
+ end
end
seq = sequence("clean up dirs")
all_devices.size.times do
## one occurrence of set / run / set("/dummy") for each device
- Puppet.settings.expects(:set_value).with(&p).in_sequence(seq)
+ Puppet.expects(:[]=).with(&p).in_sequence(seq)
@configurer.expects(:run).in_sequence(seq)
- Puppet.settings.expects(:set_value).with(setting, make_absolute("/dummy"), :cli).in_sequence(seq)
+ Puppet.expects(:[]=).with(setting, make_absolute("/dummy")).in_sequence(seq)
end
@device.main
- # make sure that we were called with each of the defined devices
- all_devices.should == found_devices
-
+ expect(found_devices).to eq(all_devices)
end
end
it "should cleanup the certname setting after the run" do
all_devices = Set.new(@device_hash.keys)
found_devices = Set.new()
- # a block to use in a few places later to validate the arguments passed to "set_value"
- p = Proc.new do |my_setting, my_value, my_type|
- success =
- (my_setting == :certname) &&
- (my_type == :cli) &&
- (all_devices.include?(my_value))
- found_devices.add(my_value) if success
- success
- #true
+ # a block to use in a few places later to validate the updated settings
+ p = Proc.new do |my_setting, my_value|
+ if my_setting == :certname && all_devices.include?(my_value)
+ found_devices.add(my_value)
+ true
+ else
+ false
+ end
end
seq = sequence("clean up certname")
all_devices.size.times do
## one occurrence of set / run / set("certname") for each device
- Puppet.settings.expects(:set_value).with(&p).in_sequence(seq)
+ Puppet.expects(:[]=).with(&p).in_sequence(seq)
@configurer.expects(:run).in_sequence(seq)
- Puppet.settings.expects(:set_value).with(:certname, "certname", :cli).in_sequence(seq)
+ Puppet.expects(:[]=).with(:certname, "certname").in_sequence(seq)
end
@device.main
# make sure that we were called with each of the defined devices
- all_devices.should == found_devices
-
+ expect(found_devices).to eq(all_devices)
end
it "should expire all cached attributes" do
Puppet::SSL::Host.expects(:reset).twice
@device.main
end
end
end
end
diff --git a/spec/unit/application/doc_spec.rb b/spec/unit/application/doc_spec.rb
index e2dc0fe69..d8e924c43 100755
--- a/spec/unit/application/doc_spec.rb
+++ b/spec/unit/application/doc_spec.rb
@@ -1,333 +1,333 @@
#! /usr/bin/env ruby
require 'spec_helper'
require 'puppet/application/doc'
require 'puppet/util/reference'
require 'puppet/util/rdoc'
describe Puppet::Application::Doc do
before :each do
@doc = Puppet::Application[:doc]
@doc.stubs(:puts)
@doc.preinit
Puppet::Util::Log.stubs(:newdestination)
end
it "should declare an other command" do
@doc.should respond_to(:other)
end
it "should declare a rdoc command" do
@doc.should respond_to(:rdoc)
end
it "should declare a fallback for unknown options" do
@doc.should respond_to(:handle_unknown)
end
it "should declare a preinit block" do
@doc.should respond_to(:preinit)
end
describe "in preinit" do
it "should set references to []" do
@doc.preinit
@doc.options[:references].should == []
end
it "should init mode to text" do
@doc.preinit
@doc.options[:mode].should == :text
end
it "should init format to to_markdown" do
@doc.preinit
@doc.options[:format].should == :to_markdown
end
end
describe "when handling options" do
[:all, :outputdir, :verbose, :debug, :charset].each do |option|
it "should declare handle_#{option} method" do
@doc.should respond_to("handle_#{option}".to_sym)
end
it "should store argument value when calling handle_#{option}" do
@doc.options.expects(:[]=).with(option, 'arg')
@doc.send("handle_#{option}".to_sym, 'arg')
end
end
it "should store the format if valid" do
Puppet::Util::Reference.stubs(:method_defined?).with('to_format').returns(true)
@doc.handle_format('format')
@doc.options[:format].should == 'to_format'
end
it "should raise an error if the format is not valid" do
Puppet::Util::Reference.stubs(:method_defined?).with('to_format').returns(false)
expect { @doc.handle_format('format') }.to raise_error(RuntimeError, /Invalid output format/)
end
it "should store the mode if valid" do
Puppet::Util::Reference.stubs(:modes).returns(stub('mode', :include? => true))
@doc.handle_mode('mode')
@doc.options[:mode].should == :mode
end
it "should store the mode if :rdoc" do
Puppet::Util::Reference.modes.stubs(:include?).with('rdoc').returns(false)
@doc.handle_mode('rdoc')
@doc.options[:mode].should == :rdoc
end
it "should raise an error if the mode is not valid" do
Puppet::Util::Reference.modes.stubs(:include?).with('unknown').returns(false)
expect { @doc.handle_mode('unknown') }.to raise_error(RuntimeError, /Invalid output mode/)
end
it "should list all references on list and exit" do
reference = stubs 'reference'
ref = stubs 'ref'
Puppet::Util::Reference.stubs(:references).returns([reference])
Puppet::Util::Reference.expects(:reference).with(reference).returns(ref)
ref.expects(:doc)
expect { @doc.handle_list(nil) }.to exit_with 0
end
it "should add reference to references list with --reference" do
@doc.options[:references] = [:ref1]
@doc.handle_reference('ref2')
@doc.options[:references].should == [:ref1,:ref2]
end
end
describe "during setup" do
before :each do
Puppet::Log.stubs(:newdestination)
@doc.command_line.stubs(:args).returns([])
end
it "should default to rdoc mode if there are command line arguments" do
@doc.command_line.stubs(:args).returns(["1"])
@doc.stubs(:setup_rdoc)
@doc.setup
@doc.options[:mode].should == :rdoc
end
it "should call setup_rdoc in rdoc mode" do
@doc.options[:mode] = :rdoc
@doc.expects(:setup_rdoc)
@doc.setup
end
it "should call setup_reference if not rdoc" do
@doc.options[:mode] = :test
@doc.expects(:setup_reference)
@doc.setup
end
describe "configuring logging" do
before :each do
Puppet::Util::Log.stubs(:newdestination)
end
describe "with --debug" do
before do
@doc.options[:debug] = true
end
it "should set log level to debug" do
@doc.setup
Puppet::Util::Log.level.should == :debug
end
it "should set log destination to console" do
Puppet::Util::Log.expects(:newdestination).with(:console)
@doc.setup
end
end
describe "with --verbose" do
before do
@doc.options[:verbose] = true
end
it "should set log level to info" do
@doc.setup
Puppet::Util::Log.level.should == :info
end
it "should set log destination to console" do
Puppet::Util::Log.expects(:newdestination).with(:console)
@doc.setup
end
end
describe "without --debug or --verbose" do
before do
@doc.options[:debug] = false
@doc.options[:verbose] = false
end
it "should set log level to warning" do
@doc.setup
Puppet::Util::Log.level.should == :warning
end
it "should set log destination to console" do
Puppet::Util::Log.expects(:newdestination).with(:console)
@doc.setup
end
end
end
describe "in non-rdoc mode" do
it "should get all non-dynamic reference if --all" do
@doc.options[:all] = true
static = stub 'static', :dynamic? => false
dynamic = stub 'dynamic', :dynamic? => true
Puppet::Util::Reference.stubs(:reference).with(:static).returns(static)
Puppet::Util::Reference.stubs(:reference).with(:dynamic).returns(dynamic)
Puppet::Util::Reference.stubs(:references).returns([:static,:dynamic])
@doc.setup_reference
@doc.options[:references].should == [:static]
end
it "should default to :type if no references" do
@doc.setup_reference
@doc.options[:references].should == [:type]
end
end
describe "in rdoc mode" do
describe "when there are unknown args" do
it "should expand --modulepath if any" do
@doc.unknown_args = [ { :opt => "--modulepath", :arg => "path" } ]
Puppet.settings.stubs(:handlearg)
- File.expects(:expand_path).with("path")
-
@doc.setup_rdoc
+
+ @doc.unknown_args[0][:arg].should == File.expand_path('path')
end
it "should expand --manifestdir if any" do
@doc.unknown_args = [ { :opt => "--manifestdir", :arg => "path" } ]
Puppet.settings.stubs(:handlearg)
- File.expects(:expand_path).with("path")
-
@doc.setup_rdoc
+
+ @doc.unknown_args[0][:arg].should == File.expand_path('path')
end
it "should give them to Puppet.settings" do
@doc.unknown_args = [ { :opt => :option, :arg => :argument } ]
Puppet.settings.expects(:handlearg).with(:option,:argument)
@doc.setup_rdoc
end
end
it "should operate in master run_mode" do
@doc.class.run_mode.name.should == :master
@doc.setup_rdoc
end
end
end
describe "when running" do
describe "in rdoc mode" do
let(:modules) { File.expand_path("modules") }
let(:manifests) { File.expand_path("manifests") }
before :each do
@doc.manifest = false
Puppet.stubs(:info)
Puppet[:trace] = false
@env = stub 'env'
@env.stubs(:modulepath).returns([modules])
@env.stubs(:[]).with(:manifest).returns('manifests/site.pp')
Puppet::Node::Environment.stubs(:new).returns(@env)
Puppet[:modulepath] = modules
Puppet[:manifestdir] = manifests
@doc.options[:all] = false
@doc.options[:outputdir] = 'doc'
@doc.options[:charset] = nil
Puppet.settings.stubs(:define_settings)
Puppet::Util::RDoc.stubs(:rdoc)
@doc.command_line.stubs(:args).returns([])
end
it "should set document_all on --all" do
@doc.options[:all] = true
Puppet.settings.expects(:[]=).with(:document_all, true)
expect { @doc.rdoc }.to exit_with 0
end
it "should call Puppet::Util::RDoc.rdoc in full mode" do
Puppet::Util::RDoc.expects(:rdoc).with('doc', [modules, 'manifests'], nil)
expect { @doc.rdoc }.to exit_with 0
end
it "should call Puppet::Util::RDoc.rdoc with a charset if --charset has been provided" do
@doc.options[:charset] = 'utf-8'
Puppet::Util::RDoc.expects(:rdoc).with('doc', [modules, 'manifests'], "utf-8")
expect { @doc.rdoc }.to exit_with 0
end
it "should call Puppet::Util::RDoc.rdoc in full mode with outputdir set to doc if no --outputdir" do
@doc.options[:outputdir] = false
Puppet::Util::RDoc.expects(:rdoc).with('doc', [modules, 'manifests'], nil)
expect { @doc.rdoc }.to exit_with 0
end
it "should call Puppet::Util::RDoc.manifestdoc in manifest mode" do
@doc.manifest = true
Puppet::Util::RDoc.expects(:manifestdoc)
expect { @doc.rdoc }.to exit_with 0
end
it "should get modulepath and manifestdir values from the environment" do
@env.expects(:modulepath).returns(['envmodules1','envmodules2'])
@env.expects(:[]).with(:manifest).returns('envmanifests/site.pp')
Puppet::Util::RDoc.expects(:rdoc).with('doc', ['envmodules1','envmodules2','envmanifests'], nil)
expect { @doc.rdoc }.to exit_with 0
end
end
describe "in the other modes" do
it "should get reference in given format" do
reference = stub 'reference'
@doc.options[:mode] = :none
@doc.options[:references] = [:ref]
Puppet::Util::Reference.expects(:reference).with(:ref).returns(reference)
@doc.options[:format] = :format
@doc.stubs(:exit)
reference.expects(:send).with { |format,contents| format == :format }.returns('doc')
@doc.other
end
end
end
end
diff --git a/spec/unit/application/filebucket_spec.rb b/spec/unit/application/filebucket_spec.rb
index 4301c7dcd..926ea9c1d 100755
--- a/spec/unit/application/filebucket_spec.rb
+++ b/spec/unit/application/filebucket_spec.rb
@@ -1,207 +1,207 @@
#! /usr/bin/env ruby
require 'spec_helper'
require 'puppet/application/filebucket'
require 'puppet/file_bucket/dipper'
describe Puppet::Application::Filebucket do
before :each do
@filebucket = Puppet::Application[:filebucket]
end
it "should declare a get command" do
@filebucket.should respond_to(:get)
end
it "should declare a backup command" do
@filebucket.should respond_to(:backup)
end
it "should declare a restore command" do
@filebucket.should respond_to(:restore)
end
[:bucket, :debug, :local, :remote, :verbose].each do |option|
it "should declare handle_#{option} method" do
@filebucket.should respond_to("handle_#{option}".to_sym)
end
it "should store argument value when calling handle_#{option}" do
@filebucket.options.expects(:[]=).with("#{option}".to_sym, 'arg')
@filebucket.send("handle_#{option}".to_sym, 'arg')
end
end
describe "during setup" do
before :each do
Puppet::Log.stubs(:newdestination)
Puppet.stubs(:settraps)
Puppet::FileBucket::Dipper.stubs(:new)
@filebucket.options.stubs(:[]).with(any_parameters)
end
it "should set console as the log destination" do
Puppet::Log.expects(:newdestination).with(:console)
@filebucket.setup
end
it "should trap INT" do
Signal.expects(:trap).with(:INT)
@filebucket.setup
end
it "should set log level to debug if --debug was passed" do
@filebucket.options.stubs(:[]).with(:debug).returns(true)
@filebucket.setup
Puppet::Log.level.should == :debug
end
it "should set log level to info if --verbose was passed" do
@filebucket.options.stubs(:[]).with(:verbose).returns(true)
@filebucket.setup
Puppet::Log.level.should == :info
end
it "should print puppet config if asked to in Puppet config" do
Puppet.settings.stubs(:print_configs?).returns(true)
Puppet.settings.expects(:print_configs).returns(true)
expect { @filebucket.setup }.to exit_with 0
end
it "should exit after printing puppet config if asked to in Puppet config" do
Puppet.settings.stubs(:print_configs?).returns(true)
expect { @filebucket.setup }.to exit_with 1
end
describe "with local bucket" do
let(:path) { File.expand_path("path") }
before :each do
@filebucket.options.stubs(:[]).with(:local).returns(true)
end
it "should create a client with the default bucket if none passed" do
Puppet[:bucketdir] = path
Puppet::FileBucket::Dipper.expects(:new).with { |h| h[:Path] == path }
@filebucket.setup
end
it "should create a local Dipper with the given bucket" do
@filebucket.options.stubs(:[]).with(:bucket).returns(path)
Puppet::FileBucket::Dipper.expects(:new).with { |h| h[:Path] == path }
@filebucket.setup
end
end
describe "with remote bucket" do
it "should create a remote Client to the configured server" do
Puppet[:server] = "puppet.reductivelabs.com"
Puppet::FileBucket::Dipper.expects(:new).with { |h| h[:Server] == "puppet.reductivelabs.com" }
@filebucket.setup
end
end
end
describe "when running" do
before :each do
Puppet::Log.stubs(:newdestination)
Puppet.stubs(:settraps)
Puppet::FileBucket::Dipper.stubs(:new)
@filebucket.options.stubs(:[]).with(any_parameters)
@client = stub 'client'
Puppet::FileBucket::Dipper.stubs(:new).returns(@client)
@filebucket.setup
end
it "should use the first non-option parameter as the dispatch" do
@filebucket.command_line.stubs(:args).returns(['get'])
@filebucket.expects(:get)
@filebucket.run_command
end
describe "the command get" do
before :each do
@filebucket.stubs(:print)
@filebucket.stubs(:args).returns([])
end
it "should call the client getfile method" do
@client.expects(:getfile)
@filebucket.get
end
it "should call the client getfile method with the given md5" do
md5="DEADBEEF"
@filebucket.stubs(:args).returns([md5])
@client.expects(:getfile).with(md5)
@filebucket.get
end
it "should print the file content" do
@client.stubs(:getfile).returns("content")
@filebucket.expects(:print).returns("content")
@filebucket.get
end
end
describe "the command backup" do
it "should fail if no arguments are specified" do
@filebucket.stubs(:args).returns([])
lambda { @filebucket.backup }.should raise_error
end
it "should call the client backup method for each given parameter" do
@filebucket.stubs(:puts)
- Puppet::FileSystem::File.stubs(:exist?).returns(true)
+ Puppet::FileSystem.stubs(:exist?).returns(true)
FileTest.stubs(:readable?).returns(true)
@filebucket.stubs(:args).returns(["file1", "file2"])
@client.expects(:backup).with("file1")
@client.expects(:backup).with("file2")
@filebucket.backup
end
end
describe "the command restore" do
it "should call the client getfile method with the given md5" do
md5="DEADBEEF"
file="testfile"
@filebucket.stubs(:args).returns([file, md5])
@client.expects(:restore).with(file,md5)
@filebucket.restore
end
end
end
end
diff --git a/spec/unit/application/master_spec.rb b/spec/unit/application/master_spec.rb
index d7f26b74a..4d3167ce8 100755
--- a/spec/unit/application/master_spec.rb
+++ b/spec/unit/application/master_spec.rb
@@ -1,355 +1,355 @@
#! /usr/bin/env ruby
require 'spec_helper'
require 'puppet/application/master'
require 'puppet/daemon'
require 'puppet/network/server'
describe Puppet::Application::Master, :unless => Puppet.features.microsoft_windows? do
before :each do
@master = Puppet::Application[:master]
@daemon = stub_everything 'daemon'
Puppet::Daemon.stubs(:new).returns(@daemon)
Puppet::Util::Log.stubs(:newdestination)
Puppet::Node.indirection.stubs(:terminus_class=)
Puppet::Node.indirection.stubs(:cache_class=)
Puppet::Node::Facts.indirection.stubs(:terminus_class=)
Puppet::Node::Facts.indirection.stubs(:cache_class=)
Puppet::Transaction::Report.indirection.stubs(:terminus_class=)
Puppet::Resource::Catalog.indirection.stubs(:terminus_class=)
Puppet::SSL::Host.stubs(:ca_location=)
end
it "should operate in master run_mode" do
@master.class.run_mode.name.should equal(:master)
end
it "should declare a main command" do
@master.should respond_to(:main)
end
it "should declare a compile command" do
@master.should respond_to(:compile)
end
it "should declare a preinit block" do
@master.should respond_to(:preinit)
end
describe "during preinit" do
before :each do
@master.stubs(:trap)
end
it "should catch INT" do
@master.stubs(:trap).with { |arg,block| arg == :INT }
@master.preinit
end
end
[:debug,:verbose].each do |option|
it "should declare handle_#{option} method" do
@master.should respond_to("handle_#{option}".to_sym)
end
it "should store argument value when calling handle_#{option}" do
@master.options.expects(:[]=).with(option, 'arg')
@master.send("handle_#{option}".to_sym, 'arg')
end
end
describe "when applying options" do
before do
@master.command_line.stubs(:args).returns([])
end
it "should set the log destination with --logdest" do
Puppet::Log.expects(:newdestination).with("console")
@master.handle_logdest("console")
end
it "should put the setdest options to true" do
@master.options.expects(:[]=).with(:setdest,true)
@master.handle_logdest("console")
end
it "should parse the log destination from ARGV" do
@master.command_line.stubs(:args).returns(%w{--logdest /my/file})
Puppet::Util::Log.expects(:newdestination).with("/my/file")
@master.parse_options
end
it "should support dns alt names from ARGV" do
Puppet.settings.initialize_global_settings(["--dns_alt_names", "foo,bar,baz"])
@master.preinit
@master.parse_options
Puppet[:dns_alt_names].should == "foo,bar,baz"
end
end
describe "during setup" do
before :each do
Puppet::Log.stubs(:newdestination)
Puppet.stubs(:settraps)
Puppet::SSL::CertificateAuthority.stubs(:instance)
Puppet::SSL::CertificateAuthority.stubs(:ca?)
Puppet.settings.stubs(:use)
@master.options.stubs(:[]).with(any_parameters)
end
it "should abort stating that the master is not supported on Windows" do
Puppet.features.stubs(:microsoft_windows?).returns(true)
expect { @master.setup }.to raise_error(Puppet::Error, /Puppet master is not supported on Microsoft Windows/)
end
it "should set log level to debug if --debug was passed" do
@master.options.stubs(:[]).with(:debug).returns(true)
@master.setup
Puppet::Log.level.should == :debug
end
it "should set log level to info if --verbose was passed" do
@master.options.stubs(:[]).with(:verbose).returns(true)
@master.setup
Puppet::Log.level.should == :info
end
it "should set console as the log destination if no --logdest and --daemonize" do
@master.stubs(:[]).with(:daemonize).returns(:false)
Puppet::Log.expects(:newdestination).with(:syslog)
@master.setup
end
it "should set syslog as the log destination if no --logdest and not --daemonize" do
Puppet::Log.expects(:newdestination).with(:syslog)
@master.setup
end
it "should set syslog as the log destination if --rack" do
@master.options.stubs(:[]).with(:rack).returns(:true)
Puppet::Log.expects(:newdestination).with(:syslog)
@master.setup
end
it "should print puppet config if asked to in Puppet config" do
Puppet.settings.stubs(:print_configs?).returns(true)
Puppet.settings.expects(:print_configs).returns(true)
expect { @master.setup }.to exit_with 0
end
it "should exit after printing puppet config if asked to in Puppet config" do
Puppet.settings.stubs(:print_configs?).returns(true)
expect { @master.setup }.to exit_with 1
end
it "should tell Puppet.settings to use :main,:ssl,:master and :metrics category" do
Puppet.settings.expects(:use).with(:main,:master,:ssl,:metrics)
@master.setup
end
describe "with no ca" do
it "should set the ca_location to none" do
Puppet::SSL::Host.expects(:ca_location=).with(:none)
@master.setup
end
end
describe "with a ca configured" do
before :each do
Puppet::SSL::CertificateAuthority.stubs(:ca?).returns(true)
end
it "should set the ca_location to local" do
Puppet::SSL::Host.expects(:ca_location=).with(:local)
@master.setup
end
it "should tell Puppet.settings to use :ca category" do
Puppet.settings.expects(:use).with(:ca)
@master.setup
end
it "should instantiate the CertificateAuthority singleton" do
Puppet::SSL::CertificateAuthority.expects(:instance)
@master.setup
end
end
end
describe "when running" do
before do
@master.preinit
end
it "should dispatch to compile if called with --compile" do
@master.options[:node] = "foo"
@master.expects(:compile)
@master.run_command
end
it "should dispatch to main otherwise" do
@master.options[:node] = nil
@master.expects(:main)
@master.run_command
end
describe "the compile command" do
before do
Puppet[:manifest] = "site.pp"
Puppet.stubs(:err)
@master.stubs(:puts)
end
it "should compile a catalog for the specified node" do
@master.options[:node] = "foo"
Puppet::Resource::Catalog.indirection.expects(:find).with("foo").returns Puppet::Resource::Catalog.new
expect { @master.compile }.to exit_with 0
end
it "should convert the catalog to a pure-resource catalog and use 'PSON::pretty_generate' to pretty-print the catalog" do
catalog = Puppet::Resource::Catalog.new
PSON.stubs(:pretty_generate)
Puppet::Resource::Catalog.indirection.expects(:find).returns catalog
catalog.expects(:to_resource).returns("rescat")
@master.options[:node] = "foo"
PSON.expects(:pretty_generate).with('rescat', :allow_nan => true, :max_nesting => false)
expect { @master.compile }.to exit_with 0
end
it "should exit with error code 30 if no catalog can be found" do
@master.options[:node] = "foo"
Puppet::Resource::Catalog.indirection.expects(:find).returns nil
- $stderr.expects(:puts)
+ Puppet.expects(:log_exception)
expect { @master.compile }.to exit_with 30
end
it "should exit with error code 30 if there's a failure" do
@master.options[:node] = "foo"
Puppet::Resource::Catalog.indirection.expects(:find).raises ArgumentError
- $stderr.expects(:puts)
+ Puppet.expects(:log_exception)
expect { @master.compile }.to exit_with 30
end
end
describe "the main command" do
before :each do
@master.preinit
@server = stub_everything 'server'
Puppet::Network::Server.stubs(:new).returns(@server)
@app = stub_everything 'app'
Puppet::SSL::Host.stubs(:localhost)
Puppet::SSL::CertificateAuthority.stubs(:ca?)
Process.stubs(:uid).returns(1000)
Puppet.stubs(:service)
Puppet[:daemonize] = false
Puppet.stubs(:notice)
Puppet.stubs(:start)
end
it "should create a Server" do
Puppet::Network::Server.expects(:new)
@master.main
end
it "should give the server to the daemon" do
@daemon.expects(:server=).with(@server)
@master.main
end
it "should generate a SSL cert for localhost" do
Puppet::SSL::Host.expects(:localhost)
@master.main
end
it "should make sure to *only* hit the CA for data" do
Puppet::SSL::CertificateAuthority.stubs(:ca?).returns(true)
Puppet::SSL::Host.expects(:ca_location=).with(:only)
@master.main
end
it "should drop privileges if running as root" do
Puppet.features.stubs(:root?).returns true
Puppet::Util.expects(:chuser)
@master.main
end
it "should daemonize if needed" do
Puppet[:daemonize] = true
@daemon.expects(:daemonize)
@master.main
end
it "should start the service" do
@daemon.expects(:start)
@master.main
end
describe "with --rack", :if => Puppet.features.rack? do
before do
require 'puppet/network/http/rack'
Puppet::Network::HTTP::Rack.stubs(:new).returns(@app)
end
it "it should not start a daemon" do
@master.options.stubs(:[]).with(:rack).returns(:true)
@daemon.expects(:start).never
@master.main
end
it "it should return the app" do
@master.options.stubs(:[]).with(:rack).returns(:true)
app = @master.main
app.should equal(@app)
end
end
end
end
end
diff --git a/spec/unit/application_spec.rb b/spec/unit/application_spec.rb
index 1c9dd49b8..b96f829d9 100755
--- a/spec/unit/application_spec.rb
+++ b/spec/unit/application_spec.rb
@@ -1,645 +1,645 @@
#! /usr/bin/env ruby
require 'spec_helper'
require 'puppet/application'
require 'puppet'
require 'getoptlong'
require 'timeout'
describe Puppet::Application do
before(:each) do
Puppet::Util::Instrumentation.stubs(:init)
@app = Class.new(Puppet::Application).new
@appclass = @app.class
@app.stubs(:name).returns("test_app")
end
describe "application commandline" do
it "should not pick up changes to the array of arguments" do
args = %w{subcommand --arg}
command_line = Puppet::Util::CommandLine.new('puppet', args)
app = Puppet::Application.new(command_line)
args[0] = 'different_subcommand'
args[1] = '--other-arg'
app.command_line.subcommand_name.should == 'subcommand'
app.command_line.args.should == ['--arg']
end
end
describe "application defaults" do
it "should fail if required app default values are missing" do
@app.stubs(:app_defaults).returns({ :foo => 'bar' })
Puppet.expects(:err).with(regexp_matches(/missing required app default setting/))
expect {
@app.run
}.to exit_with 1
end
end
describe "finding" do
before do
@klass = Puppet::Application
@klass.stubs(:puts)
end
it "should find classes in the namespace" do
@klass.find("Agent").should == @klass::Agent
end
it "should not find classes outside the namespace" do
expect { @klass.find("String") }.to raise_error(LoadError)
end
it "should error if it can't find a class" do
Puppet.expects(:err).with do |value|
value =~ /Unable to find application 'ThisShallNeverEverEverExist'/ and
value =~ /puppet\/application\/thisshallneverevereverexist/ and
value =~ /no such file to load|cannot load such file/
end
expect {
@klass.find("ThisShallNeverEverEverExist")
}.to raise_error(LoadError)
end
it "#12114: should prevent File namespace collisions" do
# have to require the file face once, then the second time around it would fail
@klass.find("File").should == Puppet::Application::File
@klass.find("File").should == Puppet::Application::File
end
end
describe "#available_application_names" do
it 'should be able to find available application names' do
apps = %w{describe filebucket kick queue resource agent cert apply doc master}
Puppet::Util::Autoload.expects(:files_to_load).returns(apps)
Puppet::Application.available_application_names.should =~ apps
end
it 'should find applications from multiple paths' do
Puppet::Util::Autoload.expects(:files_to_load).with('puppet/application').returns(%w{ /a/foo.rb /b/bar.rb })
Puppet::Application.available_application_names.should =~ %w{ foo bar }
end
it 'should return unique application names' do
Puppet::Util::Autoload.expects(:files_to_load).with('puppet/application').returns(%w{ /a/foo.rb /b/foo.rb })
Puppet::Application.available_application_names.should == %w{ foo }
end
end
describe ".run_mode" do
it "should default to user" do
@appclass.run_mode.name.should == :user
end
it "should set and get a value" do
@appclass.run_mode :agent
@appclass.run_mode.name.should == :agent
end
end
# These tests may look a little weird and repetative in its current state;
# it used to illustrate several ways that the run_mode could be changed
# at run time; there are fewer ways now, but it would still be nice to
# get to a point where it was entirely impossible.
describe "when dealing with run_mode" do
class TestApp < Puppet::Application
run_mode :master
def run_command
# no-op
end
end
it "should sadly and frighteningly allow run_mode to change at runtime via #initialize_app_defaults" do
Puppet.features.stubs(:syslog?).returns(true)
app = TestApp.new
app.initialize_app_defaults
Puppet.run_mode.should be_master
end
it "should sadly and frighteningly allow run_mode to change at runtime via #run" do
app = TestApp.new
app.run
app.class.run_mode.name.should == :master
Puppet.run_mode.should be_master
end
end
it "should explode when an invalid run mode is set at runtime, for great victory" do
expect {
class InvalidRunModeTestApp < Puppet::Application
run_mode :abracadabra
def run_command
# no-op
end
end
}.to raise_error
end
it "should have a run entry-point" do
@app.should respond_to(:run)
end
it "should have a read accessor to options" do
@app.should respond_to(:options)
end
it "should include a default setup method" do
@app.should respond_to(:setup)
end
it "should include a default preinit method" do
@app.should respond_to(:preinit)
end
it "should include a default run_command method" do
@app.should respond_to(:run_command)
end
it "should invoke main as the default" do
@app.expects( :main )
@app.run_command
end
it "should initialize the Puppet Instrumentation layer early in the life cycle" do
# Not proud of this, but the fact that we are stubbing init_app_defaults
# below means that we will get errors if anyone tries to access any
# settings that depend on app_defaults. In general this whole test
# seems to be testing too many implementation details rather than
# functionality, but, hey.
Puppet[:route_file] = "/dev/null"
startup_sequence = sequence('startup')
@app.expects(:initialize_app_defaults).in_sequence(startup_sequence)
Puppet::Util::Instrumentation.expects(:init).in_sequence(startup_sequence)
@app.expects(:preinit).in_sequence(startup_sequence)
expect { @app.run }.to exit_with(1)
end
describe 'when invoking clear!' do
before :each do
Puppet::Application.run_status = :stop_requested
Puppet::Application.clear!
end
it 'should have nil run_status' do
Puppet::Application.run_status.should be_nil
end
it 'should return false for restart_requested?' do
Puppet::Application.restart_requested?.should be_false
end
it 'should return false for stop_requested?' do
Puppet::Application.stop_requested?.should be_false
end
it 'should return false for interrupted?' do
Puppet::Application.interrupted?.should be_false
end
it 'should return true for clear?' do
Puppet::Application.clear?.should be_true
end
end
describe 'after invoking stop!' do
before :each do
Puppet::Application.run_status = nil
Puppet::Application.stop!
end
after :each do
Puppet::Application.run_status = nil
end
it 'should have run_status of :stop_requested' do
Puppet::Application.run_status.should == :stop_requested
end
it 'should return true for stop_requested?' do
Puppet::Application.stop_requested?.should be_true
end
it 'should return false for restart_requested?' do
Puppet::Application.restart_requested?.should be_false
end
it 'should return true for interrupted?' do
Puppet::Application.interrupted?.should be_true
end
it 'should return false for clear?' do
Puppet::Application.clear?.should be_false
end
end
describe 'when invoking restart!' do
before :each do
Puppet::Application.run_status = nil
Puppet::Application.restart!
end
after :each do
Puppet::Application.run_status = nil
end
it 'should have run_status of :restart_requested' do
Puppet::Application.run_status.should == :restart_requested
end
it 'should return true for restart_requested?' do
Puppet::Application.restart_requested?.should be_true
end
it 'should return false for stop_requested?' do
Puppet::Application.stop_requested?.should be_false
end
it 'should return true for interrupted?' do
Puppet::Application.interrupted?.should be_true
end
it 'should return false for clear?' do
Puppet::Application.clear?.should be_false
end
end
describe 'when performing a controlled_run' do
it 'should not execute block if not :clear?' do
Puppet::Application.run_status = :stop_requested
target = mock 'target'
target.expects(:some_method).never
Puppet::Application.controlled_run do
target.some_method
end
end
it 'should execute block if :clear?' do
Puppet::Application.run_status = nil
target = mock 'target'
target.expects(:some_method).once
Puppet::Application.controlled_run do
target.some_method
end
end
describe 'on POSIX systems', :if => Puppet.features.posix? do
it 'should signal process with HUP after block if restart requested during block execution' do
Timeout::timeout(3) do # if the signal doesn't fire, this causes failure.
has_run = false
old_handler = trap('HUP') { has_run = true }
begin
Puppet::Application.controlled_run do
Puppet::Application.run_status = :restart_requested
end
# Ruby 1.9 uses a separate OS level thread to run the signal
# handler, so we have to poll - ideally, in a way that will kick
# the OS into running other threads - for a while.
#
# You can't just use the Ruby Thread yield thing either, because
# that is just an OS hint, and Linux ... doesn't take that
# seriously. --daniel 2012-03-22
sleep 0.001 while not has_run
ensure
trap('HUP', old_handler)
end
end
end
end
after :each do
Puppet::Application.run_status = nil
end
end
describe "when parsing command-line options" do
before :each do
@app.command_line.stubs(:args).returns([])
Puppet.settings.stubs(:optparse_addargs).returns([])
end
it "should pass the banner to the option parser" do
option_parser = stub "option parser"
option_parser.stubs(:on)
option_parser.stubs(:parse!)
@app.class.instance_eval do
banner "banner"
end
OptionParser.expects(:new).with("banner").returns(option_parser)
@app.parse_options
end
it "should ask OptionParser to parse the command-line argument" do
@app.command_line.stubs(:args).returns(%w{ fake args })
OptionParser.any_instance.expects(:parse!).with(%w{ fake args })
@app.parse_options
end
describe "when using --help" do
it "should call exit" do
@app.stubs(:puts)
expect { @app.handle_help(nil) }.to exit_with 0
end
end
describe "when using --version" do
it "should declare a version option" do
@app.should respond_to(:handle_version)
end
it "should exit after printing the version" do
@app.stubs(:puts)
expect { @app.handle_version(nil) }.to exit_with 0
end
end
describe "when dealing with an argument not declared directly by the application" do
it "should pass it to handle_unknown if this method exists" do
Puppet.settings.stubs(:optparse_addargs).returns([["--not-handled", :REQUIRED]])
@app.expects(:handle_unknown).with("--not-handled", "value").returns(true)
@app.command_line.stubs(:args).returns(["--not-handled", "value"])
@app.parse_options
end
it "should transform boolean option to normal form for Puppet.settings" do
@app.expects(:handle_unknown).with("--option", true)
@app.send(:handlearg, "--[no-]option", true)
end
it "should transform boolean option to no- form for Puppet.settings" do
@app.expects(:handle_unknown).with("--no-option", false)
@app.send(:handlearg, "--[no-]option", false)
end
end
end
describe "when calling default setup" do
before :each do
@app.options.stubs(:[])
end
[ :debug, :verbose ].each do |level|
it "should honor option #{level}" do
@app.options.stubs(:[]).with(level).returns(true)
Puppet::Util::Log.stubs(:newdestination)
@app.setup
Puppet::Util::Log.level.should == (level == :verbose ? :info : :debug)
end
end
it "should honor setdest option" do
@app.options.stubs(:[]).with(:setdest).returns(false)
Puppet::Util::Log.expects(:setup_default)
@app.setup
end
end
describe "when configuring routes" do
include PuppetSpec::Files
before :each do
Puppet::Node.indirection.reset_terminus_class
end
after :each do
Puppet::Node.indirection.reset_terminus_class
end
it "should use the routes specified for only the active application" do
Puppet[:route_file] = tmpfile('routes')
File.open(Puppet[:route_file], 'w') do |f|
f.print <<-ROUTES
test_app:
node:
terminus: exec
other_app:
node:
terminus: plain
catalog:
terminus: invalid
ROUTES
end
@app.configure_indirector_routes
Puppet::Node.indirection.terminus_class.should == 'exec'
end
it "should not fail if the route file doesn't exist" do
Puppet[:route_file] = "/dev/null/non-existent"
expect { @app.configure_indirector_routes }.to_not raise_error
end
it "should raise an error if the routes file is invalid" do
Puppet[:route_file] = tmpfile('routes')
File.open(Puppet[:route_file], 'w') do |f|
f.print <<-ROUTES
invalid : : yaml
ROUTES
end
expect { @app.configure_indirector_routes }.to raise_error
end
end
describe "when running" do
before :each do
@app.stubs(:preinit)
@app.stubs(:setup)
@app.stubs(:parse_options)
end
it "should call preinit" do
@app.stubs(:run_command)
@app.expects(:preinit)
@app.run
end
it "should call parse_options" do
@app.stubs(:run_command)
@app.expects(:parse_options)
@app.run
end
it "should call run_command" do
@app.expects(:run_command)
@app.run
end
it "should call run_command" do
@app.expects(:run_command)
@app.run
end
it "should call main as the default command" do
@app.expects(:main)
@app.run
end
it "should warn and exit if no command can be called" do
Puppet.expects(:err)
expect { @app.run }.to exit_with 1
end
it "should raise an error if dispatch returns no command" do
@app.stubs(:get_command).returns(nil)
Puppet.expects(:err)
expect { @app.run }.to exit_with 1
end
it "should raise an error if dispatch returns an invalid command" do
@app.stubs(:get_command).returns(:this_function_doesnt_exist)
Puppet.expects(:err)
expect { @app.run }.to exit_with 1
end
end
describe "when metaprogramming" do
describe "when calling option" do
it "should create a new method named after the option" do
@app.class.option("--test1","-t") do
end
@app.should respond_to(:handle_test1)
end
it "should transpose in option name any '-' into '_'" do
@app.class.option("--test-dashes-again","-t") do
end
@app.should respond_to(:handle_test_dashes_again)
end
it "should create a new method called handle_test2 with option(\"--[no-]test2\")" do
@app.class.option("--[no-]test2","-t") do
end
@app.should respond_to(:handle_test2)
end
describe "when a block is passed" do
it "should create a new method with it" do
@app.class.option("--[no-]test2","-t") do
raise "I can't believe it, it works!"
end
expect { @app.handle_test2 }.to raise_error
end
it "should declare the option to OptionParser" do
OptionParser.any_instance.stubs(:on)
OptionParser.any_instance.expects(:on).with { |*arg| arg[0] == "--[no-]test3" }
@app.class.option("--[no-]test3","-t") do
end
@app.parse_options
end
it "should pass a block that calls our defined method" do
OptionParser.any_instance.stubs(:on)
OptionParser.any_instance.stubs(:on).with('--test4','-t').yields(nil)
@app.expects(:send).with(:handle_test4, nil)
@app.class.option("--test4","-t") do
end
@app.parse_options
end
end
describe "when no block is given" do
it "should declare the option to OptionParser" do
OptionParser.any_instance.stubs(:on)
OptionParser.any_instance.expects(:on).with("--test4","-t")
@app.class.option("--test4","-t")
@app.parse_options
end
- it "should give to OptionParser a block that adds the the value to the options array" do
+ it "should give to OptionParser a block that adds the value to the options array" do
OptionParser.any_instance.stubs(:on)
OptionParser.any_instance.stubs(:on).with("--test4","-t").yields(nil)
@app.options.expects(:[]=).with(:test4,nil)
@app.class.option("--test4","-t")
@app.parse_options
end
end
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 a818370fc..0952fcd7b 100755
--- a/spec/unit/configurer/downloader_spec.rb
+++ b/spec/unit/configurer/downloader_spec.rb
@@ -1,210 +1,210 @@
#! /usr/bin/env ruby
require 'spec_helper'
require 'puppet/configurer/downloader'
describe Puppet::Configurer::Downloader do
require 'puppet_spec/files'
include PuppetSpec::Files
it "should require a name" do
lambda { Puppet::Configurer::Downloader.new }.should raise_error(ArgumentError)
end
it "should require a path and a source at initialization" do
lambda { Puppet::Configurer::Downloader.new("name") }.should raise_error(ArgumentError)
end
it "should set the name, path and source appropriately" do
dler = Puppet::Configurer::Downloader.new("facts", "path", "source")
dler.name.should == "facts"
dler.path.should == "path"
dler.source.should == "source"
end
describe "when creating the file that does the downloading" do
before do
@dler = Puppet::Configurer::Downloader.new("foo", "path", "source")
end
it "should create a file instance with the right path and source" do
Puppet::Type.type(:file).expects(:new).with { |opts| opts[:path] == "path" and opts[:source] == "source" }
@dler.file
end
it "should tag the file with the downloader name" do
Puppet::Type.type(:file).expects(:new).with { |opts| opts[:tag] == "foo" }
@dler.file
end
it "should always recurse" do
Puppet::Type.type(:file).expects(:new).with { |opts| opts[:recurse] == true }
@dler.file
end
it "should always purge" do
Puppet::Type.type(:file).expects(:new).with { |opts| opts[:purge] == true }
@dler.file
end
it "should never be in noop" do
Puppet::Type.type(:file).expects(:new).with { |opts| opts[:noop] == false }
@dler.file
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 }
@dler.file
end
it "should always set the group to the current GID" do
Process.expects(:gid).returns 61
Puppet::Type.type(:file).expects(:new).with { |opts| opts[:group] == 61 }
@dler.file
end
end
describe "on Windows", :as_platform => :windows do
it "should omit the owner" do
Puppet::Type.type(:file).expects(:new).with { |opts| opts[:owner] == nil }
@dler.file
end
it "should omit the group" 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
Puppet::Type.type(:file).expects(:new).with { |opts| opts[:force] == true }
@dler.file
end
it "should never back up when downloading" do
Puppet::Type.type(:file).expects(:new).with { |opts| opts[:backup] == false }
@dler.file
end
it "should support providing an 'ignore' parameter" do
Puppet::Type.type(:file).expects(:new).with { |opts| opts[:ignore] == [".svn"] }
@dler = Puppet::Configurer::Downloader.new("foo", "path", "source", ".svn")
@dler.file
end
it "should split the 'ignore' parameter on whitespace" do
Puppet::Type.type(:file).expects(:new).with { |opts| opts[:ignore] == %w{.svn CVS} }
@dler = Puppet::Configurer::Downloader.new("foo", "path", "source", ".svn CVS")
@dler.file
end
end
describe "when creating the catalog to do the downloading" do
before do
@path = make_absolute("/download/path")
@dler = Puppet::Configurer::Downloader.new("foo", @path, make_absolute("source"))
end
it "should create a catalog and add the file to it" do
catalog = @dler.catalog
catalog.resources.size.should == 1
catalog.resources.first.class.should == Puppet::Type::File
catalog.resources.first.name.should == @path
end
it "should specify that it is not managing a host catalog" do
@dler.catalog.host_config.should == false
end
end
describe "when downloading" do
before do
@dl_name = tmpfile("downloadpath")
source_name = tmpfile("source")
File.open(source_name, 'w') {|f| f.write('hola mundo') }
@dler = Puppet::Configurer::Downloader.new("foo", @dl_name, source_name)
end
it "should not skip downloaded resources when filtering on tags" do
Puppet[:tags] = 'maytag'
@dler.evaluate
- Puppet::FileSystem::File.exist?(@dl_name).should be_true
+ Puppet::FileSystem.exist?(@dl_name).should be_true
end
it "should log that it is downloading" do
Puppet.expects(:info)
Timeout.stubs(:timeout)
@dler.evaluate
end
it "should set a timeout for the download using the `configtimeout` setting" do
Puppet[:configtimeout] = 50
Timeout.expects(:timeout).with(50)
@dler.evaluate
end
it "should apply the catalog within the timeout block" do
catalog = mock 'catalog'
@dler.expects(:catalog).returns(catalog)
Timeout.expects(:timeout).yields
catalog.expects(:apply)
@dler.evaluate
end
it "should return all changed file paths" do
trans = mock 'transaction'
catalog = mock 'catalog'
@dler.expects(:catalog).returns(catalog)
catalog.expects(:apply).yields(trans)
Timeout.expects(:timeout).yields
resource = mock 'resource'
resource.expects(:[]).with(:path).returns "/changed/file"
trans.expects(:changed?).returns([resource])
@dler.evaluate.should == %w{/changed/file}
end
it "should yield the resources if a block is given" do
trans = mock 'transaction'
catalog = mock 'catalog'
@dler.expects(:catalog).returns(catalog)
catalog.expects(:apply).yields(trans)
Timeout.expects(:timeout).yields
resource = mock 'resource'
resource.expects(:[]).with(:path).returns "/changed/file"
trans.expects(:changed?).returns([resource])
yielded = nil
@dler.evaluate { |r| yielded = r }
yielded.should == resource
end
it "should catch and log exceptions" do
Puppet.expects(:err)
Timeout.stubs(:timeout).raises(Puppet::Error, "testing")
lambda { @dler.evaluate }.should_not raise_error
end
end
end
diff --git a/spec/unit/configurer/fact_handler_spec.rb b/spec/unit/configurer/fact_handler_spec.rb
index a92ad7d4f..3e3c033ae 100755
--- a/spec/unit/configurer/fact_handler_spec.rb
+++ b/spec/unit/configurer/fact_handler_spec.rb
@@ -1,100 +1,89 @@
#! /usr/bin/env ruby
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
+require 'matchers/json'
class FactHandlerTester
include Puppet::Configurer::FactHandler
def reload_facter
# don't want to do this in tests
end
end
describe Puppet::Configurer::FactHandler do
+ include JSONMatchers
+
before :each do
@facthandler = FactHandlerTester.new
Puppet::Node::Facts.indirection.terminus_class = :memory
end
describe "when finding facts" do
it "should use the node name value to retrieve the facts" do
foo_facts = Puppet::Node::Facts.new('foo')
bar_facts = Puppet::Node::Facts.new('bar')
Puppet::Node::Facts.indirection.save(foo_facts)
Puppet::Node::Facts.indirection.save(bar_facts)
Puppet[:certname] = 'foo'
Puppet[:node_name_value] = 'bar'
@facthandler.find_facts.should == bar_facts
end
it "should set the facts name based on the node_name_fact" do
facts = Puppet::Node::Facts.new(Puppet[:node_name_value], 'my_name_fact' => 'other_node_name')
Puppet::Node::Facts.indirection.save(facts)
Puppet[:node_name_fact] = 'my_name_fact'
@facthandler.find_facts.name.should == 'other_node_name'
end
it "should set the node_name_value based on the node_name_fact" do
facts = Puppet::Node::Facts.new(Puppet[:node_name_value], 'my_name_fact' => 'other_node_name')
Puppet::Node::Facts.indirection.save(facts)
Puppet[:node_name_fact] = 'my_name_fact'
@facthandler.find_facts
Puppet[:node_name_value].should == 'other_node_name'
end
it "should fail if finding facts fails" do
Puppet::Node::Facts.indirection.expects(:find).raises RuntimeError
expect { @facthandler.find_facts }.to raise_error(Puppet::Error, /Could not retrieve local facts/)
end
it "should only load fact plugins once" do
Puppet::Node::Facts.indirection.expects(:find).once
@facthandler.find_facts
end
end
it "should serialize and CGI escape the fact values for uploading" do
facts = Puppet::Node::Facts.new(Puppet[:node_name_value], 'my_name_fact' => 'other_node_name')
Puppet::Node::Facts.indirection.save(facts)
text = CGI.escape(@facthandler.find_facts.render(:pson))
@facthandler.facts_for_uploading.should == {:facts_format => :pson, :facts => text}
end
it "should properly accept facts containing a '+'" do
facts = Puppet::Node::Facts.new('foo', 'afact' => 'a+b')
Puppet::Node::Facts.indirection.save(facts)
text = CGI.escape(@facthandler.find_facts.render(:pson))
@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
+ it "should generate valid facts data against the facts schema" 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]))
+ expect(CGI.unescape(@facthandler.facts_for_uploading[:facts])).to validate_against('api/schemas/facts.json')
end
end
diff --git a/spec/unit/configurer_spec.rb b/spec/unit/configurer_spec.rb
index 7732bea08..4157ba8e3 100755
--- a/spec/unit/configurer_spec.rb
+++ b/spec/unit/configurer_spec.rb
@@ -1,655 +1,655 @@
#! /usr/bin/env ruby
require 'spec_helper'
require 'puppet/configurer'
describe Puppet::Configurer do
before do
Puppet.settings.stubs(:use).returns(true)
@agent = Puppet::Configurer.new
@agent.stubs(:init_storage)
Puppet::Util::Storage.stubs(:store)
Puppet[:server] = "puppetmaster"
Puppet[:report] = true
end
it "should include the Plugin Handler module" do
Puppet::Configurer.ancestors.should be_include(Puppet::Configurer::PluginHandler)
end
it "should include the Fact Handler module" do
Puppet::Configurer.ancestors.should be_include(Puppet::Configurer::FactHandler)
end
describe "when executing a pre-run hook" do
it "should do nothing if the hook is set to an empty string" do
Puppet.settings[:prerun_command] = ""
Puppet::Util.expects(:exec).never
@agent.execute_prerun_command
end
it "should execute any pre-run command provided via the 'prerun_command' setting" do
Puppet.settings[:prerun_command] = "/my/command"
Puppet::Util::Execution.expects(:execute).with(["/my/command"]).raises(Puppet::ExecutionFailure, "Failed")
@agent.execute_prerun_command
end
it "should fail if the command fails" do
Puppet.settings[:prerun_command] = "/my/command"
Puppet::Util::Execution.expects(:execute).with(["/my/command"]).raises(Puppet::ExecutionFailure, "Failed")
@agent.execute_prerun_command.should be_false
end
end
describe "when executing a post-run hook" do
it "should do nothing if the hook is set to an empty string" do
Puppet.settings[:postrun_command] = ""
Puppet::Util.expects(:exec).never
@agent.execute_postrun_command
end
it "should execute any post-run command provided via the 'postrun_command' setting" do
Puppet.settings[:postrun_command] = "/my/command"
Puppet::Util::Execution.expects(:execute).with(["/my/command"]).raises(Puppet::ExecutionFailure, "Failed")
@agent.execute_postrun_command
end
it "should fail if the command fails" do
Puppet.settings[:postrun_command] = "/my/command"
Puppet::Util::Execution.expects(:execute).with(["/my/command"]).raises(Puppet::ExecutionFailure, "Failed")
@agent.execute_postrun_command.should be_false
end
end
describe "when executing a catalog run" do
before do
Puppet.settings.stubs(:use).returns(true)
@agent.stubs(:download_plugins)
Puppet::Node::Facts.indirection.terminus_class = :memory
@facts = Puppet::Node::Facts.new(Puppet[:node_name_value])
Puppet::Node::Facts.indirection.save(@facts)
@catalog = Puppet::Resource::Catalog.new
@catalog.stubs(:to_ral).returns(@catalog)
Puppet::Resource::Catalog.indirection.terminus_class = :rest
Puppet::Resource::Catalog.indirection.stubs(:find).returns(@catalog)
@agent.stubs(:send_report)
@agent.stubs(:save_last_run_summary)
Puppet::Util::Log.stubs(:close_all)
end
after :all do
Puppet::Node::Facts.indirection.reset_terminus_class
Puppet::Resource::Catalog.indirection.reset_terminus_class
end
it "should initialize storage" do
Puppet::Util::Storage.expects(:load)
@agent.run
end
it "downloads plugins when told" do
@agent.expects(:download_plugins)
@agent.run(:pluginsync => true)
end
it "does not download plugins when told" do
@agent.expects(:download_plugins).never
@agent.run(:pluginsync => false)
end
it "should carry on when it can't fetch its node definition" do
error = Net::HTTPError.new(400, 'dummy server communication error')
Puppet::Node.indirection.expects(:find).raises(error)
@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
@agent.run
end
it "should respect node_name_fact when setting the host on a report" do
Puppet[:node_name_fact] = 'my_name_fact'
@facts.values = {'my_name_fact' => 'node_name_from_fact'}
report = Puppet::Transaction::Report.new("apply")
@agent.run(:report => report)
report.host.should == 'node_name_from_fact'
end
it "should pass the new report to the catalog" do
report = Puppet::Transaction::Report.new("apply")
Puppet::Transaction::Report.stubs(:new).returns report
@catalog.expects(:apply).with{|options| options[:report] == report}
@agent.run
end
it "should use the provided report if it was passed one" do
report = Puppet::Transaction::Report.new("apply")
@catalog.expects(:apply).with {|options| options[:report] == report}
@agent.run(:report => report)
end
it "should set the report as a log destination" do
report = Puppet::Transaction::Report.new("apply")
report.expects(:<<).with(instance_of(Puppet::Util::Log)).at_least_once
@agent.run(:report => report)
end
it "should retrieve the catalog" do
@agent.expects(:retrieve_catalog)
@agent.run
end
it "should log a failure and do nothing if no catalog can be retrieved" do
@agent.expects(:retrieve_catalog).returns nil
Puppet.expects(:err).with "Could not retrieve catalog; skipping run"
@agent.run
end
it "should apply the catalog with all options to :run" do
@agent.expects(:retrieve_catalog).returns @catalog
@catalog.expects(:apply).with { |args| args[:one] == true }
@agent.run :one => true
end
it "should accept a catalog and use it instead of retrieving a different one" do
@agent.expects(:retrieve_catalog).never
@catalog.expects(:apply)
@agent.run :one => true, :catalog => @catalog
end
it "should benchmark how long it takes to apply the catalog" do
@agent.expects(:benchmark).with(:notice, "Finished catalog run")
@agent.expects(:retrieve_catalog).returns @catalog
@catalog.expects(:apply).never # because we're not yielding
@agent.run
end
it "should execute post-run hooks after the run" do
@agent.expects(:execute_postrun_command)
@agent.run
end
it "should send the report" do
report = Puppet::Transaction::Report.new("apply")
Puppet::Transaction::Report.expects(:new).returns(report)
@agent.expects(:send_report).with(report)
@agent.run
end
it "should send the transaction report even if the catalog could not be retrieved" do
@agent.expects(:retrieve_catalog).returns nil
report = Puppet::Transaction::Report.new("apply")
Puppet::Transaction::Report.expects(:new).returns(report)
@agent.expects(:send_report)
@agent.run
end
it "should send the transaction report even if there is a failure" do
@agent.expects(:retrieve_catalog).raises "whatever"
report = Puppet::Transaction::Report.new("apply")
Puppet::Transaction::Report.expects(:new).returns(report)
@agent.expects(:send_report)
@agent.run.should be_nil
end
it "should remove the report as a log destination when the run is finished" do
report = Puppet::Transaction::Report.new("apply")
Puppet::Transaction::Report.expects(:new).returns(report)
@agent.run
Puppet::Util::Log.destinations.should_not include(report)
end
it "should return the report exit_status as the result of the run" do
report = Puppet::Transaction::Report.new("apply")
Puppet::Transaction::Report.expects(:new).returns(report)
report.expects(:exit_status).returns(1234)
@agent.run.should == 1234
end
it "should send the transaction report even if the pre-run command fails" do
report = Puppet::Transaction::Report.new("apply")
Puppet::Transaction::Report.expects(:new).returns(report)
Puppet.settings[:prerun_command] = "/my/command"
Puppet::Util::Execution.expects(:execute).with(["/my/command"]).raises(Puppet::ExecutionFailure, "Failed")
@agent.expects(:send_report)
@agent.run.should be_nil
end
it "should include the pre-run command failure in the report" do
report = Puppet::Transaction::Report.new("apply")
Puppet::Transaction::Report.expects(:new).returns(report)
Puppet.settings[:prerun_command] = "/my/command"
Puppet::Util::Execution.expects(:execute).with(["/my/command"]).raises(Puppet::ExecutionFailure, "Failed")
@agent.run.should be_nil
report.logs.find { |x| x.message =~ /Could not run command from prerun_command/ }.should be
end
it "should send the transaction report even if the post-run command fails" do
report = Puppet::Transaction::Report.new("apply")
Puppet::Transaction::Report.expects(:new).returns(report)
Puppet.settings[:postrun_command] = "/my/command"
Puppet::Util::Execution.expects(:execute).with(["/my/command"]).raises(Puppet::ExecutionFailure, "Failed")
@agent.expects(:send_report)
@agent.run.should be_nil
end
it "should include the post-run command failure in the report" do
report = Puppet::Transaction::Report.new("apply")
Puppet::Transaction::Report.expects(:new).returns(report)
Puppet.settings[:postrun_command] = "/my/command"
Puppet::Util::Execution.expects(:execute).with(["/my/command"]).raises(Puppet::ExecutionFailure, "Failed")
report.expects(:<<).with { |log| log.message.include?("Could not run command from postrun_command") }
@agent.run.should be_nil
end
it "should execute post-run command even if the pre-run command fails" do
Puppet.settings[:prerun_command] = "/my/precommand"
Puppet.settings[:postrun_command] = "/my/postcommand"
Puppet::Util::Execution.expects(:execute).with(["/my/precommand"]).raises(Puppet::ExecutionFailure, "Failed")
Puppet::Util::Execution.expects(:execute).with(["/my/postcommand"])
@agent.run.should be_nil
end
it "should finalize the report" do
report = Puppet::Transaction::Report.new("apply")
Puppet::Transaction::Report.expects(:new).returns(report)
report.expects(:finalize_report)
@agent.run
end
it "should not apply the catalog if the pre-run command fails" do
report = Puppet::Transaction::Report.new("apply")
Puppet::Transaction::Report.expects(:new).returns(report)
Puppet.settings[:prerun_command] = "/my/command"
Puppet::Util::Execution.expects(:execute).with(["/my/command"]).raises(Puppet::ExecutionFailure, "Failed")
@catalog.expects(:apply).never()
@agent.expects(:send_report)
@agent.run.should be_nil
end
it "should apply the catalog, send the report, and return nil if the post-run command fails" do
report = Puppet::Transaction::Report.new("apply")
Puppet::Transaction::Report.expects(:new).returns(report)
Puppet.settings[:postrun_command] = "/my/command"
Puppet::Util::Execution.expects(:execute).with(["/my/command"]).raises(Puppet::ExecutionFailure, "Failed")
@catalog.expects(:apply)
@agent.expects(:send_report)
@agent.run.should be_nil
end
it "should refetch the catalog if the server specifies a new environment in the catalog" do
@catalog.stubs(:environment).returns("second_env")
@agent.expects(:retrieve_catalog).returns(@catalog).twice
@agent.run
end
it "should change the environment setting if the server specifies a new environment in the catalog" do
@catalog.stubs(:environment).returns("second_env")
@agent.run
@agent.environment.should == "second_env"
end
it "should clear the global caches" do
$env_module_directories = false
@agent.run
$env_module_directories.should == nil
end
describe "when not using a REST terminus for catalogs" do
it "should not pass any facts when retrieving the catalog" do
Puppet::Resource::Catalog.indirection.terminus_class = :compiler
@agent.expects(:facts_for_uploading).never
Puppet::Resource::Catalog.indirection.expects(:find).with { |name, options|
options[:facts].nil?
}.returns @catalog
@agent.run
end
end
describe "when using a REST terminus for catalogs" do
it "should pass the prepared facts and the facts format as arguments when retrieving the catalog" do
Puppet::Resource::Catalog.indirection.terminus_class = :rest
@agent.expects(:facts_for_uploading).returns(:facts => "myfacts", :facts_format => :foo)
Puppet::Resource::Catalog.indirection.expects(:find).with { |name, options|
options[:facts] == "myfacts" and options[:facts_format] == :foo
}.returns @catalog
@agent.run
end
end
end
describe "when sending a report" do
include PuppetSpec::Files
before do
Puppet.settings.stubs(:use).returns(true)
@configurer = Puppet::Configurer.new
Puppet[:lastrunfile] = tmpfile('last_run_file')
@report = Puppet::Transaction::Report.new("apply")
Puppet[:reports] = "none"
end
it "should print a report summary if configured to do so" do
Puppet.settings[:summarize] = true
@report.expects(:summary).returns "stuff"
@configurer.expects(:puts).with("stuff")
@configurer.send_report(@report)
end
it "should not print a report summary if not configured to do so" do
Puppet.settings[:summarize] = false
@configurer.expects(:puts).never
@configurer.send_report(@report)
end
it "should save the report if reporting is enabled" do
Puppet.settings[:report] = true
Puppet::Transaction::Report.indirection.expects(:save).with(@report, nil, instance_of(Hash))
@configurer.send_report(@report)
end
it "should not save the report if reporting is disabled" do
Puppet.settings[:report] = false
Puppet::Transaction::Report.indirection.expects(:save).with(@report, nil, instance_of(Hash)).never
@configurer.send_report(@report)
end
it "should save the last run summary if reporting is enabled" do
Puppet.settings[:report] = true
@configurer.expects(:save_last_run_summary).with(@report)
@configurer.send_report(@report)
end
it "should save the last run summary if reporting is disabled" do
Puppet.settings[:report] = false
@configurer.expects(:save_last_run_summary).with(@report)
@configurer.send_report(@report)
end
it "should log but not fail if saving the report fails" do
Puppet.settings[:report] = true
Puppet::Transaction::Report.indirection.expects(:save).raises("whatever")
Puppet.expects(:err)
lambda { @configurer.send_report(@report) }.should_not raise_error
end
end
describe "when saving the summary report file" do
include PuppetSpec::Files
before do
Puppet.settings.stubs(:use).returns(true)
@configurer = Puppet::Configurer.new
@report = stub 'report', :raw_summary => {}
Puppet[:lastrunfile] = tmpfile('last_run_file')
end
it "should write the last run file" do
@configurer.save_last_run_summary(@report)
- Puppet::FileSystem::File.exist?(Puppet[:lastrunfile]).should be_true
+ Puppet::FileSystem.exist?(Puppet[:lastrunfile]).should be_true
end
it "should write the raw summary as yaml" do
@report.expects(:raw_summary).returns("summary")
@configurer.save_last_run_summary(@report)
File.read(Puppet[:lastrunfile]).should == YAML.dump("summary")
end
it "should log but not fail if saving the last run summary fails" do
# The mock will raise an exception on any method used. This should
# simulate a nice hard failure from the underlying OS for us.
fh = Class.new(Object) do
def method_missing(*args)
raise "failed to do #{args[0]}"
end
end.new
Puppet::Util.expects(:replace_file).yields(fh)
Puppet.expects(:err)
expect { @configurer.save_last_run_summary(@report) }.to_not raise_error
end
it "should create the last run file with the correct mode" do
Puppet.settings.setting(:lastrunfile).expects(:mode).returns('664')
@configurer.save_last_run_summary(@report)
if Puppet::Util::Platform.windows?
require 'puppet/util/windows/security'
mode = Puppet::Util::Windows::Security.get_mode(Puppet[:lastrunfile])
else
- mode = Puppet::FileSystem::File.new(Puppet[:lastrunfile]).stat.mode
+ mode = Puppet::FileSystem.stat(Puppet[:lastrunfile]).mode
end
(mode & 0777).should == 0664
end
it "should report invalid last run file permissions" do
Puppet.settings.setting(:lastrunfile).expects(:mode).returns('892')
Puppet.expects(:err).with(regexp_matches(/Could not save last run local report.*892 is invalid/))
@configurer.save_last_run_summary(@report)
end
end
describe "when retrieving a catalog" do
before do
Puppet.settings.stubs(:use).returns(true)
@agent.stubs(:facts_for_uploading).returns({})
@catalog = Puppet::Resource::Catalog.new
# this is the default when using a Configurer instance
Puppet::Resource::Catalog.indirection.stubs(:terminus_class).returns :rest
@agent.stubs(:convert_catalog).returns @catalog
end
describe "and configured to only retrieve a catalog from the cache" do
before do
Puppet.settings[:use_cached_catalog] = true
end
it "should first look in the cache for a catalog" do
Puppet::Resource::Catalog.indirection.expects(:find).with { |name, options| options[:ignore_terminus] == true }.returns @catalog
Puppet::Resource::Catalog.indirection.expects(:find).with { |name, options| options[:ignore_cache] == true }.never
@agent.retrieve_catalog({}).should == @catalog
end
it "should compile a new catalog if none is found in the cache" do
Puppet::Resource::Catalog.indirection.expects(:find).with { |name, options| options[:ignore_terminus] == true }.returns nil
Puppet::Resource::Catalog.indirection.expects(:find).with { |name, options| options[:ignore_cache] == true }.returns @catalog
@agent.retrieve_catalog({}).should == @catalog
end
end
it "should use the Catalog class to get its catalog" do
Puppet::Resource::Catalog.indirection.expects(:find).returns @catalog
@agent.retrieve_catalog({})
end
it "should use its node_name_value to retrieve the catalog" do
Facter.stubs(:value).returns "eh"
Puppet.settings[:node_name_value] = "myhost.domain.com"
Puppet::Resource::Catalog.indirection.expects(:find).with { |name, options| name == "myhost.domain.com" }.returns @catalog
@agent.retrieve_catalog({})
end
it "should default to returning a catalog retrieved directly from the server, skipping the cache" do
Puppet::Resource::Catalog.indirection.expects(:find).with { |name, options| options[:ignore_cache] == true }.returns @catalog
@agent.retrieve_catalog({}).should == @catalog
end
it "should log and return the cached catalog when no catalog can be retrieved from the server" do
Puppet::Resource::Catalog.indirection.expects(:find).with { |name, options| options[:ignore_cache] == true }.returns nil
Puppet::Resource::Catalog.indirection.expects(:find).with { |name, options| options[:ignore_terminus] == true }.returns @catalog
Puppet.expects(:notice)
@agent.retrieve_catalog({}).should == @catalog
end
it "should not look in the cache for a catalog if one is returned from the server" do
Puppet::Resource::Catalog.indirection.expects(:find).with { |name, options| options[:ignore_cache] == true }.returns @catalog
Puppet::Resource::Catalog.indirection.expects(:find).with { |name, options| options[:ignore_terminus] == true }.never
@agent.retrieve_catalog({}).should == @catalog
end
it "should return the cached catalog when retrieving the remote catalog throws an exception" do
Puppet::Resource::Catalog.indirection.expects(:find).with { |name, options| options[:ignore_cache] == true }.raises "eh"
Puppet::Resource::Catalog.indirection.expects(:find).with { |name, options| options[:ignore_terminus] == true }.returns @catalog
@agent.retrieve_catalog({}).should == @catalog
end
it "should log and return nil if no catalog can be retrieved from the server and :usecacheonfailure is disabled" do
Puppet[:usecacheonfailure] = false
Puppet::Resource::Catalog.indirection.expects(:find).with { |name, options| options[:ignore_cache] == true }.returns nil
Puppet.expects(:warning)
@agent.retrieve_catalog({}).should be_nil
end
it "should return nil if no cached catalog is available and no catalog can be retrieved from the server" do
Puppet::Resource::Catalog.indirection.expects(:find).with { |name, options| options[:ignore_cache] == true }.returns nil
Puppet::Resource::Catalog.indirection.expects(:find).with { |name, options| options[:ignore_terminus] == true }.returns nil
@agent.retrieve_catalog({}).should be_nil
end
it "should convert the catalog before returning" do
Puppet::Resource::Catalog.indirection.stubs(:find).returns @catalog
@agent.expects(:convert_catalog).with { |cat, dur| cat == @catalog }.returns "converted catalog"
@agent.retrieve_catalog({}).should == "converted catalog"
end
it "should return nil if there is an error while retrieving the catalog" do
Puppet::Resource::Catalog.indirection.expects(:find).at_least_once.raises "eh"
@agent.retrieve_catalog({}).should be_nil
end
end
describe "when converting the catalog" do
before do
Puppet.settings.stubs(:use).returns(true)
@catalog = Puppet::Resource::Catalog.new
@oldcatalog = stub 'old_catalog', :to_ral => @catalog
end
it "should convert the catalog to a RAL-formed catalog" do
@oldcatalog.expects(:to_ral).returns @catalog
@agent.convert_catalog(@oldcatalog, 10).should equal(@catalog)
end
it "should finalize the catalog" do
@catalog.expects(:finalize)
@agent.convert_catalog(@oldcatalog, 10)
end
it "should record the passed retrieval time with the RAL catalog" do
@catalog.expects(:retrieval_duration=).with 10
@agent.convert_catalog(@oldcatalog, 10)
end
it "should write the RAL catalog's class file" do
@catalog.expects(:write_class_file)
@agent.convert_catalog(@oldcatalog, 10)
end
it "should write the RAL catalog's resource file" do
@catalog.expects(:write_resource_file)
@agent.convert_catalog(@oldcatalog, 10)
end
end
end
diff --git a/spec/unit/confine/exists_spec.rb b/spec/unit/confine/exists_spec.rb
index 87959fe34..a2a575bd2 100755
--- a/spec/unit/confine/exists_spec.rb
+++ b/spec/unit/confine/exists_spec.rb
@@ -1,80 +1,80 @@
#! /usr/bin/env ruby
require 'spec_helper'
require 'puppet/confine/exists'
describe Puppet::Confine::Exists do
before do
@confine = Puppet::Confine::Exists.new("/my/file")
@confine.label = "eh"
end
it "should be named :exists" do
Puppet::Confine::Exists.name.should == :exists
end
it "should not pass if exists is nil" do
confine = Puppet::Confine::Exists.new(nil)
confine.label = ":exists => nil"
confine.expects(:pass?).with(nil)
confine.should_not be_valid
end
it "should use the 'pass?' method to test validity" do
@confine.expects(:pass?).with("/my/file")
@confine.valid?
end
it "should return false if the value is false" do
@confine.pass?(false).should be_false
end
it "should return false if the value does not point to a file" do
- Puppet::FileSystem::File.expects(:exist?).with("/my/file").returns false
+ Puppet::FileSystem.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
- Puppet::FileSystem::File.expects(:exist?).with("/my/file").returns true
+ Puppet::FileSystem.expects(:exist?).with("/my/file").returns true
@confine.pass?("/my/file").should be_true
end
it "should produce a message saying that a file is missing" do
@confine.message("/my/file").should be_include("does not exist")
end
describe "and the confine is for binaries" do
before { @confine.stubs(:for_binary).returns true }
it "should use its 'which' method to look up the full path of the file" do
@confine.expects(:which).returns nil
@confine.pass?("/my/file")
end
it "should return false if no executable can be found" do
@confine.expects(:which).with("/my/file").returns nil
@confine.pass?("/my/file").should be_false
end
it "should return true if the executable can be found" do
@confine.expects(:which).with("/my/file").returns "/my/file"
@confine.pass?("/my/file").should be_true
end
end
it "should produce a summary containing all missing files" do
- 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
+ Puppet::FileSystem.stubs(:exist?).returns true
+ Puppet::FileSystem.expects(:exist?).with("/two").returns false
+ Puppet::FileSystem.expects(:exist?).with("/four").returns false
confine = Puppet::Confine::Exists.new %w{/one /two /three /four}
confine.summary.should == %w{/two /four}
end
it "should summarize multiple instances by returning a flattened array of their summaries" do
c1 = mock '1', :summary => %w{one}
c2 = mock '2', :summary => %w{two}
c3 = mock '3', :summary => %w{three}
Puppet::Confine::Exists.summarize([c1, c2, c3]).should == %w{one two three}
end
end
diff --git a/spec/unit/context/trusted_information_spec.rb b/spec/unit/context/trusted_information_spec.rb
new file mode 100644
index 000000000..c93d7f20e
--- /dev/null
+++ b/spec/unit/context/trusted_information_spec.rb
@@ -0,0 +1,124 @@
+require 'spec_helper'
+
+describe Puppet::Context::TrustedInformation do
+ let(:key) do
+ key = Puppet::SSL::Key.new("myname")
+ key.generate
+ key
+ end
+
+ let(:csr) do
+ csr = Puppet::SSL::CertificateRequest.new("csr")
+ csr.generate(key, :extension_requests => {
+ '1.3.6.1.4.1.15.1.2.1' => 'Ignored CSR extension',
+
+ '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',
+ })
+ csr
+ end
+
+ let(:cert) do
+ Puppet::SSL::Certificate.from_instance(Puppet::SSL::CertificateFactory.build('ca', csr, csr.content, 1))
+ end
+
+ context "when remote" do
+ it "has no cert information when it isn't authenticated" do
+ trusted = Puppet::Context::TrustedInformation.remote(false, 'ignored', nil)
+
+ expect(trusted.authenticated).to eq(false)
+ expect(trusted.certname).to be_nil
+ expect(trusted.extensions).to eq({})
+ end
+
+ it "is remote and has certificate information when it is authenticated" do
+ trusted = Puppet::Context::TrustedInformation.remote(true, 'cert name', cert)
+
+ expect(trusted.authenticated).to eq('remote')
+ expect(trusted.certname).to eq('cert name')
+ expect(trusted.extensions).to eq({
+ '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',
+ })
+ end
+
+ it "is remote but lacks certificate information when it is authenticated" do
+ Puppet.expects(:info).once.with("TrustedInformation expected a certificate, but none was given.")
+
+ trusted = Puppet::Context::TrustedInformation.remote(true, 'cert name', nil)
+
+ expect(trusted.authenticated).to eq('remote')
+ expect(trusted.certname).to eq('cert name')
+ expect(trusted.extensions).to eq({})
+ end
+ end
+
+ context "when local" do
+ it "is authenticated local with the nodes clientcert" do
+ node = Puppet::Node.new('testing', :parameters => { 'clientcert' => 'cert name' })
+
+ trusted = Puppet::Context::TrustedInformation.local(node)
+
+ expect(trusted.authenticated).to eq('local')
+ expect(trusted.certname).to eq('cert name')
+ expect(trusted.extensions).to eq({})
+ end
+
+ it "is authenticated local with no clientcert when there is no node" do
+ trusted = Puppet::Context::TrustedInformation.local(nil)
+
+ expect(trusted.authenticated).to eq('local')
+ expect(trusted.certname).to be_nil
+ expect(trusted.extensions).to eq({})
+ end
+ end
+
+ it "converts itself to a hash" do
+ trusted = Puppet::Context::TrustedInformation.remote(true, 'cert name', cert)
+
+ expect(trusted.to_h).to eq({
+ 'authenticated' => 'remote',
+ 'certname' => 'cert name',
+ 'extensions' => {
+ '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',
+ }
+ })
+ end
+
+ it "freezes the hash" do
+ trusted = Puppet::Context::TrustedInformation.remote(true, 'cert name', cert)
+
+ expect(trusted.to_h).to be_deeply_frozen
+ end
+
+ matcher :be_deeply_frozen do
+ match do |actual|
+ unfrozen_items(actual).empty?
+ end
+
+ failure_message_for_should do |actual|
+ "expected all items to be frozen but <#{unfrozen_items(actual).join(', ')}> was not"
+ end
+
+ define_method :unfrozen_items do |actual|
+ unfrozen = []
+ stack = [actual]
+ while item = stack.pop
+ if !item.frozen?
+ unfrozen.push(item)
+ end
+
+ case item
+ when Hash
+ stack.concat(item.keys)
+ stack.concat(item.values)
+ when Array
+ stack.concat(item)
+ end
+ end
+
+ unfrozen
+ end
+ end
+end
diff --git a/spec/unit/context_spec.rb b/spec/unit/context_spec.rb
new file mode 100644
index 000000000..505192f18
--- /dev/null
+++ b/spec/unit/context_spec.rb
@@ -0,0 +1,74 @@
+require 'spec_helper'
+
+describe Puppet::Context do
+ let(:context) { Puppet::Context.new({ :testing => "value" }) }
+
+ context "with the implicit test_helper.rb pushed context" do
+ it "fails to lookup a value that does not exist" do
+ expect { context.lookup("a") }.to raise_error(Puppet::Context::UndefinedBindingError)
+ end
+
+ it "calls a provided block for a default value when none is found" do
+ expect(context.lookup("a") { "default" }).to eq("default")
+ end
+
+ it "behaves as if pushed a {} if you push nil" do
+ context.push(nil)
+ expect(context.lookup(:testing)).to eq("value")
+ context.pop
+ end
+
+ it "fails if you try to pop off the top of the stack" do
+ expect { context.pop }.to raise_error(Puppet::Context::StackUnderflow)
+ end
+ end
+
+ describe "with additional context" do
+ before :each do
+ context.push("a" => 1)
+ end
+
+ it "holds values for later lookup" do
+ expect(context.lookup("a")).to eq(1)
+ end
+
+ it "allows rebinding values in a nested context" do
+ inner = nil
+ context.override("a" => 2) do
+ inner = context.lookup("a")
+ end
+
+ expect(inner).to eq(2)
+ end
+
+ it "outer bindings are available in an overridden context" do
+ inner_a = nil
+ inner_b = nil
+ context.override("b" => 2) do
+ inner_a = context.lookup("a")
+ inner_b = context.lookup("b")
+ end
+
+ expect(inner_a).to eq(1)
+ expect(inner_b).to eq(2)
+ end
+
+ it "overridden bindings do not exist outside of the override" do
+ context.override("a" => 2) do
+ end
+
+ expect(context.lookup("a")).to eq(1)
+ end
+
+ it "overridden bindings do not exist outside of the override even when leaving via an error" do
+ begin
+ context.override("a" => 2) do
+ raise "this should still cause the bindings to leave"
+ end
+ rescue
+ end
+
+ expect(context.lookup("a")).to eq(1)
+ end
+ end
+end
diff --git a/spec/unit/environments_spec.rb b/spec/unit/environments_spec.rb
new file mode 100644
index 000000000..3a1f81628
--- /dev/null
+++ b/spec/unit/environments_spec.rb
@@ -0,0 +1,126 @@
+require 'spec_helper'
+require 'puppet/environments'
+require 'puppet/file_system'
+require 'matchers/include'
+
+describe Puppet::Environments do
+ include Matchers::Include
+ include PuppetSpec::Files
+
+ FS = Puppet::FileSystem
+
+ describe "directories loader" do
+
+ RSpec::Matchers.define :environment do |name|
+ match do |env|
+ env.name == name &&
+ (!@manifest || @manifest == env.manifest) &&
+ (!@modulepath || @modulepath == env.modulepath)
+ end
+
+ chain :with_manifest do |manifest|
+ @manifest = manifest
+ end
+
+ chain :with_modulepath do |modulepath|
+ @modulepath = modulepath
+ end
+
+ description do
+ "environment #{expected}" +
+ (@manifest ? " with manifest #{@manifest}" : "") +
+ (@modulepath ? " with modulepath [#{@modulepath.join(', ')}]" : "")
+ end
+
+ failure_message_for_should do |env|
+ "expected <#{env.name}: modulepath = [#{env.modulepath.join(', ')}], manifest = #{env.manifest}> to be #{description}"
+ end
+ end
+
+ def loader_from(options, &block)
+ FS.overlay(*options[:filesystem]) do
+ yield Puppet::Environments::Directories.new(
+ options[:directory],
+ options[:modulepath] || []
+ )
+ end
+ end
+
+ it "lists environments" do
+ global_path_1_location = File.expand_path("global_path_1")
+ global_path_2_location = File.expand_path("global_path_2")
+ global_path_1 = FS::MemoryFile.a_directory(global_path_1_location)
+ global_path_2 = FS::MemoryFile.a_directory(global_path_2_location)
+
+ envdir = FS::MemoryFile.a_directory(File.expand_path("envdir"), [
+ FS::MemoryFile.a_directory("env1", [
+ FS::MemoryFile.a_directory("modules"),
+ FS::MemoryFile.a_directory("manifests"),
+ ]),
+ FS::MemoryFile.a_directory("env2")
+ ])
+
+ loader_from(:filesystem => [envdir, global_path_1, global_path_2],
+ :directory => envdir,
+ :modulepath => [global_path_1_location, global_path_2_location]) do |loader|
+ expect(loader.list).to include_in_any_order(
+ environment(:env1).
+ with_manifest("#{FS.path_string(envdir)}/env1/manifests").
+ with_modulepath(["#{FS.path_string(envdir)}/env1/modules",
+ global_path_1_location,
+ global_path_2_location]),
+ environment(:env2))
+ end
+ end
+
+ it "does not list files" do
+ envdir = FS::MemoryFile.a_directory("envdir", [
+ FS::MemoryFile.a_regular_file_containing("foo", ''),
+ FS::MemoryFile.a_directory("env1"),
+ FS::MemoryFile.a_directory("env2"),
+ ])
+
+ loader_from(:filesystem => [envdir],
+ :directory => envdir) do |loader|
+ expect(loader.list).to include_in_any_order(environment(:env1), environment(:env2))
+ end
+ end
+
+ it "it ignores directories that are not valid env names (alphanumeric and _)" do
+ envdir = FS::MemoryFile.a_directory("envdir", [
+ FS::MemoryFile.a_directory(".foo"),
+ FS::MemoryFile.a_directory("bar-thing"),
+ FS::MemoryFile.a_directory("with spaces"),
+ FS::MemoryFile.a_directory("some.thing"),
+ FS::MemoryFile.a_directory("env1"),
+ FS::MemoryFile.a_directory("env2"),
+ ])
+
+ loader_from(:filesystem => [envdir],
+ :directory => envdir) do |loader|
+ expect(loader.list).to include_in_any_order(environment(:env1), environment(:env2))
+ end
+ end
+
+ it "gets a particular environment" do
+ directory_tree = FS::MemoryFile.a_directory("envdir", [
+ FS::MemoryFile.a_directory("env1"),
+ FS::MemoryFile.a_directory("env2"),
+ ])
+
+ loader_from(:filesystem => [directory_tree],
+ :directory => directory_tree) do |loader|
+ expect(loader.get("env1")).to environment(:env1)
+ end
+ end
+
+ it "returns nil if an environment can't be found" do
+ directory_tree = FS::MemoryFile.a_directory("envdir", [])
+
+ loader_from(:filesystem => [directory_tree],
+ :directory => directory_tree) do |loader|
+ expect(loader.get("env_not_in_this_list")).to be_nil
+ end
+ end
+ end
+end
diff --git a/spec/unit/face/config_spec.rb b/spec/unit/face/config_spec.rb
index 1d1aee43f..338b532a3 100755
--- a/spec/unit/face/config_spec.rb
+++ b/spec/unit/face/config_spec.rb
@@ -1,31 +1,46 @@
#! /usr/bin/env ruby
require 'spec_helper'
require 'puppet/face'
describe Puppet::Face[:config, '0.0.1'] do
- it "should use Settings#print_config_options when asked to print" do
- Puppet.settings.stubs(:puts)
- Puppet.settings.expects(:print_config_options)
- subject.print
+ it "prints a single setting without the name" do
+ Puppet[:trace] = true
+
+ subject.expects(:puts).with(true)
+
+ subject.print("trace").should be_nil
end
- it "should set 'configprint' to all desired values and call print_config_options when a specific value is provided" do
- Puppet.settings.stubs(:puts)
- Puppet.settings.expects(:print_config_options)
- subject.print("libdir", "ssldir")
- Puppet.settings[:configprint].should == "libdir,ssldir"
+ it "prints multiple settings with the names" do
+ Puppet[:trace] = true
+ Puppet[:syslogfacility] = "file"
+
+ subject.expects(:puts).with("trace = true")
+ subject.expects(:puts).with("syslogfacility = file")
+
+ subject.print("trace", "syslogfacility")
end
- it "should always return nil" do
- Puppet.settings.stubs(:puts)
- Puppet.settings.expects(:print_config_options)
- subject.print("libdir").should be_nil
+ it "prints the setting from the selected section" do
+ Puppet.settings.parse_config(<<-CONF)
+ [other]
+ syslogfacility = file
+ CONF
+
+ subject.expects(:puts).with("file")
+
+ subject.print("syslogfacility", :section => "other")
end
it "should default to all when no arguments are given" do
- Puppet.settings.stubs(:puts)
- Puppet.settings.expects(:print_config_options)
+ subject.expects(:puts).times(Puppet.settings.to_a.length)
+
subject.print
- Puppet.settings[:configprint].should == "all"
+ end
+
+ it "prints out all of the settings when asked for 'all'" do
+ subject.expects(:puts).times(Puppet.settings.to_a.length)
+
+ subject.print('all')
end
end
diff --git a/spec/unit/face/module/build_spec.rb b/spec/unit/face/module/build_spec.rb
index d372ceb30..02bbb7e9d 100644
--- a/spec/unit/face/module/build_spec.rb
+++ b/spec/unit/face/module/build_spec.rb
@@ -1,68 +1,69 @@
require 'spec_helper'
require 'puppet/face'
+require 'puppet/module_tool'
describe "puppet module build" do
subject { Puppet::Face[:module, :current] }
describe "when called without any options" do
it "if current directory is a module root should call builder with it" do
Dir.expects(:pwd).returns('/a/b/c')
Puppet::ModuleTool.expects(:find_module_root).with('/a/b/c').returns('/a/b/c')
Puppet::ModuleTool.expects(:set_option_defaults).returns({})
Puppet::ModuleTool::Applications::Builder.expects(:run).with('/a/b/c', {})
subject.build
end
it "if parent directory of current dir is a module root should call builder with it" do
Dir.expects(:pwd).returns('/a/b/c')
Puppet::ModuleTool.expects(:find_module_root).with('/a/b/c').returns('/a/b')
Puppet::ModuleTool.expects(:set_option_defaults).returns({})
Puppet::ModuleTool::Applications::Builder.expects(:run).with('/a/b', {})
subject.build
end
it "if current directory or parents contain no module root, should return exception" do
Dir.expects(:pwd).returns('/a/b/c')
Puppet::ModuleTool.expects(:find_module_root).returns(nil)
expect { subject.build }.to raise_error RuntimeError, "Unable to find module root at /a/b/c or parent directories"
end
end
describe "when called with a path" do
it "if path is a module root should call builder with it" do
Puppet::ModuleTool.expects(:is_module_root?).with('/a/b/c').returns(true)
Puppet::ModuleTool.expects(:set_option_defaults).returns({})
Puppet::ModuleTool::Applications::Builder.expects(:run).with('/a/b/c', {})
subject.build('/a/b/c')
end
it "if path is not a module root should raise exception" do
Puppet::ModuleTool.expects(:is_module_root?).with('/a/b/c').returns(false)
expect { subject.build('/a/b/c') }.to raise_error RuntimeError, "Unable to find module root at /a/b/c"
end
end
describe "with options" do
it "should pass through options to builder when provided" do
Puppet::ModuleTool.stubs(:is_module_root?).returns(true)
Puppet::ModuleTool.expects(:set_option_defaults).returns({})
Puppet::ModuleTool::Applications::Builder.expects(:run).with('/a/b/c', {:modulepath => '/x/y/z'})
subject.build('/a/b/c', :modulepath => '/x/y/z')
end
end
describe "inline documentation" do
subject { Puppet::Face[:module, :current].get_action :build }
its(:summary) { should =~ /build.*module/im }
its(:description) { should =~ /build.*module/im }
its(:returns) { should =~ /pathname/i }
its(:examples) { should_not be_empty }
%w{ license copyright summary description returns examples }.each do |doc|
context "of the" do
its(doc.to_sym) { should_not =~ /(FIXME|REVISIT|TODO)/ }
end
end
end
end
diff --git a/spec/unit/face/module/install_spec.rb b/spec/unit/face/module/install_spec.rb
index a8c18dd1c..868b22511 100644
--- a/spec/unit/face/module/install_spec.rb
+++ b/spec/unit/face/module/install_spec.rb
@@ -1,196 +1,113 @@
require 'spec_helper'
require 'puppet/face'
require 'puppet/module_tool'
describe "puppet module install" do
include PuppetSpec::Files
subject { Puppet::Face[:module, :current] }
- let(:options) do
- {}
- end
-
describe "option validation" do
- before do
- Puppet.settings[:modulepath] = fakemodpath
+ let(:sep) { File::PATH_SEPARATOR }
+ let(:fakefirstpath) { make_absolute("/my/fake/modpath") }
+ let(:fakesecondpath) { make_absolute("/other/fake/path") }
+ let(:fakemodpath) { "#{fakefirstpath}#{sep}#{fakesecondpath}" }
+ let(:fakedirpath) { make_absolute("/my/fake/path") }
+ let(:options) { {} }
+ let(:environment) do
+ Puppet::Node::Environment.create(:env, [fakefirstpath, fakesecondpath], '')
end
-
let(:expected_options) do
{
:target_dir => fakefirstpath,
- :modulepath => fakemodpath,
- :environment => 'production'
+ :environment_instance => environment,
}
end
- let(:sep) { File::PATH_SEPARATOR }
- let(:fakefirstpath) { make_absolute("/my/fake/modpath") }
- let(:fakesecondpath) { make_absolute("/other/fake/path") }
- let(:fakemodpath) { "#{fakefirstpath}#{sep}#{fakesecondpath}" }
- let(:fakedirpath) { make_absolute("/my/fake/path") }
+ around(:each) do |example|
+ Puppet.override(:current_environment => environment) do
+ example.run
+ end
+ end
context "without any options" do
- it "should require a name" do
+ it "requires a name" do
pattern = /wrong number of arguments/
expect { subject.install }.to raise_error ArgumentError, pattern
end
- it "should not require any options" do
+ it "does not require any options" do
expects_installer_run_with("puppetlabs-apache", expected_options)
subject.install("puppetlabs-apache")
end
end
- it "should accept the --force option" do
+ it "accepts the --force option" do
options[:force] = true
expected_options.merge!(options)
expects_installer_run_with("puppetlabs-apache", expected_options)
subject.install("puppetlabs-apache", options)
end
- it "should accept the --target-dir option" do
+ it "accepts the --target-dir option" do
options[:target_dir] = make_absolute("/foo/puppet/modules")
expected_options.merge!(options)
- expected_options[:modulepath] = "#{options[:target_dir]}#{sep}#{fakemodpath}"
+ expected_options[:environment_instance] = environment.override_with(:modulepath => [options[:target_dir], fakefirstpath, fakesecondpath])
expects_installer_run_with("puppetlabs-apache", expected_options)
subject.install("puppetlabs-apache", options)
end
- it "should accept the --version option" do
+ it "accepts the --version option" do
options[:version] = "0.0.1"
expected_options.merge!(options)
expects_installer_run_with("puppetlabs-apache", expected_options)
subject.install("puppetlabs-apache", options)
end
- it "should accept the --ignore-dependencies option" do
+ it "accepts the --ignore-dependencies option" do
options[:ignore_dependencies] = true
expected_options.merge!(options)
expects_installer_run_with("puppetlabs-apache", expected_options)
subject.install("puppetlabs-apache", options)
end
-
- describe "when modulepath option is passed" do
- let(:expected_options) { { :modulepath => fakemodpath, :environment => Puppet[:environment] } }
- let(:options) { { :modulepath => fakemodpath } }
-
- describe "when target-dir option is not passed" do
- it "should set target-dir to be first path from modulepath" do
- expected_options[:target_dir] = fakefirstpath
-
- expects_installer_run_with("puppetlabs-apache", expected_options)
-
- Puppet::Face[:module, :current].install("puppetlabs-apache", options)
-
- Puppet.settings[:modulepath].should == fakemodpath
- end
-
- it "should expand the target directory derived from the modulepath" do
- options[:modulepath] = "modules"
- expanded_path = File.expand_path("modules")
- expected_options.merge!(options)
- expected_options[:target_dir] = expanded_path
- expected_options[:modulepath] = "modules"
-
- expects_installer_run_with("puppetlabs-apache", expected_options)
- Puppet::Face[:module, :current].install("puppetlabs-apache", options)
- end
- end
-
- describe "when target-dir option is passed" do
- it "should set target-dir to be first path of modulepath" do
- options[:target_dir] = fakedirpath
- expected_options[:target_dir] = fakedirpath
- expected_options[:modulepath] = "#{fakedirpath}#{sep}#{fakemodpath}"
-
- expects_installer_run_with("puppetlabs-apache", expected_options)
-
- Puppet::Face[:module, :current].install("puppetlabs-apache", options)
-
- Puppet.settings[:modulepath].should == "#{fakedirpath}#{sep}#{fakemodpath}"
- end
- end
- end
-
- describe "when modulepath option is not passed" do
- before do
- Puppet.settings[:modulepath] = fakemodpath
- end
-
- describe "when target-dir option is not passed" do
- it "should set target-dir to be first path of default mod path" do
- expected_options[:target_dir] = fakefirstpath
- expected_options[:modulepath] = fakemodpath
-
- expects_installer_run_with("puppetlabs-apache", expected_options)
-
- Puppet::Face[:module, :current].install("puppetlabs-apache", options)
- end
- end
-
- describe "when target-dir option is passed" do
- it "should prepend target-dir to modulepath" do
- options[:target_dir] = fakedirpath
- expected_options[:target_dir] = fakedirpath
- expected_options[:modulepath] = "#{options[:target_dir]}#{sep}#{fakemodpath}"
-
- expects_installer_run_with("puppetlabs-apache", expected_options)
-
- Puppet::Face[:module, :current].install("puppetlabs-apache", options)
- Puppet.settings[:modulepath].should == expected_options[:modulepath]
- end
-
- it "should expand the target directory when target_dir is set" do
- options[:target_dir] = "modules"
- expanded_path = File.expand_path("modules")
- expected_options.merge!(options)
- expected_options[:target_dir] = expanded_path
- expected_options[:modulepath] = "#{expanded_path}#{sep}#{fakemodpath}"
-
- expects_installer_run_with("puppetlabs-apache", expected_options)
- Puppet::Face[:module, :current].install("puppetlabs-apache", options)
- end
- end
- end
end
describe "inline documentation" do
subject { Puppet::Face[:module, :current].get_action :install }
its(:summary) { should =~ /install.*module/im }
its(:description) { should =~ /install.*module/im }
its(:returns) { should =~ /pathname/i }
its(:examples) { should_not be_empty }
%w{ license copyright summary description returns examples }.each do |doc|
context "of the" do
its(doc.to_sym) { should_not =~ /(FIXME|REVISIT|TODO)/ }
end
end
end
def expects_installer_run_with(name, options)
installer = mock("Installer")
install_dir = mock("InstallDir")
forge = mock("Forge")
Puppet::Forge.expects(:new).with("PMT", subject.version).returns(forge)
Puppet::ModuleTool::InstallDirectory.expects(:new).
with(Pathname.new(expected_options[:target_dir])).
returns(install_dir)
Puppet::ModuleTool::Applications::Installer.expects(:new).
with("puppetlabs-apache", forge, install_dir, expected_options).
returns(installer)
installer.expects(:run)
end
end
diff --git a/spec/unit/face/module/list_spec.rb b/spec/unit/face/module/list_spec.rb
index 3ffb474dc..9fc4e7121 100644
--- a/spec/unit/face/module/list_spec.rb
+++ b/spec/unit/face/module/list_spec.rb
@@ -1,179 +1,195 @@
# encoding: UTF-8
require 'spec_helper'
require 'puppet/face'
require 'puppet/module_tool'
require 'puppet_spec/modules'
describe "puppet module list" do
include PuppetSpec::Files
before do
dir = tmpdir("deep_path")
@modpath1 = File.join(dir, "modpath1")
@modpath2 = File.join(dir, "modpath2")
@modulepath = "#{@modpath1}#{File::PATH_SEPARATOR}#{@modpath2}"
Puppet.settings[:modulepath] = @modulepath
FileUtils.mkdir_p(@modpath1)
FileUtils.mkdir_p(@modpath2)
end
+ around do |example|
+ Puppet.override(:environments => Puppet::Environments::Legacy.new()) do
+ example.run
+ end
+ end
+
it "should return an empty list per dir in path if there are no modules" do
Puppet.settings[:modulepath] = @modulepath
Puppet::Face[:module, :current].list.should == {
@modpath1 => [],
@modpath2 => []
}
end
it "should include modules separated by the environment's modulepath" do
foomod1 = PuppetSpec::Modules.create('foo', @modpath1)
barmod1 = PuppetSpec::Modules.create('bar', @modpath1)
foomod2 = PuppetSpec::Modules.create('foo', @modpath2)
- env = Puppet::Node::Environment.new
+ usedenv = Puppet::Node::Environment.create(:useme, [@modpath1, @modpath2], '')
- Puppet::Face[:module, :current].list.should == {
- @modpath1 => [
- Puppet::Module.new('bar', barmod1.path, env),
- Puppet::Module.new('foo', foomod1.path, env)
- ],
- @modpath2 => [Puppet::Module.new('foo', foomod2.path, env)]
- }
+ Puppet.override(:environments => Puppet::Environments::Static.new(usedenv)) do
+ Puppet::Face[:module, :current].list(:environment => 'useme').should == {
+ @modpath1 => [
+ Puppet::Module.new('bar', barmod1.path, usedenv),
+ Puppet::Module.new('foo', foomod1.path, usedenv)
+ ],
+ @modpath2 => [Puppet::Module.new('foo', foomod2.path, usedenv)]
+ }
+ end
end
it "should use the specified environment" do
foomod = PuppetSpec::Modules.create('foo', @modpath1)
barmod = PuppetSpec::Modules.create('bar', @modpath1)
- usedenv = Puppet::Node::Environment.new('useme')
- usedenv.modulepath = [@modpath1, @modpath2]
+ usedenv = Puppet::Node::Environment.create(:useme, [@modpath1, @modpath2], '')
- Puppet::Face[:module, :current].list(:environment => 'useme').should == {
- @modpath1 => [
- Puppet::Module.new('bar', barmod.path, usedenv),
- Puppet::Module.new('foo', foomod.path, usedenv)
- ],
- @modpath2 => []
- }
+ Puppet.override(:environments => Puppet::Environments::Static.new(usedenv)) do
+ Puppet::Face[:module, :current].list(:environment => 'useme').should == {
+ @modpath1 => [
+ Puppet::Module.new('bar', barmod.path, usedenv),
+ Puppet::Module.new('foo', foomod.path, usedenv)
+ ],
+ @modpath2 => []
+ }
+ end
end
it "should use the specified modulepath" do
foomod = PuppetSpec::Modules.create('foo', @modpath1)
barmod = PuppetSpec::Modules.create('bar', @modpath2)
- Puppet::Face[:module, :current].list(:modulepath => "#{@modpath1}#{File::PATH_SEPARATOR}#{@modpath2}").should == {
- @modpath1 => [ Puppet::Module.new('foo', foomod.path, Puppet::Node::Environment.new) ],
- @modpath2 => [ Puppet::Module.new('bar', barmod.path, Puppet::Node::Environment.new) ]
- }
+ modules = Puppet::Face[:module, :current].list(:modulepath => "#{@modpath1}#{File::PATH_SEPARATOR}#{@modpath2}")
+
+ expect(modules[@modpath1].first.name).to eq('foo')
+ expect(modules[@modpath1].first.path).to eq(foomod.path)
+ expect(modules[@modpath1].first.environment.modulepath).to eq([@modpath1, @modpath2])
+
+ expect(modules[@modpath2].first.name).to eq('bar')
+ expect(modules[@modpath2].first.path).to eq(barmod.path)
+ expect(modules[@modpath2].first.environment.modulepath).to eq([@modpath1, @modpath2])
end
- it "should use the specified modulepath over the specified environment in place of the environment's default path" do
- foomod1 = PuppetSpec::Modules.create('foo', @modpath1)
- barmod2 = PuppetSpec::Modules.create('bar', @modpath2)
- env = Puppet::Node::Environment.new('myenv')
- env.modulepath = ['/tmp/notused']
-
- list = Puppet::Face[:module, :current].list(:environment => 'myenv', :modulepath => "#{@modpath1}#{File::PATH_SEPARATOR}#{@modpath2}")
-
- # Changing Puppet[:modulepath] causes Puppet::Node::Environment.new('myenv')
- # to have a different object_id than the env above
- env = Puppet::Node::Environment.new('myenv')
- list.should == {
- @modpath1 => [ Puppet::Module.new('foo', foomod1.path, env) ],
- @modpath2 => [ Puppet::Module.new('bar', barmod2.path, env) ]
- }
+ it "prefers a given modulepath over the modulepath from the given environment" do
+ foomod = PuppetSpec::Modules.create('foo', @modpath1)
+ barmod = PuppetSpec::Modules.create('bar', @modpath2)
+ env = Puppet::Node::Environment.create(:myenv, ['/tmp/notused'], '')
+ Puppet[:modulepath] = ""
+
+ modules = Puppet::Face[:module, :current].list(:environment => 'myenv', :modulepath => "#{@modpath1}#{File::PATH_SEPARATOR}#{@modpath2}")
+
+ expect(modules[@modpath1].first.name).to eq('foo')
+ expect(modules[@modpath1].first.path).to eq(foomod.path)
+ expect(modules[@modpath1].first.environment.modulepath).to eq([@modpath1, @modpath2])
+ expect(modules[@modpath1].first.environment.name).to_not eq(:myenv)
+
+ expect(modules[@modpath2].first.name).to eq('bar')
+ expect(modules[@modpath2].first.path).to eq(barmod.path)
+ expect(modules[@modpath2].first.environment.modulepath).to eq([@modpath1, @modpath2])
+ expect(modules[@modpath2].first.environment.name).to_not eq(:myenv)
end
describe "inline documentation" do
subject { Puppet::Face[:module, :current].get_action :list }
its(:summary) { should =~ /list.*module/im }
its(:description) { should =~ /list.*module/im }
its(:returns) { should =~ /hash of paths to module objects/i }
its(:examples) { should_not be_empty }
end
describe "when rendering" do
it "should explicitly state when a modulepath is empty" do
empty_modpath = tmpdir('empty')
Puppet::Face[:module, :current].list_when_rendering_console(
{ empty_modpath => [] },
{:modulepath => empty_modpath}
).should == <<-HEREDOC.gsub(' ', '')
#{empty_modpath} (no modules installed)
HEREDOC
end
it "should print both modules with and without metadata" do
modpath = tmpdir('modpath')
Puppet.settings[:modulepath] = modpath
PuppetSpec::Modules.create('nometadata', modpath)
PuppetSpec::Modules.create('metadata', modpath, :metadata => {:author => 'metaman'})
dependency_tree = Puppet::Face[:module, :current].list
output = Puppet::Face[:module, :current].
list_when_rendering_console(dependency_tree, {})
output.should == <<-HEREDOC.gsub(' ', '')
#{modpath}
├── metaman-metadata (\e[0;36mv9.9.9\e[0m)
└── nometadata (\e[0;36m???\e[0m)
HEREDOC
end
it "should print the modulepaths in the order they are in the modulepath setting" do
path1 = tmpdir('b')
path2 = tmpdir('c')
path3 = tmpdir('a')
sep = File::PATH_SEPARATOR
Puppet.settings[:modulepath] = "#{path1}#{sep}#{path2}#{sep}#{path3}"
Puppet::Face[:module, :current].list_when_rendering_console(
{
path2 => [],
path3 => [],
path1 => [],
},
{}
).should == <<-HEREDOC.gsub(' ', '')
#{path1} (no modules installed)
#{path2} (no modules installed)
#{path3} (no modules installed)
HEREDOC
end
it "should print dependencies as a tree" do
PuppetSpec::Modules.create('dependable', @modpath1, :metadata => { :version => '0.0.5'})
PuppetSpec::Modules.create(
'other_mod',
@modpath1,
:metadata => {
:version => '1.0.0',
:dependencies => [{
"version_requirement" => ">= 0.0.5",
"name" => "puppetlabs/dependable"
}]
}
)
dependency_tree = Puppet::Face[:module, :current].list
output = Puppet::Face[:module, :current].list_when_rendering_console(
dependency_tree,
{:tree => true}
)
output.should == <<-HEREDOC.gsub(' ', '')
#{@modpath1}
└─┬ puppetlabs-other_mod (\e[0;36mv1.0.0\e[0m)
└── puppetlabs-dependable (\e[0;36mv0.0.5\e[0m)
#{@modpath2} (no modules installed)
HEREDOC
end
end
end
diff --git a/spec/unit/face/module/uninstall_spec.rb b/spec/unit/face/module/uninstall_spec.rb
index f75a1f9cc..36209dc96 100644
--- a/spec/unit/face/module/uninstall_spec.rb
+++ b/spec/unit/face/module/uninstall_spec.rb
@@ -1,77 +1,70 @@
require 'spec_helper'
require 'puppet/face'
require 'puppet/module_tool'
describe "puppet module uninstall" do
subject { Puppet::Face[:module, :current] }
let(:options) do
{}
end
+ let(:modulepath) { File.expand_path('/module/path') }
+ let(:environment) do
+ Puppet::Node::Environment.create(:env, [modulepath], '')
+ end
+ let(:expected_options) do
+ {
+ :target_dir => modulepath,
+ :environment_instance => environment,
+ }
+ end
describe "option validation" do
+ around(:each) do |example|
+ Puppet.override(:current_environment => environment) do
+ example.run
+ end
+ end
+
context "without any options" do
it "should require a name" do
pattern = /wrong number of arguments/
expect { subject.uninstall }.to raise_error ArgumentError, pattern
end
it "should not require any options" do
Puppet::ModuleTool::Applications::Uninstaller.expects(:run).once
subject.uninstall("puppetlabs-apache")
end
end
- it "should accept the --environment option" do
- options[:environment] = "development"
- expected_options = { :environment => 'development' }
- Puppet::ModuleTool::Applications::Uninstaller.expects(:run).with("puppetlabs-apache", has_entries(expected_options)).once
- subject.uninstall("puppetlabs-apache", options)
- end
-
- it "should accept the --modulepath option" do
- options[:modulepath] = "/foo/puppet/modules"
- expected_options = {
- :modulepath => '/foo/puppet/modules',
- :environment => 'production',
- }
- Puppet::ModuleTool::Applications::Uninstaller.expects(:run).with("puppetlabs-apache", has_entries(expected_options)).once
- subject.uninstall("puppetlabs-apache", options)
- end
-
it "should accept the --version option" do
options[:version] = "1.0.0"
- expected_options = {
- :version => '1.0.0',
- :environment => 'production',
- }
+ expected_options.merge!(:version => '1.0.0')
Puppet::ModuleTool::Applications::Uninstaller.expects(:run).with("puppetlabs-apache", has_entries(expected_options)).once
subject.uninstall("puppetlabs-apache", options)
end
it "should accept the --force flag" do
options[:force] = true
- expected_options = {
- :environment => 'production',
- :force => true,
- }
+ expected_options.merge!(:force => true)
Puppet::ModuleTool::Applications::Uninstaller.expects(:run).with("puppetlabs-apache", has_entries(expected_options)).once
subject.uninstall("puppetlabs-apache", options)
end
end
describe "inline documentation" do
subject { Puppet::Face[:module, :current].get_action :uninstall }
its(:summary) { should =~ /uninstall.*module/im }
its(:description) { should =~ /uninstall.*module/im }
its(:returns) { should =~ /hash of module objects.*/im }
its(:examples) { should_not be_empty }
%w{ license copyright summary description returns examples }.each do |doc|
context "of the" do
its(doc.to_sym) { should_not =~ /(FIXME|REVISIT|TODO)/ }
end
end
end
end
diff --git a/spec/unit/face/parser_spec.rb b/spec/unit/face/parser_spec.rb
index a517ae641..3bf24bcb0 100644
--- a/spec/unit/face/parser_spec.rb
+++ b/spec/unit/face/parser_spec.rb
@@ -1,54 +1,70 @@
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
+ context "from an interactive terminal" do
+ before :each do
+ from_an_interactive_terminal
+ end
- expect { parser.validate() }.to exit_with(1)
- end
+ it "validates the configured site manifest when no files are given" do
+ Puppet[:manifest] = file_containing('site.pp', "{ invalid =>")
- it "validates the given file" do
- manifest = file_containing('site.pp', "{ invalid =>")
- from_an_interactive_terminal
+ expect { parser.validate() }.to exit_with(1)
+ end
- expect { parser.validate(manifest) }.to exit_with(1)
- end
+ it "validates the given file" do
+ manifest = file_containing('site.pp', "{ invalid =>")
- 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(manifest) }.to exit_with(1)
+ end
- 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: }")
- 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
+ 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
+
+ it "parses supplied manifest files in the context of a directory environment" do
+ manifest = file_containing('test.pp', "{ invalid =>")
+
+ env_loader = Puppet::Environments::Static.new(
+ Puppet::Node::Environment.create(:special, [], '')
+ )
+ Puppet.override(:environments => env_loader) do
+ Puppet[:environment] = 'special'
+ expect { parser.validate(manifest) }.to exit_with(1)
+ end
+
+ expect(@logs.join).to match(/environment special.*Syntax error at '\{'/)
+ end
- parser.validate(manifest)
end
- it "reports missing files" do
- from_an_interactive_terminal
+ it "validates the contents of STDIN when no files given and STDIN is not a tty" do
+ from_a_piped_input_of("{ invalid =>")
- expect do
- parser.validate("missing.pp")
- end.to raise_error(Puppet::Error, /One or more file\(s\) specified did not exist.*missing\.pp/m)
+ expect { parser.validate() }.to exit_with(1)
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 83e914851..a6d961695 100755
--- a/spec/unit/file_bucket/dipper_spec.rb
+++ b/spec/unit/file_bucket/dipper_spec.rb
@@ -1,170 +1,170 @@
#! /usr/bin/env ruby
require 'spec_helper'
require 'pathname'
require 'puppet/file_bucket/dipper'
require 'puppet/indirector/file_bucket_file/rest'
describe Puppet::FileBucket::Dipper do
include PuppetSpec::Files
def make_tmp_file(contents)
file = tmpfile("file_bucket_file")
File.open(file, 'wb') { |f| f.write(contents) }
file
end
it "should fail in an informative way when there are failures checking for the file on the server" do
@dipper = Puppet::FileBucket::Dipper.new(:Path => make_absolute("/my/bucket"))
file = make_tmp_file('contents')
Puppet::FileBucket::File.indirection.expects(:head).raises ArgumentError
lambda { @dipper.backup(file) }.should raise_error(Puppet::Error)
end
it "should fail in an informative way when there are failures backing up to the server" do
@dipper = Puppet::FileBucket::Dipper.new(:Path => make_absolute("/my/bucket"))
file = make_tmp_file('contents')
Puppet::FileBucket::File.indirection.expects(:head).returns false
Puppet::FileBucket::File.indirection.expects(:save).raises ArgumentError
lambda { @dipper.backup(file) }.should raise_error(Puppet::Error)
end
it "should backup files to a local bucket" do
Puppet[:bucketdir] = "/non/existent/directory"
file_bucket = tmpdir("bucket")
@dipper = Puppet::FileBucket::Dipper.new(:Path => file_bucket)
file = make_tmp_file("my\r\ncontents")
checksum = "f0d7d4e480ad698ed56aeec8b6bd6dea"
Digest::MD5.hexdigest("my\r\ncontents").should == checksum
@dipper.backup(file).should == checksum
- Puppet::FileSystem::File.exist?("#{file_bucket}/f/0/d/7/d/4/e/4/f0d7d4e480ad698ed56aeec8b6bd6dea/contents").should == true
+ Puppet::FileSystem.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
@dipper = Puppet::FileBucket::Dipper.new(:Path => "/my/bucket")
file = make_tmp_file("my\r\ncontents")
checksum = Digest::MD5.hexdigest("my\r\ncontents")
Puppet::FileBucket::File.indirection.expects(:head).returns true
Puppet::FileBucket::File.indirection.expects(:save).never
@dipper.backup(file).should == checksum
end
it "should retrieve files from a local bucket" do
@dipper = Puppet::FileBucket::Dipper.new(:Path => "/my/bucket")
checksum = Digest::MD5.hexdigest('my contents')
request = nil
Puppet::FileBucketFile::File.any_instance.expects(:find).with{ |r| request = r }.once.returns(Puppet::FileBucket::File.new('my contents'))
@dipper.getfile(checksum).should == 'my contents'
request.key.should == "md5/#{checksum}"
end
it "should backup files to a remote server" do
@dipper = Puppet::FileBucket::Dipper.new(:Server => "puppetmaster", :Port => "31337")
file = make_tmp_file("my\r\ncontents")
checksum = Digest::MD5.hexdigest("my\r\ncontents")
real_path = Pathname.new(file).realpath
request1 = nil
request2 = nil
Puppet::FileBucketFile::Rest.any_instance.expects(:head).with { |r| request1 = r }.once.returns(nil)
Puppet::FileBucketFile::Rest.any_instance.expects(:save).with { |r| request2 = r }.once
@dipper.backup(file).should == checksum
[request1, request2].each do |r|
r.server.should == 'puppetmaster'
r.port.should == 31337
r.key.should == "md5/#{checksum}/#{real_path}"
end
end
it "should retrieve files from a remote server" do
@dipper = Puppet::FileBucket::Dipper.new(:Server => "puppetmaster", :Port => "31337")
checksum = Digest::MD5.hexdigest('my contents')
request = nil
Puppet::FileBucketFile::Rest.any_instance.expects(:find).with { |r| request = r }.returns(Puppet::FileBucket::File.new('my contents'))
@dipper.getfile(checksum).should == "my contents"
request.server.should == 'puppetmaster'
request.port.should == 31337
request.key.should == "md5/#{checksum}"
end
describe "#restore" do
shared_examples_for "a restorable file" do
let(:contents) { "my\ncontents" }
let(:md5) { Digest::MD5.hexdigest(contents) }
let(:dest) { tmpfile('file_bucket_dest') }
it "should restore the file" do
request = nil
klass.any_instance.expects(:find).with { |r| request = r }.returns(Puppet::FileBucket::File.new(contents))
dipper.restore(dest, md5).should == md5
- Digest::MD5.hexdigest(Puppet::FileSystem::File.new(dest).binread).should == md5
+ Digest::MD5.hexdigest(Puppet::FileSystem.binread(dest)).should == md5
request.key.should == "md5/#{md5}"
request.server.should == server
request.port.should == port
end
it "should skip restoring if existing file has the same checksum" do
crnl = "my\r\ncontents"
File.open(dest, 'wb') {|f| f.print(crnl) }
dipper.expects(:getfile).never
dipper.restore(dest, Digest::MD5.hexdigest(crnl)).should be_nil
end
it "should overwrite existing file if it has different checksum" do
klass.any_instance.expects(:find).returns(Puppet::FileBucket::File.new(contents))
File.open(dest, 'wb') {|f| f.print('other contents') }
dipper.restore(dest, md5).should == md5
end
end
describe "when restoring from a remote server" do
let(:klass) { Puppet::FileBucketFile::Rest }
let(:server) { "puppetmaster" }
let(:port) { 31337 }
it_behaves_like "a restorable file" do
let (:dipper) { Puppet::FileBucket::Dipper.new(:Server => server, :Port => port.to_s) }
end
end
describe "when restoring from a local server" do
let(:klass) { Puppet::FileBucketFile::File }
let(:server) { nil }
let(:port) { nil }
it_behaves_like "a restorable file" do
let (:dipper) { Puppet::FileBucket::Dipper.new(:Path => "/my/bucket") }
end
end
end
end
diff --git a/spec/unit/file_serving/base_spec.rb b/spec/unit/file_serving/base_spec.rb
index 65168d3a3..5b9794211 100755
--- a/spec/unit/file_serving/base_spec.rb
+++ b/spec/unit/file_serving/base_spec.rb
@@ -1,172 +1,168 @@
#! /usr/bin/env ruby
require 'spec_helper'
require 'puppet/file_serving/base'
describe Puppet::FileServing::Base do
let(:path) { File.expand_path('/module/dir/file') }
let(:file) { File.expand_path('/my/file') }
it "should accept a path" do
Puppet::FileServing::Base.new(path).path.should == path
end
it "should require that paths be fully qualified" do
lambda { Puppet::FileServing::Base.new("module/dir/file") }.should raise_error(ArgumentError)
end
it "should allow specification of whether links should be managed" do
Puppet::FileServing::Base.new(path, :links => :manage).links.should == :manage
end
it "should have a :source attribute" do
file = Puppet::FileServing::Base.new(path)
file.should respond_to(:source)
file.should respond_to(:source=)
end
it "should consider :ignore links equivalent to :manage links" do
Puppet::FileServing::Base.new(path, :links => :ignore).links.should == :manage
end
it "should fail if :links is set to anything other than :manage, :follow, or :ignore" do
proc { Puppet::FileServing::Base.new(path, :links => :else) }.should raise_error(ArgumentError)
end
it "should allow links values to be set as strings" do
Puppet::FileServing::Base.new(path, :links => "follow").links.should == :follow
end
it "should default to :manage for :links" do
Puppet::FileServing::Base.new(path).links.should == :manage
end
it "should allow specification of a path" do
- Puppet::FileSystem::File.stubs(:exist?).returns(true)
+ Puppet::FileSystem.stubs(:exist?).returns(true)
Puppet::FileServing::Base.new(path, :path => file).path.should == file
end
it "should allow specification of a relative path" do
- Puppet::FileSystem::File.stubs(:exist?).returns(true)
+ Puppet::FileSystem.stubs(:exist?).returns(true)
Puppet::FileServing::Base.new(path, :relative_path => "my/file").relative_path.should == "my/file"
end
it "should have a means of determining if the file exists" do
Puppet::FileServing::Base.new(file).should respond_to(:exist?)
end
it "should correctly indicate if the file is present" do
- mock_file = mock(file, :lstat => stub('stat'))
- Puppet::FileSystem::File.expects(:new).with(file).returns mock_file
+ Puppet::FileSystem.expects(:lstat).with(file).returns stub('stat')
Puppet::FileServing::Base.new(file).exist?.should be_true
end
it "should correctly indicate if the file is absent" do
- mock_file = mock(file)
- Puppet::FileSystem::File.expects(:new).with(file).returns mock_file
- mock_file.expects(:lstat).raises RuntimeError
+ Puppet::FileSystem.expects(:lstat).with(file).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)
- Puppet::FileSystem::File.stubs(:exist?).returns(true)
+ Puppet::FileSystem.stubs(:exist?).returns(true)
proc { @file.relative_path = File.expand_path("/qualified/file") }.should raise_error(ArgumentError)
end
end
describe "when determining the full file path" do
let(:path) { File.expand_path('/this/file') }
let(:file) { Puppet::FileServing::Base.new(path) }
it "should return the path if there is no relative path" do
file.full_path.should == path
end
it "should return the path if the relative_path is set to ''" do
file.relative_path = ""
file.full_path.should == path
end
it "should return the path if the relative_path is set to '.'" do
file.relative_path = "."
file.full_path.should == path
end
it "should return the path joined with the relative path if there is a relative path and it is not set to '/' or ''" do
file.relative_path = "not/qualified"
file.full_path.should == File.join(path, "not/qualified")
end
it "should strip extra slashes" do
file = Puppet::FileServing::Base.new(File.join(File.expand_path('/'), "//this//file"))
file.full_path.should == path
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
- Puppet::FileSystem::File.expects(:new).with(path).returns stubbed_file
+ Puppet::FileSystem.expects(:lstat).with(path).returns stat
file.stat
end
it "should fail if the file does not exist" do
- Puppet::FileSystem::File.expects(:new).with(path).returns stubbed_file
- stubbed_file.expects(:lstat).raises(Errno::ENOENT)
+ Puppet::FileSystem.expects(:lstat).with(path).raises(Errno::ENOENT)
proc { file.stat }.should raise_error(Errno::ENOENT)
end
it "should use :lstat if :links is set to :manage" do
- Puppet::FileSystem::File.expects(:new).with(path).returns stubbed_file
+ Puppet::FileSystem.expects(:lstat).with(path).returns stubbed_file
file.stat
end
it "should use :stat if :links is set to :follow" do
- Puppet::FileSystem::File.expects(:new).with(path).returns stubbed_file
+ Puppet::FileSystem.expects(:stat).with(path).returns stubbed_file
file.links = :follow
file.stat
end
end
describe "#absolute?" do
it "should be accept POSIX paths" do
Puppet::FileServing::Base.should be_absolute('/')
end
it "should accept Windows paths on Windows" do
Puppet.features.stubs(:microsoft_windows?).returns(true)
Puppet.features.stubs(:posix?).returns(false)
Puppet::FileServing::Base.should be_absolute('c:/foo')
end
it "should reject Windows paths on POSIX" do
Puppet.features.stubs(:microsoft_windows?).returns(false)
Puppet::FileServing::Base.should_not be_absolute('c:/foo')
end
end
end
diff --git a/spec/unit/file_serving/configuration/parser_spec.rb b/spec/unit/file_serving/configuration/parser_spec.rb
index 75744d317..d9d712999 100755
--- a/spec/unit/file_serving/configuration/parser_spec.rb
+++ b/spec/unit/file_serving/configuration/parser_spec.rb
@@ -1,180 +1,186 @@
#! /usr/bin/env ruby
require 'spec_helper'
require 'puppet/file_serving/configuration/parser'
module FSConfigurationParserTesting
def write_config_file(content)
# We want an array, but we actually want our carriage returns on all of it.
File.open(@path, 'w') {|f| f.puts content}
end
end
describe Puppet::FileServing::Configuration::Parser do
include PuppetSpec::Files
before :each do
@path = tmpfile('fileserving_config')
FileUtils.touch(@path)
@parser = Puppet::FileServing::Configuration::Parser.new(@path)
end
describe Puppet::FileServing::Configuration::Parser, " when parsing" do
include FSConfigurationParserTesting
it "should allow comments" do
write_config_file("# this is a comment\n")
proc { @parser.parse }.should_not raise_error
end
it "should allow blank lines" do
write_config_file("\n")
proc { @parser.parse }.should_not raise_error
end
it "should create a new mount for each section in the configuration" do
mount1 = mock 'one', :validate => true
mount2 = mock 'two', :validate => true
Puppet::FileServing::Mount::File.expects(:new).with("one").returns(mount1)
Puppet::FileServing::Mount::File.expects(:new).with("two").returns(mount2)
write_config_file "[one]\n[two]\n"
@parser.parse
end
# This test is almost the exact same as the previous one.
it "should return a hash of the created mounts" do
mount1 = mock 'one', :validate => true
mount2 = mock 'two', :validate => true
Puppet::FileServing::Mount::File.expects(:new).with("one").returns(mount1)
Puppet::FileServing::Mount::File.expects(:new).with("two").returns(mount2)
write_config_file "[one]\n[two]\n"
result = @parser.parse
result["one"].should equal(mount1)
result["two"].should equal(mount2)
end
it "should only allow mount names that are alphanumeric plus dashes" do
write_config_file "[a*b]\n"
proc { @parser.parse }.should raise_error(ArgumentError)
end
it "should fail if the value for path/allow/deny starts with an equals sign" do
write_config_file "[one]\npath = /testing"
proc { @parser.parse }.should raise_error(ArgumentError)
end
it "should validate each created mount" do
mount1 = mock 'one'
Puppet::FileServing::Mount::File.expects(:new).with("one").returns(mount1)
write_config_file "[one]\n"
mount1.expects(:validate)
@parser.parse
end
it "should fail if any mount does not pass validation" do
mount1 = mock 'one'
Puppet::FileServing::Mount::File.expects(:new).with("one").returns(mount1)
write_config_file "[one]\n"
mount1.expects(:validate).raises RuntimeError
lambda { @parser.parse }.should raise_error(RuntimeError)
end
+
+ it "should return comprehensible error message, if invalid line detected" do
+ write_config_file "[one]\n\n\x01path /etc/puppet/files\n\x01allow *\n"
+
+ proc { @parser.parse }.should raise_error(ArgumentError, /Invalid line.*in.*, line 3/)
+ end
end
describe Puppet::FileServing::Configuration::Parser, " when parsing mount attributes" do
include FSConfigurationParserTesting
before do
@mount = stub 'testmount', :name => "one", :validate => true
Puppet::FileServing::Mount::File.expects(:new).with("one").returns(@mount)
@parser.stubs(:add_modules_mount)
end
it "should set the mount path to the path attribute from that section" do
write_config_file "[one]\npath /some/path\n"
@mount.expects(:path=).with("/some/path")
@parser.parse
end
it "should tell the mount to allow any allow values from the section" do
write_config_file "[one]\nallow something\n"
@mount.expects(:info)
@mount.expects(:allow).with("something")
@parser.parse
end
it "should support inline comments" do
write_config_file "[one]\nallow something \# will it work?\n"
@mount.expects(:info)
@mount.expects(:allow).with("something")
@parser.parse
end
it "should tell the mount to deny any deny values from the section" do
write_config_file "[one]\ndeny something\n"
@mount.expects(:info)
@mount.expects(:deny).with("something")
@parser.parse
end
- it "should fail on any attributes other than path, allow, and deny" do
+ it "should return comprehensible error message, if failed on invalid attribute" do
write_config_file "[one]\ndo something\n"
- proc { @parser.parse }.should raise_error(ArgumentError)
+ proc { @parser.parse }.should raise_error(ArgumentError, /Invalid argument 'do' in .*, line 2/)
end
end
describe Puppet::FileServing::Configuration::Parser, " when parsing the modules mount" do
include FSConfigurationParserTesting
before do
@mount = stub 'modulesmount', :name => "modules", :validate => true
end
it "should create an instance of the Modules Mount class" do
write_config_file "[modules]\n"
Puppet::FileServing::Mount::Modules.expects(:new).with("modules").returns @mount
@parser.parse
end
it "should warn if a path is set" do
write_config_file "[modules]\npath /some/path\n"
Puppet::FileServing::Mount::Modules.expects(:new).with("modules").returns(@mount)
Puppet.expects(:warning)
@parser.parse
end
end
describe Puppet::FileServing::Configuration::Parser, " when parsing the plugins mount" do
include FSConfigurationParserTesting
before do
@mount = stub 'pluginsmount', :name => "plugins", :validate => true
end
it "should create an instance of the Plugins Mount class" do
write_config_file "[plugins]\n"
Puppet::FileServing::Mount::Plugins.expects(:new).with("plugins").returns @mount
@parser.parse
end
it "should warn if a path is set" do
write_config_file "[plugins]\npath /some/path\n"
Puppet.expects(:warning)
@parser.parse
end
end
end
diff --git a/spec/unit/file_serving/configuration_spec.rb b/spec/unit/file_serving/configuration_spec.rb
index b6999e086..a2c808051 100755
--- a/spec/unit/file_serving/configuration_spec.rb
+++ b/spec/unit/file_serving/configuration_spec.rb
@@ -1,231 +1,231 @@
#! /usr/bin/env ruby
require 'spec_helper'
require 'puppet/file_serving/configuration'
describe Puppet::FileServing::Configuration do
include PuppetSpec::Files
before :each do
@path = make_absolute("/path/to/configuration/file.conf")
Puppet[:trace] = false
Puppet[:fileserverconfig] = @path
end
after :each do
Puppet::FileServing::Configuration.instance_variable_set(:@configuration, nil)
end
it "should make :new a private method" do
expect { Puppet::FileServing::Configuration.new }.to raise_error
end
it "should return the same configuration each time 'configuration' is called" do
Puppet::FileServing::Configuration.configuration.should equal(Puppet::FileServing::Configuration.configuration)
end
describe "when initializing" do
it "should work without a configuration file" do
- Puppet::FileSystem::File.stubs(:exist?).with(@path).returns(false)
+ Puppet::FileSystem.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
- Puppet::FileSystem::File.stubs(:exist?).with(@path).returns(true)
+ Puppet::FileSystem.stubs(:exist?).with(@path).returns(true)
@parser = mock 'parser'
@parser.expects(:parse).returns({})
Puppet::FileServing::Configuration::Parser.stubs(:new).returns(@parser)
Puppet::FileServing::Configuration.configuration
end
it "should determine the path to the configuration file from the Puppet settings" do
Puppet::FileServing::Configuration.configuration
end
end
describe "when parsing the configuration file" do
before do
- Puppet::FileSystem::File.stubs(:exist?).with(@path).returns(true)
+ Puppet::FileSystem.stubs(:exist?).with(@path).returns(true)
@parser = mock 'parser'
Puppet::FileServing::Configuration::Parser.stubs(:new).returns(@parser)
end
it "should set the mount list to the results of parsing" do
@parser.expects(:parse).returns("one" => mock("mount"))
config = Puppet::FileServing::Configuration.configuration
config.mounted?("one").should be_true
end
it "should not raise exceptions" do
@parser.expects(:parse).raises(ArgumentError)
expect { Puppet::FileServing::Configuration.configuration }.to_not raise_error
end
it "should replace the existing mount list with the results of reparsing" do
@parser.expects(:parse).returns("one" => mock("mount"))
config = Puppet::FileServing::Configuration.configuration
config.mounted?("one").should be_true
# Now parse again
@parser.expects(:parse).returns("two" => mock('other'))
config.send(:readconfig, false)
config.mounted?("one").should be_false
config.mounted?("two").should be_true
end
it "should not replace the mount list until the file is entirely parsed successfully" do
@parser.expects(:parse).returns("one" => mock("mount"))
@parser.expects(:parse).raises(ArgumentError)
config = Puppet::FileServing::Configuration.configuration
# Now parse again, so the exception gets thrown
config.send(:readconfig, false)
config.mounted?("one").should be_true
end
it "should add modules and plugins mounts even if the file does not exist" do
- Puppet::FileSystem::File.expects(:exist?).returns false # the file doesn't exist
+ Puppet::FileSystem.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
- Puppet::FileSystem::File.expects(:exist?).returns false # the file doesn't exist
+ Puppet::FileSystem.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('*')
plugins = stub 'plugins', :empty? => true
Puppet::FileServing::Mount::Plugins.stubs(:new).returns(plugins)
plugins.expects(:allow).with('*')
Puppet::FileServing::Configuration.configuration
end
it "should not allow access from all to modules and plugins if the fileserver.conf provided some rules" do
- Puppet::FileSystem::File.expects(:exist?).returns false # the file doesn't exist
+ Puppet::FileSystem.expects(:exist?).returns false # the file doesn't exist
modules = stub 'modules', :empty? => false
Puppet::FileServing::Mount::Modules.stubs(:new).returns(modules)
modules.expects(:allow).with('*').never
plugins = stub 'plugins', :empty? => false
Puppet::FileServing::Mount::Plugins.stubs(:new).returns(plugins)
plugins.expects(:allow).with('*').never
Puppet::FileServing::Configuration.configuration
end
it "should add modules and plugins mounts even if they are not returned by the parser" do
@parser.expects(:parse).returns("one" => mock("mount"))
- Puppet::FileSystem::File.expects(:exist?).returns true # the file doesn't exist
+ Puppet::FileSystem.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
end
end
describe "when finding the specified mount" do
it "should choose the named mount if one exists" do
config = Puppet::FileServing::Configuration.configuration
config.expects(:mounts).returns("one" => "foo")
config.find_mount("one", mock('env')).should == "foo"
end
it "should return nil if there is no such named mount" do
config = Puppet::FileServing::Configuration.configuration
env = mock 'environment'
mount = mock 'mount'
config.stubs(:mounts).returns("modules" => mount)
config.find_mount("foo", env).should be_nil
end
end
describe "#split_path" do
let(:config) { Puppet::FileServing::Configuration.configuration }
let(:request) { stub 'request', :key => "foo/bar/baz", :options => {}, :node => nil, :environment => mock("env") }
before do
config.stubs(:find_mount)
end
it "should reread the configuration" do
config.expects(:readconfig)
config.split_path(request)
end
it "should treat the first field of the URI path as the mount name" do
config.expects(:find_mount).with { |name, node| name == "foo" }
config.split_path(request)
end
it "should fail if the mount name is not alpha-numeric" do
request.expects(:key).returns "foo&bar/asdf"
expect { config.split_path(request) }.to raise_error(ArgumentError)
end
it "should support dashes in the mount name" do
request.expects(:key).returns "foo-bar/asdf"
expect { config.split_path(request) }.to_not raise_error
end
it "should use the mount name and environment to find the mount" do
config.expects(:find_mount).with { |name, env| name == "foo" and env == request.environment }
request.stubs(:node).returns("mynode")
config.split_path(request)
end
it "should return nil if the mount cannot be found" do
config.expects(:find_mount).returns nil
config.split_path(request).should be_nil
end
it "should return the mount and the relative path if the mount is found" do
mount = stub 'mount', :name => "foo"
config.expects(:find_mount).returns mount
config.split_path(request).should == [mount, "bar/baz"]
end
it "should remove any double slashes" do
request.stubs(:key).returns "foo/bar//baz"
mount = stub 'mount', :name => "foo"
config.expects(:find_mount).returns mount
config.split_path(request).should == [mount, "bar/baz"]
end
it "should fail if the path contains .." do
request.stubs(:key).returns 'module/foo/../../bar'
expect do
config.split_path(request)
end.to raise_error(ArgumentError, /Invalid relative path/)
end
it "should return the relative path as nil if it is an empty string" do
request.expects(:key).returns "foo"
mount = stub 'mount', :name => "foo"
config.expects(:find_mount).returns mount
config.split_path(request).should == [mount, nil]
end
it "should add 'modules/' to the relative path if the modules mount is used but not specified, for backward compatibility" do
request.expects(:key).returns "foo/bar"
mount = stub 'mount', :name => "modules"
config.expects(:find_mount).returns mount
config.split_path(request).should == [mount, "foo/bar"]
end
end
end
diff --git a/spec/unit/file_serving/content_spec.rb b/spec/unit/file_serving/content_spec.rb
index 2cb159f0a..168ef9292 100755
--- a/spec/unit/file_serving/content_spec.rb
+++ b/spec/unit/file_serving/content_spec.rb
@@ -1,117 +1,112 @@
#! /usr/bin/env ruby
require 'spec_helper'
require 'puppet/file_serving/content'
describe Puppet::FileServing::Content do
let(:path) { File.expand_path('/path') }
it "should be a subclass of Base" do
Puppet::FileServing::Content.superclass.should equal(Puppet::FileServing::Base)
end
it "should indirect file_content" do
Puppet::FileServing::Content.indirection.name.should == :file_content
end
it "should only support the raw format" do
Puppet::FileServing::Content.supported_formats.should == [:raw]
end
it "should have a method for collecting its attributes" do
Puppet::FileServing::Content.new(path).should respond_to(:collect)
end
it "should not retrieve and store its contents when its attributes are collected if the file is a normal file" do
content = Puppet::FileServing::Content.new(path)
result = "foo"
- stub_file = stub(path, :lstat => stub('stat', :ftype => "file"))
- Puppet::FileSystem::File.expects(:new).with(path).returns stub_file
+ Puppet::FileSystem.expects(:lstat).with(path).returns stub('stat', :ftype => "file")
File.expects(:read).with(path).never
content.collect
content.instance_variable_get("@content").should be_nil
end
it "should not attempt to retrieve its contents if the file is a directory" do
content = Puppet::FileServing::Content.new(path)
result = "foo"
- stub_file = stub(path, :lstat => stub('stat', :ftype => "directory"))
- Puppet::FileSystem::File.expects(:new).with(path).returns stub_file
+ Puppet::FileSystem.expects(:lstat).with(path).returns stub('stat', :ftype => "directory")
File.expects(:read).with(path).never
content.collect
content.instance_variable_get("@content").should be_nil
end
it "should have a method for setting its content" do
content = Puppet::FileServing::Content.new(path)
content.should respond_to(:content=)
end
it "should make content available when set externally" do
content = Puppet::FileServing::Content.new(path)
content.content = "foo/bar"
content.content.should == "foo/bar"
end
it "should be able to create a content instance from raw file contents" do
Puppet::FileServing::Content.should respond_to(:from_raw)
end
it "should create an instance with a fake file name and correct content when converting from raw" do
instance = mock 'instance'
Puppet::FileServing::Content.expects(:new).with("/this/is/a/fake/path").returns instance
instance.expects(:content=).with "foo/bar"
Puppet::FileServing::Content.from_raw("foo/bar").should equal(instance)
end
it "should return an opened File when converted to raw" do
content = Puppet::FileServing::Content.new(path)
File.expects(:new).with(path, "rb").returns :file
content.to_raw.should == :file
end
end
describe Puppet::FileServing::Content, "when returning the contents" do
let(:path) { File.expand_path('/my/path') }
let(:content) { Puppet::FileServing::Content.new(path, :links => :follow) }
it "should fail if the file is a symlink and links are set to :manage" do
content.links = :manage
- stub_file = stub(path, :lstat => stub("stat", :ftype => "symlink"))
- Puppet::FileSystem::File.expects(:new).with(path).returns stub_file
+ Puppet::FileSystem.expects(:lstat).with(path).returns stub("stat", :ftype => "symlink")
proc { content.content }.should raise_error(ArgumentError)
end
it "should fail if a path is not set" do
proc { content.content }.should raise_error(Errno::ENOENT)
end
it "should raise Errno::ENOENT if the file is absent" do
content.path = File.expand_path("/there/is/absolutely/no/chance/that/this/path/exists")
proc { content.content }.should raise_error(Errno::ENOENT)
end
it "should return the contents of the path if the file exists" do
- 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)
+ Puppet::FileSystem.expects(:stat).with(path).returns(stub('stat', :ftype => 'file'))
+ Puppet::FileSystem.expects(:binread).with(path).returns(:mycontent)
content.content.should == :mycontent
end
it "should cache the returned contents" do
- 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)
+ Puppet::FileSystem.expects(:stat).with(path).returns(stub('stat', :ftype => 'file'))
+ Puppet::FileSystem.expects(:binread).with(path).returns(:mycontent)
content.content
# The second run would throw a failure if the content weren't being cached.
content.content
end
end
diff --git a/spec/unit/file_serving/fileset_spec.rb b/spec/unit/file_serving/fileset_spec.rb
index 0e61a2a59..e4df5c93d 100755
--- a/spec/unit/file_serving/fileset_spec.rb
+++ b/spec/unit/file_serving/fileset_spec.rb
@@ -1,358 +1,354 @@
#! /usr/bin/env ruby
require 'spec_helper'
require 'puppet/file_serving/fileset'
describe Puppet::FileServing::Fileset do
include PuppetSpec::Files
let(:somefile) { make_absolute("/some/file") }
context "when initializing" do
it "requires a path" do
expect { Puppet::FileServing::Fileset.new }.to raise_error(ArgumentError)
end
it "fails if its path is not fully qualified" do
expect { Puppet::FileServing::Fileset.new("some/file") }.to raise_error(ArgumentError, "Fileset paths must be fully qualified: some/file")
end
it "removes a trailing file path separator" do
path_with_separator = "#{somefile}#{File::SEPARATOR}"
- stub_file = stub(somefile, :lstat => stub('stat'))
- Puppet::FileSystem::File.expects(:new).with(somefile).returns stub_file
+ Puppet::FileSystem.expects(:lstat).with(somefile).returns stub('stat')
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)
- stub_file = stub(path, :lstat => stub('stat'))
- Puppet::FileSystem::File.expects(:new).with(path).returns stub_file
+ Puppet::FileSystem.expects(:lstat).with(path).returns stub('stat')
fileset = Puppet::FileServing::Fileset.new(path)
fileset.path.should == path
end
it "fails if its path does not exist" do
- mock_file = mock(somefile)
- Puppet::FileSystem::File.expects(:new).with(somefile).returns mock_file
- mock_file.expects(:lstat).raises(Errno::ENOENT)
+ Puppet::FileSystem.expects(:lstat).with(somefile).raises(Errno::ENOENT)
expect { Puppet::FileServing::Fileset.new(somefile) }.to raise_error(ArgumentError, "Fileset paths must exist")
end
it "accepts a 'recurse' option" do
- stub_file = stub(somefile, :lstat => stub('stat'))
- Puppet::FileSystem::File.expects(:new).with(somefile).returns stub_file
+ Puppet::FileSystem.expects(:lstat).with(somefile).returns stub('stat')
set = Puppet::FileServing::Fileset.new(somefile, :recurse => true)
set.recurse.should be_true
end
it "accepts a 'recurselimit' option" do
- stub_file = stub(somefile, :lstat => stub('stat'))
- Puppet::FileSystem::File.expects(:new).with(somefile).returns stub_file
+ Puppet::FileSystem.expects(:lstat).with(somefile).returns stub('stat')
set = Puppet::FileServing::Fileset.new(somefile, :recurselimit => 3)
set.recurselimit.should == 3
end
it "accepts an 'ignore' option" do
- stub_file = stub(somefile, :lstat => stub('stat'))
- Puppet::FileSystem::File.expects(:new).with(somefile).returns stub_file
+ Puppet::FileSystem.expects(:lstat).with(somefile).returns stub('stat')
set = Puppet::FileServing::Fileset.new(somefile, :ignore => ".svn")
set.ignore.should == [".svn"]
end
it "accepts a 'links' option" do
- stub_file = stub(somefile, :lstat => stub('stat'))
- Puppet::FileSystem::File.expects(:new).with(somefile).returns stub_file
+ Puppet::FileSystem.expects(:lstat).with(somefile).returns stub('stat')
set = Puppet::FileServing::Fileset.new(somefile, :links => :manage)
set.links.should == :manage
end
it "accepts a 'checksum_type' option" do
- stub_file = stub(somefile, :lstat => stub('stat'))
- Puppet::FileSystem::File.expects(:new).with(somefile).returns stub_file
+ Puppet::FileSystem.expects(:lstat).with(somefile).returns stub('stat')
set = Puppet::FileServing::Fileset.new(somefile, :checksum_type => :test)
set.checksum_type.should == :test
end
it "fails if 'links' is set to anything other than :manage or :follow" do
expect { Puppet::FileServing::Fileset.new(somefile, :links => :whatever) }.to raise_error(ArgumentError, "Invalid :links value 'whatever'")
end
it "defaults to 'false' for recurse" do
- stub_file = stub(somefile, :lstat => stub('stat'))
- Puppet::FileSystem::File.expects(:new).with(somefile).returns stub_file
+ Puppet::FileSystem.expects(:lstat).with(somefile).returns stub('stat')
Puppet::FileServing::Fileset.new(somefile).recurse.should == false
end
it "defaults to :infinite for recurselimit" do
- stub_file = stub(somefile, :lstat => stub('stat'))
- Puppet::FileSystem::File.expects(:new).with(somefile).returns stub_file
+ Puppet::FileSystem.expects(:lstat).with(somefile).returns stub('stat')
Puppet::FileServing::Fileset.new(somefile).recurselimit.should == :infinite
end
it "defaults to an empty ignore list" do
- stub_file = stub(somefile, :lstat => stub('stat'))
- Puppet::FileSystem::File.expects(:new).with(somefile).returns stub_file
+ Puppet::FileSystem.expects(:lstat).with(somefile).returns stub('stat')
Puppet::FileServing::Fileset.new(somefile).ignore.should == []
end
it "defaults to :manage for links" do
- stub_file = stub(somefile, :lstat => stub('stat'))
- Puppet::FileSystem::File.expects(:new).with(somefile).returns stub_file
+ Puppet::FileSystem.expects(:lstat).with(somefile).returns stub('stat')
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
- Puppet::FileSystem::File.expects(:new).with(somefile).returns stub_file
+ Puppet::FileSystem.expects(:lstat).with(somefile).returns stub('stat')
end
[:recurse, :recurselimit, :ignore, :links].each do |option|
it "passes the #{option} option on to the fileset if present" do
request = Puppet::Indirector::Request.new(:file_serving, :find, "foo", nil, {option => values[option]})
Puppet::FileServing::Fileset.new(somefile, request).send(option).should == values[option]
end
end
it "converts the integer as a string to their integer counterpart when setting options" do
request = Puppet::Indirector::Request.new(:file_serving, :find, "foo", nil,
{:recurselimit => "1234"})
Puppet::FileServing::Fileset.new(somefile, request).recurselimit.should == 1234
end
it "converts the string 'true' to the boolean true when setting options" do
request = Puppet::Indirector::Request.new(:file_serving, :find, "foo", nil,
{:recurse => "true"})
Puppet::FileServing::Fileset.new(somefile, request).recurse.should == true
end
it "converts the string 'false' to the boolean false when setting options" do
request = Puppet::Indirector::Request.new(:file_serving, :find, "foo", nil,
{:recurse => "false"})
Puppet::FileServing::Fileset.new(somefile, request).recurse.should == false
end
end
end
context "when recursing" do
before do
@path = make_absolute("/my/path")
- @stub_file = stub(@path, :lstat => stub('stat', :directory? => true))
- Puppet::FileSystem::File.stubs(:new).with(@path).returns @stub_file
+ Puppet::FileSystem.stubs(:lstat).with(@path).returns stub('stat', :directory? => true)
+
@fileset = Puppet::FileServing::Fileset.new(@path)
@dirstat = stub 'dirstat', :directory? => true
@filestat = stub 'filestat', :directory? => false
end
def mock_dir_structure(path, stat_method = :lstat)
- @stub_file.stubs(stat_method).returns(@dirstat)
- Dir.stubs(:entries).with(path).returns(%w{one two .svn CVS})
+ Puppet::FileSystem.stubs(stat_method).with(path).returns @dirstat
# Keep track of the files we're stubbing.
@files = %w{.}
- %w{one two .svn CVS}.each do |subdir|
+ top_names = %w{one two .svn CVS}
+ sub_names = %w{file1 file2 .svn CVS 0 false}
+
+ Dir.stubs(:entries).with(path).returns(top_names)
+ top_names.each do |subdir|
@files << subdir # relative path
subpath = File.join(path, subdir)
- 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|
+ Puppet::FileSystem.stubs(stat_method).with(subpath).returns @dirstat
+ Dir.stubs(:entries).with(subpath).returns(sub_names)
+ sub_names.each do |file|
@files << File.join(subdir, file) # relative path
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
+ Puppet::FileSystem.stubs(stat_method).with(subfile_path).returns(@filestat)
end
end
end
MockStat = Struct.new(:path, :directory) do
# struct doesn't support thing ending in ?
def directory?
directory
end
end
MockDirectory = Struct.new(:name, :entries) do
def mock(base_path)
extend Mocha::API
path = File.join(base_path, name)
- stub_dir = stub(path, :lstat => MockStat.new(path, true))
- Puppet::FileSystem::File.stubs(:new).with(path).returns stub_dir
+ Puppet::FileSystem.stubs(:lstat).with(path).returns MockStat.new(path, true)
Dir.stubs(:entries).with(path).returns(['.', '..'] + entries.map(&:name))
entries.each do |entry|
entry.mock(path)
end
end
end
MockFile = Struct.new(:name) do
def mock(base_path)
extend Mocha::API
path = File.join(base_path, name)
- stub_file = stub(path, :lstat => MockStat.new(path, false))
- Puppet::FileSystem::File.stubs(:new).with(path).returns stub_file
+ Puppet::FileSystem.stubs(:lstat).with(path).returns MockStat.new(path, false)
end
end
it "doesn't ignore pending directories when the last entry at the top level is a file" do
structure = MockDirectory.new('path',
[MockDirectory.new('dir1',
[MockDirectory.new('a', [MockFile.new('f')])]),
MockFile.new('file')])
structure.mock(make_absolute('/your'))
fileset = Puppet::FileServing::Fileset.new(make_absolute('/your/path'))
fileset.recurse = true
fileset.links = :manage
fileset.files.should == [".", "dir1", "file", "dir1/a", "dir1/a/f"]
end
it "recurses through the whole file tree if :recurse is set to 'true'" do
mock_dir_structure(@path)
@fileset.recurse = true
@fileset.files.sort.should == @files.sort
end
it "does not recurse if :recurse is set to 'false'" do
mock_dir_structure(@path)
@fileset.recurse = false
@fileset.files.should == %w{.}
end
it "recurses to the level set by :recurselimit" do
mock_dir_structure(@path)
@fileset.recurse = true
@fileset.recurselimit = 1
@fileset.files.should == %w{. one two .svn CVS}
end
it "ignores the '.' and '..' directories in subdirectories" do
mock_dir_structure(@path)
@fileset.recurse = true
@fileset.files.sort.should == @files.sort
end
it "does not fail if the :ignore value provided is nil" do
mock_dir_structure(@path)
@fileset.recurse = true
@fileset.ignore = nil
expect { @fileset.files }.to_not raise_error
end
it "ignores files that match a single pattern in the ignore list" do
mock_dir_structure(@path)
@fileset.recurse = true
@fileset.ignore = ".svn"
@fileset.files.find { |file| file.include?(".svn") }.should be_nil
end
it "ignores files that match any of multiple patterns in the ignore list" do
mock_dir_structure(@path)
@fileset.recurse = true
@fileset.ignore = %w{.svn CVS}
@fileset.files.find { |file| file.include?(".svn") or file.include?("CVS") }.should be_nil
end
- it "uses Puppet::FileSystem::File#stat if :links is set to :follow" do
+ it "ignores files that match a pattern given as a number" do
+ mock_dir_structure(@path)
+ @fileset.recurse = true
+ @fileset.ignore = [0]
+ @fileset.files.find { |file| file.include?("0") }.should be_nil
+ end
+
+ it "ignores files that match a pattern given as a boolean" do
+ mock_dir_structure(@path)
+ @fileset.recurse = true
+ @fileset.ignore = [false]
+ @fileset.files.find { |file| file.include?("false") }.should be_nil
+ end
+
+ it "uses Puppet::FileSystem#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 Puppet::FileSystem::File#lstat if :links is set to :manage" do
+ it "uses Puppet::FileSystem#lstat if :links is set to :manage" do
mock_dir_structure(@path, :lstat)
@fileset.recurse = true
@fileset.links = :manage
@fileset.files.sort.should == @files.sort
end
it "works when paths have regexp significant characters" do
@path = make_absolute("/my/path/rV1x2DafFr0R6tGG+1bbk++++TM")
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
+ Puppet::FileSystem.expects(:lstat).with(@path).returns stub(@path, :stat => stat, :lstat => stat)
@fileset = Puppet::FileServing::Fileset.new(@path)
mock_dir_structure(@path)
@fileset.recurse = true
@fileset.files.sort.should == @files.sort
end
end
it "manages the links to missing files" do
path = make_absolute("/my/path")
stat = stub 'stat', :directory? => true
- mock_file = mock(path, :lstat => stat, :stat => stat)
- Puppet::FileSystem::File.expects(:new).with(path).twice.returns mock_file
+ Puppet::FileSystem.expects(:stat).with(path).returns stat
+ Puppet::FileSystem.expects(:lstat).with(path).returns stat
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)
+ Puppet::FileSystem.expects(:stat).with(link_path).raises(Errno::ENOENT)
Dir.stubs(:entries).with(path).returns(["mylink"])
fileset = Puppet::FileServing::Fileset.new(path)
fileset.links = :follow
fileset.recurse = true
fileset.files.sort.should == %w{. mylink}.sort
end
context "when merging other filesets" do
before do
@paths = [make_absolute("/first/path"), make_absolute("/second/path"), make_absolute("/third/path")]
- stub_file = stub(:lstat => stub('stat', :directory? => false))
- Puppet::FileSystem::File.stubs(:new).returns stub_file
+ Puppet::FileSystem.stubs(:lstat).returns stub('stat', :directory? => false)
@filesets = @paths.collect do |path|
- stub_dir = stub(path, :lstat => stub('stat', :directory? => true))
- Puppet::FileSystem::File.stubs(:new).with(path).returns stub_dir
+ Puppet::FileSystem.stubs(:lstat).with(path).returns stub('stat', :directory? => true)
Puppet::FileServing::Fileset.new(path, :recurse => true)
end
Dir.stubs(:entries).returns []
end
it "returns a hash of all files in each fileset with the value being the base path" do
Dir.expects(:entries).with(make_absolute("/first/path")).returns(%w{one uno})
Dir.expects(:entries).with(make_absolute("/second/path")).returns(%w{two dos})
Dir.expects(:entries).with(make_absolute("/third/path")).returns(%w{three tres})
Puppet::FileServing::Fileset.merge(*@filesets).should == {
"." => make_absolute("/first/path"),
"one" => make_absolute("/first/path"),
"uno" => make_absolute("/first/path"),
"two" => make_absolute("/second/path"),
"dos" => make_absolute("/second/path"),
"three" => make_absolute("/third/path"),
"tres" => make_absolute("/third/path"),
}
end
it "includes the base directory from the first fileset" do
Dir.expects(:entries).with(make_absolute("/first/path")).returns(%w{one})
Dir.expects(:entries).with(make_absolute("/second/path")).returns(%w{two})
Puppet::FileServing::Fileset.merge(*@filesets)["."].should == make_absolute("/first/path")
end
it "uses the base path of the first found file when relative file paths conflict" do
Dir.expects(:entries).with(make_absolute("/first/path")).returns(%w{one})
Dir.expects(:entries).with(make_absolute("/second/path")).returns(%w{one})
Puppet::FileServing::Fileset.merge(*@filesets)["one"].should == make_absolute("/first/path")
end
end
end
diff --git a/spec/unit/file_serving/metadata_spec.rb b/spec/unit/file_serving/metadata_spec.rb
index 8b74d4b7e..f3b0c4347 100755
--- a/spec/unit/file_serving/metadata_spec.rb
+++ b/spec/unit/file_serving/metadata_spec.rb
@@ -1,381 +1,432 @@
#! /usr/bin/env ruby
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
+require 'matchers/json'
describe Puppet::FileServing::Metadata do
let(:foobar) { File.expand_path('/foo/bar') }
it "should be a subclass of Base" do
Puppet::FileServing::Metadata.superclass.should equal(Puppet::FileServing::Base)
end
it "should indirect file_metadata" do
Puppet::FileServing::Metadata.indirection.name.should == :file_metadata
end
it "should have a method that triggers attribute collection" do
Puppet::FileServing::Metadata.new(foobar).should respond_to(:collect)
end
it "should support pson serialization" do
Puppet::FileServing::Metadata.new(foobar).should respond_to(:to_pson)
end
it "should support to_pson_data_hash" do
Puppet::FileServing::Metadata.new(foobar).should respond_to(:to_pson_data_hash)
end
- it "should support pson deserialization" do
- Puppet::FileServing::Metadata.should respond_to(:from_pson)
+ it "should support deserialization" do
+ Puppet::FileServing::Metadata.should respond_to(:from_data_hash)
end
describe "when serializing" do
let(:metadata) { Puppet::FileServing::Metadata.new(foobar) }
it "should serialize as FileMetadata" do
metadata.to_pson_data_hash['document_type'].should == "FileMetadata"
end
it "the data should include the path, relative_path, links, owner, group, mode, checksum, type, and destination" do
metadata.to_pson_data_hash['data'].keys.sort.should == %w{ path relative_path links owner group mode checksum type destination }.sort
end
it "should pass the path in the hash verbatim" do
metadata.to_pson_data_hash['data']['path'].should == metadata.path
end
it "should pass the relative_path in the hash verbatim" do
metadata.to_pson_data_hash['data']['relative_path'].should == metadata.relative_path
end
it "should pass the links in the hash verbatim" do
metadata.to_pson_data_hash['data']['links'].should == metadata.links
end
it "should pass the path owner in the hash verbatim" do
metadata.to_pson_data_hash['data']['owner'].should == metadata.owner
end
it "should pass the group in the hash verbatim" do
metadata.to_pson_data_hash['data']['group'].should == metadata.group
end
it "should pass the mode in the hash verbatim" do
metadata.to_pson_data_hash['data']['mode'].should == metadata.mode
end
it "should pass the ftype in the hash verbatim as the 'type'" do
metadata.to_pson_data_hash['data']['type'].should == metadata.ftype
end
it "should pass the destination verbatim" do
metadata.to_pson_data_hash['data']['destination'].should == metadata.destination
end
it "should pass the checksum in the hash as a nested hash" do
metadata.to_pson_data_hash['data']['checksum'].should be_is_a(Hash)
end
it "should pass the checksum_type in the hash verbatim as the checksum's type" do
metadata.to_pson_data_hash['data']['checksum']['type'].should == metadata.checksum_type
end
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
describe Puppet::FileServing::Metadata do
+ include JSONMatchers
include PuppetSpec::Files
shared_examples_for "metadata collector" do
let(:metadata) do
data = described_class.new(path)
data.collect
data
end
describe "when collecting attributes" do
describe "when managing files" do
let(:path) { tmpfile('file_serving_metadata') }
before :each do
FileUtils.touch(path)
end
it "should set the owner to the file's current owner" do
metadata.owner.should == owner
end
it "should set the group to the file's current group" do
metadata.group.should == group
end
it "should set the mode to the file's masked mode" do
set_mode(33261, path)
metadata.mode.should == 0755
end
describe "#checksum" do
let(:checksum) { Digest::MD5.hexdigest("some content\n") }
before :each do
File.open(path, "wb") {|f| f.print("some content\n")}
end
it "should default to a checksum of type MD5 with the file's current checksum" do
metadata.checksum.should == "{md5}#{checksum}"
end
it "should give a mtime checksum when checksum_type is set" do
time = Time.now
metadata.checksum_type = "mtime"
metadata.expects(:mtime_file).returns(@time)
metadata.collect
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)
+ it "should validate against the schema" do
+ expect(metadata.to_pson).to validate_against('api/schemas/file_metadata.json')
end
end
describe "when managing directories" do
let(:path) { tmpdir('file_serving_metadata_dir') }
let(:time) { Time.now }
before :each do
metadata.expects(:ctime_file).returns(time)
end
it "should only use checksums of type 'ctime' for directories" do
metadata.collect
metadata.checksum.should == "{ctime}#{time}"
end
it "should only use checksums of type 'ctime' for directories even if checksum_type set" do
metadata.checksum_type = "mtime"
metadata.expects(:mtime_file).never
metadata.collect
metadata.checksum.should == "{ctime}#{time}"
end
- it "should validate against the schema", :unless => Puppet.features.microsoft_windows? do
+ it "should validate against the schema" do
metadata.collect
- validate_json_for_file_metadata(metadata)
+ expect(metadata.to_pson).to validate_against('api/schemas/file_metadata.json')
end
end
end
end
+ describe "WindowsStat", :if => Puppet.features.microsoft_windows? do
+ include PuppetSpec::Files
+
+ it "should return default owner, group and mode when the given path has an invalid DACL (such as a non-NTFS volume)" do
+ invalid_error = Puppet::Util::Windows::Error.new('Invalid DACL', 1336)
+ path = tmpfile('foo')
+ FileUtils.touch(path)
+
+ Puppet::Util::Windows::Security.stubs(:get_owner).with(path).raises(invalid_error)
+ Puppet::Util::Windows::Security.stubs(:get_group).with(path).raises(invalid_error)
+ Puppet::Util::Windows::Security.stubs(:get_mode).with(path).raises(invalid_error)
+
+ stat = Puppet::FileSystem.stat(path)
+
+ win_stat = Puppet::FileServing::Metadata::WindowsStat.new(stat, path)
+
+ win_stat.owner.should == 'S-1-5-32-544'
+ win_stat.group.should == 'S-1-0-0'
+ win_stat.mode.should == 0644
+ end
+
+ it "should still raise errors that are not the result of an 'Invalid DACL'" do
+ invalid_error = ArgumentError.new('bar')
+ path = tmpfile('bar')
+ FileUtils.touch(path)
+
+ Puppet::Util::Windows::Security.stubs(:get_owner).with(path).raises(invalid_error)
+ Puppet::Util::Windows::Security.stubs(:get_group).with(path).raises(invalid_error)
+ Puppet::Util::Windows::Security.stubs(:get_mode).with(path).raises(invalid_error)
+
+ stat = Puppet::FileSystem.stat(path)
+
+ win_stat = Puppet::FileServing::Metadata::WindowsStat.new(stat, path)
+
+ expect { win_stat.owner }.to raise_error(ArgumentError)
+ expect { win_stat.group }.to raise_error(ArgumentError)
+ expect { win_stat.mode }.to raise_error(ArgumentError)
+ end
+ end
+
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) { Puppet::FileSystem::File.new(path).lstat.mode & 0777 }
+ let(:fmode) { Puppet::FileSystem.lstat(path).mode & 0777 }
before :each do
File.open(target, "wb") {|f| f.print("some content\n")}
set_mode(0644, target)
- Puppet::FileSystem::File.new(target).symlink(path)
+ Puppet::FileSystem.symlink(target, 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)
+ it "should validate against the schema" do
+ expect(metadata.to_pson).to validate_against('api/schemas/file_metadata.json')
end
end
end
describe Puppet::FileServing::Metadata, " when finding the file to use for setting attributes" do
let(:path) { tmpfile('file_serving_metadata_find_file') }
before :each do
File.open(path, "wb") {|f| f.print("some content\n")}
set_mode(0755, path)
end
it "should accept a base path to which the file should be relative" do
dir = tmpdir('metadata_dir')
metadata = described_class.new(dir)
metadata.relative_path = 'relative_path'
FileUtils.touch(metadata.full_path)
metadata.collect
end
it "should use the set base path if one is not provided" do
metadata.collect
end
it "should raise an exception if the file does not exist" do
File.delete(path)
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)
+ it "should validate against the schema" do
+ expect(metadata.to_pson).to validate_against('api/schemas/file_metadata.json')
end
end
end
describe "on POSIX systems", :if => Puppet.features.posix? do
let(:owner) {10}
let(:group) {20}
before :each do
File::Stat.any_instance.stubs(:uid).returns owner
File::Stat.any_instance.stubs(:gid).returns group
end
it_should_behave_like "metadata collector"
it_should_behave_like "metadata collector symlinks"
def set_mode(mode, path)
File.chmod(mode, path)
end
end
describe "on Windows systems", :if => Puppet.features.microsoft_windows? do
let(:owner) {'S-1-1-50'}
let(:group) {'S-1-1-51'}
before :each do
require 'puppet/util/windows/security'
Puppet::Util::Windows::Security.stubs(:get_owner).returns owner
Puppet::Util::Windows::Security.stubs(:get_group).returns group
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') }
let(:metadata) do
data = described_class.new(path)
data.collect
data
end
+ let (:invalid_dacl_error) do
+ Puppet::Util::Windows::Error.new('Invalid DACL', 1336)
+ end
it "should default owner" do
Puppet::Util::Windows::Security.stubs(:get_owner).returns nil
metadata.owner.should == 'S-1-5-32-544'
end
it "should default group" do
Puppet::Util::Windows::Security.stubs(:get_group).returns nil
metadata.group.should == 'S-1-0-0'
end
it "should default mode" do
Puppet::Util::Windows::Security.stubs(:get_mode).returns nil
metadata.mode.should == 0644
end
+
+ describe "when the path raises an Invalid ACL error" do
+ # these simulate the behavior of a symlink file whose target does not support ACLs
+ it "should default owner" do
+ Puppet::Util::Windows::Security.stubs(:get_owner).raises(invalid_dacl_error)
+
+ metadata.owner.should == 'S-1-5-32-544'
+ end
+
+ it "should default group" do
+ Puppet::Util::Windows::Security.stubs(:get_group).raises(invalid_dacl_error)
+
+ metadata.group.should == 'S-1-0-0'
+ end
+
+ it "should default mode" do
+ Puppet::Util::Windows::Security.stubs(:get_mode).raises(invalid_dacl_error)
+
+ metadata.mode.should == 0644
+ end
+ end
+
end
def set_mode(mode, path)
Puppet::Util::Windows::Security.set_mode(mode, path)
end
end
end
describe Puppet::FileServing::Metadata, " when pointing to a link", :if => Puppet.features.manages_symlinks? do
describe "when links are managed" do
before do
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
+ Puppet::FileSystem.expects(:lstat).with(path).at_least_once.returns stat
+ Puppet::FileSystem.expects(:readlink).with(path).at_least_once.returns "/some/other/path"
@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"
end
pending "should not collect the checksum if links are :manage" do
# We'd like this to be true, but we need to always collect the checksum because in the server/client/server round trip we lose the distintion between manage and follow.
@file.collect
@file.checksum.should be_nil
end
it "should collect the checksum if links are :manage" do # see pending note above
@file.collect
@file.checksum.should == "{md5}#{@checksum}"
end
end
describe "when links are followed" do
before do
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
+ Puppet::FileSystem.expects(:stat).with(path).at_least_once.returns stat
+ Puppet::FileSystem.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
it "should not store the destination of the link in :destination if links are :follow" do
@file.collect
@file.destination.should be_nil
end
it "should collect the checksum if links are :follow" do
@file.collect
@file.checksum.should == "{md5}#{@checksum}"
end
end
end
diff --git a/spec/unit/file_serving/mount/file_spec.rb b/spec/unit/file_serving/mount/file_spec.rb
index 5821f3f98..f889b8eb1 100755
--- a/spec/unit/file_serving/mount/file_spec.rb
+++ b/spec/unit/file_serving/mount/file_spec.rb
@@ -1,189 +1,189 @@
#! /usr/bin/env ruby
require 'spec_helper'
require 'puppet/file_serving/mount/file'
module FileServingMountTesting
def stub_facter(hostname)
Facter.stubs(:value).with("hostname").returns(hostname.sub(/\..+/, ''))
Facter.stubs(:value).with("domain").returns(hostname.sub(/^[^.]+\./, ''))
end
end
describe Puppet::FileServing::Mount::File do
it "should be invalid if it does not have a path" do
expect { Puppet::FileServing::Mount::File.new("foo").validate }.to raise_error(ArgumentError)
end
it "should be valid if it has a path" do
FileTest.stubs(:directory?).returns true
FileTest.stubs(:readable?).returns true
mount = Puppet::FileServing::Mount::File.new("foo")
mount.path = "/foo"
expect { mount.validate }.not_to raise_error
end
describe "when setting the path" do
before do
@mount = Puppet::FileServing::Mount::File.new("test")
@dir = "/this/path/does/not/exist"
end
it "should fail if the path is not a directory" do
FileTest.expects(:directory?).returns(false)
expect { @mount.path = @dir }.to raise_error(ArgumentError)
end
it "should fail if the path is not readable" do
FileTest.expects(:directory?).returns(true)
FileTest.expects(:readable?).returns(false)
expect { @mount.path = @dir }.to raise_error(ArgumentError)
end
end
describe "when substituting hostnames and ip addresses into file paths" do
include FileServingMountTesting
before do
FileTest.stubs(:directory?).returns(true)
FileTest.stubs(:readable?).returns(true)
@mount = Puppet::FileServing::Mount::File.new("test")
@host = "host.domain.com"
end
after :each do
Puppet::FileServing::Mount::File.instance_variable_set(:@localmap, nil)
end
it "should replace incidences of %h in the path with the client's short name" do
@mount.path = "/dir/%h/yay"
@mount.path(@host).should == "/dir/host/yay"
end
it "should replace incidences of %H in the path with the client's fully qualified name" do
@mount.path = "/dir/%H/yay"
@mount.path(@host).should == "/dir/host.domain.com/yay"
end
it "should replace incidences of %d in the path with the client's domain name" do
@mount.path = "/dir/%d/yay"
@mount.path(@host).should == "/dir/domain.com/yay"
end
it "should perform all necessary replacements" do
@mount.path = "/%h/%d/%H"
@mount.path(@host).should == "/host/domain.com/host.domain.com"
end
it "should use local host information if no client data is provided" do
stub_facter("myhost.mydomain.com")
@mount.path = "/%h/%d/%H"
@mount.path.should == "/myhost/mydomain.com/myhost.mydomain.com"
end
end
describe "when determining the complete file path" do
include FileServingMountTesting
before do
- Puppet::FileSystem::File.stubs(:exist?).returns(true)
+ Puppet::FileSystem.stubs(:exist?).returns(true)
FileTest.stubs(:directory?).returns(true)
FileTest.stubs(:readable?).returns(true)
@mount = Puppet::FileServing::Mount::File.new("test")
@mount.path = "/mount"
stub_facter("myhost.mydomain.com")
@host = "host.domain.com"
end
it "should return nil if the file is absent" do
- Puppet::FileSystem::File.stubs(:exist?).returns(false)
+ Puppet::FileSystem.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
- Puppet::FileSystem::File.stubs(:exist?).returns(false)
+ Puppet::FileSystem.stubs(:exist?).returns(false)
Puppet.expects(:info).with("File does not exist or is not accessible: /mount/my/path")
@mount.complete_path("/my/path", nil)
end
it "should return the file path if the file is present" do
- Puppet::FileSystem::File.stubs(:exist?).with("/my/path").returns(true)
+ Puppet::FileSystem.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
- Puppet::FileSystem::File.stubs(:exist?).returns(true)
+ Puppet::FileSystem.stubs(:exist?).returns(true)
@mount.complete_path(nil, nil).should == "/mount"
end
it "should use the client host name if provided in the options" do
@mount.path = "/mount/%h"
@mount.complete_path("/my/path", @host).should == "/mount/host/my/path"
end
it "should perform replacements on the base path" do
@mount.path = "/blah/%h"
@mount.complete_path("/my/stuff", @host).should == "/blah/host/my/stuff"
end
it "should not perform replacements on the per-file path" do
@mount.path = "/blah"
@mount.complete_path("/%h/stuff", @host).should == "/blah/%h/stuff"
end
it "should look for files relative to its base directory" do
@mount.complete_path("/my/stuff", @host).should == "/mount/my/stuff"
end
end
describe "when finding files" do
include FileServingMountTesting
before do
- Puppet::FileSystem::File.stubs(:exist?).returns(true)
+ Puppet::FileSystem.stubs(:exist?).returns(true)
FileTest.stubs(:directory?).returns(true)
FileTest.stubs(:readable?).returns(true)
@mount = Puppet::FileServing::Mount::File.new("test")
@mount.path = "/mount"
stub_facter("myhost.mydomain.com")
@host = "host.domain.com"
@request = stub 'request', :node => "foo"
end
it "should return the results of the complete file path" do
- Puppet::FileSystem::File.stubs(:exist?).returns(false)
+ Puppet::FileSystem.stubs(:exist?).returns(false)
@mount.expects(:complete_path).with("/my/path", "foo").returns "eh"
@mount.find("/my/path", @request).should == "eh"
end
end
describe "when searching for files" do
include FileServingMountTesting
before do
- Puppet::FileSystem::File.stubs(:exist?).returns(true)
+ Puppet::FileSystem.stubs(:exist?).returns(true)
FileTest.stubs(:directory?).returns(true)
FileTest.stubs(:readable?).returns(true)
@mount = Puppet::FileServing::Mount::File.new("test")
@mount.path = "/mount"
stub_facter("myhost.mydomain.com")
@host = "host.domain.com"
@request = stub 'request', :node => "foo"
end
it "should return the results of the complete file path as an array" do
- Puppet::FileSystem::File.stubs(:exist?).returns(false)
+ Puppet::FileSystem.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
- Puppet::FileSystem::File.stubs(:exist?).returns(false)
+ Puppet::FileSystem.stubs(:exist?).returns(false)
@mount.expects(:complete_path).with("/my/path", "foo").returns nil
@mount.search("/my/path", @request).should be_nil
end
end
end
diff --git a/spec/unit/file_system/file_spec.rb b/spec/unit/file_system/file_spec.rb
deleted file mode 100644
index 564d4096c..000000000
--- a/spec/unit/file_system/file_spec.rb
+++ /dev/null
@@ -1,486 +0,0 @@
-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
index 5ad0a8abc..eb13b0406 100644
--- a/spec/unit/file_system/tempfile_spec.rb
+++ b/spec/unit/file_system/tempfile_spec.rb
@@ -1,48 +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")
+ expect(Puppet::FileSystem.read(file.path)).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
+ expect(Puppet::FileSystem.exist?(filename)).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
+ expect(Puppet::FileSystem.exist?(filename)).to be_false
end
end
diff --git a/spec/unit/file_system_spec.rb b/spec/unit/file_system_spec.rb
new file mode 100644
index 000000000..f3b36a66d
--- /dev/null
+++ b/spec/unit/file_system_spec.rb
@@ -0,0 +1,508 @@
+require 'spec_helper'
+require 'puppet/file_system'
+require 'puppet/util/platform'
+
+describe "Puppet::FileSystem" do
+ include PuppetSpec::Files
+
+ context "#exclusive_open" do
+ it "opens ands allows updating of an existing file" do
+ file = file_containing("file_to_update", "the contents")
+
+ Puppet::FileSystem.exclusive_open(file, 0660, 'r+') do |fh|
+ old = fh.read
+ fh.truncate(0)
+ fh.rewind
+ fh.write("updated #{old}")
+ end
+
+ expect(Puppet::FileSystem.read(file)).to eq("updated the contents")
+ end
+
+ it "opens, creates ands allows updating of a new file" do
+ file = tmpfile("file_to_update")
+
+ Puppet::FileSystem.exclusive_open(file, 0660, 'w') do |fh|
+ fh.write("updated new file")
+ end
+
+ expect(Puppet::FileSystem.read(file)).to eq("updated new file")
+ end
+
+ it "excludes other processes from updating at the same time", :unless => Puppet::Util::Platform.windows? do
+ file = file_containing("file_to_update", "0")
+
+ increment_counter_in_multiple_processes(file, 5, 'r+')
+
+ expect(Puppet::FileSystem.read(file)).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 = tmpfile("file_to_update")
+
+ increment_counter_in_multiple_processes(file, 5, 'a+')
+
+ expect(Puppet::FileSystem.read(file)).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.exclusive_open(file, 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.exclusive_open(file, 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 = []
+ num_procs.times do
+ children << Kernel.fork do
+ Puppet::FileSystem.exclusive_open(file, 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) { tmpfile("somefile") }
+ let(:missing_file) { 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)
+ end
+
+ it "should raise an error when trying to create a symlink" do
+ expect { Puppet::FileSystem.symlink(file, 'foo') }.to raise_error(Puppet::Util::Windows::Error)
+ end
+
+ it "should return false when trying to check if a path is a symlink" do
+ Puppet::FileSystem.symlink?(file).should be_false
+ end
+
+ it "should raise an error when trying to read a symlink" do
+ expect { Puppet::FileSystem.readlink(file) }.to raise_error(Puppet::Util::Windows::Error)
+ end
+
+ it "should return a File::Stat instance when calling stat on an existing file" do
+ Puppet::FileSystem.stat(file).should be_instance_of(File::Stat)
+ end
+
+ it "should raise Errno::ENOENT when calling stat on a missing file" do
+ expect { Puppet::FileSystem.stat(missing_file) }.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(Puppet::FileSystem.assert_path(file))
+
+ Puppet::FileSystem.lstat(file)
+ end
+ end
+
+ describe "symlink", :if => Puppet.features.manages_symlinks? do
+
+ let(:file) { tmpfile("somefile") }
+ let(:missing_file) { tmpfile("missingfile") }
+ let(:dir) { tmpdir("somedir") }
+
+ before :each do
+ FileUtils.touch(file)
+ end
+
+ it "should return true for exist? on a present file" do
+ Puppet::FileSystem.exist?(file).should be_true
+ end
+
+ it "should return true for file? on a present file" do
+ Puppet::FileSystem.file?(file).should be_true
+ end
+
+ it "should return false for exist? on a non-existant file" do
+ Puppet::FileSystem.exist?(missing_file).should be_false
+ end
+
+ it "should return true for exist? on a present directory" do
+ Puppet::FileSystem.exist?(dir).should be_true
+ end
+
+ it "should return false for exist? on a dangling symlink" do
+ symlink = tmpfile("somefile_link")
+ Puppet::FileSystem.symlink(missing_file, symlink)
+
+ Puppet::FileSystem.exist?(missing_file).should be_false
+ Puppet::FileSystem.exist?(symlink).should be_false
+ end
+
+ it "should return true for exist? on valid symlinks" do
+ [file, dir].each do |target|
+ symlink = tmpfile("#{Puppet::FileSystem.basename(target).to_s}_link")
+ Puppet::FileSystem.symlink(target, symlink)
+
+ Puppet::FileSystem.exist?(target).should be_true
+ Puppet::FileSystem.exist?(symlink).should be_true
+ end
+ end
+
+ it "should not create a symlink when the :noop option is specified" do
+ [file, dir].each do |target|
+ symlink = tmpfile("#{Puppet::FileSystem.basename(target)}_link")
+ Puppet::FileSystem.symlink(target, symlink, { :noop => true })
+
+ Puppet::FileSystem.exist?(target).should be_true
+ Puppet::FileSystem.exist?(symlink).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 = tmpfile("#{Puppet::FileSystem.basename(file)}_link")
+ FileUtils.touch(existing_file)
+
+ [file, dir].each do |target|
+ expect { Puppet::FileSystem.symlink(target, existing_file) }.to raise_error(Errno::EEXIST)
+
+ Puppet::FileSystem.exist?(existing_file).should be_true
+ Puppet::FileSystem.symlink?(existing_file).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 = tmpdir("#{Puppet::FileSystem.basename(file)}_dir")
+
+ [file, dir].each do |target|
+ Puppet::FileSystem.symlink(target, existing_dir).should == 0
+
+ Puppet::FileSystem.exist?(existing_dir).should be_true
+ File.directory?(existing_dir).should be_true
+ Puppet::FileSystem.symlink?(existing_dir).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 = tmpdir("#{Puppet::FileSystem.basename(target)}_dir")
+ symlink = tmpfile("#{Puppet::FileSystem.basename(existing_dir)}_link")
+ Puppet::FileSystem.symlink(existing_dir, symlink)
+
+ Puppet::FileSystem.readlink(symlink).should == Puppet::FileSystem.path_string(existing_dir)
+
+ # now try to point it at the new target, no error raised, but file system unchanged
+ Puppet::FileSystem.symlink(target, symlink).should == 0
+ Puppet::FileSystem.readlink(symlink).should == existing_dir.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 = tmpfile("#{Puppet::FileSystem.basename(file)}_link")
+ file_2 = tmpfile("#{Puppet::FileSystem.basename(file)}_2")
+ FileUtils.touch(file_2)
+ # symlink -> file_2
+ Puppet::FileSystem.symlink(file_2, symlink)
+
+ [file, dir].each do |target|
+ expect { Puppet::FileSystem.symlink(target, symlink) }.to raise_error(Errno::EEXIST)
+ Puppet::FileSystem.readlink(symlink).should == file_2.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 = tmpfile("#{Puppet::FileSystem.basename(target)}_existing")
+ FileUtils.touch(existing_file)
+ Puppet::FileSystem.symlink?(existing_file).should be_false
+
+ Puppet::FileSystem.symlink(target, existing_file, { :force => true })
+
+ Puppet::FileSystem.symlink?(existing_file).should be_true
+ Puppet::FileSystem.readlink(existing_file).should == target.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 = tmpfile("#{Puppet::FileSystem.basename(target)}_existing")
+ FileUtils.touch(existing_file)
+ existing_symlink = tmpfile("#{Puppet::FileSystem.basename(existing_file)}_link")
+ Puppet::FileSystem.symlink(existing_file, existing_symlink)
+
+ Puppet::FileSystem.readlink(existing_symlink).should == existing_file.to_s
+
+ Puppet::FileSystem.symlink(target, existing_symlink, { :force => true })
+
+ Puppet::FileSystem.readlink(existing_symlink).should == target.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 = tmpdir("#{Puppet::FileSystem.basename(target)}_existing")
+
+ Puppet::FileSystem.symlink(target, existing_dir, { :force => true }).should == 0
+
+ Puppet::FileSystem.symlink?(existing_dir).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 = tmpdir("#{Puppet::FileSystem.basename(target)}_existing")
+ existing_symlink = tmpfile("#{Puppet::FileSystem.basename(existing_dir)}_link")
+ Puppet::FileSystem.symlink(existing_dir, existing_symlink)
+
+ Puppet::FileSystem.readlink(existing_symlink).should == existing_dir.to_s
+
+ Puppet::FileSystem.symlink(target, existing_symlink, { :force => true }).should == 0
+
+ Puppet::FileSystem.readlink(existing_symlink).should == existing_dir.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.exist?(f).should be_false }
+ end
+
+ it "should return a File::Stat instance when calling stat on an existing file" do
+ Puppet::FileSystem.stat(file).should be_instance_of(File::Stat)
+ end
+
+ it "should raise Errno::ENOENT when calling stat on a missing file" do
+ expect { Puppet::FileSystem.stat(missing_file) }.to raise_error(Errno::ENOENT)
+ end
+
+ it "should be able to create a symlink, and verify it with symlink?" do
+ symlink = tmpfile("somefile_link")
+ Puppet::FileSystem.symlink(file, symlink)
+
+ Puppet::FileSystem.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|
+ Puppet::FileSystem.symlink?(f).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 = tmpfile("somefile_link")
+ Puppet::FileSystem.symlink(file, symlink)
+
+ stat = Puppet::FileSystem.lstat(symlink)
+ 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 = tmpfile("somefile_link")
+ Puppet::FileSystem.symlink(missing_file, symlink)
+
+ stat = Puppet::FileSystem.lstat(symlink)
+ 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 = tmpfile("somefile_link")
+ Puppet::FileSystem.symlink(file, symlink)
+
+ stat = Puppet::FileSystem.stat(symlink)
+ 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 = tmpfile("somefile_link")
+ Puppet::FileSystem.symlink(dir, symlink)
+
+ stat = Puppet::FileSystem.stat(symlink)
+ stat.should be_instance_of(File::Stat)
+ stat.ftype.should == 'directory'
+
+ # on Windows, this won't get cleaned up if still linked
+ Puppet::FileSystem.unlink(symlink)
+ 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 = tmpfile("somefile_link")
+ Puppet::FileSystem.symlink(file, symlink)
+
+ # point symlink2 -> symlink
+ symlink2 = tmpfile("somefile_link2")
+ Puppet::FileSystem.symlink(symlink, symlink2)
+
+ Puppet::FileSystem.stat(symlink2).ftype.should == 'file'
+ end
+
+
+ it "should raise Errno::ENOENT when calling stat on a dangling symlink" do
+ symlink = tmpfile("somefile_link")
+ Puppet::FileSystem.symlink(missing_file, symlink)
+
+ expect { Puppet::FileSystem.stat(symlink) }.to raise_error(Errno::ENOENT)
+ end
+
+ it "should be able to readlink to resolve the physical path to a symlink" do
+ symlink = tmpfile("somefile_link")
+ Puppet::FileSystem.symlink(file, symlink)
+
+ Puppet::FileSystem.exist?(file).should be_true
+ Puppet::FileSystem.readlink(symlink).should == file.to_s
+ end
+
+ it "should not resolve entire symlink chain with readlink on a symlink'd symlink" do
+ # point symlink -> file
+ symlink = tmpfile("somefile_link")
+ Puppet::FileSystem.symlink(file, symlink)
+
+ # point symlink2 -> symlink
+ symlink2 = tmpfile("somefile_link2")
+ Puppet::FileSystem.symlink(symlink, symlink2)
+
+ Puppet::FileSystem.exist?(file).should be_true
+ Puppet::FileSystem.readlink(symlink2).should == symlink.to_s
+ end
+
+ it "should be able to readlink to resolve the physical path to a dangling symlink" do
+ symlink = tmpfile("somefile_link")
+ Puppet::FileSystem.symlink(missing_file, symlink)
+
+ Puppet::FileSystem.exist?(missing_file).should be_false
+ Puppet::FileSystem.readlink(symlink).should == missing_file.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 = tmpfile("#{Puppet::FileSystem.basename(target)}_link")
+ Puppet::FileSystem.symlink(target, symlink)
+
+ Puppet::FileSystem.exist?(target).should be_true
+ Puppet::FileSystem.readlink(symlink).should == target.to_s
+
+ Puppet::FileSystem.unlink(symlink).should == 1 # count of files
+
+ Puppet::FileSystem.exist?(target).should be_true
+ Puppet::FileSystem.exist?(symlink).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 = tmpfile("#{Puppet::FileSystem.basename(target)}_link")
+ Puppet::FileSystem.symlink(target, symlink)
+
+ Puppet::FileSystem.exist?(target).should be_true
+ Puppet::FileSystem.readlink(symlink).should == target.to_s
+
+ Puppet::FileSystem.unlink(symlink).should == 1 # count of files
+
+ Puppet::FileSystem.exist?(target).should be_true
+ Puppet::FileSystem.exist?(symlink).should be_false
+ end
+ end
+
+ describe "unlink" do
+ it "should delete files with unlink" do
+ Puppet::FileSystem.exist?(file).should be_true
+
+ Puppet::FileSystem.unlink(file).should == 1 # count of files
+
+ Puppet::FileSystem.exist?(file).should be_false
+ end
+
+ it "should delete files with unlink class method" do
+ Puppet::FileSystem.exist?(file).should be_true
+
+ Puppet::FileSystem.unlink(file).should == 1 # count of files
+
+ Puppet::FileSystem.exist?(file).should be_false
+ end
+
+ it "should delete multiple files with unlink class method" do
+ paths = (1..3).collect do |i|
+ f = tmpfile("somefile_#{i}")
+ FileUtils.touch(f)
+ Puppet::FileSystem.exist?(f).should be_true
+ f.to_s
+ end
+
+ Puppet::FileSystem.unlink(*paths).should == 3 # count of files
+
+ paths.each { |p| Puppet::FileSystem.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
+ Puppet::FileSystem.exist?(dir).should be_true
+
+ ex = nil
+ begin
+ Puppet::FileSystem.unlink(dir)
+ rescue Exception => e
+ ex = e
+ end
+
+ [
+ Errno::EPERM, # Windows and OSX
+ Errno::EISDIR # Linux
+ ].should include(ex.class)
+
+ Puppet::FileSystem.exist?(dir).should be_true
+ end
+ end
+
+ describe "exclusive_create" do
+ it "should create a file that doesn't exist" do
+ Puppet::FileSystem.exist?(missing_file).should be_false
+
+ Puppet::FileSystem.exclusive_create(missing_file, nil) {}
+
+ Puppet::FileSystem.exist?(missing_file).should be_true
+ end
+
+ it "should raise Errno::EEXIST creating a file that does exist" do
+ Puppet::FileSystem.exist?(file).should be_true
+
+ expect do
+ Puppet::FileSystem.exclusive_create(file, nil) {}
+ end.to raise_error(Errno::EEXIST)
+ end
+ end
+ end
+end
diff --git a/spec/unit/forge/errors_spec.rb b/spec/unit/forge/errors_spec.rb
index 686790e3a..057bf014a 100644
--- a/spec/unit/forge/errors_spec.rb
+++ b/spec/unit/forge/errors_spec.rb
@@ -1,82 +1,82 @@
require 'spec_helper'
-require 'puppet/forge/errors'
+require 'puppet/forge'
describe Puppet::Forge::Errors do
describe 'SSLVerifyError' do
subject { Puppet::Forge::Errors::SSLVerifyError }
let(:exception) { subject.new(:uri => 'https://fake.com:1111') }
it 'should return a valid single line error' do
exception.message.should == 'Unable to verify the SSL certificate at https://fake.com:1111'
end
it 'should return a valid multiline error' do
exception.multiline.should == <<-EOS.chomp
Could not connect via HTTPS to https://fake.com:1111
Unable to verify the SSL certificate
The certificate may not be signed by a valid CA
The CA bundle included with OpenSSL may not be valid or up to date
EOS
end
end
describe 'CommunicationError' do
subject { Puppet::Forge::Errors::CommunicationError }
let(:socket_exception) { SocketError.new('There was a problem') }
let(:exception) { subject.new(:uri => 'http://fake.com:1111', :original => socket_exception) }
it 'should return a valid single line error' do
exception.message.should == 'Unable to connect to the server at http://fake.com:1111. Detail: There was a problem.'
end
it 'should return a valid multiline error' do
exception.multiline.should == <<-EOS.chomp
Could not connect to http://fake.com:1111
There was a network communications problem
The error we caught said 'There was a problem'
Check your network connection and try again
EOS
end
end
describe 'ResponseError' do
subject { Puppet::Forge::Errors::ResponseError }
let(:response) { stub(:body => '{}', :code => '404', :message => "not found") }
context 'without message' do
let(:exception) { subject.new(:uri => 'http://fake.com:1111', :response => response, :input => 'user/module') }
it 'should return a valid single line error' do
exception.message.should == 'Could not execute operation for \'user/module\'. Detail: 404 not found.'
end
it 'should return a valid multiline error' do
exception.multiline.should == <<-eos.chomp
Could not execute operation for 'user/module'
The server being queried was http://fake.com:1111
The HTTP response we received was '404 not found'
Check the author and module names are correct.
eos
end
end
context 'with message' do
let(:exception) { subject.new(:uri => 'http://fake.com:1111', :response => response, :input => 'user/module', :message => 'no such module') }
it 'should return a valid single line error' do
exception.message.should == 'Could not execute operation for \'user/module\'. Detail: no such module / 404 not found.'
end
it 'should return a valid multiline error' do
exception.multiline.should == <<-eos.chomp
Could not execute operation for 'user/module'
The server being queried was http://fake.com:1111
The HTTP response we received was '404 not found'
The message we received said 'no such module'
Check the author and module names are correct.
eos
end
end
end
end
diff --git a/spec/unit/forge/repository_spec.rb b/spec/unit/forge/repository_spec.rb
index 31d28b5d9..95bf8f9af 100644
--- a/spec/unit/forge/repository_spec.rb
+++ b/spec/unit/forge/repository_spec.rb
@@ -1,123 +1,121 @@
# encoding: utf-8
require 'spec_helper'
require 'net/http'
-require 'puppet/forge/repository'
-require 'puppet/forge/cache'
-require 'puppet/forge/errors'
+require 'puppet/forge'
describe Puppet::Forge::Repository do
let(:consumer_version) { "Test/1.0" }
let(:repository) { Puppet::Forge::Repository.new('http://fake.com', consumer_version) }
let(:ssl_repository) { Puppet::Forge::Repository.new('https://fake.com', consumer_version) }
it "retrieve accesses the cache" do
path = '/module/foo.tar.gz'
repository.cache.expects(:retrieve)
repository.retrieve(path)
end
it "retrieve merges forge URI and path specified" do
path = '/module/foo.tar.gz'
repo_uri = 'http://fake.com/test'
repository = Puppet::Forge::Repository.new(repo_uri, consumer_version)
repository.cache.expects(:retrieve).with(URI.parse(repo_uri+path))
repository.retrieve(path)
end
describe "making a request" do
before :each do
proxy_settings_of("proxy", 1234)
end
it "returns the result object from the request" do
result = "the http response"
performs_an_http_request result do |http|
http.expects(:request).with(responds_with(:path, "the_path"))
end
repository.make_http_request("the_path").should == result
end
it 'returns the result object from a request with ssl' do
result = "the http response"
performs_an_https_request result do |http|
http.expects(:request).with(responds_with(:path, "the_path"))
end
ssl_repository.make_http_request("the_path").should == result
end
it 'return a valid exception when there is an SSL verification problem' do
performs_an_https_request "the http response" do |http|
http.expects(:request).with(responds_with(:path, "the_path")).raises OpenSSL::SSL::SSLError.new("certificate verify failed")
end
expect { ssl_repository.make_http_request("the_path") }.to raise_error Puppet::Forge::Errors::SSLVerifyError, 'Unable to verify the SSL certificate at https://fake.com'
end
it 'return a valid exception when there is a communication problem' do
performs_an_http_request "the http response" do |http|
http.expects(:request).with(responds_with(:path, "the_path")).raises SocketError
end
expect { repository.make_http_request("the_path") }.
to raise_error Puppet::Forge::Errors::CommunicationError,
'Unable to connect to the server at http://fake.com. Detail: SocketError.'
end
it "sets the user agent for the request" do
performs_an_http_request do |http|
http.expects(:request).with() do |request|
puppet_version = /Puppet\/\d+\..*/
os_info = /\(.*\)/
ruby_version = /Ruby\/\d+\.\d+\.\d+(-p-?\d+)? \(\d{4}-\d{2}-\d{2}; .*\)/
request["User-Agent"] =~ /^#{consumer_version} #{puppet_version} #{os_info} #{ruby_version}/
end
end
repository.make_http_request("the_path")
end
it "escapes the received URI" do
unescaped_uri = "héllo world !! ç à"
performs_an_http_request do |http|
http.expects(:request).with(responds_with(:path, URI.escape(unescaped_uri)))
end
repository.make_http_request(unescaped_uri)
end
def performs_an_http_request(result = nil, &block)
http = mock("http client")
yield http
proxy_class = mock("http proxy class")
proxy = mock("http proxy")
proxy_class.expects(:new).with("fake.com", 80).returns(proxy)
proxy.expects(:start).yields(http).returns(result)
Net::HTTP.expects(:Proxy).with("proxy", 1234).returns(proxy_class)
end
def performs_an_https_request(result = nil, &block)
http = mock("http client")
yield http
proxy_class = mock("http proxy class")
proxy = mock("http proxy")
proxy_class.expects(:new).with("fake.com", 443).returns(proxy)
proxy.expects(:start).yields(http).returns(result)
proxy.expects(:use_ssl=).with(true)
proxy.expects(:cert_store=)
proxy.expects(:verify_mode=).with(OpenSSL::SSL::VERIFY_PEER)
Net::HTTP.expects(:Proxy).with("proxy", 1234).returns(proxy_class)
end
end
def proxy_settings_of(host, port)
Puppet[:http_proxy_host] = host
Puppet[:http_proxy_port] = port
end
end
diff --git a/spec/unit/hiera/scope_spec.rb b/spec/unit/hiera/scope_spec.rb
index a60be78a9..d53b435ce 100644
--- a/spec/unit/hiera/scope_spec.rb
+++ b/spec/unit/hiera/scope_spec.rb
@@ -1,85 +1,89 @@
require 'spec_helper'
require 'hiera/scope'
+require 'puppet_spec/scope'
+
describe Hiera::Scope do
- let(:real) { Puppet::Parser::Scope.new_for_test_harness("test_node") }
+ include PuppetSpec::Scope
+
+ let(:real) { create_test_scope_for_node("test_node") }
let(:scope) { Hiera::Scope.new(real) }
describe "#initialize" do
it "should store the supplied puppet scope" do
scope.real.should == real
end
end
describe "#[]" do
it "should return nil when no value is found" do
scope["foo"].should == nil
end
it "should treat '' as nil" do
real["foo"] = ""
scope["foo"].should == nil
end
it "should return found data" do
real["foo"] = "bar"
scope["foo"].should == "bar"
end
it "preserves the case of a string that is found" do
real["foo"] = "CAPITAL!"
scope["foo"].should == "CAPITAL!"
end
it "aliases $module_name as calling_module" do
real["module_name"] = "the_module"
scope["calling_module"].should == "the_module"
end
it "uses the name of the of the scope's class as the calling_class" do
real.source = Puppet::Resource::Type.new(:hostclass,
"testing",
:module_name => "the_module")
scope["calling_class"].should == "testing"
end
it "downcases the calling_class" do
real.source = Puppet::Resource::Type.new(:hostclass,
"UPPER CASE",
:module_name => "the_module")
scope["calling_class"].should == "upper case"
end
it "looks for the class which includes the defined type as the calling_class" do
- parent = Puppet::Parser::Scope.new_for_test_harness("parent")
+ parent = create_test_scope_for_node("parent")
real.parent = parent
parent.source = Puppet::Resource::Type.new(:hostclass,
"name_of_the_class_including_the_definition",
:module_name => "class_module")
real.source = Puppet::Resource::Type.new(:definition,
"definition_name",
:module_name => "definition_module")
scope["calling_class"].should == "name_of_the_class_including_the_definition"
end
end
describe "#include?" do
it "should correctly report missing data" do
real["foo"] = ""
scope.include?("foo").should == false
end
it "should always return true for calling_class and calling_module" do
scope.include?("calling_class").should == true
scope.include?("calling_module").should == true
end
end
end
diff --git a/spec/unit/hiera_puppet_spec.rb b/spec/unit/hiera_puppet_spec.rb
index 894b6e334..221dfa9c6 100644
--- a/spec/unit/hiera_puppet_spec.rb
+++ b/spec/unit/hiera_puppet_spec.rb
@@ -1,111 +1,118 @@
require 'spec_helper'
require 'hiera_puppet'
+require 'puppet_spec/scope'
describe 'HieraPuppet' do
+ include PuppetSpec::Scope
+
+ after(:all) do
+ HieraPuppet.instance_variable_set(:@hiera, nil)
+ end
+
describe 'HieraPuppet#hiera_config' do
let(:hiera_config_data) do
{ :backend => 'yaml' }
end
context "when the hiera_config_file exists" do
before do
Hiera::Config.expects(:load).returns(hiera_config_data)
HieraPuppet.expects(:hiera_config_file).returns(true)
end
it "should return a configuration hash" do
expected_results = {
:backend => 'yaml',
:logger => 'puppet'
}
HieraPuppet.send(:hiera_config).should == expected_results
end
end
context "when the hiera_config_file does not exist" do
before do
Hiera::Config.expects(:load).never
HieraPuppet.expects(:hiera_config_file).returns(nil)
end
it "should return a configuration hash" do
HieraPuppet.send(:hiera_config).should == { :logger => 'puppet' }
end
end
end
describe 'HieraPuppet#hiera_config_file' do
it "should return nil when we cannot derive the hiera config file from Puppet.settings" do
begin
Puppet.settings[:hiera_config] = nil
rescue ArgumentError => detail
- raise unless detail.message =~ /unknown configuration parameter/
+ raise unless detail.message =~ /unknown setting/
end
HieraPuppet.send(:hiera_config_file).should be_nil
end
it "should use Puppet.settings[:hiera_config] as the hiera config file" do
begin
Puppet.settings[:hiera_config] = "/dev/null/my_hiera.yaml"
rescue ArgumentError => detail
- raise unless detail.message =~ /unknown configuration parameter/
+ raise unless detail.message =~ /unknown setting/
pending("This example does not apply to Puppet #{Puppet.version} because it does not have this setting")
end
- Puppet::FileSystem::File.stubs(:exist?).with(Puppet[:hiera_config]).returns(true)
+ Puppet::FileSystem.stubs(:exist?).with(Puppet[:hiera_config]).returns(true)
HieraPuppet.send(:hiera_config_file).should == Puppet[:hiera_config]
end
it "should use Puppet.settings[:confdir] as the base directory when hiera_config is not set" do
begin
Puppet.settings[:hiera_config] = nil
rescue ArgumentError => detail
- raise unless detail.message =~ /unknown configuration parameter/
+ raise unless detail.message =~ /unknown setting/
end
Puppet.settings[:confdir] = "/dev/null/puppet"
hiera_config = File.join(Puppet[:confdir], 'hiera.yaml')
- Puppet::FileSystem::File.stubs(:exist?).with(hiera_config).returns(true)
+ Puppet::FileSystem.stubs(:exist?).with(hiera_config).returns(true)
HieraPuppet.send(:hiera_config_file).should == hiera_config
end
end
describe 'HieraPuppet#lookup' do
- let :scope do Puppet::Parser::Scope.new_for_test_harness('foo') end
+ let :scope do create_test_scope_for_node('foo') end
before :each do
Puppet[:hiera_config] = PuppetSpec::Files.tmpfile('hiera_config')
end
it "should return the value from Hiera" do
Hiera.any_instance.stubs(:lookup).returns('8080')
HieraPuppet.lookup('port', nil, scope, nil, :priority).should == '8080'
Hiera.any_instance.stubs(:lookup).returns(['foo', 'bar'])
HieraPuppet.lookup('ntpservers', nil, scope, nil, :array).should == ['foo', 'bar']
Hiera.any_instance.stubs(:lookup).returns({'uid' => '1000'})
HieraPuppet.lookup('user', nil, scope, nil, :hash).should == {'uid' => '1000'}
end
it "should raise a useful error when the answer is nil" do
Hiera.any_instance.stubs(:lookup).returns(nil)
expect do
HieraPuppet.lookup('port', nil, scope, nil, :priority)
end.to raise_error(Puppet::ParseError,
/Could not find data item port in any Hiera data file and no default supplied/)
end
end
describe 'HieraPuppet#parse_args' do
it 'should return a 3 item array' do
args = ['foo', '8080', nil, nil]
HieraPuppet.parse_args(args).should == ['foo', '8080', nil]
end
it 'should raise a useful error when no key is supplied' do
expect { HieraPuppet.parse_args([]) }.to raise_error(Puppet::ParseError,
/Please supply a parameter to perform a Hiera lookup/)
end
end
end
diff --git a/spec/unit/indirector/catalog/msgpack_spec.rb b/spec/unit/indirector/catalog/msgpack_spec.rb
new file mode 100755
index 000000000..f266c897d
--- /dev/null
+++ b/spec/unit/indirector/catalog/msgpack_spec.rb
@@ -0,0 +1,12 @@
+#! /usr/bin/env ruby
+require 'spec_helper'
+require 'puppet/resource/catalog'
+require 'puppet/indirector/catalog/msgpack'
+
+describe Puppet::Resource::Catalog::Msgpack, :if => Puppet.features.msgpack? do
+ # This is it for local functionality: we don't *do* anything else.
+ it "should be registered with the catalog store indirection" do
+ Puppet::Resource::Catalog.indirection.terminus(:msgpack).
+ should be_an_instance_of described_class
+ end
+end
diff --git a/spec/unit/indirector/catalog/static_compiler_spec.rb b/spec/unit/indirector/catalog/static_compiler_spec.rb
index 36480f0a8..7f5aa997e 100644
--- a/spec/unit/indirector/catalog/static_compiler_spec.rb
+++ b/spec/unit/indirector/catalog/static_compiler_spec.rb
@@ -1,194 +1,225 @@
#! /usr/bin/env ruby
require 'spec_helper'
require 'puppet/indirector/catalog/static_compiler'
require 'puppet/file_serving/metadata'
require 'puppet/file_serving/content'
require 'yaml'
describe Puppet::Resource::Catalog::StaticCompiler do
before :all do
@num_file_resources = 10
end
+ before :each do
+ Facter.stubs(:to_hash).returns({})
+ Facter.stubs(:value)
+ end
+
let(:request) do
Puppet::Indirector::Request.new(:the_indirection_named_foo,
:find,
"the-node-named-foo",
:environment => "production")
end
describe "#find" do
it "returns a catalog" do
subject.find(request).should be_a_kind_of(Puppet::Resource::Catalog)
end
it "returns nil if there is no compiled catalog" do
- compiler = mock('compiler indirection')
- compiler.expects(:find).with(request).returns(nil)
-
- subject.expects(:compiler).returns(compiler)
+ subject.expects(:compile).returns(nil)
subject.find(request).should be_nil
end
describe "a catalog with file resources containing source parameters with puppet:// URIs" do
it "filters file resource source URI's to checksums" do
stub_the_compiler
resource_catalog = subject.find(request)
resource_catalog.resources.each do |resource|
next unless resource.type == "File"
resource[:content].should == "{md5}361fadf1c712e812d198c4cab5712a79"
resource[:source].should be_nil
end
end
it "does not modify file resources with non-puppet:// URI's" do
uri = "/this/is/not/a/puppet/uri.txt"
stub_the_compiler(:source => uri)
resource_catalog = subject.find(request)
resource_catalog.resources.each do |resource|
next unless resource.type == "File"
resource[:content].should be_nil
resource[:source].should == uri
end
end
it "copies the owner, group and mode from the fileserer" do
stub_the_compiler
resource_catalog = subject.find(request)
resource_catalog.resources.each do |resource|
next unless resource.type == "File"
resource[:owner].should == 0
resource[:group].should == 0
resource[:mode].should == 420
end
end
end
end
describe "(#15193) when storing content to the filebucket" do
it "explicitly uses the indirection method" do
- # Do not stub the store_content method which is stubbed by default in the
- # value of the stub_methods option.
- stub_the_compiler(:stub_methods => false)
# We expect the content to be retrieved from the FileServer ...
fake_content = mock('FileServer Content')
fake_content.expects(:content).returns("HELLO WORLD")
# Mock the FileBucket to behave as if the file content does not exist.
# NOTE, we're simulating the first call returning false, indicating the
# file is not present, then all subsequent calls returning true. This
# mocked behavior is intended to replicate the real behavior of the same
# file being stored to the filebucket multiple times.
Puppet::FileBucket::File.indirection.
expects(:find).times(@num_file_resources).
returns(false).then.returns(true)
Puppet::FileServing::Content.indirection.
expects(:find).once.
returns(fake_content)
# Once retrived from the FileServer, we expect the file to be stored into
# the FileBucket only once. All of the file resources in the fake
# catalog have the same content.
Puppet::FileBucket::File.indirection.expects(:save).once.with do |file|
file.contents == "HELLO WORLD"
end
# Obtain the Static Catalog
+ subject.stubs(:compile).returns(build_catalog)
resource_catalog = subject.find(request)
# Ensure all of the file resources were filtered
resource_catalog.resources.each do |resource|
next unless resource.type == "File"
resource[:content].should == "{md5}361fadf1c712e812d198c4cab5712a79"
resource[:source].should be_nil
end
end
end
# Spec helper methods
def stub_the_compiler(options = {:stub_methods => [:store_content]})
# Build a resource catalog suitable for specifying the behavior of the
# static compiler.
compiler = mock('indirection terminus compiler')
compiler.stubs(:find).returns(build_catalog(options))
subject.stubs(:compiler).returns(compiler)
# Mock the store content method to prevent copying the contents to the
# file bucket.
(options[:stub_methods] || []).each do |mthd|
subject.stubs(mthd)
end
end
def build_catalog(options = {})
options = options.dup
options[:source] ||= 'puppet:///modules/mymodule/config_file.txt'
options[:request] ||= request
# Build a catalog suitable for the static compiler to operate on
catalog = Puppet::Resource::Catalog.new("#{options[:request].key}")
# Mock out the fileserver, otherwise converting the catalog to a
fake_fileserver_metadata = fileserver_metadata(options)
# Stub the call to the FileServer metadata API so we don't have to have
# a real fileserver initialized for testing.
Puppet::FileServing::Metadata.
- indirection.stubs(:find).
- with() { |*args| args[0] == options[:source].sub('puppet:///','') and args[1] == {:links => :manage, :environment => nil}}.
- returns(fake_fileserver_metadata)
+ indirection.stubs(:find).with do |uri, opts|
+ expect(uri).to eq options[:source].sub('puppet:///','')
+ expect(opts[:links]).to eq :manage
+ expect(opts[:environment]).to eq nil
+ end.returns(fake_fileserver_metadata)
# I want a resource that all the file resources require and another
# that requires them.
resources = Array.new
resources << Puppet::Resource.new("notify", "alpha")
resources << Puppet::Resource.new("notify", "omega")
# Create some File resources with source parameters.
1.upto(@num_file_resources) do |idx|
parameters = {
:ensure => 'file',
:source => options[:source],
:require => "Notify[alpha]",
:before => "Notify[omega]"
}
# The static compiler does not operate on a RAL catalog, so we're
# using Puppet::Resource to produce a resource catalog.
agnostic_path = File.expand_path("/tmp/file_#{idx}.txt") # Windows Friendly
rsrc = Puppet::Resource.new("file", agnostic_path, :parameters => parameters)
rsrc.file = 'site.pp'
rsrc.line = idx
resources << rsrc
end
resources.each do |rsrc|
catalog.add_resource(rsrc)
end
# Return the resource catalog
catalog
end
+ describe "(#22744) when filtering resources" do
+ let(:catalog) { stub_everything 'catalog' }
+
+ it "should delegate to the catalog instance filtering" do
+ catalog.expects(:filter)
+ subject.filter(catalog)
+ end
+
+ it "should filter out virtual resources" do
+ resource = mock 'resource', :virtual? => true
+ catalog.stubs(:filter).yields(resource)
+
+ subject.filter(catalog)
+ end
+
+ it "should return the same catalog if it doesn't support filtering" do
+ catalog.stubs(:respond_to?).with(:filter)
+ subject.filter(catalog).should == catalog
+ end
+
+ it "should return the filtered catalog" do
+ filtered_catalog = stub 'filtered catalog'
+ catalog.stubs(:filter).returns(filtered_catalog)
+
+ subject.filter(catalog).should == filtered_catalog
+ end
+
+ end
+
def fileserver_metadata(options = {})
yaml = <<EOFILESERVERMETADATA
--- !ruby/object:Puppet::FileServing::Metadata
checksum: "{md5}361fadf1c712e812d198c4cab5712a79"
checksum_type: md5
- destination:
+ destination:
expiration: #{Time.now + 1800}
ftype: file
group: 0
links: !ruby/sym manage
mode: 420
owner: 0
path: #{File.expand_path('/etc/puppet/modules/mymodule/files/config_file.txt')}
source: #{options[:source]}
stat_method: !ruby/sym lstat
EOFILESERVERMETADATA
# Return a deserialized metadata object suitable for returning from a stub.
YAML.load(yaml)
end
end
diff --git a/spec/unit/indirector/direct_file_server_spec.rb b/spec/unit/indirector/direct_file_server_spec.rb
index 8b08195bb..b47b13a11 100755
--- a/spec/unit/indirector/direct_file_server_spec.rb
+++ b/spec/unit/indirector/direct_file_server_spec.rb
@@ -1,80 +1,80 @@
#! /usr/bin/env ruby
require 'spec_helper'
require 'puppet/indirector/direct_file_server'
describe Puppet::Indirector::DirectFileServer do
before :all do
Puppet::Indirector::Terminus.stubs(:register_terminus_class)
@model = mock 'model'
@indirection = stub 'indirection', :name => :mystuff, :register_terminus_type => nil, :model => @model
Puppet::Indirector::Indirection.stubs(:instance).returns(@indirection)
module Testing; end
@direct_file_class = class Testing::Mytype < Puppet::Indirector::DirectFileServer
self
end
@server = @direct_file_class.new
@path = File.expand_path('/my/local')
@uri = Puppet::Util.path_to_uri(@path).to_s
@request = Puppet::Indirector::Request.new(:mytype, :find, @uri, nil)
end
describe Puppet::Indirector::DirectFileServer, "when finding a single file" do
it "should return nil if the file does not exist" do
- Puppet::FileSystem::File.expects(:exist?).with(@path).returns false
+ Puppet::FileSystem.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
- Puppet::FileSystem::File.expects(:exist?).with(@path).returns true
+ Puppet::FileSystem.expects(:exist?).with(@path).returns true
@model.expects(:new).returns(:mycontent)
@server.find(@request).should == :mycontent
end
end
describe Puppet::Indirector::DirectFileServer, "when creating the instance for a single found file" do
before do
@data = mock 'content'
@data.stubs(:collect)
- Puppet::FileSystem::File.expects(:exist?).with(@path).returns true
+ Puppet::FileSystem.expects(:exist?).with(@path).returns true
end
it "should pass the full path to the instance" do
@model.expects(:new).with { |key, options| key == @path }.returns(@data)
@server.find(@request)
end
it "should pass the :links setting on to the created Content instance if the file exists and there is a value for :links" do
@model.expects(:new).returns(@data)
@data.expects(:links=).with(:manage)
@request.stubs(:options).returns(:links => :manage)
@server.find(@request)
end
end
describe Puppet::Indirector::DirectFileServer, "when searching for multiple files" do
it "should return nil if the file does not exist" do
- Puppet::FileSystem::File.expects(:exist?).with(@path).returns false
+ Puppet::FileSystem.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
- Puppet::FileSystem::File.expects(:exist?).with(@path).returns true
+ Puppet::FileSystem.expects(:exist?).with(@path).returns true
@server.expects(:path2instances)
@server.search(@request)
end
it "should pass the original request to :path2instances" do
- Puppet::FileSystem::File.expects(:exist?).with(@path).returns true
+ Puppet::FileSystem.expects(:exist?).with(@path).returns true
@server.expects(:path2instances).with(@request, @path)
@server.search(@request)
end
end
end
diff --git a/spec/unit/indirector/facts/facter_spec.rb b/spec/unit/indirector/facts/facter_spec.rb
index 4c55ec6c3..925830d32 100755
--- a/spec/unit/indirector/facts/facter_spec.rb
+++ b/spec/unit/indirector/facts/facter_spec.rb
@@ -1,197 +1,197 @@
#! /usr/bin/env ruby
require 'spec_helper'
require 'puppet/indirector/facts/facter'
describe Puppet::Node::Facts::Facter do
it "should be a subclass of the Code terminus" do
Puppet::Node::Facts::Facter.superclass.should equal(Puppet::Indirector::Code)
end
it "should have documentation" do
Puppet::Node::Facts::Facter.doc.should_not be_nil
end
it "should be registered with the configuration store indirection" do
indirection = Puppet::Indirector::Indirection.instance(:facts)
Puppet::Node::Facts::Facter.indirection.should equal(indirection)
end
it "should have its name set to :facter" do
Puppet::Node::Facts::Facter.name.should == :facter
end
describe "when reloading Facter" do
before do
@facter_class = Puppet::Node::Facts::Facter
Facter.stubs(:clear)
Facter.stubs(:load)
Facter.stubs(:loadfacts)
end
it "should clear Facter" do
Facter.expects(:clear)
@facter_class.reload_facter
end
it "should load all Facter facts" do
Facter.expects(:loadfacts)
@facter_class.reload_facter
end
end
end
describe Puppet::Node::Facts::Facter do
before :each do
Puppet::Node::Facts::Facter.stubs(:reload_facter)
@facter = Puppet::Node::Facts::Facter.new
Facter.stubs(:to_hash).returns({})
@name = "me"
@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
it "should reset and load facts" do
clear = sequence 'clear'
Puppet::Node::Facts::Facter.expects(:reload_facter).in_sequence(clear)
Puppet::Node::Facts::Facter.expects(:load_fact_plugins).in_sequence(clear)
@facter.find(@request)
end
it "should 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
it "should return a Facts instance with the provided key as the name" do
@facter.find(@request).name.should == @name
end
it "should return the Facter facts as the values in the Facts instance" do
Facter.expects(:to_hash).returns("one" => "two")
facts = @facter.find(@request)
facts.values["one"].should == "two"
end
it "should add local facts" do
facts = Puppet::Node::Facts.new("foo")
Puppet::Node::Facts.expects(:new).returns facts
facts.expects(:add_local_facts)
@facter.find(@request)
end
it "should convert facts into strings when stringify_facts is true" do
Puppet[:stringify_facts] = true
facts = Puppet::Node::Facts.new("foo")
Puppet::Node::Facts.expects(:new).returns facts
facts.expects(:stringify)
@facter.find(@request)
end
it "should sanitize facts when stringify_facts is false" do
Puppet[:stringify_facts] = false
facts = Puppet::Node::Facts.new("foo")
Puppet::Node::Facts.expects(:new).returns facts
facts.expects(:sanitize)
@facter.find(@request)
end
end
describe Puppet::Node::Facts::Facter, " when saving facts" do
it "should fail" do
proc { @facter.save(@facts) }.should raise_error(Puppet::DevError)
end
end
describe Puppet::Node::Facts::Facter, " when destroying facts" do
it "should fail" do
proc { @facter.destroy(@facts) }.should raise_error(Puppet::DevError)
end
end
it "should skip files when asked to load a directory" do
FileTest.expects(:directory?).with("myfile").returns false
Puppet::Node::Facts::Facter.load_facts_in_dir("myfile")
end
it "should load each ruby file when asked to load a directory" do
FileTest.expects(:directory?).with("mydir").returns true
Dir.expects(:chdir).with("mydir").yields
Dir.expects(:glob).with("*.rb").returns %w{a.rb b.rb}
Puppet::Node::Facts::Facter.expects(:load).with("a.rb")
Puppet::Node::Facts::Facter.expects(:load).with("b.rb")
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
+ :if => (Puppet.features.external_facts? and not Puppet.features.microsoft_windows?) do
Puppet[:pluginfactdest] = "/plugin/dest"
@facter.find(@request)
- Facter::Util::Config.external_facts_dirs.include?("/plugin/dest")
+ Facter.search_external_path.include?("/plugin/dest")
end
it "should include pluginfactdest when loading external facts",
- :if => Puppet.features.external_facts?, :if => Puppet.features.microsoft_windows? do
+ :if => (Puppet.features.external_facts? and Puppet.features.microsoft_windows?) do
Puppet[:pluginfactdest] = "/plugin/dest"
@facter.find(@request)
- Facter::Util::Config.external_facts_dirs.include?("C:/plugin/dest")
+ Facter.search_external_path.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") }
it "should load each directory in the Fact path" do
Puppet[:factpath] = [one, two].join(File::PATH_SEPARATOR)
Puppet::Node::Facts::Facter.expects(:load_facts_in_dir).with(one)
Puppet::Node::Facts::Facter.expects(:load_facts_in_dir).with(two)
Puppet::Node::Facts::Facter.load_fact_plugins
end
it "should load all facts from the modules" do
Puppet::Node::Facts::Facter.stubs(:load_facts_in_dir)
Puppet[:modulepath] = [one, two].join(File::PATH_SEPARATOR)
Dir.stubs(:glob).returns []
Dir.expects(:glob).with("#{one}/*/lib/facter").returns %w{oneA oneB}
Dir.expects(:glob).with("#{two}/*/lib/facter").returns %w{twoA twoB}
Puppet::Node::Facts::Facter.expects(:load_facts_in_dir).with("oneA")
Puppet::Node::Facts::Facter.expects(:load_facts_in_dir).with("oneB")
Puppet::Node::Facts::Facter.expects(:load_facts_in_dir).with("twoA")
Puppet::Node::Facts::Facter.expects(:load_facts_in_dir).with("twoB")
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")
+ Facter.search_external_path.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 2f9e140f7..338aaf999 100755
--- a/spec/unit/indirector/file_bucket_file/file_spec.rb
+++ b/spec/unit/indirector/file_bucket_file/file_spec.rb
@@ -1,285 +1,285 @@
#! /usr/bin/env ruby
require 'spec_helper'
require 'puppet/indirector/file_bucket_file/file'
require 'puppet/util/platform'
describe Puppet::FileBucketFile::File do
include PuppetSpec::Files
describe "non-stubbing tests" do
include PuppetSpec::Files
before do
Puppet[:bucketdir] = tmpdir('bucketdir')
end
def save_bucket_file(contents, path = "/who_cares")
bucket_file = Puppet::FileBucket::File.new(contents)
Puppet::FileBucket::File.indirection.save(bucket_file, "#{bucket_file.name}#{path}")
bucket_file.checksum_data
end
describe "when servicing a save request" do
it "should return a result whose content is empty" do
bucket_file = Puppet::FileBucket::File.new('stuff')
result = Puppet::FileBucket::File.indirection.save(bucket_file, "md5/c13d88cb4cb02003daedb8a84e5d272a")
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"
- 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"
+ contents_file = "#{dir_path}/contents"
+ paths_file = "#{dir_path}/paths"
+ Puppet::FileSystem.binread(contents_file).should == "stuff\r\n"
+ Puppet::FileSystem.read(paths_file).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"
end
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"
end
end
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 == ""
end
end
end
describe "when servicing a head/find request" do
describe "when supplying a path" do
it "should return false/nil if the file isn't bucketed" do
Puppet::FileBucket::File.indirection.head("md5/0ae2ec1980410229885fe72f7b44fe55/foo/bar").should == false
Puppet::FileBucket::File.indirection.find("md5/0ae2ec1980410229885fe72f7b44fe55/foo/bar").should == nil
end
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.checksum.should == "{md5}#{checksum}"
find_result.to_s.should == contents
end
end
describe "when not supplying a path" do
[false, true].each do |trailing_slash|
describe "#{trailing_slash ? 'with' : 'without'} a trailing slash" do
trailing_string = trailing_slash ? '/' : ''
it "should return false/nil if the file isn't bucketed" do
Puppet::FileBucket::File.indirection.head("md5/0ae2ec1980410229885fe72f7b44fe55#{trailing_string}").should == false
Puppet::FileBucket::File.indirection.find("md5/0ae2ec1980410229885fe72f7b44fe55#{trailing_string}").should == nil
end
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.checksum.should == "{md5}#{checksum}"
find_result.to_s.should == contents
end
end
end
end
end
describe "when diffing files", :unless => Puppet.features.microsoft_windows? do
it "should generate an empty string if there is no diff" do
checksum = save_bucket_file("I'm the contents of a file")
Puppet::FileBucket::File.indirection.find("md5/#{checksum}", :diff_with => checksum).should == ''
end
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
< bar
---
> biz
HERE
end
it "should raise an exception if the hash to diff against isn't found" do
bogus_checksum = "d1bf072d0e2c6e20e3fbd23f022089a1"
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
bogus_checksum = "d1bf072d0e2c6e20e3fbd23f022089a1"
checksum = save_bucket_file("whatever")
Puppet::FileBucket::File.indirection.find("md5/#{bogus_checksum}", :diff_with => checksum).should == nil
end
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|
describe "when #{supply_path ? 'supplying' : 'not supplying'} a path" do
before :each do
Puppet.settings.stubs(:use)
@store = Puppet::FileBucketFile::File.new
@contents = "my content"
@digest = "f2bfa7fc155c4f42cb91404198dda01f"
@digest.should == Digest::MD5.hexdigest(@contents)
@bucket_dir = tmpdir("bucket")
if override_bucket_path
Puppet[:bucketdir] = "/bogus/path" # should not be used
else
Puppet[:bucketdir] = @bucket_dir
end
@dir = "#{@bucket_dir}/f/2/b/f/a/7/f/c/f2bfa7fc155c4f42cb91404198dda01f"
@contents_path = "#{@dir}/contents"
end
describe "when retrieving files" do
before :each do
request_options = {}
if override_bucket_path
request_options[:bucket_path] = @bucket_dir
end
key = "md5/#{@digest}"
if supply_path
key += "/path/to/file"
end
@request = Puppet::Indirector::Request.new(:indirection_name, :find, key, nil, request_options)
end
def make_bucketed_file
FileUtils.mkdir_p(@dir)
File.open(@contents_path, 'w') { |f| f.write @contents }
end
it "should return an instance of Puppet::FileBucket::File created with the content if the file exists" do
make_bucketed_file
if supply_path
@store.find(@request).should == nil
@store.head(@request).should == false # because path didn't match
else
bucketfile = @store.find(@request)
bucketfile.should be_a(Puppet::FileBucket::File)
bucketfile.contents.should == @contents
@store.head(@request).should == true
end
end
it "should return nil if no file is found" do
@store.find(@request).should be_nil
@store.head(@request).should == false
end
end
describe "when saving files" do
it "should save the contents to the calculated path" do
options = {}
if override_bucket_path
options[:bucket_path] = @bucket_dir
end
key = "md5/#{@digest}"
if supply_path
key += "//path/to/file"
end
file_instance = Puppet::FileBucket::File.new(@contents, options)
request = Puppet::Indirector::Request.new(:indirection_name, :save, key, file_instance)
@store.save(request)
File.read("#{@dir}/contents").should == @contents
end
end
end
end
end
end
end
diff --git a/spec/unit/indirector/file_metadata/file_spec.rb b/spec/unit/indirector/file_metadata/file_spec.rb
index 459c1dbe8..fd6ef8ce5 100755
--- a/spec/unit/indirector/file_metadata/file_spec.rb
+++ b/spec/unit/indirector/file_metadata/file_spec.rb
@@ -1,50 +1,50 @@
#! /usr/bin/env ruby
require 'spec_helper'
require 'puppet/indirector/file_metadata/file'
describe Puppet::Indirector::FileMetadata::File do
it "should be registered with the file_metadata indirection" do
Puppet::Indirector::Terminus.terminus_class(:file_metadata, :file).should equal(Puppet::Indirector::FileMetadata::File)
end
it "should be a subclass of the DirectFileServer terminus" do
Puppet::Indirector::FileMetadata::File.superclass.should equal(Puppet::Indirector::DirectFileServer)
end
describe "when creating the instance for a single found file" do
before do
@metadata = Puppet::Indirector::FileMetadata::File.new
@path = File.expand_path('/my/local')
@uri = Puppet::Util.path_to_uri(@path).to_s
@data = mock 'metadata'
@data.stubs(:collect)
- Puppet::FileSystem::File.expects(:exist?).with(@path).returns true
+ Puppet::FileSystem.expects(:exist?).with(@path).returns true
@request = Puppet::Indirector::Request.new(:file_metadata, :find, @uri, nil)
end
it "should collect its attributes when a file is found" do
@data.expects(:collect)
Puppet::FileServing::Metadata.expects(:new).returns(@data)
@metadata.find(@request).should == @data
end
end
describe "when searching for multiple files" do
before do
@metadata = Puppet::Indirector::FileMetadata::File.new
@path = File.expand_path('/my/local')
@uri = Puppet::Util.path_to_uri(@path).to_s
@request = Puppet::Indirector::Request.new(:file_metadata, :find, @uri, nil)
end
it "should collect the attributes of the instances returned" do
- Puppet::FileSystem::File.expects(:exist?).with(@path).returns true
+ Puppet::FileSystem.expects(:exist?).with(@path).returns true
@metadata.expects(:path2instances).returns( [mock("one", :collect => nil), mock("two", :collect => nil)] )
@metadata.search(@request)
end
end
end
diff --git a/spec/unit/indirector/file_server_spec.rb b/spec/unit/indirector/file_server_spec.rb
index 6f491b065..9e354556d 100755
--- a/spec/unit/indirector/file_server_spec.rb
+++ b/spec/unit/indirector/file_server_spec.rb
@@ -1,263 +1,263 @@
#! /usr/bin/env ruby
require 'spec_helper'
require 'puppet/indirector/file_server'
require 'puppet/file_serving/configuration'
describe Puppet::Indirector::FileServer do
before :all do
Puppet::Indirector::Terminus.stubs(:register_terminus_class)
@model = mock 'model'
@indirection = stub 'indirection', :name => :mystuff, :register_terminus_type => nil, :model => @model
Puppet::Indirector::Indirection.stubs(:instance).returns(@indirection)
module Testing; end
@file_server_class = class Testing::MyFileServer < Puppet::Indirector::FileServer
self
end
end
before :each do
@file_server = @file_server_class.new
@uri = "puppet://host/my/local/file"
@configuration = mock 'configuration'
Puppet::FileServing::Configuration.stubs(:configuration).returns(@configuration)
@request = Puppet::Indirector::Request.new(:myind, :mymethod, @uri, :environment => "myenv")
end
describe "when finding files" do
before do
@mount = stub 'mount', :find => nil
@instance = stub('instance', :links= => nil, :collect => nil)
end
it "should use the configuration to find the mount and relative path" do
@configuration.expects(:split_path).with(@request)
@file_server.find(@request)
end
it "should return nil if it cannot find the mount" do
@configuration.expects(:split_path).with(@request).returns(nil, nil)
@file_server.find(@request).should be_nil
end
it "should use the mount to find the full path" do
@configuration.expects(:split_path).with(@request).returns([@mount, "rel/path"])
@mount.expects(:find).with { |key, request| key == "rel/path" }
@file_server.find(@request)
end
it "should pass the request when finding a file" do
@configuration.expects(:split_path).with(@request).returns([@mount, "rel/path"])
@mount.expects(:find).with { |key, request| request == @request }
@file_server.find(@request)
end
it "should return nil if it cannot find a full path" do
@configuration.expects(:split_path).with(@request).returns([@mount, "rel/path"])
@mount.expects(:find).with { |key, request| key == "rel/path" }.returns nil
@file_server.find(@request).should be_nil
end
it "should create an instance with the found path" do
@configuration.expects(:split_path).with(@request).returns([@mount, "rel/path"])
@mount.expects(:find).with { |key, request| key == "rel/path" }.returns "/my/file"
@model.expects(:new).with("/my/file").returns @instance
@file_server.find(@request).should equal(@instance)
end
it "should set 'links' on the instance if it is set in the request options" do
@request.options[:links] = true
@configuration.expects(:split_path).with(@request).returns([@mount, "rel/path"])
@mount.expects(:find).with { |key, request| key == "rel/path" }.returns "/my/file"
@model.expects(:new).with("/my/file").returns @instance
@instance.expects(:links=).with(true)
@file_server.find(@request).should equal(@instance)
end
it "should collect the instance" do
@request.options[:links] = true
@configuration.expects(:split_path).with(@request).returns([@mount, "rel/path"])
@mount.expects(:find).with { |key, request| key == "rel/path" }.returns "/my/file"
@model.expects(:new).with("/my/file").returns @instance
@instance.expects(:collect)
@file_server.find(@request).should equal(@instance)
end
end
describe "when searching for instances" do
before do
@mount = stub 'mount', :search => nil
@instance = stub('instance', :links= => nil, :collect => nil)
end
it "should use the configuration to search the mount and relative path" do
@configuration.expects(:split_path).with(@request)
@file_server.search(@request)
end
it "should return nil if it cannot search the mount" do
@configuration.expects(:split_path).with(@request).returns(nil, nil)
@file_server.search(@request).should be_nil
end
it "should use the mount to search for the full paths" do
@configuration.expects(:split_path).with(@request).returns([@mount, "rel/path"])
@mount.expects(:search).with { |key, request| key == "rel/path" }
@file_server.search(@request)
end
it "should pass the request" do
@configuration.stubs(:split_path).returns([@mount, "rel/path"])
@mount.expects(:search).with { |key, request| request == @request }
@file_server.search(@request)
end
it "should return nil if searching does not find any full paths" do
@configuration.expects(:split_path).with(@request).returns([@mount, "rel/path"])
@mount.expects(:search).with { |key, request| key == "rel/path" }.returns nil
@file_server.search(@request).should be_nil
end
it "should create a fileset with each returned path and merge them" do
@configuration.expects(:split_path).with(@request).returns([@mount, "rel/path"])
@mount.expects(:search).with { |key, request| key == "rel/path" }.returns %w{/one /two}
- Puppet::FileSystem::File.stubs(:exist?).returns true
+ Puppet::FileSystem.stubs(:exist?).returns true
one = mock 'fileset_one'
Puppet::FileServing::Fileset.expects(:new).with("/one", @request).returns(one)
two = mock 'fileset_two'
Puppet::FileServing::Fileset.expects(:new).with("/two", @request).returns(two)
Puppet::FileServing::Fileset.expects(:merge).with(one, two).returns []
@file_server.search(@request)
end
it "should create an instance with each path resulting from the merger of the filesets" do
@configuration.expects(:split_path).with(@request).returns([@mount, "rel/path"])
@mount.expects(:search).with { |key, request| key == "rel/path" }.returns []
- Puppet::FileSystem::File.stubs(:exist?).returns true
+ Puppet::FileSystem.stubs(:exist?).returns true
Puppet::FileServing::Fileset.expects(:merge).returns("one" => "/one", "two" => "/two")
one = stub 'one', :collect => nil
@model.expects(:new).with("/one", :relative_path => "one").returns one
two = stub 'two', :collect => nil
@model.expects(:new).with("/two", :relative_path => "two").returns two
# order can't be guaranteed
result = @file_server.search(@request)
result.should be_include(one)
result.should be_include(two)
result.length.should == 2
end
it "should set 'links' on the instances if it is set in the request options" do
@configuration.expects(:split_path).with(@request).returns([@mount, "rel/path"])
@mount.expects(:search).with { |key, request| key == "rel/path" }.returns []
- Puppet::FileSystem::File.stubs(:exist?).returns true
+ Puppet::FileSystem.stubs(:exist?).returns true
Puppet::FileServing::Fileset.expects(:merge).returns("one" => "/one")
one = stub 'one', :collect => nil
@model.expects(:new).with("/one", :relative_path => "one").returns one
one.expects(:links=).with true
@request.options[:links] = true
@file_server.search(@request)
end
it "should collect the instances" do
@configuration.expects(:split_path).with(@request).returns([@mount, "rel/path"])
@mount.expects(:search).with { |key, options| key == "rel/path" }.returns []
- Puppet::FileSystem::File.stubs(:exist?).returns true
+ Puppet::FileSystem.stubs(:exist?).returns true
Puppet::FileServing::Fileset.expects(:merge).returns("one" => "/one")
one = mock 'one'
@model.expects(:new).with("/one", :relative_path => "one").returns one
one.expects(:collect)
@file_server.search(@request)
end
end
describe "when checking authorization" do
before do
@request.method = :find
@mount = stub 'mount'
@configuration.stubs(:split_path).with(@request).returns([@mount, "rel/path"])
@request.stubs(:node).returns("mynode")
@request.stubs(:ip).returns("myip")
@mount.stubs(:allowed?).with("mynode", "myip").returns "something"
end
it "should return false when destroying" do
@request.method = :destroy
@file_server.should_not be_authorized(@request)
end
it "should return false when saving" do
@request.method = :save
@file_server.should_not be_authorized(@request)
end
it "should use the configuration to find the mount and relative path" do
@configuration.expects(:split_path).with(@request)
@file_server.authorized?(@request)
end
it "should return false if it cannot find the mount" do
@configuration.expects(:split_path).with(@request).returns(nil, nil)
@file_server.should_not be_authorized(@request)
end
it "should return the results of asking the mount whether the node and IP are authorized" do
@file_server.authorized?(@request).should == "something"
end
end
end
diff --git a/spec/unit/indirector/json_spec.rb b/spec/unit/indirector/json_spec.rb
index 664a32749..53431fc8e 100755
--- a/spec/unit/indirector/json_spec.rb
+++ b/spec/unit/indirector/json_spec.rb
@@ -1,193 +1,193 @@
#! /usr/bin/env ruby
require 'spec_helper'
require 'puppet_spec/files'
require 'puppet/indirector/indirector_testing/json'
describe Puppet::Indirector::JSON do
include PuppetSpec::Files
subject { Puppet::IndirectorTesting::JSON.new }
let :model do Puppet::IndirectorTesting end
let :indirection do model.indirection end
context "#path" do
before :each do
Puppet[:server_datadir] = '/sample/datadir/master'
Puppet[:client_datadir] = '/sample/datadir/client'
end
it "uses the :server_datadir setting if this is the master" do
Puppet.run_mode.stubs(:master?).returns(true)
expected = File.join(Puppet[:server_datadir], 'indirector_testing', 'testing.json')
subject.path('testing').should == expected
end
it "uses the :client_datadir setting if this is not the master" do
Puppet.run_mode.stubs(:master?).returns(false)
expected = File.join(Puppet[:client_datadir], 'indirector_testing', 'testing.json')
subject.path('testing').should == expected
end
it "overrides the default extension with a supplied value" do
Puppet.run_mode.stubs(:master?).returns(true)
expected = File.join(Puppet[:server_datadir], 'indirector_testing', 'testing.not-json')
subject.path('testing', '.not-json').should == expected
end
['../foo', '..\\foo', './../foo', '.\\..\\foo',
'/foo', '//foo', '\\foo', '\\\\goo',
"test\0/../bar", "test\0\\..\\bar",
"..\\/bar", "/tmp/bar", "/tmp\\bar", "tmp\\bar",
" / bar", " /../ bar", " \\..\\ bar",
"c:\\foo", "c:/foo", "\\\\?\\UNC\\bar", "\\\\foo\\bar",
"\\\\?\\c:\\foo", "//?/UNC/bar", "//foo/bar",
"//?/c:/foo",
].each do |input|
it "should resist directory traversal attacks (#{input.inspect})" do
expect { subject.path(input) }.to raise_error ArgumentError, 'invalid key'
end
end
end
context "handling requests" do
before :each do
Puppet.run_mode.stubs(:master?).returns(true)
Puppet[:server_datadir] = tmpdir('jsondir')
FileUtils.mkdir_p(File.join(Puppet[:server_datadir], 'indirector_testing'))
end
let :file do subject.path(request.key) end
def with_content(text)
FileUtils.mkdir_p(File.dirname(file))
File.open(file, 'w') {|f| f.puts text }
yield if block_given?
end
it "data saves and then loads again correctly" do
subject.save(indirection.request(:save, 'example', model.new('banana')))
subject.find(indirection.request(:find, 'example', nil)).value.should == 'banana'
end
context "#find" do
let :request do indirection.request(:find, 'example', nil) end
it "returns nil if the file doesn't exist" do
subject.find(request).should be_nil
end
it "raises a descriptive error when the file can't be read" do
with_content(model.new('foo').to_pson) do
# I don't like this, but there isn't a credible alternative that
# also works on Windows, so a stub it is. At least the expectation
# will fail if the implementation changes. Sorry to the next dev.
File.expects(:read).with(file).raises(Errno::EPERM)
expect { subject.find(request) }.
to raise_error Puppet::Error, /Could not read JSON/
end
end
it "raises a descriptive error when the file content is invalid" do
with_content("this is totally invalid JSON") do
expect { subject.find(request) }.
to raise_error Puppet::Error, /Could not parse JSON data/
end
end
it "should return an instance of the indirected object when valid" do
with_content(model.new(1).to_pson) do
instance = subject.find(request)
instance.should be_an_instance_of model
instance.value.should == 1
end
end
end
context "#save" do
let :instance do model.new(4) end
let :request do indirection.request(:find, 'example', instance) end
it "should save the instance of the request as JSON to disk" do
subject.save(request)
content = File.read(file)
content.should =~ /"document_type"\s*:\s*"IndirectorTesting"/
content.should =~ /"value"\s*:\s*4/
end
it "should create the indirection directory if required" do
target = File.join(Puppet[:server_datadir], 'indirector_testing')
Dir.rmdir(target)
subject.save(request)
- File.should be_directory target
+ File.should be_directory(target)
end
end
context "#destroy" do
let :request do indirection.request(:find, 'example', nil) end
it "removes an existing file" do
with_content('hello') do
subject.destroy(request)
end
- Puppet::FileSystem::File.exist?(file).should be_false
+ Puppet::FileSystem.exist?(file).should be_false
end
it "silently succeeds when files don't exist" do
- Puppet::FileSystem::File.unlink(file) rescue nil
+ Puppet::FileSystem.unlink(file) rescue nil
subject.destroy(request).should be_true
end
it "raises an informative error for other failures" do
- Puppet::FileSystem::File.stubs(:unlink).with(file).raises(Errno::EPERM, 'fake permission problem')
+ Puppet::FileSystem.stubs(:unlink).with(file).raises(Errno::EPERM, 'fake permission problem')
with_content('hello') do
- expect { subject.destroy(request) }.to raise_error Puppet::Error
+ expect { subject.destroy(request) }.to raise_error(Puppet::Error)
end
- Puppet::FileSystem::File.unstub(:unlink) # thanks, mocha
+ Puppet::FileSystem.unstub(:unlink) # thanks, mocha
end
end
end
context "#search" do
before :each do
Puppet.run_mode.stubs(:master?).returns(true)
Puppet[:server_datadir] = tmpdir('jsondir')
FileUtils.mkdir_p(File.join(Puppet[:server_datadir], 'indirector_testing'))
end
def request(glob)
indirection.request(:search, glob, nil)
end
def create_file(name, value = 12)
File.open(subject.path(name, ''), 'w') do |f|
f.puts Puppet::IndirectorTesting.new(value).to_pson
end
end
it "returns an empty array when nothing matches the key as a glob" do
subject.search(request('*')).should == []
end
it "returns an array with one item if one item matches" do
create_file('foo.json', 'foo')
create_file('bar.json', 'bar')
subject.search(request('f*')).map(&:value).should == ['foo']
end
it "returns an array of items when more than one item matches" do
create_file('foo.json', 'foo')
create_file('bar.json', 'bar')
create_file('baz.json', 'baz')
subject.search(request('b*')).map(&:value).should =~ ['bar', 'baz']
end
it "only items with the .json extension" do
create_file('foo.json', 'foo-json')
create_file('foo.pson', 'foo-pson')
create_file('foo.json~', 'foo-backup')
subject.search(request('f*')).map(&:value).should == ['foo-json']
end
end
end
diff --git a/spec/unit/indirector/key/file_spec.rb b/spec/unit/indirector/key/file_spec.rb
index 95c212523..44b658cc2 100755
--- a/spec/unit/indirector/key/file_spec.rb
+++ b/spec/unit/indirector/key/file_spec.rb
@@ -1,97 +1,97 @@
#! /usr/bin/env ruby
require 'spec_helper'
require 'puppet/indirector/key/file'
describe Puppet::SSL::Key::File do
it "should have documentation" do
Puppet::SSL::Key::File.doc.should be_instance_of(String)
end
it "should use the :privatekeydir as the collection directory" do
Puppet[:privatekeydir] = File.expand_path("/key/dir")
Puppet::SSL::Key::File.collection_directory.should == Puppet[:privatekeydir]
end
it "should store the ca key at the :cakey location" do
Puppet.settings.stubs(:use)
Puppet[:cakey] = File.expand_path("/ca/key")
file = Puppet::SSL::Key::File.new
file.stubs(:ca?).returns true
file.path("whatever").should == Puppet[:cakey]
end
describe "when choosing the path for the public key" do
it "should use the :capub setting location if the key is for the certificate authority" do
Puppet[:capub] = File.expand_path("/ca/pubkey")
Puppet.settings.stubs(:use)
@searcher = Puppet::SSL::Key::File.new
@searcher.stubs(:ca?).returns true
@searcher.public_key_path("whatever").should == Puppet[:capub]
end
it "should use the host name plus '.pem' in :publickeydir for normal hosts" do
Puppet[:privatekeydir] = File.expand_path("/private/key/dir")
Puppet[:publickeydir] = File.expand_path("/public/key/dir")
Puppet.settings.stubs(:use)
@searcher = Puppet::SSL::Key::File.new
@searcher.stubs(:ca?).returns false
@searcher.public_key_path("whatever").should == File.expand_path("/public/key/dir/whatever.pem")
end
end
describe "when managing private keys" do
before do
@searcher = Puppet::SSL::Key::File.new
@private_key_path = File.join("/fake/key/path")
@public_key_path = File.join("/other/fake/key/path")
@searcher.stubs(:public_key_path).returns @public_key_path
@searcher.stubs(:path).returns @private_key_path
FileTest.stubs(:directory?).returns true
FileTest.stubs(:writable?).returns true
@public_key = stub 'public_key'
@real_key = stub 'sslkey', :public_key => @public_key
@key = stub 'key', :name => "myname", :content => @real_key
@request = stub 'request', :key => "myname", :instance => @key
end
it "should save the public key when saving the private key" do
fh = StringIO.new
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"
@searcher.save(@request)
expect(fh.string).to eq("my pem")
end
it "should destroy the public key when destroying the private key" do
- 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)
+ Puppet::FileSystem.expects(:unlink).with(Puppet::FileSystem.pathname(@private_key_path))
+ Puppet::FileSystem.expects(:exist?).with(Puppet::FileSystem.pathname(@private_key_path)).returns true
+ Puppet::FileSystem.expects(:exist?).with(Puppet::FileSystem.pathname(@public_key_path)).returns true
+ Puppet::FileSystem.expects(:unlink).with(Puppet::FileSystem.pathname(@public_key_path))
@searcher.destroy(@request)
end
it "should not fail if the public key does not exist when deleting the private key" do
- Puppet::FileSystem::File.stubs(:unlink).with(@private_key_path)
+ Puppet::FileSystem.stubs(:unlink).with(Puppet::FileSystem.pathname(@private_key_path))
- 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
+ Puppet::FileSystem.stubs(:exist?).with(Puppet::FileSystem.pathname(@private_key_path)).returns true
+ Puppet::FileSystem.expects(:exist?).with(Puppet::FileSystem.pathname(@public_key_path)).returns false
+ Puppet::FileSystem.expects(:unlink).with(Puppet::FileSystem.pathname(@public_key_path)).never
@searcher.destroy(@request)
end
end
end
diff --git a/spec/unit/indirector/json_spec.rb b/spec/unit/indirector/msgpack_spec.rb
similarity index 74%
copy from spec/unit/indirector/json_spec.rb
copy to spec/unit/indirector/msgpack_spec.rb
index 664a32749..9391f94b0 100755
--- a/spec/unit/indirector/json_spec.rb
+++ b/spec/unit/indirector/msgpack_spec.rb
@@ -1,193 +1,191 @@
#! /usr/bin/env ruby
require 'spec_helper'
require 'puppet_spec/files'
-require 'puppet/indirector/indirector_testing/json'
+require 'puppet/indirector/indirector_testing/msgpack'
-describe Puppet::Indirector::JSON do
+describe Puppet::Indirector::Msgpack, :if => Puppet.features.msgpack? do
include PuppetSpec::Files
- subject { Puppet::IndirectorTesting::JSON.new }
+ subject { Puppet::IndirectorTesting::Msgpack.new }
let :model do Puppet::IndirectorTesting end
let :indirection do model.indirection end
context "#path" do
before :each do
Puppet[:server_datadir] = '/sample/datadir/master'
Puppet[:client_datadir] = '/sample/datadir/client'
end
it "uses the :server_datadir setting if this is the master" do
Puppet.run_mode.stubs(:master?).returns(true)
- expected = File.join(Puppet[:server_datadir], 'indirector_testing', 'testing.json')
+ expected = File.join(Puppet[:server_datadir], 'indirector_testing', 'testing.msgpack')
subject.path('testing').should == expected
end
it "uses the :client_datadir setting if this is not the master" do
Puppet.run_mode.stubs(:master?).returns(false)
- expected = File.join(Puppet[:client_datadir], 'indirector_testing', 'testing.json')
+ expected = File.join(Puppet[:client_datadir], 'indirector_testing', 'testing.msgpack')
subject.path('testing').should == expected
end
it "overrides the default extension with a supplied value" do
Puppet.run_mode.stubs(:master?).returns(true)
- expected = File.join(Puppet[:server_datadir], 'indirector_testing', 'testing.not-json')
- subject.path('testing', '.not-json').should == expected
+ expected = File.join(Puppet[:server_datadir], 'indirector_testing', 'testing.not-msgpack')
+ subject.path('testing', '.not-msgpack').should == expected
end
['../foo', '..\\foo', './../foo', '.\\..\\foo',
'/foo', '//foo', '\\foo', '\\\\goo',
"test\0/../bar", "test\0\\..\\bar",
"..\\/bar", "/tmp/bar", "/tmp\\bar", "tmp\\bar",
" / bar", " /../ bar", " \\..\\ bar",
"c:\\foo", "c:/foo", "\\\\?\\UNC\\bar", "\\\\foo\\bar",
"\\\\?\\c:\\foo", "//?/UNC/bar", "//foo/bar",
"//?/c:/foo",
].each do |input|
it "should resist directory traversal attacks (#{input.inspect})" do
expect { subject.path(input) }.to raise_error ArgumentError, 'invalid key'
end
end
end
context "handling requests" do
before :each do
Puppet.run_mode.stubs(:master?).returns(true)
- Puppet[:server_datadir] = tmpdir('jsondir')
+ Puppet[:server_datadir] = tmpdir('msgpackdir')
FileUtils.mkdir_p(File.join(Puppet[:server_datadir], 'indirector_testing'))
end
let :file do subject.path(request.key) end
def with_content(text)
FileUtils.mkdir_p(File.dirname(file))
- File.open(file, 'w') {|f| f.puts text }
+ File.open(file, 'w') {|f| f.write text }
yield if block_given?
end
it "data saves and then loads again correctly" do
subject.save(indirection.request(:save, 'example', model.new('banana')))
subject.find(indirection.request(:find, 'example', nil)).value.should == 'banana'
end
context "#find" do
let :request do indirection.request(:find, 'example', nil) end
it "returns nil if the file doesn't exist" do
subject.find(request).should be_nil
end
it "raises a descriptive error when the file can't be read" do
- with_content(model.new('foo').to_pson) do
+ with_content(model.new('foo').to_msgpack) do
# I don't like this, but there isn't a credible alternative that
# also works on Windows, so a stub it is. At least the expectation
# will fail if the implementation changes. Sorry to the next dev.
File.expects(:read).with(file).raises(Errno::EPERM)
expect { subject.find(request) }.
- to raise_error Puppet::Error, /Could not read JSON/
+ to raise_error Puppet::Error, /Could not read MessagePack/
end
end
it "raises a descriptive error when the file content is invalid" do
- with_content("this is totally invalid JSON") do
+ with_content("this is totally invalid MessagePack") do
expect { subject.find(request) }.
- to raise_error Puppet::Error, /Could not parse JSON data/
+ to raise_error Puppet::Error, /Could not parse MessagePack data/
end
end
it "should return an instance of the indirected object when valid" do
- with_content(model.new(1).to_pson) do
+ with_content(model.new(1).to_msgpack) do
instance = subject.find(request)
instance.should be_an_instance_of model
instance.value.should == 1
end
end
end
context "#save" do
let :instance do model.new(4) end
let :request do indirection.request(:find, 'example', instance) end
- it "should save the instance of the request as JSON to disk" do
+ it "should save the instance of the request as MessagePack to disk" do
subject.save(request)
content = File.read(file)
- content.should =~ /"document_type"\s*:\s*"IndirectorTesting"/
- content.should =~ /"value"\s*:\s*4/
+ MessagePack.unpack(content)['value'].should == 4
end
it "should create the indirection directory if required" do
target = File.join(Puppet[:server_datadir], 'indirector_testing')
Dir.rmdir(target)
subject.save(request)
- File.should be_directory target
+ File.should be_directory(target)
end
end
context "#destroy" do
let :request do indirection.request(:find, 'example', nil) end
it "removes an existing file" do
with_content('hello') do
subject.destroy(request)
end
- Puppet::FileSystem::File.exist?(file).should be_false
+ Puppet::FileSystem.exist?(file).should be_false
end
it "silently succeeds when files don't exist" do
- Puppet::FileSystem::File.unlink(file) rescue nil
+ Puppet::FileSystem.unlink(file) rescue nil
subject.destroy(request).should be_true
end
it "raises an informative error for other failures" do
- Puppet::FileSystem::File.stubs(:unlink).with(file).raises(Errno::EPERM, 'fake permission problem')
+ Puppet::FileSystem.stubs(:unlink).with(file).raises(Errno::EPERM, 'fake permission problem')
with_content('hello') do
- expect { subject.destroy(request) }.to raise_error Puppet::Error
+ expect { subject.destroy(request) }.to raise_error(Puppet::Error)
end
- Puppet::FileSystem::File.unstub(:unlink) # thanks, mocha
+ Puppet::FileSystem.unstub(:unlink) # thanks, mocha
end
end
end
context "#search" do
before :each do
Puppet.run_mode.stubs(:master?).returns(true)
- Puppet[:server_datadir] = tmpdir('jsondir')
+ Puppet[:server_datadir] = tmpdir('msgpackdir')
FileUtils.mkdir_p(File.join(Puppet[:server_datadir], 'indirector_testing'))
end
def request(glob)
indirection.request(:search, glob, nil)
end
def create_file(name, value = 12)
File.open(subject.path(name, ''), 'w') do |f|
- f.puts Puppet::IndirectorTesting.new(value).to_pson
+ f.write Puppet::IndirectorTesting.new(value).to_msgpack
end
end
it "returns an empty array when nothing matches the key as a glob" do
subject.search(request('*')).should == []
end
it "returns an array with one item if one item matches" do
- create_file('foo.json', 'foo')
- create_file('bar.json', 'bar')
+ create_file('foo.msgpack', 'foo')
+ create_file('bar.msgpack', 'bar')
subject.search(request('f*')).map(&:value).should == ['foo']
end
it "returns an array of items when more than one item matches" do
- create_file('foo.json', 'foo')
- create_file('bar.json', 'bar')
- create_file('baz.json', 'baz')
+ create_file('foo.msgpack', 'foo')
+ create_file('bar.msgpack', 'bar')
+ create_file('baz.msgpack', 'baz')
subject.search(request('b*')).map(&:value).should =~ ['bar', 'baz']
end
- it "only items with the .json extension" do
- create_file('foo.json', 'foo-json')
- create_file('foo.pson', 'foo-pson')
- create_file('foo.json~', 'foo-backup')
- subject.search(request('f*')).map(&:value).should == ['foo-json']
+ it "only items with the .msgpack extension" do
+ create_file('foo.msgpack', 'foo-msgpack')
+ create_file('foo.msgpack~', 'foo-backup')
+ subject.search(request('f*')).map(&:value).should == ['foo-msgpack']
end
end
end
diff --git a/spec/unit/indirector/node/active_record_spec.rb b/spec/unit/indirector/node/active_record_spec.rb
index 2365dcb3b..ac3c98bc4 100755
--- a/spec/unit/indirector/node/active_record_spec.rb
+++ b/spec/unit/indirector/node/active_record_spec.rb
@@ -1,48 +1,48 @@
#! /usr/bin/env ruby
require 'spec_helper'
require 'puppet/node'
describe "Puppet::Node::ActiveRecord", :if => Puppet.features.rails? && Puppet.features.sqlite? do
include PuppetSpec::Files
let(:nodename) { "mynode" }
let(:fact_values) { {:afact => "a value"} }
let(:facts) { Puppet::Node::Facts.new(nodename, fact_values) }
- let(:environment) { Puppet::Node::Environment.new("myenv") }
+ let(:environment) { Puppet::Node::Environment.create(:myenv, [], '') }
let(:request) { Puppet::Indirector::Request.new(:node, :find, nodename, nil, :environment => environment) }
let(:node_indirection) { Puppet::Node::ActiveRecord.new }
before do
require 'puppet/indirector/node/active_record'
end
it "should issue a deprecation warning" do
Puppet.expects(:deprecation_warning).with() { |msg| msg =~ /ActiveRecord-based storeconfigs and inventory are deprecated/ }
Puppet[:statedir] = tmpdir('active_record_tmp')
Puppet[:railslog] = '$statedir/rails.log'
node_indirection
end
it "should be a subclass of the ActiveRecord terminus class" do
Puppet::Node::ActiveRecord.ancestors.should be_include(Puppet::Indirector::ActiveRecord)
end
it "should use Puppet::Rails::Host as its ActiveRecord model" do
Puppet::Node::ActiveRecord.ar_model.should equal(Puppet::Rails::Host)
end
it "should call fact_merge when a node is found" do
db_instance = stub 'db_instance'
Puppet::Node::ActiveRecord.ar_model.expects(:find_by_name).returns db_instance
node = Puppet::Node.new(nodename)
db_instance.expects(:to_puppet).returns node
Puppet[:statedir] = tmpdir('active_record_tmp')
Puppet[:railslog] = '$statedir/rails.log'
Puppet::Node::Facts.indirection.expects(:find).with(nodename, :environment => environment).returns(facts)
node_indirection.find(request).parameters.should include(fact_values)
end
end
diff --git a/spec/unit/indirector/node/ldap_spec.rb b/spec/unit/indirector/node/ldap_spec.rb
index 42c08f72b..c19a30150 100755
--- a/spec/unit/indirector/node/ldap_spec.rb
+++ b/spec/unit/indirector/node/ldap_spec.rb
@@ -1,432 +1,441 @@
#! /usr/bin/env ruby
require 'spec_helper'
require 'puppet/indirector/node/ldap'
describe Puppet::Node::Ldap do
let(:nodename) { "mynode.domain.com" }
let(:node_indirection) { Puppet::Node::Ldap.new }
- let(:environment) { Puppet::Node::Environment.new("myenv") }
+ let(:environment) { Puppet::Node::Environment.create(:myenv, [], '') }
let(:fact_values) { {:afact => "a value", "one" => "boo"} }
let(:facts) { Puppet::Node::Facts.new(nodename, fact_values) }
before do
Puppet::Node::Facts.indirection.stubs(:find).with(nodename, :environment => environment).returns(facts)
end
describe "when searching for a single node" do
let(:request) { Puppet::Indirector::Request.new(:node, :find, nodename, nil, :environment => environment) }
it "should convert the hostname into a search filter" do
entry = stub 'entry', :dn => 'cn=mynode.domain.com,ou=hosts,dc=madstop,dc=com', :vals => %w{}, :to_hash => {}
node_indirection.expects(:ldapsearch).with("(&(objectclass=puppetClient)(cn=#{nodename}))").yields entry
node_indirection.name2hash(nodename)
end
it "should convert any found entry into a hash" do
entry = stub 'entry', :dn => 'cn=mynode.domain.com,ou=hosts,dc=madstop,dc=com', :vals => %w{}, :to_hash => {}
node_indirection.expects(:ldapsearch).with("(&(objectclass=puppetClient)(cn=#{nodename}))").yields entry
myhash = {"myhash" => true}
node_indirection.expects(:entry2hash).with(entry).returns myhash
node_indirection.name2hash(nodename).should == myhash
end
# This heavily tests our entry2hash method, so we don't have to stub out the stupid entry information any more.
describe "when an ldap entry is found" do
before do
@entry = stub 'entry', :dn => 'cn=mynode,ou=hosts,dc=madstop,dc=com', :vals => %w{}, :to_hash => {}
node_indirection.stubs(:ldapsearch).yields @entry
end
it "should convert the entry to a hash" do
node_indirection.entry2hash(@entry).should be_instance_of(Hash)
end
it "should add the entry's common name to the hash if fqdn if false" do
node_indirection.entry2hash(@entry,fqdn = false)[:name].should == "mynode"
end
it "should add the entry's fqdn name to the hash if fqdn if true" do
node_indirection.entry2hash(@entry,fqdn = true)[:name].should == "mynode.madstop.com"
end
it "should add all of the entry's classes to the hash" do
@entry.stubs(:vals).with("puppetclass").returns %w{one two}
node_indirection.entry2hash(@entry)[:classes].should == %w{one two}
end
it "should deduplicate class values" do
@entry.stubs(:to_hash).returns({})
node_indirection.stubs(:class_attributes).returns(%w{one two})
@entry.stubs(:vals).with("one").returns(%w{a b})
@entry.stubs(:vals).with("two").returns(%w{b c})
node_indirection.entry2hash(@entry)[:classes].should == %w{a b c}
end
it "should add the entry's environment to the hash" do
@entry.stubs(:to_hash).returns("environment" => %w{production})
node_indirection.entry2hash(@entry)[:environment].should == "production"
end
it "should add all stacked parameters as parameters in the hash" do
@entry.stubs(:vals).with("puppetvar").returns(%w{one=two three=four})
result = node_indirection.entry2hash(@entry)
result[:parameters]["one"].should == "two"
result[:parameters]["three"].should == "four"
end
it "should not add the stacked parameter as a normal parameter" do
@entry.stubs(:vals).with("puppetvar").returns(%w{one=two three=four})
@entry.stubs(:to_hash).returns("puppetvar" => %w{one=two three=four})
node_indirection.entry2hash(@entry)[:parameters]["puppetvar"].should be_nil
end
it "should add all other attributes as parameters in the hash" do
@entry.stubs(:to_hash).returns("foo" => %w{one two})
node_indirection.entry2hash(@entry)[:parameters]["foo"].should == %w{one two}
end
it "should return single-value parameters as strings, not arrays" do
@entry.stubs(:to_hash).returns("foo" => %w{one})
node_indirection.entry2hash(@entry)[:parameters]["foo"].should == "one"
end
it "should convert 'true' values to the boolean 'true'" do
@entry.stubs(:to_hash).returns({"one" => ["true"]})
node_indirection.entry2hash(@entry)[:parameters]["one"].should == true
end
it "should convert 'false' values to the boolean 'false'" do
@entry.stubs(:to_hash).returns({"one" => ["false"]})
node_indirection.entry2hash(@entry)[:parameters]["one"].should == false
end
it "should convert 'true' values to the boolean 'true' inside an array" do
@entry.stubs(:to_hash).returns({"one" => ["true", "other"]})
node_indirection.entry2hash(@entry)[:parameters]["one"].should == [true, "other"]
end
it "should convert 'false' values to the boolean 'false' inside an array" do
@entry.stubs(:to_hash).returns({"one" => ["false", "other"]})
node_indirection.entry2hash(@entry)[:parameters]["one"].should == [false, "other"]
end
it "should add the parent's name if present" do
@entry.stubs(:vals).with("parentnode").returns(%w{foo})
node_indirection.entry2hash(@entry)[:parent].should == "foo"
end
it "should fail if more than one parent is specified" do
@entry.stubs(:vals).with("parentnode").returns(%w{foo})
node_indirection.entry2hash(@entry)[:parent].should == "foo"
end
end
it "should search first for the provided key" do
node_indirection.expects(:name2hash).with("mynode.domain.com").returns({})
node_indirection.find(request)
end
it "should search for the short version of the provided key if the key looks like a hostname and no results are found for the key itself" do
node_indirection.expects(:name2hash).with("mynode.domain.com").returns(nil)
node_indirection.expects(:name2hash).with("mynode").returns({})
node_indirection.find(request)
end
it "should search for default information if no information can be found for the key" do
node_indirection.expects(:name2hash).with("mynode.domain.com").returns(nil)
node_indirection.expects(:name2hash).with("mynode").returns(nil)
node_indirection.expects(:name2hash).with("default").returns({})
node_indirection.find(request)
end
it "should return nil if no results are found in ldap" do
node_indirection.stubs(:name2hash).returns nil
node_indirection.find(request).should be_nil
end
it "should return a node object if results are found in ldap" do
node_indirection.stubs(:name2hash).returns({})
node_indirection.find(request).should be
end
describe "and node information is found in LDAP" do
before do
@result = {}
node_indirection.stubs(:name2hash).returns @result
end
it "should create the node with the correct name, even if it was found by a different name" do
node_indirection.expects(:name2hash).with(nodename).returns nil
node_indirection.expects(:name2hash).with("mynode").returns @result
node_indirection.find(request).name.should == nodename
end
it "should add any classes from ldap" do
classes = %w{a b c d}
@result[:classes] = classes
node_indirection.find(request).classes.should == classes
end
it "should add all entry attributes as node parameters" do
params = {"one" => "two", "three" => "four"}
@result[:parameters] = params
node_indirection.find(request).parameters.should include(params)
end
it "should set the node's environment to the environment of the results" do
- result_env = Puppet::Node::Environment.new("local_test")
+ result_env = Puppet::Node::Environment.create(:local_test, [], '')
Puppet::Node::Facts.indirection.stubs(:find).with(nodename, :environment => result_env).returns(facts)
@result[:environment] = "local_test"
- node_indirection.find(request).environment.should == result_env
+
+ Puppet.override(:environments => Puppet::Environments::Static.new(result_env)) do
+ node_indirection.find(request).environment.should == result_env
+ end
end
it "should retain false parameter values" do
@result[:parameters] = {}
@result[:parameters]["one"] = false
node_indirection.find(request).parameters.should include({"one" => false})
end
it "should merge the node's facts after the parameters from ldap are assigned" do
# Make sure we've got data to start with, so the parameters are actually set.
params = {"one" => "yay", "two" => "hooray"}
@result[:parameters] = params
# Node implements its own merge so that an existing param takes
# precedence over facts. We get the same result here by merging params
# into facts
node_indirection.find(request).parameters.should == facts.values.merge(params)
end
describe "and a parent node is specified" do
before do
@entry = {:classes => [], :parameters => {}}
@parent = {:classes => [], :parameters => {}}
@parent_parent = {:classes => [], :parameters => {}}
node_indirection.stubs(:name2hash).with(nodename).returns(@entry)
node_indirection.stubs(:name2hash).with('parent').returns(@parent)
node_indirection.stubs(:name2hash).with('parent_parent').returns(@parent_parent)
node_indirection.stubs(:parent_attribute).returns(:parent)
end
it "should search for the parent node" do
@entry[:parent] = "parent"
node_indirection.expects(:name2hash).with(nodename).returns @entry
node_indirection.expects(:name2hash).with('parent').returns @parent
node_indirection.find(request)
end
it "should fail if the parent cannot be found" do
@entry[:parent] = "parent"
node_indirection.expects(:name2hash).with('parent').returns nil
proc { node_indirection.find(request) }.should raise_error(Puppet::Error, /Could not find parent node/)
end
it "should add any parent classes to the node's classes" do
@entry[:parent] = "parent"
@entry[:classes] = %w{a b}
@parent[:classes] = %w{c d}
node_indirection.find(request).classes.should == %w{a b c d}
end
it "should add any parent parameters to the node's parameters" do
@entry[:parent] = "parent"
@entry[:parameters]["one"] = "two"
@parent[:parameters]["three"] = "four"
node_indirection.find(request).parameters.should include({"one" => "two", "three" => "four"})
end
it "should prefer node parameters over parent parameters" do
@entry[:parent] = "parent"
@entry[:parameters]["one"] = "two"
@parent[:parameters]["one"] = "three"
node_indirection.find(request).parameters.should include({"one" => "two"})
end
it "should use the parent's environment if the node has none" do
- env = Puppet::Node::Environment.new("parent")
+ env = Puppet::Node::Environment.create(:parent, [], '')
@entry[:parent] = "parent"
@parent[:environment] = "parent"
Puppet::Node::Facts.indirection.stubs(:find).with(nodename, :environment => env).returns(facts)
- node_indirection.find(request).environment.should == env
+
+ Puppet.override(:environments => Puppet::Environments::Static.new(env)) do
+ node_indirection.find(request).environment.should == env
+ end
end
it "should prefer the node's environment to the parent's" do
- child_env = Puppet::Node::Environment.new("child")
+ child_env = Puppet::Node::Environment.create(:child, [], '')
@entry[:parent] = "parent"
@entry[:environment] = "child"
@parent[:environment] = "parent"
Puppet::Node::Facts.indirection.stubs(:find).with(nodename, :environment => child_env).returns(facts)
- node_indirection.find(request).environment.should == child_env
+ Puppet.override(:environments => Puppet::Environments::Static.new(child_env)) do
+
+ node_indirection.find(request).environment.should == child_env
+ end
end
it "should recursively look up parent information" do
@entry[:parent] = "parent"
@entry[:parameters]["one"] = "two"
@parent[:parent] = "parent_parent"
@parent[:parameters]["three"] = "four"
@parent_parent[:parameters]["five"] = "six"
node_indirection.find(request).parameters.should include("one" => "two", "three" => "four", "five" => "six")
end
it "should not allow loops in parent declarations" do
@entry[:parent] = "parent"
@parent[:parent] = nodename
proc { node_indirection.find(request) }.should raise_error(ArgumentError)
end
end
end
end
describe "when searching for multiple nodes" do
let(:options) { {:environment => environment} }
let(:request) { Puppet::Indirector::Request.new(:node, :find, nodename, nil, options) }
before :each do
Puppet::Node::Facts.indirection.stubs(:terminus_class).returns :yaml
end
it "should find all nodes if no arguments are provided" do
node_indirection.expects(:ldapsearch).with("(objectclass=puppetClient)")
# LAK:NOTE The search method requires an essentially bogus key. It's
# an API problem that I don't really know how to fix.
node_indirection.search request
end
describe "and a class is specified" do
it "should find all nodes that are members of that class" do
node_indirection.expects(:ldapsearch).with("(&(objectclass=puppetClient)(puppetclass=one))")
options[:class] = "one"
node_indirection.search request
end
end
describe "multiple classes are specified" do
it "should find all nodes that are members of all classes" do
node_indirection.expects(:ldapsearch).with("(&(objectclass=puppetClient)(puppetclass=one)(puppetclass=two))")
options[:class] = %w{one two}
node_indirection.search request
end
end
it "should process each found entry" do
# .yields can't be used to yield multiple values :/
node_indirection.expects(:ldapsearch).yields("one")
node_indirection.expects(:entry2hash).with("one",nil).returns(:name => nodename)
node_indirection.search request
end
it "should return a node for each processed entry with the name from the entry" do
node_indirection.expects(:ldapsearch).yields("whatever")
node_indirection.expects(:entry2hash).with("whatever",nil).returns(:name => nodename)
result = node_indirection.search(request)
result[0].should be_instance_of(Puppet::Node)
result[0].name.should == nodename
end
it "should merge each node's facts" do
node_indirection.stubs(:ldapsearch).yields("one")
node_indirection.stubs(:entry2hash).with("one",nil).returns(:name => nodename)
node_indirection.search(request)[0].parameters.should include(fact_values)
end
it "should pass the request's fqdn option to entry2hash" do
options[:fqdn] = :hello
node_indirection.stubs(:ldapsearch).yields("one")
node_indirection.expects(:entry2hash).with("one",:hello).returns(:name => nodename)
node_indirection.search(request)
end
end
describe Puppet::Node::Ldap, " when developing the search query" do
it "should return the value of the :ldapclassattrs split on commas as the class attributes" do
Puppet[:ldapclassattrs] = "one,two"
node_indirection.class_attributes.should == %w{one two}
end
it "should return nil as the parent attribute if the :ldapparentattr is set to an empty string" do
Puppet[:ldapparentattr] = ""
node_indirection.parent_attribute.should be_nil
end
it "should return the value of the :ldapparentattr as the parent attribute" do
Puppet[:ldapparentattr] = "pere"
node_indirection.parent_attribute.should == "pere"
end
it "should use the value of the :ldapstring as the search filter" do
Puppet[:ldapstring] = "mystring"
node_indirection.search_filter("testing").should == "mystring"
end
it "should replace '%s' with the node name in the search filter if it is present" do
Puppet[:ldapstring] = "my%sstring"
node_indirection.search_filter("testing").should == "mytestingstring"
end
it "should not modify the global :ldapstring when replacing '%s' in the search filter" do
filter = mock 'filter'
filter.expects(:include?).with("%s").returns(true)
filter.expects(:gsub).with("%s", "testing").returns("mynewstring")
Puppet[:ldapstring] = filter
node_indirection.search_filter("testing").should == "mynewstring"
end
end
describe Puppet::Node::Ldap, " when deciding attributes to search for" do
it "should use 'nil' if the :ldapattrs setting is 'all'" do
Puppet[:ldapattrs] = "all"
node_indirection.search_attributes.should be_nil
end
it "should split the value of :ldapattrs on commas and use the result as the attribute list" do
Puppet[:ldapattrs] = "one,two"
node_indirection.stubs(:class_attributes).returns([])
node_indirection.stubs(:parent_attribute).returns(nil)
node_indirection.search_attributes.should == %w{one two}
end
it "should add the class attributes to the search attributes if not returning all attributes" do
Puppet[:ldapattrs] = "one,two"
node_indirection.stubs(:class_attributes).returns(%w{three four})
node_indirection.stubs(:parent_attribute).returns(nil)
# Sort them so i don't have to care about return order
node_indirection.search_attributes.sort.should == %w{one two three four}.sort
end
it "should add the parent attribute to the search attributes if not returning all attributes" do
Puppet[:ldapattrs] = "one,two"
node_indirection.stubs(:class_attributes).returns([])
node_indirection.stubs(:parent_attribute).returns("parent")
node_indirection.search_attributes.sort.should == %w{one two parent}.sort
end
it "should not add nil parent attributes to the search attributes" do
Puppet[:ldapattrs] = "one,two"
node_indirection.stubs(:class_attributes).returns([])
node_indirection.stubs(:parent_attribute).returns(nil)
node_indirection.search_attributes.should == %w{one two}
end
end
end
diff --git a/spec/unit/indirector/node/msgpack_spec.rb b/spec/unit/indirector/node/msgpack_spec.rb
new file mode 100755
index 000000000..18ba50f97
--- /dev/null
+++ b/spec/unit/indirector/node/msgpack_spec.rb
@@ -0,0 +1,24 @@
+#! /usr/bin/env ruby
+require 'spec_helper'
+
+require 'puppet/node'
+require 'puppet/indirector/node/msgpack'
+
+describe Puppet::Node::Msgpack, :if => Puppet.features.msgpack? do
+ it "should be a subclass of the Msgpack terminus" do
+ Puppet::Node::Msgpack.superclass.should equal(Puppet::Indirector::Msgpack)
+ end
+
+ it "should have documentation" do
+ Puppet::Node::Msgpack.doc.should_not be_nil
+ end
+
+ it "should be registered with the configuration store indirection" do
+ indirection = Puppet::Indirector::Indirection.instance(:node)
+ Puppet::Node::Msgpack.indirection.should equal(indirection)
+ end
+
+ it "should have its name set to :msgpack" do
+ Puppet::Node::Msgpack.name.should == :msgpack
+ end
+end
diff --git a/spec/unit/indirector/node/plain_spec.rb b/spec/unit/indirector/node/plain_spec.rb
index 15cef217d..8e1d0decf 100755
--- a/spec/unit/indirector/node/plain_spec.rb
+++ b/spec/unit/indirector/node/plain_spec.rb
@@ -1,26 +1,26 @@
#! /usr/bin/env ruby
require 'spec_helper'
require 'puppet/indirector/node/plain'
describe Puppet::Node::Plain do
let(:nodename) { "mynode" }
let(:fact_values) { {:afact => "a value"} }
let(:facts) { Puppet::Node::Facts.new(nodename, fact_values) }
- let(:environment) { Puppet::Node::Environment.new("myenv") }
+ let(:environment) { Puppet::Node::Environment.create(:myenv, [], '') }
let(:request) { Puppet::Indirector::Request.new(:node, :find, nodename, nil, :environment => environment) }
let(:node_indirection) { Puppet::Node::Plain.new }
before do
Puppet::Node::Facts.indirection.expects(:find).with(nodename, :environment => environment).returns(facts)
end
it "merges facts into the node" do
node_indirection.find(request).parameters.should include(fact_values)
end
it "should set the node environment from the request" do
node_indirection.find(request).environment.should == environment
end
end
diff --git a/spec/unit/indirector/queue_spec.rb b/spec/unit/indirector/queue_spec.rb
index 3711b1846..b14b4cbb3 100755
--- a/spec/unit/indirector/queue_spec.rb
+++ b/spec/unit/indirector/queue_spec.rb
@@ -1,115 +1,115 @@
#! /usr/bin/env ruby
require 'spec_helper'
require 'puppet/indirector/queue'
class Puppet::Indirector::Queue::TestClient
end
class FooExampleData
attr_accessor :name
def self.pson_create(pson)
new(pson['data'].to_sym)
end
def initialize(name = nil)
@name = name if name
end
def render(format = :pson)
to_pson
end
def to_pson(*args)
{:type => self.class.to_s, :data => name}.to_pson(*args)
end
end
describe Puppet::Indirector::Queue do
before :each do
@model = mock 'model'
@indirection = stub 'indirection', :name => :my_queue, :register_terminus_type => nil, :model => @model
Puppet::Indirector::Indirection.stubs(:instance).with(:my_queue).returns(@indirection)
module MyQueue; end
@store_class = class MyQueue::MyType < Puppet::Indirector::Queue
self
end
@store = @store_class.new
@subject_class = FooExampleData
@subject = @subject_class.new
@subject.name = :me
Puppet[:queue_type] = :test_client
Puppet::Util::Queue.stubs(:queue_type_to_class).with(:test_client).returns(Puppet::Indirector::Queue::TestClient)
@request = stub 'request', :key => :me, :instance => @subject
end
it 'should use the correct client type and queue' do
@store.queue.should == :my_queue
@store.client.should be_an_instance_of(Puppet::Indirector::Queue::TestClient)
end
describe "when saving" do
it 'should render the instance using pson' do
@subject.expects(:render).with(:pson)
@store.client.stubs(:publish_message)
@store.save(@request)
end
it "should publish the rendered message to the appropriate queue on the client" do
@subject.expects(:render).returns "mypson"
@store.client.expects(:publish_message).with(:my_queue, "mypson")
@store.save(@request)
end
it "should catch any exceptions raised" do
@store.client.expects(:publish_message).raises ArgumentError
expect { @store.save(@request) }.to raise_error(Puppet::Error)
end
end
describe "when subscribing to the queue" do
before do
@store_class.stubs(:model).returns @model
end
it "should use the model's Format support to intern the message from pson" do
@model.expects(:convert_from).with(:pson, "mymessage")
@store_class.client.expects(:subscribe).yields("mymessage")
@store_class.subscribe {|o| o }
end
it "should yield each interned received message" do
@model.stubs(:convert_from).returns "something"
@subject_two = @subject_class.new
@subject_two.name = :too
@store_class.client.expects(:subscribe).with(:my_queue).multiple_yields(@subject, @subject_two)
received = []
@store_class.subscribe do |obj|
received.push(obj)
end
received.should == %w{something something}
end
it "should log but not propagate errors" do
@store_class.client.expects(:subscribe).yields("foo")
@store_class.expects(:intern).raises(ArgumentError)
expect { @store_class.subscribe {|o| o } }.to_not raise_error
@logs.length.should == 1
- @logs.first.message.should =~ /Error occured with subscription to queue my_queue for indirection my_queue: ArgumentError/
+ @logs.first.message.should =~ /Error occurred with subscription to queue my_queue for indirection my_queue: ArgumentError/
@logs.first.level.should == :err
end
end
end
diff --git a/spec/unit/indirector/report/msgpack_spec.rb b/spec/unit/indirector/report/msgpack_spec.rb
new file mode 100755
index 000000000..20bc225af
--- /dev/null
+++ b/spec/unit/indirector/report/msgpack_spec.rb
@@ -0,0 +1,28 @@
+#! /usr/bin/env ruby
+require 'spec_helper'
+
+require 'puppet/transaction/report'
+require 'puppet/indirector/report/msgpack'
+
+describe Puppet::Transaction::Report::Msgpack, :if => Puppet.features.msgpack? do
+ it "should be a subclass of the Msgpack terminus" do
+ Puppet::Transaction::Report::Msgpack.superclass.should equal(Puppet::Indirector::Msgpack)
+ end
+
+ it "should have documentation" do
+ Puppet::Transaction::Report::Msgpack.doc.should_not be_nil
+ end
+
+ it "should be registered with the report indirection" do
+ indirection = Puppet::Indirector::Indirection.instance(:report)
+ Puppet::Transaction::Report::Msgpack.indirection.should equal(indirection)
+ end
+
+ it "should have its name set to :msgpack" do
+ Puppet::Transaction::Report::Msgpack.name.should == :msgpack
+ end
+
+ it "should unconditionally save/load from the --lastrunreport setting" do
+ subject.path(:me).should == Puppet[:lastrunreport]
+ end
+end
diff --git a/spec/unit/indirector/request_spec.rb b/spec/unit/indirector/request_spec.rb
index c43c46709..e0d9df339 100755
--- a/spec/unit/indirector/request_spec.rb
+++ b/spec/unit/indirector/request_spec.rb
@@ -1,597 +1,605 @@
#! /usr/bin/env ruby
require 'spec_helper'
require 'matchers/json'
require 'puppet/indirector/request'
require 'puppet/util/pson'
describe Puppet::Indirector::Request do
+ include JSONMatchers
describe "when registering the document type" do
it "should register its document type with JSON" do
PSON.registered_document_types["IndirectorRequest"].should equal(Puppet::Indirector::Request)
end
end
describe "when initializing" do
it "should always convert the indirection name to a symbol" do
Puppet::Indirector::Request.new("ind", :method, "mykey", nil).indirection_name.should == :ind
end
it "should use provided value as the key if it is a string" do
Puppet::Indirector::Request.new(:ind, :method, "mykey", nil).key.should == "mykey"
end
it "should use provided value as the key if it is a symbol" do
Puppet::Indirector::Request.new(:ind, :method, :mykey, nil).key.should == :mykey
end
it "should use the name of the provided instance as its key if an instance is provided as the key instead of a string" do
instance = mock 'instance', :name => "mykey"
request = Puppet::Indirector::Request.new(:ind, :method, nil, instance)
request.key.should == "mykey"
request.instance.should equal(instance)
end
it "should support options specified as a hash" do
expect { Puppet::Indirector::Request.new(:ind, :method, :key, nil, :one => :two) }.to_not raise_error
end
it "should support nil options" do
expect { Puppet::Indirector::Request.new(:ind, :method, :key, nil, nil) }.to_not raise_error
end
it "should support unspecified options" do
expect { Puppet::Indirector::Request.new(:ind, :method, :key, nil) }.to_not raise_error
end
it "should use an empty options hash if nil was provided" do
Puppet::Indirector::Request.new(:ind, :method, :key, nil, nil).options.should == {}
end
it "should default to a nil node" do
Puppet::Indirector::Request.new(:ind, :method, :key, nil).node.should be_nil
end
it "should set its node attribute if provided in the options" do
Puppet::Indirector::Request.new(:ind, :method, :key, nil, :node => "foo.com").node.should == "foo.com"
end
it "should default to a nil ip" do
Puppet::Indirector::Request.new(:ind, :method, :key, nil).ip.should be_nil
end
it "should set its ip attribute if provided in the options" do
Puppet::Indirector::Request.new(:ind, :method, :key, nil, :ip => "192.168.0.1").ip.should == "192.168.0.1"
end
it "should default to being unauthenticated" do
Puppet::Indirector::Request.new(:ind, :method, :key, nil).should_not be_authenticated
end
it "should set be marked authenticated if configured in the options" do
Puppet::Indirector::Request.new(:ind, :method, :key, nil, :authenticated => "eh").should be_authenticated
end
it "should keep its options as a hash even if a node is specified" do
Puppet::Indirector::Request.new(:ind, :method, :key, nil, :node => "eh").options.should be_instance_of(Hash)
end
it "should keep its options as a hash even if another option is specified" do
Puppet::Indirector::Request.new(:ind, :method, :key, nil, :foo => "bar").options.should be_instance_of(Hash)
end
it "should treat options other than :ip, :node, and :authenticated as options rather than attributes" do
Puppet::Indirector::Request.new(:ind, :method, :key, nil, :server => "bar").options[:server].should == "bar"
end
it "should normalize options to use symbols as keys" do
Puppet::Indirector::Request.new(:ind, :method, :key, nil, "foo" => "bar").options[:foo].should == "bar"
end
describe "and the request key is a URI" do
let(:file) { File.expand_path("/my/file with spaces") }
describe "and the URI is a 'file' URI" do
before do
@request = Puppet::Indirector::Request.new(:ind, :method, "#{URI.unescape(Puppet::Util.path_to_uri(file).to_s)}", nil)
end
it "should set the request key to the unescaped full file path" do
@request.key.should == file
end
it "should not set the protocol" do
@request.protocol.should be_nil
end
it "should not set the port" do
@request.port.should be_nil
end
it "should not set the server" do
@request.server.should be_nil
end
end
it "should set the protocol to the URI scheme" do
Puppet::Indirector::Request.new(:ind, :method, "http://host/stuff", nil).protocol.should == "http"
end
it "should set the server if a server is provided" do
Puppet::Indirector::Request.new(:ind, :method, "http://host/stuff", nil).server.should == "host"
end
it "should set the server and port if both are provided" do
Puppet::Indirector::Request.new(:ind, :method, "http://host:543/stuff", nil).port.should == 543
end
it "should default to the masterport if the URI scheme is 'puppet'" do
Puppet[:masterport] = "321"
Puppet::Indirector::Request.new(:ind, :method, "puppet://host/stuff", nil).port.should == 321
end
it "should use the provided port if the URI scheme is not 'puppet'" do
Puppet::Indirector::Request.new(:ind, :method, "http://host/stuff", nil).port.should == 80
end
it "should set the request key to the unescaped key part path from the URI" do
Puppet::Indirector::Request.new(:ind, :method, "http://host/environment/terminus/stuff with spaces", nil).key.should == "stuff with spaces"
end
it "should set the :uri attribute to the full URI" do
Puppet::Indirector::Request.new(:ind, :method, "http:///stu ff", nil).uri.should == 'http:///stu ff'
end
it "should not parse relative URI" do
Puppet::Indirector::Request.new(:ind, :method, "foo/bar", nil).uri.should be_nil
end
it "should not parse opaque URI" do
Puppet::Indirector::Request.new(:ind, :method, "mailto:joe", nil).uri.should be_nil
end
end
it "should allow indication that it should not read a cached instance" do
Puppet::Indirector::Request.new(:ind, :method, :key, nil, :ignore_cache => true).should be_ignore_cache
end
it "should default to not ignoring the cache" do
Puppet::Indirector::Request.new(:ind, :method, :key, nil).should_not be_ignore_cache
end
it "should allow indication that it should not not read an instance from the terminus" do
Puppet::Indirector::Request.new(:ind, :method, :key, nil, :ignore_terminus => true).should be_ignore_terminus
end
it "should default to not ignoring the terminus" do
Puppet::Indirector::Request.new(:ind, :method, :key, nil).should_not be_ignore_terminus
end
end
it "should look use the Indirection class to return the appropriate indirection" do
ind = mock 'indirection'
Puppet::Indirector::Indirection.expects(:instance).with(:myind).returns ind
request = Puppet::Indirector::Request.new(:myind, :method, :key, nil)
request.indirection.should equal(ind)
end
it "should use its indirection to look up the appropriate model" do
ind = mock 'indirection'
Puppet::Indirector::Indirection.expects(:instance).with(:myind).returns ind
request = Puppet::Indirector::Request.new(:myind, :method, :key, nil)
ind.expects(:model).returns "mymodel"
request.model.should == "mymodel"
end
it "should fail intelligently when asked to find a model but the indirection cannot be found" do
Puppet::Indirector::Indirection.expects(:instance).with(:myind).returns nil
request = Puppet::Indirector::Request.new(:myind, :method, :key, nil)
expect { request.model }.to raise_error(ArgumentError)
end
it "should have a method for determining if the request is plural or singular" do
Puppet::Indirector::Request.new(:myind, :method, :key, nil).should respond_to(:plural?)
end
it "should be considered plural if the method is 'search'" do
Puppet::Indirector::Request.new(:myind, :search, :key, nil).should be_plural
end
it "should not be considered plural if the method is not 'search'" do
Puppet::Indirector::Request.new(:myind, :find, :key, nil).should_not be_plural
end
it "should use its uri, if it has one, as its string representation" do
Puppet::Indirector::Request.new(:myind, :find, "foo://bar/baz", nil).to_s.should == "foo://bar/baz"
end
it "should use its indirection name and key, if it has no uri, as its string representation" do
Puppet::Indirector::Request.new(:myind, :find, "key", nil) == "/myind/key"
end
it "should be able to return the URI-escaped key" do
Puppet::Indirector::Request.new(:myind, :find, "my key", nil).escaped_key.should == URI.escape("my key")
end
- it "should have an environment accessor" do
- Puppet::Indirector::Request.new(:myind, :find, "my key", nil, :environment => "foo").should respond_to(:environment)
- end
-
it "should set its environment to an environment instance when a string is specified as its environment" do
- Puppet::Indirector::Request.new(:myind, :find, "my key", nil, :environment => "foo").environment.should == Puppet::Node::Environment.new("foo")
+ env = Puppet::Node::Environment.create(:foo, [], '')
+
+ Puppet.override(:environments => Puppet::Environments::Static.new(env)) do
+ Puppet::Indirector::Request.new(:myind, :find, "my key", nil, :environment => "foo").environment.should == env
+ end
end
it "should use any passed in environment instances as its environment" do
- env = Puppet::Node::Environment.new("foo")
+ env = Puppet::Node::Environment.create(:foo, [], '')
+
Puppet::Indirector::Request.new(:myind, :find, "my key", nil, :environment => env).environment.should equal(env)
end
- it "should use the default environment when none is provided" do
- Puppet::Indirector::Request.new(:myind, :find, "my key", nil ).environment.should equal(Puppet::Node::Environment.new)
+ it "should use the configured environment when none is provided" do
+ configured = Puppet::Node::Environment.create(:foo, [], '')
+
+ Puppet[:environment] = "foo"
+
+ Puppet.override(:environments => Puppet::Environments::Static.new(configured)) do
+ Puppet::Indirector::Request.new(:myind, :find, "my key", nil).environment.should == configured
+ end
end
it "should support converting its options to a hash" do
Puppet::Indirector::Request.new(:myind, :find, "my key", nil ).should respond_to(:to_hash)
end
it "should include all of its attributes when its options are converted to a hash" do
Puppet::Indirector::Request.new(:myind, :find, "my key", nil, :node => 'foo').to_hash[:node].should == 'foo'
end
describe "when building a query string from its options" do
def a_request_with_options(options)
Puppet::Indirector::Request.new(:myind, :find, "my key", nil, options)
end
def the_parsed_query_string_from(request)
CGI.parse(request.query_string.sub(/^\?/, ''))
end
it "should return an empty query string if there are no options" do
request = a_request_with_options(nil)
request.query_string.should == ""
end
it "should return an empty query string if the options are empty" do
request = a_request_with_options({})
request.query_string.should == ""
end
it "should prefix the query string with '?'" do
request = a_request_with_options(:one => "two")
request.query_string.should =~ /^\?/
end
it "should include all options in the query string, separated by '&'" do
request = a_request_with_options(:one => "two", :three => "four")
the_parsed_query_string_from(request).should == {
"one" => ["two"],
"three" => ["four"]
}
end
it "should ignore nil options" do
request = a_request_with_options(:one => "two", :three => nil)
the_parsed_query_string_from(request).should == {
"one" => ["two"]
}
end
it "should convert 'true' option values into strings" do
request = a_request_with_options(:one => true)
the_parsed_query_string_from(request).should == {
"one" => ["true"]
}
end
it "should convert 'false' option values into strings" do
request = a_request_with_options(:one => false)
the_parsed_query_string_from(request).should == {
"one" => ["false"]
}
end
it "should convert to a string all option values that are integers" do
request = a_request_with_options(:one => 50)
the_parsed_query_string_from(request).should == {
"one" => ["50"]
}
end
it "should convert to a string all option values that are floating point numbers" do
request = a_request_with_options(:one => 1.2)
the_parsed_query_string_from(request).should == {
"one" => ["1.2"]
}
end
it "should CGI-escape all option values that are strings" do
request = a_request_with_options(:one => "one two")
the_parsed_query_string_from(request).should == {
"one" => ["one two"]
}
end
it "should convert an array of values into multiple entries for the same key" do
request = a_request_with_options(:one => %w{one two})
the_parsed_query_string_from(request).should == {
"one" => ["one", "two"]
}
end
it "should convert an array of values into a single yaml entry when in legacy mode" do
Puppet[:legacy_query_parameter_serialization] = true
request = a_request_with_options(:one => %w{one two})
the_parsed_query_string_from(request).should == {
"one" => ["--- \n - one\n - two"]
}
end
it "should stringify simple data types inside an array" do
request = a_request_with_options(:one => ['one', nil])
the_parsed_query_string_from(request).should == {
"one" => ["one"]
}
end
it "should error if an array contains another array" do
request = a_request_with_options(:one => ['one', ["not allowed"]])
expect { request.query_string }.to raise_error(ArgumentError)
end
it "should error if an array contains illegal data" do
request = a_request_with_options(:one => ['one', { :not => "allowed" }])
expect { request.query_string }.to raise_error(ArgumentError)
end
it "should convert to a string and CGI-escape all option values that are symbols" do
request = a_request_with_options(:one => :"sym bol")
the_parsed_query_string_from(request).should == {
"one" => ["sym bol"]
}
end
it "should fail if options other than booleans or strings are provided" do
request = a_request_with_options(:one => { :one => :two })
expect { request.query_string }.to raise_error(ArgumentError)
end
end
describe "when converting to json" do
before do
@request = Puppet::Indirector::Request.new(:facts, :find, "foo", nil)
end
it "should produce a hash with the document_type set to 'request'" do
@request.should set_json_document_type_to("IndirectorRequest")
end
it "should set the 'key'" do
@request.should set_json_attribute("key").to("foo")
end
it "should include an attribute for its indirection name" do
@request.should set_json_attribute("type").to("facts")
end
it "should include a 'method' attribute set to its method" do
@request.should set_json_attribute("method").to("find")
end
it "should add all attributes under the 'attributes' attribute" do
@request.ip = "127.0.0.1"
@request.should set_json_attribute("attributes", "ip").to("127.0.0.1")
end
it "should add all options under the 'attributes' attribute" do
@request.options["opt"] = "value"
PSON.parse(@request.to_pson)["data"]['attributes']['opt'].should == "value"
end
it "should include the instance if provided" do
facts = Puppet::Node::Facts.new("foo")
@request.instance = facts
PSON.parse(@request.to_pson)["data"]['instance'].should be_instance_of(Hash)
end
end
describe "when converting from json" do
before do
@request = Puppet::Indirector::Request.new(:facts, :find, "foo", nil)
@klass = Puppet::Indirector::Request
@format = Puppet::Network::FormatHandler.format('pson')
end
def from_json(json)
@format.intern(Puppet::Indirector::Request, json)
end
it "should set the 'key'" do
from_json(@request.to_pson).key.should == "foo"
end
it "should fail if no key is provided" do
json = PSON.parse(@request.to_pson)
json['data'].delete("key")
expect { from_json(json.to_pson) }.to raise_error(ArgumentError)
end
it "should set its indirector name" do
from_json(@request.to_pson).indirection_name.should == :facts
end
it "should fail if no type is provided" do
json = PSON.parse(@request.to_pson)
json['data'].delete("type")
expect { from_json(json.to_pson) }.to raise_error(ArgumentError)
end
it "should set its method" do
from_json(@request.to_pson).method.should == "find"
end
it "should fail if no method is provided" do
json = PSON.parse(@request.to_pson)
json['data'].delete("method")
expect { from_json(json.to_pson) }.to raise_error(ArgumentError)
end
it "should initialize with all attributes and options" do
@request.ip = "127.0.0.1"
@request.options["opt"] = "value"
result = from_json(@request.to_pson)
result.options[:opt].should == "value"
result.ip.should == "127.0.0.1"
end
it "should set its instance as an instance if one is provided" do
facts = Puppet::Node::Facts.new("foo")
@request.instance = facts
result = from_json(@request.to_pson)
result.instance.should be_instance_of(Puppet::Node::Facts)
end
end
context '#do_request' do
before :each do
@request = Puppet::Indirector::Request.new(:myind, :find, "my key", nil)
end
context 'when not using SRV records' do
before :each do
Puppet.settings[:use_srv_records] = false
end
it "yields the request with the default server and port when no server or port were specified on the original request" do
count = 0
rval = @request.do_request(:puppet, 'puppet.example.com', '90210') do |got|
count += 1
got.server.should == 'puppet.example.com'
got.port.should == '90210'
'Block return value'
end
count.should == 1
rval.should == 'Block return value'
end
end
context 'when using SRV records' do
before :each do
Puppet.settings[:use_srv_records] = true
Puppet.settings[:srv_domain] = 'example.com'
end
it "yields the request with the original server and port unmodified" do
@request.server = 'puppet.example.com'
@request.port = '90210'
count = 0
rval = @request.do_request do |got|
count += 1
got.server.should == 'puppet.example.com'
got.port.should == '90210'
'Block return value'
end
count.should == 1
rval.should == 'Block return value'
end
context "when SRV returns servers" do
before :each do
@dns_mock = mock('dns')
Resolv::DNS.expects(:new).returns(@dns_mock)
@port = 7205
@host = '_x-puppet._tcp.example.com'
@srv_records = [Resolv::DNS::Resource::IN::SRV.new(0, 0, @port, @host)]
@dns_mock.expects(:getresources).
with("_x-puppet._tcp.#{Puppet.settings[:srv_domain]}", Resolv::DNS::Resource::IN::SRV).
returns(@srv_records)
end
it "yields a request using the server and port from the SRV record" do
count = 0
rval = @request.do_request do |got|
count += 1
got.server.should == '_x-puppet._tcp.example.com'
got.port.should == 7205
@block_return
end
count.should == 1
rval.should == @block_return
end
it "should fall back to the default server when the block raises a SystemCallError" do
count = 0
second_pass = nil
rval = @request.do_request(:puppet, 'puppet', 8140) do |got|
count += 1
if got.server == '_x-puppet._tcp.example.com' then
raise SystemCallError, "example failure"
else
second_pass = got
end
@block_return
end
second_pass.server.should == 'puppet'
second_pass.port.should == 8140
count.should == 2
rval.should == @block_return
end
end
end
end
describe "#remote?" do
def request(options = {})
Puppet::Indirector::Request.new('node', 'find', 'localhost', nil, options)
end
it "should not be unless node or ip is set" do
request.should_not be_remote
end
it "should be remote if node is set" do
request(:node => 'example.com').should be_remote
end
it "should be remote if ip is set" do
request(:ip => '127.0.0.1').should be_remote
end
it "should be remote if node and ip are set" do
request(:node => 'example.com', :ip => '127.0.0.1').should be_remote
end
end
end
diff --git a/spec/unit/indirector/rest_spec.rb b/spec/unit/indirector/rest_spec.rb
index 0bf0bcb48..59558ab93 100755
--- a/spec/unit/indirector/rest_spec.rb
+++ b/spec/unit/indirector/rest_spec.rb
@@ -1,541 +1,537 @@
#! /usr/bin/env ruby
require 'spec_helper'
require 'puppet/indirector'
require 'puppet/indirector/errors'
require 'puppet/indirector/rest'
HTTP_ERROR_CODES = [300, 400, 500]
# Just one from each category since the code makes no real distinctions
shared_examples_for "a REST terminus method" do |terminus_method|
describe "when talking to an older master" do
it "should set backward compatibility settings" do
response.stubs(:[]).with(Puppet::Network::HTTP::HEADER_PUPPET_VERSION).returns nil
terminus.send(terminus_method, request)
Puppet[:report_serialization_format].should == 'yaml'
Puppet[:legacy_query_parameter_serialization].should == true
end
end
describe "when talking to a 3.3.1 master" do
it "should not set backward compatibility settings" do
response.stubs(:[]).with(Puppet::Network::HTTP::HEADER_PUPPET_VERSION).returns "3.3.1"
terminus.send(terminus_method, request)
Puppet[:report_serialization_format].should == 'pson'
Puppet[:legacy_query_parameter_serialization].should == false
end
end
HTTP_ERROR_CODES.each do |code|
describe "when the response code is #{code}" do
let(:response) { mock_response(code, 'error messaged!!!') }
it "raises an http error with the body of the response" do
expect {
terminus.send(terminus_method, request)
}.to raise_error(Net::HTTPError, "Error #{code} on SERVER: #{response.body}")
end
it "does not attempt to deserialize the response" do
model.expects(:convert_from).never
expect {
terminus.send(terminus_method, request)
}.to raise_error(Net::HTTPError)
end
# I'm not sure what this means or if it's used
it "if the body is empty raises an http error with the response header" do
response.stubs(:body).returns ""
response.stubs(:message).returns "fhqwhgads"
expect {
terminus.send(terminus_method, request)
}.to raise_error(Net::HTTPError, "Error #{code} on SERVER: #{response.message}")
end
describe "and the body is compressed" do
it "raises an http error with the decompressed body of the response" do
uncompressed_body = "why"
compressed_body = Zlib::Deflate.deflate(uncompressed_body)
response = mock_response(code, compressed_body, 'text/plain', 'deflate')
connection.expects(http_method).returns(response)
expect {
terminus.send(terminus_method, request)
}.to raise_error(Net::HTTPError, "Error #{code} on SERVER: #{uncompressed_body}")
end
end
end
end
end
shared_examples_for "a deserializing terminus method" do |terminus_method|
describe "when the response has no content-type" do
let(:response) { mock_response(200, "body", nil, nil) }
it "raises an error" do
expect {
terminus.send(terminus_method, request)
}.to raise_error(RuntimeError, "No content type in http response; cannot parse")
end
end
it "doesn't catch errors in deserialization" do
model.expects(:convert_from).raises(Puppet::Error, "Whoa there")
expect { terminus.send(terminus_method, request) }.to raise_error(Puppet::Error, "Whoa there")
end
end
describe Puppet::Indirector::REST do
before :all do
class Puppet::TestModel
extend Puppet::Indirector
indirects :test_model
attr_accessor :name, :data
def initialize(name = "name", data = '')
@name = name
@data = data
end
def self.convert_from(format, string)
new('', string)
end
def self.convert_from_multiple(format, string)
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
end
# The subclass must not be all caps even though the superclass is
class Puppet::TestModel::Rest < Puppet::Indirector::REST
end
Puppet::TestModel.indirection.terminus_class = :rest
end
after :all do
Puppet::TestModel.indirection.delete
# Remove the class, unlinking it from the rest of the system.
Puppet.send(:remove_const, :TestModel)
end
let(:terminus_class) { Puppet::TestModel::Rest }
let(:terminus) { Puppet::TestModel.indirection.terminus(:rest) }
let(:indirection) { Puppet::TestModel.indirection }
let(:model) { Puppet::TestModel }
def mock_response(code, body, content_type='text/plain', encoding=nil)
obj = stub('http 200 ok', :code => code.to_s, :body => body)
obj.stubs(:[]).with('content-type').returns(content_type)
obj.stubs(:[]).with('content-encoding').returns(encoding)
obj.stubs(:[]).with(Puppet::Network::HTTP::HEADER_PUPPET_VERSION).returns(Puppet.version)
obj
end
def find_request(key, options={})
Puppet::Indirector::Request.new(:test_model, :find, key, nil, options)
end
def head_request(key, options={})
Puppet::Indirector::Request.new(:test_model, :head, key, nil, options)
end
def search_request(key, options={})
Puppet::Indirector::Request.new(:test_model, :search, key, nil, options)
end
def delete_request(key, options={})
Puppet::Indirector::Request.new(:test_model, :destroy, key, nil, options)
end
def save_request(key, instance, options={})
Puppet::Indirector::Request.new(:test_model, :save, key, instance, options)
end
- it "should include the v1 REST API module" do
- Puppet::Indirector::REST.ancestors.should be_include(Puppet::Network::HTTP::API::V1)
- end
-
it "should have a method for specifying what setting a subclass should use to retrieve its server" do
terminus_class.should respond_to(:use_server_setting)
end
it "should use any specified setting to pick the server" do
terminus_class.expects(:server_setting).returns :inventory_server
Puppet[:inventory_server] = "myserver"
terminus_class.server.should == "myserver"
end
it "should default to :server for the server setting" do
terminus_class.expects(:server_setting).returns nil
Puppet[:server] = "myserver"
terminus_class.server.should == "myserver"
end
it "should have a method for specifying what setting a subclass should use to retrieve its port" do
terminus_class.should respond_to(:use_port_setting)
end
it "should use any specified setting to pick the port" do
terminus_class.expects(:port_setting).returns :ca_port
Puppet[:ca_port] = "321"
terminus_class.port.should == 321
end
it "should default to :port for the port setting" do
terminus_class.expects(:port_setting).returns nil
Puppet[:masterport] = "543"
terminus_class.port.should == 543
end
it 'should default to :puppet for the srv_service' do
Puppet::Indirector::REST.srv_service.should == :puppet
end
describe "when creating an HTTP client" do
it "should use the class's server and port if the indirection request provides neither" do
@request = stub 'request', :key => "foo", :server => nil, :port => nil
terminus.class.expects(:port).returns 321
terminus.class.expects(:server).returns "myserver"
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::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::HttpPool.expects(:http_instance).with("myserver", 321).returns "myconn"
terminus.network(@request).should == "myconn"
end
end
describe "#find" do
let(:http_method) { :get }
let(:response) { mock_response(200, 'body') }
let(:connection) { stub('mock http connection', :get => response, :verify_callback= => nil) }
let(:request) { find_request('foo') }
before :each do
terminus.stubs(:network).returns(connection)
end
it_behaves_like 'a REST terminus method', :find
it_behaves_like 'a deserializing terminus method', :find
describe "with a long set of parameters" do
it "calls post on the connection with the query params in the body" do
params = {}
'aa'.upto('zz') do |s|
params[s] = 'foo'
end
# The request special-cases this parameter, and it
# won't be passed on to the server, so we remove it here
# to avoid a failure.
params.delete('ip')
request = find_request('whoa', params)
connection.expects(:post).with do |uri, body|
body.split("&").sort == params.map {|key,value| "#{key}=#{value}"}.sort
end.returns(mock_response(200, 'body'))
terminus.find(request)
end
end
describe "with no parameters" do
it "calls get on the connection" do
request = find_request('foo bar')
connection.expects(:get).with('/production/test_model/foo%20bar?', anything).returns(mock_response('200', 'response body'))
terminus.find(request).should == model.new('foo bar', 'response body')
end
end
it "returns nil on 404" do
response = mock_response('404', nil)
connection.expects(:get).returns(response)
terminus.find(request).should == nil
end
it "asks the model to deserialize the response body and sets the name on the resulting object to the find key" do
connection.expects(:get).returns response
model.expects(:convert_from).with(response['content-type'], response.body).returns(
model.new('overwritten', 'decoded body')
)
terminus.find(request).should == model.new('foo', 'decoded body')
end
it "doesn't require the model to support name=" do
connection.expects(:get).returns response
instance = model.new('name', 'decoded body')
model.expects(:convert_from).with(response['content-type'], response.body).returns(instance)
instance.expects(:respond_to?).with(:name=).returns(false)
instance.expects(:name=).never
terminus.find(request).should == model.new('name', 'decoded body')
end
it "provides an Accept header containing the list of supported formats joined with commas" do
connection.expects(:get).with(anything, has_entry("Accept" => "supported, formats")).returns(response)
terminus.model.expects(:supported_formats).returns %w{supported formats}
terminus.find(request)
end
it "adds an Accept-Encoding header" do
terminus.expects(:add_accept_encoding).returns({"accept-encoding" => "gzip"})
connection.expects(:get).with(anything, has_entry("accept-encoding" => "gzip")).returns(response)
terminus.find(request)
end
it "uses only the mime-type from the content-type header when asking the model to deserialize" do
response = mock_response('200', 'mydata', "text/plain; charset=utf-8")
connection.expects(:get).returns(response)
model.expects(:convert_from).with("text/plain", "mydata").returns "myobject"
terminus.find(request).should == "myobject"
end
it "decompresses the body before passing it to the model for deserialization" do
uncompressed_body = "Why hello there"
compressed_body = Zlib::Deflate.deflate(uncompressed_body)
response = mock_response('200', compressed_body, 'text/plain', 'deflate')
connection.expects(:get).returns(response)
model.expects(:convert_from).with("text/plain", uncompressed_body).returns "myobject"
terminus.find(request).should == "myobject"
end
end
describe "#head" do
let(:http_method) { :head }
let(:response) { mock_response(200, nil) }
let(:connection) { stub('mock http connection', :head => response, :verify_callback= => nil) }
let(:request) { head_request('foo') }
before :each do
terminus.stubs(:network).returns(connection)
end
it_behaves_like 'a REST terminus method', :head
it "returns true if there was a successful http response" do
connection.expects(:head).returns mock_response('200', nil)
terminus.head(request).should == true
end
it "returns false on a 404 response" do
connection.expects(:head).returns mock_response('404', nil)
terminus.head(request).should == false
end
end
describe "#search" do
let(:http_method) { :get }
let(:response) { mock_response(200, 'data1,data2,data3') }
let(:connection) { stub('mock http connection', :get => response, :verify_callback= => nil) }
let(:request) { search_request('foo') }
before :each do
terminus.stubs(:network).returns(connection)
end
it_behaves_like 'a REST terminus method', :search
it_behaves_like 'a deserializing terminus method', :search
it "should call the GET http method on a network connection" do
connection.expects(:get).with('/production/test_models/foo', has_key('Accept')).returns mock_response(200, 'data3, data4')
terminus.search(request)
end
it "returns an empty list on 404" do
response = mock_response('404', nil)
connection.expects(:get).returns(response)
terminus.search(request).should == []
end
it "asks the model to deserialize the response body into multiple instances" do
terminus.search(request).should == [model.new('', 'data1'), model.new('', 'data2'), model.new('', 'data3')]
end
it "should provide an Accept header containing the list of supported formats joined with commas" do
connection.expects(:get).with(anything, has_entry("Accept" => "supported, formats")).returns(mock_response(200, ''))
terminus.model.expects(:supported_formats).returns %w{supported formats}
terminus.search(request)
end
it "should return an empty array if serialization returns nil" do
model.stubs(:convert_from_multiple).returns nil
terminus.search(request).should == []
end
end
describe "#destroy" do
let(:http_method) { :delete }
let(:response) { mock_response(200, 'body') }
let(:connection) { stub('mock http connection', :delete => response, :verify_callback= => nil) }
let(:request) { delete_request('foo') }
before :each do
terminus.stubs(:network).returns(connection)
end
it_behaves_like 'a REST terminus method', :destroy
it_behaves_like 'a deserializing terminus method', :destroy
it "should call the DELETE http method on a network connection" do
connection.expects(:delete).with('/production/test_model/foo', has_key('Accept')).returns(response)
terminus.destroy(request)
end
it "should fail if any options are provided, since DELETE apparently does not support query options" do
request = delete_request('foo', :one => "two", :three => "four")
expect { terminus.destroy(request) }.to raise_error(ArgumentError)
end
it "should deserialize and return the http response" do
connection.expects(:delete).returns response
terminus.destroy(request).should == model.new('', 'body')
end
it "returns nil on 404" do
response = mock_response('404', nil)
connection.expects(:delete).returns(response)
terminus.destroy(request).should == nil
end
it "should provide an Accept header containing the list of supported formats joined with commas" do
connection.expects(:delete).with(anything, has_entry("Accept" => "supported, formats")).returns(response)
terminus.model.expects(:supported_formats).returns %w{supported formats}
terminus.destroy(request)
end
end
describe "#save" do
let(:http_method) { :put }
let(:response) { mock_response(200, 'body') }
let(:connection) { stub('mock http connection', :put => response, :verify_callback= => nil) }
let(:instance) { model.new('the thing', 'some contents') }
let(:request) { save_request(instance.name, instance) }
before :each do
terminus.stubs(:network).returns(connection)
end
it_behaves_like 'a REST terminus method', :save
it "should call the PUT http method on a network connection" do
connection.expects(:put).with('/production/test_model/the%20thing', anything, has_key("Content-Type")).returns response
terminus.save(request)
end
it "should fail if any options are provided, since PUT apparently does not support query options" do
request = save_request(instance.name, instance, :one => "two", :three => "four")
expect { terminus.save(request) }.to raise_error(ArgumentError)
end
it "should serialize the instance using the default format and pass the result as the body of the request" do
instance.expects(:render).returns "serial_instance"
connection.expects(:put).with(anything, "serial_instance", anything).returns response
terminus.save(request)
end
it "returns nil on 404" do
response = mock_response('404', nil)
connection.expects(:put).returns(response)
terminus.save(request).should == nil
end
it "returns nil" do
connection.expects(:put).returns response
terminus.save(request).should be_nil
end
it "should provide an Accept header containing the list of supported formats joined with commas" do
connection.expects(:put).with(anything, anything, has_entry("Accept" => "supported, formats")).returns(response)
instance.expects(:render).returns('')
model.expects(:supported_formats).returns %w{supported formats}
instance.expects(:mime).returns "supported"
terminus.save(request)
end
it "should provide a Content-Type header containing the mime-type of the sent object" do
instance.expects(:mime).returns "mime"
connection.expects(:put).with(anything, anything, has_entry('Content-Type' => "mime")).returns(response)
terminus.save(request)
end
end
context 'dealing with SRV settings' do
[
:destroy,
:find,
:head,
:save,
:search
].each do |method|
it "##{method} passes the SRV service, and fall-back server & port to the request's do_request method" do
request = Puppet::Indirector::Request.new(:indirection, method, 'key', nil)
stub_response = mock_response('200', 'body')
request.expects(:do_request).with(terminus.class.srv_service, terminus.class.server, terminus.class.port).returns(stub_response)
terminus.send(method, request)
end
end
end
end
diff --git a/spec/unit/indirector/ssl_file_spec.rb b/spec/unit/indirector/ssl_file_spec.rb
index 8406cc9d7..8d13bdc94 100755
--- a/spec/unit/indirector/ssl_file_spec.rb
+++ b/spec/unit/indirector/ssl_file_spec.rb
@@ -1,327 +1,328 @@
#! /usr/bin/env ruby
require 'spec_helper'
require 'puppet/indirector/ssl_file'
describe Puppet::Indirector::SslFile do
include PuppetSpec::Files
before :all do
@indirection = stub 'indirection', :name => :testing, :model => @model
Puppet::Indirector::Indirection.expects(:instance).with(:testing).returns(@indirection)
module Testing; end
@file_class = class Testing::MyType < Puppet::Indirector::SslFile
self
end
end
before :each do
@model = mock 'model'
@setting = :certdir
@file_class.store_in @setting
@file_class.store_at nil
@file_class.store_ca_at nil
@path = make_absolute("/thisdoesntexist/my_directory")
Puppet[:noop] = false
Puppet[@setting] = @path
Puppet[:trace] = false
end
after :each do
@file_class.store_in nil
@file_class.store_at nil
@file_class.store_ca_at nil
end
it "should use :main and :ssl upon initialization" do
Puppet.settings.expects(:use).with(:main, :ssl)
@file_class.new
end
it "should return a nil collection directory if no directory setting has been provided" do
@file_class.store_in nil
@file_class.collection_directory.should be_nil
end
it "should return a nil file location if no location has been provided" do
@file_class.store_at nil
@file_class.file_location.should be_nil
end
it "should fail if no store directory or file location has been set" do
Puppet.settings.expects(:use).with(:main, :ssl)
@file_class.store_in nil
@file_class.store_at nil
expect {
@file_class.new
}.to raise_error(Puppet::DevError, /No file or directory setting provided/)
end
describe "when managing ssl files" do
before do
Puppet.settings.stubs(:use)
@searcher = @file_class.new
@cert = stub 'certificate', :name => "myname"
@certpath = File.join(@path, "myname.pem")
@request = stub 'request', :key => @cert.name, :instance => @cert
end
it "should consider the file a ca file if the name is equal to what the SSL::Host class says is the CA name" do
Puppet::SSL::Host.expects(:ca_name).returns "amaca"
@searcher.should be_ca("amaca")
end
describe "when choosing the location for certificates" do
it "should set them at the ca setting's path if a ca setting is available and the name resolves to the CA name" do
@file_class.store_in nil
@file_class.store_at :mysetting
@file_class.store_ca_at :cakey
Puppet[:cakey] = File.expand_path("/ca/file")
@searcher.expects(:ca?).with(@cert.name).returns true
@searcher.path(@cert.name).should == Puppet[:cakey]
end
it "should set them at the file location if a file setting is available" do
@file_class.store_in nil
@file_class.store_at :cacrl
Puppet[:cacrl] = File.expand_path("/some/file")
@searcher.path(@cert.name).should == Puppet[:cacrl]
end
it "should set them in the setting directory, with the certificate name plus '.pem', if a directory setting is available" do
@searcher.path(@cert.name).should == @certpath
end
['../foo', '..\\foo', './../foo', '.\\..\\foo',
'/foo', '//foo', '\\foo', '\\\\goo',
"test\0/../bar", "test\0\\..\\bar",
"..\\/bar", "/tmp/bar", "/tmp\\bar", "tmp\\bar",
" / bar", " /../ bar", " \\..\\ bar",
"c:\\foo", "c:/foo", "\\\\?\\UNC\\bar", "\\\\foo\\bar",
"\\\\?\\c:\\foo", "//?/UNC/bar", "//foo/bar",
"//?/c:/foo",
].each do |input|
it "should resist directory traversal attacks (#{input.inspect})" do
expect { @searcher.path(input) }.to raise_error
end
end
# REVISIT: Should probably test MS-DOS reserved names here, too, since
# they would represent a vulnerability on a Win32 system, should we ever
# support that path. Don't forget that 'CON.foo' == 'CON'
# --daniel 2011-09-24
end
describe "when finding certificates on disk" do
describe "and no certificate is present" do
it "should return nil" do
- Puppet::FileSystem::File.expects(:exist?).with(@path).returns(true)
+ Puppet::FileSystem.expects(:exist?).with(@path).returns(true)
Dir.expects(:entries).with(@path).returns([])
- Puppet::FileSystem::File.expects(:exist?).with(@certpath).returns(false)
+ Puppet::FileSystem.expects(:exist?).with(@certpath).returns(false)
@searcher.find(@request).should be_nil
end
end
describe "and a certificate is present" do
let(:cert) { mock 'cert' }
let(:model) { mock 'model' }
before(:each) do
@file_class.stubs(:model).returns model
end
context "is readable" do
it "should return an instance of the model, which it should use to read the certificate" do
- Puppet::FileSystem::File.expects(:exist?).with(@certpath).returns true
+ Puppet::FileSystem.expects(:exist?).with(@certpath).returns true
model.expects(:new).with("myname").returns cert
cert.expects(:read).with(@certpath)
@searcher.find(@request).should equal(cert)
end
end
context "is unreadable" do
it "should raise an exception" do
- Puppet::FileSystem::File.expects(:exist?).with(@certpath).returns(true)
+ Puppet::FileSystem.expects(:exist?).with(@certpath).returns(true)
model.expects(:new).with("myname").returns cert
cert.expects(:read).with(@certpath).raises(Errno::EACCES)
expect {
@searcher.find(@request)
}.to raise_error(Errno::EACCES)
end
end
end
describe "and a certificate is present but has uppercase letters" do
before do
@request = stub 'request', :key => "myhost"
end
# This is kind of more an integration test; it's for #1382, until
# 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")
- Puppet::FileSystem::File.expects(:exist?).with(@path).returns(false)
+ Puppet::FileSystem.expects(:exist?).with(@path).returns(false)
dir, file = File.split(@path)
- Puppet::FileSystem::File.expects(:exist?).with(dir).returns true
+ Puppet::FileSystem.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)
cert = mock 'cert'
model = mock 'model'
@searcher.stubs(:model).returns model
@searcher.model.expects(:new).with("myhost").returns cert
cert.expects(:read).with(@path)
@searcher.find(@request)
end
end
end
describe "when saving certificates to disk" do
before do
FileTest.stubs(:directory?).returns true
FileTest.stubs(:writable?).returns true
end
it "should fail if the directory is absent" do
FileTest.expects(:directory?).with(File.dirname(@certpath)).returns false
lambda { @searcher.save(@request) }.should raise_error(Puppet::Error)
end
it "should fail if the directory is not writeable" do
FileTest.stubs(:directory?).returns true
FileTest.expects(:writable?).with(File.dirname(@certpath)).returns false
lambda { @searcher.save(@request) }.should raise_error(Puppet::Error)
end
it "should save to the path the output of converting the certificate to a string" do
fh = mock 'filehandle'
fh.expects(:print).with("mycert")
@searcher.stubs(:write).yields fh
@cert.expects(:to_s).returns "mycert"
@searcher.save(@request)
end
describe "and a directory setting is set" do
it "should use the Settings class to write the file" do
@searcher.class.store_in @setting
fh = mock 'filehandle'
fh.stubs :print
Puppet.settings.setting(@setting).expects(:open_file).with(@certpath, 'w').yields fh
@searcher.save(@request)
end
end
describe "and a file location is set" do
it "should use the filehandle provided by the Settings" do
@searcher.class.store_at @setting
fh = mock 'filehandle'
fh.stubs :print
Puppet.settings.setting(@setting).expects(:open).with('w').yields fh
@searcher.save(@request)
end
end
describe "and the name is the CA name and a ca setting is set" do
it "should use the filehandle provided by the Settings" do
@searcher.class.store_at @setting
@searcher.class.store_ca_at :cakey
Puppet[:cakey] = "castuff stub"
fh = mock 'filehandle'
fh.stubs :print
Puppet.settings.setting(:cakey).expects(:open).with('w').yields fh
@searcher.stubs(:ca?).returns true
@searcher.save(@request)
end
end
end
describe "when destroying certificates" do
describe "that do not exist" do
before do
- Puppet::FileSystem::File.expects(:exist?).with(@certpath).returns false
+ Puppet::FileSystem.expects(:exist?).with(Puppet::FileSystem.pathname(@certpath)).returns false
end
it "should return false" do
@searcher.destroy(@request).should be_false
end
end
describe "that exist" do
it "should unlink the certificate file" do
- Puppet::FileSystem::File.expects(:exist?).with(@certpath).returns true
- Puppet::FileSystem::File.expects(:unlink).with(@certpath)
+ path = Puppet::FileSystem.pathname(@certpath)
+ Puppet::FileSystem.expects(:exist?).with(path).returns true
+ Puppet::FileSystem.expects(:unlink).with(path)
@searcher.destroy(@request)
end
it "should log that is removing the file" do
- Puppet::FileSystem::File.stubs(:exist?).returns true
- Puppet::FileSystem::File.stubs(:unlink)
+ Puppet::FileSystem.stubs(:exist?).returns true
+ Puppet::FileSystem.stubs(:unlink)
Puppet.expects(:notice)
@searcher.destroy(@request)
end
end
end
describe "when searching for certificates" do
let(:one) { stub 'one' }
let(:two) { stub 'two' }
let(:one_path) { File.join(@path, 'one.pem') }
let(:two_path) { File.join(@path, 'two.pem') }
let(:model) { mock 'model' }
before :each do
@file_class.stubs(:model).returns model
end
it "should return a certificate instance for all files that exist" do
Dir.expects(:entries).with(@path).returns(%w{. .. one.pem two.pem})
model.expects(:new).with("one").returns one
one.expects(:read).with(one_path)
model.expects(:new).with("two").returns two
two.expects(:read).with(two_path)
@searcher.search(@request).should == [one, two]
end
it "should raise an exception if any file is unreadable" do
Dir.expects(:entries).with(@path).returns(%w{. .. one.pem two.pem})
model.expects(:new).with("one").returns(one)
one.expects(:read).with(one_path)
model.expects(:new).with("two").returns(two)
two.expects(:read).raises(Errno::EACCES)
expect {
@searcher.search(@request)
}.to raise_error(Errno::EACCES)
end
it "should skip any files that do not match /\.pem$/" do
Dir.expects(:entries).with(@path).returns(%w{. .. one two.notpem})
model.expects(:new).never
@searcher.search(@request).should == []
end
end
end
end
diff --git a/spec/unit/indirector/yaml_spec.rb b/spec/unit/indirector/yaml_spec.rb
index 6830869b1..6fae8831c 100755
--- a/spec/unit/indirector/yaml_spec.rb
+++ b/spec/unit/indirector/yaml_spec.rb
@@ -1,166 +1,166 @@
#! /usr/bin/env ruby
require 'spec_helper'
require 'puppet/indirector/yaml'
describe Puppet::Indirector::Yaml do
include PuppetSpec::Files
class TestSubject
attr_accessor :name
end
before :all do
@indirection = stub 'indirection', :name => :my_yaml, :register_terminus_type => nil
Puppet::Indirector::Indirection.expects(:instance).with(:my_yaml).returns(@indirection)
module MyYaml; end
@store_class = class MyYaml::MyType < Puppet::Indirector::Yaml
self
end
end
before :each do
@store = @store_class.new
@subject = TestSubject.new
@subject.name = :me
@dir = tmpdir("yaml_indirector")
Puppet[:clientyamldir] = @dir
Puppet.run_mode.stubs(:master?).returns false
@request = stub 'request', :key => :me, :instance => @subject
end
let(:serverdir) { File.expand_path("/server/yaml/dir") }
let(:clientdir) { File.expand_path("/client/yaml/dir") }
describe "when choosing file location" do
it "should use the server_datadir if the run_mode is master" do
Puppet.run_mode.stubs(:master?).returns true
Puppet[:yamldir] = serverdir
@store.path(:me).should =~ /^#{serverdir}/
end
it "should use the client yamldir if the run_mode is not master" do
Puppet.run_mode.stubs(:master?).returns false
Puppet[:clientyamldir] = clientdir
@store.path(:me).should =~ /^#{clientdir}/
end
it "should use the extension if one is specified" do
Puppet.run_mode.stubs(:master?).returns true
Puppet[:yamldir] = serverdir
@store.path(:me,'.farfignewton').should =~ %r{\.farfignewton$}
end
it "should assume an extension of .yaml if none is specified" do
Puppet.run_mode.stubs(:master?).returns true
Puppet[:yamldir] = serverdir
@store.path(:me).should =~ %r{\.yaml$}
end
it "should store all files in a single file root set in the Puppet defaults" do
@store.path(:me).should =~ %r{^#{@dir}}
end
it "should use the terminus name for choosing the subdirectory" do
@store.path(:me).should =~ %r{^#{@dir}/my_yaml}
end
it "should use the object's name to determine the file name" do
@store.path(:me).should =~ %r{me.yaml$}
end
['../foo', '..\\foo', './../foo', '.\\..\\foo',
'/foo', '//foo', '\\foo', '\\\\goo',
"test\0/../bar", "test\0\\..\\bar",
"..\\/bar", "/tmp/bar", "/tmp\\bar", "tmp\\bar",
" / bar", " /../ bar", " \\..\\ bar",
"c:\\foo", "c:/foo", "\\\\?\\UNC\\bar", "\\\\foo\\bar",
"\\\\?\\c:\\foo", "//?/UNC/bar", "//foo/bar",
"//?/c:/foo",
].each do |input|
it "should resist directory traversal attacks (#{input.inspect})" do
expect { @store.path(input) }.to raise_error
end
end
end
describe "when storing objects as YAML" do
it "should only store objects that respond to :name" do
@request.stubs(:instance).returns Object.new
proc { @store.save(@request) }.should raise_error(ArgumentError)
end
end
describe "when retrieving YAML" do
it "should read YAML in from disk and convert it to Ruby objects" do
@store.save(Puppet::Indirector::Request.new(:my_yaml, :save, "testing", @subject))
@store.find(Puppet::Indirector::Request.new(:my_yaml, :find, "testing", nil)).name.should == :me
end
it "should fail coherently when the stored YAML is invalid" do
saved_structure = Struct.new(:name).new("testing")
@store.save(Puppet::Indirector::Request.new(:my_yaml, :save, "testing", saved_structure))
File.open(@store.path(saved_structure.name), "w") do |file|
file.puts "{ invalid"
end
expect {
@store.find(Puppet::Indirector::Request.new(:my_yaml, :find, "testing", nil))
}.to raise_error(Puppet::Error, /Could not parse YAML data/)
end
end
describe "when searching" do
it "should return an array of fact instances with one instance for each file when globbing *" do
@request = stub 'request', :key => "*", :instance => @subject
@one = mock 'one'
@two = mock 'two'
@store.expects(:path).with(@request.key,'').returns :glob
Dir.expects(:glob).with(:glob).returns(%w{one.yaml two.yaml})
YAML.expects(:load_file).with("one.yaml").returns @one;
YAML.expects(:load_file).with("two.yaml").returns @two;
@store.search(@request).should == [@one, @two]
end
it "should return an array containing a single instance of fact when globbing 'one*'" do
@request = stub 'request', :key => "one*", :instance => @subject
@one = mock 'one'
@store.expects(:path).with(@request.key,'').returns :glob
Dir.expects(:glob).with(:glob).returns(%w{one.yaml})
YAML.expects(:load_file).with("one.yaml").returns @one;
@store.search(@request).should == [@one]
end
it "should return an empty array when the glob doesn't match anything" do
@request = stub 'request', :key => "f*ilglobcanfail*", :instance => @subject
@store.expects(:path).with(@request.key,'').returns :glob
Dir.expects(:glob).with(:glob).returns []
@store.search(@request).should == []
end
describe "when destroying" do
let(:path) do
File.join(@dir, @store.class.indirection_name.to_s, @request.key.to_s + ".yaml")
end
it "should unlink the right yaml file if it exists" do
- Puppet::FileSystem::File.expects(:exist?).with(path).returns true
- Puppet::FileSystem::File.expects(:unlink).with(path)
+ Puppet::FileSystem.expects(:exist?).with(path).returns true
+ Puppet::FileSystem.expects(:unlink).with(path)
@store.destroy(@request)
end
it "should not unlink the yaml file if it does not exists" do
- Puppet::FileSystem::File.expects(:exist?).with(path).returns false
- Puppet::FileSystem::File.expects(:unlink).with(path).never
+ Puppet::FileSystem.expects(:exist?).with(path).returns false
+ Puppet::FileSystem.expects(:unlink).with(path).never
@store.destroy(@request)
end
end
end
end
diff --git a/spec/unit/man_spec.rb b/spec/unit/man_spec.rb
new file mode 100755
index 000000000..28a208543
--- /dev/null
+++ b/spec/unit/man_spec.rb
@@ -0,0 +1,32 @@
+#! /usr/bin/env ruby
+require 'spec_helper'
+require 'puppet/face'
+
+describe Puppet::Face[:man, :current] do
+ let(:pager) { '/path/to/our/pager' }
+
+ around do |example|
+ oldpager = ENV['MANPAGER']
+ ENV['MANPAGER'] = pager
+ example.run
+ ENV['MANPAGER'] = oldpager
+ end
+
+ it "exits with 0 when generating man documentation for each available application" do
+ Puppet::Util.stubs(:which).with('ronn').returns(nil)
+ Puppet::Util.stubs(:which).with(pager).returns(pager)
+
+ Puppet::Application.available_application_names.each do |name|
+ next if %w{man face_base indirection_base}.include? name
+
+ klass = Puppet::Application.find('man')
+ app = klass.new(Puppet::Util::CommandLine.new('puppet', ['man', name]))
+
+ expect do
+ IO.stubs(:popen).with(pager, anything).yields($stdout)
+
+ expect { app.run }.to exit_with(0)
+ end.to_not have_printed(/undefined method `gsub'/)
+ end
+ end
+end
diff --git a/spec/unit/module_spec.rb b/spec/unit/module_spec.rb
index 5c4065e14..8e3c46989 100755
--- a/spec/unit/module_spec.rb
+++ b/spec/unit/module_spec.rb
@@ -1,696 +1,710 @@
#! /usr/bin/env ruby
require 'spec_helper'
require 'puppet_spec/files'
require 'puppet_spec/modules'
require 'puppet/module_tool/checksums'
describe Puppet::Module do
include PuppetSpec::Files
let(:env) { mock("environment") }
let(:path) { "/path" }
let(:name) { "mymod" }
let(:mod) { Puppet::Module.new(name, path, env) }
before do
# This is necessary because of the extra checks we have for the deprecated
# 'plugins' directory
- Puppet::FileSystem::File.stubs(:exist?).returns false
+ Puppet::FileSystem.stubs(:exist?).returns false
end
it "should have a class method that returns a named module from a given environment" do
env = mock 'module'
env.expects(:module).with(name).returns "yep"
Puppet::Node::Environment.expects(:new).with("myenv").returns env
Puppet::Module.find(name, "myenv").should == "yep"
end
it "should return nil if asked for a named module that doesn't exist" do
env = mock 'module'
env.expects(:module).with(name).returns nil
Puppet::Node::Environment.expects(:new).with("myenv").returns env
Puppet::Module.find(name, "myenv").should be_nil
end
describe "attributes" do
it "should support a 'version' attribute" do
mod.version = 1.09
mod.version.should == 1.09
end
it "should support a 'source' attribute" do
mod.source = "http://foo/bar"
mod.source.should == "http://foo/bar"
end
it "should support a 'project_page' attribute" do
mod.project_page = "http://foo/bar"
mod.project_page.should == "http://foo/bar"
end
it "should support an 'author' attribute" do
mod.author = "Luke Kanies <luke@madstop.com>"
mod.author.should == "Luke Kanies <luke@madstop.com>"
end
it "should support a 'license' attribute" do
mod.license = "GPL2"
mod.license.should == "GPL2"
end
it "should support a 'summary' attribute" do
mod.summary = "GPL2"
mod.summary.should == "GPL2"
end
it "should support a 'description' attribute" do
mod.description = "GPL2"
mod.description.should == "GPL2"
end
it "should support specifying a compatible puppet version" do
mod.puppetversion = "0.25"
mod.puppetversion.should == "0.25"
end
end
it "should validate that the puppet version is compatible" do
mod.puppetversion = "0.25"
Puppet.expects(:version).returns "0.25"
mod.validate_puppet_version
end
it "should fail if the specified puppet version is not compatible" do
mod.puppetversion = "0.25"
Puppet.stubs(:version).returns "0.24"
lambda { mod.validate_puppet_version }.should raise_error(Puppet::Module::IncompatibleModule)
end
describe "when finding unmet dependencies" do
before do
- Puppet::FileSystem::File.unstub(:exist?)
+ Puppet::FileSystem.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,
:metadata => {
:dependencies => [{
"version_requirement" => ">= 2.2.0",
"name" => "baz/foobar"
}]
}
)
mod.unmet_dependencies.should == [{
:reason => :missing,
:name => "baz/foobar",
:version_constraint => ">= 2.2.0",
:parent => { :name => 'puppetlabs/needy', :version => 'v9.9.9' },
:mod_details => { :installed_version => nil }
}]
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,
:metadata => {
:dependencies => [{
"version_requirement" => ">= 2.2.0",
"name" => "baz/foobar=bar"
}]
}
)
mod.unmet_dependencies.should == [{
:reason => :missing,
:name => "baz/foobar=bar",
:version_constraint => ">= 2.2.0",
:parent => { :name => 'puppetlabs/needy', :version => 'v9.9.9' },
:mod_details => { :installed_version => nil }
}]
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
+ env = Puppet::Node::Environment.create(:testing, [@modpath], '')
+
mod = PuppetSpec::Modules.create(
- 'foobar',
+ 'test_gte_req',
@modpath,
:metadata => {
:dependencies => [{
"version_requirement" => ">= 2.2.0",
"name" => "baz/foobar"
}]
- }
+ },
+ :environment => env
)
mod2 = PuppetSpec::Modules.create(
- 'foobaz',
+ 'test_specific_req',
@modpath,
:metadata => {
:dependencies => [{
"version_requirement" => "1.0.0",
"name" => "baz/foobar"
}]
- }
+ },
+ :environment => env
)
PuppetSpec::Modules.create(
'foobar',
@modpath,
- :metadata => { :version => '2.0.0', :author => 'baz' }
+ :metadata => { :version => '2.0.0', :author => 'baz' },
+ :environment => env
)
mod.unmet_dependencies.should == [{
:reason => :version_mismatch,
:name => "baz/foobar",
:version_constraint => ">= 2.2.0",
- :parent => { :version => "v9.9.9", :name => "puppetlabs/foobar" },
+ :parent => { :version => "v9.9.9", :name => "puppetlabs/test_gte_req" },
:mod_details => { :installed_version => "2.0.0" }
}]
mod2.unmet_dependencies.should == [{
:reason => :version_mismatch,
:name => "baz/foobar",
:version_constraint => "v1.0.0",
- :parent => { :version => "v9.9.9", :name => "puppetlabs/foobaz" },
+ :parent => { :version => "v9.9.9", :name => "puppetlabs/test_specific_req" },
:mod_details => { :installed_version => "2.0.0" }
}]
end
it "should consider a dependency without a version requirement to be satisfied" do
+ env = Puppet::Node::Environment.create(:testing, [@modpath], '')
+
mod = PuppetSpec::Modules.create(
'foobar',
@modpath,
:metadata => {
:dependencies => [{
"name" => "baz/foobar"
}]
- }
+ },
+ :environment => env
)
PuppetSpec::Modules.create(
'foobar',
@modpath,
:metadata => {
:version => '2.0.0',
:author => 'baz'
- }
+ },
+ :environment => env
)
mod.unmet_dependencies.should be_empty
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
+ env = Puppet::Node::Environment.create(:testing, [@modpath], '')
+
mod = PuppetSpec::Modules.create(
'foobar',
@modpath,
:metadata => {
:dependencies => [{
"name" => "baz/foobar"
}]
- }
+ },
+ :environment => env
)
PuppetSpec::Modules.create(
'foobar',
@modpath,
:metadata => {
:version => '5.1',
:author => 'baz'
- }
+ },
+ :environment => env
)
mod.unmet_dependencies.should == [{
:reason => :non_semantic_version,
:parent => { :version => "v9.9.9", :name => "puppetlabs/foobar" },
:mod_details => { :installed_version => "5.1" },
:name => "baz/foobar",
:version_constraint => ">= 0.0.0"
}]
end
it "should have valid dependencies when no dependencies have been specified" do
mod = PuppetSpec::Modules.create(
'foobar',
@modpath,
:metadata => {
:dependencies => []
}
)
mod.unmet_dependencies.should == []
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
+ env = Puppet::Node::Environment.create(:testing, [@modpath], '')
+
mod = PuppetSpec::Modules.create(
name,
@modpath,
:metadata => {
:dependencies => [
{
"version_requirement" => ">= 2.2.0",
"name" => "baz/satisfied"
},
{
"version_requirement" => ">= 2.2.0",
"name" => "baz/notsatisfied"
}
]
- }
+ },
+ :environment => env
)
PuppetSpec::Modules.create(
'satisfied',
@modpath,
:metadata => {
:version => '3.3.0',
:author => 'baz'
- }
+ },
+ :environment => env
)
mod.unmet_dependencies.should == [{
:reason => :missing,
:mod_details => { :installed_version => nil },
:parent => { :version => "v9.9.9", :name => "puppetlabs/#{name}" },
:name => "baz/notsatisfied",
:version_constraint => ">= 2.2.0"
}]
end
it "should be empty when all dependencies are met" do
+ env = Puppet::Node::Environment.create(:testing, [@modpath], '')
+
mod = PuppetSpec::Modules.create(
'mymod2',
@modpath,
:metadata => {
:dependencies => [
{
"version_requirement" => ">= 2.2.0",
"name" => "baz/satisfied"
},
{
"version_requirement" => "< 2.2.0",
"name" => "baz/alsosatisfied"
}
]
- }
+ },
+ :environment => env
)
PuppetSpec::Modules.create(
'satisfied',
@modpath,
:metadata => {
:version => '3.3.0',
:author => 'baz'
- }
+ },
+ :environment => env
)
PuppetSpec::Modules.create(
'alsosatisfied',
@modpath,
:metadata => {
:version => '2.1.0',
:author => 'baz'
- }
+ },
+ :environment => env
)
mod.unmet_dependencies.should be_empty
end
end
describe "when managing supported platforms" do
it "should support specifying a supported platform" do
mod.supports "solaris"
end
it "should support specifying a supported platform and version" do
mod.supports "solaris", 1.0
end
end
it "should return nil if asked for a module whose name is 'nil'" do
Puppet::Module.find(nil, "myenv").should be_nil
end
it "should provide support for logging" do
Puppet::Module.ancestors.should be_include(Puppet::Util::Logging)
end
it "should be able to be converted to a string" do
mod.to_s.should == "Module #{name}(#{path})"
end
it "should fail if its name is not alphanumeric" do
lambda { Puppet::Module.new(".something", "/path", env) }.should raise_error(Puppet::Module::InvalidName)
end
it "should require a name at initialization" do
lambda { Puppet::Module.new }.should raise_error(ArgumentError)
end
it "should accept an environment at initialization" do
Puppet::Module.new("foo", "/path", env).environment.should == env
end
describe '#modulepath' do
it "should return the directory the module is installed in, if a path exists" do
mod = Puppet::Module.new("foo", "/a/foo", env)
mod.modulepath.should == '/a'
end
end
[: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")
- Puppet::FileSystem::File.expects(:exist?).with(module_file).returns true
+ Puppet::FileSystem.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)
- Puppet::FileSystem::File.expects(:exist?).with(module_file).returns true
+ Puppet::FileSystem.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)
- Puppet::FileSystem::File.expects(:exist?).with(module_file).returns false
+ Puppet::FileSystem.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")
- Puppet::FileSystem::File.expects(:exist?).with(module_file).returns false
+ Puppet::FileSystem.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)
- Puppet::FileSystem::File.expects(:exist?).with(base).returns true
+ Puppet::FileSystem.expects(:exist?).with(base).returns true
mod.send(filetype.to_s.sub(/s$/, ''), nil).should == base
end
end
it "should return the path to the plugin directory" do
mod.plugin_directory.should == File.join(path, "lib")
end
end
describe Puppet::Module, "when finding matching manifests" do
before do
@mod = Puppet::Module.new("mymod", "/a", mock("environment"))
@pq_glob_with_extension = "yay/*.xx"
@fq_glob_with_extension = "/a/manifests/#{@pq_glob_with_extension}"
end
it "should return all manifests matching the glob pattern" do
Dir.expects(:glob).with(@fq_glob_with_extension).returns(%w{foo bar})
FileTest.stubs(:directory?).returns false
@mod.match_manifests(@pq_glob_with_extension).should == %w{foo bar}
end
it "should not return directories" do
Dir.expects(:glob).with(@fq_glob_with_extension).returns(%w{foo bar})
FileTest.expects(:directory?).with("foo").returns false
FileTest.expects(:directory?).with("bar").returns true
@mod.match_manifests(@pq_glob_with_extension).should == %w{foo}
end
it "should default to the 'init' file if no glob pattern is specified" do
- Puppet::FileSystem::File.expects(:exist?).with("/a/manifests/init.pp").returns(true)
- Puppet::FileSystem::File.expects(:exist?).with("/a/manifests/init.rb").returns(false)
+ Puppet::FileSystem.expects(:exist?).with("/a/manifests/init.pp").returns(true)
+ Puppet::FileSystem.expects(:exist?).with("/a/manifests/init.rb").returns(false)
@mod.match_manifests(nil).should == %w{/a/manifests/init.pp}
end
it "should return all manifests matching the glob pattern in all existing paths" do
Dir.expects(:glob).with(@fq_glob_with_extension).returns(%w{a b})
@mod.match_manifests(@pq_glob_with_extension).should == %w{a b}
end
it "should match the glob pattern plus '.{pp,rb}' if no extention is specified" do
Dir.expects(:glob).with("/a/manifests/yay/foo.{pp,rb}").returns(%w{yay})
@mod.match_manifests("yay/foo").should == %w{yay}
end
it "should return an empty array if no manifests matched" do
Dir.expects(:glob).with(@fq_glob_with_extension).returns([])
@mod.match_manifests(@pq_glob_with_extension).should == []
end
it "should raise an error if the pattern tries to leave the manifest directory" do
expect do
@mod.match_manifests("something/../../*")
end.to raise_error(Puppet::Module::InvalidFilePattern, 'The pattern "something/../../*" to find manifests in the module "mymod" is invalid and potentially unsafe.')
end
end
describe Puppet::Module do
include PuppetSpec::Files
before do
@modpath = tmpdir('modpath')
@module = PuppetSpec::Modules.create('mymod', @modpath)
end
it "should use 'License' in its current path as its metadata file" do
@module.license_file.should == "#{@modpath}/mymod/License"
end
it "should cache the license file" do
@module.expects(:path).once.returns nil
@module.license_file
@module.license_file
end
it "should use 'metadata.json' in its current path as its metadata file" do
@module.metadata_file.should == "#{@modpath}/mymod/metadata.json"
end
it "should have metadata if it has a metadata file and its data is not empty" do
- Puppet::FileSystem::File.expects(:exist?).with(@module.metadata_file).returns true
+ Puppet::FileSystem.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
- Puppet::FileSystem::File.expects(:exist?).with(@module.metadata_file).returns true
+ Puppet::FileSystem.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
- Puppet::FileSystem::File.expects(:exist?).with(@module.metadata_file).returns true
+ Puppet::FileSystem.expects(:exist?).with(@module.metadata_file).returns true
File.stubs(:read).with(@module.metadata_file).returns "/*
+-----------------------------------------------------------------------+
| |
| ==> DO NOT EDIT THIS FILE! <== |
| |
| You should edit the `Modulefile` and run `puppet-module build` |
| to generate the `metadata.json` file for your releases. |
| |
+-----------------------------------------------------------------------+
*/
{}"
@module.should_not be_has_metadata
end
it "should know if it is missing a metadata file" do
- Puppet::FileSystem::File.expects(:exist?).with(@module.metadata_file).returns false
+ Puppet::FileSystem.expects(:exist?).with(@module.metadata_file).returns false
@module.should_not be_has_metadata
end
it "should be able to parse its metadata file" do
@module.should respond_to(:load_metadata)
end
it "should parse its metadata file on initialization if it is present" do
Puppet::Module.any_instance.expects(:has_metadata?).returns true
Puppet::Module.any_instance.expects(:load_metadata)
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
+ Puppet::FileSystem.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
mod = Puppet::Module.new("foo", "/path", mock("env"))
mod.stubs(:metadata_file).returns "/my/file"
File.stubs(:read).with("/my/file").returns text
mod
end
describe "when loading the metadata file" do
before do
@data = {
:license => "GPL2",
:author => "luke",
:version => "1.0",
:source => "http://foo/",
:puppetversion => "0.25",
:dependencies => []
}
@module = a_module_with_metadata(@data)
end
%w{source author version license}.each do |attr|
it "should set #{attr} if present in the metadata file" do
@module.load_metadata
@module.send(attr).should == @data[attr.to_sym]
end
it "should fail if #{attr} is not present in the metadata file" do
@data.delete(attr.to_sym)
@text = @data.to_pson
File.stubs(:read).with("/my/file").returns @text
lambda { @module.load_metadata }.should raise_error(
Puppet::Module::MissingMetadata,
"No #{attr} module metadata provided for foo"
)
end
end
it "should set puppetversion if present in the metadata file" do
@module.load_metadata
@module.puppetversion.should == @data[:puppetversion]
end
context "when versionRequirement is used for dependency version info" do
before do
@data = {
:license => "GPL2",
:author => "luke",
:version => "1.0",
:source => "http://foo/",
:puppetversion => "0.25",
:dependencies => [
{
"versionRequirement" => "0.0.1",
"name" => "pmtacceptance/stdlib"
},
{
"versionRequirement" => "0.1.0",
"name" => "pmtacceptance/apache"
}
]
}
@module = a_module_with_metadata(@data)
end
it "should set the dependency version_requirement key" do
@module.load_metadata
@module.dependencies[0]['version_requirement'].should == "0.0.1"
end
it "should set the version_requirement key for all dependencies" do
@module.load_metadata
@module.dependencies[0]['version_requirement'].should == "0.0.1"
@module.dependencies[1]['version_requirement'].should == "0.1.0"
end
end
end
it "should be able to tell if there are local changes" do
modpath = tmpdir('modpath')
foo_checksum = 'acbd18db4cc2f85cedef654fccc4a4d8'
checksummed_module = PuppetSpec::Modules.create(
'changed',
modpath,
:metadata => {
:checksums => {
"foo" => foo_checksum,
}
}
)
foo_path = Pathname.new(File.join(checksummed_module.path, 'foo'))
IO.binwrite(foo_path, 'notfoo')
Puppet::ModuleTool::Checksums.new(foo_path).checksum(foo_path).should_not == foo_checksum
checksummed_module.has_local_changes?.should be_true
IO.binwrite(foo_path, 'foo')
Puppet::ModuleTool::Checksums.new(foo_path).checksum(foo_path).should == foo_checksum
checksummed_module.has_local_changes?.should be_false
end
it "should know what other modules require it" do
- Puppet.settings[:modulepath] = @modpath
+ env = Puppet::Node::Environment.create(:testing, [@modpath], '')
+
dependable = PuppetSpec::Modules.create(
'dependable',
@modpath,
- :metadata => {:author => 'puppetlabs'}
+ :metadata => {:author => 'puppetlabs'},
+ :environment => env
)
PuppetSpec::Modules.create(
'needy',
@modpath,
:metadata => {
:author => 'beggar',
:dependencies => [{
"version_requirement" => ">= 2.2.0",
"name" => "puppetlabs/dependable"
}]
- }
+ },
+ :environment => env
)
PuppetSpec::Modules.create(
'wantit',
@modpath,
:metadata => {
:author => 'spoiled',
:dependencies => [{
"version_requirement" => "< 5.0.0",
"name" => "puppetlabs/dependable"
}]
- }
+ },
+ :environment => env
)
dependable.required_by.should =~ [
{
"name" => "beggar/needy",
"version" => "9.9.9",
"version_requirement" => ">= 2.2.0"
},
{
"name" => "spoiled/wantit",
"version" => "9.9.9",
"version_requirement" => "< 5.0.0"
}
]
end
end
diff --git a/spec/unit/module_tool/applications/checksummer_spec.rb b/spec/unit/module_tool/applications/checksummer_spec.rb
index 96010f96c..a9db5b430 100644
--- a/spec/unit/module_tool/applications/checksummer_spec.rb
+++ b/spec/unit/module_tool/applications/checksummer_spec.rb
@@ -1,134 +1,134 @@
require 'spec_helper'
require 'puppet/module_tool/applications'
-describe Puppet::ModuleTool::Applications::Checksummer, :unless => Puppet.features.microsoft_windows? do
+describe Puppet::ModuleTool::Applications::Checksummer do
subject {
Puppet::ModuleTool::Applications::Checksummer.new(module_install_path)
}
let(:module_install_path) { 'foo' }
let(:module_metadata_file) { 'metadata.json' }
let(:module_install_pathname) {
module_install_pathname = mock()
Pathname.expects(:new).with(module_install_path).\
returns(module_install_pathname)
module_install_pathname
}
def stub_module_file_pathname(relative_path, present)
module_file_pathname = mock() do
expects(:exist?).with().returns(present)
end
module_install_pathname.expects(:+).with(relative_path).\
returns(module_file_pathname)
module_file_pathname
end
context %q{when metadata.json doesn't exist in the specified module install path} do
before(:each) do
stub_module_file_pathname(module_metadata_file, false)
subject.expects(:metadata_file).with().\
returns(module_install_pathname + module_metadata_file)
end
it 'raises an ArgumentError exception' do
lambda {
subject.run
}.should raise_error(ArgumentError, 'No metadata.json found.')
end
end
context 'when metadata.json exists in the specified module install path' do
module_files = {
'README' => '1',
'CHANGELOG' => '2',
'Modulefile' => '3',
}
let(:module_files) { module_files }
let(:checksum_computer) {
checksum_computer = mock()
Puppet::ModuleTool::Checksums.\
expects(:new).with(module_install_pathname).\
returns(checksum_computer)
checksum_computer
}
# all possible combinations (of all lengths) of the module files
module_files_combination =
1.upto(module_files.size()).inject([]) { |module_files_combination, n|
module_files.keys.combination(n) { |combination|
module_files_combination << combination
}
module_files_combination
}
def stub_module_file_pathname_with_checksum(relative_path, checksum)
module_file_pathname =
stub_module_file_pathname(relative_path, present = !checksum.nil?)
# mock the call of Puppet::ModuleTool::Checksums#checksum
expectation = checksum_computer.\
expects(:checksum).with(module_file_pathname)
if present
# return the cheksum directly
expectation.returns(checksum)
else
# if the file is not present, then the method should not be called
expectation.times(0)
end
module_file_pathname
end
def stub_module_files(overrides = {})
overrides.reject! { |key, value|
!module_files.include?(key)
}
module_files.merge(overrides).each { |relative_path, checksum|
stub_module_file_pathname_with_checksum(relative_path, checksum)
}
end
before(:each) do
stub_module_file_pathname(module_metadata_file, true)
subject.expects(:metadata_file).with().\
returns(module_install_pathname + module_metadata_file)
subject.expects(:metadata).with().\
returns({ 'checksums' => module_files })
end
module_files_combination.each do |removed_files|
it "reports removed file(s) #{removed_files.inspect}" do
stub_module_files(
removed_files.inject({}) { |overrides, removed_file|
overrides[removed_file] = nil
overrides
}
)
subject.run.should == removed_files
end
end
module_files_combination.each do |modified_files|
it "reports modified file(s) #{modified_files.inspect}" do
stub_module_files(
modified_files.inject({}) { |overrides, modified_file|
modified_checksum = module_files[modified_file].to_s.succ
modified_checksum = ' ' if modified_checksum.empty?
overrides[modified_file] = modified_checksum
overrides
}
)
subject.run.should == modified_files
end
end
it 'does not report unmodified files' do
stub_module_files()
subject.run.should == []
end
end
end
diff --git a/spec/unit/module_tool/applications/installer_spec.rb b/spec/unit/module_tool/applications/installer_spec.rb
index 3d3461949..dc6d7b009 100644
--- a/spec/unit/module_tool/applications/installer_spec.rb
+++ b/spec/unit/module_tool/applications/installer_spec.rb
@@ -1,294 +1,331 @@
require 'spec_helper'
require 'puppet/module_tool/applications'
require 'puppet_spec/modules'
require 'semver'
-describe Puppet::ModuleTool::Applications::Installer, :unless => Puppet.features.microsoft_windows? do
+describe Puppet::ModuleTool::Applications::Installer do
include PuppetSpec::Files
- before do
- FileUtils.mkdir_p(modpath1)
- fake_env.modulepath = [modpath1]
- FileUtils.touch(stdlib_pkg)
- Puppet.settings[:modulepath] = modpath1
- end
-
let(:unpacker) { stub(:run) }
let(:installer_class) { Puppet::ModuleTool::Applications::Installer }
- let(:modpath1) { File.join(tmpdir("installer"), "modpath1") }
- let(:stdlib_pkg) { File.join(modpath1, "pmtacceptance-stdlib-0.0.1.tar.gz") }
- let(:fake_env) { Puppet::Node::Environment.new('fake_env') }
- let(:options) { { :target_dir => modpath1 } }
+ let(:modpath1) do
+ path = File.join(tmpdir("installer"), "modpath1")
+ FileUtils.mkdir_p(path)
+ path
+ end
+ let(:stdlib_pkg) do
+ mod = File.join(modpath1, "pmtacceptance-stdlib-0.0.1.tar.gz")
+ FileUtils.touch(mod)
+ mod
+ end
+ let(:env) { Puppet::Node::Environment.create(:env, [modpath1], '') }
+ let(:options) do
+ {
+ :target_dir => modpath1,
+ :environment_instance => env,
+ }
+ end
let(:forge) do
forge = mock("Puppet::Forge")
forge.stubs(:remote_dependency_info).returns(remote_dependency_info)
forge.stubs(:uri).returns('forge-dev.puppetlabs.com')
remote_dependency_info.each_key do |mod|
remote_dependency_info[mod].each do |release|
forge.stubs(:retrieve).with(release['file']).returns("/fake_cache#{release['file']}")
end
end
forge
end
let(:install_dir) do
install_dir = mock("Puppet::ModuleTool::InstallDirectory")
install_dir.stubs(:prepare)
install_dir
end
let(:remote_dependency_info) do
{
"pmtacceptance/apache" => [
{ "dependencies" => [],
"version" => "1.0.0-alpha",
"file" => "/pmtacceptance-apache-1.0.0-alpha.tar.gz" },
{ "dependencies" => [],
"version" => "1.0.0-beta",
"file" => "/pmtacceptance-apache-1.0.0-beta.tar.gz" },
{ "dependencies" => [],
"version" => "1.0.0-rc1",
"file" => "/pmtacceptance-apache-1.0.0-rc1.tar.gz" },
],
"pmtacceptance/stdlib" => [
{ "dependencies" => [],
"version" => "0.0.1",
"file" => "/pmtacceptance-stdlib-0.0.1.tar.gz" },
{ "dependencies" => [],
"version" => "0.0.2",
"file" => "/pmtacceptance-stdlib-0.0.2.tar.gz" },
{ "dependencies" => [],
"version" => "1.0.0-pre",
"file" => "/pmtacceptance-stdlib-1.0.0-pre.tar.gz" },
{ "dependencies" => [],
"version" => "1.0.0",
"file" => "/pmtacceptance-stdlib-1.0.0.tar.gz" },
{ "dependencies" => [],
"version" => "1.5.0-pre",
"file" => "/pmtacceptance-stdlib-1.5.0-pre.tar.gz" },
],
"pmtacceptance/java" => [
{ "dependencies" => [["pmtacceptance/stdlib", ">= 0.0.1"]],
"version" => "1.7.0",
"file" => "/pmtacceptance-java-1.7.0.tar.gz" },
{ "dependencies" => [["pmtacceptance/stdlib", "1.0.0"]],
"version" => "1.7.1",
"file" => "/pmtacceptance-java-1.7.1.tar.gz" }
],
"pmtacceptance/apollo" => [
{ "dependencies" => [
["pmtacceptance/java", "1.7.1"],
["pmtacceptance/stdlib", "0.0.1"]
],
"version" => "0.0.1",
"file" => "/pmtacceptance-apollo-0.0.1.tar.gz" },
{ "dependencies" => [
["pmtacceptance/java", ">= 1.7.0"],
["pmtacceptance/stdlib", ">= 1.0.0"]
],
"version" => "0.0.2",
"file" => "/pmtacceptance-apollo-0.0.2.tar.gz" }
]
}
end
describe "the behavior of .is_module_package?" do
it "should return true when file is a module package" do
installer = installer_class.new("foo", forge, install_dir, options)
installer.send(:is_module_package?, stdlib_pkg).should be_true
end
it "should return false when file is not a module package" do
installer = installer_class.new("foo", forge, install_dir, options)
installer.send(:is_module_package?, "pmtacceptance-apollo-0.0.2.tar").
should be_false
end
end
context "when the source is a repository" do
it "should require a valid name" do
lambda { installer_class.run('puppet', install_dir, params) }.should
raise_error(ArgumentError, "Could not install module with invalid name: puppet")
end
it "should install the current stable version of the requested module" do
Puppet::ModuleTool::Applications::Unpacker.expects(:new).
with('/fake_cache/pmtacceptance-stdlib-1.0.0.tar.gz', options).
returns(unpacker)
results = installer_class.run('pmtacceptance-stdlib', forge, install_dir, options)
results[:installed_modules].length == 1
results[:installed_modules][0][:module].should == "pmtacceptance-stdlib"
results[:installed_modules][0][:version][:vstring].should == "1.0.0"
end
it "should install the most recent version of requested module in the absence of a stable version" do
Puppet::ModuleTool::Applications::Unpacker.expects(:new).
with('/fake_cache/pmtacceptance-apache-1.0.0-rc1.tar.gz', options).
returns(unpacker)
results = installer_class.run('pmtacceptance-apache', forge, install_dir, options)
results[:installed_modules].length == 1
results[:installed_modules][0][:module].should == "pmtacceptance-apache"
results[:installed_modules][0][:version][:vstring].should == "1.0.0-rc1"
end
it "should install the most recent stable version of requested module for the requested version range" do
Puppet::ModuleTool::Applications::Unpacker.expects(:new).
with('/fake_cache/pmtacceptance-stdlib-1.0.0.tar.gz', options.merge(:version => '1.x')).
returns(unpacker)
results = installer_class.run('pmtacceptance-stdlib', forge, install_dir, options.merge(:version => '1.x'))
results[:installed_modules].length == 1
results[:installed_modules][0][:module].should == "pmtacceptance-stdlib"
results[:installed_modules][0][:version][:vstring].should == "1.0.0"
end
it "should install the most recent version of requested module for the requested version range in the absence of a stable version" do
Puppet::ModuleTool::Applications::Unpacker.expects(:new).
with('/fake_cache/pmtacceptance-stdlib-1.5.0-pre.tar.gz', options.merge(:version => '1.5.0-pre')).
returns(unpacker)
results = installer_class.run('pmtacceptance-stdlib', forge, install_dir, options.merge(:version => '1.5.0-pre'))
results[:installed_modules].length == 1
results[:installed_modules][0][:module].should == "pmtacceptance-stdlib"
results[:installed_modules][0][:version][:vstring].should == "1.5.0-pre"
end
context "should check the target directory" do
let(:installer) do
installer_class.new('pmtacceptance-stdlib', forge, install_dir, options)
end
def expect_normal_unpacker
Puppet::ModuleTool::Applications::Unpacker.expects(:new).
with('/fake_cache/pmtacceptance-stdlib-1.0.0.tar.gz', options).
returns(unpacker)
end
def expect_normal_results
results
end
it "(#15202) prepares the install directory" do
expect_normal_unpacker
install_dir.expects(:prepare).with("pmtacceptance-stdlib", "latest")
results = installer.run
results[:installed_modules].length.should eq 1
results[:installed_modules][0][:module].should == "pmtacceptance-stdlib"
results[:installed_modules][0][:version][:vstring].should == "1.0.0"
end
it "(#15202) reports an error when the install directory cannot be prepared" do
install_dir.expects(:prepare).with("pmtacceptance-stdlib", "latest").
raises(Puppet::ModuleTool::Errors::PermissionDeniedCreateInstallDirectoryError.new("original", :module => "pmtacceptance-stdlib"))
results = installer.run
results[:result].should == :failure
results[:error][:oneline].should =~ /Permission is denied/
end
end
context "when the requested module has dependencies" do
it "should install dependencies" do
Puppet::ModuleTool::Applications::Unpacker.expects(:new).
with('/fake_cache/pmtacceptance-stdlib-1.0.0.tar.gz', options).
returns(unpacker)
Puppet::ModuleTool::Applications::Unpacker.expects(:new).
with('/fake_cache/pmtacceptance-apollo-0.0.2.tar.gz', options).
returns(unpacker)
Puppet::ModuleTool::Applications::Unpacker.expects(:new).
with('/fake_cache/pmtacceptance-java-1.7.1.tar.gz', options).
returns(unpacker)
results = installer_class.run('pmtacceptance-apollo', forge, install_dir, options)
installed_dependencies = results[:installed_modules][0][:dependencies]
dependencies = installed_dependencies.inject({}) do |result, dep|
result[dep[:module]] = dep[:version][:vstring]
result
end
dependencies.length.should == 2
dependencies['pmtacceptance-java'].should == '1.7.1'
dependencies['pmtacceptance-stdlib'].should == '1.0.0'
end
it "should install requested module if the '--force' flag is used" do
- options = { :force => true, :target_dir => modpath1 }
+ options.merge!(:force => true)
Puppet::ModuleTool::Applications::Unpacker.expects(:new).
with('/fake_cache/pmtacceptance-apollo-0.0.2.tar.gz', options).
returns(unpacker)
results = installer_class.run('pmtacceptance-apollo', forge, install_dir, options)
results[:installed_modules][0][:module].should == "pmtacceptance-apollo"
end
it "should not install dependencies if the '--force' flag is used" do
- options = { :force => true, :target_dir => modpath1 }
+ options.merge!(:force => true)
Puppet::ModuleTool::Applications::Unpacker.expects(:new).
with('/fake_cache/pmtacceptance-apollo-0.0.2.tar.gz', options).
returns(unpacker)
results = installer_class.run('pmtacceptance-apollo', forge, install_dir, options)
dependencies = results[:installed_modules][0][:dependencies]
dependencies.should == []
end
it "should not install dependencies if the '--ignore-dependencies' flag is used" do
- options = { :ignore_dependencies => true, :target_dir => modpath1 }
+ options.merge!(:ignore_dependencies => true)
Puppet::ModuleTool::Applications::Unpacker.expects(:new).
with('/fake_cache/pmtacceptance-apollo-0.0.2.tar.gz', options).
returns(unpacker)
results = installer_class.run('pmtacceptance-apollo', forge, install_dir, options)
dependencies = results[:installed_modules][0][:dependencies]
dependencies.should == []
end
it "should set an error if dependencies can't be resolved" do
- options = { :version => '0.0.1', :target_dir => modpath1 }
+ options.merge!(:version => '0.0.1')
oneline = "'pmtacceptance-apollo' (v0.0.1) requested; Invalid dependency cycle"
multiline = <<-MSG.strip
Could not install module 'pmtacceptance-apollo' (v0.0.1)
No version of 'pmtacceptance-stdlib' will satisfy dependencies
You specified 'pmtacceptance-apollo' (v0.0.1),
which depends on 'pmtacceptance-java' (v1.7.1),
which depends on 'pmtacceptance-stdlib' (v1.0.0)
Use `puppet module install --force` to install this module anyway
MSG
results = installer_class.run('pmtacceptance-apollo', forge, install_dir, options)
results[:result].should == :failure
results[:error][:oneline].should == oneline
results[:error][:multiline].should == multiline
end
+
+ it "resolves conflicts for each dependency only once" do
+ Puppet::Log.level = :debug
+
+ installed_modules = [
+ Puppet::Module.new('ntp', File.join(modpath1, 'ntp'), env.name),
+ Puppet::Module.new('mysql', File.join(modpath1, 'mysql'), env.name),
+ Puppet::Module.new('apache', File.join(modpath1, 'apache'), env.name)
+ ]
+
+ env.stubs(:modules_by_path).returns({modpath1 => installed_modules})
+
+ Puppet::ModuleTool::Applications::Unpacker.expects(:new).
+ with('/fake_cache/pmtacceptance-apollo-0.0.2.tar.gz', options).
+ returns(unpacker)
+ Puppet::ModuleTool::Applications::Unpacker.expects(:new).
+ with('/fake_cache/pmtacceptance-java-1.7.1.tar.gz', options).
+ returns(unpacker)
+ Puppet::ModuleTool::Applications::Unpacker.expects(:new).
+ with('/fake_cache/pmtacceptance-stdlib-1.0.0.tar.gz', options).
+ returns(unpacker)
+
+ installer_class.run('pmtacceptance-apollo', forge, install_dir, options)
+
+ modules = @logs.map do |log|
+ data = log.message.match(/Resolving conflicts for (.*)/)
+ data ? data[1] : nil
+ end.compact
+
+ expect(modules).to eq(["pmtacceptance-apollo", "pmtacceptance-java,pmtacceptance-stdlib"])
+ end
end
context "when there are modules installed" do
it "should use local version when already exists and satisfies constraints"
it "should reinstall the local version if force is used"
it "should upgrade local version when necessary to satisfy constraints"
it "should error when a local version can't be upgraded to satisfy constraints"
end
context "when a local module needs upgrading to satisfy constraints but has changes" do
it "should error"
it "should warn and continue if force is used"
end
it "should error when a local version of a dependency has no version metadata"
it "should error when a local version of a dependency has a non-semver version"
it "should error when a local version of a dependency has a different forge name"
it "should error when a local version of a dependency has no metadata"
end
context "when the source is a filesystem" do
before do
@sourcedir = tmpdir('sourcedir')
end
it "should error if it can't parse the name"
it "should try to get_release_package_from_filesystem if it has a valid name"
end
end
diff --git a/spec/unit/module_tool/applications/uninstaller_spec.rb b/spec/unit/module_tool/applications/uninstaller_spec.rb
index 9ec52fc4c..f7b1d9bba 100644
--- a/spec/unit/module_tool/applications/uninstaller_spec.rb
+++ b/spec/unit/module_tool/applications/uninstaller_spec.rb
@@ -1,206 +1,207 @@
require 'spec_helper'
require 'puppet/module_tool'
require 'tmpdir'
require 'puppet_spec/modules'
describe Puppet::ModuleTool::Applications::Uninstaller do
include PuppetSpec::Files
def mkmod(name, path, metadata=nil)
modpath = File.join(path, name)
FileUtils.mkdir_p(modpath)
if metadata
File.open(File.join(modpath, 'metadata.json'), 'w') do |f|
f.write(metadata.to_pson)
end
end
modpath
end
describe "the behavior of the instances" do
-
- before do
- @uninstaller = Puppet::ModuleTool::Applications::Uninstaller
- FileUtils.mkdir_p(modpath1)
- FileUtils.mkdir_p(modpath2)
- fake_env.modulepath = [modpath1, modpath2]
+ let(:working_dir) { tmpdir("uninstaller") }
+ let(:modpath1) { create_temp_dir("modpath1") }
+ let(:modpath2) { create_temp_dir("modpath2") }
+ let(:env) { Puppet::Node::Environment.create(:env, [modpath1, modpath2], '') }
+ let(:options) { { :environment_instance => env } }
+ let(:uninstaller) { Puppet::ModuleTool::Applications::Uninstaller.new("puppetlabs-foo", options) }
+
+ def create_temp_dir(name)
+ path = File.join(working_dir, name)
+ FileUtils.mkdir_p(path)
+ path
end
- let(:modpath1) { File.join(tmpdir("uninstaller"), "modpath1") }
- let(:modpath2) { File.join(tmpdir("uninstaller"), "modpath2") }
- let(:fake_env) { Puppet::Node::Environment.new('fake_env') }
- let(:options) { {:environment => "fake_env"} }
-
let(:foo_metadata) do
{
:author => "puppetlabs",
:name => "puppetlabs/foo",
:version => "1.0.0",
:source => "http://dummyurl/foo",
:license => "Apache2",
:dependencies => [],
}
end
let(:bar_metadata) do
{
:author => "puppetlabs",
:name => "puppetlabs/bar",
:version => "1.0.0",
:source => "http://dummyurl/bar",
:license => "Apache2",
:dependencies => [],
}
end
context "when the module is not installed" do
it "should fail" do
- @uninstaller.new('fakemod_not_installed', options).run[:result].should == :failure
+ Puppet::ModuleTool::Applications::Uninstaller.new('fakemod_not_installed', options).run[:result].should == :failure
end
end
context "when the module is installed" do
it "should uninstall the module" do
PuppetSpec::Modules.create('foo', modpath1, :metadata => foo_metadata)
- results = @uninstaller.new("puppetlabs-foo", options).run
+ results = uninstaller.run
+
results[:affected_modules].first.forge_name.should == "puppetlabs/foo"
end
it "should only uninstall the requested module" do
PuppetSpec::Modules.create('foo', modpath1, :metadata => foo_metadata)
PuppetSpec::Modules.create('bar', modpath1, :metadata => bar_metadata)
- results = @uninstaller.new("puppetlabs-foo", options).run
+ results = uninstaller.run
results[:affected_modules].length == 1
results[:affected_modules].first.forge_name.should == "puppetlabs/foo"
end
it "should uninstall fail if a module exists twice in the modpath" do
PuppetSpec::Modules.create('foo', modpath1, :metadata => foo_metadata)
PuppetSpec::Modules.create('foo', modpath2, :metadata => foo_metadata)
- @uninstaller.new('puppetlabs-foo', options).run[:result].should == :failure
+ uninstaller.run[:result].should == :failure
end
context "when options[:version] is specified" do
it "should uninstall the module if the version matches" do
PuppetSpec::Modules.create('foo', modpath1, :metadata => foo_metadata)
options[:version] = "1.0.0"
- results = @uninstaller.new("puppetlabs-foo", options).run
+ results = uninstaller.run
results[:affected_modules].length.should == 1
results[:affected_modules].first.forge_name.should == "puppetlabs/foo"
results[:affected_modules].first.version.should == "1.0.0"
end
it "should not uninstall the module if the version does not match" do
PuppetSpec::Modules.create('foo', modpath1, :metadata => foo_metadata)
options[:version] = "2.0.0"
- @uninstaller.new("puppetlabs-foo", options).run[:result].should == :failure
+ uninstaller.run[:result].should == :failure
end
end
context "when the module metadata is missing" do
it "should not uninstall the module" do
PuppetSpec::Modules.create('foo', modpath1)
- @uninstaller.new("puppetlabs-foo", options).run[:result].should == :failure
+ uninstaller.run[:result].should == :failure
end
end
context "when the module has local changes" do
it "should not uninstall the module" do
- PuppetSpec::Modules.create('foo', modpath1, :metadata => foo_metadata)
- Puppet::Module.any_instance.stubs(:has_local_changes?).returns(true)
+ mod = PuppetSpec::Modules.create('foo', modpath1, :metadata => foo_metadata)
+ Puppet::ModuleTool::Applications::Checksummer.expects(:run).with(mod.path).returns(['change'])
- @uninstaller.new("puppetlabs-foo", options).run[:result].should == :failure
+ uninstaller.run[:result].should == :failure
end
end
context "when the module does not have local changes" do
it "should uninstall the module" do
PuppetSpec::Modules.create('foo', modpath1, :metadata => foo_metadata)
- results = @uninstaller.new("puppetlabs-foo", options).run
+ results = uninstaller.run
results[:affected_modules].length.should == 1
results[:affected_modules].first.forge_name.should == "puppetlabs/foo"
end
end
context "when uninstalling the module will cause broken dependencies" do
it "should not uninstall the module" do
Puppet.settings[:modulepath] = modpath1
PuppetSpec::Modules.create('foo', modpath1, :metadata => foo_metadata)
PuppetSpec::Modules.create(
'needy',
modpath1,
:metadata => {
:author => 'beggar',
:dependencies => [{
"version_requirement" => ">= 1.0.0",
"name" => "puppetlabs/foo"
}]
}
)
- @uninstaller.new("puppetlabs-foo", options).run[:result].should == :failure
+ uninstaller.run[:result].should == :failure
end
end
context "when using the --force flag" do
let(:fakemod) do
stub(
:forge_name => 'puppetlabs/fakemod',
:version => '0.0.1',
:has_local_changes? => true
)
end
it "should ignore local changes" do
foo = mkmod("foo", modpath1, foo_metadata)
options[:force] = true
- results = @uninstaller.new("puppetlabs-foo", options).run
+ results = uninstaller.run
results[:affected_modules].length.should == 1
results[:affected_modules].first.forge_name.should == "puppetlabs/foo"
end
it "should ignore broken dependencies" do
Puppet.settings[:modulepath] = modpath1
PuppetSpec::Modules.create('foo', modpath1, :metadata => foo_metadata)
PuppetSpec::Modules.create(
'needy',
modpath1,
:metadata => {
:author => 'beggar',
:dependencies => [{
"version_requirement" => ">= 1.0.0",
"name" => "puppetlabs/foo"
}]
}
)
options[:force] = true
- results = @uninstaller.new("puppetlabs-foo", options).run
+ results = uninstaller.run
results[:affected_modules].length.should == 1
results[:affected_modules].first.forge_name.should == "puppetlabs/foo"
end
end
end
end
end
diff --git a/spec/unit/module_tool/tar_spec.rb b/spec/unit/module_tool/tar_spec.rb
index 3de0dda48..75034e5bd 100644
--- a/spec/unit/module_tool/tar_spec.rb
+++ b/spec/unit/module_tool/tar_spec.rb
@@ -1,45 +1,45 @@
require 'spec_helper'
-require 'puppet/module_tool/tar'
+require 'puppet/module_tool'
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/module_tool_spec.rb b/spec/unit/module_tool_spec.rb
index 93db99afe..63bd5e295 100755
--- a/spec/unit/module_tool_spec.rb
+++ b/spec/unit/module_tool_spec.rb
@@ -1,277 +1,204 @@
#! /usr/bin/env ruby
# encoding: UTF-8
require 'spec_helper'
require 'puppet/module_tool'
describe Puppet::ModuleTool do
describe '.is_module_root?' do
it 'should return true if directory has a module file' do
FileTest.expects(:file?).with(responds_with(:to_s, '/a/b/c/Modulefile')).
returns(true)
subject.is_module_root?(Pathname.new('/a/b/c')).should be_true
end
it 'should return false if directory does not have a module file' do
FileTest.expects(:file?).with(responds_with(:to_s, '/a/b/c/Modulefile')).
returns(false)
subject.is_module_root?(Pathname.new('/a/b/c')).should be_false
end
end
describe '.find_module_root' do
let(:sample_path) { Pathname.new('/a/b/c').expand_path }
it 'should return the first path as a pathname when it contains a module file' do
Puppet::ModuleTool.expects(:is_module_root?).with(sample_path).
returns(true)
subject.find_module_root(sample_path).should == sample_path
end
it 'should return a parent path as a pathname when it contains a module file' do
Puppet::ModuleTool.expects(:is_module_root?).
with(responds_with(:to_s, File.expand_path('/a/b/c'))).returns(false)
Puppet::ModuleTool.expects(:is_module_root?).
with(responds_with(:to_s, File.expand_path('/a/b'))).returns(true)
subject.find_module_root(sample_path).should == Pathname.new('/a/b').expand_path
end
it 'should return nil when no module root can be found' do
Puppet::ModuleTool.expects(:is_module_root?).at_least_once.returns(false)
subject.find_module_root(sample_path).should be_nil
end
end
describe '.format_tree' do
it 'should return an empty tree when given an empty list' do
subject.format_tree([]).should == ''
end
it 'should return a shallow when given a list without dependencies' do
list = [ { :text => 'first' }, { :text => 'second' }, { :text => 'third' } ]
subject.format_tree(list).should == <<-TREE
├── first
├── second
└── third
TREE
end
it 'should return a deeply nested tree when given a list with deep dependencies' do
list = [
{
:text => 'first',
:dependencies => [
{
:text => 'second',
:dependencies => [
{ :text => 'third' }
]
}
]
},
]
subject.format_tree(list).should == <<-TREE
└─┬ first
└─┬ second
└── third
TREE
end
it 'should show connectors when deep dependencies are not on the last node of the top level' do
list = [
{
:text => 'first',
:dependencies => [
{
:text => 'second',
:dependencies => [
{ :text => 'third' }
]
}
]
},
{ :text => 'fourth' }
]
subject.format_tree(list).should == <<-TREE
├─┬ first
│ └─┬ second
│ └── third
└── fourth
TREE
end
it 'should show connectors when deep dependencies are not on the last node of any level' do
list = [
{
:text => 'first',
:dependencies => [
{
:text => 'second',
:dependencies => [
{ :text => 'third' }
]
},
{ :text => 'fourth' }
]
}
]
subject.format_tree(list).should == <<-TREE
└─┬ first
├─┬ second
│ └── third
└── fourth
TREE
end
it 'should show connectors in every case when deep dependencies are not on the last node' do
list = [
{
:text => 'first',
:dependencies => [
{
:text => 'second',
:dependencies => [
{ :text => 'third' }
]
},
{ :text => 'fourth' }
]
},
{ :text => 'fifth' }
]
subject.format_tree(list).should == <<-TREE
├─┬ first
│ ├─┬ second
│ │ └── third
│ └── fourth
└── fifth
TREE
end
end
describe '.set_option_defaults' do
- describe 'option :environment' do
- context 'passed:' do
- let (:environment) { "ahgkduerh" }
- let (:options) { {:environment => environment} }
-
- it 'Puppet[:environment] should be set to the value of the option' do
- subject.set_option_defaults options
-
- Puppet[:environment].should == environment
- end
-
- it 'the option value should not be overridden' do
- Puppet[:environment] = :foo
- subject.set_option_defaults options
-
- options[:environment].should == environment
- end
- end
-
- context 'NOT passed:' do
- it 'Puppet[:environment] should NOT be overridden' do
- Puppet[:environment] = :foo
-
- subject.set_option_defaults({})
- Puppet[:environment].should == :foo
- end
-
- it 'the option should be set to the value of Puppet[:environment]' do
- options_to_modify = Hash.new
- Puppet[:environment] = :abcdefg
-
- subject.set_option_defaults options_to_modify
-
- options_to_modify[:environment].should == :abcdefg
- end
+ let(:options) { {} }
+ let(:modulepath) { [File.expand_path('/env/module/path'), File.expand_path('/global/module/path')] }
+ let(:environment) { Puppet::Node::Environment.create('current', modulepath, '') }
+ around(:each) do |example|
+ Puppet.override(:current_environment => environment) do
+ example.run
end
end
- describe 'option :modulepath' do
- context 'passed:' do
- let (:modulepath) { PuppetSpec::Files.make_absolute('/bar') }
- let (:options) { {:modulepath => modulepath} }
-
- it 'Puppet[:modulepath] should be set to the value of the option' do
-
- subject.set_option_defaults options
-
- Puppet[:modulepath].should == modulepath
- end
-
- it 'the option value should not be overridden' do
- Puppet[:modulepath] = "/foo"
-
- subject.set_option_defaults options
-
- options[:modulepath].should == modulepath
- end
- end
-
- context 'NOT passed:' do
- let (:options) { {:environment => :pmttestenv} }
-
- before(:each) do
- Puppet[:modulepath] = "/no"
- Puppet[:environment] = :pmttestenv
- Puppet.settings.set_value(:modulepath,
- ["/foo", "/bar", "/no"].join(File::PATH_SEPARATOR),
- :pmttestenv)
- end
-
- it 'Puppet[:modulepath] should be reset to the module path of the current environment' do
- subject.set_option_defaults options
-
- Puppet[:modulepath].should == Puppet.settings.value(:modulepath, :pmttestenv)
- end
-
- it 'the option should be set to the module path of the current environment' do
- subject.set_option_defaults options
-
- options[:modulepath].should == Puppet.settings.value(:modulepath, :pmttestenv)
- end
+ describe 'option :environment_instance' do
+ it 'adds an environment_instance to the options hash' do
+ subject.set_option_defaults(options)
+ expect(options[:environment_instance].name).to eq(environment.name)
+ expect(options[:environment_instance].modulepath).to eq(environment.modulepath)
end
end
describe 'option :target_dir' do
let (:target_dir) { 'boo' }
context 'passed:' do
let (:options) { {:target_dir => target_dir} }
- it 'the option value should be prepended to the Puppet[:modulepath]' do
- Puppet[:modulepath] = "/fuz"
- original_modulepath = Puppet[:modulepath]
-
+ it 'prepends the target_dir into the environment_instance modulepath' do
subject.set_option_defaults options
- Puppet[:modulepath].should == options[:target_dir] + File::PATH_SEPARATOR + original_modulepath
+ expect(options[:environment_instance].full_modulepath).
+ to eq([File.expand_path(target_dir)] + modulepath)
end
- it 'the option value should be turned into an absolute path' do
+ it 'expands the target dir' do
subject.set_option_defaults options
options[:target_dir].should == File.expand_path(target_dir)
end
end
- describe 'NOT passed:' do
- before :each do
- Puppet[:modulepath] = 'foo' + File::PATH_SEPARATOR + 'bar'
- end
-
+ context 'NOT passed:' do
it 'the option should be set to the first component of Puppet[:modulepath]' do
options = Hash.new
subject.set_option_defaults options
- options[:target_dir].should == Puppet[:modulepath].split(File::PATH_SEPARATOR)[0]
+ expect(options[:target_dir]).to eq(environment.full_modulepath.first)
end
end
end
end
end
diff --git a/spec/unit/network/authconfig_spec.rb b/spec/unit/network/authconfig_spec.rb
index be45152c0..12ff1c0d3 100755
--- a/spec/unit/network/authconfig_spec.rb
+++ b/spec/unit/network/authconfig_spec.rb
@@ -1,110 +1,109 @@
#! /usr/bin/env ruby
require 'spec_helper'
require 'puppet/network/authconfig'
describe Puppet::Network::AuthConfig do
before :each do
- stub_file = stub('file', :stat => stub('stat', :ctime => :now))
- Puppet::FileSystem::File.stubs(:new).returns stub_file
+ Puppet::FileSystem.stubs(:stat).returns stub('stat', :ctime => :now)
Time.stubs(:now).returns Time.now
Puppet::Network::AuthConfig.any_instance.stubs(:exists?).returns(true)
# FIXME @authconfig = Puppet::Network::AuthConfig.new("dummy")
end
describe "when initializing" do
it "inserts default ACLs after setting initial rights" do
Puppet::Network::AuthConfig.any_instance.expects(:insert_default_acl)
Puppet::Network::AuthConfig.new
end
end
describe "when defining an acl with mk_acl" do
before :each do
Puppet::Network::AuthConfig.any_instance.stubs(:insert_default_acl)
@authconfig = Puppet::Network::AuthConfig.new
end
it "should create a new right for each default acl" do
@authconfig.mk_acl(:acl => '/')
@authconfig.rights['/'].should be
end
it "allows everyone for each default right" do
@authconfig.mk_acl(:acl => '/')
@authconfig.rights['/'].should be_globalallow
end
it "accepts an argument to restrict the method" do
@authconfig.mk_acl(:acl => '/', :method => :find)
@authconfig.rights['/'].methods.should == [:find]
end
it "creates rights with authentication set to true by default" do
@authconfig.mk_acl(:acl => '/')
@authconfig.rights['/'].authentication.should be_true
end
it "accepts an argument to set the authentication requirement" do
@authconfig.mk_acl(:acl => '/', :authenticated => :any)
@authconfig.rights['/'].authentication.should be_false
end
end
describe "when adding default ACLs" do
before :each do
Puppet::Network::AuthConfig.any_instance.stubs(:insert_default_acl)
@authconfig = Puppet::Network::AuthConfig.new
Puppet::Network::AuthConfig.any_instance.unstub(:insert_default_acl)
end
Puppet::Network::AuthConfig::DEFAULT_ACL.each do |acl|
it "should create a default right for #{acl[:acl]}" do
@authconfig.stubs(:mk_acl)
@authconfig.expects(:mk_acl).with(acl)
@authconfig.insert_default_acl
end
end
it "should log at info loglevel" do
Puppet.expects(:info).at_least_once
@authconfig.insert_default_acl
end
it "creates an empty catch-all rule for '/' for any authentication request state" do
@authconfig.stubs(:mk_acl)
@authconfig.insert_default_acl
@authconfig.rights['/'].should be_empty
@authconfig.rights['/'].authentication.should be_false
end
it '(CVE-2013-2275) allows report submission only for the node matching the certname by default' do
acl = {
:acl => "~ ^\/report\/([^\/]+)$",
:method => :save,
:allow => '$1',
:authenticated => true
}
@authconfig.stubs(:mk_acl)
@authconfig.expects(:mk_acl).with(acl)
@authconfig.insert_default_acl
end
end
describe "when checking authorization" do
it "should ask for authorization to the ACL subsystem" do
params = {
:ip => "127.0.0.1",
:node => "me",
:environment => :env,
:authenticated => true
}
- Puppet::Network::Rights.any_instance.expects(:is_request_forbidden_and_why?).with("path", :save, "to/resource", params)
+ Puppet::Network::Rights.any_instance.expects(:is_request_forbidden_and_why?).with(:save, "/path/to/resource", params)
- described_class.new.check_authorization("path", :save, "to/resource", params)
+ described_class.new.check_authorization(:save, "/path/to/resource", params)
end
end
end
diff --git a/spec/unit/network/authentication_spec.rb b/spec/unit/network/authentication_spec.rb
index c18552ab8..8f3653cad 100755
--- a/spec/unit/network/authentication_spec.rb
+++ b/spec/unit/network/authentication_spec.rb
@@ -1,86 +1,100 @@
#! /usr/bin/env ruby
require 'spec_helper'
load 'puppet/network/authentication.rb'
class AuthenticationTest
include Puppet::Network::Authentication
end
describe Puppet::Network::Authentication do
subject { AuthenticationTest.new }
let(:now) { Time.now }
let(:cert) { Puppet::SSL::Certificate.new('cert') }
let(:host) { stub 'host', :certificate => cert }
# this is necessary since the logger is a class variable, and it needs to be stubbed
def reload_module
load 'puppet/network/authentication.rb'
end
describe "when warning about upcoming expirations" do
before do
Puppet::SSL::CertificateAuthority.stubs(:ca?).returns(false)
- Puppet::FileSystem::File.stubs(:exist?).returns(false)
+ Puppet::FileSystem.stubs(:exist?).returns(false)
end
it "should check the expiration of the CA certificate" do
ca = stub 'ca', :host => host
Puppet::SSL::CertificateAuthority.stubs(:ca?).returns(true)
Puppet::SSL::CertificateAuthority.stubs(:instance).returns(ca)
cert.expects(:near_expiration?).returns(false)
subject.warn_if_near_expiration
end
- it "should check the expiration of the localhost certificate" do
- Puppet::SSL::Host.stubs(:localhost).returns(host)
- cert.expects(:near_expiration?).returns(false)
- Puppet::FileSystem::File.stubs(:exist?).with(Puppet[:hostcert]).returns(true)
- subject.warn_if_near_expiration
+ context "when examining the local host" do
+ before do
+ Puppet::SSL::Host.stubs(:localhost).returns(host)
+ Puppet::FileSystem.stubs(:exist?).with(Puppet[:hostcert]).returns(true)
+ end
+
+ it "should not load the localhost certificate if the local CA certificate is missing" do
+ # Redmine-21869: Infinite recursion occurs if CA cert is missing.
+ Puppet::FileSystem.stubs(:exist?).with(Puppet[:localcacert]).returns(false)
+ host.unstub(:certificate)
+ host.expects(:certificate).never
+ subject.warn_if_near_expiration
+ end
+
+ it "should check the expiration of the localhost certificate if the local CA certificate is present" do
+ Puppet::FileSystem.stubs(:exist?).with(Puppet[:localcacert]).returns(true)
+ cert.expects(:near_expiration?).returns(false)
+ subject.warn_if_near_expiration
+ end
end
it "should check the expiration of any certificates passed in as arguments" do
cert.expects(:near_expiration?).twice.returns(false)
subject.warn_if_near_expiration(cert, cert)
end
it "should accept instances of OpenSSL::X509::Certificate" do
raw_cert = stub 'cert'
raw_cert.stubs(:is_a?).with(OpenSSL::X509::Certificate).returns(true)
Puppet::SSL::Certificate.stubs(:from_instance).with(raw_cert).returns(cert)
cert.expects(:near_expiration?).returns(false)
subject.warn_if_near_expiration(raw_cert)
end
it "should use a rate-limited logger for expiration warnings that uses `runinterval` as its interval" do
Puppet::Util::Log::RateLimitedLogger.expects(:new).with(Puppet[:runinterval])
reload_module
end
context "in the logs" do
let(:logger) { stub 'logger' }
before do
Puppet::Util::Log::RateLimitedLogger.stubs(:new).returns(logger)
reload_module
cert.stubs(:near_expiration?).returns(true)
cert.stubs(:expiration).returns(now)
cert.stubs(:unmunged_name).returns('foo')
end
it "should log a warning if a certificate's expiration is near" do
logger.expects(:warning)
subject.warn_if_near_expiration(cert)
end
it "should use the certificate's unmunged name in the message" do
logger.expects(:warning).with { |message| message.include? 'foo' }
subject.warn_if_near_expiration(cert)
end
it "should show certificate's expiration date in the message using ISO 8601 format" do
logger.expects(:warning).with { |message| message.include? now.strftime('%Y-%m-%dT%H:%M:%S%Z') }
subject.warn_if_near_expiration(cert)
end
end
end
end
diff --git a/spec/unit/network/authorization_spec.rb b/spec/unit/network/authorization_spec.rb
index 397f55dcc..10b9d1def 100644
--- a/spec/unit/network/authorization_spec.rb
+++ b/spec/unit/network/authorization_spec.rb
@@ -1,24 +1,34 @@
#! /usr/bin/env ruby
require 'spec_helper'
require 'puppet/network/authorization'
describe Puppet::Network::Authorization do
class AuthTest
include Puppet::Network::Authorization
end
subject { AuthTest.new }
describe "when creating an authconfig object" do
- it "creates default ACL entries if no file has been read" do
+ before :each do
# Other tests may have created an authconfig, so we have to undo that.
+ @orig_auth_config = Puppet::Network::AuthConfigLoader.instance_variable_get(:@auth_config)
+ @orig_auth_config_file = Puppet::Network::AuthConfigLoader.instance_variable_get(:@auth_config_file)
+
Puppet::Network::AuthConfigLoader.instance_variable_set(:@auth_config, nil)
Puppet::Network::AuthConfigLoader.instance_variable_set(:@auth_config_file, nil)
+ end
+ after :each do
+ Puppet::Network::AuthConfigLoader.instance_variable_set(:@auth_config, @orig_auth_config)
+ Puppet::Network::AuthConfigLoader.instance_variable_set(:@auth_config_file, @orig_auth_config_file)
+ end
+
+ it "creates default ACL entries if no file has been read" do
Puppet::Network::AuthConfigParser.expects(:new_from_file).raises Errno::ENOENT
Puppet::Network::AuthConfig.any_instance.expects(:insert_default_acl)
subject.authconfig
end
end
end
diff --git a/spec/unit/network/formats_spec.rb b/spec/unit/network/formats_spec.rb
index 57ff77795..99535ff1e 100755
--- a/spec/unit/network/formats_spec.rb
+++ b/spec/unit/network/formats_spec.rb
@@ -1,399 +1,422 @@
#! /usr/bin/env ruby
require 'spec_helper'
require 'puppet/network/formats'
class PsonTest
attr_accessor :string
def ==(other)
string == other.string
end
- def self.from_pson(data)
+ def self.from_data_hash(data)
new(data)
end
def initialize(string)
@string = string
end
def to_pson(*args)
{
'type' => self.class.name,
'data' => @string
}.to_pson(*args)
end
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
+ it "should fail when one element does not have a from_data_hash" do
expect do
@msgpack.intern_multiple(Hash, MessagePack.pack(["foo"]))
end.to raise_error(NoMethodError)
end
+
+ it "should be able to serialize a catalog" do
+ cat = Puppet::Resource::Catalog.new('foo')
+ cat.add_resource(Puppet::Resource.new(:file, 'my_file'))
+ catunpack = MessagePack.unpack(cat.to_msgpack)
+ catunpack.should include(
+ "tags"=>[],
+ "name"=>"foo",
+ "version"=>nil,
+ "environment"=>"",
+ "edges"=>[],
+ "classes"=>[]
+ )
+ catunpack["resources"][0].should include(
+ "type"=>"File",
+ "title"=>"my_file",
+ "exported"=>false
+ )
+ catunpack["resources"][0]["tags"].should include(
+ "file",
+ "my_file"
+ )
+ end
end
it "should include a yaml format" do
Puppet::Network::FormatHandler.format(:yaml).should_not be_nil
end
describe "yaml" do
before do
@yaml = Puppet::Network::FormatHandler.format(:yaml)
end
it "should have its mime type set to text/yaml" do
@yaml.mime.should == "text/yaml"
end
it "should be supported on Strings" do
@yaml.should be_supported(String)
end
it "should render by calling 'to_yaml' on the instance" do
instance = mock 'instance'
instance.expects(:to_yaml).returns "foo"
@yaml.render(instance).should == "foo"
end
it "should render multiple instances by calling 'to_yaml' on the array" do
instances = [mock('instance')]
instances.expects(:to_yaml).returns "foo"
@yaml.render_multiple(instances).should == "foo"
end
it "should deserialize YAML" do
@yaml.intern(String, YAML.dump("foo")).should == "foo"
end
it "should deserialize symbols as strings" do
@yaml.intern(String, YAML.dump(:foo)).should == "foo"
end
it "should load from yaml when deserializing an array" do
text = YAML.dump(["foo"])
@yaml.intern_multiple(String, text).should == ["foo"]
end
it "fails intelligibly instead of calling to_pson with something other than a hash" do
expect do
@yaml.intern(Puppet::Node, '')
end.to raise_error(Puppet::Network::FormatHandler::FormatError, /did not contain a valid instance/)
end
it "fails intelligibly when intern_multiple is called and yaml doesn't decode to an array" do
expect do
@yaml.intern_multiple(Puppet::Node, '')
end.to raise_error(Puppet::Network::FormatHandler::FormatError, /did not contain a collection/)
end
it "fails intelligibly instead of calling to_pson with something other than a hash when interning multiple" do
expect do
@yaml.intern_multiple(Puppet::Node, YAML.dump(["hello"]))
end.to raise_error(Puppet::Network::FormatHandler::FormatError, /did not contain a valid instance/)
end
end
describe "base64 compressed yaml", :if => Puppet.features.zlib? do
before do
@yaml = Puppet::Network::FormatHandler.format(:b64_zlib_yaml)
end
it "should have its mime type set to text/b64_zlib_yaml" do
@yaml.mime.should == "text/b64_zlib_yaml"
end
it "should render by calling 'to_yaml' on the instance" do
instance = mock 'instance'
instance.expects(:to_yaml).returns "foo"
@yaml.render(instance)
end
it "should encode generated yaml on render" do
instance = mock 'instance', :to_yaml => "foo"
@yaml.expects(:encode).with("foo").returns "bar"
@yaml.render(instance).should == "bar"
end
it "should render multiple instances by calling 'to_yaml' on the array" do
instances = [mock('instance')]
instances.expects(:to_yaml).returns "foo"
@yaml.render_multiple(instances)
end
it "should encode generated yaml on render" do
instances = [mock('instance')]
instances.stubs(:to_yaml).returns "foo"
@yaml.expects(:encode).with("foo").returns "bar"
@yaml.render(instances).should == "bar"
end
it "should round trip data" do
@yaml.intern(String, @yaml.encode("foo")).should == "foo"
end
it "should round trip multiple data elements" do
data = @yaml.render_multiple(["foo", "bar"])
@yaml.intern_multiple(String, data).should == ["foo", "bar"]
end
it "should intern by base64 decoding, uncompressing and safely Yaml loading" do
input = Base64.encode64(Zlib::Deflate.deflate(YAML.dump("data in")))
@yaml.intern(String, input).should == "data in"
end
it "should render by compressing and base64 encoding" do
output = @yaml.render("foo")
YAML.load(Zlib::Inflate.inflate(Base64.decode64(output))).should == "foo"
end
describe "when zlib is disabled" do
before do
Puppet[:zlib] = false
end
it "use_zlib? should return false" do
@yaml.use_zlib?.should == false
end
it "should refuse to encode" do
expect { @yaml.render("foo") }.to raise_error(Puppet::Error, /zlib library is not installed/)
end
it "should refuse to decode" do
expect { @yaml.intern(String, "foo") }.to raise_error(Puppet::Error, /zlib library is not installed/)
end
end
describe "when zlib is not installed" do
it "use_zlib? should return false" do
Puppet[:zlib] = true
Puppet.features.expects(:zlib?).returns(false)
@yaml.use_zlib?.should == false
end
end
end
describe "plaintext" do
before do
@text = Puppet::Network::FormatHandler.format(:s)
end
it "should have its mimetype set to text/plain" do
@text.mime.should == "text/plain"
end
it "should use 'txt' as its extension" do
@text.extension.should == "txt"
end
end
describe "dot" do
before do
@dot = Puppet::Network::FormatHandler.format(:dot)
end
it "should have its mimetype set to text/dot" do
@dot.mime.should == "text/dot"
end
end
describe Puppet::Network::FormatHandler.format(:raw) do
before do
@format = Puppet::Network::FormatHandler.format(:raw)
end
it "should exist" do
@format.should_not be_nil
end
it "should have its mimetype set to application/x-raw" do
@format.mime.should == "application/x-raw"
end
it "should always be supported" do
@format.should be_supported(String)
end
it "should fail if its multiple_render method is used" do
lambda { @format.render_multiple("foo") }.should raise_error(NotImplementedError)
end
it "should fail if its multiple_intern method is used" do
lambda { @format.intern_multiple(String, "foo") }.should raise_error(NotImplementedError)
end
it "should have a weight of 1" do
@format.weight.should == 1
end
end
it "should include a pson format" do
Puppet::Network::FormatHandler.format(:pson).should_not be_nil
end
describe "pson" do
before do
@pson = Puppet::Network::FormatHandler.format(:pson)
end
it "should have its mime type set to text/pson" do
Puppet::Network::FormatHandler.format(:pson).mime.should == "text/pson"
end
it "should require the :render_method" do
Puppet::Network::FormatHandler.format(:pson).required_methods.should be_include(:render_method)
end
it "should require the :intern_method" do
Puppet::Network::FormatHandler.format(:pson).required_methods.should be_include(:intern_method)
end
it "should have a weight of 10" do
@pson.weight.should == 10
end
describe "when supported" do
it "should render by calling 'to_pson' on the instance" do
instance = PsonTest.new("foo")
instance.expects(:to_pson).returns "foo"
@pson.render(instance).should == "foo"
end
it "should render multiple instances by calling 'to_pson' on the array" do
instances = [mock('instance')]
instances.expects(:to_pson).returns "foo"
@pson.render_multiple(instances).should == "foo"
end
- it "should intern by calling 'PSON.parse' on the text and then using from_pson to convert the data into an instance" do
+ it "should intern by calling 'PSON.parse' on the text and then using from_data_hash to convert the data into an instance" do
text = "foo"
PSON.expects(:parse).with("foo").returns("type" => "PsonTest", "data" => "foo")
- PsonTest.expects(:from_pson).with("foo").returns "parsed_pson"
+ PsonTest.expects(:from_data_hash).with("foo").returns "parsed_pson"
@pson.intern(PsonTest, text).should == "parsed_pson"
end
it "should not render twice if 'PSON.parse' creates the appropriate instance" do
text = "foo"
instance = PsonTest.new("foo")
PSON.expects(:parse).with("foo").returns(instance)
- PsonTest.expects(:from_pson).never
+ PsonTest.expects(:from_data_hash).never
@pson.intern(PsonTest, text).should equal(instance)
end
- it "should intern by calling 'PSON.parse' on the text and then using from_pson to convert the actual into an instance if the pson has no class/data separation" do
+ it "should intern by calling 'PSON.parse' on the text and then using from_data_hash to convert the actual into an instance if the pson has no class/data separation" do
text = "foo"
PSON.expects(:parse).with("foo").returns("foo")
- PsonTest.expects(:from_pson).with("foo").returns "parsed_pson"
+ PsonTest.expects(:from_data_hash).with("foo").returns "parsed_pson"
@pson.intern(PsonTest, text).should == "parsed_pson"
end
it "should intern multiples by parsing the text and using 'class.intern' on each resulting data structure" do
text = "foo"
PSON.expects(:parse).with("foo").returns ["bar", "baz"]
- PsonTest.expects(:from_pson).with("bar").returns "BAR"
- PsonTest.expects(:from_pson).with("baz").returns "BAZ"
+ PsonTest.expects(:from_data_hash).with("bar").returns "BAR"
+ PsonTest.expects(:from_data_hash).with("baz").returns "BAZ"
@pson.intern_multiple(PsonTest, text).should == %w{BAR BAZ}
end
it "fails intelligibly when given invalid data" do
expect do
@pson.intern(Puppet::Node, '')
end.to raise_error(PSON::ParserError, /source did not contain any PSON/)
end
end
end
describe ":console format" do
subject { Puppet::Network::FormatHandler.format(:console) }
it { should be_an_instance_of Puppet::Network::Format }
let :json do Puppet::Network::FormatHandler.format(:pson) end
[:intern, :intern_multiple].each do |method|
it "should not implement #{method}" do
expect { subject.send(method, String, 'blah') }.to raise_error NotImplementedError
end
end
["hello", 1, 1.0].each do |input|
it "should just return a #{input.inspect}" do
subject.render(input).should == input
end
end
[[1, 2], ["one"], [{ 1 => 1 }]].each do |input|
it "should render #{input.inspect} as one item per line" do
subject.render(input).should == input.collect { |item| item.to_s + "\n" }.join('')
end
end
it "should render empty hashes as empty strings" do
subject.render({}).should == ''
end
it "should render a non-trivially-keyed Hash as JSON" do
hash = { [1,2] => 3, [2,3] => 5, [3,4] => 7 }
subject.render(hash).should == json.render(hash).chomp
end
it "should render a {String,Numeric}-keyed Hash into a table" do
object = Object.new
hash = { "one" => 1, "two" => [], "three" => {}, "four" => object,
5 => 5, 6.0 => 6 }
# Gotta love ASCII-betical sort order. Hope your objects are better
# structured for display than my test one is. --daniel 2011-04-18
subject.render(hash).should == <<EOT
5 5
6.0 6
four #{json.render(object).chomp}
one 1
three {}
two []
EOT
end
it "should render a hash nicely with a multi-line value" do
pending "Moving to PSON rather than PP makes this unsupportable."
hash = {
"number" => { "1" => '1' * 40, "2" => '2' * 40, '3' => '3' * 40 },
"text" => { "a" => 'a' * 40, 'b' => 'b' * 40, 'c' => 'c' * 40 }
}
subject.render(hash).should == <<EOT
number {"1"=>"1111111111111111111111111111111111111111",
"2"=>"2222222222222222222222222222222222222222",
"3"=>"3333333333333333333333333333333333333333"}
text {"a"=>"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
"b"=>"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
"c"=>"cccccccccccccccccccccccccccccccccccccccc"}
EOT
end
end
end
diff --git a/spec/unit/network/http/api/v1_spec.rb b/spec/unit/network/http/api/v1_spec.rb
index c5b39a082..a80838150 100755
--- a/spec/unit/network/http/api/v1_spec.rb
+++ b/spec/unit/network/http/api/v1_spec.rb
@@ -1,216 +1,519 @@
#! /usr/bin/env ruby
require 'spec_helper'
+require 'puppet/network/http'
require 'puppet/network/http/api/v1'
-
-class V1RestApiTester
- include Puppet::Network::HTTP::API::V1
-end
+require 'puppet/indirector_testing'
describe Puppet::Network::HTTP::API::V1 do
- before do
- @tester = V1RestApiTester.new
+ let(:not_found_code) { Puppet::Network::HTTP::Error::HTTPNotFoundError::CODE }
+ let(:not_acceptable_code) { Puppet::Network::HTTP::Error::HTTPNotAcceptableError::CODE }
+ let(:not_authorized_code) { Puppet::Network::HTTP::Error::HTTPNotAuthorizedError::CODE }
+
+ let(:indirection) { Puppet::IndirectorTesting.indirection }
+ let(:handler) { Puppet::Network::HTTP::API::V1.new }
+ let(:response) { Puppet::Network::HTTP::MemoryResponse.new }
+
+ def a_request_that_heads(data, request = {})
+ Puppet::Network::HTTP::Request.from_hash({
+ :headers => {
+ 'accept' => request[:accept_header],
+ 'content-type' => "text/yaml", },
+ :method => "HEAD",
+ :path => "/production/#{indirection.name}/#{data.value}",
+ :params => {},
+ })
+ end
+
+ def a_request_that_submits(data, request = {})
+ Puppet::Network::HTTP::Request.from_hash({
+ :headers => {
+ 'accept' => request[:accept_header],
+ 'content-type' => request[:content_type_header] || "text/yaml", },
+ :method => "PUT",
+ :path => "/production/#{indirection.name}/#{data.value}",
+ :params => {},
+ :body => request[:body] || data.render("text/yaml")
+ })
+ end
+
+ def a_request_that_destroys(data, request = {})
+ Puppet::Network::HTTP::Request.from_hash({
+ :headers => {
+ 'accept' => request[:accept_header],
+ 'content-type' => "text/yaml", },
+ :method => "DELETE",
+ :path => "/production/#{indirection.name}/#{data.value}",
+ :params => {},
+ :body => ''
+ })
end
- it "should be able to convert a URI into a request" do
- @tester.should respond_to(:uri2indirection)
+ def a_request_that_finds(data, request = {})
+ Puppet::Network::HTTP::Request.from_hash({
+ :headers => {
+ 'accept' => request[:accept_header],
+ 'content-type' => "text/yaml", },
+ :method => "GET",
+ :path => "/production/#{indirection.name}/#{data.value}",
+ :params => {},
+ :body => ''
+ })
end
- it "should be able to convert a request into a URI" do
- @tester.should respond_to(:indirection2uri)
+ def a_request_that_searches(key, request = {})
+ Puppet::Network::HTTP::Request.from_hash({
+ :headers => {
+ 'accept' => request[:accept_header],
+ 'content-type' => "text/yaml", },
+ :method => "GET",
+ :path => "/production/#{indirection.name}s/#{key}",
+ :params => {},
+ :body => ''
+ })
+ end
+
+
+ before do
+ Puppet::IndirectorTesting.indirection.terminus_class = :memory
+ Puppet::IndirectorTesting.indirection.terminus.clear
+ handler.stubs(:check_authorization)
+ handler.stubs(:warn_if_near_expiration)
end
describe "when converting a URI into a request" do
before do
- @tester.stubs(:handler).returns "foo"
+ handler.stubs(:handler).returns "foo"
end
it "should require the http method, the URI, and the query parameters" do
# Not a terribly useful test, but an important statement for the spec
- lambda { @tester.uri2indirection("/foo") }.should raise_error(ArgumentError)
+ lambda { handler.uri2indirection("/foo") }.should raise_error(ArgumentError)
end
it "should use the first field of the URI as the environment" do
- @tester.uri2indirection("GET", "/env/foo/bar", {})[3][:environment].to_s.should == "env"
+ handler.uri2indirection("GET", "/env/foo/bar", {})[3][:environment].to_s.should == "env"
end
it "should fail if the environment is not alphanumeric" do
- lambda { @tester.uri2indirection("GET", "/env ness/foo/bar", {}) }.should raise_error(ArgumentError)
+ lambda { handler.uri2indirection("GET", "/env ness/foo/bar", {}) }.should raise_error(ArgumentError)
end
it "should use the environment from the URI even if one is specified in the parameters" do
- @tester.uri2indirection("GET", "/env/foo/bar", {:environment => "otherenv"})[3][:environment].to_s.should == "env"
+ handler.uri2indirection("GET", "/env/foo/bar", {:environment => "otherenv"})[3][:environment].to_s.should == "env"
end
it "should not pass a buck_path parameter through (See Bugs #13553, #13518, #13511)" do
- @tester.uri2indirection("GET", "/env/foo/bar", { :bucket_path => "/malicious/path" })[3].should_not include({ :bucket_path => "/malicious/path" })
+ handler.uri2indirection("GET", "/env/foo/bar", { :bucket_path => "/malicious/path" })[3].should_not include({ :bucket_path => "/malicious/path" })
end
it "should pass allowed parameters through" do
- @tester.uri2indirection("GET", "/env/foo/bar", { :allowed_param => "value" })[3].should include({ :allowed_param => "value" })
+ handler.uri2indirection("GET", "/env/foo/bar", { :allowed_param => "value" })[3].should include({ :allowed_param => "value" })
end
it "should return the environment as a Puppet::Node::Environment" do
- @tester.uri2indirection("GET", "/env/foo/bar", {})[3][:environment].should be_a Puppet::Node::Environment
+ handler.uri2indirection("GET", "/env/foo/bar", {})[3][:environment].should be_a Puppet::Node::Environment
end
it "should not pass a buck_path parameter through (See Bugs #13553, #13518, #13511)" do
- @tester.uri2indirection("GET", "/env/foo/bar", { :bucket_path => "/malicious/path" })[3].should_not include({ :bucket_path => "/malicious/path" })
+ handler.uri2indirection("GET", "/env/foo/bar", { :bucket_path => "/malicious/path" })[3].should_not include({ :bucket_path => "/malicious/path" })
end
it "should pass allowed parameters through" do
- @tester.uri2indirection("GET", "/env/foo/bar", { :allowed_param => "value" })[3].should include({ :allowed_param => "value" })
+ handler.uri2indirection("GET", "/env/foo/bar", { :allowed_param => "value" })[3].should include({ :allowed_param => "value" })
end
it "should use the second field of the URI as the indirection name" do
- @tester.uri2indirection("GET", "/env/foo/bar", {})[0].should == "foo"
+ handler.uri2indirection("GET", "/env/foo/bar", {})[0].should == "foo"
end
it "should fail if the indirection name is not alphanumeric" do
- lambda { @tester.uri2indirection("GET", "/env/foo ness/bar", {}) }.should raise_error(ArgumentError)
+ lambda { handler.uri2indirection("GET", "/env/foo ness/bar", {}) }.should raise_error(ArgumentError)
end
it "should use the remainder of the URI as the indirection key" do
- @tester.uri2indirection("GET", "/env/foo/bar", {})[2].should == "bar"
+ handler.uri2indirection("GET", "/env/foo/bar", {})[2].should == "bar"
end
it "should support the indirection key being a /-separated file path" do
- @tester.uri2indirection("GET", "/env/foo/bee/baz/bomb", {})[2].should == "bee/baz/bomb"
+ handler.uri2indirection("GET", "/env/foo/bee/baz/bomb", {})[2].should == "bee/baz/bomb"
end
it "should fail if no indirection key is specified" do
- lambda { @tester.uri2indirection("GET", "/env/foo/", {}) }.should raise_error(ArgumentError)
- lambda { @tester.uri2indirection("GET", "/env/foo", {}) }.should raise_error(ArgumentError)
+ lambda { handler.uri2indirection("GET", "/env/foo/", {}) }.should raise_error(ArgumentError)
+ lambda { handler.uri2indirection("GET", "/env/foo", {}) }.should raise_error(ArgumentError)
end
it "should choose 'find' as the indirection method if the http method is a GET and the indirection name is singular" do
- @tester.uri2indirection("GET", "/env/foo/bar", {})[1].should == :find
+ handler.uri2indirection("GET", "/env/foo/bar", {})[1].should == :find
end
it "should choose 'find' as the indirection method if the http method is a POST and the indirection name is singular" do
- @tester.uri2indirection("POST", "/env/foo/bar", {})[1].should == :find
+ handler.uri2indirection("POST", "/env/foo/bar", {})[1].should == :find
end
it "should choose 'head' as the indirection method if the http method is a HEAD and the indirection name is singular" do
- @tester.uri2indirection("HEAD", "/env/foo/bar", {})[1].should == :head
+ handler.uri2indirection("HEAD", "/env/foo/bar", {})[1].should == :head
end
it "should choose 'search' as the indirection method if the http method is a GET and the indirection name is plural" do
- @tester.uri2indirection("GET", "/env/foos/bar", {})[1].should == :search
+ handler.uri2indirection("GET", "/env/foos/bar", {})[1].should == :search
end
it "should choose 'find' as the indirection method if the http method is a GET and the indirection name is facts" do
- @tester.uri2indirection("GET", "/env/facts/bar", {})[1].should == :find
+ handler.uri2indirection("GET", "/env/facts/bar", {})[1].should == :find
end
it "should choose 'save' as the indirection method if the http method is a PUT and the indirection name is facts" do
- @tester.uri2indirection("PUT", "/env/facts/bar", {})[1].should == :save
+ handler.uri2indirection("PUT", "/env/facts/bar", {})[1].should == :save
end
it "should choose 'search' as the indirection method if the http method is a GET and the indirection name is inventory" do
- @tester.uri2indirection("GET", "/env/inventory/search", {})[1].should == :search
+ handler.uri2indirection("GET", "/env/inventory/search", {})[1].should == :search
end
it "should choose 'find' as the indirection method if the http method is a GET and the indirection name is facts" do
- @tester.uri2indirection("GET", "/env/facts/bar", {})[1].should == :find
+ handler.uri2indirection("GET", "/env/facts/bar", {})[1].should == :find
end
it "should choose 'save' as the indirection method if the http method is a PUT and the indirection name is facts" do
- @tester.uri2indirection("PUT", "/env/facts/bar", {})[1].should == :save
+ handler.uri2indirection("PUT", "/env/facts/bar", {})[1].should == :save
end
it "should choose 'search' as the indirection method if the http method is a GET and the indirection name is inventory" do
- @tester.uri2indirection("GET", "/env/inventory/search", {})[1].should == :search
+ handler.uri2indirection("GET", "/env/inventory/search", {})[1].should == :search
end
it "should choose 'search' as the indirection method if the http method is a GET and the indirection name is facts_search" do
- @tester.uri2indirection("GET", "/env/facts_search/bar", {})[1].should == :search
+ handler.uri2indirection("GET", "/env/facts_search/bar", {})[1].should == :search
end
it "should change indirection name to 'facts' if the http method is a GET and the indirection name is facts_search" do
- @tester.uri2indirection("GET", "/env/facts_search/bar", {})[0].should == 'facts'
+ handler.uri2indirection("GET", "/env/facts_search/bar", {})[0].should == 'facts'
end
it "should not change indirection name from 'facts' if the http method is a GET and the indirection name is facts" do
- @tester.uri2indirection("GET", "/env/facts/bar", {})[0].should == 'facts'
+ handler.uri2indirection("GET", "/env/facts/bar", {})[0].should == 'facts'
end
it "should change indirection name to 'status' if the http method is a GET and the indirection name is statuses" do
- @tester.uri2indirection("GET", "/env/statuses/bar", {})[0].should == 'status'
+ handler.uri2indirection("GET", "/env/statuses/bar", {})[0].should == 'status'
end
it "should change indirection name to 'probe' if the http method is a GET and the indirection name is probes" do
- @tester.uri2indirection("GET", "/env/probes/bar", {})[0].should == 'probe'
+ handler.uri2indirection("GET", "/env/probes/bar", {})[0].should == 'probe'
end
it "should choose 'delete' as the indirection method if the http method is a DELETE and the indirection name is singular" do
- @tester.uri2indirection("DELETE", "/env/foo/bar", {})[1].should == :destroy
+ handler.uri2indirection("DELETE", "/env/foo/bar", {})[1].should == :destroy
end
it "should choose 'save' as the indirection method if the http method is a PUT and the indirection name is singular" do
- @tester.uri2indirection("PUT", "/env/foo/bar", {})[1].should == :save
+ handler.uri2indirection("PUT", "/env/foo/bar", {})[1].should == :save
end
it "should fail if an indirection method cannot be picked" do
- lambda { @tester.uri2indirection("UPDATE", "/env/foo/bar", {}) }.should raise_error(ArgumentError)
+ lambda { handler.uri2indirection("UPDATE", "/env/foo/bar", {}) }.should raise_error(ArgumentError)
end
it "should URI unescape the indirection key" do
escaped = URI.escape("foo bar")
- indirection_name, method, key, params = @tester.uri2indirection("GET", "/env/foo/#{escaped}", {})
+ indirection_name, method, key, params = handler.uri2indirection("GET", "/env/foo/#{escaped}", {})
key.should == "foo bar"
end
end
describe "when converting a request into a URI" do
- before do
- @request = Puppet::Indirector::Request.new(:foo, :find, "with spaces", nil, :foo => :bar, :environment => "myenv")
- end
+ let(:request) { Puppet::Indirector::Request.new(:foo, :find, "with spaces", nil, :foo => :bar, :environment => "myenv") }
it "should use the environment as the first field of the URI" do
- @tester.indirection2uri(@request).split("/")[1].should == "myenv"
+ handler.class.indirection2uri(request).split("/")[1].should == "myenv"
end
it "should use the indirection as the second field of the URI" do
- @tester.indirection2uri(@request).split("/")[2].should == "foo"
+ handler.class.indirection2uri(request).split("/")[2].should == "foo"
end
it "should pluralize the indirection name if the method is 'search'" do
- @request.stubs(:method).returns :search
- @tester.indirection2uri(@request).split("/")[2].should == "foos"
+ request.stubs(:method).returns :search
+ handler.class.indirection2uri(request).split("/")[2].should == "foos"
end
it "should use the escaped key as the remainder of the URI" do
escaped = URI.escape("with spaces")
- @tester.indirection2uri(@request).split("/")[3].sub(/\?.+/, '').should == escaped
+ handler.class.indirection2uri(request).split("/")[3].sub(/\?.+/, '').should == escaped
end
it "should add the query string to the URI" do
- @request.expects(:query_string).returns "?query"
- @tester.indirection2uri(@request).should =~ /\?query$/
+ request.expects(:query_string).returns "?query"
+ handler.class.indirection2uri(request).should =~ /\?query$/
end
end
describe "when converting a request into a URI with body" do
- before :each do
- @request = Puppet::Indirector::Request.new(:foo, :find, "with spaces", nil, :foo => :bar, :environment => "myenv")
- end
+ let(:request) { Puppet::Indirector::Request.new(:foo, :find, "with spaces", nil, :foo => :bar, :environment => "myenv") }
it "should use the environment as the first field of the URI" do
- @tester.request_to_uri_and_body(@request).first.split("/")[1].should == "myenv"
+ handler.class.request_to_uri_and_body(request).first.split("/")[1].should == "myenv"
end
it "should use the indirection as the second field of the URI" do
- @tester.request_to_uri_and_body(@request).first.split("/")[2].should == "foo"
+ handler.class.request_to_uri_and_body(request).first.split("/")[2].should == "foo"
end
it "should use the escaped key as the remainder of the URI" do
escaped = URI.escape("with spaces")
- @tester.request_to_uri_and_body(@request).first.split("/")[3].sub(/\?.+/, '').should == escaped
+ handler.class.request_to_uri_and_body(request).first.split("/")[3].sub(/\?.+/, '').should == escaped
end
it "should return the URI and body separately" do
- @tester.request_to_uri_and_body(@request).should == ["/myenv/foo/with%20spaces", "foo=bar"]
+ handler.class.request_to_uri_and_body(request).should == ["/myenv/foo/with%20spaces", "foo=bar"]
+ end
+ end
+
+ describe "when processing a request" do
+ it "should return not_authorized_code if the request is not authorized" do
+ request = a_request_that_heads(Puppet::IndirectorTesting.new("my data"))
+
+ handler.expects(:check_authorization).raises(Puppet::Network::AuthorizationError.new("forbidden"))
+
+ handler.call(request, response)
+
+ expect(response.code).to eq(not_authorized_code)
+ end
+
+ it "should return an error code if the indirection does not support remote requests" do
+ request = a_request_that_heads(Puppet::IndirectorTesting.new("my data"))
+
+ indirection.expects(:allow_remote_requests?).returns(false)
+
+ handler.call(request, response)
+
+ expect(response.code).to eq(not_found_code)
+ end
+
+ it "should serialize a controller exception when an exception is thrown while finding the model instance" do
+ request = a_request_that_finds(Puppet::IndirectorTesting.new("key"))
+ handler.expects(:do_find).raises(ArgumentError, "The exception")
+
+ handler.call(request, response)
+
+ expect(response.code).to eq(400)
+ expect(response.body).to eq("The exception")
+ expect(response.type).to eq("text/plain")
+ end
+ end
+
+ describe "when finding a model instance" do
+ it "uses the first supported format for the response" do
+ data = Puppet::IndirectorTesting.new("my data")
+ indirection.save(data, "my data")
+ request = a_request_that_finds(data, :accept_header => "unknown, pson, yaml")
+
+ handler.call(request, response)
+
+ expect(response.body).to eq(data.render(:pson))
+ expect(response.type).to eq(Puppet::Network::FormatHandler.format(:pson))
+ end
+
+ it "responds with a not_acceptable_code error when no accept header is provided" do
+ data = Puppet::IndirectorTesting.new("my data")
+ indirection.save(data, "my data")
+ request = a_request_that_finds(data, :accept_header => nil)
+
+ handler.call(request, response)
+
+ expect(response.code).to eq(not_acceptable_code)
+ end
+
+ it "raises an error when no accepted formats are known" do
+ data = Puppet::IndirectorTesting.new("my data")
+ indirection.save(data, "my data")
+ request = a_request_that_finds(data, :accept_header => "unknown, also/unknown")
+
+ handler.call(request, response)
+
+ expect(response.code).to eq(not_acceptable_code)
+ end
+
+ it "should pass the result through without rendering it if the result is a string" do
+ data = Puppet::IndirectorTesting.new("my data")
+ data_string = "my data string"
+ request = a_request_that_finds(data, :accept_header => "pson")
+ indirection.expects(:find).returns(data_string)
+
+ handler.call(request, response)
+
+ expect(response.body).to eq(data_string)
+ expect(response.type).to eq(Puppet::Network::FormatHandler.format(:pson))
+ end
+
+ it "should return a not_found_code when no model instance can be found" do
+ data = Puppet::IndirectorTesting.new("my data")
+ request = a_request_that_finds(data, :accept_header => "unknown, pson, yaml")
+
+ handler.call(request, response)
+ expect(response.code).to eq(not_found_code)
+ end
+ end
+
+ describe "when searching for model instances" do
+ it "uses the first supported format for the response" do
+ data = Puppet::IndirectorTesting.new("my data")
+ indirection.save(data, "my data")
+ request = a_request_that_searches("my", :accept_header => "unknown, pson, yaml")
+
+ handler.call(request, response)
+
+ expect(response.type).to eq(Puppet::Network::FormatHandler.format(:pson))
+ expect(response.body).to eq(Puppet::IndirectorTesting.render_multiple(:pson, [data]))
+ end
+
+ it "should return [] when searching returns an empty array" do
+ request = a_request_that_searches("nothing", :accept_header => "unknown, pson, yaml")
+
+ handler.call(request, response)
+
+ expect(response.body).to eq("[]")
+ expect(response.type).to eq(Puppet::Network::FormatHandler.format(:pson))
+ end
+
+ it "should return a not_found_code when searching returns nil" do
+ request = a_request_that_searches("nothing", :accept_header => "unknown, pson, yaml")
+ indirection.expects(:search).returns(nil)
+
+ handler.call(request, response)
+
+ expect(response.code).to eq(not_found_code)
+ end
+ end
+
+ describe "when destroying a model instance" do
+ it "destroys the data indicated in the request" do
+ data = Puppet::IndirectorTesting.new("my data")
+ indirection.save(data, "my data")
+ request = a_request_that_destroys(data)
+
+ handler.call(request, response)
+
+ Puppet::IndirectorTesting.indirection.find("my data").should be_nil
+ end
+
+ it "responds with yaml when no Accept header is given" do
+ data = Puppet::IndirectorTesting.new("my data")
+ indirection.save(data, "my data")
+ request = a_request_that_destroys(data, :accept_header => nil)
+
+ handler.call(request, response)
+
+ expect(response.body).to eq(data.render(:yaml))
+ expect(response.type).to eq(Puppet::Network::FormatHandler.format(:yaml))
+ end
+
+ it "uses the first supported format for the response" do
+ data = Puppet::IndirectorTesting.new("my data")
+ indirection.save(data, "my data")
+ request = a_request_that_destroys(data, :accept_header => "unknown, pson, yaml")
+
+ handler.call(request, response)
+
+ expect(response.body).to eq(data.render(:pson))
+ expect(response.type).to eq(Puppet::Network::FormatHandler.format(:pson))
+ end
+
+ it "raises an error and does not destroy when no accepted formats are known" do
+ data = Puppet::IndirectorTesting.new("my data")
+ indirection.save(data, "my data")
+ request = a_request_that_submits(data, :accept_header => "unknown, also/unknown")
+
+ handler.call(request, response)
+
+ expect(response.code).to eq(not_acceptable_code)
+ Puppet::IndirectorTesting.indirection.find("my data").should_not be_nil
+ end
+ end
+
+ describe "when saving a model instance" do
+ it "allows an empty body when the format supports it" do
+ class Puppet::IndirectorTesting::Nonvalidatingmemory < Puppet::IndirectorTesting::Memory
+ def validate_key(_)
+ # nothing
+ end
+ end
+
+ indirection.terminus_class = :nonvalidatingmemory
+
+ data = Puppet::IndirectorTesting.new("test")
+ request = a_request_that_submits(data,
+ :content_type_header => "application/x-raw",
+ :body => '')
+
+ handler.call(request, response)
+
+ Puppet::IndirectorTesting.indirection.find("test").name.should == ''
+ end
+
+ it "saves the data sent in the request" do
+ data = Puppet::IndirectorTesting.new("my data")
+ request = a_request_that_submits(data)
+
+ handler.call(request, response)
+
+ saved = Puppet::IndirectorTesting.indirection.find("my data")
+ expect(saved.name).to eq(data.name)
+ end
+
+ it "responds with yaml when no Accept header is given" do
+ data = Puppet::IndirectorTesting.new("my data")
+ request = a_request_that_submits(data, :accept_header => nil)
+
+ handler.call(request, response)
+
+ expect(response.body).to eq(data.render(:yaml))
+ expect(response.type).to eq(Puppet::Network::FormatHandler.format(:yaml))
+ end
+
+ it "uses the first supported format for the response" do
+ data = Puppet::IndirectorTesting.new("my data")
+ request = a_request_that_submits(data, :accept_header => "unknown, pson, yaml")
+
+ handler.call(request, response)
+
+ expect(response.body).to eq(data.render(:pson))
+ expect(response.type).to eq(Puppet::Network::FormatHandler.format(:pson))
+ end
+
+ it "raises an error and does not save when no accepted formats are known" do
+ data = Puppet::IndirectorTesting.new("my data")
+ request = a_request_that_submits(data, :accept_header => "unknown, also/unknown")
+
+ handler.call(request, response)
+
+ expect(Puppet::IndirectorTesting.indirection.find("my data")).to be_nil
+ expect(response.code).to eq(not_acceptable_code)
+ end
+ end
+
+ describe "when performing head operation" do
+ it "should not generate a response when a model head call succeeds" do
+ data = Puppet::IndirectorTesting.new("my data")
+ indirection.save(data, "my data")
+ request = a_request_that_heads(data)
+
+ handler.call(request, response)
+
+ expect(response.code).to eq(nil)
+ end
+
+ it "should return a not_found_code when the model head call returns false" do
+ data = Puppet::IndirectorTesting.new("my data")
+ request = a_request_that_heads(data)
+
+ handler.call(request, response)
+
+ expect(response.code).to eq(not_found_code)
+ expect(response.type).to eq("text/plain")
+ expect(response.body).to eq("Not Found: Could not find indirector_testing my data")
end
end
end
diff --git a/spec/unit/network/http/api/v2/authorization_spec.rb b/spec/unit/network/http/api/v2/authorization_spec.rb
new file mode 100644
index 000000000..ecdb76192
--- /dev/null
+++ b/spec/unit/network/http/api/v2/authorization_spec.rb
@@ -0,0 +1,57 @@
+require 'spec_helper'
+
+require 'puppet/network/http'
+
+describe Puppet::Network::HTTP::API::V2::Authorization do
+ HTTP = Puppet::Network::HTTP
+
+ let(:response) { HTTP::MemoryResponse.new }
+ let(:authz) { HTTP::API::V2::Authorization.new }
+
+ it "only authorizes GET requests" do
+ request = HTTP::Request.from_hash({
+ :method => "POST"
+ })
+
+ expect do
+ authz.call(request, response)
+ end.to raise_error(HTTP::Error::HTTPNotAuthorizedError)
+ end
+
+ it "accepts v2 api requests that match allowed authconfig entries" do
+ request = HTTP::Request.from_hash({
+ :path => "/v2.0/environments",
+ :method => "GET",
+ :params => { :authenticated => true, :node => "testing", :ip => "127.0.0.1" }
+ })
+
+ authz.stubs(:authconfig).returns(Puppet::Network::AuthConfigParser.new(<<-AUTH).parse)
+path /v2.0/environments
+method find
+allow *
+ AUTH
+
+ expect do
+ authz.call(request, response)
+ end.to_not raise_error
+ end
+
+ it "rejects v2 api requests that are disallowed by authconfig entries" do
+ request = HTTP::Request.from_hash({
+ :path => "/v2.0/environments",
+ :method => "GET",
+ :params => { :node => "testing", :ip => "127.0.0.1" }
+ })
+
+ authz.stubs(:authconfig).returns(Puppet::Network::AuthConfigParser.new(<<-AUTH).parse)
+path /v2.0/environments
+method find
+auth any
+deny testing
+ AUTH
+
+ expect do
+ authz.call(request, response)
+ end.to raise_error(HTTP::Error::HTTPNotAuthorizedError, /Forbidden request/)
+ end
+end
diff --git a/spec/unit/network/http/api/v2/environments_spec.rb b/spec/unit/network/http/api/v2/environments_spec.rb
new file mode 100644
index 000000000..6e97f905d
--- /dev/null
+++ b/spec/unit/network/http/api/v2/environments_spec.rb
@@ -0,0 +1,42 @@
+require 'spec_helper'
+
+require 'puppet/node/environment'
+require 'puppet/network/http'
+require 'matchers/json'
+
+describe Puppet::Network::HTTP::API::V2::Environments do
+ include JSONMatchers
+
+ it "responds with all of the available environments" do
+ environment = Puppet::Node::Environment.create(:production, ["/first", "/second"], '/manifests')
+ loader = Puppet::Environments::Static.new(environment)
+ handler = Puppet::Network::HTTP::API::V2::Environments.new(loader)
+ response = Puppet::Network::HTTP::MemoryResponse.new
+
+ handler.call(Puppet::Network::HTTP::Request.from_hash(:headers => { 'accept' => 'application/json' }), response)
+
+ expect(response.code).to eq(200)
+ expect(response.type).to eq("application/json")
+ expect(JSON.parse(response.body)).to eq({
+ "search_paths" => loader.search_paths,
+ "environments" => {
+ "production" => {
+ "settings" => {
+ "modulepath" => [File.expand_path("/first"), File.expand_path("/second")],
+ "manifest" => File.expand_path("/manifests")
+ }
+ }
+ }
+ })
+ end
+
+ it "the response conforms to the environments schema" do
+ environment = Puppet::Node::Environment.create(:production, [], '')
+ handler = Puppet::Network::HTTP::API::V2::Environments.new(Puppet::Environments::Static.new(environment))
+ response = Puppet::Network::HTTP::MemoryResponse.new
+
+ handler.call(Puppet::Network::HTTP::Request.from_hash(:headers => { 'accept' => 'application/json' }), response)
+
+ expect(response.body).to validate_against('api/schemas/environments.json')
+ end
+end
diff --git a/spec/unit/network/http/api/v2_spec.rb b/spec/unit/network/http/api/v2_spec.rb
new file mode 100644
index 000000000..30cbaaac0
--- /dev/null
+++ b/spec/unit/network/http/api/v2_spec.rb
@@ -0,0 +1,14 @@
+require 'spec_helper'
+
+require 'puppet/network/http'
+
+describe Puppet::Network::HTTP::API::V2 do
+ it "responds to unknown paths with a 404" do
+ response = Puppet::Network::HTTP::MemoryResponse.new
+ request = Puppet::Network::HTTP::Request.from_hash(:path => "/v2.0/unknown")
+
+ expect do
+ Puppet::Network::HTTP::API::V2.routes.process(request, response)
+ end.to raise_error(Puppet::Network::HTTP::Error::HTTPNotFoundError)
+ end
+end
diff --git a/spec/unit/network/http/connection_spec.rb b/spec/unit/network/http/connection_spec.rb
index 467705f01..a5e6f64ae 100644
--- a/spec/unit/network/http/connection_spec.rb
+++ b/spec/unit/network/http/connection_spec.rb
@@ -1,237 +1,271 @@
#! /usr/bin/env ruby
require 'spec_helper'
require 'puppet/network/http/connection'
require 'puppet/network/authentication'
describe Puppet::Network::HTTP::Connection do
let (:host) { "me" }
let (:port) { 54321 }
subject { Puppet::Network::HTTP::Connection.new(host, port, :verify => Puppet::SSL::Validator.no_validator) }
+ let (:httpok) { Net::HTTPOK.new('1.1', 200, '') }
context "when providing HTTP connections" do
after do
Puppet::Network::HTTP::Connection.instance_variable_set("@ssl_host", nil)
end
context "when initializing http instances" do
before :each do
# All of the cert stuff is tested elsewhere
Puppet::Network::HTTP::Connection.stubs(:cert_setup)
end
it "should return an http instance created with the passed host and port" do
http = subject.send(:connection)
http.should be_an_instance_of Net::HTTP
http.address.should == host
http.port.should == port
end
it "should enable ssl on the http instance by default" do
http = subject.send(:connection)
http.should be_use_ssl
end
it "can set ssl using an option" do
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, :verify => Puppet::SSL::Validator.no_validator).send(:connection) }
before :each do
Puppet[:http_proxy_host] = "myhost"
Puppet[:http_proxy_port] = 432
Puppet[:configtimeout] = 120
end
its(:open_timeout) { should == Puppet[:configtimeout] }
its(:read_timeout) { should == Puppet[:configtimeout] }
its(:proxy_address) { should == Puppet[:http_proxy_host] }
its(:proxy_port) { should == Puppet[:http_proxy_port] }
end
it "should not set a proxy if the value is 'none'" do
Puppet[:http_proxy_host] = 'none'
subject.send(:connection).proxy_address.should be_nil
end
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
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, :verify => Puppet::SSL::Validator.no_validator) }
- let (:httpok) { Net::HTTPOK.new('1.1', 200, '') }
before :each do
httpok.stubs(:body).returns ""
# This stubbing relies a bit more on knowledge of the internals of Net::HTTP
# than I would prefer, but it works on ruby 1.8.7 and 1.9.3, and it seems
# valuable enough to have tests for blocks that this is probably warranted.
socket = stub_everything("socket")
TCPSocket.stubs(:open).returns(socket)
Net::HTTP::Post.any_instance.stubs(:exec).returns("")
Net::HTTP::Head.any_instance.stubs(:exec).returns("")
Net::HTTP::Get.any_instance.stubs(:exec).returns("")
Net::HTTPResponse.stubs(:read_new).returns(httpok)
end
[:request_get, :request_head, :request_post].each do |method|
context "##{method}" do
it "should yield to the block" do
block_executed = false
subject.send(method, "/foo", {}) do |response|
block_executed = true
end
block_executed.should == true
end
end
end
end
context "when validating HTTPS requests" do
include PuppetSpec::Files
let (:host) { "my_server" }
let (:port) { 8140 }
- let (:httpok) { Net::HTTPOK.new('1.1', 200, '') }
it "should provide a useful error message when one is available and certificate validation fails", :unless => Puppet.features.microsoft_windows? do
connection = Puppet::Network::HTTP::Connection.new(
host, port,
:verify => ConstantErrorValidator.new(:fails_with => 'certificate verify failed',
:error_string => 'shady looking signature'))
expect do
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
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 = Puppet::Network::HTTP::Connection.new(
host, port,
:verify => ConstantErrorValidator.new(:fails_with => 'some other message'))
expect do
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
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))
- Net::HTTP.any_instance.stubs(:get).returns(httpok)
+ Net::HTTP.any_instance.stubs(:request).returns(httpok)
connection.expects(:warn_if_near_expiration).with(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
def setup_connection(connection)
- connection.stubs(:get).with do
+ connection.stubs(:request).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
context "when response is a redirect" 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, :verify => Puppet::SSL::Validator.no_validator) }
let (:httpredirection) { Net::HTTPFound.new('1.1', 302, 'Moved Temporarily') }
- let (:httpok) { Net::HTTPOK.new('1.1', 200, '') }
before :each do
httpredirection['location'] = "http://#{other_host}:#{other_port}/#{other_path}"
httpredirection.stubs(:read_body).returns("This resource has moved")
socket = stub_everything("socket")
TCPSocket.stubs(:open).returns(socket)
Net::HTTP::Get.any_instance.stubs(:exec).returns("")
Net::HTTP::Post.any_instance.stubs(:exec).returns("")
end
it "should redirect to the final resource location" do
httpok.stubs(:read_body).returns(:body)
Net::HTTPResponse.stubs(:read_new).returns(httpredirection).then.returns(httpok)
subject.get("/foo").body.should == :body
subject.port.should == other_port
subject.address.should == other_host
end
it "should raise an error after too many redirections" do
Net::HTTPResponse.stubs(:read_new).returns(httpredirection)
expect {
subject.get("/foo")
}.to raise_error(Puppet::Network::HTTP::RedirectionLimitExceededException)
end
end
+
+ it "allows setting basic auth on get requests" do
+ expect_request_with_basic_auth
+
+ subject.get('/path', nil, :basic_auth => { :user => 'user', :password => 'password' })
+ end
+
+ it "allows setting basic auth on post requests" do
+ expect_request_with_basic_auth
+
+ subject.post('/path', 'data', nil, :basic_auth => { :user => 'user', :password => 'password' })
+ end
+
+ it "allows setting basic auth on head requests" do
+ expect_request_with_basic_auth
+
+ subject.head('/path', nil, :basic_auth => { :user => 'user', :password => 'password' })
+ end
+
+ it "allows setting basic auth on delete requests" do
+ expect_request_with_basic_auth
+
+ subject.delete('/path', nil, :basic_auth => { :user => 'user', :password => 'password' })
+ end
+
+ it "allows setting basic auth on put requests" do
+ expect_request_with_basic_auth
+
+ subject.put('/path', 'data', nil, :basic_auth => { :user => 'user', :password => 'password' })
+ end
+
+ def expect_request_with_basic_auth
+ Net::HTTP.any_instance.expects(:request).with do |request|
+ expect(request['authorization']).to match(/^Basic/)
+ end.returns(httpok)
+ end
end
diff --git a/spec/unit/network/http/error_spec.rb b/spec/unit/network/http/error_spec.rb
new file mode 100644
index 000000000..0df295038
--- /dev/null
+++ b/spec/unit/network/http/error_spec.rb
@@ -0,0 +1,30 @@
+require 'spec_helper'
+require 'matchers/json'
+
+require 'puppet/network/http'
+
+describe Puppet::Network::HTTP::Error do
+ include JSONMatchers
+
+ describe Puppet::Network::HTTP::Error::HTTPError do
+ it "should serialize to JSON that matches the error schema" do
+ error = Puppet::Network::HTTP::Error::HTTPError.new("I don't like the looks of you", 400, :SHIFTY_USER)
+
+ expect(error.to_json).to validate_against('api/schemas/error.json')
+ end
+ end
+
+ describe Puppet::Network::HTTP::Error::HTTPServerError do
+ it "should serialize to JSON that matches the error schema and has the optional stacktrace property" do
+ begin
+ raise Exception, "a wild Exception appeared!"
+ rescue Exception => e
+ culpable = e
+ end
+ error = Puppet::Network::HTTP::Error::HTTPServerError.new(culpable)
+
+ expect(error.to_json).to validate_against('api/schemas/error.json')
+ end
+ end
+
+end
diff --git a/spec/unit/network/http/handler_spec.rb b/spec/unit/network/http/handler_spec.rb
index 53da5be01..345818b4a 100755
--- a/spec/unit/network/http/handler_spec.rb
+++ b/spec/unit/network/http/handler_spec.rb
@@ -1,572 +1,222 @@
#! /usr/bin/env ruby
require 'spec_helper'
-require 'puppet/network/http'
-require 'puppet/network/http/handler'
+require 'puppet/indirector_testing'
+
require 'puppet/network/authorization'
require 'puppet/network/authentication'
-require 'puppet/indirector/memory'
+
+require 'puppet/network/http'
describe Puppet::Network::HTTP::Handler do
before :each do
- class Puppet::TestModel
- extend Puppet::Indirector
- indirects :test_model
- attr_accessor :name, :data
- def initialize(name = "name", data = '')
- @name = name
- @data = data
- end
-
- def self.from_raw(raw)
- new(nil, raw)
- end
-
- def self.from_pson(pson)
- new(pson["name"], pson["data"])
- end
-
- def to_pson
- {
- "name" => @name,
- "data" => @data
- }.to_pson
- end
-
- def ==(other)
- other.is_a? Puppet::TestModel and other.name == name and other.data == data
- end
- end
-
- class Puppet::TestModel::Memory < Puppet::Indirector::Memory
- def supports_remote_requests?
- true
- end
- end
-
- Puppet::TestModel.indirection.terminus_class = :memory
- end
-
- after :each do
- Puppet::TestModel.indirection.delete
- # Remove the class, unlinking it from the rest of the system.
- Puppet.send(:remove_const, :TestModel)
+ Puppet::IndirectorTesting.indirection.terminus_class = :memory
end
- let(:terminus_class) { Puppet::TestModel::Memory }
- let(:terminus) { Puppet::TestModel.indirection.terminus(:memory) }
- let(:indirection) { Puppet::TestModel.indirection }
- let(:model) { Puppet::TestModel }
+ let(:indirection) { Puppet::IndirectorTesting.indirection }
- def a_request
+ def a_request(method = "HEAD", path = "/production/#{indirection.name}/unknown")
{
:accept_header => "pson",
:content_type_header => "text/yaml",
- :http_method => "HEAD",
- :path => "/production/#{indirection.name}/unknown",
+ :http_method => method,
+ :path => path,
:params => {},
:client_cert => nil,
:headers => {},
:body => nil
}
end
- def a_request_that_heads(data, request = {})
- {
- :accept_header => request[:accept_header],
- :content_type_header => "text/yaml",
- :http_method => "HEAD",
- :path => "/production/#{indirection.name}/#{data.name}",
- :params => {},
- :client_cert => nil,
- :body => nil
- }
- end
+ let(:handler) { TestingHandler.new() }
- def a_request_that_submits(data, request = {})
- {
- :accept_header => request[:accept_header],
- :content_type_header => "text/yaml",
- :http_method => "PUT",
- :path => "/production/#{indirection.name}/#{data.name}",
- :params => {},
- :client_cert => nil,
- :body => data.render("text/yaml")
- }
- end
+ describe "the HTTP Handler" do
+ def respond(text)
+ lambda { |req, res| res.respond_with(200, "text/plain", text) }
+ end
- def a_request_that_destroys(data, request = {})
- {
- :accept_header => request[:accept_header],
- :content_type_header => "text/yaml",
- :http_method => "DELETE",
- :path => "/production/#{indirection.name}/#{data.name}",
- :params => {},
- :client_cert => nil,
- :body => ''
- }
- end
+ it "hands the request to the first route that matches the request path" do
+ handler = TestingHandler.new(
+ Puppet::Network::HTTP::Route.path(%r{^/foo}).get(respond("skipped")),
+ Puppet::Network::HTTP::Route.path(%r{^/vtest}).get(respond("used")),
+ Puppet::Network::HTTP::Route.path(%r{^/vtest/foo}).get(respond("ignored")))
- def a_request_that_finds(data, request = {})
- {
- :accept_header => request[:accept_header],
- :content_type_header => "text/yaml",
- :http_method => "GET",
- :path => "/production/#{indirection.name}/#{data.name}",
- :params => {},
- :client_cert => nil,
- :body => ''
- }
- end
+ req = a_request("GET", "/vtest/foo")
+ res = {}
- def a_request_that_searches(key, request = {})
- {
- :accept_header => request[:accept_header],
- :content_type_header => "text/yaml",
- :http_method => "GET",
- :path => "/production/#{indirection.name}s/#{key}",
- :params => {},
- :client_cert => nil,
- :body => ''
- }
- end
+ handler.process(req, res)
+
+ expect(res[:body]).to eq("used")
+ end
- let(:handler) { TestingHandler.new }
+ it "raises an error if multiple routes with the same path regex are registered" do
+ expect do
+ handler = TestingHandler.new(
+ Puppet::Network::HTTP::Route.path(%r{^/foo}).get(respond("ignored")),
+ Puppet::Network::HTTP::Route.path(%r{^/foo}).post(respond("also ignored")))
+ end.to raise_error(ArgumentError)
+ end
- it "should include the v1 REST API" do
- Puppet::Network::HTTP::Handler.ancestors.should be_include(Puppet::Network::HTTP::API::V1)
- end
+ it "raises an HTTP not found error if no routes match" do
+ handler = TestingHandler.new
- it "should include the Rest Authorization system" do
- Puppet::Network::HTTP::Handler.ancestors.should be_include(Puppet::Network::Authorization)
- end
+ req = a_request("GET", "/vtest/foo")
+ res = {}
+
+ handler.process(req, res)
- describe "when initializing" do
- it "should fail when no server type has been provided" do
- lambda { handler.initialize_for_puppet }.should raise_error(ArgumentError)
+ res_body = JSON(res[:body])
+
+ expect(res[:content_type_header]).to eq("application/json")
+ expect(res_body["issue_kind"]).to eq("HANDLER_NOT_FOUND")
+ expect(res_body["message"]).to eq("Not Found: No route for GET /vtest/foo")
+ expect(res[:status]).to eq(404)
end
- it "should set server type" do
- handler.initialize_for_puppet("foo")
- handler.server.should == "foo"
+ it "returns a structured error response with a stacktrace when the server encounters an internal error" do
+ handler = TestingHandler.new(
+ Puppet::Network::HTTP::Route.path(/.*/).get(lambda { |_, _| raise Exception.new("the sky is falling!")}))
+
+ req = a_request("GET", "/vtest/foo")
+ res = {}
+
+ handler.process(req, res)
+
+ res_body = JSON(res[:body])
+
+ expect(res[:content_type_header]).to eq("application/json")
+ expect(res_body["issue_kind"]).to eq(Puppet::Network::HTTP::Issues::RUNTIME_ERROR.to_s)
+ expect(res_body["message"]).to eq("Server Error: the sky is falling!")
+ expect(res_body["stacktrace"].is_a?(Array) && !res_body["stacktrace"].empty?).to be_true
+ expect(res_body["stacktrace"][0]).to match("spec/unit/network/http/handler_spec.rb")
+ expect(res[:status]).to eq(500)
end
+
end
describe "when processing a request" do
let(:response) do
{ :status => 200 }
end
before do
handler.stubs(:check_authorization)
handler.stubs(:warn_if_near_expiration)
end
it "should check the client certificate for upcoming expiration" do
request = a_request
cert = mock 'cert'
handler.expects(:client_cert).returns(cert).with(request)
handler.expects(:warn_if_near_expiration).with(cert)
handler.process(request, response)
end
it "should setup a profiler when the puppet-profiling header exists" do
request = a_request
request[:headers][Puppet::Network::HTTP::HEADER_ENABLE_PROFILING.downcase] = "true"
handler.process(request, response)
Puppet::Util::Profiler.current.should be_kind_of(Puppet::Util::Profiler::WallClock)
end
it "should not setup profiler when the profile parameter is missing" do
request = a_request
request[:params] = { }
handler.process(request, response)
Puppet::Util::Profiler.current.should == Puppet::Util::Profiler::NONE
end
- it "should create an indirection request from the path, parameters, and http method" do
- request = a_request
- request[:path] = "mypath"
- request[:http_method] = "mymethod"
- request[:params] = { :params => "mine" }
-
- handler.expects(:uri2indirection).with("mymethod", "mypath", { :params => "mine" }).returns stub("request", :method => :find)
-
- handler.stubs(:do_find)
-
- handler.process(request, response)
- end
-
- 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).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)
-
- handler.process(request, response)
- end
-
- it "should return an error code if the indirection does not support remote requests" do
- request = a_request
-
- 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_that_finds(Puppet::TestModel.new("key"))
-
- handler.expects(:do_find).raises(ArgumentError, "The exception")
- handler.expects(:set_response).with(anything, "The exception", 400)
- handler.process(request, response)
- end
-
- it "should set the format to text/plain when serializing an exception" do
- handler.expects(:set_content_type).with(response, "text/plain")
-
- handler.do_exception(response, Exception.new("A test"), 404)
- end
-
- it "sends an exception string with the given status" do
- handler.expects(:set_response).with(response, "A test", 404)
-
- handler.do_exception(response, Exception.new("A test"), 404)
- end
-
- it "sends an exception error with the exception's status" do
- data = Puppet::TestModel.new("not_found", "not found")
- request = a_request_that_finds(data, :accept_header => "pson")
-
- error = Puppet::Network::HTTP::Handler::HTTPNotFoundError.new("Could not find test_model not_found")
- handler.expects(:set_response).with(response, error.to_s, error.status)
-
- handler.process(request, response)
- end
-
- it "logs an HTTP response exception at info level (most are harmless)" do
- data = Puppet::TestModel.new("not_found", "not found")
- error = Puppet::Network::HTTP::Handler::HTTPNotFoundError.new("Could not find test_model not_found")
-
- request = a_request_that_finds(data, :accept_header => "pson")
- Puppet.expects(:info).with(error.message)
-
- handler.process(request, response)
- end
-
it "should raise an error if the request is formatted in an unknown format" do
handler.stubs(:content_type_header).returns "unknown format"
lambda { handler.request_format(request) }.should raise_error
end
it "should still find the correct format if content type contains charset information" do
- request = a_request
- handler.stubs(:content_type_header).returns "text/plain; charset=UTF-8"
- handler.request_format(request).should == "s"
+ request = Puppet::Network::HTTP::Request.new({ 'content-type' => "text/plain; charset=UTF-8" },
+ {}, 'GET', '/', nil)
+ request.format.should == "s"
end
it "should deserialize YAML parameters" do
params = {'my_param' => [1,2,3].to_yaml}
decoded_params = handler.send(:decode_params, params)
decoded_params.should == {:my_param => [1,2,3]}
end
it "should ignore tags on YAML parameters" do
params = {'my_param' => "--- !ruby/object:Array {}"}
decoded_params = handler.send(:decode_params, params)
decoded_params[:my_param].should be_a(Hash)
end
-
- describe "when finding a model instance" do
- it "uses the first supported format for the response" do
- data = Puppet::TestModel.new("my data", "some data")
- indirection.save(data, "my data")
- request = a_request_that_finds(data, :accept_header => "unknown, pson, yaml")
-
- 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, "my data", {}, request, response)
- end
-
- it "responds with a 406 error when no accept header is provided" do
- data = Puppet::TestModel.new("my data", "some data")
- indirection.save(data, "my data")
- request = a_request_that_finds(data, :accept_header => nil)
-
- expect do
- handler.do_find(indirection, "my data", {}, request, response)
- end.to raise_error(Puppet::Network::HTTP::Handler::HTTPNotAcceptableError)
- end
-
- it "raises an error when no accepted formats are known" do
- data = Puppet::TestModel.new("my data", "some data")
- indirection.save(data, "my data")
- request = a_request_that_finds(data, :accept_header => "unknown, also/unknown")
-
- expect do
- handler.do_find(indirection, "my data", {}, request, response)
- end.to raise_error(Puppet::Network::HTTP::Handler::HTTPNotAcceptableError)
- end
-
- it "should pass the result through without rendering it if the result is a string" do
- data = Puppet::TestModel.new("my data", "some data")
- data_string = "my data string"
- request = a_request_that_finds(data, :accept_header => "pson")
- indirection.expects(:find).returns(data_string)
-
- handler.expects(:set_response).with(response, data_string)
- handler.expects(:set_content_type).with(response, Puppet::Network::FormatHandler.format(:pson))
-
- handler.do_find(indirection, "my data", {}, request, response)
- end
-
- it "should return a 404 when no model instance can be found" do
- data = Puppet::TestModel.new("my data", "some data")
- request = a_request_that_finds(data, :accept_header => "unknown, pson, yaml")
-
- expect do
- handler.do_find(indirection, "my data", {}, request, response)
- end.to raise_error(Puppet::Network::HTTP::Handler::HTTPNotFoundError)
- end
- end
-
- describe "when performing head operation" do
- it "should not generate a response when a model head call succeeds" do
- data = Puppet::TestModel.new("my data", "some data")
- indirection.save(data, "my data")
- request = a_request_that_heads(data)
-
- handler.expects(:set_response).never
-
- handler.process(request, response)
- end
-
- it "should return a 404 when the model head call returns false" do
- data = Puppet::TestModel.new("my data", "data not there")
- request = a_request_that_heads(data)
-
- handler.expects(:set_response).with(response, "Not Found: Could not find test_model my data", 404)
-
- handler.process(request, response)
- end
- end
-
- describe "when searching for model instances" do
- it "uses the first supported format for the response" do
- data = Puppet::TestModel.new("my data", "some data")
- indirection.save(data, "my data")
- request = a_request_that_searches("my", :accept_header => "unknown, pson, yaml")
-
- 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, "my", {}, request, response)
- end
-
- it "should return [] when searching returns an empty array" do
- request = a_request_that_searches("nothing", :accept_header => "unknown, pson, yaml")
-
- 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, "nothing", {}, request, response)
- end
-
- it "should return a 404 when searching returns nil" do
- request = a_request_that_searches("nothing", :accept_header => "unknown, pson, yaml")
- indirection.expects(:search).returns(nil)
-
- expect do
- handler.do_search(indirection, "nothing", {}, request, response)
- end.to raise_error(Puppet::Network::HTTP::Handler::HTTPNotFoundError)
- end
- end
-
- describe "when destroying a model instance" do
- it "destroys the data indicated in the request" do
- data = Puppet::TestModel.new("my data", "some data")
- indirection.save(data, "my data")
- request = a_request_that_destroys(data)
-
- handler.do_destroy(indirection, "my data", {}, request, response)
-
- Puppet::TestModel.indirection.find("my data").should be_nil
- end
-
- it "responds with yaml when no Accept header is given" do
- data = Puppet::TestModel.new("my data", "some data")
- indirection.save(data, "my data")
- request = a_request_that_destroys(data, :accept_header => nil)
-
- 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, "my data", {}, request, response)
- end
-
- it "uses the first supported format for the response" do
- data = Puppet::TestModel.new("my data", "some data")
- indirection.save(data, "my data")
- request = a_request_that_destroys(data, :accept_header => "unknown, pson, yaml")
-
- 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, "my data", {}, request, response)
- end
-
- it "raises an error and does not destory when no accepted formats are known" do
- data = Puppet::TestModel.new("my data", "some data")
- indirection.save(data, "my data")
- request = a_request_that_submits(data, :accept_header => "unknown, also/unknown")
-
- expect do
- 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
- end
- end
-
- describe "when saving a model instance" do
- it "allows an empty body when the format supports it" do
- class Puppet::TestModel::Nonvalidatingmemory < Puppet::TestModel::Memory
- def validate_key(_)
- # nothing
- end
- end
-
- indirection.terminus_class = :nonvalidatingmemory
-
- data = Puppet::TestModel.new("test", '')
- request = a_request_that_submits(data)
- request[:content_type_header] = "application/x-raw"
- request[:body] = ''
-
- handler.do_save(indirection, "test", {}, request, response)
-
- Puppet::TestModel.indirection.find("test").data.should == ''
- end
-
- it "saves the data sent in the request" do
- data = Puppet::TestModel.new("my data", "some data")
- request = a_request_that_submits(data)
-
- handler.do_save(indirection, "my data", {}, request, response)
-
- Puppet::TestModel.indirection.find("my data").should == data
- end
-
- it "responds with yaml when no Accept header is given" do
- data = Puppet::TestModel.new("my data", "some data")
- request = a_request_that_submits(data, :accept_header => nil)
-
- 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, "my data", {}, request, response)
- end
-
- it "uses the first supported format for the response" do
- data = Puppet::TestModel.new("my data", "some data")
- request = a_request_that_submits(data, :accept_header => "unknown, pson, yaml")
-
- 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, "my data", {}, request, response)
- end
-
- it "raises an error and does not save when no accepted formats are known" do
- data = Puppet::TestModel.new("my data", "some data")
- request = a_request_that_submits(data, :accept_header => "unknown, also/unknown")
-
- expect do
- 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
- end
- end
end
+
describe "when resolving node" do
it "should use a look-up from the ip address" do
Resolv.expects(:getname).with("1.2.3.4").returns("host.domain.com")
handler.resolve_node(:ip => "1.2.3.4")
end
it "should return the look-up result" do
Resolv.stubs(:getname).with("1.2.3.4").returns("host.domain.com")
handler.resolve_node(:ip => "1.2.3.4").should == "host.domain.com"
end
it "should return the ip address if resolving fails" do
Resolv.stubs(:getname).with("1.2.3.4").raises(RuntimeError, "no such host")
handler.resolve_node(:ip => "1.2.3.4").should == "1.2.3.4"
end
end
class TestingHandler
include Puppet::Network::HTTP::Handler
- def accept_header(request)
- request[:accept_header]
- end
-
- def content_type_header(request)
- request[:content_type_header]
+ def initialize(* routes)
+ register(routes)
end
def set_content_type(response, format)
- "my_result"
+ response[:content_type_header] = format
end
def set_response(response, body, status = 200)
response[:body] = body
response[:status] = status
end
def http_method(request)
request[:http_method]
end
def path(request)
request[:path]
end
def params(request)
request[:params]
end
def client_cert(request)
request[:client_cert]
end
def body(request)
request[:body]
end
def headers(request)
request[:headers] || {}
end
end
end
diff --git a/spec/unit/network/http/rack/rest_spec.rb b/spec/unit/network/http/rack/rest_spec.rb
index e9527a460..165b6ceb9 100755
--- a/spec/unit/network/http/rack/rest_spec.rb
+++ b/spec/unit/network/http/rack/rest_spec.rb
@@ -1,324 +1,316 @@
#! /usr/bin/env ruby
require 'spec_helper'
require 'puppet/network/http/rack' if Puppet.features.rack?
require 'puppet/network/http/rack/rest'
describe "Puppet::Network::HTTP::RackREST", :if => Puppet.features.rack? do
it "should include the Puppet::Network::HTTP::Handler module" do
Puppet::Network::HTTP::RackREST.ancestors.should be_include(Puppet::Network::HTTP::Handler)
end
- describe "when initializing" do
- it "should call the Handler's initialization hook with its provided arguments" do
- Puppet::Network::HTTP::RackREST.any_instance.expects(:initialize_for_puppet).with(:server => "my", :handler => "arguments")
- Puppet::Network::HTTP::RackREST.new(:server => "my", :handler => "arguments")
- end
- end
-
describe "when serving a request" do
before :all do
@model_class = stub('indirected model class')
Puppet::Indirector::Indirection.stubs(:model).with(:foo).returns(@model_class)
@handler = Puppet::Network::HTTP::RackREST.new(:handler => :foo)
end
before :each do
@response = Rack::Response.new
end
def mk_req(uri, opts = {})
env = Rack::MockRequest.env_for(uri, opts)
Rack::Request.new(env)
end
+ let(:minimal_certificate) do
+ cert = OpenSSL::X509::Certificate.new
+ cert.version = 2
+ cert.serial = 0
+ cert.not_before = Time.now
+ cert.not_after = Time.now + 3600
+ cert.public_key = OpenSSL::PKey::RSA.new(512)
+ cert.subject = OpenSSL::X509::Name.parse("/CN=testing")
+ cert
+ end
+
describe "#headers" do
it "should return the headers (parsed from env with prefix 'HTTP_')" do
req = mk_req('/', {'HTTP_Accept' => 'myaccept',
'HTTP_X_Custom_Header' => 'mycustom',
'NOT_HTTP_foo' => 'not an http header'})
@handler.headers(req).should == {"accept" => 'myaccept',
- "x-custom-header" => 'mycustom'}
+ "x-custom-header" => 'mycustom',
+ "content-type" => nil }
end
end
describe "and using the HTTP Handler interface" do
- it "should return the HTTP_ACCEPT parameter as the accept header" do
- req = mk_req('/', 'HTTP_ACCEPT' => 'myaccept')
- @handler.accept_header(req).should == "myaccept"
- end
-
it "should return the CONTENT_TYPE parameter as the content type header" do
req = mk_req('/', 'CONTENT_TYPE' => 'mycontent')
- @handler.content_type_header(req).should == "mycontent"
+ @handler.headers(req)['content-type'].should == "mycontent"
end
it "should use the REQUEST_METHOD as the http method" do
req = mk_req('/', :method => 'MYMETHOD')
@handler.http_method(req).should == "MYMETHOD"
end
it "should return the request path as the path" do
req = mk_req('/foo/bar')
@handler.path(req).should == "/foo/bar"
end
it "should return the request body as the body" do
req = mk_req('/foo/bar', :input => 'mybody')
@handler.body(req).should == "mybody"
end
- it "should return the an OpenSSL::X509::Certificate instance as the client_cert" do
- cert = stub 'cert'
- req = mk_req('/foo/bar', 'SSL_CLIENT_CERT' => 'certificate in pem format')
- OpenSSL::X509::Certificate.expects(:new).with('certificate in pem format').returns(cert)
- @handler.client_cert(req).should == cert
+ it "should return the an Puppet::SSL::Certificate instance as the client_cert" do
+ req = mk_req('/foo/bar', 'SSL_CLIENT_CERT' => minimal_certificate.to_pem)
+ expect(@handler.client_cert(req).content.to_pem).to eq(minimal_certificate.to_pem)
end
it "returns nil when SSL_CLIENT_CERT is empty" do
- cert = stub 'cert'
req = mk_req('/foo/bar', 'SSL_CLIENT_CERT' => '')
- OpenSSL::X509::Certificate.expects(:new).never
- @handler.client_cert(req).should be_nil
- end
- it "(#16769) does not raise error 'header too long'" do
- cert = stub 'cert'
- req = mk_req('/foo/bar', 'SSL_CLIENT_CERT' => '')
- lambda { @handler.client_cert(req) }.should_not raise_error
+ @handler.client_cert(req).should be_nil
end
it "should set the response's content-type header when setting the content type" do
@header = mock 'header'
@response.expects(:header).returns @header
@header.expects(:[]=).with('Content-Type', "mytype")
@handler.set_content_type(@response, "mytype")
end
it "should set the status and write the body when setting the response for a request" do
@response.expects(:status=).with(400)
@response.expects(:write).with("mybody")
@handler.set_response(@response, "mybody", 400)
end
describe "when result is a File" do
before :each do
stat = stub 'stat', :size => 100
@file = stub 'file', :stat => stat, :path => "/tmp/path"
@file.stubs(:is_a?).with(File).returns(true)
end
it "should set the Content-Length header as a string" do
@response.expects(:[]=).with("Content-Length", '100')
@handler.set_response(@response, @file, 200)
end
it "should return a RackFile adapter as body" do
@response.expects(:body=).with { |val| val.is_a?(Puppet::Network::HTTP::RackREST::RackFile) }
@handler.set_response(@response, @file, 200)
end
end
it "should ensure the body has been read on success" do
req = mk_req('/production/report/foo', :method => 'PUT')
req.body.expects(:read).at_least_once
Puppet::Transaction::Report.stubs(:save)
@handler.process(req, @response)
end
it "should ensure the body has been partially read on failure" do
req = mk_req('/production/report/foo')
req.body.expects(:read).with(1)
- req.stubs(:check_authorization).raises(Exception)
+
+ @handler.stubs(:headers).raises(Exception)
@handler.process(req, @response)
end
end
describe "and determining the request parameters" do
it "should include the HTTP request parameters, with the keys as symbols" do
req = mk_req('/?foo=baz&bar=xyzzy')
result = @handler.params(req)
result[:foo].should == "baz"
result[:bar].should == "xyzzy"
end
it "should return multi-values params as an array of the values" do
req = mk_req('/?foo=baz&foo=xyzzy')
result = @handler.params(req)
result[:foo].should == ["baz", "xyzzy"]
end
it "should return parameters from the POST body" do
req = mk_req("/", :method => 'POST', :input => 'foo=baz&bar=xyzzy')
result = @handler.params(req)
result[:foo].should == "baz"
result[:bar].should == "xyzzy"
end
it "should not return multi-valued params in a POST body as an array of values" do
req = mk_req("/", :method => 'POST', :input => 'foo=baz&foo=xyzzy')
result = @handler.params(req)
result[:foo].should be_one_of("baz", "xyzzy")
end
it "should CGI-decode the HTTP parameters" do
encoding = CGI.escape("foo bar")
req = mk_req("/?foo=#{encoding}")
result = @handler.params(req)
result[:foo].should == "foo bar"
end
it "should convert the string 'true' to the boolean" do
req = mk_req("/?foo=true")
result = @handler.params(req)
result[:foo].should be_true
end
it "should convert the string 'false' to the boolean" do
req = mk_req("/?foo=false")
result = @handler.params(req)
result[:foo].should be_false
end
it "should convert integer arguments to Integers" do
req = mk_req("/?foo=15")
result = @handler.params(req)
result[:foo].should == 15
end
it "should convert floating point arguments to Floats" do
req = mk_req("/?foo=1.5")
result = @handler.params(req)
result[:foo].should == 1.5
end
it "should YAML-load and CGI-decode values that are YAML-encoded" do
escaping = CGI.escape(YAML.dump(%w{one two}))
req = mk_req("/?foo=#{escaping}")
result = @handler.params(req)
result[:foo].should == %w{one two}
end
it "should not allow the client to set the node via the query string" do
req = mk_req("/?node=foo")
@handler.params(req)[:node].should be_nil
end
it "should not allow the client to set the IP address via the query string" do
req = mk_req("/?ip=foo")
@handler.params(req)[:ip].should be_nil
end
it "should pass the client's ip address to model find" do
req = mk_req("/", 'REMOTE_ADDR' => 'ipaddress')
@handler.params(req)[:ip].should == "ipaddress"
end
it "should set 'authenticated' to false if no certificate is present" do
req = mk_req('/')
@handler.params(req)[:authenticated].should be_false
end
end
describe "with pre-validated certificates" do
it "should retrieve the hostname by finding the CN given in :ssl_client_header, in the format returned by Apache (RFC2253)" do
Puppet[:ssl_client_header] = "myheader"
req = mk_req('/', "myheader" => "O=Foo\\, Inc,CN=host.domain.com")
@handler.params(req)[:node].should == "host.domain.com"
end
it "should retrieve the hostname by finding the CN given in :ssl_client_header, in the format returned by nginx" do
Puppet[:ssl_client_header] = "myheader"
req = mk_req('/', "myheader" => "/CN=host.domain.com")
@handler.params(req)[:node].should == "host.domain.com"
end
it "should retrieve the hostname by finding the CN given in :ssl_client_header, ignoring other fields" do
Puppet[:ssl_client_header] = "myheader"
req = mk_req('/', "myheader" => 'ST=Denial,CN=host.domain.com,O=Domain\\, Inc.')
@handler.params(req)[:node].should == "host.domain.com"
end
it "should use the :ssl_client_header to determine the parameter for checking whether the host certificate is valid" do
Puppet[:ssl_client_header] = "certheader"
Puppet[:ssl_client_verify_header] = "myheader"
req = mk_req('/', "myheader" => "SUCCESS", "certheader" => "CN=host.domain.com")
@handler.params(req)[:authenticated].should be_true
end
it "should consider the host unauthenticated if the validity parameter does not contain 'SUCCESS'" do
Puppet[:ssl_client_header] = "certheader"
Puppet[:ssl_client_verify_header] = "myheader"
req = mk_req('/', "myheader" => "whatever", "certheader" => "CN=host.domain.com")
@handler.params(req)[:authenticated].should be_false
end
it "should consider the host unauthenticated if no certificate information is present" do
Puppet[:ssl_client_header] = "certheader"
Puppet[:ssl_client_verify_header] = "myheader"
req = mk_req('/', "myheader" => nil, "certheader" => "CN=host.domain.com")
@handler.params(req)[:authenticated].should be_false
end
it "should resolve the node name with an ip address look-up if no certificate is present" do
Puppet[:ssl_client_header] = "myheader"
req = mk_req('/', "myheader" => nil)
@handler.expects(:resolve_node).returns("host.domain.com")
@handler.params(req)[:node].should == "host.domain.com"
end
it "should resolve the node name with an ip address look-up if a certificate without a CN is present" do
Puppet[:ssl_client_header] = "myheader"
req = mk_req('/', "myheader" => "O=no CN")
@handler.expects(:resolve_node).returns("host.domain.com")
@handler.params(req)[:node].should == "host.domain.com"
end
it "should not allow authentication via the verify header if there is no CN available" do
Puppet[:ssl_client_header] = "dn_header"
Puppet[:ssl_client_verify_header] = "verify_header"
req = mk_req('/', "dn_header" => "O=no CN", "verify_header" => 'SUCCESS')
@handler.expects(:resolve_node).returns("host.domain.com")
@handler.params(req)[:authenticated].should be_false
end
end
end
end
describe Puppet::Network::HTTP::RackREST::RackFile do
before(:each) do
stat = stub 'stat', :size => 100
@file = stub 'file', :stat => stat, :path => "/tmp/path"
@rackfile = Puppet::Network::HTTP::RackREST::RackFile.new(@file)
end
it "should have an each method" do
@rackfile.should be_respond_to(:each)
end
it "should yield file chunks by chunks" do
@file.expects(:read).times(3).with(8192).returns("1", "2", nil)
i = 1
@rackfile.each do |chunk|
chunk.to_i.should == i
i += 1
end
end
it "should have a close method" do
@rackfile.should be_respond_to(:close)
end
it "should delegate close to File close" do
@file.expects(:close)
@rackfile.close
end
end
diff --git a/spec/unit/network/http/route_spec.rb b/spec/unit/network/http/route_spec.rb
new file mode 100644
index 000000000..04cc0e875
--- /dev/null
+++ b/spec/unit/network/http/route_spec.rb
@@ -0,0 +1,75 @@
+#! /usr/bin/env ruby
+require 'spec_helper'
+require 'puppet/indirector_testing'
+
+require 'puppet/network/http'
+
+describe Puppet::Network::HTTP::Route do
+ def request(method, path)
+ Puppet::Network::HTTP::Request.from_hash({
+ :method => method,
+ :path => path,
+ :routing_path => path })
+ end
+
+ def respond(text)
+ lambda { |req, res| res.respond_with(200, "text/plain", text) }
+ end
+
+ let(:req) { request("GET", "/vtest/foo") }
+ let(:res) { Puppet::Network::HTTP::MemoryResponse.new }
+
+ describe "an HTTP Route" do
+ it "can match a request" do
+ route = Puppet::Network::HTTP::Route.path(%r{^/vtest})
+ expect(route.matches?(req)).to be_true
+ end
+
+ it "will raise a Method Not Allowed error when no handler for the request's method is given" do
+ route = Puppet::Network::HTTP::Route.path(%r{^/vtest}).post(respond("ignored"))
+ expect do
+ route.process(req, res)
+ end.to raise_error(Puppet::Network::HTTP::Error::HTTPMethodNotAllowedError)
+ end
+
+ it "can match any HTTP method" do
+ route = Puppet::Network::HTTP::Route.path(%r{^/vtest/foo}).any(respond("used"))
+ expect(route.matches?(req)).to be_true
+
+ route.process(req, res)
+
+ expect(res.body).to eq("used")
+ end
+
+ it "calls the method handlers in turn" do
+ call_count = 0
+ handler = lambda { |request, response| call_count += 1 }
+ route = Puppet::Network::HTTP::Route.path(%r{^/vtest/foo}).get(handler, handler)
+
+ route.process(req, res)
+ expect(call_count).to eq(2)
+ end
+
+ it "stops calling handlers if one of them raises an error" do
+ ignored_called = false
+ ignored = lambda { |req, res| ignored_called = true }
+ raise_error = lambda { |req, res| raise Puppet::Network::HTTP::Error::HTTPNotAuthorizedError, "go away" }
+ route = Puppet::Network::HTTP::Route.path(%r{^/vtest/foo}).get(raise_error, ignored)
+
+ expect do
+ route.process(req, res)
+ end.to raise_error(Puppet::Network::HTTP::Error::HTTPNotAuthorizedError)
+ expect(ignored_called).to be_false
+ end
+
+ it "chains to other routes after calling its handlers" do
+ inner_route = Puppet::Network::HTTP::Route.path(%r{^/inner}).any(respond("inner"))
+ unused_inner_route = Puppet::Network::HTTP::Route.path(%r{^/unused_inner}).any(respond("unused"))
+
+ top_route = Puppet::Network::HTTP::Route.path(%r{^/vtest}).any(respond("top")).chain(unused_inner_route, inner_route)
+ top_route.process(request("GET", "/vtest/inner"), res)
+
+ expect(res.body).to eq("topinner")
+ end
+ end
+end
diff --git a/spec/unit/network/http/webrick/rest_spec.rb b/spec/unit/network/http/webrick/rest_spec.rb
index 852fe1547..36b2bcff9 100755
--- a/spec/unit/network/http/webrick/rest_spec.rb
+++ b/spec/unit/network/http/webrick/rest_spec.rb
@@ -1,268 +1,231 @@
#! /usr/bin/env ruby
require 'spec_helper'
require 'puppet/network/http'
require 'webrick'
require 'puppet/network/http/webrick/rest'
describe Puppet::Network::HTTP::WEBrickREST do
it "should include the Puppet::Network::HTTP::Handler module" do
Puppet::Network::HTTP::WEBrickREST.ancestors.should be_include(Puppet::Network::HTTP::Handler)
end
- describe "when initializing" do
- it "should call the Handler's initialization hook with its provided arguments as the server and handler" do
- server = WEBrick::HTTPServer.new(:BindAddress => '127.0.0.1',
- # Probablistically going to succeed
- # even if we run more than one test
- # instance at once.
- :Port => 40000 + rand(10000),
- # Just discard any log output, thanks.
- :Logger => stub_everything('logger'))
-
- Puppet::Network::HTTP::WEBrickREST.any_instance.
- expects(:initialize_for_puppet).with(:server => server, :handler => "arguments")
-
- Puppet::Network::HTTP::WEBrickREST.new(server, "arguments")
- end
- end
-
describe "when receiving a request" do
before do
@request = stub('webrick http request', :query => {}, :peeraddr => %w{eh boo host ip}, :client_cert => nil)
- @response = stub('webrick http response', :status= => true, :body= => true)
+ @response = mock('webrick http response')
@model_class = stub('indirected model class')
@webrick = stub('webrick http server', :mount => true, :[] => {})
Puppet::Indirector::Indirection.stubs(:model).with(:foo).returns(@model_class)
- @handler = Puppet::Network::HTTP::WEBrickREST.new(@webrick, :foo)
+ @handler = Puppet::Network::HTTP::WEBrickREST.new(@webrick)
end
it "should delegate its :service method to its :process method" do
@handler.expects(:process).with(@request, @response).returns "stuff"
@handler.service(@request, @response).should == "stuff"
end
describe "#headers" do
let(:fake_request) { {"Foo" => "bar", "BAZ" => "bam" } }
it "should iterate over the request object using #each" do
fake_request.expects(:each)
@handler.headers(fake_request)
end
it "should return a hash with downcased header names" do
result = @handler.headers(fake_request)
result.should == fake_request.inject({}) { |m,(k,v)| m[k.downcase] = v; m }
end
end
describe "when using the Handler interface" do
- it "should use the 'accept' request parameter as the Accept header" do
- @request.expects(:[]).with("accept").returns "foobar"
- @handler.accept_header(@request).should == "foobar"
- end
-
- it "should use the 'content-type' request header as the Content-Type header" do
- @request.expects(:[]).with("content-type").returns "foobar"
- @handler.content_type_header(@request).should == "foobar"
- end
-
it "should use the request method as the http method" do
@request.expects(:request_method).returns "FOO"
@handler.http_method(@request).should == "FOO"
end
it "should return the request path as the path" do
@request.expects(:path).returns "/foo/bar"
@handler.path(@request).should == "/foo/bar"
end
it "should return the request body as the body" do
@request.expects(:body).returns "my body"
@handler.body(@request).should == "my body"
end
it "should set the response's 'content-type' header when setting the content type" do
@response.expects(:[]=).with("content-type", "text/html")
@handler.set_content_type(@response, "text/html")
end
it "should set the status and body on the response when setting the response for a successful query" do
@response.expects(:status=).with 200
@response.expects(:body=).with "mybody"
@handler.set_response(@response, "mybody", 200)
end
- describe "when the result is a File" do
- before(:each) do
- stat = stub 'stat', :size => 100
- @file = stub 'file', :stat => stat, :path => "/tmp/path"
- @file.stubs(:is_a?).with(File).returns(true)
- end
-
- it "should serve it" do
- @response.stubs(:[]=)
-
- @response.expects(:status=).with 200
- @response.expects(:body=).with @file
+ it "serves a file" do
+ stat = stub 'stat', :size => 100
+ @file = stub 'file', :stat => stat, :path => "/tmp/path"
+ @file.stubs(:is_a?).with(File).returns(true)
- @handler.set_response(@response, @file, 200)
- end
-
- it "should set the Content-Length header" do
- @response.expects(:[]=).with('content-length', 100)
+ @response.expects(:[]=).with('content-length', 100)
+ @response.expects(:status=).with 200
+ @response.expects(:body=).with @file
- @handler.set_response(@response, @file, 200)
- end
+ @handler.set_response(@response, @file, 200)
end
it "should set the status and message on the response when setting the response for a failed query" do
@response.expects(:status=).with 400
- @response.expects(:reason_phrase=).with "mybody"
+ @response.expects(:body=).with "mybody"
@handler.set_response(@response, "mybody", 400)
end
end
describe "and determining the request parameters" do
def query_of(options)
request = Puppet::Indirector::Request.new(:myind, :find, "my key", nil, options)
WEBrick::HTTPUtils.parse_query(request.query_string.sub(/^\?/, ''))
end
def a_request_querying(query_data)
@request.expects(:query).returns(query_of(query_data))
@request
end
+ def certificate_with_subject(subj)
+ cert = OpenSSL::X509::Certificate.new
+ cert.subject = OpenSSL::X509::Name.parse(subj)
+ cert
+ end
+
it "has no parameters when there is no query string" do
only_server_side_information = [:authenticated, :ip, :node]
@request.stubs(:query).returns(nil)
result = @handler.params(@request)
result.keys.sort.should == only_server_side_information
end
it "should include the HTTP request parameters, with the keys as symbols" do
request = a_request_querying("foo" => "baz", "bar" => "xyzzy")
result = @handler.params(request)
result[:foo].should == "baz"
result[:bar].should == "xyzzy"
end
it "should handle parameters with no value" do
request = a_request_querying('foo' => "")
result = @handler.params(request)
result[:foo].should == ""
end
it "should convert the string 'true' to the boolean" do
request = a_request_querying('foo' => "true")
result = @handler.params(request)
result[:foo].should == true
end
it "should convert the string 'false' to the boolean" do
request = a_request_querying('foo' => "false")
result = @handler.params(request)
result[:foo].should == false
end
it "should reconstruct arrays" do
request = a_request_querying('foo' => ["a", "b", "c"])
result = @handler.params(request)
result[:foo].should == ["a", "b", "c"]
end
it "should convert values inside arrays into primitive types" do
request = a_request_querying('foo' => ["true", "false", "1", "1.2"])
result = @handler.params(request)
result[:foo].should == [true, false, 1, 1.2]
end
it "should YAML-load values that are YAML-encoded" do
request = a_request_querying('foo' => YAML.dump(%w{one two}))
result = @handler.params(request)
result[:foo].should == %w{one two}
end
it "should YAML-load that are YAML-encoded" do
request = a_request_querying('foo' => YAML.dump(%w{one two}))
result = @handler.params(request)
result[:foo].should == %w{one two}
end
it "should not allow clients to set the node via the request parameters" do
request = a_request_querying("node" => "foo")
@handler.stubs(:resolve_node)
@handler.params(request)[:node].should be_nil
end
it "should not allow clients to set the IP via the request parameters" do
request = a_request_querying("ip" => "foo")
@handler.params(request)[:ip].should_not == "foo"
end
it "should pass the client's ip address to model find" do
@request.stubs(:peeraddr).returns(%w{noidea dunno hostname ipaddress})
@handler.params(@request)[:ip].should == "ipaddress"
end
it "should set 'authenticated' to true if a certificate is present" do
cert = stub 'cert', :subject => [%w{CN host.domain.com}]
@request.stubs(:client_cert).returns cert
@handler.params(@request)[:authenticated].should be_true
end
it "should set 'authenticated' to false if no certificate is present" do
@request.stubs(:client_cert).returns nil
@handler.params(@request)[:authenticated].should be_false
end
it "should pass the client's certificate name to model method if a certificate is present" do
- subj = stub 'subj'
- cert = stub 'cert', :subject => subj
- @request.stubs(:client_cert).returns cert
- Puppet::Util::SSL.expects(:cn_from_subject).with(subj).returns 'host.domain.com'
+ @request.stubs(:client_cert).returns(certificate_with_subject("/CN=host.domain.com"))
+
@handler.params(@request)[:node].should == "host.domain.com"
end
it "should resolve the node name with an ip address look-up if no certificate is present" do
@request.stubs(:client_cert).returns nil
@handler.expects(:resolve_node).returns(:resolved_node)
@handler.params(@request)[:node].should == :resolved_node
end
it "should resolve the node name with an ip address look-up if CN parsing fails" do
- subj = stub 'subj'
- cert = stub 'cert', :subject => subj
- @request.stubs(:client_cert).returns cert
- Puppet::Util::SSL.expects(:cn_from_subject).with(subj).returns nil
+ @request.stubs(:client_cert).returns(certificate_with_subject("/C=company"))
@handler.expects(:resolve_node).returns(:resolved_node)
@handler.params(@request)[:node].should == :resolved_node
end
- end
+ end
end
end
diff --git a/spec/unit/network/http_pool_spec.rb b/spec/unit/network/http_pool_spec.rb
index 4ee507568..fc10a9827 100755
--- a/spec/unit/network/http_pool_spec.rb
+++ b/spec/unit/network/http_pool_spec.rb
@@ -1,74 +1,74 @@
#! /usr/bin/env ruby
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
it "should return an http instance created with the passed host and port" do
http = Puppet::Network::HttpPool.http_instance("me", 54321)
http.should be_an_instance_of Puppet::Network::HTTP::Connection
http.address.should == 'me'
http.port.should == 54321
end
it "should enable ssl on the http instance by default" do
Puppet::Network::HttpPool.http_instance("me", 54321).should be_use_ssl
end
it "can set ssl using an option" do
Puppet::Network::HttpPool.http_instance("me", 54321, false).should_not be_use_ssl
Puppet::Network::HttpPool.http_instance("me", 54321, true).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')
Puppet[:ssl_client_ca_auth] = ca_cert_file
- Puppet::FileSystem::File.stubs(:exist?).with(ca_cert_file).returns(true)
+ Puppet::FileSystem.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')
- Puppet::FileSystem::File.stubs(:exist?).with(host_cert_file).returns(true)
+ Puppet::FileSystem.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::SSL::Host.stubs(:localhost).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::HttpPool.http_instance("me", 54321, true, true).send(:connection).verify_mode.should == OpenSSL::SSL::VERIFY_PEER
end
it 'can disable peer verification' do
Puppet::Network::HttpPool.http_instance("me", 54321, true, false).send(:connection).verify_mode.should == OpenSSL::SSL::VERIFY_NONE
end
end
it "should not cache http instances" do
Puppet::Network::HttpPool.http_instance("me", 54321).
- should_not equal Puppet::Network::HttpPool.http_instance("me", 54321)
+ should_not equal(Puppet::Network::HttpPool.http_instance("me", 54321))
end
end
end
diff --git a/spec/unit/network/rights_spec.rb b/spec/unit/network/rights_spec.rb
index 539f7a808..5440ae19d 100755
--- a/spec/unit/network/rights_spec.rb
+++ b/spec/unit/network/rights_spec.rb
@@ -1,438 +1,438 @@
#! /usr/bin/env ruby
require 'spec_helper'
require 'puppet/network/rights'
describe Puppet::Network::Rights do
before do
@right = Puppet::Network::Rights.new
end
describe "when validating a :head request" do
[:find, :save].each do |allowed_method|
it "should allow the request if only #{allowed_method} is allowed" do
rights = Puppet::Network::Rights.new
right = rights.newright("/")
right.allow("*")
right.restrict_method(allowed_method)
right.restrict_authenticated(:any)
- rights.is_request_forbidden_and_why?(:indirection_name, :head, "key", {}).should == nil
+ rights.is_request_forbidden_and_why?(:head, "/indirection_name/key", {}).should == nil
end
end
it "should disallow the request if neither :find nor :save is allowed" do
rights = Puppet::Network::Rights.new
- why_forbidden = rights.is_request_forbidden_and_why?(:indirection_name, :head, "key", {})
+ why_forbidden = rights.is_request_forbidden_and_why?(:head, "/indirection_name/key", {})
why_forbidden.should be_instance_of(Puppet::Network::AuthorizationError)
why_forbidden.to_s.should == "Forbidden request: access to /indirection_name/key [find]"
end
end
it "should throw an error if type can't be determined" do
lambda { @right.newright("name") }.should raise_error
end
describe "when creating new path ACLs" do
it "should not throw an error if the ACL already exists" do
@right.newright("/name")
lambda { @right.newright("/name")}.should_not raise_error
end
it "should throw an error if the acl uri path is not absolute" do
lambda { @right.newright("name")}.should raise_error
end
it "should create a new ACL with the correct path" do
@right.newright("/name")
@right["/name"].should_not be_nil
end
it "should create an ACL of type Puppet::Network::AuthStore" do
@right.newright("/name")
@right["/name"].should be_a_kind_of(Puppet::Network::AuthStore)
end
end
describe "when creating new regex ACLs" do
it "should not throw an error if the ACL already exists" do
@right.newright("~ .rb$")
lambda { @right.newright("~ .rb$")}.should_not raise_error
end
it "should create a new ACL with the correct regex" do
@right.newright("~ .rb$")
@right.include?(".rb$").should_not be_nil
end
it "should be able to lookup the regex" do
@right.newright("~ .rb$")
@right[".rb$"].should_not be_nil
end
it "should be able to lookup the regex by its full name" do
@right.newright("~ .rb$")
@right["~ .rb$"].should_not be_nil
end
it "should create an ACL of type Puppet::Network::AuthStore" do
@right.newright("~ .rb$").should be_a_kind_of(Puppet::Network::AuthStore)
end
end
describe "when checking ACLs existence" do
it "should return false if there are no matching rights" do
@right.include?("name").should be_false
end
it "should return true if a path right exists" do
@right.newright("/name")
@right.include?("/name").should be_true
end
it "should return false if no matching path rights exist" do
@right.newright("/name")
@right.include?("/differentname").should be_false
end
it "should return true if a regex right exists" do
@right.newright("~ .rb$")
@right.include?(".rb$").should be_true
end
it "should return false if no matching path rights exist" do
@right.newright("~ .rb$")
@right.include?(".pp$").should be_false
end
end
describe "when checking if right is allowed" do
before :each do
@right.stubs(:right).returns(nil)
@pathacl = stub 'pathacl', :"<=>" => 1, :line => 0, :file => 'dummy'
Puppet::Network::Rights::Right.stubs(:new).returns(@pathacl)
end
it "should delegate to is_forbidden_and_why?" do
@right.expects(:is_forbidden_and_why?).with("namespace", :node => "host.domain.com", :ip => "127.0.0.1").returns(nil)
@right.allowed?("namespace", "host.domain.com", "127.0.0.1")
end
it "should return true if is_forbidden_and_why? returns nil" do
@right.stubs(:is_forbidden_and_why?).returns(nil)
@right.allowed?("namespace", :args).should be_true
end
it "should return false if is_forbidden_and_why? returns an AuthorizationError" do
@right.stubs(:is_forbidden_and_why?).returns(Puppet::Network::AuthorizationError.new("forbidden"))
@right.allowed?("namespace", :args1, :args2).should be_false
end
it "should pass the match? return to allowed?" do
@right.newright("/path/to/there")
@pathacl.expects(:match?).returns(:match)
@pathacl.expects(:allowed?).with { |node,ip,h| h[:match] == :match }.returns(true)
@right.is_forbidden_and_why?("/path/to/there", {}).should == nil
end
describe "with path acls" do
before :each do
@long_acl = stub 'longpathacl', :name => "/path/to/there", :line => 0, :file => 'dummy'
Puppet::Network::Rights::Right.stubs(:new).with("/path/to/there", 0, nil).returns(@long_acl)
@short_acl = stub 'shortpathacl', :name => "/path/to", :line => 0, :file => 'dummy'
Puppet::Network::Rights::Right.stubs(:new).with("/path/to", 0, nil).returns(@short_acl)
@long_acl.stubs(:"<=>").with(@short_acl).returns(0)
@short_acl.stubs(:"<=>").with(@long_acl).returns(0)
end
it "should select the first match" do
@right.newright("/path/to", 0)
@right.newright("/path/to/there", 0)
@long_acl.stubs(:match?).returns(true)
@short_acl.stubs(:match?).returns(true)
@short_acl.expects(:allowed?).returns(true)
@long_acl.expects(:allowed?).never
@right.is_forbidden_and_why?("/path/to/there/and/there", {}).should == nil
end
it "should select the first match that doesn't return :dunno" do
@right.newright("/path/to/there", 0, nil)
@right.newright("/path/to", 0, nil)
@long_acl.stubs(:match?).returns(true)
@short_acl.stubs(:match?).returns(true)
@long_acl.expects(:allowed?).returns(:dunno)
@short_acl.expects(:allowed?).returns(true)
@right.is_forbidden_and_why?("/path/to/there/and/there", {}).should == nil
end
it "should not select an ACL that doesn't match" do
@right.newright("/path/to/there", 0)
@right.newright("/path/to", 0)
@long_acl.stubs(:match?).returns(false)
@short_acl.stubs(:match?).returns(true)
@long_acl.expects(:allowed?).never
@short_acl.expects(:allowed?).returns(true)
@right.is_forbidden_and_why?("/path/to/there/and/there", {}).should == nil
end
it "should not raise an AuthorizationError if allowed" do
@right.newright("/path/to/there", 0)
@long_acl.stubs(:match?).returns(true)
@long_acl.stubs(:allowed?).returns(true)
@right.is_forbidden_and_why?("/path/to/there/and/there", {}).should == nil
end
it "should raise an AuthorizationError if the match is denied" do
@right.newright("/path/to/there", 0, nil)
@long_acl.stubs(:match?).returns(true)
@long_acl.stubs(:allowed?).returns(false)
@right.is_forbidden_and_why?("/path/to/there", {}).should be_instance_of(Puppet::Network::AuthorizationError)
end
it "should raise an AuthorizationError if no path match" do
@right.is_forbidden_and_why?("/nomatch", {}).should be_instance_of(Puppet::Network::AuthorizationError)
end
end
describe "with regex acls" do
before :each do
@regex_acl1 = stub 'regex_acl1', :name => "/files/(.*)/myfile", :line => 0, :file => 'dummy'
Puppet::Network::Rights::Right.stubs(:new).with("~ /files/(.*)/myfile", 0, nil).returns(@regex_acl1)
@regex_acl2 = stub 'regex_acl2', :name => "/files/(.*)/myfile/", :line => 0, :file => 'dummy'
Puppet::Network::Rights::Right.stubs(:new).with("~ /files/(.*)/myfile/", 0, nil).returns(@regex_acl2)
@regex_acl1.stubs(:"<=>").with(@regex_acl2).returns(0)
@regex_acl2.stubs(:"<=>").with(@regex_acl1).returns(0)
end
it "should select the first match" do
@right.newright("~ /files/(.*)/myfile", 0)
@right.newright("~ /files/(.*)/myfile/", 0)
@regex_acl1.stubs(:match?).returns(true)
@regex_acl2.stubs(:match?).returns(true)
@regex_acl1.expects(:allowed?).returns(true)
@regex_acl2.expects(:allowed?).never
@right.is_forbidden_and_why?("/files/repository/myfile/other", {}).should == nil
end
it "should select the first match that doesn't return :dunno" do
@right.newright("~ /files/(.*)/myfile", 0)
@right.newright("~ /files/(.*)/myfile/", 0)
@regex_acl1.stubs(:match?).returns(true)
@regex_acl2.stubs(:match?).returns(true)
@regex_acl1.expects(:allowed?).returns(:dunno)
@regex_acl2.expects(:allowed?).returns(true)
@right.is_forbidden_and_why?("/files/repository/myfile/other", {}).should == nil
end
it "should not select an ACL that doesn't match" do
@right.newright("~ /files/(.*)/myfile", 0)
@right.newright("~ /files/(.*)/myfile/", 0)
@regex_acl1.stubs(:match?).returns(false)
@regex_acl2.stubs(:match?).returns(true)
@regex_acl1.expects(:allowed?).never
@regex_acl2.expects(:allowed?).returns(true)
@right.is_forbidden_and_why?("/files/repository/myfile/other", {}).should == nil
end
it "should not raise an AuthorizationError if allowed" do
@right.newright("~ /files/(.*)/myfile", 0)
@regex_acl1.stubs(:match?).returns(true)
@regex_acl1.stubs(:allowed?).returns(true)
@right.is_forbidden_and_why?("/files/repository/myfile/other", {}).should == nil
end
it "should raise an error if no regex acl match" do
@right.is_forbidden_and_why?("/path", {}).should be_instance_of(Puppet::Network::AuthorizationError)
end
it "should raise an AuthorizedError on deny" do
@right.is_forbidden_and_why?("/path", {}).should be_instance_of(Puppet::Network::AuthorizationError)
end
end
end
describe Puppet::Network::Rights::Right do
before :each do
@acl = Puppet::Network::Rights::Right.new("/path",0, nil)
end
describe "with path" do
it "should match up to its path length" do
@acl.match?("/path/that/works").should_not be_nil
end
it "should match up to its path length" do
@acl.match?("/paththatalsoworks").should_not be_nil
end
it "should return nil if no match" do
@acl.match?("/notpath").should be_nil
end
end
describe "with regex" do
before :each do
@acl = Puppet::Network::Rights::Right.new("~ .rb$",0, nil)
end
it "should match as a regex" do
@acl.match?("this should work.rb").should_not be_nil
end
it "should return nil if no match" do
@acl.match?("do not match").should be_nil
end
end
it "should allow all rest methods by default" do
@acl.methods.should == Puppet::Network::Rights::Right::ALL
end
it "should allow only authenticated request by default" do
@acl.authentication.should be_true
end
it "should allow modification of the methods filters" do
@acl.restrict_method(:save)
@acl.methods.should == [:save]
end
it "should stack methods filters" do
@acl.restrict_method(:save)
@acl.restrict_method(:destroy)
@acl.methods.should == [:save, :destroy]
end
it "should raise an error if the method is already filtered" do
@acl.restrict_method(:save)
lambda { @acl.restrict_method(:save) }.should raise_error
end
it "should allow setting an environment filters" do
Puppet::Node::Environment.stubs(:new).with(:environment).returns(:env)
@acl.restrict_environment(:environment)
@acl.environment.should == [:env]
end
["on", "yes", "true", true].each do |auth|
it "should allow filtering on authenticated requests with '#{auth}'" do
@acl.restrict_authenticated(auth)
@acl.authentication.should be_true
end
end
["off", "no", "false", false, "all", "any", :all, :any].each do |auth|
it "should allow filtering on authenticated or unauthenticated requests with '#{auth}'" do
@acl.restrict_authenticated(auth)
@acl.authentication.should be_false
end
end
describe "when checking right authorization" do
it "should return :dunno if this right is not restricted to the given method" do
@acl.restrict_method(:destroy)
@acl.allowed?("me","127.0.0.1", { :method => :save } ).should == :dunno
end
it "should return allow/deny if this right is restricted to the given method" do
@acl.restrict_method(:save)
@acl.allow("127.0.0.1")
@acl.allowed?("me","127.0.0.1", { :method => :save }).should be_true
end
it "should return :dunno if this right is not restricted to the given environment" do
Puppet::Node::Environment.stubs(:new).returns(:production)
@acl.restrict_environment(:production)
@acl.allowed?("me","127.0.0.1", { :method => :save, :environment => :development }).should == :dunno
end
it "should return :dunno if this right is not restricted to the given request authentication state" do
@acl.restrict_authenticated(true)
@acl.allowed?("me","127.0.0.1", { :method => :save, :authenticated => false }).should == :dunno
end
it "should return allow/deny if this right is restricted to the given request authentication state" do
@acl.restrict_authenticated(false)
@acl.allow("127.0.0.1")
@acl.allowed?("me","127.0.0.1", { :authenticated => false }).should be_true
end
it "should interpolate allow/deny patterns with the given match" do
@acl.expects(:interpolate).with(:match)
@acl.allowed?("me","127.0.0.1", { :method => :save, :match => :match, :authenticated => true })
end
it "should reset interpolation after the match" do
@acl.expects(:reset_interpolation)
@acl.allowed?("me","127.0.0.1", { :method => :save, :match => :match, :authenticated => true })
end
# mocha doesn't allow testing super...
# it "should delegate to the AuthStore for the result" do
# @acl.method(:save)
#
# @acl.expects(:allowed?).with("me","127.0.0.1")
#
# @acl.allowed?("me","127.0.0.1", :save)
# end
end
end
end
diff --git a/spec/unit/node/environment_spec.rb b/spec/unit/node/environment_spec.rb
index 576e44312..5d265e00b 100755
--- a/spec/unit/node/environment_spec.rb
+++ b/spec/unit/node/environment_spec.rb
@@ -1,447 +1,466 @@
#! /usr/bin/env ruby
require 'spec_helper'
require 'tmpdir'
require 'puppet/node/environment'
require 'puppet/util/execution'
require 'puppet_spec/modules'
require 'puppet/parser/parser_factory'
describe Puppet::Node::Environment do
let(:env) { Puppet::Node::Environment.new("testing") }
include PuppetSpec::Files
after do
Puppet::Node::Environment.clear
end
shared_examples_for 'the 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 "overriding an existing environment" do
+ let(:original_path) { [tmpdir('original')] }
+ let(:new_path) { [tmpdir('new')] }
+ let(:environment) { Puppet::Node::Environment.create(:overridden, original_path, 'orig.pp') }
+
+ it "overrides modulepath" do
+ overridden = environment.override_with(:modulepath => new_path)
+ expect(overridden).to_not be_equal(environment)
+ expect(overridden.name).to eq(:overridden)
+ expect(overridden.manifest).to eq(File.expand_path('orig.pp'))
+ expect(overridden.modulepath).to eq(new_path)
+ end
+
+ it "overrides manifest" do
+ overridden = environment.override_with(:manifest => 'new.pp')
+ expect(overridden).to_not be_equal(environment)
+ expect(overridden.name).to eq(:overridden)
+ expect(overridden.manifest).to eq(File.expand_path('new.pp'))
+ expect(overridden.modulepath).to eq(original_path)
+ end
+ end
+
+ describe "watching a file" do
+ let(:filename) { "filename" }
+
+ it "accepts a File" do
+ file = tmpfile(filename)
+ env.known_resource_types.expects(:watch_file).with(file.to_s)
+ env.watch_file(file)
+ end
+
+ it "accepts a String" do
+ env.known_resource_types.expects(:watch_file).with(filename)
+ env.watch_file(filename)
+ end
+ 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(''))
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 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
env.check_for_reparse
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")
- @path_two = tmpdir("path_one")
- 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]
+ first_puppetlib = tmpdir('puppetlib1')
+ second_puppetlib = tmpdir('puppetlib2')
+ first_moduledir = tmpdir('moduledir1')
+ second_moduledir = tmpdir('moduledir2')
+ Puppet::Util.withenv("PUPPETLIB" => [first_puppetlib, second_puppetlib].join(File::PATH_SEPARATOR)) do
+ Puppet[:modulepath] = [first_moduledir, second_moduledir].join(File::PATH_SEPARATOR)
+
+ env.modulepath.should == [first_puppetlib, second_puppetlib, first_moduledir, second_moduledir]
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)
+ Puppet.settings.parse_config(<<-CONF)
+ [testing]
+ server = myval
+ CONF
+
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 = Puppet::Node::Environment.create(:testing, [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 == {
@first => [],
@second => []
}
end
it "should include modules even if they exist in multiple dirs in the modulepath" do
modpath1 = File.join(@first, "foo")
FileUtils.mkdir_p(modpath1)
modpath2 = File.join(@second, "foo")
FileUtils.mkdir_p(modpath2)
env.modules_by_path.should == {
@first => [Puppet::Module.new('foo', 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'))
FileUtils.mkdir_p(File.join(@first, 'foo-bar'))
FileUtils.mkdir_p(File.join(@first, 'foo_bar'))
FileUtils.mkdir_p(File.join(@first, 'foo=bar'))
FileUtils.mkdir_p(File.join(@first, 'foo bar'))
FileUtils.mkdir_p(File.join(@first, 'foo.bar'))
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(
'foo',
@first,
:metadata => {
:author => 'puppetlabs',
:dependencies => [{ 'name' => 'puppetlabs/bar', "version_requirement" => ">= 1.0.0" }]
}
)
PuppetSpec::Modules.create(
'bar',
@second,
:metadata => {
:author => 'puppetlabs',
:dependencies => [{ 'name' => 'puppetlabs/foo', "version_requirement" => "<= 2.0.0" }]
}
)
PuppetSpec::Modules.create(
'baz',
@first,
:metadata => {
:author => 'puppetlabs',
:dependencies => [{ 'name' => 'puppetlabs/bar', "version_requirement" => "3.0.0" }]
}
)
PuppetSpec::Modules.create(
'alpha',
@first,
:metadata => {
:author => 'puppetlabs',
:dependencies => [{ 'name' => 'puppetlabs/bar', "version_requirement" => "~3.0.0" }]
}
)
env.module_requirements.should == {
'puppetlabs/alpha' => [],
'puppetlabs/foo' => [
{
"name" => "puppetlabs/bar",
"version" => "9.9.9",
"version_requirement" => "<= 2.0.0"
}
],
'puppetlabs/bar' => [
{
"name" => "puppetlabs/alpha",
"version" => "9.9.9",
"version_requirement" => "~3.0.0"
},
{
"name" => "puppetlabs/baz",
"version" => "9.9.9",
"version_requirement" => "3.0.0"
},
{
"name" => "puppetlabs/foo",
"version" => "9.9.9",
"version_requirement" => ">= 1.0.0"
}
],
'puppetlabs/baz' => []
}
end
end
describe ".module_by_forge_name" do
it "should find modules by forge_name" do
mod = PuppetSpec::Modules.create(
'baz',
@first,
:metadata => {:author => 'puppetlabs'},
:environment => env
)
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',
@first,
:metadata => {:author => 'sneakylabs'},
:environment => env
)
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))
end
%w{bee baz}.each do |mod_name|
FileUtils.mkdir_p(File.join(@second, mod_name))
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'))
FileUtils.mkdir_p(File.join(@first, 'foo-bar'))
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
+ describe "when performing initial import" do
+ def parser_and_environment(name)
+ env = Puppet::Node::Environment.new(name)
+ parser = Puppet::Parser::ParserFactory.parser(env)
+ Puppet::Parser::ParserFactory.stubs(:parser).returns(parser)
- it "should accept an environment directly" do
- @helper.environment = Puppet::Node::Environment.new(:foo)
- @helper.environment.name.should == :foo
+ [parser, env]
end
- it "should accept an environment as a string" do
- @helper.environment = 'foo'
- @helper.environment.name.should == :foo
- end
- end
+ it "should set the parser's string to the 'code' setting and parse if code is available" do
+ Puppet[:code] = "my code"
+ parser, env = parser_and_environment('testing')
- 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
+ parser.expects(:string=).with "my code"
+ parser.expects(:parse)
- 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| }
- Puppet.settings[:manifest] = filename
- @parser.expects(:file=).with filename
- @parser.expects(:parse)
+ Puppet[:manifest] = filename
+ parser, env = parser_and_environment('testing')
+
+ parser.expects(:file=).with filename
+ 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] = ""
- Puppet.settings[:manifest] = filename
- @parser.expects(:file=).with(filename).once
- @parser.expects(:parse).once
+ Puppet[:code] = ""
+ Puppet[:manifest] = filename
+ parser, env = parser_and_environment('testing')
+
+ parser.expects(:file=).with(filename).once
+ parser.expects(:parse).once
+
env.instance_eval { perform_initial_import }
end
it "should fail helpfully if there is an error importing" do
- Puppet::FileSystem::File.stubs(:exist?).returns true
- @parser.expects(:file=).once
- @parser.expects(:parse).raises ArgumentError
- lambda { env.known_resource_types }.should raise_error(Puppet::Error)
+ Puppet::FileSystem.stubs(:exist?).returns true
+ parser, env = parser_and_environment('testing')
+
+ parser.expects(:file=).once
+ parser.expects(:parse).raises ArgumentError
+
+ expect do
+ env.known_resource_types
+ end.to 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
- @parser.expects(:file=).never
- @parser.expects(:parse).never
+ Puppet[:ignoreimport] = true
+ parser, env = parser_and_environment('testing')
+
+ parser.expects(:string=).never
+ parser.expects(:file=).never
+ 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 ...")
+ parser, env = parser_and_environment('testing')
- lambda { env.known_resource_types }.should raise_error(Puppet::Error, /Syntax error at .../)
+ parser.expects(:parse).raises Puppet::ParseError.new("Syntax error at ...")
+
+ expect do
+ env.known_resource_types
+ end.to raise_error(Puppet::Error, /Syntax error at .../)
env.known_resource_types.require_reparse?.should be_true
end
end
end
+
describe 'with classic parser' do
before :each do
Puppet[:parser] = 'current'
end
it_behaves_like 'the environment'
end
+
describe 'with future parser' do
before :each do
Puppet[:parser] = 'future'
end
it_behaves_like 'the environment'
end
+ describe '#current' do
+ it 'should return the current context' do
+ env = Puppet::Node::Environment.new(:test)
+ Puppet::Context.any_instance.expects(:lookup).with(:current_environment).returns(env)
+ Puppet.expects(:deprecation_warning).once
+ Puppet::Node::Environment.current.should equal(env)
+ end
+ end
+
end
diff --git a/spec/unit/node/facts_spec.rb b/spec/unit/node/facts_spec.rb
index 55e3dbbf6..f4b50b37e 100755
--- a/spec/unit/node/facts_spec.rb
+++ b/spec/unit/node/facts_spec.rb
@@ -1,182 +1,175 @@
#! /usr/bin/env ruby
require 'spec_helper'
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
+require 'matchers/json'
describe Puppet::Node::Facts, "when indirecting" do
+ include JSONMatchers
+
before do
@facts = Puppet::Node::Facts.new("me")
end
it "should be able to convert all fact values to strings" do
@facts.values["one"] = 1
@facts.stringify
@facts.values["one"].should == "1"
end
describe "adding local facts" do
it "should add the node's certificate name as the 'clientcert' fact" do
@facts.add_local_facts
@facts.values["clientcert"].should == Puppet.settings[:certname]
end
it "adds the Puppet version as a 'clientversion' fact" do
@facts.add_local_facts
@facts.values["clientversion"].should == Puppet.version.to_s
end
it "adds the agent side noop setting as 'clientnoop'" do
@facts.add_local_facts
@facts.values["clientnoop"].should == Puppet.settings[:noop]
end
it "doesn't add the current environment" do
@facts.add_local_facts
@facts.values.should_not include("environment")
end
it "doesn't replace any existing environment fact when adding local facts" do
@facts.values["environment"] = "foo"
@facts.add_local_facts
@facts.values["environment"].should == "foo"
end
end
describe "when sanitizing facts" do
it "should convert fact values if needed" do
@facts.values["test"] = /foo/
@facts.sanitize
@facts.values["test"].should == "(?-mix:foo)"
end
it "should convert hash keys if needed" do
@facts.values["test"] = {/foo/ => "bar"}
@facts.sanitize
@facts.values["test"].should == {"(?-mix:foo)" => "bar"}
end
it "should convert hash values if needed" do
@facts.values["test"] = {"foo" => /bar/}
@facts.sanitize
@facts.values["test"].should == {"foo" => "(?-mix:bar)"}
end
it "should convert array elements if needed" do
@facts.values["test"] = [1, "foo", /bar/]
@facts.sanitize
@facts.values["test"].should == [1, "foo", "(?-mix:bar)"]
end
it "should handle nested arrays" do
@facts.values["test"] = [1, "foo", [/bar/]]
@facts.sanitize
@facts.values["test"].should == [1, "foo", ["(?-mix:bar)"]]
end
it "should handle nested hashes" do
@facts.values["test"] = {/foo/ => {"bar" => /baz/}}
@facts.sanitize
@facts.values["test"].should == {"(?-mix:foo)" => {"bar" => "(?-mix:baz)"}}
end
it "should handle nester arrays and hashes" do
@facts.values["test"] = {/foo/ => ["bar", /baz/]}
@facts.sanitize
@facts.values["test"].should == {"(?-mix:foo)" => ["bar", "(?-mix:baz)"]}
end
end
describe "when indirecting" do
before do
@indirection = stub 'indirection', :request => mock('request'), :name => :facts
@facts = Puppet::Node::Facts.new("me", "one" => "two")
end
it "should redirect to the specified fact store for storage" do
Puppet::Node::Facts.stubs(:indirection).returns(@indirection)
@indirection.expects(:save)
Puppet::Node::Facts.indirection.save(@facts)
end
describe "when the Puppet application is 'master'" do
it "should default to the 'yaml' terminus" do
pending "Cannot test the behavior of defaults in defaults.rb"
# Puppet::Node::Facts.indirection.terminus_class.should == :yaml
end
end
describe "when the Puppet application is not 'master'" do
it "should default to the 'facter' terminus" do
pending "Cannot test the behavior of defaults in defaults.rb"
# Puppet::Node::Facts.indirection.terminus_class.should == :facter
end
end
end
describe "when storing and retrieving" do
it "should add metadata to the facts" do
facts = Puppet::Node::Facts.new("me", "one" => "two", "three" => "four")
facts.values['_timestamp'].should be_instance_of(Time)
end
describe "using pson" do
before :each do
@timestamp = Time.parse("Thu Oct 28 11:16:31 -0700 2010")
@expiration = Time.parse("Thu Oct 28 11:21:31 -0700 2010")
end
it "should accept properly formatted pson" do
pson = %Q({"name": "foo", "expiration": "#{@expiration}", "timestamp": "#{@timestamp}", "values": {"a": "1", "b": "2", "c": "3"}})
format = Puppet::Network::FormatHandler.format('pson')
facts = format.intern(Puppet::Node::Facts,pson)
facts.name.should == 'foo'
facts.expiration.should == @expiration
facts.values.should == {'a' => '1', 'b' => '2', 'c' => '3', '_timestamp' => @timestamp}
end
it "should generate properly formatted pson" do
Time.stubs(:now).returns(@timestamp)
facts = Puppet::Node::Facts.new("foo", {'a' => 1, 'b' => 2, 'c' => 3})
facts.expiration = @expiration
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.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
+ it "should generate valid facts data against the facts schema" 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)
+ expect(facts.to_pson).to validate_against('api/schemas/facts.json')
end
it "should not include nil values" do
facts = Puppet::Node::Facts.new("foo", {'a' => 1, 'b' => 2, 'c' => 3})
pson = PSON.parse(facts.to_pson)
pson.should_not be_include("expiration")
end
it "should be able to handle nil values" do
pson = %Q({"name": "foo", "values": {"a": "1", "b": "2", "c": "3"}})
format = Puppet::Network::FormatHandler.format('pson')
facts = format.intern(Puppet::Node::Facts,pson)
facts.name.should == 'foo'
facts.expiration.should be_nil
end
end
end
end
diff --git a/spec/unit/node_spec.rb b/spec/unit/node_spec.rb
index f8691ed9d..11f24729e 100755
--- a/spec/unit/node_spec.rb
+++ b/spec/unit/node_spec.rb
@@ -1,323 +1,323 @@
#! /usr/bin/env ruby
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 Puppet::Node do
+ include JSONMatchers
- describe "node schema" do
- it "should validate against the json meta-schema" do
- JSON::Validator.validate!(JSON_META_SCHEMA, NODE_SCHEMA)
- end
- end
-end
+ let(:environment) { Puppet::Node::Environment.create(:bar, [], '') }
+ let(:env_loader) { Puppet::Environments::Static.new(environment) }
-describe Puppet::Node do
it "should register its document type as Node" do
PSON.registered_document_types["Node"].should equal(Puppet::Node)
end
describe "when managing its environment" do
it "should use any set environment" do
- Puppet::Node.new("foo", :environment => "bar").environment.name.should == :bar
+ Puppet.override(:environments => env_loader) do
+ Puppet::Node.new("foo", :environment => "bar").environment.should == environment
+ end
end
it "should support providing an actual environment instance" do
- Puppet::Node.new("foo", :environment => Puppet::Node::Environment.new(:bar)).environment.name.should == :bar
+ Puppet::Node.new("foo", :environment => environment).environment.name.should == :bar
end
it "should determine its environment from its parameters if no environment is set" do
- Puppet::Node.new("foo", :parameters => {"environment" => :bar}).environment.name.should == :bar
+ Puppet.override(:environments => env_loader) do
+ Puppet::Node.new("foo", :parameters => {"environment" => :bar}).environment.should == environment
+ end
end
- it "should use the default environment if no environment is provided" do
- Puppet::Node.new("foo").environment.name.should == Puppet::Node::Environment.new.name
- end
+ it "should use the configured environment if no environment is provided" do
+ Puppet[:environment] = environment.name.to_s
- it "should always return an environment instance rather than a string" do
- Puppet::Node.new("foo").environment.should be_instance_of(Puppet::Node::Environment)
+ Puppet.override(:environments => env_loader) do
+ Puppet::Node.new("foo").environment.should == environment
+ end
end
it "should allow the environment to be set after initialization" do
node = Puppet::Node.new("foo")
node.environment = :bar
node.environment.name.should == :bar
end
it "should allow its environment to be set by parameters after initialization" do
node = Puppet::Node.new("foo")
node.parameters["environment"] = :bar
node.environment.name.should == :bar
end
end
it "can survive a round-trip through YAML" 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('yaml', node.render('yaml'))
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 "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)
+ expect(node.to_pson).to validate_against('api/schemas/node.json')
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)
+ expect(node.to_pson).to validate_against('api/schemas/node.json')
end
describe "when converting to json" do
before do
@node = Puppet::Node.new("mynode")
end
it "should provide its name" do
@node.should set_json_attribute('name').to("mynode")
end
it "should produce a hash with the document_type set to 'Node'" do
@node.should set_json_document_type_to("Node")
end
it "should include the classes if set" do
@node.classes = %w{a b c}
@node.should set_json_attribute("classes").to(%w{a b c})
end
it "should not include the classes if there are none" do
@node.should_not set_json_attribute('classes')
end
it "should include parameters if set" do
@node.parameters = {"a" => "b", "c" => "d"}
@node.should set_json_attribute('parameters').to({"a" => "b", "c" => "d"})
end
it "should not include the parameters if there are none" do
@node.should_not set_json_attribute('parameters')
end
it "should include the environment" do
@node.environment = "production"
@node.should set_json_attribute('environment').to('production')
end
end
describe "when converting from json" do
before do
@node = Puppet::Node.new("mynode")
@format = Puppet::Network::FormatHandler.format('pson')
end
def from_json(json)
@format.intern(Puppet::Node, json)
end
it "should set its name" do
Puppet::Node.should read_json_attribute('name').from(@node.to_pson).as("mynode")
end
it "should include the classes if set" do
@node.classes = %w{a b c}
Puppet::Node.should read_json_attribute('classes').from(@node.to_pson).as(%w{a b c})
end
it "should include parameters if set" do
@node.parameters = {"a" => "b", "c" => "d"}
Puppet::Node.should read_json_attribute('parameters').from(@node.to_pson).as({"a" => "b", "c" => "d"})
end
it "should include the environment" do
- @node.environment = "production"
- Puppet::Node.should read_json_attribute('environment').from(@node.to_pson).as(Puppet::Node::Environment.new(:production))
+ Puppet.override(:environments => env_loader) do
+ @node.environment = environment
+ Puppet::Node.should read_json_attribute('environment').from(@node.to_pson).as(environment)
+ end
end
end
end
describe Puppet::Node, "when initializing" do
before do
@node = Puppet::Node.new("testnode")
end
it "should set the node name" do
@node.name.should == "testnode"
end
it "should not allow nil node names" do
proc { Puppet::Node.new(nil) }.should raise_error(ArgumentError)
end
it "should default to an empty parameter hash" do
@node.parameters.should == {}
end
it "should default to an empty class array" do
@node.classes.should == []
end
it "should note its creation time" do
@node.time.should be_instance_of(Time)
end
it "should accept parameters passed in during initialization" do
params = {"a" => "b"}
@node = Puppet::Node.new("testing", :parameters => params)
@node.parameters.should == params
end
it "should accept classes passed in during initialization" do
classes = %w{one two}
@node = Puppet::Node.new("testing", :classes => classes)
@node.classes.should == classes
end
it "should always return classes as an array" do
@node = Puppet::Node.new("testing", :classes => "myclass")
@node.classes.should == ["myclass"]
end
end
describe Puppet::Node, "when merging facts" do
before do
@node = Puppet::Node.new("testnode")
Puppet::Node::Facts.indirection.stubs(:find).with(@node.name, instance_of(Hash)).returns(Puppet::Node::Facts.new(@node.name, "one" => "c", "two" => "b"))
end
it "should fail intelligently if it cannot find facts" do
Puppet::Node::Facts.indirection.expects(:find).with(@node.name, instance_of(Hash)).raises "foo"
lambda { @node.fact_merge }.should raise_error(Puppet::Error)
end
it "should prefer parameters already set on the node over facts from the node" do
@node = Puppet::Node.new("testnode", :parameters => {"one" => "a"})
@node.fact_merge
@node.parameters["one"].should == "a"
end
it "should add passed parameters to the parameter list" do
@node = Puppet::Node.new("testnode", :parameters => {"one" => "a"})
@node.fact_merge
@node.parameters["two"].should == "b"
end
it "should accept arbitrary parameters to merge into its parameters" do
@node = Puppet::Node.new("testnode", :parameters => {"one" => "a"})
@node.merge "two" => "three"
@node.parameters["two"].should == "three"
end
it "should add the environment to the list of parameters" do
Puppet[:environment] = "one"
@node = Puppet::Node.new("testnode", :environment => "one")
@node.merge "two" => "three"
@node.parameters["environment"].should == "one"
end
it "should not set the environment if it is already set in the parameters" do
Puppet[:environment] = "one"
@node = Puppet::Node.new("testnode", :environment => "one")
@node.merge "environment" => "two"
@node.parameters["environment"].should == "two"
end
end
describe Puppet::Node, "when indirecting" do
it "should default to the 'plain' node terminus" do
Puppet::Node.indirection.reset_terminus_class
Puppet::Node.indirection.terminus_class.should == :plain
end
end
describe Puppet::Node, "when generating the list of names to search through" do
before do
@node = Puppet::Node.new("foo.domain.com", :parameters => {"hostname" => "yay", "domain" => "domain.com"})
end
it "should return an array of names" do
@node.names.should be_instance_of(Array)
end
describe "and the node name is fully qualified" do
it "should contain an entry for each part of the node name" do
@node.names.should be_include("foo.domain.com")
@node.names.should be_include("foo.domain")
@node.names.should be_include("foo")
end
end
it "should include the node's fqdn" do
@node.names.should be_include("yay.domain.com")
end
it "should combine and include the node's hostname and domain if no fqdn is available" do
@node.names.should be_include("yay.domain.com")
end
it "should contain an entry for each name available by stripping a segment of the fqdn" do
@node.parameters["fqdn"] = "foo.deep.sub.domain.com"
@node.names.should be_include("foo.deep.sub.domain")
@node.names.should be_include("foo.deep.sub")
end
describe "and :node_name is set to 'cert'" do
before do
Puppet[:strict_hostname_checking] = false
Puppet[:node_name] = "cert"
end
it "should use the passed-in key as the first value" do
@node.names[0].should == "foo.domain.com"
end
describe "and strict hostname checking is enabled" do
it "should only use the passed-in key" do
Puppet[:strict_hostname_checking] = true
@node.names.should == ["foo.domain.com"]
end
end
end
describe "and :node_name is set to 'facter'" do
before do
Puppet[:strict_hostname_checking] = false
Puppet[:node_name] = "facter"
end
it "should use the node's 'hostname' fact as the first value" do
@node.names[0].should == "yay"
end
end
end
diff --git a/spec/unit/parser/ast/collection_spec.rb b/spec/unit/parser/ast/collection_spec.rb
index bd4f5f82f..a5e40b2c3 100755
--- a/spec/unit/parser/ast/collection_spec.rb
+++ b/spec/unit/parser/ast/collection_spec.rb
@@ -1,70 +1,70 @@
#! /usr/bin/env ruby
require 'spec_helper'
describe Puppet::Parser::AST::Collection do
before :each do
@mytype = Puppet::Resource::Type.new(:definition, "mytype")
- @environment = Puppet::Node::Environment.new
+ @environment = Puppet::Node::Environment.create(:testing, [], '')
@environment.known_resource_types.add @mytype
@compiler = Puppet::Parser::Compiler.new(Puppet::Node.new("foonode", :environment => @environment))
@scope = Puppet::Parser::Scope.new(@compiler)
@overrides = stub_everything 'overrides'
@overrides.stubs(:is_a?).with(Puppet::Parser::AST).returns(true)
end
it "should evaluate its query" do
query = mock 'query'
collection = Puppet::Parser::AST::Collection.new :query => query, :form => :virtual
collection.type = 'mytype'
query.expects(:safeevaluate).with(@scope)
collection.evaluate(@scope)
end
it "should instantiate a Collector for this type" do
collection = Puppet::Parser::AST::Collection.new :form => :virtual, :type => "test"
@test_type = Puppet::Resource::Type.new(:definition, "test")
@environment.known_resource_types.add @test_type
Puppet::Parser::Collector.expects(:new).with(@scope, "test", nil, nil, :virtual)
collection.evaluate(@scope)
end
it "should tell the compiler about this collector" do
collection = Puppet::Parser::AST::Collection.new :form => :virtual, :type => "mytype"
Puppet::Parser::Collector.stubs(:new).returns("whatever")
@compiler.expects(:add_collection).with("whatever")
collection.evaluate(@scope)
end
it "should evaluate overriden paramaters" do
collector = stub_everything 'collector'
collection = Puppet::Parser::AST::Collection.new :form => :virtual, :type => "mytype", :override => @overrides
Puppet::Parser::Collector.stubs(:new).returns(collector)
@overrides.expects(:safeevaluate).with(@scope)
collection.evaluate(@scope)
end
it "should tell the collector about overrides" do
collector = mock 'collector'
collection = Puppet::Parser::AST::Collection.new :form => :virtual, :type => "mytype", :override => @overrides
Puppet::Parser::Collector.stubs(:new).returns(collector)
collector.expects(:add_override)
collection.evaluate(@scope)
end
it "should fail when evaluating undefined resource types" do
collection = Puppet::Parser::AST::Collection.new :form => :virtual, :type => "bogus"
lambda { collection.evaluate(@scope) }.should raise_error "Resource type bogus doesn't exist"
end
end
diff --git a/spec/unit/parser/ast/leaf_spec.rb b/spec/unit/parser/ast/leaf_spec.rb
index bee9fc2bd..9cbbbf978 100755
--- a/spec/unit/parser/ast/leaf_spec.rb
+++ b/spec/unit/parser/ast/leaf_spec.rb
@@ -1,509 +1,511 @@
#! /usr/bin/env ruby
require 'spec_helper'
describe Puppet::Parser::AST::Leaf do
before :each do
node = Puppet::Node.new('localhost')
compiler = Puppet::Parser::Compiler.new(node)
@scope = Puppet::Parser::Scope.new(compiler)
@value = stub 'value'
@leaf = Puppet::Parser::AST::Leaf.new(:value => @value)
end
it "should have an evaluate_match method" do
Puppet::Parser::AST::Leaf.new(:value => "value").should respond_to(:evaluate_match)
end
describe "when converting to string" do
it "should transform its value to string" do
value = stub 'value', :is_a? => true
value.expects(:to_s)
Puppet::Parser::AST::Leaf.new( :value => value ).to_s
end
end
it "should have a match method" do
@leaf.should respond_to(:match)
end
it "should delegate match to ==" do
@value.expects(:==).with("value")
@leaf.match("value")
end
end
describe Puppet::Parser::AST::FlatString do
describe "when converting to string" do
it "should transform its value to a quoted string" do
Puppet::Parser::AST::FlatString.new(:value => 'ab').to_s.should == "\"ab\""
end
it "should escape embedded double-quotes" do
value = Puppet::Parser::AST::FlatString.new(:value => 'hello "friend"')
value.to_s.should == "\"hello \\\"friend\\\"\""
end
end
end
describe Puppet::Parser::AST::String do
describe "when converting to string" do
it "should transform its value to a quoted string" do
Puppet::Parser::AST::String.new(:value => 'ab').to_s.should == "\"ab\""
end
it "should escape embedded double-quotes" do
value = Puppet::Parser::AST::String.new(:value => 'hello "friend"')
value.to_s.should == "\"hello \\\"friend\\\"\""
end
it "should return a dup of its value" do
value = ""
Puppet::Parser::AST::String.new( :value => value ).evaluate(stub('scope')).should_not be_equal(value)
end
end
end
describe Puppet::Parser::AST::Concat do
describe "when evaluating" do
before :each do
node = Puppet::Node.new('localhost')
compiler = Puppet::Parser::Compiler.new(node)
@scope = Puppet::Parser::Scope.new(compiler)
end
it "should interpolate variables and concatenate their values" do
one = Puppet::Parser::AST::String.new(:value => "one")
one.stubs(:evaluate).returns("one ")
two = Puppet::Parser::AST::String.new(:value => "two")
two.stubs(:evaluate).returns(" two ")
three = Puppet::Parser::AST::String.new(:value => "three")
three.stubs(:evaluate).returns(" three")
var = Puppet::Parser::AST::Variable.new(:value => "myvar")
var.stubs(:evaluate).returns("foo")
array = Puppet::Parser::AST::Variable.new(:value => "array")
array.stubs(:evaluate).returns(["bar","baz"])
concat = Puppet::Parser::AST::Concat.new(:value => [one,var,two,array,three])
concat.evaluate(@scope).should == 'one foo two barbaz three'
end
it "should transform undef variables to empty string" do
var = Puppet::Parser::AST::Variable.new(:value => "myvar")
var.stubs(:evaluate).returns(:undef)
concat = Puppet::Parser::AST::Concat.new(:value => [var])
concat.evaluate(@scope).should == ''
end
end
end
describe Puppet::Parser::AST::Undef do
before :each do
node = Puppet::Node.new('localhost')
compiler = Puppet::Parser::Compiler.new(node)
@scope = Puppet::Parser::Scope.new(compiler)
@undef = Puppet::Parser::AST::Undef.new(:value => :undef)
end
it "should match undef with undef" do
@undef.evaluate_match(:undef, @scope).should be_true
end
it "should not match undef with an empty string" do
@undef.evaluate_match("", @scope).should be_true
end
end
describe Puppet::Parser::AST::HashOrArrayAccess do
before :each do
node = Puppet::Node.new('localhost')
compiler = Puppet::Parser::Compiler.new(node)
@scope = Puppet::Parser::Scope.new(compiler)
end
describe "when evaluating" do
it "should evaluate the variable part if necessary" do
@scope["a"] = ["b"]
variable = stub 'variable', :evaluate => "a"
access = Puppet::Parser::AST::HashOrArrayAccess.new(:variable => variable, :key => 0 )
variable.expects(:safeevaluate).with(@scope).returns("a")
access.evaluate(@scope).should == "b"
end
it "should evaluate the access key part if necessary" do
@scope["a"] = ["b"]
index = stub 'index', :evaluate => 0
access = Puppet::Parser::AST::HashOrArrayAccess.new(:variable => "a", :key => index )
index.expects(:safeevaluate).with(@scope).returns(0)
access.evaluate(@scope).should == "b"
end
it "should be able to return an array member" do
@scope["a"] = %w{val1 val2 val3}
access = Puppet::Parser::AST::HashOrArrayAccess.new(:variable => "a", :key => 1 )
access.evaluate(@scope).should == "val2"
end
it "should be able to return an array member when index is a stringified number" do
@scope["a"] = %w{val1 val2 val3}
access = Puppet::Parser::AST::HashOrArrayAccess.new(:variable => "a", :key => "1" )
access.evaluate(@scope).should == "val2"
end
it "should raise an error when accessing an array with a key" do
@scope["a"] = ["val1", "val2", "val3"]
access = Puppet::Parser::AST::HashOrArrayAccess.new(:variable => "a", :key => "get_me_the_second_element_please" )
lambda { access.evaluate(@scope) }.should raise_error
end
it "should be able to return :undef for an unknown array index" do
@scope["a"] = ["val1", "val2", "val3"]
access = Puppet::Parser::AST::HashOrArrayAccess.new(:variable => "a", :key => 6 )
access.evaluate(@scope).should == :undef
end
it "should be able to return a hash value" do
@scope["a"] = { "key1" => "val1", "key2" => "val2", "key3" => "val3" }
access = Puppet::Parser::AST::HashOrArrayAccess.new(:variable => "a", :key => "key2" )
access.evaluate(@scope).should == "val2"
end
it "should be able to return :undef for unknown hash keys" do
@scope["a"] = { "key1" => "val1", "key2" => "val2", "key3" => "val3" }
access = Puppet::Parser::AST::HashOrArrayAccess.new(:variable => "a", :key => "key12" )
access.evaluate(@scope).should == :undef
end
it "should be able to return a hash value with a numerical key" do
@scope["a"] = { "key1" => "val1", "key2" => "val2", "45" => "45", "key3" => "val3" }
access = Puppet::Parser::AST::HashOrArrayAccess.new(:variable => "a", :key => "45" )
access.evaluate(@scope).should == "45"
end
it "should raise an error if the variable lookup didn't return a hash or an array" do
@scope["a"] = "I'm a string"
access = Puppet::Parser::AST::HashOrArrayAccess.new(:variable => "a", :key => "key2" )
lambda { access.evaluate(@scope) }.should raise_error
end
it "should raise an error if the variable wasn't in the scope" do
access = Puppet::Parser::AST::HashOrArrayAccess.new(:variable => "a", :key => "key2" )
lambda { access.evaluate(@scope) }.should raise_error
end
it "should return a correct string representation" do
access = Puppet::Parser::AST::HashOrArrayAccess.new(:variable => "a", :key => "key2" )
access.to_s.should == '$a[key2]'
end
it "should work with recursive hash access" do
@scope["a"] = { "key" => { "subkey" => "b" }}
access1 = Puppet::Parser::AST::HashOrArrayAccess.new(:variable => "a", :key => "key")
access2 = Puppet::Parser::AST::HashOrArrayAccess.new(:variable => access1, :key => "subkey")
access2.evaluate(@scope).should == 'b'
end
it "should work with interleaved array and hash access" do
@scope['a'] = { "key" => [ "a" , "b" ]}
access1 = Puppet::Parser::AST::HashOrArrayAccess.new(:variable => "a", :key => "key")
access2 = Puppet::Parser::AST::HashOrArrayAccess.new(:variable => access1, :key => 1)
access2.evaluate(@scope).should == 'b'
end
it "should raise a useful error for hash access on undef" do
@scope["a"] = :undef
access = Puppet::Parser::AST::HashOrArrayAccess.new(:variable => "a", :key => "key")
expect {
access.evaluate(@scope)
}.to raise_error(Puppet::ParseError, /not a hash or array/)
end
it "should raise a useful error for hash access on TrueClass" do
@scope["a"] = true
access = Puppet::Parser::AST::HashOrArrayAccess.new(:variable => "a", :key => "key")
expect {
access.evaluate(@scope)
}.to raise_error(Puppet::ParseError, /not a hash or array/)
end
it "should raise a useful error for recursive undef hash access" do
@scope["a"] = { "key" => "val" }
access1 = Puppet::Parser::AST::HashOrArrayAccess.new(:variable => "a", :key => "nonexistent")
access2 = Puppet::Parser::AST::HashOrArrayAccess.new(:variable => access1, :key => "subkey")
expect {
access2.evaluate(@scope)
}.to raise_error(Puppet::ParseError, /not a hash or array/)
end
it "should produce boolean values when value is a boolean" do
@scope["a"] = [true, false]
access = Puppet::Parser::AST::HashOrArrayAccess.new(:variable => "a", :key => 0 )
expect(access.evaluate(@scope)).to be == true
access = Puppet::Parser::AST::HashOrArrayAccess.new(:variable => "a", :key => 1 )
expect(access.evaluate(@scope)).to be == false
end
end
describe "when assigning" do
it "should add a new key and value" do
+ Puppet.expects(:warning).once
node = Puppet::Node.new('localhost')
compiler = Puppet::Parser::Compiler.new(node)
scope = Puppet::Parser::Scope.new(compiler)
scope['a'] = { 'a' => 'b' }
access = Puppet::Parser::AST::HashOrArrayAccess.new(:variable => "a", :key => "b")
access.assign(scope, "c" )
scope['a'].should be_include("b")
end
it "should raise an error when assigning an array element with a key" do
@scope['a'] = []
access = Puppet::Parser::AST::HashOrArrayAccess.new(:variable => "a", :key => "get_me_the_second_element_please" )
lambda { access.assign(@scope, "test") }.should raise_error
end
it "should be able to return an array member when index is a stringified number" do
+ Puppet.expects(:warning).once
node = Puppet::Node.new('localhost')
compiler = Puppet::Parser::Compiler.new(node)
scope = Puppet::Parser::Scope.new(compiler)
scope['a'] = []
access = Puppet::Parser::AST::HashOrArrayAccess.new(:variable => "a", :key => "0" )
access.assign(scope, "val2")
scope['a'].should == ["val2"]
end
it "should raise an error when trying to overwrite a hash value" do
@scope['a'] = { "key" => [ "a" , "b" ]}
access = Puppet::Parser::AST::HashOrArrayAccess.new(:variable => "a", :key => "key")
lambda { access.assign(@scope, "test") }.should raise_error
end
end
end
describe Puppet::Parser::AST::Regex do
before :each do
node = Puppet::Node.new('localhost')
compiler = Puppet::Parser::Compiler.new(node)
@scope = Puppet::Parser::Scope.new(compiler)
end
describe "when initializing" do
it "should create a Regexp with its content when value is not a Regexp" do
Regexp.expects(:new).with("/ab/")
Puppet::Parser::AST::Regex.new :value => "/ab/"
end
it "should not create a Regexp with its content when value is a Regexp" do
value = Regexp.new("/ab/")
Regexp.expects(:new).with("/ab/").never
Puppet::Parser::AST::Regex.new :value => value
end
end
describe "when evaluating" do
it "should return self" do
val = Puppet::Parser::AST::Regex.new :value => "/ab/"
val.evaluate(@scope).should === val
end
end
describe "when evaluate_match" do
before :each do
@value = stub 'regex'
@value.stubs(:match).with("value").returns(true)
Regexp.stubs(:new).returns(@value)
@regex = Puppet::Parser::AST::Regex.new :value => "/ab/"
end
it "should issue the regexp match" do
@value.expects(:match).with("value")
@regex.evaluate_match("value", @scope)
end
it "should not downcase the paramater value" do
@value.expects(:match).with("VaLuE")
@regex.evaluate_match("VaLuE", @scope)
end
it "should set ephemeral scope vars if there is a match" do
@scope.expects(:ephemeral_from).with(true, nil, nil)
@regex.evaluate_match("value", @scope)
end
it "should return the match to the caller" do
@value.stubs(:match).with("value").returns(:match)
@scope.stubs(:ephemeral_from)
@regex.evaluate_match("value", @scope)
end
end
it "should match undef to the empty string" do
regex = Puppet::Parser::AST::Regex.new(:value => "^$")
regex.evaluate_match(:undef, @scope).should be_true
end
it "should not match undef to a non-empty string" do
regex = Puppet::Parser::AST::Regex.new(:value => '\w')
regex.evaluate_match(:undef, @scope).should be_false
end
it "should match a string against a string" do
regex = Puppet::Parser::AST::Regex.new(:value => '\w')
regex.evaluate_match('foo', @scope).should be_true
end
it "should return the regex source with to_s" do
regex = stub 'regex'
Regexp.stubs(:new).returns(regex)
val = Puppet::Parser::AST::Regex.new :value => "/ab/"
regex.expects(:source)
val.to_s
end
it "should delegate match to the underlying regexp match method" do
regex = Regexp.new("/ab/")
val = Puppet::Parser::AST::Regex.new :value => regex
regex.expects(:match).with("value")
val.match("value")
end
end
describe Puppet::Parser::AST::Variable do
before :each do
node = Puppet::Node.new('localhost')
compiler = Puppet::Parser::Compiler.new(node)
@scope = Puppet::Parser::Scope.new(compiler)
@var = Puppet::Parser::AST::Variable.new(:value => "myvar", :file => 'my.pp', :line => 222)
end
it "should lookup the variable in scope" do
@scope["myvar"] = :myvalue
@var.safeevaluate(@scope).should == :myvalue
end
it "should pass the source location to lookupvar" do
@scope.setvar("myvar", :myvalue, :file => 'my.pp', :line => 222 )
@var.safeevaluate(@scope).should == :myvalue
end
it "should return undef if the variable wasn't set" do
@var.safeevaluate(@scope).should == :undef
end
describe "when converting to string" do
it "should transform its value to a variable" do
value = stub 'value', :is_a? => true, :to_s => "myvar"
Puppet::Parser::AST::Variable.new( :value => value ).to_s.should == "\$myvar"
end
end
end
describe Puppet::Parser::AST::HostName do
before :each do
node = Puppet::Node.new('localhost')
compiler = Puppet::Parser::Compiler.new(node)
@scope = Puppet::Parser::Scope.new(compiler)
@value = 'value'
@value.stubs(:to_s).returns(@value)
@value.stubs(:downcase).returns(@value)
@host = Puppet::Parser::AST::HostName.new(:value => @value)
end
it "should raise an error if hostname is not valid" do
lambda { Puppet::Parser::AST::HostName.new( :value => "not a hostname!" ) }.should raise_error
end
it "should not raise an error if hostname is a regex" do
lambda { Puppet::Parser::AST::HostName.new( :value => Puppet::Parser::AST::Regex.new(:value => "/test/") ) }.should_not raise_error
end
it "should stringify the value" do
value = stub 'value', :=~ => false
value.expects(:to_s).returns("test")
Puppet::Parser::AST::HostName.new(:value => value)
end
it "should downcase the value" do
value = stub 'value', :=~ => false
value.stubs(:to_s).returns("UPCASED")
host = Puppet::Parser::AST::HostName.new(:value => value)
host.value == "upcased"
end
it "should evaluate to its value" do
@host.evaluate(@scope).should == @value
end
it "should delegate eql? to the underlying value if it is an HostName" do
@value.expects(:eql?).with("value")
@host.eql?("value")
end
it "should delegate eql? to the underlying value if it is not an HostName" do
value = stub 'compared', :is_a? => true, :value => "value"
@value.expects(:eql?).with("value")
@host.eql?(value)
end
it "should delegate hash to the underlying value" do
@value.expects(:hash)
@host.hash
end
end
diff --git a/spec/unit/parser/ast/resource_spec.rb b/spec/unit/parser/ast/resource_spec.rb
index abf815c39..00aa263ff 100755
--- a/spec/unit/parser/ast/resource_spec.rb
+++ b/spec/unit/parser/ast/resource_spec.rb
@@ -1,183 +1,183 @@
#! /usr/bin/env ruby
require 'spec_helper'
describe Puppet::Parser::AST::Resource do
ast = Puppet::Parser::AST
describe "for builtin types" do
before :each do
@title = Puppet::Parser::AST::String.new(:value => "mytitle")
@compiler = Puppet::Parser::Compiler.new(Puppet::Node.new("mynode"))
@scope = Puppet::Parser::Scope.new(@compiler)
@scope.stubs(:resource).returns(stub_everything)
@instance = ast::ResourceInstance.new(:title => @title, :parameters => ast::ASTArray.new(:children => []))
@resource = ast::Resource.new(:type => "file", :instances => ast::ASTArray.new(:children => [@instance]))
@resource.stubs(:qualified_type).returns("Resource")
end
it "should evaluate all its parameters" do
param = stub 'param'
param.expects(:safeevaluate).with(@scope).returns Puppet::Parser::Resource::Param.new(:name => "myparam", :value => "myvalue", :source => stub("source"))
@instance.stubs(:parameters).returns [param]
@resource.evaluate(@scope)
end
it "should evaluate its title" do
@resource.evaluate(@scope)[0].title.should == "mytitle"
end
it "should flatten the titles array" do
titles = []
%w{one two}.each do |title|
titles << Puppet::Parser::AST::String.new(:value => title)
end
array = Puppet::Parser::AST::ASTArray.new(:children => titles)
@instance.title = array
result = @resource.evaluate(@scope).collect { |r| r.title }
result.should be_include("one")
result.should be_include("two")
end
it "should create and return one resource objects per title" do
titles = []
%w{one two}.each do |title|
titles << Puppet::Parser::AST::String.new(:value => title)
end
array = Puppet::Parser::AST::ASTArray.new(:children => titles)
@instance.title = array
result = @resource.evaluate(@scope).collect { |r| r.title }
result.should be_include("one")
result.should be_include("two")
end
it "should implicitly iterate over instances" do
new_title = Puppet::Parser::AST::String.new(:value => "other_title")
new_instance = ast::ResourceInstance.new(:title => new_title, :parameters => ast::ASTArray.new(:children => []))
@resource.instances.push(new_instance)
@resource.evaluate(@scope).collect { |r| r.title }.should == ["mytitle", "other_title"]
end
it "should handover resources to the compiler" do
titles = []
%w{one two}.each do |title|
titles << Puppet::Parser::AST::String.new(:value => title)
end
array = Puppet::Parser::AST::ASTArray.new(:children => titles)
@instance.title = array
result = @resource.evaluate(@scope)
result.each do |res|
@compiler.catalog.resource(res.ref).should be_instance_of(Puppet::Parser::Resource)
end
end
it "should generate virtual resources if it is virtual" do
@resource.virtual = true
result = @resource.evaluate(@scope)
result[0].should be_virtual
end
it "should generate virtual and exported resources if it is exported" do
@resource.exported = true
result = @resource.evaluate(@scope)
result[0].should be_virtual
result[0].should be_exported
end
# Related to #806, make sure resources always look up the full path to the resource.
describe "when generating qualified resources" do
before do
@scope = Puppet::Parser::Scope.new Puppet::Parser::Compiler.new(Puppet::Node.new("mynode"))
- @parser = Puppet::Parser::Parser.new(Puppet::Node::Environment.new)
+ @parser = Puppet::Parser::Parser.new(@scope.environment)
["one", "one::two", "three"].each do |name|
@parser.environment.known_resource_types.add(Puppet::Resource::Type.new(:definition, name, {}))
end
@twoscope = @scope.newscope(:namespace => "one")
@twoscope.resource = @scope.resource
end
def resource(type, params = nil)
params ||= Puppet::Parser::AST::ASTArray.new(:children => [])
instance = Puppet::Parser::AST::ResourceInstance.new(
:title => Puppet::Parser::AST::String.new(:value => "myresource"), :parameters => params)
Puppet::Parser::AST::Resource.new(:type => type,
:instances => Puppet::Parser::AST::ASTArray.new(:children => [instance]))
end
it "should be able to generate resources with fully qualified type information" do
resource("two").evaluate(@twoscope)[0].type.should == "One::Two"
end
it "should be able to generate resources with unqualified type information" do
resource("one").evaluate(@twoscope)[0].type.should == "One"
end
it "should correctly generate resources that can look up builtin types" do
resource("file").evaluate(@twoscope)[0].type.should == "File"
end
it "should correctly generate resources that can look up defined classes by title" do
@scope.known_resource_types.add_hostclass Puppet::Resource::Type.new(:hostclass, "Myresource", {})
@scope.compiler.stubs(:evaluate_classes)
res = resource("class").evaluate(@twoscope)[0]
res.type.should == "Class"
res.title.should == "Myresource"
end
it "should evaluate parameterized classes when they are instantiated" do
@scope.known_resource_types.add_hostclass Puppet::Resource::Type.new(:hostclass, "Myresource", {})
@scope.compiler.expects(:evaluate_classes).with(['myresource'],@twoscope,false,true)
resource("class").evaluate(@twoscope)[0]
end
it "should fail for resource types that do not exist" do
lambda { resource("nosuchtype").evaluate(@twoscope) }.should raise_error(Puppet::ParseError)
end
end
end
describe "for class resources" do
before do
@title = Puppet::Parser::AST::String.new(:value => "classname")
@compiler = Puppet::Parser::Compiler.new(Puppet::Node.new("mynode"))
@scope = Puppet::Parser::Scope.new(@compiler)
@scope.stubs(:resource).returns(stub_everything)
@instance = ast::ResourceInstance.new(:title => @title, :parameters => ast::ASTArray.new(:children => []))
@resource = ast::Resource.new(:type => "Class", :instances => ast::ASTArray.new(:children => [@instance]))
@resource.stubs(:qualified_type).returns("Resource")
@type = Puppet::Resource::Type.new(:hostclass, "classname")
@compiler.known_resource_types.add(@type)
end
it "should instantiate the class" do
@compiler.stubs(:evaluate_classes)
result = @resource.evaluate(@scope)
result.length.should == 1
result.first.ref.should == "Class[Classname]"
@compiler.catalog.resource("Class[Classname]").should equal(result.first)
end
it "should cause its parent to be evaluated" do
parent_type = Puppet::Resource::Type.new(:hostclass, "parentname")
@compiler.stubs(:evaluate_classes)
@compiler.known_resource_types.add(parent_type)
@type.parent = "parentname"
result = @resource.evaluate(@scope)
result.length.should == 1
result.first.ref.should == "Class[Classname]"
@compiler.catalog.resource("Class[Classname]").should equal(result.first)
@compiler.catalog.resource("Class[Parentname]").should be_instance_of(Puppet::Parser::Resource)
end
end
end
diff --git a/spec/unit/parser/compiler_spec.rb b/spec/unit/parser/compiler_spec.rb
index 68210a4df..8a6621520 100755
--- a/spec/unit/parser/compiler_spec.rb
+++ b/spec/unit/parser/compiler_spec.rb
@@ -1,901 +1,903 @@
#! /usr/bin/env ruby
require 'spec_helper'
require 'puppet_spec/compiler'
class CompilerTestResource
attr_accessor :builtin, :virtual, :evaluated, :type, :title
def initialize(type, title)
@type = type
@title = title
end
def [](attr)
return nil if attr == :stage
:main
end
def ref
"#{type.to_s.capitalize}[#{title}]"
end
def evaluated?
@evaluated
end
def builtin_type?
@builtin
end
def virtual?
@virtual
end
def class?
false
end
def stage?
false
end
def evaluate
end
def file
"/fake/file/goes/here"
end
def line
"42"
end
end
describe Puppet::Parser::Compiler do
include PuppetSpec::Files
def resource(type, title)
Puppet::Parser::Resource.new(type, title, :scope => @scope)
end
before :each do
# Push me faster, I wanna go back in time! (Specifically, freeze time
# across the test since we have a bunch of version == timestamp code
# hidden away in the implementation and we keep losing the race.)
# --daniel 2011-04-21
now = Time.now
Time.stubs(:now).returns(now)
- @node = Puppet::Node.new("testnode", :facts => Puppet::Node::Facts.new("facts", {}))
- @known_resource_types = Puppet::Resource::TypeCollection.new "development"
+ environment = Puppet::Node::Environment.create(:testing, [], '')
+ @node = Puppet::Node.new("testnode",
+ :facts => Puppet::Node::Facts.new("facts", {}),
+ :environment => environment)
+ @known_resource_types = environment.known_resource_types
@compiler = Puppet::Parser::Compiler.new(@node)
@scope = Puppet::Parser::Scope.new(@compiler, :source => stub('source'))
@scope_resource = Puppet::Parser::Resource.new(:file, "/my/file", :scope => @scope)
@scope.resource = @scope_resource
- @compiler.environment.stubs(:known_resource_types).returns @known_resource_types
end
it "should have a class method that compiles, converts, and returns a catalog" do
compiler = stub 'compiler'
Puppet::Parser::Compiler.expects(:new).with(@node).returns compiler
catalog = stub 'catalog'
compiler.expects(:compile).returns catalog
converted_catalog = stub 'converted_catalog'
catalog.expects(:to_resource).returns converted_catalog
Puppet::Parser::Compiler.compile(@node).should equal(converted_catalog)
end
it "should fail intelligently when a class-level compile fails" do
Puppet::Parser::Compiler.expects(:new).raises ArgumentError
lambda { Puppet::Parser::Compiler.compile(@node) }.should raise_error(Puppet::Error)
end
it "should use the node's environment as its environment" do
@compiler.environment.should equal(@node.environment)
end
it "should include the resource type collection helper" do
Puppet::Parser::Compiler.ancestors.should be_include(Puppet::Resource::TypeCollectionHelper)
end
it "should be able to return a class list containing all added classes" do
@compiler.add_class ""
@compiler.add_class "one"
@compiler.add_class "two"
@compiler.classlist.sort.should == %w{one two}.sort
end
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 = "rspec"
$env_module_directories = "rspec"
Puppet::Parser::Compiler.compile(@node)
$known_resource_types = nil
$env_module_directories = nil
end
describe "when initializing" do
it "should set its node attribute" do
@compiler.node.should equal(@node)
end
it "should detect when ast nodes are absent" do
@compiler.ast_nodes?.should be_false
end
it "should detect when ast nodes are present" do
@known_resource_types.expects(:nodes?).returns true
@compiler.ast_nodes?.should be_true
end
it "should copy the known_resource_types version to the catalog" do
@compiler.catalog.version.should == @known_resource_types.version
end
it "should copy any node classes into the class list" do
node = Puppet::Node.new("mynode")
node.classes = %w{foo bar}
compiler = Puppet::Parser::Compiler.new(node)
compiler.classlist.should =~ ['foo', 'bar']
end
it "should transform node class hashes into a class list" do
node = Puppet::Node.new("mynode")
node.classes = {'foo'=>{'one'=>'p1'}, 'bar'=>{'two'=>'p2'}}
compiler = Puppet::Parser::Compiler.new(node)
compiler.classlist.should =~ ['foo', 'bar']
end
it "should add a 'main' stage to the catalog" do
@compiler.catalog.resource(:stage, :main).should be_instance_of(Puppet::Parser::Resource)
end
end
describe "when managing scopes" do
it "should create a top scope" do
@compiler.topscope.should be_instance_of(Puppet::Parser::Scope)
end
it "should be able to create new scopes" do
@compiler.newscope(@compiler.topscope).should be_instance_of(Puppet::Parser::Scope)
end
it "should set the parent scope of the new scope to be the passed-in parent" do
scope = mock 'scope'
newscope = @compiler.newscope(scope)
newscope.parent.should equal(scope)
end
it "should set the parent scope of the new scope to its topscope if the parent passed in is nil" do
scope = mock 'scope'
newscope = @compiler.newscope(nil)
newscope.parent.should equal(@compiler.topscope)
end
end
describe "when compiling" do
def compile_methods
[:set_node_parameters, :evaluate_main, :evaluate_ast_node, :evaluate_node_classes, :evaluate_generators, :fail_on_unevaluated,
:finish, :store, :extract, :evaluate_relationships]
end
# Stub all of the main compile methods except the ones we're specifically interested in.
def compile_stub(*except)
(compile_methods - except).each { |m| @compiler.stubs(m) }
end
it "should set node parameters as variables in the top scope" do
params = {"a" => "b", "c" => "d"}
@node.stubs(:parameters).returns(params)
compile_stub(:set_node_parameters)
@compiler.compile
@compiler.topscope['a'].should == "b"
@compiler.topscope['c'].should == "d"
end
it "should set the client and server versions on the catalog" do
params = {"clientversion" => "2", "serverversion" => "3"}
@node.stubs(:parameters).returns(params)
compile_stub(:set_node_parameters)
@compiler.compile
@compiler.catalog.client_version.should == "2"
@compiler.catalog.server_version.should == "3"
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, "")
main_class.expects(:evaluate_code).with { |r| r.is_a?(Puppet::Parser::Resource) }
@compiler.topscope.expects(:source=).with(main_class)
@compiler.compile
end
it "should create a new, empty 'main' if no main class exists" do
compile_stub(:evaluate_main)
@compiler.compile
@known_resource_types.find_hostclass([""], "").should be_instance_of(Puppet::Resource::Type)
end
it "should add an edge between the main stage and main class" do
@compiler.compile
(stage = @compiler.catalog.resource(:stage, "main")).should be_instance_of(Puppet::Parser::Resource)
(klass = @compiler.catalog.resource(:class, "")).should be_instance_of(Puppet::Parser::Resource)
@compiler.catalog.edge?(stage, klass).should be_true
end
it "should evaluate all added collections" do
colls = []
# And when the collections fail to evaluate.
colls << mock("coll1-false")
colls << mock("coll2-false")
colls.each { |c| c.expects(:evaluate).returns(false) }
@compiler.add_collection(colls[0])
@compiler.add_collection(colls[1])
compile_stub(:evaluate_generators)
@compiler.compile
end
it "should ignore builtin resources" do
resource = resource(:file, "testing")
@compiler.add_resource(@scope, resource)
resource.expects(:evaluate).never
@compiler.compile
end
it "should evaluate unevaluated resources" do
resource = CompilerTestResource.new(:file, "testing")
@compiler.add_resource(@scope, resource)
# We have to now mark the resource as evaluated
resource.expects(:evaluate).with { |*whatever| resource.evaluated = true }
@compiler.compile
end
it "should not evaluate already-evaluated resources" do
resource = resource(:file, "testing")
resource.stubs(:evaluated?).returns true
@compiler.add_resource(@scope, resource)
resource.expects(:evaluate).never
@compiler.compile
end
it "should evaluate unevaluated resources created by evaluating other resources" do
resource = CompilerTestResource.new(:file, "testing")
@compiler.add_resource(@scope, resource)
resource2 = CompilerTestResource.new(:file, "other")
# We have to now mark the resource as evaluated
resource.expects(:evaluate).with { |*whatever| resource.evaluated = true; @compiler.add_resource(@scope, resource2) }
resource2.expects(:evaluate).with { |*whatever| resource2.evaluated = true }
@compiler.compile
end
describe "when finishing" do
before do
@compiler.send(:evaluate_main)
@catalog = @compiler.catalog
end
def add_resource(name, parent = nil)
resource = Puppet::Parser::Resource.new "file", name, :scope => @scope
@compiler.add_resource(@scope, resource)
@catalog.add_edge(parent, resource) if parent
resource
end
it "should call finish() on all resources" do
# Add a resource that does respond to :finish
resource = Puppet::Parser::Resource.new "file", "finish", :scope => @scope
resource.expects(:finish)
@compiler.add_resource(@scope, resource)
# And one that does not
dnf_resource = stub_everything "dnf", :ref => "File[dnf]", :type => "file"
@compiler.add_resource(@scope, dnf_resource)
@compiler.send(:finish)
end
it "should call finish() in add_resource order" do
resources = sequence('resources')
resource1 = add_resource("finish1")
resource1.expects(:finish).in_sequence(resources)
resource2 = add_resource("finish2")
resource2.expects(:finish).in_sequence(resources)
@compiler.send(:finish)
end
it "should add each container's metaparams to its contained resources" do
main = @catalog.resource(:class, :main)
main[:noop] = true
resource1 = add_resource("meh", main)
@compiler.send(:finish)
resource1[:noop].should be_true
end
it "should add metaparams recursively" do
main = @catalog.resource(:class, :main)
main[:noop] = true
resource1 = add_resource("meh", main)
resource2 = add_resource("foo", resource1)
@compiler.send(:finish)
resource2[:noop].should be_true
end
it "should prefer metaparams from immediate parents" do
main = @catalog.resource(:class, :main)
main[:noop] = true
resource1 = add_resource("meh", main)
resource2 = add_resource("foo", resource1)
resource1[:noop] = false
@compiler.send(:finish)
resource2[:noop].should be_false
end
it "should merge tags downward" do
main = @catalog.resource(:class, :main)
main.tag("one")
resource1 = add_resource("meh", main)
resource1.tag "two"
resource2 = add_resource("foo", resource1)
@compiler.send(:finish)
resource2.tags.should be_include("one")
resource2.tags.should be_include("two")
end
it "should work if only middle resources have metaparams set" do
main = @catalog.resource(:class, :main)
resource1 = add_resource("meh", main)
resource1[:noop] = true
resource2 = add_resource("foo", resource1)
@compiler.send(:finish)
resource2[:noop].should be_true
end
end
it "should return added resources in add order" do
resource1 = resource(:file, "yay")
@compiler.add_resource(@scope, resource1)
resource2 = resource(:file, "youpi")
@compiler.add_resource(@scope, resource2)
@compiler.resources.should == [resource1, resource2]
end
it "should add resources that do not conflict with existing resources" do
resource = resource(:file, "yay")
@compiler.add_resource(@scope, resource)
@compiler.catalog.should be_vertex(resource)
end
it "should fail to add resources that conflict with existing resources" do
path = make_absolute("/foo")
file1 = resource(:file, path)
file2 = resource(:file, path)
@compiler.add_resource(@scope, file1)
lambda { @compiler.add_resource(@scope, file2) }.should raise_error(Puppet::Resource::Catalog::DuplicateResourceError)
end
it "should add an edge from the scope resource to the added resource" do
resource = resource(:file, "yay")
@compiler.add_resource(@scope, resource)
@compiler.catalog.should be_edge(@scope.resource, resource)
end
it "should not add non-class resources that don't specify a stage to the 'main' stage" do
main = @compiler.catalog.resource(:stage, :main)
resource = resource(:file, "foo")
@compiler.add_resource(@scope, resource)
@compiler.catalog.should_not be_edge(main, resource)
end
it "should not add any parent-edges to stages" do
stage = resource(:stage, "other")
@compiler.add_resource(@scope, stage)
@scope.resource = resource(:class, "foo")
@compiler.catalog.edge?(@scope.resource, stage).should be_false
end
it "should not attempt to add stages to other stages" do
other_stage = resource(:stage, "other")
second_stage = resource(:stage, "second")
@compiler.add_resource(@scope, other_stage)
@compiler.add_resource(@scope, second_stage)
second_stage[:stage] = "other"
@compiler.catalog.edge?(other_stage, second_stage).should be_false
end
it "should have a method for looking up resources" do
resource = resource(:yay, "foo")
@compiler.add_resource(@scope, resource)
@compiler.findresource("Yay[foo]").should equal(resource)
end
it "should be able to look resources up by type and title" do
resource = resource(:yay, "foo")
@compiler.add_resource(@scope, resource)
@compiler.findresource("Yay", "foo").should equal(resource)
end
it "should not evaluate virtual defined resources" do
resource = resource(:file, "testing")
resource.virtual = true
@compiler.add_resource(@scope, resource)
resource.expects(:evaluate).never
@compiler.compile
end
end
describe "when evaluating collections" do
it "should evaluate each collection" do
2.times { |i|
coll = mock 'coll%s' % i
@compiler.add_collection(coll)
# This is the hard part -- we have to emulate the fact that
# collections delete themselves if they are done evaluating.
coll.expects(:evaluate).with do
@compiler.delete_collection(coll)
end
}
@compiler.compile
end
it "should not fail when there are unevaluated resource collections that do not refer to specific resources" do
coll = stub 'coll', :evaluate => false
coll.expects(:resources).returns(nil)
@compiler.add_collection(coll)
lambda { @compiler.compile }.should_not raise_error
end
it "should fail when there are unevaluated resource collections that refer to a specific resource" do
coll = stub 'coll', :evaluate => false
coll.expects(:resources).returns(:something)
@compiler.add_collection(coll)
lambda { @compiler.compile }.should raise_error Puppet::ParseError, 'Failed to realize virtual resources something'
end
it "should fail when there are unevaluated resource collections that refer to multiple specific resources" do
coll = stub 'coll', :evaluate => false
coll.expects(:resources).returns([:one, :two])
@compiler.add_collection(coll)
lambda { @compiler.compile }.should raise_error Puppet::ParseError, 'Failed to realize virtual resources one, two'
end
end
describe "when evaluating relationships" do
it "should evaluate each relationship with its catalog" do
dep = stub 'dep'
dep.expects(:evaluate).with(@compiler.catalog)
@compiler.add_relationship dep
@compiler.evaluate_relationships
end
end
describe "when told to evaluate missing classes" do
it "should fail if there's no source listed for the scope" do
scope = stub 'scope', :source => nil
proc { @compiler.evaluate_classes(%w{one two}, scope) }.should raise_error(Puppet::DevError)
end
it "should raise an error if a class is not found" do
@scope.expects(:find_hostclass).with("notfound", {:assume_fqname => false}).returns(nil)
lambda{ @compiler.evaluate_classes(%w{notfound}, @scope) }.should raise_error(Puppet::Error, /Could not find class/)
end
it "should raise an error when it can't find class" do
klasses = {'foo'=>nil}
@node.classes = klasses
@compiler.topscope.expects(:find_hostclass).with('foo', {:assume_fqname => false}).returns(nil)
lambda{ @compiler.compile }.should raise_error(Puppet::Error, /Could not find class foo for testnode/)
end
end
describe "when evaluating found classes" do
before do
Puppet.settings[:data_binding_terminus] = "none"
@class = stub 'class', :name => "my::class"
@scope.stubs(:find_hostclass).with("myclass", {:assume_fqname => false}).returns(@class)
@resource = stub 'resource', :ref => "Class[myclass]", :type => "file"
end
it "should evaluate each class" do
@compiler.catalog.stubs(:tag)
@class.expects(:ensure_in_catalog).with(@scope)
@scope.stubs(:class_scope).with(@class)
@compiler.evaluate_classes(%w{myclass}, @scope)
end
describe "and the classes are specified as a hash with parameters" do
before do
@node.classes = {}
@ast_obj = Puppet::Parser::AST::String.new(:value => 'foo')
end
# Define the given class with default parameters
def define_class(name, parameters)
@node.classes[name] = parameters
klass = Puppet::Resource::Type.new(:hostclass, name, :arguments => {'p1' => @ast_obj, 'p2' => @ast_obj})
@compiler.topscope.known_resource_types.add klass
end
def compile
@catalog = @compiler.compile
end
it "should record which classes are evaluated" do
classes = {'foo'=>{}, 'bar::foo'=>{}, 'bar'=>{}}
classes.each { |c, params| define_class(c, params) }
compile()
classes.each { |name, p| @catalog.classes.should include(name) }
end
it "should provide default values for parameters that have no values specified" do
define_class('foo', {})
compile()
@catalog.resource(:class, 'foo')['p1'].should == "foo"
end
it "should use any provided values" do
define_class('foo', {'p1' => 'real_value'})
compile()
@catalog.resource(:class, 'foo')['p1'].should == "real_value"
end
it "should support providing some but not all values" do
define_class('foo', {'p1' => 'real_value'})
compile()
@catalog.resource(:class, 'Foo')['p1'].should == "real_value"
@catalog.resource(:class, 'Foo')['p2'].should == "foo"
end
it "should ensure each node class is in catalog and has appropriate tags" do
klasses = ['bar::foo']
@node.classes = klasses
ast_obj = Puppet::Parser::AST::String.new(:value => 'foo')
klasses.each do |name|
klass = Puppet::Resource::Type.new(:hostclass, name, :arguments => {'p1' => ast_obj, 'p2' => ast_obj})
@compiler.topscope.known_resource_types.add klass
end
catalog = @compiler.compile
r2 = catalog.resources.detect {|r| r.title == 'Bar::Foo' }
r2.tags.should == Puppet::Util::TagSet.new(['bar::foo', 'class', 'bar', 'foo'])
end
end
it "should fail if required parameters are missing" do
klass = {'foo'=>{'a'=>'one'}}
@node.classes = klass
klass = Puppet::Resource::Type.new(:hostclass, 'foo', :arguments => {'a' => nil, 'b' => nil})
@compiler.topscope.known_resource_types.add klass
lambda { @compiler.compile }.should raise_error(Puppet::ParseError, "Must pass b to Class[Foo]")
end
it "should fail if invalid parameters are passed" do
klass = {'foo'=>{'3'=>'one'}}
@node.classes = klass
klass = Puppet::Resource::Type.new(:hostclass, 'foo', :arguments => {})
@compiler.topscope.known_resource_types.add klass
lambda { @compiler.compile }.should raise_error(Puppet::ParseError, "Invalid parameter 3")
end
it "should ensure class is in catalog without params" do
@node.classes = klasses = {'foo'=>nil}
foo = Puppet::Resource::Type.new(:hostclass, 'foo')
@compiler.topscope.known_resource_types.add foo
catalog = @compiler.compile
catalog.classes.should include 'foo'
end
it "should not evaluate the resources created for found classes unless asked" do
@compiler.catalog.stubs(:tag)
@resource.expects(:evaluate).never
@class.expects(:ensure_in_catalog).returns(@resource)
@scope.stubs(:class_scope).with(@class)
@compiler.evaluate_classes(%w{myclass}, @scope)
end
it "should immediately evaluate the resources created for found classes when asked" do
@compiler.catalog.stubs(:tag)
@resource.expects(:evaluate)
@class.expects(:ensure_in_catalog).returns(@resource)
@scope.stubs(:class_scope).with(@class)
@compiler.evaluate_classes(%w{myclass}, @scope, false)
end
it "should skip classes that have already been evaluated" do
@compiler.catalog.stubs(:tag)
@scope.stubs(:class_scope).with(@class).returns("something")
@compiler.expects(:add_resource).never
@resource.expects(:evaluate).never
Puppet::Parser::Resource.expects(:new).never
@compiler.evaluate_classes(%w{myclass}, @scope, false)
end
it "should skip classes previously evaluated with different capitalization" do
@compiler.catalog.stubs(:tag)
@scope.stubs(:find_hostclass).with("MyClass",{:assume_fqname => false}).returns(@class)
@scope.stubs(:class_scope).with(@class).returns("something")
@compiler.expects(:add_resource).never
@resource.expects(:evaluate).never
Puppet::Parser::Resource.expects(:new).never
@compiler.evaluate_classes(%w{MyClass}, @scope, false)
end
end
describe "when evaluating AST nodes with no AST nodes present" do
it "should do nothing" do
@compiler.expects(:ast_nodes?).returns(false)
@compiler.known_resource_types.expects(:nodes).never
Puppet::Parser::Resource.expects(:new).never
@compiler.send(:evaluate_ast_node)
end
end
describe "when evaluating AST nodes with AST nodes present" do
before do
@compiler.known_resource_types.stubs(:nodes?).returns true
# Set some names for our test
@node.stubs(:names).returns(%w{a b c})
@compiler.known_resource_types.stubs(:node).with("a").returns(nil)
@compiler.known_resource_types.stubs(:node).with("b").returns(nil)
@compiler.known_resource_types.stubs(:node).with("c").returns(nil)
# It should check this last, of course.
@compiler.known_resource_types.stubs(:node).with("default").returns(nil)
end
it "should fail if the named node cannot be found" do
proc { @compiler.send(:evaluate_ast_node) }.should raise_error(Puppet::ParseError)
end
it "should evaluate the first node class matching the node name" do
node_class = stub 'node', :name => "c", :evaluate_code => nil
@compiler.known_resource_types.stubs(:node).with("c").returns(node_class)
node_resource = stub 'node resource', :ref => "Node[c]", :evaluate => nil, :type => "node"
node_class.expects(:ensure_in_catalog).returns(node_resource)
@compiler.compile
end
it "should match the default node if no matching node can be found" do
node_class = stub 'node', :name => "default", :evaluate_code => nil
@compiler.known_resource_types.stubs(:node).with("default").returns(node_class)
node_resource = stub 'node resource', :ref => "Node[default]", :evaluate => nil, :type => "node"
node_class.expects(:ensure_in_catalog).returns(node_resource)
@compiler.compile
end
it "should evaluate the node resource immediately rather than using lazy evaluation" do
node_class = stub 'node', :name => "c"
@compiler.known_resource_types.stubs(:node).with("c").returns(node_class)
node_resource = stub 'node resource', :ref => "Node[c]", :type => "node"
node_class.expects(:ensure_in_catalog).returns(node_resource)
node_resource.expects(:evaluate)
@compiler.send(:evaluate_ast_node)
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
@override = stub 'override', :ref => "File[/foo]", :type => "my"
@resource = resource(:file, "/foo")
end
it "should be able to store overrides" do
lambda { @compiler.add_override(@override) }.should_not raise_error
end
it "should apply overrides to the appropriate resources" do
@compiler.add_resource(@scope, @resource)
@resource.expects(:merge).with(@override)
@compiler.add_override(@override)
@compiler.compile
end
it "should accept overrides before the related resource has been created" do
@resource.expects(:merge).with(@override)
# First store the override
@compiler.add_override(@override)
# Then the resource
@compiler.add_resource(@scope, @resource)
# And compile, so they get resolved
@compiler.compile
end
it "should fail if the compile is finished and resource overrides have not been applied" do
@compiler.add_override(@override)
lambda { @compiler.compile }.should raise_error Puppet::ParseError, 'Could not find resource(s) File[/foo] for overriding'
end
end
end
diff --git a/spec/unit/parser/files_spec.rb b/spec/unit/parser/files_spec.rb
index ca7e45b13..02ef3d5cf 100755
--- a/spec/unit/parser/files_spec.rb
+++ b/spec/unit/parser/files_spec.rb
@@ -1,159 +1,149 @@
#! /usr/bin/env ruby
require 'spec_helper'
require 'puppet/parser/files'
describe Puppet::Parser::Files do
include PuppetSpec::Files
+ let(:environment) { Puppet::Node::Environment.create(:testing, [], '') }
+
before do
@basepath = make_absolute("/somepath")
end
describe "when searching for templates" do
it "should return fully-qualified templates directly" do
Puppet::Parser::Files.expects(:modulepath).never
- Puppet::Parser::Files.find_template(@basepath + "/my/template").should == @basepath + "/my/template"
+ Puppet::Parser::Files.find_template(@basepath + "/my/template", environment).should == @basepath + "/my/template"
end
it "should return the template from the first found module" do
mod = mock 'module'
- Puppet::Node::Environment.new.expects(:module).with("mymod").returns mod
-
mod.expects(:template).returns("/one/mymod/templates/mytemplate")
- Puppet::Parser::Files.find_template("mymod/mytemplate").should == "/one/mymod/templates/mytemplate"
+ environment.expects(:module).with("mymod").returns mod
+
+ Puppet::Parser::Files.find_template("mymod/mytemplate", environment).should == "/one/mymod/templates/mytemplate"
end
it "should return the file in the templatedir if it exists" do
Puppet[:templatedir] = "/my/templates"
Puppet[:modulepath] = "/one:/two"
File.stubs(:directory?).returns(true)
- Puppet::FileSystem::File.stubs(:exist?).returns(true)
- Puppet::Parser::Files.find_template("mymod/mytemplate").should == File.join(Puppet[:templatedir], "mymod/mytemplate")
+ Puppet::FileSystem.stubs(:exist?).returns(true)
+ Puppet::Parser::Files.find_template("mymod/mytemplate", environment).should == File.join(Puppet[:templatedir], "mymod/mytemplate")
end
it "should not raise an error if no valid templatedir exists and the template exists in a module" do
mod = mock 'module'
- Puppet::Node::Environment.new.expects(:module).with("mymod").returns mod
-
mod.expects(:template).returns("/one/mymod/templates/mytemplate")
- Puppet::Parser::Files.stubs(:templatepath).with(nil).returns(nil)
+ environment.expects(:module).with("mymod").returns mod
+ Puppet::Parser::Files.stubs(:templatepath).with(environment).returns(nil)
- Puppet::Parser::Files.find_template("mymod/mytemplate").should == "/one/mymod/templates/mytemplate"
+ Puppet::Parser::Files.find_template("mymod/mytemplate", environment).should == "/one/mymod/templates/mytemplate"
end
it "should return unqualified templates if they exist in the template dir" do
- 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"
+ Puppet::FileSystem.stubs(:exist?).returns true
+ Puppet::Parser::Files.stubs(:templatepath).with(environment).returns(["/my/templates"])
+
+ Puppet::Parser::Files.find_template("mytemplate", environment).should == "/my/templates/mytemplate"
end
it "should only return templates if they actually exist" do
- 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"
+ Puppet::FileSystem.expects(:exist?).with("/my/templates/mytemplate").returns true
+ Puppet::Parser::Files.stubs(:templatepath).with(environment).returns(["/my/templates"])
+ Puppet::Parser::Files.find_template("mytemplate", environment).should == "/my/templates/mytemplate"
end
it "should return nil when asked for a template that doesn't exist" do
- 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
- 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")
+ Puppet::FileSystem.expects(:exist?).with("/my/templates/mytemplate").returns false
+ Puppet::Parser::Files.stubs(:templatepath).with(environment).returns(["/my/templates"])
+ Puppet::Parser::Files.find_template("mytemplate", environment).should be_nil
end
it "should accept relative templatedirs" do
- Puppet::FileSystem::File.stubs(:exist?).returns true
+ Puppet::FileSystem.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")
+ Puppet::Parser::Files.find_template("mytemplate", environment).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
- 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"
+ Puppet::FileSystem.stubs(:exist?).returns true
+ Puppet::Parser::Files.stubs(:templatepath).with(environment).returns(["/myenv/templates"])
+ Puppet::Parser::Files.find_template("mymod/mytemplate", environment).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
- 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"
+ Puppet::FileSystem.stubs(:exist?).returns true
+ Puppet::Parser::Files.stubs(:templatepath).with(environment).returns(["/myenv/templates", "/two/templates"])
+ Puppet::Parser::Files.find_template("mymod/mytemplate", environment).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"])
- Puppet::FileSystem::File.expects(:exist?).with("/one/templates/mytemplate").returns(true)
- Puppet::Parser::Files.find_template("mytemplate").should == "/one/templates/mytemplate"
+ Puppet::FileSystem.expects(:exist?).with("/one/templates/mytemplate").returns(true)
+ Puppet::Parser::Files.find_template("mytemplate", environment).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"])
- 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"
+ Puppet::FileSystem.expects(:exist?).with("/one/templates/mytemplate").returns(false)
+ Puppet::FileSystem.expects(:exist?).with("/two/templates/mytemplate").returns(true)
+ Puppet::Parser::Files.find_template("mytemplate", environment).should == "/two/templates/mytemplate"
end
it "should use the node environment if specified" do
mod = mock 'module'
- Puppet::Node::Environment.new("myenv").expects(:module).with("mymod").returns mod
+ environment.expects(:module).with("mymod").returns mod
mod.expects(:template).returns("/my/modules/mymod/templates/envtemplate")
- Puppet::Parser::Files.find_template("mymod/envtemplate", "myenv").should == "/my/modules/mymod/templates/envtemplate"
+ Puppet::Parser::Files.find_template("mymod/envtemplate", environment).should == "/my/modules/mymod/templates/envtemplate"
end
it "should return nil if no template can be found" do
- Puppet::Parser::Files.find_template("foomod/envtemplate", "myenv").should be_nil
+ Puppet::Parser::Files.find_template("foomod/envtemplate", environment).should be_nil
end
-
- after { Puppet.settings.clear }
end
describe "when searching for manifests" do
it "should ignore invalid modules" do
mod = mock 'module'
- env = Puppet::Node::Environment.new
- env.expects(:module).with("mymod").raises(Puppet::Module::InvalidName, "name is invalid")
+ environment.expects(:module).with("mymod").raises(Puppet::Module::InvalidName, "name is invalid")
Puppet.expects(:value).with(:modulepath).never
Dir.stubs(:glob).returns %w{foo}
- Puppet::Parser::Files.find_manifests_in_modules("mymod/init.pp", env)
+ Puppet::Parser::Files.find_manifests_in_modules("mymod/init.pp", environment)
end
end
describe "when searching for manifests in a module" do
def a_module_in_environment(env, name)
mod = Puppet::Module.new(name, "/one/#{name}", env)
env.stubs(:module).with(name).returns mod
mod.stubs(:match_manifests).with("init.pp").returns(["/one/#{name}/manifests/init.pp"])
end
- let(:environment) { Puppet::Node::Environment.new }
-
it "returns no files when no module is found" do
module_name, files = Puppet::Parser::Files.find_manifests_in_modules("not_here_module/foo", environment)
expect(files).to be_empty
expect(module_name).to be_nil
end
it "should return the name of the module and the manifests from the first found module" do
a_module_in_environment(environment, "mymod")
Puppet::Parser::Files.find_manifests_in_modules("mymod/init.pp", environment).should ==
["mymod", ["/one/mymod/manifests/init.pp"]]
end
it "does not find the module when it is a different environment" do
- different_env = Puppet::Node::Environment.new("different")
+ different_env = Puppet::Node::Environment.create(:different, [], '')
a_module_in_environment(environment, "mymod")
Puppet::Parser::Files.find_manifests_in_modules("mymod/init.pp", different_env).should_not include("mymod")
end
end
end
diff --git a/spec/unit/parser/functions/defined_spec.rb b/spec/unit/parser/functions/defined_spec.rb
index 044d264de..edbf29863 100755
--- a/spec/unit/parser/functions/defined_spec.rb
+++ b/spec/unit/parser/functions/defined_spec.rb
@@ -1,52 +1,114 @@
#! /usr/bin/env ruby
require 'spec_helper'
+require 'puppet/pops'
describe "the 'defined' function" do
before :all do
Puppet::Parser::Functions.autoloader.loadall
end
before :each do
- Puppet::Node::Environment.stubs(:current).returns(nil)
@compiler = Puppet::Parser::Compiler.new(Puppet::Node.new("foo"))
@scope = Puppet::Parser::Scope.new(@compiler)
end
- it "should exist" do
- Puppet::Parser::Functions.function("defined").should == "function_defined"
+ it "exists" do
+ expect(Puppet::Parser::Functions.function("defined")).to be_eql("function_defined")
end
- it "should be true when the name is defined as a class" do
+ it "is true when the name is defined as a class" do
@scope.known_resource_types.add Puppet::Resource::Type.new(:hostclass, "yayness")
- @scope.function_defined(["yayness"]).should be_true
+ expect(@scope.function_defined(["yayness"])).to be_true
end
- it "should be true when the name is defined as a definition" do
+ it "is true when the name is defined as a definition" do
@scope.known_resource_types.add Puppet::Resource::Type.new(:definition, "yayness")
- @scope.function_defined(["yayness"]).should be_true
+ expect(@scope.function_defined(["yayness"])).to be_true
end
- it "should be true when the name is defined as a builtin type" do
- @scope.function_defined(["file"]).should be_true
+ it "is true when the name is defined as a builtin type" do
+ expect(@scope.function_defined(["file"])).to be_true
end
-
- it "should be true when any of the provided names are defined" do
+ it "is true when any of the provided names are defined" do
@scope.known_resource_types.add Puppet::Resource::Type.new(:definition, "yayness")
- @scope.function_defined(["meh", "yayness", "booness"]).should be_true
+ expect(@scope.function_defined(["meh", "yayness", "booness"])).to be_true
end
- it "should be false when a single given name is not defined" do
- @scope.function_defined(["meh"]).should be_false
+ it "is false when a single given name is not defined" do
+ expect(@scope.function_defined(["meh"])).to be_false
end
- it "should be false when none of the names are defined" do
- @scope.function_defined(["meh", "yayness", "booness"]).should be_false
+ it "is false when none of the names are defined" do
+ expect(@scope.function_defined(["meh", "yayness", "booness"])).to be_false
end
- it "should be true when a resource reference is provided and the resource is in the catalog" do
+ it "is true when a resource reference is provided and the resource is in the catalog" do
resource = Puppet::Resource.new("file", "/my/file")
@compiler.add_resource(@scope, resource)
- @scope.function_defined([resource]).should be_true
+ expect(@scope.function_defined([resource])).to be_true
+ end
+
+ context "with string variable references" do
+ it "is true when variable exists in scope" do
+ @scope['x'] = 'something'
+ expect(@scope.function_defined(['$x'])).to be_true
+ end
+
+ it "is true when at least one variable exists in scope" do
+ @scope['x'] = 'something'
+ expect(@scope.function_defined(['$y', '$x', '$z'])).to be_true
+ end
+
+ it "is false when variable does not exist in scope" do
+ expect(@scope.function_defined(['$x'])).to be_false
+ end
+ end
+
+ context "with future parser" do
+ before(:each) do
+ Puppet[:parser] = 'future'
+ end
+
+ it "is true when a future resource type reference is provided, and the resource is in the catalog" do
+ resource = Puppet::Resource.new("file", "/my/file")
+ @compiler.add_resource(@scope, resource)
+
+ resource_type = Puppet::Pops::Types::TypeFactory.resource('file', '/my/file')
+ expect(@scope.function_defined([resource_type])).to be_true
+ end
+
+ it "raises an argument error if you ask if Resource is defined" do
+ resource_type = Puppet::Pops::Types::TypeFactory.resource
+ expect { @scope.function_defined([resource_type]) }.to raise_error(ArgumentError, /reference to all.*type/)
+ end
+
+ it "is true if referencing a built in type" do
+ resource_type = Puppet::Pops::Types::TypeFactory.resource('file')
+ expect(@scope.function_defined([resource_type])).to be_true
+ end
+
+ it "is true if referencing a defined type" do
+ @scope.known_resource_types.add Puppet::Resource::Type.new(:definition, "yayness")
+ resource_type = Puppet::Pops::Types::TypeFactory.resource('yayness')
+ expect(@scope.function_defined([resource_type])).to be_true
+ end
+
+ it "is false if referencing an undefined type" do
+ resource_type = Puppet::Pops::Types::TypeFactory.resource('barbershops')
+ expect(@scope.function_defined([resource_type])).to be_false
+ end
+
+ it "is true when a future class reference type is provided" do
+ @scope.known_resource_types.add Puppet::Resource::Type.new(:hostclass, "cowabunga")
+
+ class_type = Puppet::Pops::Types::TypeFactory.host_class("cowabunga")
+ expect(@scope.function_defined([class_type])).to be_true
+ end
+
+ it "raises an argument error if you ask if Class is defined" do
+ class_type = Puppet::Pops::Types::TypeFactory.host_class
+ expect { @scope.function_defined([class_type]) }.to raise_error(ArgumentError, /reference to all.*class/)
+ end
end
end
diff --git a/spec/unit/parser/functions/epp_spec.rb b/spec/unit/parser/functions/epp_spec.rb
new file mode 100644
index 000000000..536478ee3
--- /dev/null
+++ b/spec/unit/parser/functions/epp_spec.rb
@@ -0,0 +1,88 @@
+
+require 'spec_helper'
+
+describe "the epp function" do
+ include PuppetSpec::Files
+
+ before :all do
+ Puppet::Parser::Functions.autoloader.loadall
+ end
+
+ before :each do
+ Puppet[:parser] = 'future'
+ end
+
+ let :node do Puppet::Node.new('localhost') end
+ let :compiler do Puppet::Parser::Compiler.new(node) end
+ let :scope do Puppet::Parser::Scope.new(compiler) end
+
+ context "when accessing scope variables as $ variables" do
+ it "looks up the value from the scope" do
+ scope["what"] = "are belong"
+ eval_template("all your base <%= $what %> to us").should == "all your base are belong to us"
+ end
+
+ it "get nil accessing a variable that does not exist" do
+ eval_template("<%= $kryptonite == undef %>").should == "true"
+ end
+
+ it "get nil accessing a variable that is undef" do
+ scope['undef_var'] = :undef
+ eval_template("<%= $undef_var == undef %>").should == "true"
+ end
+
+ it "gets shadowed variable if args are given" do
+ scope['phantom'] = 'of the opera'
+ eval_template_with_args("<%= $phantom == dragos %>", 'phantom' => 'dragos').should == "true"
+ end
+
+ it "gets shadowed variable if args are given and parameters are specified" do
+ scope['x'] = 'wrong one'
+ eval_template_with_args("<%-( $x )-%><%= $x == correct %>", 'x' => 'correct').should == "true"
+ end
+
+ it "raises an error if required variable is not given" do
+ scope['x'] = 'wrong one'
+ expect {
+ eval_template_with_args("<%-| $x |-%><%= $x == correct %>", 'y' => 'correct')
+ }.to raise_error(/no value given for required parameters x/)
+ end
+
+ it "raises an error if too many arguments are given" do
+ scope['x'] = 'wrong one'
+ expect {
+ eval_template_with_args("<%-| $x |-%><%= $x == correct %>", 'x' => 'correct', 'y' => 'surplus')
+ }.to raise_error(/Too many arguments: 2 for 1/)
+ end
+ end
+
+ # although never a problem with epp
+ it "is not interfered with by having a variable named 'string' (#14093)" do
+ scope['string'] = "this output should not be seen"
+ eval_template("some text that is static").should == "some text that is static"
+ end
+
+ it "has access to a variable named 'string' (#14093)" do
+ scope['string'] = "the string value"
+ eval_template("string was: <%= $string %>").should == "string was: the string value"
+ end
+
+
+ def eval_template_with_args(content, args_hash)
+ file_path = tmpdir('epp_spec_content')
+ filename = File.join(file_path, "template.epp")
+ File.open(filename, "w+") { |f| f.write(content) }
+
+ Puppet::Parser::Files.stubs(:find_template).returns(filename)
+ scope.function_epp(['template', args_hash])
+ end
+
+ def eval_template(content)
+ file_path = tmpdir('epp_spec_content')
+ filename = File.join(file_path, "template.epp")
+ File.open(filename, "w+") { |f| f.write(content) }
+
+ Puppet::Parser::Files.stubs(:find_template).returns(filename)
+ scope.function_epp(['template'])
+ end
+end
diff --git a/spec/unit/parser/functions/fqdn_rand_spec.rb b/spec/unit/parser/functions/fqdn_rand_spec.rb
index cef161000..49a169661 100755
--- a/spec/unit/parser/functions/fqdn_rand_spec.rb
+++ b/spec/unit/parser/functions/fqdn_rand_spec.rb
@@ -1,43 +1,46 @@
#! /usr/bin/env ruby
require 'spec_helper'
+require 'puppet_spec/scope'
describe "the fqdn_rand function" do
+ include PuppetSpec::Scope
+
it "provides a random number strictly less than the given max" do
fqdn_rand(3).should satisfy {|n| n.to_i < 3 }
end
it "provides the same 'random' value on subsequent calls for the same host" do
fqdn_rand(3).should eql(fqdn_rand(3))
end
it "considers the same host and same extra arguments to have the same random sequence" do
first_random = fqdn_rand(3, :extra_identifier => [1, "same", "host"])
second_random = fqdn_rand(3, :extra_identifier => [1, "same", "host"])
first_random.should eql(second_random)
end
it "allows extra arguments to control the random value on a single host" do
first_random = fqdn_rand(10000, :extra_identifier => [1, "different", "host"])
second_different_random = fqdn_rand(10000, :extra_identifier => [2, "different", "host"])
first_random.should_not eql(second_different_random)
end
it "should return different sequences of value for different hosts" do
val1 = fqdn_rand(1000000000, :host => "first.host.com")
val2 = fqdn_rand(1000000000, :host => "second.host.com")
val1.should_not eql(val2)
end
def fqdn_rand(max, args = {})
host = args[:host] || '127.0.0.1'
extra = args[:extra_identifier] || []
- scope = Puppet::Parser::Scope.new_for_test_harness('localhost')
+ scope = create_test_scope_for_node('localhost')
scope.stubs(:[]).with("::fqdn").returns(host)
scope.function_fqdn_rand([max] + extra)
end
end
diff --git a/spec/unit/parser/functions/generate_spec.rb b/spec/unit/parser/functions/generate_spec.rb
index 593703d63..832ac13ae 100755
--- a/spec/unit/parser/functions/generate_spec.rb
+++ b/spec/unit/parser/functions/generate_spec.rb
@@ -1,128 +1,132 @@
#! /usr/bin/env ruby
require 'spec_helper'
describe "the generate function" do
include PuppetSpec::Files
before :all do
Puppet::Parser::Functions.autoloader.loadall
end
let :node do Puppet::Node.new('localhost') end
let :compiler do Puppet::Parser::Compiler.new(node) end
let :scope do Puppet::Parser::Scope.new(compiler) end
it "should exist" do
Puppet::Parser::Functions.function("generate").should == "function_generate"
end
it "accept a fully-qualified path as a command" do
command = File.expand_path('/command/foo')
Dir.expects(:chdir).with(File.dirname(command)).returns("yay")
scope.function_generate([command]).should == "yay"
end
it "should not accept a relative path as a command" do
lambda { scope.function_generate(["command"]) }.should raise_error(Puppet::ParseError)
end
it "should not accept a command containing illegal characters" do
lambda { scope.function_generate([File.expand_path('/##/command')]) }.should raise_error(Puppet::ParseError)
end
it "should not accept a command containing spaces" do
lambda { scope.function_generate([File.expand_path('/com mand')]) }.should raise_error(Puppet::ParseError)
end
it "should not accept a command containing '..'" do
command = File.expand_path("/command/../")
lambda { scope.function_generate([command]) }.should raise_error(Puppet::ParseError)
end
it "should execute the generate script with the correct working directory" do
command = File.expand_path("/command")
Dir.expects(:chdir).with(File.dirname(command)).returns("yay")
scope.function_generate([command]).should == 'yay'
end
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")
scope.function_generate([command]).should == 'yay'
end
it "should accept lower-case drive letters" do
command = 'd:/command/foo'
Dir.expects(:chdir).with(File.dirname(command)).returns("yay")
scope.function_generate([command]).should == 'yay'
end
it "should accept upper-case drive letters" do
command = 'D:/command/foo'
Dir.expects(:chdir).with(File.dirname(command)).returns("yay")
scope.function_generate([command]).should == 'yay'
end
it "should accept forward and backslashes in the path" do
command = 'D:\command/foo\bar'
Dir.expects(:chdir).with(File.dirname(command)).returns("yay")
scope.function_generate([command]).should == 'yay'
end
it "should reject colons when not part of the drive letter" do
lambda { scope.function_generate(['C:/com:mand']) }.should raise_error(Puppet::ParseError)
end
it "should reject root drives" do
lambda { scope.function_generate(['C:/']) }.should raise_error(Puppet::ParseError)
end
end
describe "on non-Windows", :as_platform => :posix do
it "should reject backslashes" do
lambda { scope.function_generate(['/com\\mand']) }.should raise_error(Puppet::ParseError)
end
it "should accept plus and dash" do
command = "/var/folders/9z/9zXImgchH8CZJh6SgiqS2U+++TM/-Tmp-/foo"
Dir.expects(:chdir).with(File.dirname(command)).returns("yay")
scope.function_generate([command]).should == 'yay'
end
end
let :command do
cmd = tmpfile('function_generate')
if Puppet.features.microsoft_windows?
cmd += '.bat'
text = '@echo off' + "\n" + 'echo a-%1 b-%2'
else
text = '#!/bin/sh' + "\n" + 'echo a-$1 b-$2'
end
File.open(cmd, 'w') {|fh| fh.puts text }
File.chmod 0700, cmd
cmd
end
+ after :each do
+ File.delete(command) if Puppet::FileSystem.exist?(command)
+ end
+
it "should call generator with no arguments" do
scope.function_generate([command]).should == "a- b-\n"
end
it "should call generator with one argument" do
scope.function_generate([command, 'one']).should == "a-one b-\n"
end
it "should call generator with wo arguments" do
scope.function_generate([command, 'one', 'two']).should == "a-one b-two\n"
end
it "should fail if generator is not absolute" do
- expect { scope.function_generate(['boo']) }.to raise_error Puppet::ParseError
+ expect { scope.function_generate(['boo']) }.to raise_error(Puppet::ParseError)
end
it "should fail if generator fails" do
- expect { scope.function_generate(['/boo']) }.to raise_error Puppet::ParseError
+ expect { scope.function_generate(['/boo']) }.to raise_error(Puppet::ParseError)
end
end
diff --git a/spec/unit/parser/functions/hiera_array_spec.rb b/spec/unit/parser/functions/hiera_array_spec.rb
index e8efb727f..ca9bb5c88 100644
--- a/spec/unit/parser/functions/hiera_array_spec.rb
+++ b/spec/unit/parser/functions/hiera_array_spec.rb
@@ -1,23 +1,26 @@
require 'spec_helper'
+require 'puppet_spec/scope'
describe 'Puppet::Parser::Functions#hiera_array' do
+ include PuppetSpec::Scope
+
before :each do
Puppet[:hiera_config] = PuppetSpec::Files.tmpfile('hiera_config')
end
- let :scope do Puppet::Parser::Scope.new_for_test_harness('foo') end
+ let :scope do create_test_scope_for_node('foo') end
it 'should require a key argument' do
expect { scope.function_hiera_array([]) }.to raise_error(ArgumentError)
end
it 'should raise a useful error when nil is returned' do
Hiera.any_instance.expects(:lookup).returns(nil)
expect { scope.function_hiera_array(["badkey"]) }.to raise_error(Puppet::ParseError, /Could not find data item badkey/ )
end
it 'should use the array resolution_type' do
- Hiera.any_instance.expects(:lookup).with() { |*args| args[4].should be :array }.returns([])
+ Hiera.any_instance.expects(:lookup).with() { |*args| args[4].should be(:array) }.returns([])
scope.function_hiera_array(['key'])
end
end
diff --git a/spec/unit/parser/functions/hiera_hash_spec.rb b/spec/unit/parser/functions/hiera_hash_spec.rb
index a345a6c7f..9b89b2efd 100644
--- a/spec/unit/parser/functions/hiera_hash_spec.rb
+++ b/spec/unit/parser/functions/hiera_hash_spec.rb
@@ -1,19 +1,22 @@
require 'spec_helper'
+require 'puppet_spec/scope'
describe 'Puppet::Parser::Functions#hiera_hash' do
- let :scope do Puppet::Parser::Scope.new_for_test_harness('foo') end
+ include PuppetSpec::Scope
+
+ let :scope do create_test_scope_for_node('foo') end
it 'should require a key argument' do
expect { scope.function_hiera_hash([]) }.to raise_error(ArgumentError)
end
it 'should raise a useful error when nil is returned' do
Hiera.any_instance.expects(:lookup).returns(nil)
expect { scope.function_hiera_hash(["badkey"]) }.to raise_error(Puppet::ParseError, /Could not find data item badkey/ )
end
it 'should use the hash resolution_type' do
Hiera.any_instance.expects(:lookup).with() { |*args| args[4].should be :hash }.returns({})
scope.function_hiera_hash(['key'])
end
end
diff --git a/spec/unit/parser/functions/hiera_include_spec.rb b/spec/unit/parser/functions/hiera_include_spec.rb
index 76a2c5dec..2909e07ae 100644
--- a/spec/unit/parser/functions/hiera_include_spec.rb
+++ b/spec/unit/parser/functions/hiera_include_spec.rb
@@ -1,36 +1,39 @@
require 'spec_helper'
+require 'puppet_spec/scope'
describe 'Puppet::Parser::Functions#hiera_include' do
- let :scope do Puppet::Parser::Scope.new_for_test_harness('foo') end
+ include PuppetSpec::Scope
+
+ let :scope do create_test_scope_for_node('foo') end
before :each do
Puppet[:hiera_config] = PuppetSpec::Files.tmpfile('hiera_config')
end
it 'should require a key argument' do
expect { scope.function_hiera_include([]) }.to raise_error(ArgumentError)
end
it 'should raise a useful error when nil is returned' do
HieraPuppet.expects(:lookup).returns(nil)
expect { scope.function_hiera_include(["badkey"]) }.
to raise_error(Puppet::ParseError, /Could not find data item badkey/ )
end
it 'should use the array resolution_type' do
- HieraPuppet.expects(:lookup).with() { |*args| args[4].should be :array }.returns(['someclass'])
- expect { scope.function_hiera_include(['key']) }.to raise_error Puppet::Error, /Could not find class someclass/
+ HieraPuppet.expects(:lookup).with() { |*args| args[4].should be(:array) }.returns(['someclass'])
+ expect { scope.function_hiera_include(['key']) }.to raise_error(Puppet::Error, /Could not find class someclass/)
end
it 'should call the `include` function with the classes' do
HieraPuppet.expects(:lookup).returns %w[foo bar baz]
scope.expects(:function_include).with([%w[foo bar baz]])
scope.function_hiera_include(['key'])
end
it 'should not raise an error if the resulting hiera lookup returns an empty array' do
HieraPuppet.expects(:lookup).returns []
expect { scope.function_hiera_include(['key']) }.to_not raise_error
end
end
diff --git a/spec/unit/parser/functions/hiera_spec.rb b/spec/unit/parser/functions/hiera_spec.rb
index f22eee9d4..db31fdf82 100755
--- a/spec/unit/parser/functions/hiera_spec.rb
+++ b/spec/unit/parser/functions/hiera_spec.rb
@@ -1,19 +1,22 @@
require 'spec_helper'
+require 'puppet_spec/scope'
describe 'Puppet::Parser::Functions#hiera' do
- let :scope do Puppet::Parser::Scope.new_for_test_harness('foo') end
+ include PuppetSpec::Scope
+
+ let :scope do create_test_scope_for_node('foo') end
it 'should require a key argument' do
expect { scope.function_hiera([]) }.to raise_error(ArgumentError)
end
it 'should raise a useful error when nil is returned' do
Hiera.any_instance.expects(:lookup).returns(nil)
expect { scope.function_hiera(["badkey"]) }.to raise_error(Puppet::ParseError, /Could not find data item badkey/ )
end
it 'should use the priority resolution_type' do
- Hiera.any_instance.expects(:lookup).with() { |*args| args[4].should be :priority }.returns('foo_result')
+ Hiera.any_instance.expects(:lookup).with() { |*args| args[4].should be(:priority) }.returns('foo_result')
scope.function_hiera(['key']).should == 'foo_result'
end
end
diff --git a/spec/unit/parser/functions/include_spec.rb b/spec/unit/parser/functions/include_spec.rb
index fbe51e7fb..c1a5cbd5c 100755
--- a/spec/unit/parser/functions/include_spec.rb
+++ b/spec/unit/parser/functions/include_spec.rb
@@ -1,52 +1,51 @@
#! /usr/bin/env ruby
require 'spec_helper'
describe "the 'include' function" do
before :all do
Puppet::Parser::Functions.autoloader.loadall
end
before :each do
- Puppet::Node::Environment.stubs(:current).returns(nil)
@compiler = Puppet::Parser::Compiler.new(Puppet::Node.new("foo"))
@scope = Puppet::Parser::Scope.new(@compiler)
end
it "should exist" do
Puppet::Parser::Functions.function("include").should == "function_include"
end
it "should include a single class" do
inc = "foo"
@compiler.expects(:evaluate_classes).with {|klasses,parser,lazy| klasses == [inc]}.returns([inc])
@scope.function_include(["foo"])
end
it "should include multiple classes" do
inc = ["foo","bar"]
@compiler.expects(:evaluate_classes).with {|klasses,parser,lazy| klasses == inc}.returns(inc)
@scope.function_include(["foo","bar"])
end
it "should include multiple classes passed in an array" do
inc = ["foo","bar"]
@compiler.expects(:evaluate_classes).with {|klasses,parser,lazy| klasses == inc}.returns(inc)
@scope.function_include([["foo","bar"]])
end
it "should flatten nested arrays" do
inc = ["foo","bar","baz"]
@compiler.expects(:evaluate_classes).with {|klasses,parser,lazy| klasses == inc}.returns(inc)
@scope.function_include([["foo","bar"],"baz"])
end
it "should not lazily evaluate the included class" do
@compiler.expects(:evaluate_classes).with {|klasses,parser,lazy| lazy == false}.returns("foo")
@scope.function_include(["foo"])
end
it "should raise if the class is not found" do
@scope.stubs(:source).returns(true)
expect { @scope.function_include(["nosuchclass"]) }.to raise_error Puppet::Error
end
end
diff --git a/spec/unit/parser/functions/inline_epp_spec.rb b/spec/unit/parser/functions/inline_epp_spec.rb
new file mode 100644
index 000000000..44b24528b
--- /dev/null
+++ b/spec/unit/parser/functions/inline_epp_spec.rb
@@ -0,0 +1,82 @@
+
+require 'spec_helper'
+
+describe "the inline_epp function" do
+ include PuppetSpec::Files
+
+ before :all do
+ Puppet::Parser::Functions.autoloader.loadall
+ end
+
+ before :each do
+ Puppet[:parser] = 'future'
+ end
+
+ let :node do Puppet::Node.new('localhost') end
+ let :compiler do Puppet::Parser::Compiler.new(node) end
+ let :scope do Puppet::Parser::Scope.new(compiler) end
+
+ context "when accessing scope variables as $ variables" do
+ it "looks up the value from the scope" do
+ scope["what"] = "are belong"
+ eval_template("all your base <%= $what %> to us").should == "all your base are belong to us"
+ end
+
+ it "get nil accessing a variable that does not exist" do
+ eval_template("<%= $kryptonite == undef %>").should == "true"
+ end
+
+ it "get nil accessing a variable that is undef" do
+ scope['undef_var'] = :undef
+ eval_template("<%= $undef_var == undef %>").should == "true"
+ end
+
+ it "gets shadowed variable if args are given" do
+ scope['phantom'] = 'of the opera'
+ eval_template_with_args("<%= $phantom == dragos %>", 'phantom' => 'dragos').should == "true"
+ end
+
+ it "gets shadowed variable if args are given and parameters are specified" do
+ scope['x'] = 'wrong one'
+ eval_template_with_args("<%-| $x |-%><%= $x == correct %>", 'x' => 'correct').should == "true"
+ end
+
+ it "raises an error if required variable is not given" do
+ scope['x'] = 'wrong one'
+ expect {
+ eval_template_with_args("<%-| $x |-%><%= $x == correct %>", 'y' => 'correct')
+ }.to raise_error(/no value given for required parameters x/)
+ end
+
+ it "raises an error if too many arguments are given" do
+ scope['x'] = 'wrong one'
+ expect {
+ eval_template_with_args("<%-| $x |-%><%= $x == correct %>", 'x' => 'correct', 'y' => 'surplus')
+ }.to raise_error(/Too many arguments: 2 for 1/)
+ end
+ end
+
+ it "renders a block expression" do
+ eval_template_with_args("<%= {($x) $x + 1} %>", 'x' => 2).should == "3"
+ end
+
+ # although never a problem with epp
+ it "is not interfered with by having a variable named 'string' (#14093)" do
+ scope['string'] = "this output should not be seen"
+ eval_template("some text that is static").should == "some text that is static"
+ end
+
+ it "has access to a variable named 'string' (#14093)" do
+ scope['string'] = "the string value"
+ eval_template("string was: <%= $string %>").should == "string was: the string value"
+ end
+
+
+ def eval_template_with_args(content, args_hash)
+ scope.function_inline_epp([content, args_hash])
+ end
+
+ def eval_template(content)
+ scope.function_inline_epp([content])
+ end
+end
diff --git a/spec/unit/parser/functions/lookup_spec.rb b/spec/unit/parser/functions/lookup_spec.rb
index a468ba7ec..b6d909b6f 100644
--- a/spec/unit/parser/functions/lookup_spec.rb
+++ b/spec/unit/parser/functions/lookup_spec.rb
@@ -1,96 +1,146 @@
require 'spec_helper'
require 'puppet/pops'
require 'stringio'
+require 'puppet_spec/scope'
describe "lookup function" do
+ include PuppetSpec::Scope
+
before(:each) do
Puppet[:binder] = true
end
it "must be called with at least a name to lookup" do
scope = scope_with_injections_from(bound(bindings))
expect do
scope.function_lookup([])
end.to raise_error(ArgumentError, /Wrong number of arguments/)
end
it "looks up a value that exists" do
scope = scope_with_injections_from(bound(bind_single("a_value", "something")))
-
expect(scope.function_lookup(['a_value'])).to eq('something')
end
- it "returns :undef when the requested value is not bound" do
+ it "searches for first found if given several names" do
scope = scope_with_injections_from(bound(bind_single("a_value", "something")))
+ expect(scope.function_lookup([['b_value', 'a_value', 'c_value']])).to eq('something')
+ end
- expect(scope.function_lookup(['not_bound_value'])).to eq(:undef)
+ it "override wins over bound" do
+ scope = scope_with_injections_from(bound(bind_single("a_value", "something")))
+ options = {:override => { 'a_value' => 'something_overridden' }}
+ expect(scope.function_lookup(['a_value', options])).to eq('something_overridden')
end
- it "raises an error when the bound type is not assignable to the requested type" do
+ it "extra option is used if nothing is found" do
+ scope = scope_with_injections_from(bound(bind_single("another_value", "something")))
+ options = {:extra => { 'a_value' => 'something_extra' }}
+ expect(scope.function_lookup(['a_value', options])).to eq('something_extra')
+ end
+
+ it "hiera is called to lookup if value is not bound" do
+ Puppet::Parser::Scope.any_instance.stubs(:function_hiera).returns('from_hiera')
+ scope = scope_with_injections_from(bound(bind_single("another_value", "something")))
+ expect(scope.function_lookup(['a_value'])).to eq('from_hiera')
+ end
+
+ it "returns :undef when the requested value is not bound and undef is accepted" do
+ scope = scope_with_injections_from(bound(bind_single("a_value", "something")))
+ expect(scope.function_lookup(['not_bound_value',{'accept_undef' => true}])).to eq(:undef)
+ end
+
+ it "fails if the requested value is not bound and undef is not allowed" do
+ scope = scope_with_injections_from(bound(bind_single("a_value", "something")))
+ expect do
+ scope.function_lookup(['not_bound_value'])
+ end.to raise_error(/did not find a value for the name 'not_bound_value'/)
+ end
+
+ it "returns the given default value when the requested value is not bound" do
+ scope = scope_with_injections_from(bound(bind_single("a_value", "something")))
+ expect(scope.function_lookup(['not_bound_value','String', 'cigar'])).to eq('cigar')
+ end
+
+ it "accepts values given in a hash of options" do
scope = scope_with_injections_from(bound(bind_single("a_value", "something")))
+ expect(scope.function_lookup(['not_bound_value',{'type' => 'String', 'default' => 'cigar'}])).to eq('cigar')
+ end
+ it "raises an error when the bound type is not assignable to the requested type" do
+ scope = scope_with_injections_from(bound(bind_single("a_value", "something")))
expect do
scope.function_lookup(['a_value', 'Integer'])
end.to raise_error(ArgumentError, /incompatible type, expected: Integer, got: String/)
end
it "returns the value if the bound type is assignable to the requested type" do
typed_bindings = bindings
typed_bindings.bind().string().name("a_value").to("something")
scope = scope_with_injections_from(bound(typed_bindings))
-
expect(scope.function_lookup(['a_value', 'Data'])).to eq("something")
end
it "yields to a given lambda and returns the result" do
scope = scope_with_injections_from(bound(bind_single("a_value", "something")))
-
expect(scope.function_lookup(['a_value', ast_lambda('|$x|{something_else}')])).to eq('something_else')
end
- it "yields to a given lambda and returns the result when giving name and type" do
+ it "fails if given lambda produces undef" do
scope = scope_with_injections_from(bound(bind_single("a_value", "something")))
+ expect do
+ scope.function_lookup(['a_value', ast_lambda('|$x|{undef}')])
+ end.to raise_error(/did not find a value for the name 'a_value'/)
+ end
+ it "yields name and result to a given lambda" do
+ scope = scope_with_injections_from(bound(bind_single("a_value", "something")))
+ expect(scope.function_lookup(['a_value', ast_lambda('|$name, $result|{[$name, $result]}')])).to eq(['a_value', 'something'])
+ end
+
+ it "yields name and result and default to a given lambda" do
+ scope = scope_with_injections_from(bound(bind_single("a_value", "something")))
+ expect(scope.function_lookup(['a_value', {'default' => 'cigar'},
+ ast_lambda('|$name, $result, $d|{[$name, $result, $d]}')])).to eq(['a_value', 'something', 'cigar'])
+ end
+
+ it "yields to a given lambda and returns the result when giving name and type" do
+ scope = scope_with_injections_from(bound(bind_single("a_value", "something")))
expect(scope.function_lookup(['a_value', 'String', ast_lambda('|$x|{something_else}')])).to eq('something_else')
end
it "yields :undef when value is not found and using a lambda" do
scope = scope_with_injections_from(bound(bind_single("a_value", "something")))
-
expect(scope.function_lookup(['not_bound_value', ast_lambda('|$x|{ if $x == undef {good} else {bad}}')])).to eq('good')
end
def scope_with_injections_from(binder)
injector = Puppet::Pops::Binder::Injector.new(binder)
- scope = Puppet::Parser::Scope.new_for_test_harness('testing')
+ scope = create_test_scope_for_node('testing')
scope.compiler.injector = injector
-
scope
end
def bindings
Puppet::Pops::Binder::BindingsFactory.named_bindings("testing")
end
def bind_single(name, value)
local_bindings = Puppet::Pops::Binder::BindingsFactory.named_bindings("testing")
local_bindings.bind().name(name).to(value)
local_bindings
end
def bound(local_bindings)
- binder = Puppet::Pops::Binder::Binder.new
- binder.define_categories(Puppet::Pops::Binder::BindingsFactory.categories([]))
- binder.define_layers(Puppet::Pops::Binder::BindingsFactory.layered_bindings(Puppet::Pops::Binder::BindingsFactory.named_layer('test layer', local_bindings.model)))
-
- binder
+ layered_bindings = Puppet::Pops::Binder::BindingsFactory.layered_bindings(Puppet::Pops::Binder::BindingsFactory.named_layer('test layer', local_bindings.model))
+ Puppet::Pops::Binder::Binder.new(layered_bindings)
end
def ast_lambda(puppet_source)
puppet_source = "fake_func() " + puppet_source
model = Puppet::Pops::Parser::EvaluatingParser.new().parse_string(puppet_source, __FILE__).current
- model = model.lambda
+ model = model.body.lambda
Puppet::Pops::Model::AstTransformer.new(@file_source, nil).transform(model)
end
end
diff --git a/spec/unit/parser/functions_spec.rb b/spec/unit/parser/functions_spec.rb
index 8ad33c874..91013586c 100755
--- a/spec/unit/parser/functions_spec.rb
+++ b/spec/unit/parser/functions_spec.rb
@@ -1,201 +1,135 @@
#! /usr/bin/env ruby
require 'spec_helper'
describe Puppet::Parser::Functions do
def callable_functions_from(mod)
Class.new { include mod }.new
end
- it "should have a method for returning an environment-specific module" do
- Puppet::Parser::Functions.environment_module(Puppet::Node::Environment.new("myenv")).should be_instance_of(Module)
- end
+ let(:function_module) { Puppet::Parser::Functions.environment_module(Puppet.lookup(:current_environment)) }
+
+ let(:environment) { Puppet::Node::Environment.create(:myenv, [], '') }
- it "should use the current default environment if no environment is provided" do
- Puppet::Parser::Functions.environment_module.should be_instance_of(Module)
+ before do
+ Puppet::Parser::Functions.reset
end
- it "should be able to retrieve environment modules asked for by name rather than instance" do
- Puppet::Parser::Functions.environment_module(Puppet::Node::Environment.new("myenv")).should equal(Puppet::Parser::Functions.environment_module("myenv"))
+ it "should have a method for returning an environment-specific module" do
+ Puppet::Parser::Functions.environment_module(environment).should be_instance_of(Module)
end
describe "when calling newfunction" do
- let(:function_module) { Module.new }
- before do
- Puppet::Parser::Functions.stubs(:environment_module).returns(function_module)
- end
-
it "should create the function in the environment module" do
Puppet::Parser::Functions.newfunction("name", :type => :rvalue) { |args| }
function_module.should be_method_defined :function_name
end
it "should warn if the function already exists" do
Puppet::Parser::Functions.newfunction("name", :type => :rvalue) { |args| }
Puppet.expects(:warning)
Puppet::Parser::Functions.newfunction("name", :type => :rvalue) { |args| }
end
it "should raise an error if the function type is not correct" do
expect { Puppet::Parser::Functions.newfunction("name", :type => :unknown) { |args| } }.to raise_error Puppet::DevError, "Invalid statement type :unknown"
end
- it "instruments the function to profiles the execution" do
+ it "instruments the function to profile the execution" do
messages = []
Puppet::Util::Profiler.current = Puppet::Util::Profiler::WallClock.new(proc { |msg| messages << msg }, "id")
Puppet::Parser::Functions.newfunction("name", :type => :rvalue) { |args| }
callable_functions_from(function_module).function_name([])
messages.first.should =~ /Called name/
end
end
describe "when calling function to test function existence" do
- let(:function_module) { Module.new }
- before do
- Puppet::Parser::Functions.stubs(:environment_module).returns(function_module)
- end
-
it "should return false if the function doesn't exist" do
Puppet::Parser::Functions.autoloader.stubs(:load)
Puppet::Parser::Functions.function("name").should be_false
end
it "should return its name if the function exists" do
Puppet::Parser::Functions.newfunction("name", :type => :rvalue) { |args| }
Puppet::Parser::Functions.function("name").should == "function_name"
end
it "should try to autoload the function if it doesn't exist yet" do
Puppet::Parser::Functions.autoloader.expects(:load)
Puppet::Parser::Functions.function("name")
end
+
+ it "combines functions from the root with those from the current environment" do
+ Puppet.override(:current_environment => Puppet.lookup(:root_environment)) do
+ Puppet::Parser::Functions.newfunction("onlyroot", :type => :rvalue) do |args|
+ end
+ end
+
+ Puppet.override(:current_environment => Puppet::Node::Environment.create(:other, [], '')) do
+ Puppet::Parser::Functions.newfunction("other_env", :type => :rvalue) do |args|
+ end
+
+ expect(Puppet::Parser::Functions.function("onlyroot")).to eq("function_onlyroot")
+ expect(Puppet::Parser::Functions.function("other_env")).to eq("function_other_env")
+ end
+
+ expect(Puppet::Parser::Functions.function("other_env")).to be_false
+ end
end
describe "when calling function to test arity" do
let(:function_module) { Module.new }
before do
Puppet::Parser::Functions.stubs(:environment_module).returns(function_module)
end
it "should raise an error if the function is called with too many arguments" do
Puppet::Parser::Functions.newfunction("name", :arity => 2) { |args| }
expect { callable_functions_from(function_module).function_name([1,2,3]) }.to raise_error ArgumentError
end
it "should raise an error if the function is called with too few arguments" do
Puppet::Parser::Functions.newfunction("name", :arity => 2) { |args| }
expect { callable_functions_from(function_module).function_name([1]) }.to raise_error ArgumentError
end
it "should not raise an error if the function is called with correct number of arguments" do
Puppet::Parser::Functions.newfunction("name", :arity => 2) { |args| }
expect { callable_functions_from(function_module).function_name([1,2]) }.to_not raise_error
end
it "should raise an error if the variable arg function is called with too few arguments" do
Puppet::Parser::Functions.newfunction("name", :arity => -3) { |args| }
expect { callable_functions_from(function_module).function_name([1]) }.to raise_error ArgumentError
end
it "should not raise an error if the variable arg function is called with correct number of arguments" do
Puppet::Parser::Functions.newfunction("name", :arity => -3) { |args| }
expect { callable_functions_from(function_module).function_name([1,2]) }.to_not raise_error
end
it "should not raise an error if the variable arg function is called with more number of arguments" do
Puppet::Parser::Functions.newfunction("name", :arity => -3) { |args| }
expect { callable_functions_from(function_module).function_name([1,2,3]) }.to_not raise_error
end
end
describe "::arity" do
it "returns the given arity of a function" do
Puppet::Parser::Functions.newfunction("name", :arity => 4) { |args| }
Puppet::Parser::Functions.arity(:name).should == 4
end
it "returns -1 if no arity is given" do
Puppet::Parser::Functions.newfunction("name") { |args| }
Puppet::Parser::Functions.arity(:name).should == -1
end
end
-
- describe "::get_function" do
- it "can retrieve a function defined on the *root* environment" do
- $environment = nil
- function = Puppet::Parser::Functions.newfunction("atest", :type => :rvalue) do
- nil
- end
-
- Puppet::Node::Environment.current = "test_env"
- Puppet::Parser::Functions.send(:get_function, "atest").should equal(function)
- end
-
- it "can retrieve a function from the current environment" do
- Puppet::Node::Environment.current = "test_env"
- function = Puppet::Parser::Functions.newfunction("atest", :type => :rvalue) do
- nil
- end
-
- Puppet::Parser::Functions.send(:get_function, "atest").should equal(function)
- end
-
- it "takes a function in the current environment over one in the root" do
- root = Puppet::Node::Environment.root
- env = Puppet::Node::Environment.current = "test_env"
- func1 = {:type => :rvalue, :name => :testfunc, :extra => :func1}
- func2 = {:type => :rvalue, :name => :testfunc, :extra => :func2}
- Puppet::Parser::Functions.instance_eval do
- @functions[Puppet::Node::Environment.root][:atest] = func1
- @functions[Puppet::Node::Environment.current][:atest] = func2
- end
-
- Puppet::Parser::Functions.send(:get_function, "atest").should equal(func2)
- end
- end
-
- describe "::merged_functions" do
- it "returns functions in both the current and root environment" do
- $environment = nil
- func_a = Puppet::Parser::Functions.newfunction("test_a", :type => :rvalue) do
- nil
- end
- Puppet::Node::Environment.current = "test_env"
- func_b = Puppet::Parser::Functions.newfunction("test_b", :type => :rvalue) do
- nil
- end
-
- Puppet::Parser::Functions.send(:merged_functions).should include(:test_a, :test_b)
- end
-
- it "returns functions from the current environment over the root environment" do
- root = Puppet::Node::Environment.root
- env = Puppet::Node::Environment.current = "test_env"
- func1 = {:type => :rvalue, :name => :testfunc, :extra => :func1}
- func2 = {:type => :rvalue, :name => :testfunc, :extra => :func2}
- Puppet::Parser::Functions.instance_eval do
- @functions[Puppet::Node::Environment.root][:atest] = func1
- @functions[Puppet::Node::Environment.current][:atest] = func2
- end
-
- Puppet::Parser::Functions.send(:merged_functions)[:atest].should equal(func2)
- end
- end
-
- describe "::add_function" do
- it "adds functions to the current environment" do
- func = {:type => :rvalue, :name => :testfunc}
- Puppet::Node::Environment.current = "add_function_test"
- Puppet::Parser::Functions.send(:add_function, :testfunc, func)
-
- Puppet::Parser::Functions.instance_variable_get(:@functions)[Puppet::Node::Environment.root].should_not include(:testfunc)
- Puppet::Parser::Functions.instance_variable_get(:@functions)[Puppet::Node::Environment.current].should include(:testfunc)
- end
- end
end
diff --git a/spec/unit/parser/lexer_spec.rb b/spec/unit/parser/lexer_spec.rb
index 972a8f1bf..62234e214 100755
--- a/spec/unit/parser/lexer_spec.rb
+++ b/spec/unit/parser/lexer_spec.rb
@@ -1,868 +1,868 @@
#! /usr/bin/env ruby
require 'spec_helper'
require 'puppet/parser/lexer'
# This is a special matcher to match easily lexer output
RSpec::Matchers.define :be_like do |*expected|
match do |actual|
expected.zip(actual).all? { |e,a| !e or a[0] == e or (e.is_a? Array and a[0] == e[0] and (a[1] == e[1] or (a[1].is_a?(Hash) and a[1][:value] == e[1]))) }
end
end
__ = nil
def tokens_scanned_from(s)
lexer = Puppet::Parser::Lexer.new
lexer.string = s
lexer.fullscan[0..-2]
end
describe Puppet::Parser::Lexer do
describe "when reading strings" do
before { @lexer = Puppet::Parser::Lexer.new }
it "should increment the line count for every carriage return in the string" do
@lexer.line = 10
@lexer.string = "this\nis\natest'"
@lexer.slurpstring("'")
@lexer.line.should == 12
end
it "should not increment the line count for escapes in the string" do
@lexer.line = 10
@lexer.string = "this\\nis\\natest'"
@lexer.slurpstring("'")
@lexer.line.should == 10
end
it "should not think the terminator is escaped, when preceeded by an even number of backslashes" do
@lexer.line = 10
@lexer.string = "here\nis\nthe\nstring\\\\'with\nextra\njunk"
@lexer.slurpstring("'")
@lexer.line.should == 13
end
{
'r' => "\r",
'n' => "\n",
't' => "\t",
's' => " "
}.each do |esc, expected_result|
it "should recognize \\#{esc} sequence" do
@lexer.string = "\\#{esc}'"
@lexer.slurpstring("'")[0].should == expected_result
end
end
end
end
describe Puppet::Parser::Lexer::Token do
before do
@token = Puppet::Parser::Lexer::Token.new(%r{something}, :NAME)
end
[:regex, :name, :string, :skip, :incr_line, :skip_text, :accumulate].each do |param|
it "should have a #{param.to_s} reader" do
@token.should be_respond_to(param)
end
it "should have a #{param.to_s} writer" do
@token.should be_respond_to(param.to_s + "=")
end
end
end
describe Puppet::Parser::Lexer::Token, "when initializing" do
it "should create a regex if the first argument is a string" do
Puppet::Parser::Lexer::Token.new("something", :NAME).regex.should == %r{something}
end
it "should set the string if the first argument is one" do
Puppet::Parser::Lexer::Token.new("something", :NAME).string.should == "something"
end
it "should set the regex if the first argument is one" do
Puppet::Parser::Lexer::Token.new(%r{something}, :NAME).regex.should == %r{something}
end
end
describe Puppet::Parser::Lexer::TokenList do
before do
@list = Puppet::Parser::Lexer::TokenList.new
end
it "should have a method for retrieving tokens by the name" do
token = @list.add_token :name, "whatever"
@list[:name].should equal(token)
end
it "should have a method for retrieving string tokens by the string" do
token = @list.add_token :name, "whatever"
@list.lookup("whatever").should equal(token)
end
it "should add tokens to the list when directed" do
token = @list.add_token :name, "whatever"
@list[:name].should equal(token)
end
it "should have a method for adding multiple tokens at once" do
@list.add_tokens "whatever" => :name, "foo" => :bar
@list[:name].should_not be_nil
@list[:bar].should_not be_nil
end
it "should fail to add tokens sharing a name with an existing token" do
@list.add_token :name, "whatever"
expect { @list.add_token :name, "whatever" }.to raise_error(ArgumentError)
end
it "should set provided options on tokens being added" do
token = @list.add_token :name, "whatever", :skip_text => true
token.skip_text.should == true
end
it "should define any provided blocks as a :convert method" do
token = @list.add_token(:name, "whatever") do "foo" end
token.convert.should == "foo"
end
it "should store all string tokens in the :string_tokens list" do
one = @list.add_token(:name, "1")
@list.string_tokens.should be_include(one)
end
it "should store all regex tokens in the :regex_tokens list" do
one = @list.add_token(:name, %r{one})
@list.regex_tokens.should be_include(one)
end
it "should not store string tokens in the :regex_tokens list" do
one = @list.add_token(:name, "1")
@list.regex_tokens.should_not be_include(one)
end
it "should not store regex tokens in the :string_tokens list" do
one = @list.add_token(:name, %r{one})
@list.string_tokens.should_not be_include(one)
end
it "should sort the string tokens inversely by length when asked" do
one = @list.add_token(:name, "1")
two = @list.add_token(:other, "12")
@list.sort_tokens
@list.string_tokens.should == [two, one]
end
end
describe Puppet::Parser::Lexer::TOKENS do
before do
@lexer = Puppet::Parser::Lexer.new
end
{
:LBRACK => '[',
:RBRACK => ']',
:LBRACE => '{',
:RBRACE => '}',
:LPAREN => '(',
:RPAREN => ')',
:EQUALS => '=',
:ISEQUAL => '==',
:GREATEREQUAL => '>=',
:GREATERTHAN => '>',
:LESSTHAN => '<',
:LESSEQUAL => '<=',
:NOTEQUAL => '!=',
:NOT => '!',
:COMMA => ',',
:DOT => '.',
:COLON => ':',
:AT => '@',
:LLCOLLECT => '<<|',
:RRCOLLECT => '|>>',
:LCOLLECT => '<|',
:RCOLLECT => '|>',
:SEMIC => ';',
:QMARK => '?',
:BACKSLASH => '\\',
:FARROW => '=>',
:PARROW => '+>',
:APPENDS => '+=',
:PLUS => '+',
:MINUS => '-',
:DIV => '/',
:TIMES => '*',
:LSHIFT => '<<',
:RSHIFT => '>>',
:MATCH => '=~',
:NOMATCH => '!~',
:IN_EDGE => '->',
:OUT_EDGE => '<-',
:IN_EDGE_SUB => '~>',
:OUT_EDGE_SUB => '<~',
}.each do |name, string|
it "should have a token named #{name.to_s}" do
Puppet::Parser::Lexer::TOKENS[name].should_not be_nil
end
it "should match '#{string}' for the token #{name.to_s}" do
Puppet::Parser::Lexer::TOKENS[name].string.should == string
end
end
{
"case" => :CASE,
"class" => :CLASS,
"default" => :DEFAULT,
"define" => :DEFINE,
"import" => :IMPORT,
"if" => :IF,
"elsif" => :ELSIF,
"else" => :ELSE,
"inherits" => :INHERITS,
"node" => :NODE,
"and" => :AND,
"or" => :OR,
"undef" => :UNDEF,
"false" => :FALSE,
"true" => :TRUE,
"in" => :IN,
"unless" => :UNLESS,
}.each do |string, name|
it "should have a keyword named #{name.to_s}" do
Puppet::Parser::Lexer::KEYWORDS[name].should_not be_nil
end
it "should have the keyword for #{name.to_s} set to #{string}" do
Puppet::Parser::Lexer::KEYWORDS[name].string.should == string
end
end
# These tokens' strings don't matter, just that the tokens exist.
[:STRING, :DQPRE, :DQMID, :DQPOST, :BOOLEAN, :NAME, :NUMBER, :COMMENT, :MLCOMMENT, :RETURN, :SQUOTE, :DQUOTE, :VARIABLE].each do |name|
it "should have a token named #{name.to_s}" do
Puppet::Parser::Lexer::TOKENS[name].should_not be_nil
end
end
end
describe Puppet::Parser::Lexer::TOKENS[:CLASSREF] do
before { @token = Puppet::Parser::Lexer::TOKENS[:CLASSREF] }
it "should match against single upper-case alpha-numeric terms" do
@token.regex.should =~ "One"
end
it "should match against upper-case alpha-numeric terms separated by double colons" do
@token.regex.should =~ "One::Two"
end
it "should match against many upper-case alpha-numeric terms separated by double colons" do
@token.regex.should =~ "One::Two::Three::Four::Five"
end
it "should match against upper-case alpha-numeric terms prefixed by double colons" do
@token.regex.should =~ "::One"
end
end
describe Puppet::Parser::Lexer::TOKENS[:NAME] do
before { @token = Puppet::Parser::Lexer::TOKENS[:NAME] }
it "should match against lower-case alpha-numeric terms" do
@token.regex.should =~ "one-two"
end
it "should return itself and the value if the matched term is not a keyword" do
Puppet::Parser::Lexer::KEYWORDS.expects(:lookup).returns(nil)
@token.convert(stub("lexer"), "myval").should == [Puppet::Parser::Lexer::TOKENS[:NAME], "myval"]
end
it "should return the keyword token and the value if the matched term is a keyword" do
keyword = stub 'keyword', :name => :testing
Puppet::Parser::Lexer::KEYWORDS.expects(:lookup).returns(keyword)
@token.convert(stub("lexer"), "myval").should == [keyword, "myval"]
end
it "should return the BOOLEAN token and 'true' if the matched term is the string 'true'" do
keyword = stub 'keyword', :name => :TRUE
Puppet::Parser::Lexer::KEYWORDS.expects(:lookup).returns(keyword)
@token.convert(stub('lexer'), "true").should == [Puppet::Parser::Lexer::TOKENS[:BOOLEAN], true]
end
it "should return the BOOLEAN token and 'false' if the matched term is the string 'false'" do
keyword = stub 'keyword', :name => :FALSE
Puppet::Parser::Lexer::KEYWORDS.expects(:lookup).returns(keyword)
@token.convert(stub('lexer'), "false").should == [Puppet::Parser::Lexer::TOKENS[:BOOLEAN], false]
end
it "should match against lower-case alpha-numeric terms separated by double colons" do
@token.regex.should =~ "one::two"
end
it "should match against many lower-case alpha-numeric terms separated by double colons" do
@token.regex.should =~ "one::two::three::four::five"
end
it "should match against lower-case alpha-numeric terms prefixed by double colons" do
@token.regex.should =~ "::one"
end
it "should match against nested terms starting with numbers" do
@token.regex.should =~ "::1one::2two::3three"
end
end
describe Puppet::Parser::Lexer::TOKENS[:NUMBER] do
before do
@token = Puppet::Parser::Lexer::TOKENS[:NUMBER]
@regex = @token.regex
end
it "should match against numeric terms" do
@regex.should =~ "2982383139"
end
it "should match against float terms" do
@regex.should =~ "29823.235"
end
it "should match against hexadecimal terms" do
@regex.should =~ "0xBEEF0023"
end
it "should match against float with exponent terms" do
@regex.should =~ "10e23"
end
it "should match against float terms with negative exponents" do
@regex.should =~ "10e-23"
end
it "should match against float terms with fractional parts and exponent" do
@regex.should =~ "1.234e23"
end
it "should return the NAME token and the value" do
@token.convert(stub("lexer"), "myval").should == [Puppet::Parser::Lexer::TOKENS[:NAME], "myval"]
end
end
describe Puppet::Parser::Lexer::TOKENS[:COMMENT] do
before { @token = Puppet::Parser::Lexer::TOKENS[:COMMENT] }
it "should match against lines starting with '#'" do
@token.regex.should =~ "# this is a comment"
end
it "should be marked to get skipped" do
@token.skip?.should be_true
end
it "should be marked to accumulate" do
@token.accumulate?.should be_true
end
it "'s block should return the comment without the #" do
@token.convert(@lexer,"# this is a comment")[1].should == "this is a comment"
end
end
describe Puppet::Parser::Lexer::TOKENS[:MLCOMMENT] do
before do
@token = Puppet::Parser::Lexer::TOKENS[:MLCOMMENT]
@lexer = stub 'lexer', :line => 0
end
it "should match against lines enclosed with '/*' and '*/'" do
@token.regex.should =~ "/* this is a comment */"
end
it "should match multiple lines enclosed with '/*' and '*/'" do
@token.regex.should =~ """/*
this is a comment
*/"""
end
it "should increase the lexer current line number by the amount of lines spanned by the comment" do
@lexer.expects(:line=).with(2)
@token.convert(@lexer, "1\n2\n3")
end
it "should not greedily match comments" do
match = @token.regex.match("/* first */ word /* second */")
match[1].should == " first "
end
it "should be marked to accumulate" do
@token.accumulate?.should be_true
end
it "'s block should return the comment without the comment marks" do
@lexer.stubs(:line=).with(0)
@token.convert(@lexer,"/* this is a comment */")[1].should == "this is a comment"
end
end
describe Puppet::Parser::Lexer::TOKENS[:RETURN] do
before { @token = Puppet::Parser::Lexer::TOKENS[:RETURN] }
it "should match against carriage returns" do
@token.regex.should =~ "\n"
end
it "should be marked to initiate text skipping" do
@token.skip_text.should be_true
end
it "should be marked to increment the line" do
@token.incr_line.should be_true
end
end
shared_examples_for "handling `-` in standard variable names" do |prefix|
# Watch out - a regex might match a *prefix* on these, not just the whole
# word, so make sure you don't have false positive or negative results based
# on that.
legal = %w{f foo f::b foo::b f::bar foo::bar 3 foo3 3foo}
illegal = %w{f- f-o -f f::-o f::o- f::o-o}
["", "::"].each do |global_scope|
legal.each do |name|
var = prefix + global_scope + name
it "should accept #{var.inspect} as a valid variable name" do
(subject.regex.match(var) || [])[0].should == var
end
end
illegal.each do |name|
var = prefix + global_scope + name
it "when `variable_with_dash` is disabled it should NOT accept #{var.inspect} as a valid variable name" do
Puppet[:allow_variables_with_dashes] = false
(subject.regex.match(var) || [])[0].should_not == var
end
it "when `variable_with_dash` is enabled it should NOT accept #{var.inspect} as a valid variable name" do
Puppet[:allow_variables_with_dashes] = true
(subject.regex.match(var) || [])[0].should_not == var
end
end
end
end
describe Puppet::Parser::Lexer::TOKENS[:DOLLAR_VAR] do
its(:skip_text) { should be_false }
its(:incr_line) { should be_false }
it_should_behave_like "handling `-` in standard variable names", '$'
end
describe Puppet::Parser::Lexer::TOKENS[:VARIABLE] do
its(:skip_text) { should be_false }
its(:incr_line) { should be_false }
it_should_behave_like "handling `-` in standard variable names", ''
end
describe "the horrible deprecation / compatibility variables with dashes" do
NamesWithDashes = %w{f- f-o -f f::-o f::o- f::o-o}
{ Puppet::Parser::Lexer::TOKENS[:DOLLAR_VAR_WITH_DASH] => '$',
Puppet::Parser::Lexer::TOKENS[:VARIABLE_WITH_DASH] => ''
}.each do |token, prefix|
describe token do
its(:skip_text) { should be_false }
its(:incr_line) { should be_false }
context "when compatibly is disabled" do
before :each do Puppet[:allow_variables_with_dashes] = false end
Puppet::Parser::Lexer::TOKENS.each do |name, value|
it "should be unacceptable after #{name}" do
token.acceptable?(:after => name).should be_false
end
end
# Yes, this should still *match*, just not be acceptable.
NamesWithDashes.each do |name|
["", "::"].each do |global_scope|
var = prefix + global_scope + name
it "should match #{var.inspect}" do
subject.regex.match(var).to_a.should == [var]
end
end
end
end
context "when compatibility is enabled" do
before :each do Puppet[:allow_variables_with_dashes] = true end
it "should be acceptable after DQPRE" do
token.acceptable?(:after => :DQPRE).should be_true
end
NamesWithDashes.each do |name|
["", "::"].each do |global_scope|
var = prefix + global_scope + name
it "should match #{var.inspect}" do
subject.regex.match(var).to_a.should == [var]
end
end
end
end
end
end
context "deprecation warnings" do
before :each do Puppet[:allow_variables_with_dashes] = true end
it "should match a top level variable" do
Puppet.expects(:deprecation_warning).once
tokens_scanned_from('$foo-bar').should == [
[:VARIABLE, { :value => 'foo-bar', :line => 1 }]
]
end
it "does not warn about a variable without a dash" do
Puppet.expects(:deprecation_warning).never
tokens_scanned_from('$c').should == [
[:VARIABLE, { :value => "c", :line => 1 }]
]
end
it "does not warn about referencing a class name that contains a dash" do
Puppet.expects(:deprecation_warning).never
tokens_scanned_from('foo-bar').should == [
[:NAME, { :value => "foo-bar", :line => 1 }]
]
end
it "warns about reference to variable" do
Puppet.expects(:deprecation_warning).once
tokens_scanned_from('$::foo-bar::baz-quux').should == [
[:VARIABLE, { :value => "::foo-bar::baz-quux", :line => 1 }]
]
end
it "warns about reference to variable interpolated in a string" do
Puppet.expects(:deprecation_warning).once
tokens_scanned_from('"$::foo-bar::baz-quux"').should == [
[:DQPRE, { :value => "", :line => 1 }],
[:VARIABLE, { :value => "::foo-bar::baz-quux", :line => 1 }],
[:DQPOST, { :value => "", :line => 1 }],
]
end
it "warns about reference to variable interpolated in a string as an expression" do
Puppet.expects(:deprecation_warning).once
tokens_scanned_from('"${::foo-bar::baz-quux}"').should == [
[:DQPRE, { :value => "", :line => 1 }],
[:VARIABLE, { :value => "::foo-bar::baz-quux", :line => 1 }],
[:DQPOST, { :value => "", :line => 1 }],
]
end
end
end
describe Puppet::Parser::Lexer,"when lexing strings" do
{
%q{'single quoted string')} => [[:STRING,'single quoted string']],
%q{"double quoted string"} => [[:STRING,'double quoted string']],
%q{'single quoted string with an escaped "\\'"'} => [[:STRING,'single quoted string with an escaped "\'"']],
%q{'single quoted string with an escaped "\$"'} => [[:STRING,'single quoted string with an escaped "\$"']],
%q{'single quoted string with an escaped "\."'} => [[:STRING,'single quoted string with an escaped "\."']],
%q{'single quoted string with an escaped "\r\n"'} => [[:STRING,'single quoted string with an escaped "\r\n"']],
%q{'single quoted string with an escaped "\n"'} => [[:STRING,'single quoted string with an escaped "\n"']],
%q{'single quoted string with an escaped "\\\\"'} => [[:STRING,'single quoted string with an escaped "\\\\"']],
%q{"string with an escaped '\\"'"} => [[:STRING,"string with an escaped '\"'"]],
%q{"string with an escaped '\\$'"} => [[:STRING,"string with an escaped '$'"]],
%Q{"string with a line ending with a backslash: \\\nfoo"} => [[:STRING,"string with a line ending with a backslash: foo"]],
%q{"string with $v (but no braces)"} => [[:DQPRE,"string with "],[:VARIABLE,'v'],[:DQPOST,' (but no braces)']],
%q["string with ${v} in braces"] => [[:DQPRE,"string with "],[:VARIABLE,'v'],[:DQPOST,' in braces']],
%q["string with ${qualified::var} in braces"] => [[:DQPRE,"string with "],[:VARIABLE,'qualified::var'],[:DQPOST,' in braces']],
%q{"string with $v and $v (but no braces)"} => [[:DQPRE,"string with "],[:VARIABLE,"v"],[:DQMID," and "],[:VARIABLE,"v"],[:DQPOST," (but no braces)"]],
%q["string with ${v} and ${v} in braces"] => [[:DQPRE,"string with "],[:VARIABLE,"v"],[:DQMID," and "],[:VARIABLE,"v"],[:DQPOST," in braces"]],
%q["string with ${'a nested single quoted string'} inside it."] => [[:DQPRE,"string with "],[:STRING,'a nested single quoted string'],[:DQPOST,' inside it.']],
%q["string with ${['an array ',$v2]} in it."] => [[:DQPRE,"string with "],:LBRACK,[:STRING,"an array "],:COMMA,[:VARIABLE,"v2"],:RBRACK,[:DQPOST," in it."]],
%q{a simple "scanner" test} => [[:NAME,"a"],[:NAME,"simple"], [:STRING,"scanner"],[:NAME,"test"]],
%q{a simple 'single quote scanner' test} => [[:NAME,"a"],[:NAME,"simple"], [:STRING,"single quote scanner"],[:NAME,"test"]],
%q{a harder 'a $b \c"'} => [[:NAME,"a"],[:NAME,"harder"], [:STRING,'a $b \c"']],
%q{a harder "scanner test"} => [[:NAME,"a"],[:NAME,"harder"], [:STRING,"scanner test"]],
%q{a hardest "scanner \"test\""} => [[:NAME,"a"],[:NAME,"hardest"],[:STRING,'scanner "test"']],
%Q{a hardestest "scanner \\"test\\"\n"} => [[:NAME,"a"],[:NAME,"hardestest"],[:STRING,%Q{scanner "test"\n}]],
%q{function("call")} => [[:NAME,"function"],[:LPAREN,"("],[:STRING,'call'],[:RPAREN,")"]],
%q["string with ${(3+5)/4} nested math."] => [[:DQPRE,"string with "],:LPAREN,[:NAME,"3"],:PLUS,[:NAME,"5"],:RPAREN,:DIV,[:NAME,"4"],[:DQPOST," nested math."]],
%q["$$$$"] => [[:STRING,"$$$$"]],
%q["$variable"] => [[:DQPRE,""],[:VARIABLE,"variable"],[:DQPOST,""]],
%q["$var$other"] => [[:DQPRE,""],[:VARIABLE,"var"],[:DQMID,""],[:VARIABLE,"other"],[:DQPOST,""]],
%q["foo$bar$"] => [[:DQPRE,"foo"],[:VARIABLE,"bar"],[:DQPOST,"$"]],
%q["foo$$bar"] => [[:DQPRE,"foo$"],[:VARIABLE,"bar"],[:DQPOST,""]],
%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"]]
}.each { |src,expected_result|
it "should handle #{src} correctly" do
tokens_scanned_from(src).should be_like(*expected_result)
end
}
end
describe Puppet::Parser::Lexer::TOKENS[:DOLLAR_VAR] do
before { @token = Puppet::Parser::Lexer::TOKENS[:DOLLAR_VAR] }
it "should match against alpha words prefixed with '$'" do
@token.regex.should =~ '$this_var'
end
it "should return the VARIABLE token and the variable name stripped of the '$'" do
@token.convert(stub("lexer"), "$myval").should == [Puppet::Parser::Lexer::TOKENS[:VARIABLE], "myval"]
end
end
describe Puppet::Parser::Lexer::TOKENS[:REGEX] do
before { @token = Puppet::Parser::Lexer::TOKENS[:REGEX] }
it "should match against any expression enclosed in //" do
@token.regex.should =~ '/this is a regex/'
end
it 'should not match if there is \n in the regex' do
@token.regex.should_not =~ "/this is \n a regex/"
end
describe "when scanning" do
it "should not consider escaped slashes to be the end of a regex" do
tokens_scanned_from("$x =~ /this \\/ foo/").should be_like(__,__,[:REGEX,%r{this / foo}])
end
it "should not lex chained division as a regex" do
tokens_scanned_from("$x = $a/$b/$c").collect { |name, data| name }.should_not be_include( :REGEX )
end
it "should accept a regular expression after NODE" do
tokens_scanned_from("node /www.*\.mysite\.org/").should be_like(__,[:REGEX,Regexp.new("www.*\.mysite\.org")])
end
it "should accept regular expressions in a CASE" do
s = %q{case $variable {
"something": {$othervar = 4096 / 2}
/regex/: {notice("this notably sucks")}
}
}
tokens_scanned_from(s).should be_like(
:CASE,:VARIABLE,:LBRACE,:STRING,:COLON,:LBRACE,:VARIABLE,:EQUALS,:NAME,:DIV,:NAME,:RBRACE,[:REGEX,/regex/],:COLON,:LBRACE,:NAME,:LPAREN,:STRING,:RPAREN,:RBRACE,:RBRACE
)
end
end
it "should return the REGEX token and a Regexp" do
@token.convert(stub("lexer"), "/myregex/").should == [Puppet::Parser::Lexer::TOKENS[:REGEX], Regexp.new(/myregex/)]
end
end
describe Puppet::Parser::Lexer, "when lexing comments" do
before { @lexer = Puppet::Parser::Lexer.new }
it "should accumulate token in munge_token" do
token = stub 'token', :skip => true, :accumulate? => true, :incr_line => nil, :skip_text => false
token.stubs(:convert).with(@lexer, "# this is a comment").returns([token, " this is a comment"])
@lexer.munge_token(token, "# this is a comment")
@lexer.munge_token(token, "# this is a comment")
@lexer.getcomment.should == " this is a comment\n this is a comment\n"
end
it "should add a new comment stack level on LBRACE" do
@lexer.string = "{"
@lexer.expects(:commentpush)
@lexer.fullscan
end
it "should add a new comment stack level on LPAREN" do
@lexer.string = "("
@lexer.expects(:commentpush)
@lexer.fullscan
end
it "should pop the current comment on RPAREN" do
@lexer.string = ")"
@lexer.expects(:commentpop)
@lexer.fullscan
end
it "should return the current comments on getcomment" do
@lexer.string = "# comment"
@lexer.fullscan
@lexer.getcomment.should == "comment\n"
end
it "should discard the previous comments on blank line" do
@lexer.string = "# 1\n\n# 2"
@lexer.fullscan
@lexer.getcomment.should == "2\n"
end
it "should skip whitespace before lexing the next token after a non-token" do
tokens_scanned_from("/* 1\n\n */ \ntest").should be_like([:NAME, "test"])
end
it "should not return comments seen after the current line" do
@lexer.string = "# 1\n\n# 2"
@lexer.fullscan
@lexer.getcomment(1).should == ""
end
it "should return a comment seen before the current line" do
@lexer.string = "# 1\n# 2"
@lexer.fullscan
@lexer.getcomment(2).should == "1\n2\n"
end
end
# FIXME: We need to rewrite all of these tests, but I just don't want to take the time right now.
describe "Puppet::Parser::Lexer in the old tests" do
before { @lexer = Puppet::Parser::Lexer.new }
it "should do simple lexing" do
{
%q{\\} => [[:BACKSLASH,"\\"]],
%q{simplest scanner test} => [[:NAME,"simplest"],[:NAME,"scanner"],[:NAME,"test"]],
%Q{returned scanner test\n} => [[:NAME,"returned"],[:NAME,"scanner"],[:NAME,"test"]]
}.each { |source,expected|
tokens_scanned_from(source).should be_like(*expected)
}
end
it "should fail usefully" do
expect { tokens_scanned_from('^') }.to raise_error(RuntimeError)
end
it "should fail if the string is not set" do
expect { @lexer.fullscan }.to raise_error(Puppet::LexError)
end
it "should correctly identify keywords" do
tokens_scanned_from("case").should be_like([:CASE, "case"])
end
it "should correctly parse class references" do
%w{Many Different Words A Word}.each { |t| tokens_scanned_from(t).should be_like([:CLASSREF,t])}
end
# #774
it "should correctly parse namespaced class refernces token" do
%w{Foo ::Foo Foo::Bar ::Foo::Bar}.each { |t| tokens_scanned_from(t).should be_like([:CLASSREF, t]) }
end
it "should correctly parse names" do
%w{this is a bunch of names}.each { |t| tokens_scanned_from(t).should be_like([:NAME,t]) }
end
it "should correctly parse names with numerals" do
%w{1name name1 11names names11}.each { |t| tokens_scanned_from(t).should be_like([:NAME,t]) }
end
it "should correctly parse empty strings" do
expect { tokens_scanned_from('$var = ""') }.to_not raise_error
end
it "should correctly parse virtual resources" do
tokens_scanned_from("@type {").should be_like([:AT, "@"], [:NAME, "type"], [:LBRACE, "{"])
end
it "should correctly deal with namespaces" do
@lexer.string = %{class myclass}
@lexer.fullscan
@lexer.namespace.should == "myclass"
@lexer.namepop
@lexer.namespace.should == ""
@lexer.string = "class base { class sub { class more"
@lexer.fullscan
@lexer.namespace.should == "base::sub::more"
@lexer.namepop
@lexer.namespace.should == "base::sub"
end
it "should not put class instantiation on the namespace" do
@lexer.string = "class base { class sub { class { mode"
@lexer.fullscan
@lexer.namespace.should == "base::sub"
end
it "should correctly handle fully qualified names" do
@lexer.string = "class base { class sub::more {"
@lexer.fullscan
@lexer.namespace.should == "base::sub::more"
@lexer.namepop
@lexer.namespace.should == "base"
end
it "should correctly lex variables" do
["$variable", "$::variable", "$qualified::variable", "$further::qualified::variable"].each do |string|
tokens_scanned_from(string).should be_like([:VARIABLE,string.sub(/^\$/,'')])
end
end
it "should end variables at `-`" do
tokens_scanned_from('$hyphenated-variable').
should be_like [:VARIABLE, "hyphenated"], [:MINUS, '-'], [:NAME, 'variable']
end
it "should not include whitespace in a variable" do
tokens_scanned_from("$foo bar").should_not be_like([:VARIABLE, "foo bar"])
end
it "should not include excess colons in a variable" do
tokens_scanned_from("$foo::::bar").should_not be_like([:VARIABLE, "foo::::bar"])
end
end
describe "Puppet::Parser::Lexer in the old tests when lexing example files" do
my_fixtures('*.pp') do |file|
it "should correctly lex #{file}" do
lexer = Puppet::Parser::Lexer.new
lexer.file = file
expect { lexer.fullscan }.to_not raise_error
end
end
end
describe "when trying to lex a non-existent file" do
include PuppetSpec::Files
it "should return an empty list of tokens" do
lexer = Puppet::Parser::Lexer.new
lexer.file = nofile = tmpfile('lexer')
- Puppet::FileSystem::File.exist?(nofile).should == false
+ Puppet::FileSystem.exist?(nofile).should == false
lexer.fullscan.should == [[false,false]]
end
end
diff --git a/spec/unit/parser/methods/filter_spec.rb b/spec/unit/parser/methods/filter_spec.rb
index 89fb15bbb..c9fed31d2 100644
--- a/spec/unit/parser/methods/filter_spec.rb
+++ b/spec/unit/parser/methods/filter_spec.rb
@@ -1,79 +1,135 @@
require 'puppet'
require 'spec_helper'
require 'puppet_spec/compiler'
require 'unit/parser/methods/shared'
describe 'the filter method' do
include PuppetSpec::Compiler
before :each do
Puppet[:parser] = 'future'
end
it 'should filter on an array (all berries)' do
catalog = compile_to_catalog(<<-MANIFEST)
$a = ['strawberry','blueberry','orange']
$a.filter |$x|{ $x =~ /berry$/}.each |$v|{
file { "/file_$v": ensure => present }
}
MANIFEST
catalog.resource(:file, "/file_strawberry")['ensure'].should == 'present'
catalog.resource(:file, "/file_blueberry")['ensure'].should == 'present'
end
+ it 'should filter on enumerable type (Integer)' do
+ catalog = compile_to_catalog(<<-MANIFEST)
+ $a = Integer[1,10]
+ $a.filter |$x|{ $x % 3 == 0}.each |$v|{
+ file { "/file_$v": ensure => present }
+ }
+ MANIFEST
+
+ catalog.resource(:file, "/file_3")['ensure'].should == 'present'
+ catalog.resource(:file, "/file_6")['ensure'].should == 'present'
+ catalog.resource(:file, "/file_9")['ensure'].should == 'present'
+ end
+
+ it 'should filter on enumerable type (Integer) using two args index/value' do
+ catalog = compile_to_catalog(<<-MANIFEST)
+ $a = Integer[10,18]
+ $a.filter |$i, $x|{ $i % 3 == 0}.each |$v|{
+ file { "/file_$v": ensure => present }
+ }
+ MANIFEST
+
+ catalog.resource(:file, "/file_10")['ensure'].should == 'present'
+ catalog.resource(:file, "/file_13")['ensure'].should == 'present'
+ catalog.resource(:file, "/file_16")['ensure'].should == 'present'
+ end
+
it 'should produce an array when acting on an array' do
catalog = compile_to_catalog(<<-MANIFEST)
$a = ['strawberry','blueberry','orange']
$b = $a.filter |$x|{ $x =~ /berry$/}
file { "/file_${b[0]}": ensure => present }
file { "/file_${b[1]}": ensure => present }
MANIFEST
catalog.resource(:file, "/file_strawberry")['ensure'].should == 'present'
catalog.resource(:file, "/file_blueberry")['ensure'].should == 'present'
end
+ it 'can filter array using index and value' do
+ catalog = compile_to_catalog(<<-MANIFEST)
+ $a = ['strawberry','blueberry','orange']
+ $b = $a.filter |$index, $x|{ $index == 0 or $index ==2}
+ file { "/file_${b[0]}": ensure => present }
+ file { "/file_${b[1]}": ensure => present }
+ MANIFEST
+
+ catalog.resource(:file, "/file_strawberry")['ensure'].should == 'present'
+ catalog.resource(:file, "/file_orange")['ensure'].should == 'present'
+ end
+
it 'filters on a hash (all berries) by key' do
catalog = compile_to_catalog(<<-MANIFEST)
$a = {'strawberry'=>'red','blueberry'=>'blue','orange'=>'orange'}
$a.filter |$x|{ $x[0] =~ /berry$/}.each |$v|{
file { "/file_${v[0]}": ensure => present }
}
MANIFEST
catalog.resource(:file, "/file_strawberry")['ensure'].should == 'present'
catalog.resource(:file, "/file_blueberry")['ensure'].should == 'present'
end
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.filter |$x|{ $x[0] =~ /berry$/}
file { "/file_${b['strawberry']}": ensure => present }
file { "/file_${b['blueberry']}": ensure => present }
file { "/file_${b['orange']}": ensure => present }
MANIFEST
catalog.resource(:file, "/file_red")['ensure'].should == 'present'
catalog.resource(:file, "/file_blue")['ensure'].should == 'present'
catalog.resource(:file, "/file_")['ensure'].should == 'present'
end
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.filter |$x|{ $x[1] =~ /berry$/}.each |$v|{
file { "/file_${v[0]}": ensure => present }
}
MANIFEST
catalog.resource(:file, "/file_strawb")['ensure'].should == 'present'
catalog.resource(:file, "/file_blueb")['ensure'].should == 'present'
end
+ context 'filter checks arguments and' do
+ it 'raises an error when block has more than 2 argument' do
+ expect do
+ compile_to_catalog(<<-MANIFEST)
+ [1].filter |$indexm, $x, $yikes|{ }
+ MANIFEST
+ end.to raise_error(Puppet::Error, /block must define at most two parameters/)
+ end
+
+ it 'raises an error when block has fewer than 1 argument' do
+ expect do
+ compile_to_catalog(<<-MANIFEST)
+ [1].filter || { }
+ MANIFEST
+ end.to raise_error(Puppet::Error, /block must define at least one parameter/)
+ end
+ end
+
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/map_spec.rb b/spec/unit/parser/methods/map_spec.rb
index 025501754..7f8e79789 100644
--- a/spec/unit/parser/methods/map_spec.rb
+++ b/spec/unit/parser/methods/map_spec.rb
@@ -1,95 +1,184 @@
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 an enumerable type (multiplying each value by 2)' do
+ catalog = compile_to_catalog(<<-MANIFEST)
+ $a = Integer[1,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 an integer (multiply each by 3)' do
+ catalog = compile_to_catalog(<<-MANIFEST)
+ 3.map |$x|{ $x*3}.each |$v|{
+ file { "/file_$v": ensure => present }
+ }
+ MANIFEST
+
+ catalog.resource(:file, "/file_0")['ensure'].should == 'present'
+ catalog.resource(:file, "/file_3")['ensure'].should == 'present'
+ catalog.resource(:file, "/file_6")['ensure'].should == 'present'
+ end
+
+ it 'map on a string' do
+ catalog = compile_to_catalog(<<-MANIFEST)
+ $a = {a=>x, b=>y}
+ "ab".map |$x|{$a[$x]}.each |$v|{
+ file { "/file_$v": ensure => present }
+ }
+ MANIFEST
+
+ catalog.resource(:file, "/file_x")['ensure'].should == 'present'
+ catalog.resource(:file, "/file_y")['ensure'].should == 'present'
+ end
+
+ it 'map on an array (multiplying value by 10 in even index position)' do
+ catalog = compile_to_catalog(<<-MANIFEST)
+ $a = [1,2,3]
+ $a.map |$i, $x|{ if $i % 2 == 0 {$x} else {$x*10}}.each |$v|{
+ file { "/file_$v": ensure => present }
+ }
+ MANIFEST
+
+ catalog.resource(:file, "/file_1")['ensure'].should == 'present'
+ catalog.resource(:file, "/file_20")['ensure'].should == 'present'
+ catalog.resource(:file, "/file_3")['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
+ it 'map on a hash selecting keys - using two block parameters' 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 }
+ $a.map |$k,$v|{ 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
+
+ it 'each on a hash selecting value - using two bloc parameters' do
+ catalog = compile_to_catalog(<<-MANIFEST)
+ $a = {'a'=>1,'b'=>2,'c'=>3}
+ $a.map |$k,$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
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'
+ catalog.resource(:file, "/file_0.something")['ensure'].should == 'present'
end
end
+
+ context 'map checks arguments and' do
+ it 'raises an error when block has more than 2 argument' do
+ expect do
+ compile_to_catalog(<<-MANIFEST)
+ [1].map |$index, $x, $yikes|{ }
+ MANIFEST
+ end.to raise_error(Puppet::Error, /block must define at most two parameters/)
+ end
+
+ it 'raises an error when block has fewer than 1 argument' do
+ expect do
+ compile_to_catalog(<<-MANIFEST)
+ [1].map || { }
+ MANIFEST
+ end.to raise_error(Puppet::Error, /block must define at least one parameter/)
+ 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 5d4549b54..4f0c14e5e 100644
--- a/spec/unit/parser/methods/reduce_spec.rb
+++ b/spec/unit/parser/methods/reduce_spec.rb
@@ -1,68 +1,78 @@
require 'puppet'
require 'spec_helper'
require 'puppet_spec/compiler'
describe 'the reduce method' do
include PuppetSpec::Compiler
before :all do
# enable switching back
@saved_parser = Puppet[:parser]
# These tests only work with future parser
end
after :all do
# switch back to original
Puppet[:parser] = @saved_parser
end
before :each do
node = Puppet::Node.new("floppy", :environment => 'production')
@compiler = Puppet::Parser::Compiler.new(node)
@scope = Puppet::Parser::Scope.new(@compiler)
@topscope = @scope.compiler.topscope
@scope.parent = @topscope
Puppet[:parser] = 'future'
end
context "should be callable as" do
it 'reduce on an array' do
catalog = compile_to_catalog(<<-MANIFEST)
$a = [1,2,3]
$b = $a.reduce |$memo, $x| { $memo + $x }
file { "/file_$b": ensure => present }
MANIFEST
catalog.resource(:file, "/file_6")['ensure'].should == 'present'
end
+ it 'reduce on enumerable type' do
+ catalog = compile_to_catalog(<<-MANIFEST)
+ $a = Integer[1,3]
+ $b = $a.reduce |$memo, $x| { $memo + $x }
+ file { "/file_$b": ensure => present }
+ MANIFEST
+
+ catalog.resource(:file, "/file_6")['ensure'].should == 'present'
+ 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 }
file { "/file_$b": ensure => present }
MANIFEST
catalog.resource(:file, "/file_10")['ensure'].should == 'present'
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]] }
file { "/file_${$b[0]}_${$b[1]}": ensure => present }
MANIFEST
catalog.resource(:file, "/file_sum_6")['ensure'].should == 'present'
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]] }
file { "/file_${$b[0]}_${$b[1]}": ensure => present }
MANIFEST
catalog.resource(:file, "/file_sum_10")['ensure'].should == 'present'
end
end
end
diff --git a/spec/unit/parser/methods/shared.rb b/spec/unit/parser/methods/shared.rb
index 704769d83..42cfd2359 100644
--- a/spec/unit/parser/methods/shared.rb
+++ b/spec/unit/parser/methods/shared.rb
@@ -1,61 +1,45 @@
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]}": } }
MANIFEST
catalog.resource(:notify, "a 1").should_not be_nil
end
end
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|{ }
- MANIFEST
- end.to raise_error(Puppet::Error, /Too few arguments/)
- end
-
- it 'raises an error when defined with fewer than 1 argument' do
- expect do
- compile_to_catalog(<<-MANIFEST)
- [1].#{func} || { }
- MANIFEST
- end.to raise_error(Puppet::Error, /Too many arguments/)
- end
-
it 'raises an error when used against an unsupported type' do
expect do
compile_to_catalog(<<-MANIFEST)
- "not correct".#{func} |$v| { }
+ 3.14.#{func} |$v| { }
MANIFEST
- end.to raise_error(Puppet::Error, /must be an Array or a Hash/)
+ end.to raise_error(Puppet::Error, /must be something enumerable/)
end
it 'raises an error when called with any parameters besides a block' do
expect do
compile_to_catalog(<<-MANIFEST)
[1].#{func}(1) |$v| { }
MANIFEST
end.to raise_error(Puppet::Error, /Wrong number of arguments/)
end
it 'raises an error when called without a block' do
expect do
compile_to_catalog(<<-MANIFEST)
[1].#{func}()
MANIFEST
end.to raise_error(Puppet::Error, /Wrong number of arguments/)
end
it 'raises an error when called without a block' do
expect do
compile_to_catalog(<<-MANIFEST)
[1].#{func}(1)
MANIFEST
end.to raise_error(Puppet::Error, /must be a parameterized block/)
end
end
diff --git a/spec/unit/parser/methods/slice_spec.rb b/spec/unit/parser/methods/slice_spec.rb
index 1069bc75a..1de1dd0f1 100644
--- a/spec/unit/parser/methods/slice_spec.rb
+++ b/spec/unit/parser/methods/slice_spec.rb
@@ -1,97 +1,135 @@
require 'puppet'
require 'spec_helper'
require 'puppet_spec/compiler'
require 'rubygems'
describe 'methods' do
include PuppetSpec::Compiler
before :all do
# enable switching back
@saved_parser = Puppet[:parser]
# These tests only work with future parser
Puppet[:parser] = 'future'
end
after :all do
# switch back to original
Puppet[:parser] = @saved_parser
end
before :each do
node = Puppet::Node.new("floppy", :environment => 'production')
@compiler = Puppet::Parser::Compiler.new(node)
@scope = Puppet::Parser::Scope.new(@compiler)
@topscope = @scope.compiler.topscope
@scope.parent = @topscope
Puppet[:parser] = 'future'
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| {
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'
end
+
it 'slice with one parameter' do
catalog = compile_to_catalog(<<-MANIFEST)
$a = [1, present, 2, absent, 3, present]
$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'
end
+
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| {
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
+ 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| {
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 "should be callable on enumerable types as" do
+ it 'slice with integer range' do
+ catalog = compile_to_catalog(<<-MANIFEST)
+ $a = Integer[1,4]
+ $a.slice(2) |$a,$b| {
+ file { "/file_${a}.${b}": ensure => present }
+ }
+ MANIFEST
+
+ catalog.resource(:file, "/file_1.2")['ensure'].should == 'present'
+ catalog.resource(:file, "/file_3.4")['ensure'].should == 'present'
+ end
+ it 'slice with integer' do
+ catalog = compile_to_catalog(<<-MANIFEST)
+ 4.slice(2) |$a,$b| {
+ file { "/file_${a}.${b}": ensure => present }
+ }
+ MANIFEST
+
+ catalog.resource(:file, "/file_0.1")['ensure'].should == 'present'
+ catalog.resource(:file, "/file_2.3")['ensure'].should == 'present'
+ end
+
+ it 'slice with string' do
+ catalog = compile_to_catalog(<<-MANIFEST)
+ 'abcd'.slice(2) |$a,$b| {
+ file { "/file_${a}.${b}": ensure => present }
+ }
+ MANIFEST
+
+ catalog.resource(:file, "/file_a.b")['ensure'].should == 'present'
+ catalog.resource(:file, "/file_c.d")['ensure'].should == 'present'
+ 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| {
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'
end
end
end
diff --git a/spec/unit/parser/parser_spec.rb b/spec/unit/parser/parser_spec.rb
index 2c4cf50df..56062d47e 100755
--- a/spec/unit/parser/parser_spec.rb
+++ b/spec/unit/parser/parser_spec.rb
@@ -1,531 +1,535 @@
#! /usr/bin/env ruby
require 'spec_helper'
describe Puppet::Parser do
Puppet::Parser::AST
before :each do
@known_resource_types = Puppet::Resource::TypeCollection.new("development")
@parser = Puppet::Parser::Parser.new "development"
@parser.stubs(:known_resource_types).returns @known_resource_types
@true_ast = Puppet::Parser::AST::Boolean.new :value => true
end
it "should require an environment at initialization" do
expect {
Puppet::Parser::Parser.new
}.to raise_error(ArgumentError, /wrong number of arguments/)
end
it "should set the environment" do
- env = Puppet::Node::Environment.new
+ env = Puppet::Node::Environment.create(:testing, [], '')
Puppet::Parser::Parser.new(env).environment.should == env
end
- it "should convert the environment into an environment instance if a string is provided" do
- env = Puppet::Node::Environment.new("testing")
- Puppet::Parser::Parser.new("testing").environment.should == env
- end
-
it "should be able to look up the environment-specific resource type collection" do
- rtc = Puppet::Node::Environment.new("development").known_resource_types
- parser = Puppet::Parser::Parser.new "development"
+ env = Puppet::Node::Environment.create(:development, [], '')
+ rtc = env.known_resource_types
+ parser = Puppet::Parser::Parser.new env
parser.known_resource_types.should equal(rtc)
end
context "when importing" do
it "uses the directory of the currently parsed file" do
@parser.lexer.stubs(:file).returns "/tmp/current_file"
@parser.known_resource_types.loader.expects(:import).with("newfile", "/tmp")
@parser.import("newfile")
end
it "uses the current working directory, when there is no file being parsed" do
@parser.known_resource_types.loader.expects(:import).with('one', Dir.pwd)
@parser.known_resource_types.loader.expects(:import).with('two', Dir.pwd)
@parser.parse("import 'one', 'two'")
end
end
describe "when parsing files" do
before do
- Puppet::FileSystem::File.stubs(:exist?).returns true
- File.stubs(:read).returns ""
+ Puppet::FileSystem.stubs(:exist?).returns true
+ Puppet::FileSystem.stubs(:read).returns ""
@parser.stubs(:watch_file)
end
it "should treat files ending in 'rb' as ruby files" do
@parser.expects(:parse_ruby_file)
@parser.file = "/my/file.rb"
@parser.parse
end
end
describe "when parsing append operator" do
it "should not raise syntax errors" do
expect { @parser.parse("$var += something") }.to_not raise_error
end
it "should raise syntax error on incomplete syntax " do
expect {
@parser.parse("$var += ")
}.to raise_error(Puppet::ParseError, /Syntax error at end of file/)
end
it "should create ast::VarDef with append=true" do
vardef = @parser.parse("$var += 2").code[0]
vardef.should be_a(Puppet::Parser::AST::VarDef)
vardef.append.should == true
end
it "should work with arrays too" do
vardef = @parser.parse("$var += ['test']").code[0]
vardef.should be_a(Puppet::Parser::AST::VarDef)
vardef.append.should == true
end
end
describe "when parsing selector" do
it "should support hash access on the left hand side" do
expect { @parser.parse("$h = { 'a' => 'b' } $a = $h['a'] ? { 'b' => 'd', default => undef }") }.to_not raise_error
end
end
describe "parsing 'unless'" do
it "should create the correct ast objects" do
Puppet::Parser::AST::Not.expects(:new).with { |h| h[:value].is_a?(Puppet::Parser::AST::Boolean) }
@parser.parse("unless false { $var = 1 }")
end
it "should not raise an error with empty statements" do
expect { @parser.parse("unless false { }") }.to_not raise_error
end
#test for bug #13296
it "should not override 'unless' as a parameter inside resources" do
lambda { @parser.parse("exec {'/bin/echo foo': unless => '/usr/bin/false',}") }.should_not raise_error
end
end
describe "when parsing parameter names" do
Puppet::Parser::Lexer::KEYWORDS.sort_tokens.each do |keyword|
it "should allow #{keyword} as a keyword" do
lambda { @parser.parse("exec {'/bin/echo foo': #{keyword} => '/usr/bin/false',}") }.should_not raise_error
end
end
end
describe "when parsing 'if'" do
it "not, it should create the correct ast objects" do
Puppet::Parser::AST::Not.expects(:new).with { |h| h[:value].is_a?(Puppet::Parser::AST::Boolean) }
@parser.parse("if ! true { $var = 1 }")
end
it "boolean operation, it should create the correct ast objects" do
Puppet::Parser::AST::BooleanOperator.expects(:new).with {
|h| h[:rval].is_a?(Puppet::Parser::AST::Boolean) and h[:lval].is_a?(Puppet::Parser::AST::Boolean) and h[:operator]=="or"
}
@parser.parse("if true or true { $var = 1 }")
end
it "comparison operation, it should create the correct ast objects" do
Puppet::Parser::AST::ComparisonOperator.expects(:new).with {
|h| h[:lval].is_a?(Puppet::Parser::AST::Name) and h[:rval].is_a?(Puppet::Parser::AST::Name) and h[:operator]=="<"
}
@parser.parse("if 1 < 2 { $var = 1 }")
end
end
describe "when parsing if complex expressions" do
it "should create a correct ast tree" do
aststub = stub_everything 'ast'
Puppet::Parser::AST::ComparisonOperator.expects(:new).with {
|h| h[:rval].is_a?(Puppet::Parser::AST::Name) and h[:lval].is_a?(Puppet::Parser::AST::Name) and h[:operator]==">"
}.returns(aststub)
Puppet::Parser::AST::ComparisonOperator.expects(:new).with {
|h| h[:rval].is_a?(Puppet::Parser::AST::Name) and h[:lval].is_a?(Puppet::Parser::AST::Name) and h[:operator]=="=="
}.returns(aststub)
Puppet::Parser::AST::BooleanOperator.expects(:new).with {
|h| h[:rval]==aststub and h[:lval]==aststub and h[:operator]=="and"
}
@parser.parse("if (1 > 2) and (1 == 2) { $var = 1 }")
end
it "should raise an error on incorrect expression" do
expect {
@parser.parse("if (1 > 2 > ) or (1 == 2) { $var = 1 }")
}.to raise_error(Puppet::ParseError, /Syntax error at '\)'/)
end
end
describe "when parsing resource references" do
it "should not raise syntax errors" do
expect { @parser.parse('exec { test: param => File["a"] }') }.to_not raise_error
end
it "should not raise syntax errors with multiple references" do
expect { @parser.parse('exec { test: param => File["a","b"] }') }.to_not raise_error
end
it "should create an ast::ResourceReference" do
Puppet::Parser::AST::ResourceReference.expects(:new).with { |arg|
arg[:line]==1 and arg[:type]=="File" and arg[:title].is_a?(Puppet::Parser::AST::ASTArray)
}
@parser.parse('exec { test: command => File["a","b"] }')
end
end
describe "when parsing resource overrides" do
it "should not raise syntax errors" do
expect { @parser.parse('Resource["title"] { param => value }') }.to_not raise_error
end
it "should not raise syntax errors with multiple overrides" do
expect { @parser.parse('Resource["title1","title2"] { param => value }') }.to_not raise_error
end
it "should create an ast::ResourceOverride" do
#Puppet::Parser::AST::ResourceOverride.expects(:new).with { |arg|
# arg[:line]==1 and arg[:object].is_a?(Puppet::Parser::AST::ResourceReference) and arg[:parameters].is_a?(Puppet::Parser::AST::ResourceParam)
#}
ro = @parser.parse('Resource["title1","title2"] { param => value }').code[0]
ro.should be_a(Puppet::Parser::AST::ResourceOverride)
ro.line.should == 1
ro.object.should be_a(Puppet::Parser::AST::ResourceReference)
ro.parameters[0].should be_a(Puppet::Parser::AST::ResourceParam)
end
end
describe "when parsing if statements" do
it "should not raise errors with empty if" do
expect { @parser.parse("if true { }") }.to_not raise_error
end
it "should not raise errors with empty else" do
expect { @parser.parse("if false { notice('if') } else { }") }.to_not raise_error
end
it "should not raise errors with empty if and else" do
expect { @parser.parse("if false { } else { }") }.to_not raise_error
end
it "should create a nop node for empty branch" do
Puppet::Parser::AST::Nop.expects(:new)
@parser.parse("if true { }")
end
it "should create a nop node for empty else branch" do
Puppet::Parser::AST::Nop.expects(:new)
@parser.parse("if true { notice('test') } else { }")
end
it "should build a chain of 'ifs' if there's an 'elsif'" do
expect { @parser.parse(<<-PP) }.to_not raise_error
if true { notice('test') } elsif true {} else { }
PP
end
end
describe "when parsing function calls" do
it "should not raise errors with no arguments" do
expect { @parser.parse("tag()") }.to_not raise_error
end
it "should not raise errors with rvalue function with no args" do
expect { @parser.parse("$a = template()") }.to_not raise_error
end
it "should not raise errors with arguments" do
expect { @parser.parse("notice(1)") }.to_not raise_error
end
it "should not raise errors with multiple arguments" do
expect { @parser.parse("notice(1,2)") }.to_not raise_error
end
it "should not raise errors with multiple arguments and a trailing comma" do
expect { @parser.parse("notice(1,2,)") }.to_not raise_error
end
end
describe "when parsing arrays" do
it "should parse an array" do
expect { @parser.parse("$a = [1,2]") }.to_not raise_error
end
it "should not raise errors with a trailing comma" do
expect { @parser.parse("$a = [1,2,]") }.to_not raise_error
end
it "should accept an empty array" do
expect { @parser.parse("$var = []\n") }.to_not raise_error
end
end
describe "when providing AST context" do
before do
@lexer = stub 'lexer', :line => 50, :file => "/foo/bar", :getcomment => "whev"
@parser.stubs(:lexer).returns @lexer
end
it "should include the lexer's line" do
@parser.ast_context[:line].should == 50
end
it "should include the lexer's file" do
@parser.ast_context[:file].should == "/foo/bar"
end
it "should include the docs if directed to do so" do
@parser.ast_context(true)[:doc].should == "whev"
end
it "should not include the docs when told not to" do
@parser.ast_context(false)[:doc].should be_nil
end
it "should not include the docs by default" do
@parser.ast_context[:doc].should be_nil
end
end
describe "when building ast nodes" do
before do
@lexer = stub 'lexer', :line => 50, :file => "/foo/bar", :getcomment => "whev"
@parser.stubs(:lexer).returns @lexer
@class = Puppet::Resource::Type.new(:hostclass, "myclass", :use_docs => false)
end
it "should return a new instance of the provided class created with the provided options" do
@class.expects(:new).with { |opts| opts[:foo] == "bar" }
@parser.ast(@class, :foo => "bar")
end
it "should merge the ast context into the provided options" do
@class.expects(:new).with { |opts| opts[:file] == "/foo" }
@parser.expects(:ast_context).returns :file => "/foo"
@parser.ast(@class, :foo => "bar")
end
it "should prefer provided options over AST context" do
@class.expects(:new).with { |opts| opts[:file] == "/bar" }
@lexer.expects(:file).returns "/foo"
@parser.ast(@class, :file => "/bar")
end
it "should include docs when the AST class uses them" do
@class.expects(:use_docs).returns true
@class.stubs(:new)
@parser.expects(:ast_context).with{ |docs, line| docs == true }.returns({})
@parser.ast(@class, :file => "/bar")
end
it "should get docs from lexer using the correct AST line number" do
@class.expects(:use_docs).returns true
@class.stubs(:new).with{ |a| a[:doc] == "doc" }
@lexer.expects(:getcomment).with(12).returns "doc"
@parser.ast(@class, :file => "/bar", :line => 12)
end
end
describe "when retrieving a specific node" do
it "should delegate to the known_resource_types node" do
@known_resource_types.expects(:node).with("node")
@parser.node("node")
end
end
describe "when retrieving a specific class" do
it "should delegate to the loaded code" do
@known_resource_types.expects(:hostclass).with("class")
@parser.hostclass("class")
end
end
describe "when retrieving a specific definitions" do
it "should delegate to the loaded code" do
@known_resource_types.expects(:definition).with("define")
@parser.definition("define")
end
end
describe "when determining the configuration version" do
it "should determine it from the resource type collection" do
@parser.known_resource_types.expects(:version).returns "foo"
@parser.version.should == "foo"
end
end
describe "when looking up definitions" do
it "should use the known resource types to check for them by name" do
@parser.known_resource_types.stubs(:find_or_load).with("namespace","name",:definition).returns(:this_value)
@parser.find_definition("namespace","name").should == :this_value
end
end
describe "when looking up hostclasses" do
it "should use the known resource types to check for them by name" do
@parser.known_resource_types.stubs(:find_or_load).with("namespace","name",:hostclass,{}).returns(:this_value)
@parser.find_hostclass("namespace","name").should == :this_value
end
end
describe "when parsing classes" do
before :each do
@krt = Puppet::Resource::TypeCollection.new("development")
@parser = Puppet::Parser::Parser.new "development"
@parser.stubs(:known_resource_types).returns @krt
end
it "should not create new classes" do
@parser.parse("class foobar {}").code[0].should be_a(Puppet::Parser::AST::Hostclass)
@krt.hostclass("foobar").should be_nil
end
it "should correctly set the parent class when one is provided" do
@parser.parse("class foobar inherits yayness {}").code[0].instantiate('')[0].parent.should == "yayness"
end
it "should correctly set the parent class for multiple classes at a time" do
statements = @parser.parse("class foobar inherits yayness {}\nclass boo inherits bar {}").code
statements[0].instantiate('')[0].parent.should == "yayness"
statements[1].instantiate('')[0].parent.should == "bar"
end
it "should define the code when some is provided" do
@parser.parse("class foobar { $var = val }").code[0].code.should_not be_nil
end
it "should accept parameters with trailing comma" do
@parser.parse("file { '/example': ensure => file, }").should be
end
it "should accept parametrized classes with trailing comma" do
@parser.parse("class foobar ($var1 = 0,) { $var = val }").code[0].code.should_not be_nil
end
it "should define parameters when provided" do
foobar = @parser.parse("class foobar($biz,$baz) {}").code[0].instantiate('')[0]
foobar.arguments.should == {"biz" => nil, "baz" => nil}
end
end
describe "when parsing resources" do
before :each do
@krt = Puppet::Resource::TypeCollection.new("development")
@parser = Puppet::Parser::Parser.new "development"
@parser.stubs(:known_resource_types).returns @krt
end
it "should be able to parse class resources" do
@krt.add(Puppet::Resource::Type.new(:hostclass, "foobar", :arguments => {"biz" => nil}))
expect { @parser.parse("class { foobar: biz => stuff }") }.to_not raise_error
end
it "should correctly mark exported resources as exported" do
@parser.parse("@@file { '/file': }").code[0].exported.should be_true
end
it "should correctly mark virtual resources as virtual" do
@parser.parse("@file { '/file': }").code[0].virtual.should be_true
end
end
describe "when parsing nodes" do
it "should be able to parse a node with a single name" do
node = @parser.parse("node foo { }").code[0]
node.should be_a Puppet::Parser::AST::Node
node.names.length.should == 1
node.names[0].value.should == "foo"
end
it "should be able to parse a node with two names" do
node = @parser.parse("node foo, bar { }").code[0]
node.should be_a Puppet::Parser::AST::Node
node.names.length.should == 2
node.names[0].value.should == "foo"
node.names[1].value.should == "bar"
end
it "should be able to parse a node with three names" do
node = @parser.parse("node foo, bar, baz { }").code[0]
node.should be_a Puppet::Parser::AST::Node
node.names.length.should == 3
node.names[0].value.should == "foo"
node.names[1].value.should == "bar"
node.names[2].value.should == "baz"
end
end
it "should fail if trying to collect defaults" do
expect {
@parser.parse("@Port { protocols => tcp }")
}.to raise_error(Puppet::ParseError, /Defaults are not virtualizable/)
end
context "when parsing collections" do
it "should parse basic collections" do
@parser.parse("Port <| |>").code.
should be_all {|x| x.is_a? Puppet::Parser::AST::Collection }
end
it "should parse fully qualified collections" do
@parser.parse("Port::Range <| |>").code.
should be_all {|x| x.is_a? Puppet::Parser::AST::Collection }
end
end
it "should not assign to a fully qualified variable" do
expect {
@parser.parse("$one::two = yay")
}.to raise_error(Puppet::ParseError, /Cannot assign to variables in other namespaces/)
end
it "should parse assignment of undef" do
tree = @parser.parse("$var = undef")
tree.code.children[0].should be_an_instance_of Puppet::Parser::AST::VarDef
tree.code.children[0].value.should be_an_instance_of Puppet::Parser::AST::Undef
end
context "#namesplit" do
{ "base::sub" => %w{base sub},
"main" => ["", "main"],
"one::two::three::four" => ["one::two::three", "four"],
}.each do |input, output|
it "should split #{input.inspect} to #{output.inspect}" do
@parser.namesplit(input).should == output
end
end
end
it "should treat classes as case insensitive" do
@parser.known_resource_types.import_ast(@parser.parse("class yayness {}"), '')
@parser.known_resource_types.hostclass('yayness').
should == @parser.find_hostclass("", "YayNess")
end
it "should treat defines as case insensitive" do
@parser.known_resource_types.import_ast(@parser.parse("define funtest {}"), '')
@parser.known_resource_types.hostclass('funtest').
should == @parser.find_hostclass("", "fUntEst")
end
+
+ context "deprecations" do
+ it "should flag use of import as deprecated" do
+ Puppet.expects(:deprecation_warning).once
+ @parser.known_resource_types.loader.expects(:import).with('foo', Dir.pwd)
+ @parser.parse("import 'foo'")
+ end
+ end
end
diff --git a/spec/unit/parser/resource_spec.rb b/spec/unit/parser/resource_spec.rb
index 74a66d1c1..f78f83982 100755
--- a/spec/unit/parser/resource_spec.rb
+++ b/spec/unit/parser/resource_spec.rb
@@ -1,592 +1,586 @@
-#! /usr/bin/env ruby
require 'spec_helper'
-# LAK: FIXME This is just new tests for resources; I have
-# not moved all tests over yet.
-
describe Puppet::Parser::Resource do
before do
- @node = Puppet::Node.new("yaynode")
- @known_resource_types = Puppet::Resource::TypeCollection.new("env")
+ environment = Puppet::Node::Environment.create(:testing, [], '')
+ @node = Puppet::Node.new("yaynode", :environment => environment)
+ @known_resource_types = environment.known_resource_types
@compiler = Puppet::Parser::Compiler.new(@node)
- @compiler.environment.stubs(:known_resource_types).returns @known_resource_types
@source = newclass ""
@scope = @compiler.topscope
end
def mkresource(args = {})
args[:source] ||= @source
args[:scope] ||= @scope
params = args[:parameters] || {:one => "yay", :three => "rah"}
if args[:parameters] == :none
args.delete(:parameters)
elsif not args[:parameters].is_a? Array
args[:parameters] = paramify(args[:source], params)
end
Puppet::Parser::Resource.new("resource", "testing", args)
end
def param(name, value, source)
Puppet::Parser::Resource::Param.new(:name => name, :value => value, :source => source)
end
def paramify(source, hash)
hash.collect do |name, value|
Puppet::Parser::Resource::Param.new(
:name => name, :value => value, :source => source
)
end
end
def newclass(name)
@known_resource_types.add Puppet::Resource::Type.new(:hostclass, name)
end
def newdefine(name)
@known_resource_types.add Puppet::Resource::Type.new(:definition, name)
end
def newnode(name)
@known_resource_types.add Puppet::Resource::Type.new(:node, name)
end
it "should get its environment from its scope" do
scope = stub 'scope', :source => stub("source"), :namespaces => nil
scope.expects(:environment).returns("foo").at_least_once
Puppet::Parser::Resource.new("file", "whatever", :scope => scope).environment.should == "foo"
end
it "should use the resource type collection helper module" do
Puppet::Parser::Resource.ancestors.should be_include(Puppet::Resource::TypeCollectionHelper)
end
it "should use the scope's environment as its environment" do
@scope.expects(:environment).returns("myenv").at_least_once
Puppet::Parser::Resource.new("file", "whatever", :scope => @scope).environment.should == "myenv"
end
it "should be isomorphic if it is builtin and models an isomorphic type" do
Puppet::Type.type(:file).expects(:isomorphic?).returns(true)
@resource = Puppet::Parser::Resource.new("file", "whatever", :scope => @scope, :source => @source).isomorphic?.should be_true
end
it "should not be isomorphic if it is builtin and models a non-isomorphic type" do
Puppet::Type.type(:file).expects(:isomorphic?).returns(false)
@resource = Puppet::Parser::Resource.new("file", "whatever", :scope => @scope, :source => @source).isomorphic?.should be_false
end
it "should be isomorphic if it is not builtin" do
newdefine "whatever"
@resource = Puppet::Parser::Resource.new("whatever", "whatever", :scope => @scope, :source => @source).isomorphic?.should be_true
end
it "should have an array-indexing method for retrieving parameter values" do
@resource = mkresource
@resource[:one].should == "yay"
end
it "should use a Puppet::Resource for converting to a ral resource" do
trans = mock 'resource', :to_ral => "yay"
@resource = mkresource
@resource.expects(:copy_as_resource).returns trans
@resource.to_ral.should == "yay"
end
it "should be able to use the indexing operator to access parameters" do
resource = Puppet::Parser::Resource.new("resource", "testing", :source => "source", :scope => @scope)
resource["foo"] = "bar"
resource["foo"].should == "bar"
end
it "should return the title when asked for a parameter named 'title'" do
Puppet::Parser::Resource.new("resource", "testing", :source => @source, :scope => @scope)[:title].should == "testing"
end
describe "when initializing" do
before do
@arguments = {:scope => @scope}
end
it "should fail unless #{name.to_s} is specified" do
expect {
Puppet::Parser::Resource.new('file', '/my/file')
}.to raise_error(ArgumentError, /Resources require a hash as last argument/)
end
it "should set the reference correctly" do
res = Puppet::Parser::Resource.new("resource", "testing", @arguments)
res.ref.should == "Resource[testing]"
end
it "should be tagged with user tags" do
tags = [ "tag1", "tag2" ]
@arguments[:parameters] = [ param(:tag, tags , :source) ]
res = Puppet::Parser::Resource.new("resource", "testing", @arguments)
res.should be_tagged("tag1")
res.should be_tagged("tag2")
end
end
describe "when evaluating" do
before do
- @node = Puppet::Node.new "test-node"
- @compiler = Puppet::Parser::Compiler.new @node
@catalog = Puppet::Resource::Catalog.new
source = stub('source')
source.stubs(:module_name)
@scope = Puppet::Parser::Scope.new(@compiler, :source => source)
@catalog.add_resource(Puppet::Parser::Resource.new("stage", :main, :scope => @scope))
end
it "should evaluate the associated AST definition" do
definition = newdefine "mydefine"
res = Puppet::Parser::Resource.new("mydefine", "whatever", :scope => @scope, :source => @source, :catalog => @catalog)
definition.expects(:evaluate_code).with(res)
res.evaluate
end
it "should evaluate the associated AST class" do
@class = newclass "myclass"
res = Puppet::Parser::Resource.new("class", "myclass", :scope => @scope, :source => @source, :catalog => @catalog)
@class.expects(:evaluate_code).with(res)
res.evaluate
end
it "should evaluate the associated AST node" do
nodedef = newnode("mynode")
res = Puppet::Parser::Resource.new("node", "mynode", :scope => @scope, :source => @source, :catalog => @catalog)
nodedef.expects(:evaluate_code).with(res)
res.evaluate
end
it "should add an edge to any specified stage for class resources" do
@compiler.known_resource_types.add Puppet::Resource::Type.new(:hostclass, "foo", {})
other_stage = Puppet::Parser::Resource.new(:stage, "other", :scope => @scope, :catalog => @catalog)
@compiler.add_resource(@scope, other_stage)
resource = Puppet::Parser::Resource.new(:class, "foo", :scope => @scope, :catalog => @catalog)
resource[:stage] = 'other'
@compiler.add_resource(@scope, resource)
resource.evaluate
@compiler.catalog.edge?(other_stage, resource).should be_true
end
it "should fail if an unknown stage is specified" do
@compiler.known_resource_types.add Puppet::Resource::Type.new(:hostclass, "foo", {})
resource = Puppet::Parser::Resource.new(:class, "foo", :scope => @scope, :catalog => @catalog)
resource[:stage] = 'other'
expect { resource.evaluate }.to raise_error(ArgumentError, /Could not find stage other specified by/)
end
it "should add edges from the class resources to the parent's stage if no stage is specified" do
main = @compiler.catalog.resource(:stage, :main)
foo_stage = Puppet::Parser::Resource.new(:stage, :foo_stage, :scope => @scope, :catalog => @catalog)
@compiler.add_resource(@scope, foo_stage)
@compiler.known_resource_types.add Puppet::Resource::Type.new(:hostclass, "foo", {})
resource = Puppet::Parser::Resource.new(:class, "foo", :scope => @scope, :catalog => @catalog)
resource[:stage] = 'foo_stage'
@compiler.add_resource(@scope, resource)
resource.evaluate
@compiler.catalog.should be_edge(foo_stage, resource)
end
it "should allow edges to propagate multiple levels down the scope hierarchy" do
Puppet[:code] = <<-MANIFEST
stage { before: before => Stage[main] }
class alpha {
include beta
}
class beta {
include gamma
}
class gamma { }
class { alpha: stage => before }
MANIFEST
catalog = Puppet::Parser::Compiler.compile(Puppet::Node.new 'anyone')
# Stringify them to make for easier lookup
edges = catalog.edges.map {|e| [e.source.ref, e.target.ref]}
edges.should include(["Stage[before]", "Class[Alpha]"])
edges.should include(["Stage[before]", "Class[Beta]"])
edges.should include(["Stage[before]", "Class[Gamma]"])
end
it "should use the specified stage even if the parent scope specifies one" do
Puppet[:code] = <<-MANIFEST
stage { before: before => Stage[main], }
stage { after: require => Stage[main], }
class alpha {
class { beta: stage => after }
}
class beta { }
class { alpha: stage => before }
MANIFEST
catalog = Puppet::Parser::Compiler.compile(Puppet::Node.new 'anyone')
edges = catalog.edges.map {|e| [e.source.ref, e.target.ref]}
edges.should include(["Stage[before]", "Class[Alpha]"])
edges.should include(["Stage[after]", "Class[Beta]"])
end
it "should add edges from top-level class resources to the main stage if no stage is specified" do
main = @compiler.catalog.resource(:stage, :main)
@compiler.known_resource_types.add Puppet::Resource::Type.new(:hostclass, "foo", {})
resource = Puppet::Parser::Resource.new(:class, "foo", :scope => @scope, :catalog => @catalog)
@compiler.add_resource(@scope, resource)
resource.evaluate
@compiler.catalog.should be_edge(main, resource)
end
end
describe "when finishing" do
before do
@class = newclass "myclass"
@nodedef = newnode("mynode")
@resource = Puppet::Parser::Resource.new("file", "whatever", :scope => @scope, :source => @source)
end
it "should do nothing if it has already been finished" do
@resource.finish
@resource.expects(:add_defaults).never
@resource.finish
end
it "should add all defaults available from the scope" do
@resource.scope.expects(:lookupdefaults).with(@resource.type).returns(:owner => param(:owner, "default", @resource.source))
@resource.finish
@resource[:owner].should == "default"
end
it "should not replace existing parameters with defaults" do
@resource.set_parameter :owner, "oldvalue"
@resource.scope.expects(:lookupdefaults).with(@resource.type).returns(:owner => :replaced)
@resource.finish
@resource[:owner].should == "oldvalue"
end
it "should add a copy of each default, rather than the actual default parameter instance" do
newparam = param(:owner, "default", @resource.source)
other = newparam.dup
other.value = "other"
newparam.expects(:dup).returns(other)
@resource.scope.expects(:lookupdefaults).with(@resource.type).returns(:owner => newparam)
@resource.finish
@resource[:owner].should == "other"
end
end
describe "when being tagged" do
before do
@scope_resource = stub 'scope_resource', :tags => %w{srone srtwo}
@scope.stubs(:resource).returns @scope_resource
@resource = Puppet::Parser::Resource.new("file", "yay", :scope => @scope, :source => mock('source'))
end
it "should get tagged with the resource type" do
@resource.tags.should be_include("file")
end
it "should get tagged with the title" do
@resource.tags.should be_include("yay")
end
it "should get tagged with each name in the title if the title is a qualified class name" do
resource = Puppet::Parser::Resource.new("file", "one::two", :scope => @scope, :source => mock('source'))
resource.tags.should be_include("one")
resource.tags.should be_include("two")
end
it "should get tagged with each name in the type if the type is a qualified class name" do
resource = Puppet::Parser::Resource.new("one::two", "whatever", :scope => @scope, :source => mock('source'))
resource.tags.should be_include("one")
resource.tags.should be_include("two")
end
it "should not get tagged with non-alphanumeric titles" do
resource = Puppet::Parser::Resource.new("file", "this is a test", :scope => @scope, :source => mock('source'))
resource.tags.should_not be_include("this is a test")
end
it "should fail on tags containing '*' characters" do
expect { @resource.tag("bad*tag") }.to raise_error(Puppet::ParseError)
end
it "should fail on tags starting with '-' characters" do
expect { @resource.tag("-badtag") }.to raise_error(Puppet::ParseError)
end
it "should fail on tags containing ' ' characters" do
expect { @resource.tag("bad tag") }.to raise_error(Puppet::ParseError)
end
it "should allow alpha tags" do
expect { @resource.tag("good_tag") }.to_not raise_error
end
end
describe "when merging overrides" do
before do
@source = "source1"
@resource = mkresource :source => @source
@override = mkresource :source => @source
end
it "should fail when the override was not created by a parent class" do
@override.source = "source2"
@override.source.expects(:child_of?).with("source1").returns(false)
expect { @resource.merge(@override) }.to raise_error(Puppet::ParseError)
end
it "should succeed when the override was created in the current scope" do
@resource.source = "source3"
@override.source = @resource.source
@override.source.expects(:child_of?).with("source3").never
params = {:a => :b, :c => :d}
@override.expects(:parameters).returns(params)
@resource.expects(:override_parameter).with(:b)
@resource.expects(:override_parameter).with(:d)
@resource.merge(@override)
end
it "should succeed when a parent class created the override" do
@resource.source = "source3"
@override.source = "source4"
@override.source.expects(:child_of?).with("source3").returns(true)
params = {:a => :b, :c => :d}
@override.expects(:parameters).returns(params)
@resource.expects(:override_parameter).with(:b)
@resource.expects(:override_parameter).with(:d)
@resource.merge(@override)
end
it "should add new parameters when the parameter is not set" do
@source.stubs(:child_of?).returns true
@override.set_parameter(:testing, "value")
@resource.merge(@override)
@resource[:testing].should == "value"
end
it "should replace existing parameter values" do
@source.stubs(:child_of?).returns true
@resource.set_parameter(:testing, "old")
@override.set_parameter(:testing, "value")
@resource.merge(@override)
@resource[:testing].should == "value"
end
it "should add values to the parameter when the override was created with the '+>' syntax" do
@source.stubs(:child_of?).returns true
param = Puppet::Parser::Resource::Param.new(:name => :testing, :value => "testing", :source => @resource.source)
param.add = true
@override.set_parameter(param)
@resource.set_parameter(:testing, "other")
@resource.merge(@override)
@resource[:testing].should == %w{other testing}
end
it "should not merge parameter values when multiple resources are overriden with '+>' at once " do
@resource_2 = mkresource :source => @source
@resource. set_parameter(:testing, "old_val_1")
@resource_2.set_parameter(:testing, "old_val_2")
@source.stubs(:child_of?).returns true
param = Puppet::Parser::Resource::Param.new(:name => :testing, :value => "new_val", :source => @resource.source)
param.add = true
@override.set_parameter(param)
@resource. merge(@override)
@resource_2.merge(@override)
@resource [:testing].should == %w{old_val_1 new_val}
@resource_2[:testing].should == %w{old_val_2 new_val}
end
it "should promote tag overrides to real tags" do
@source.stubs(:child_of?).returns true
param = Puppet::Parser::Resource::Param.new(:name => :tag, :value => "testing", :source => @resource.source)
@override.set_parameter(param)
@resource.merge(@override)
@resource.tagged?("testing").should be_true
end
end
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(:copy_as_resource)
end
describe "when being converted to a resource" do
before do
@parser_resource = mkresource :scope => @scope, :parameters => {:foo => "bar", :fee => "fum"}
end
it "should create an instance of Puppet::Resource" do
@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.copy_as_resource.type.should == @parser_resource.type
end
it "should set the title correctly on the Puppet::Resource" do
@parser_resource.copy_as_resource.title.should == @parser_resource.title
end
it "should copy over all of the parameters" do
result = @parser_resource.copy_as_resource.to_hash
# The name will be in here, also.
result[:foo].should == "bar"
result[:fee].should == "fum"
end
it "should copy over the tags" do
@parser_resource.tag "foo"
@parser_resource.tag "bar"
@parser_resource.copy_as_resource.tags.should == @parser_resource.tags
end
it "should copy over the line" do
@parser_resource.line = 40
@parser_resource.copy_as_resource.line.should == 40
end
it "should copy over the file" do
@parser_resource.file = "/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.copy_as_resource.exported.should be_true
end
it "should copy over the 'virtual' value" do
@parser_resource.virtual = 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.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.copy_as_resource
result[:fee].should == ["a", 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 of array, and even deeper" 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.copy_as_resource
result[:fee].should == ["a", Puppet::Resource.new(:file, "/my/file1"), Puppet::Resource.new(:file, "/my/file2")]
end
it "should fail if the same param is declared twice" do
lambda do
@parser_resource = mkresource :source => @source, :parameters => [
Puppet::Parser::Resource::Param.new(
:name => :foo, :value => "bar", :source => @source
),
Puppet::Parser::Resource::Param.new(
:name => :foo, :value => "baz", :source => @source
)
]
end.should raise_error(Puppet::ParseError)
end
end
describe "when validating" do
it "should check each parameter" do
resource = Puppet::Parser::Resource.new :foo, "bar", :scope => @scope, :source => stub("source")
resource[:one] = :two
resource[:three] = :four
resource.expects(:validate_parameter).with(:one)
resource.expects(:validate_parameter).with(:three)
resource.send(:validate)
end
it "should raise a parse error when there's a failure" do
resource = Puppet::Parser::Resource.new :foo, "bar", :scope => @scope, :source => stub("source")
resource[:one] = :two
resource.expects(:validate_parameter).with(:one).raises ArgumentError
expect { resource.send(:validate) }.to raise_error(Puppet::ParseError)
end
end
describe "when setting parameters" do
before do
@source = newclass "foobar"
@resource = Puppet::Parser::Resource.new :foo, "bar", :scope => @scope, :source => @source
end
it "should accept Param instances and add them to the parameter list" do
param = Puppet::Parser::Resource::Param.new :name => "foo", :value => "bar", :source => @source
@resource.set_parameter(param)
@resource["foo"].should == "bar"
end
it "should fail when provided a parameter name but no value" do
expect { @resource.set_parameter("myparam") }.to raise_error(ArgumentError)
end
it "should allow parameters to be set to 'false'" do
@resource.set_parameter("myparam", false)
@resource["myparam"].should be_false
end
it "should use its source when provided a parameter name and value" do
@resource.set_parameter("myparam", "myvalue")
@resource["myparam"].should == "myvalue"
end
end
# part of #629 -- the undef keyword. Make sure 'undef' params get skipped.
it "should not include 'undef' parameters when converting itself to a hash" do
resource = Puppet::Parser::Resource.new "file", "/tmp/testing", :source => mock("source"), :scope => mock("scope")
resource[:owner] = :undef
resource[:mode] = "755"
resource.to_hash[:owner].should be_nil
end
end
diff --git a/spec/unit/parser/scope_spec.rb b/spec/unit/parser/scope_spec.rb
index 00f807dc9..b9b10618f 100755
--- a/spec/unit/parser/scope_spec.rb
+++ b/spec/unit/parser/scope_spec.rb
@@ -1,658 +1,661 @@
#! /usr/bin/env ruby
require 'spec_helper'
require 'puppet_spec/compiler'
+require 'puppet_spec/scope'
describe Puppet::Parser::Scope do
+ include PuppetSpec::Scope
+
before :each do
@scope = Puppet::Parser::Scope.new(
Puppet::Parser::Compiler.new(Puppet::Node.new("foo"))
)
@scope.source = Puppet::Resource::Type.new(:node, :foo)
@topscope = @scope.compiler.topscope
@scope.parent = @topscope
end
- describe ".new_for_test_harness" do
+ describe "create_test_scope_for_node" do
let(:node_name) { "node_name_foo" }
- let(:scope) { described_class.new_for_test_harness(node_name) }
+ let(:scope) { create_test_scope_for_node(node_name) }
it "should be a kind of Scope" do
- scope.should be_a_kind_of Puppet::Parser::Scope
+ scope.should be_a_kind_of(Puppet::Parser::Scope)
end
it "should set the source to a node resource" do
- scope.source.should be_a_kind_of Puppet::Resource::Type
+ scope.source.should be_a_kind_of(Puppet::Resource::Type)
end
it "should have a compiler" do
- scope.compiler.should be_a_kind_of Puppet::Parser::Compiler
+ scope.compiler.should be_a_kind_of(Puppet::Parser::Compiler)
end
it "should set the parent to the compiler topscope" do
- scope.parent.should be scope.compiler.topscope
+ scope.parent.should be(scope.compiler.topscope)
end
end
it "should return a scope for use in a test harness" do
- described_class.new_for_test_harness("node_name_foo").should be_a_kind_of Puppet::Parser::Scope
+ create_test_scope_for_node("node_name_foo").should be_a_kind_of(Puppet::Parser::Scope)
end
it "should be able to retrieve class scopes by name" do
@scope.class_set "myname", "myscope"
@scope.class_scope("myname").should == "myscope"
end
it "should be able to retrieve class scopes by object" do
klass = mock 'ast_class'
klass.expects(:name).returns("myname")
@scope.class_set "myname", "myscope"
@scope.class_scope(klass).should == "myscope"
end
it "should be able to retrieve its parent module name from the source of its parent type" do
@topscope.source = Puppet::Resource::Type.new(:hostclass, :foo, :module_name => "foo")
@scope.parent_module_name.should == "foo"
end
it "should return a nil parent module name if it has no parent" do
@topscope.parent_module_name.should be_nil
end
it "should return a nil parent module name if its parent has no source" do
@scope.parent_module_name.should be_nil
end
it "should get its environment from its compiler" do
- env = Puppet::Node::Environment.new
+ env = Puppet::Node::Environment.create(:testing, [], '')
compiler = stub 'compiler', :environment => env, :is_a? => true
scope = Puppet::Parser::Scope.new(compiler)
scope.environment.should equal(env)
end
it "should fail if no compiler is supplied" do
expect {
Puppet::Parser::Scope.new
}.to raise_error(ArgumentError, /wrong number of arguments/)
end
it "should fail if something that isn't a compiler is supplied" do
expect {
Puppet::Parser::Scope.new(:compiler => true)
}.to raise_error(Puppet::DevError, /you must pass a compiler instance/)
end
it "should use the resource type collection helper to find its known resource types" do
Puppet::Parser::Scope.ancestors.should include(Puppet::Resource::TypeCollectionHelper)
end
describe "when custom functions are called" do
- let(:env) { Puppet::Node::Environment.new('testing') }
+ let(:env) { Puppet::Node::Environment.create(:testing, [], '') }
let(:compiler) { Puppet::Parser::Compiler.new(Puppet::Node.new('foo', :environment => env)) }
let(:scope) { Puppet::Parser::Scope.new(compiler) }
it "calls methods prefixed with function_ as custom functions" do
scope.function_sprintf(["%b", 123]).should == "1111011"
end
it "raises an error when arguments are not passed in an Array" do
expect do
scope.function_sprintf("%b", 123)
end.to raise_error ArgumentError, /custom functions must be called with a single array that contains the arguments/
end
it "raises an error on subsequent calls when arguments are not passed in an Array" do
scope.function_sprintf(["first call"])
expect do
scope.function_sprintf("%b", 123)
end.to raise_error ArgumentError, /custom functions must be called with a single array that contains the arguments/
end
it "raises NoMethodError when the not prefixed" do
expect { scope.sprintf(["%b", 123]) }.to raise_error(NoMethodError)
end
it "raises NoMethodError when prefixed with function_ but it doesn't exist" do
expect { scope.function_fake_bs(['cows']) }.to raise_error(NoMethodError)
end
end
describe "when initializing" do
it "should extend itself with its environment's Functions module as well as the default" do
- env = Puppet::Node::Environment.new("myenv")
- root = Puppet::Node::Environment.root
+ env = Puppet::Node::Environment.create(:myenv, [], '')
+ root = Puppet.lookup(:root_environment)
compiler = stub 'compiler', :environment => env, :is_a? => true
scope = Puppet::Parser::Scope.new(compiler)
scope.singleton_class.ancestors.should be_include(Puppet::Parser::Functions.environment_module(env))
scope.singleton_class.ancestors.should be_include(Puppet::Parser::Functions.environment_module(root))
end
it "should extend itself with the default Functions module if its environment is the default" do
- root = Puppet::Node::Environment.root
+ root = Puppet.lookup(:root_environment)
node = Puppet::Node.new('localhost')
compiler = Puppet::Parser::Compiler.new(node)
scope = Puppet::Parser::Scope.new(compiler)
scope.singleton_class.ancestors.should be_include(Puppet::Parser::Functions.environment_module(root))
end
end
describe "when looking up a variable" do
it "should support :lookupvar and :setvar for backward compatibility" do
@scope.setvar("var", "yep")
@scope.lookupvar("var").should == "yep"
end
it "should fail if invoked with a non-string name" do
expect { @scope[:foo] }.to raise_error(Puppet::ParseError, /Scope variable name .* not a string/)
expect { @scope[:foo] = 12 }.to raise_error(Puppet::ParseError, /Scope variable name .* not a string/)
end
it "should return nil for unset variables" do
@scope["var"].should be_nil
end
it "should be able to look up values" do
@scope["var"] = "yep"
@scope["var"].should == "yep"
end
it "should be able to look up hashes" do
@scope["var"] = {"a" => "b"}
@scope["var"].should == {"a" => "b"}
end
it "should be able to look up variables in parent scopes" do
@topscope["var"] = "parentval"
@scope["var"].should == "parentval"
end
it "should prefer its own values to parent values" do
@topscope["var"] = "parentval"
@scope["var"] = "childval"
@scope["var"].should == "childval"
end
it "should be able to detect when variables are set" do
@scope["var"] = "childval"
@scope.should be_include("var")
end
it "does not allow changing a set value" do
@scope["var"] = "childval"
expect {
@scope["var"] = "change"
}.to raise_error(Puppet::Error, "Cannot reassign variable var")
end
it "should be able to detect when variables are not set" do
@scope.should_not be_include("var")
end
- it "should support iteration over its variables" do
- @scope["one"] = "two"
- @scope["three"] = "four"
- hash = {}
- @scope.each { |name, value| hash[name] = value }
- hash.should == {"one" => "two", "three" => "four" }
- end
-
- it "should include Enumerable" do
- @scope.singleton_class.ancestors.should be_include(Enumerable)
- end
-
describe "and the variable is qualified" do
before :each do
@known_resource_types = @scope.known_resource_types
node = Puppet::Node.new('localhost')
@compiler = Puppet::Parser::Compiler.new(node)
end
def newclass(name)
@known_resource_types.add Puppet::Resource::Type.new(:hostclass, name)
end
def create_class_scope(name)
klass = newclass(name)
catalog = Puppet::Resource::Catalog.new
catalog.add_resource(Puppet::Parser::Resource.new("stage", :main, :scope => Puppet::Parser::Scope.new(@compiler)))
Puppet::Parser::Resource.new("class", name, :scope => @scope, :source => mock('source'), :catalog => catalog).evaluate
@scope.class_scope(klass)
end
it "should be able to look up explicitly fully qualified variables from main" do
Puppet.expects(:deprecation_warning).never
other_scope = create_class_scope("")
other_scope["othervar"] = "otherval"
@scope["::othervar"].should == "otherval"
end
it "should be able to look up explicitly fully qualified variables from other scopes" do
Puppet.expects(:deprecation_warning).never
other_scope = create_class_scope("other")
other_scope["var"] = "otherval"
@scope["::other::var"].should == "otherval"
end
it "should be able to look up deeply qualified variables" do
Puppet.expects(:deprecation_warning).never
other_scope = create_class_scope("other::deep::klass")
other_scope["var"] = "otherval"
@scope["other::deep::klass::var"].should == "otherval"
end
it "should return nil for qualified variables that cannot be found in other classes" do
other_scope = create_class_scope("other::deep::klass")
@scope["other::deep::klass::var"].should be_nil
end
it "should warn and return nil for qualified variables whose classes have not been evaluated" do
klass = newclass("other::deep::klass")
@scope.expects(:warning)
@scope["other::deep::klass::var"].should be_nil
end
it "should warn and return nil for qualified variables whose classes do not exist" do
@scope.expects(:warning)
@scope["other::deep::klass::var"].should be_nil
end
it "should return nil when asked for a non-string qualified variable from a class that does not exist" do
@scope.stubs(:warning)
@scope["other::deep::klass::var"].should be_nil
end
it "should return nil when asked for a non-string qualified variable from a class that has not been evaluated" do
@scope.stubs(:warning)
klass = newclass("other::deep::klass")
@scope["other::deep::klass::var"].should be_nil
end
end
+
+ context "and strict_variables is true" do
+ before(:each) do
+ Puppet[:strict_variables] = true
+ end
+
+ it "should raise an error when unknown variable is looked up" do
+ expect { @scope['john_doe'] }.to raise_error(/Undefined variable/)
+ end
+
+ it "should raise an error when unknown qualified variable is looked up" do
+ expect { @scope['nowhere::john_doe'] }.to raise_error(/Undefined variable/)
+ end
+ end
end
describe "when variables are set with append=true" do
it "should raise error if the variable is already defined in this scope" do
@scope.setvar("var", "1", :append => false)
expect {
@scope.setvar("var", "1", :append => true)
}.to raise_error(
Puppet::ParseError,
"Cannot append, variable var is defined in this scope"
)
end
it "should lookup current variable value" do
@scope.expects(:[]).with("var").returns("2")
@scope.setvar("var", "1", :append => true)
end
it "should store the concatenated string '42'" do
@topscope.setvar("var", "4", :append => false)
@scope.setvar("var", "2", :append => true)
@scope["var"].should == "42"
end
it "should store the concatenated array [4,2]" do
@topscope.setvar("var", [4], :append => false)
@scope.setvar("var", [2], :append => true)
@scope["var"].should == [4,2]
end
it "should store the merged hash {a => b, c => d}" do
@topscope.setvar("var", {"a" => "b"}, :append => false)
@scope.setvar("var", {"c" => "d"}, :append => true)
@scope["var"].should == {"a" => "b", "c" => "d"}
end
it "should raise an error when appending a hash with something other than another hash" do
@topscope.setvar("var", {"a" => "b"}, :append => false)
expect {
@scope.setvar("var", "not a hash", :append => true)
}.to raise_error(
ArgumentError,
"Trying to append to a hash with something which is not a hash is unsupported"
)
end
end
describe "when calling number?" do
it "should return nil if called with anything not a number" do
Puppet::Parser::Scope.number?([2]).should be_nil
end
it "should return a Fixnum for a Fixnum" do
Puppet::Parser::Scope.number?(2).should be_an_instance_of(Fixnum)
end
it "should return a Float for a Float" do
Puppet::Parser::Scope.number?(2.34).should be_an_instance_of(Float)
end
it "should return 234 for '234'" do
Puppet::Parser::Scope.number?("234").should == 234
end
it "should return nil for 'not a number'" do
Puppet::Parser::Scope.number?("not a number").should be_nil
end
it "should return 23.4 for '23.4'" do
Puppet::Parser::Scope.number?("23.4").should == 23.4
end
it "should return 23.4e13 for '23.4e13'" do
Puppet::Parser::Scope.number?("23.4e13").should == 23.4e13
end
it "should understand negative numbers" do
Puppet::Parser::Scope.number?("-234").should == -234
end
it "should know how to convert exponential float numbers ala '23e13'" do
Puppet::Parser::Scope.number?("23e13").should == 23e13
end
it "should understand hexadecimal numbers" do
Puppet::Parser::Scope.number?("0x234").should == 0x234
end
it "should understand octal numbers" do
Puppet::Parser::Scope.number?("0755").should == 0755
end
it "should return nil on malformed integers" do
Puppet::Parser::Scope.number?("0.24.5").should be_nil
end
it "should convert strings with leading 0 to integer if they are not octal" do
Puppet::Parser::Scope.number?("0788").should == 788
end
it "should convert strings of negative integers" do
Puppet::Parser::Scope.number?("-0788").should == -788
end
it "should return nil on malformed hexadecimal numbers" do
Puppet::Parser::Scope.number?("0x89g").should be_nil
end
end
describe "when using ephemeral variables" do
it "should store the variable value" do
- @scope.setvar("1", :value, :ephemeral => true)
-
+# @scope.setvar("1", :value, :ephemeral => true)
+ @scope.set_match_data({1 => :value})
@scope["1"].should == :value
end
- it "should remove the variable value when unset_ephemeral_var is called" do
- @scope.setvar("1", :value, :ephemeral => true)
+ it "should remove the variable value when unset_ephemeral_var(:all) is called" do
+# @scope.setvar("1", :value, :ephemeral => true)
+ @scope.set_match_data({1 => :value})
@scope.stubs(:parent).returns(nil)
- @scope.unset_ephemeral_var
+ @scope.unset_ephemeral_var(:all)
@scope["1"].should be_nil
end
- it "should not remove classic variables when unset_ephemeral_var is called" do
+ it "should not remove classic variables when unset_ephemeral_var(:all) is called" do
@scope['myvar'] = :value1
- @scope.setvar("1", :value2, :ephemeral => true)
+ @scope.set_match_data({1 => :value2})
@scope.stubs(:parent).returns(nil)
- @scope.unset_ephemeral_var
+ @scope.unset_ephemeral_var(:all)
@scope["myvar"].should == :value1
end
- it "should raise an error when setting it again" do
- @scope.setvar("1", :value2, :ephemeral => true)
+ it "should raise an error when setting numerical variable" do
expect {
@scope.setvar("1", :value3, :ephemeral => true)
- }.to raise_error(Puppet::ParseError, /Cannot reassign variable 1/)
- end
-
- it "should declare ephemeral number only variable names" do
- @scope.ephemeral?("0").should be_true
- end
-
- it "should not declare ephemeral other variable names" do
- @scope.ephemeral?("abc0").should be_nil
+ }.to raise_error(Puppet::ParseError, /Cannot assign to a numeric match result variable/)
end
describe "with more than one level" do
it "should prefer latest ephemeral scopes" do
- @scope.setvar("0", :earliest, :ephemeral => true)
+ @scope.set_match_data({0 => :earliest})
@scope.new_ephemeral
- @scope.setvar("0", :latest, :ephemeral => true)
+ @scope.set_match_data({0 => :latest})
@scope["0"].should == :latest
end
it "should be able to report the current level" do
@scope.ephemeral_level.should == 1
@scope.new_ephemeral
@scope.ephemeral_level.should == 2
end
- it "should check presence of an ephemeral variable accross multiple levels" do
+ it "should not check presence of an ephemeral variable accross multiple levels" do
+ # This test was testing that scope actuallys screwed up - making values from earlier matches show as if they
+ # where true for latest match - insanity !
@scope.new_ephemeral
- @scope.setvar("1", :value1, :ephemeral => true)
+ @scope.set_match_data({1 => :value1})
@scope.new_ephemeral
- @scope.setvar("0", :value2, :ephemeral => true)
+ @scope.set_match_data({0 => :value2})
@scope.new_ephemeral
- @scope.ephemeral_include?("1").should be_true
+ @scope.include?("1").should be_false
end
it "should return false when an ephemeral variable doesn't exist in any ephemeral scope" do
@scope.new_ephemeral
- @scope.setvar("1", :value1, :ephemeral => true)
+ @scope.set_match_data({1 => :value1})
@scope.new_ephemeral
- @scope.setvar("0", :value2, :ephemeral => true)
+ @scope.set_match_data({0 => :value2})
@scope.new_ephemeral
- @scope.ephemeral_include?("2").should be_false
+ @scope.include?("2").should be_false
end
- it "should get ephemeral values from earlier scope when not in later" do
- @scope.setvar("1", :value1, :ephemeral => true)
+ it "should not get ephemeral values from earlier scope when not in later" do
+ @scope.set_match_data({1 => :value1})
@scope.new_ephemeral
- @scope.setvar("0", :value2, :ephemeral => true)
- @scope["1"].should == :value1
- end
-
- describe "when calling unset_ephemeral_var without a level" do
- it "should remove all the variables values" do
- @scope.setvar("1", :value1, :ephemeral => true)
- @scope.new_ephemeral
- @scope.setvar("1", :value2, :ephemeral => true)
-
- @scope.unset_ephemeral_var
-
- @scope["1"].should be_nil
- end
+ @scope.set_match_data({0 => :value2})
+ @scope.include?("1").should be_false
end
describe "when calling unset_ephemeral_var with a level" do
it "should remove ephemeral scopes up to this level" do
- @scope.setvar("1", :value1, :ephemeral => true)
+ @scope.set_match_data({1 => :value1})
@scope.new_ephemeral
- @scope.setvar("1", :value2, :ephemeral => true)
+ @scope.set_match_data({1 => :value2})
+ level = @scope.ephemeral_level()
@scope.new_ephemeral
- @scope.setvar("1", :value3, :ephemeral => true)
+ @scope.set_match_data({1 => :value3})
- @scope.unset_ephemeral_var(2)
+ @scope.unset_ephemeral_var(level)
@scope["1"].should == :value2
end
end
end
end
context "when using ephemeral as local scope" do
it "should store all variables in local scope" do
@scope.new_ephemeral true
@scope.setvar("apple", :fruit)
@scope["apple"].should == :fruit
end
it "should remove all local scope variables on unset" do
@scope.new_ephemeral true
@scope.setvar("apple", :fruit)
@scope["apple"].should == :fruit
@scope.unset_ephemeral_var
@scope["apple"].should == nil
end
it "should be created from a hash" do
@scope.ephemeral_from({ "apple" => :fruit, "strawberry" => :berry})
@scope["apple"].should == :fruit
@scope["strawberry"].should == :berry
end
end
describe "when setting ephemeral vars from matches" do
before :each do
@match = stub 'match', :is_a? => true
@match.stubs(:[]).with(0).returns("this is a string")
@match.stubs(:captures).returns([])
@scope.stubs(:setvar)
end
it "should accept only MatchData" do
expect {
@scope.ephemeral_from("match")
}.to raise_error(ArgumentError, /Invalid regex match data/)
end
it "should set $0 with the full match" do
- @scope.expects(:setvar).with { |*arg| arg[0] == "0" and arg[1] == "this is a string" and arg[2][:ephemeral] }
-
+ # This is an internal impl detail test
+ @scope.expects(:new_match_scope).with { |*arg| arg[0][0] == "this is a string" }
@scope.ephemeral_from(@match)
end
it "should set every capture as ephemeral var" do
- @match.stubs(:captures).returns([:capture1,:capture2])
- @scope.expects(:setvar).with { |*arg| arg[0] == "1" and arg[1] == :capture1 and arg[2][:ephemeral] }
- @scope.expects(:setvar).with { |*arg| arg[0] == "2" and arg[1] == :capture2 and arg[2][:ephemeral] }
+ # This is an internal impl detail test
+ @match.stubs(:[]).with(1).returns(:capture1)
+ @match.stubs(:[]).with(2).returns(:capture2)
+ @scope.expects(:new_match_scope).with { |*arg| arg[0][1] == :capture1 && arg[0][2] == :capture2 }
+
+ @scope.ephemeral_from(@match)
+ end
+
+ it "should shadow previous match variables" do
+ # This is an internal impl detail test
+ @match.stubs(:[]).with(1).returns(:capture1)
+ @match.stubs(:[]).with(2).returns(:capture2)
+ @match2 = stub 'match', :is_a? => true
+ @match2.stubs(:[]).with(1).returns(:capture2_1)
+ @match2.stubs(:[]).with(2).returns(nil)
@scope.ephemeral_from(@match)
+ @scope.ephemeral_from(@match2)
+ @scope.lookupvar('2').should == nil
end
it "should create a new ephemeral level" do
- @scope.expects(:new_ephemeral)
+ level_before = @scope.ephemeral_level
@scope.ephemeral_from(@match)
+ expect(level_before < @scope.ephemeral_level)
end
end
it "should use its namespaces to find hostclasses" do
klass = @scope.known_resource_types.add Puppet::Resource::Type.new(:hostclass, "a::b::c")
@scope.add_namespace "a::b"
@scope.find_hostclass("c").should equal(klass)
end
it "should use its namespaces to find definitions" do
define = @scope.known_resource_types.add Puppet::Resource::Type.new(:definition, "a::b::c")
@scope.add_namespace "a::b"
@scope.find_definition("c").should equal(define)
end
describe "when managing defaults" do
it "should be able to set and lookup defaults" do
param = Puppet::Parser::Resource::Param.new(:name => :myparam, :value => "myvalue", :source => stub("source"))
@scope.define_settings(:mytype, param)
@scope.lookupdefaults(:mytype).should == {:myparam => param}
end
it "should fail if a default is already defined and a new default is being defined" do
param = Puppet::Parser::Resource::Param.new(:name => :myparam, :value => "myvalue", :source => stub("source"))
@scope.define_settings(:mytype, param)
expect {
@scope.define_settings(:mytype, param)
}.to raise_error(Puppet::ParseError, /Default already defined .* cannot redefine/)
end
it "should return multiple defaults at once" do
param1 = Puppet::Parser::Resource::Param.new(:name => :myparam, :value => "myvalue", :source => stub("source"))
@scope.define_settings(:mytype, param1)
param2 = Puppet::Parser::Resource::Param.new(:name => :other, :value => "myvalue", :source => stub("source"))
@scope.define_settings(:mytype, param2)
@scope.lookupdefaults(:mytype).should == {:myparam => param1, :other => param2}
end
it "should look up defaults defined in parent scopes" do
param1 = Puppet::Parser::Resource::Param.new(:name => :myparam, :value => "myvalue", :source => stub("source"))
@scope.define_settings(:mytype, param1)
child_scope = @scope.newscope
param2 = Puppet::Parser::Resource::Param.new(:name => :other, :value => "myvalue", :source => stub("source"))
child_scope.define_settings(:mytype, param2)
child_scope.lookupdefaults(:mytype).should == {:myparam => param1, :other => param2}
end
end
context "#true?" do
{ "a string" => true,
"true" => true,
"false" => true,
true => true,
"" => false,
:undef => false,
nil => false
}.each do |input, output|
it "should treat #{input.inspect} as #{output}" do
Puppet::Parser::Scope.true?(input).should == output
end
end
end
context "when producing a hash of all variables (as used in templates)" do
it "should contain all defined variables in the scope" do
@scope.setvar("orange", :tangerine)
@scope.setvar("pear", :green)
@scope.to_hash.should == {'orange' => :tangerine, 'pear' => :green }
end
it "should contain variables in all local scopes (#21508)" do
@scope.new_ephemeral true
@scope.setvar("orange", :tangerine)
@scope.setvar("pear", :green)
@scope.new_ephemeral true
@scope.setvar("apple", :red)
@scope.to_hash.should == {'orange' => :tangerine, 'pear' => :green, 'apple' => :red }
end
it "should contain all defined variables in the scope and all local scopes" do
@scope.setvar("orange", :tangerine)
@scope.setvar("pear", :green)
@scope.new_ephemeral true
@scope.setvar("apple", :red)
@scope.to_hash.should == {'orange' => :tangerine, 'pear' => :green, 'apple' => :red }
end
it "should not contain varaibles in match scopes (non local emphemeral)" do
@scope.new_ephemeral true
@scope.setvar("orange", :tangerine)
@scope.setvar("pear", :green)
@scope.ephemeral_from(/(f)(o)(o)/.match('foo'))
@scope.to_hash.should == {'orange' => :tangerine, 'pear' => :green }
end
it "should delete values that are :undef in inner scope" do
@scope.new_ephemeral true
@scope.setvar("orange", :tangerine)
@scope.setvar("pear", :green)
@scope.new_ephemeral true
@scope.setvar("apple", :red)
@scope.setvar("orange", :undef)
@scope.to_hash.should == {'pear' => :green, 'apple' => :red }
end
end
end
diff --git a/spec/unit/parser/type_loader_spec.rb b/spec/unit/parser/type_loader_spec.rb
index c735db7c2..659ffa942 100755
--- a/spec/unit/parser/type_loader_spec.rb
+++ b/spec/unit/parser/type_loader_spec.rb
@@ -1,229 +1,225 @@
#! /usr/bin/env ruby
require 'spec_helper'
require 'puppet/parser/type_loader'
require 'puppet/parser/parser_factory'
require 'puppet/parser/e_parser_adapter'
require 'puppet_spec/modules'
require 'puppet_spec/files'
describe Puppet::Parser::TypeLoader do
include PuppetSpec::Modules
include PuppetSpec::Files
let(:empty_hostclass) { Puppet::Parser::AST::Hostclass.new('') }
let(:loader) { Puppet::Parser::TypeLoader.new(:myenv) }
it "should support an environment" do
loader = Puppet::Parser::TypeLoader.new(:myenv)
loader.environment.name.should == :myenv
end
- it "should include the Environment Helper" do
- loader.class.ancestors.should be_include(Puppet::Node::Environment::Helper)
- end
-
it "should delegate its known resource types to its environment" do
loader.known_resource_types.should be_instance_of(Puppet::Resource::TypeCollection)
end
describe "when loading names from namespaces" do
it "should do nothing if the name to import is an empty string" do
loader.try_load_fqname(:hostclass, "").should be_nil
end
it "should attempt to import each generated name" do
loader.expects(:import_from_modules).with("foo/bar").returns([])
loader.expects(:import_from_modules).with("foo").returns([])
loader.try_load_fqname(:hostclass, "foo::bar")
end
it "should attempt to load each possible name going from most to least specific" do
path_order = sequence('path')
['foo/bar/baz', 'foo/bar', 'foo'].each do |path|
Puppet::Parser::Files.expects(:find_manifests_in_modules).with(path, anything).returns([nil, []]).in_sequence(path_order)
end
loader.try_load_fqname(:hostclass, 'foo::bar::baz')
end
end
describe "when importing" do
let(:stub_parser) { stub 'Parser', :file= => nil, :parse => empty_hostclass }
before(:each) do
Puppet::Parser::ParserFactory.stubs(:parser).with(anything).returns(stub_parser)
end
it "should return immediately when imports are being ignored" do
Puppet::Parser::Files.expects(:find_manifests_in_modules).never
Puppet[:ignoreimport] = true
loader.import("foo", "/path").should be_nil
end
it "should find all manifests matching the file or pattern" do
Puppet::Parser::Files.expects(:find_manifests_in_modules).with("myfile", anything).returns ["modname", %w{one}]
loader.import("myfile", "/path")
end
it "should pass the environment when looking for files" do
Puppet::Parser::Files.expects(:find_manifests_in_modules).with(anything, loader.environment).returns ["modname", %w{one}]
loader.import("myfile", "/path")
end
it "should fail if no files are found" do
Puppet::Parser::Files.expects(:find_manifests_in_modules).returns [nil, []]
lambda { loader.import("myfile", "/path") }.should raise_error(Puppet::ImportError)
end
it "should parse each found file" do
Puppet::Parser::Files.expects(:find_manifests_in_modules).returns ["modname", [make_absolute("/one")]]
loader.expects(:parse_file).with(make_absolute("/one")).returns(Puppet::Parser::AST::Hostclass.new(''))
loader.import("myfile", "/path")
end
it "should not attempt to import files that have already been imported" do
loader = Puppet::Parser::TypeLoader.new(:myenv)
Puppet::Parser::Files.expects(:find_manifests_in_modules).twice.returns ["modname", %w{/one}]
loader.import("myfile", "/path").should_not be_empty
loader.import("myfile", "/path").should be_empty
end
end
describe "when importing all" do
before do
@base = tmpdir("base")
# Create two module path directories
@modulebase1 = File.join(@base, "first")
FileUtils.mkdir_p(@modulebase1)
@modulebase2 = File.join(@base, "second")
FileUtils.mkdir_p(@modulebase2)
Puppet[:modulepath] = "#{@modulebase1}#{File::PATH_SEPARATOR}#{@modulebase2}"
end
def mk_module(basedir, name)
PuppetSpec::Modules.create(name, basedir)
end
# We have to pass the base path so that we can
# write to modules that are in the second search path
def mk_manifests(base, mod, type, files)
exts = {"ruby" => ".rb", "puppet" => ".pp"}
files.collect do |file|
name = mod.name + "::" + file.gsub("/", "::")
path = File.join(base, mod.name, "manifests", file + exts[type])
FileUtils.mkdir_p(File.split(path)[0])
# write out the class
if type == "ruby"
File.open(path, "w") { |f| f.print "hostclass '#{name}' do\nend" }
else
File.open(path, "w") { |f| f.print "class #{name} {}" }
end
name
end
end
it "should load all puppet manifests from all modules in the specified environment" do
@module1 = mk_module(@modulebase1, "one")
@module2 = mk_module(@modulebase2, "two")
mk_manifests(@modulebase1, @module1, "puppet", %w{a b})
mk_manifests(@modulebase2, @module2, "puppet", %w{c d})
loader.import_all
loader.environment.known_resource_types.hostclass("one::a").should be_instance_of(Puppet::Resource::Type)
loader.environment.known_resource_types.hostclass("one::b").should be_instance_of(Puppet::Resource::Type)
loader.environment.known_resource_types.hostclass("two::c").should be_instance_of(Puppet::Resource::Type)
loader.environment.known_resource_types.hostclass("two::d").should be_instance_of(Puppet::Resource::Type)
end
it "should load all ruby manifests from all modules in the specified environment" do
Puppet.expects(:deprecation_warning).at_least(1)
@module1 = mk_module(@modulebase1, "one")
@module2 = mk_module(@modulebase2, "two")
mk_manifests(@modulebase1, @module1, "ruby", %w{a b})
mk_manifests(@modulebase2, @module2, "ruby", %w{c d})
loader.import_all
loader.environment.known_resource_types.hostclass("one::a").should be_instance_of(Puppet::Resource::Type)
loader.environment.known_resource_types.hostclass("one::b").should be_instance_of(Puppet::Resource::Type)
loader.environment.known_resource_types.hostclass("two::c").should be_instance_of(Puppet::Resource::Type)
loader.environment.known_resource_types.hostclass("two::d").should be_instance_of(Puppet::Resource::Type)
end
it "should not load manifests from duplicate modules later in the module path" do
@module1 = mk_module(@modulebase1, "one")
# duplicate
@module2 = mk_module(@modulebase2, "one")
mk_manifests(@modulebase1, @module1, "puppet", %w{a})
mk_manifests(@modulebase2, @module2, "puppet", %w{c})
loader.import_all
loader.environment.known_resource_types.hostclass("one::c").should be_nil
end
it "should load manifests from subdirectories" do
@module1 = mk_module(@modulebase1, "one")
mk_manifests(@modulebase1, @module1, "puppet", %w{a a/b a/b/c})
loader.import_all
loader.environment.known_resource_types.hostclass("one::a::b").should be_instance_of(Puppet::Resource::Type)
loader.environment.known_resource_types.hostclass("one::a::b::c").should be_instance_of(Puppet::Resource::Type)
end
it "should skip modules that don't have manifests" do
@module1 = mk_module(@modulebase1, "one")
@module2 = mk_module(@modulebase2, "two")
mk_manifests(@modulebase2, @module2, "ruby", %w{c d})
loader.import_all
loader.environment.known_resource_types.hostclass("one::a").should be_nil
loader.environment.known_resource_types.hostclass("two::c").should be_instance_of(Puppet::Resource::Type)
loader.environment.known_resource_types.hostclass("two::d").should be_instance_of(Puppet::Resource::Type)
end
end
describe "when parsing a file" do
it "should create a new parser instance for each file using the current environment" do
parser = stub 'Parser', :file= => nil, :parse => empty_hostclass
Puppet::Parser::ParserFactory.expects(:parser).twice.with(loader.environment).returns(parser)
loader.parse_file("/my/file")
loader.parse_file("/my/other_file")
end
it "should assign the parser its file and parse" do
parser = mock 'parser'
Puppet::Parser::ParserFactory.expects(:parser).with(loader.environment).returns(parser)
parser.expects(:file=).with("/my/file")
parser.expects(:parse).returns(empty_hostclass)
loader.parse_file("/my/file")
end
end
it "should be able to add classes to the current resource type collection" do
file = tmpfile("simple_file.pp")
File.open(file, "w") { |f| f.puts "class foo {}" }
loader.import(File.basename(file), File.dirname(file))
loader.known_resource_types.hostclass("foo").should be_instance_of(Puppet::Resource::Type)
end
end
diff --git a/spec/unit/pops/benchmark_spec.rb b/spec/unit/pops/benchmark_spec.rb
new file mode 100644
index 000000000..03c2e743d
--- /dev/null
+++ b/spec/unit/pops/benchmark_spec.rb
@@ -0,0 +1,142 @@
+#! /usr/bin/env ruby
+require 'spec_helper'
+require 'puppet/pops'
+require 'puppet_spec/pops'
+require 'puppet_spec/scope'
+
+require 'rgen/environment'
+require 'rgen/metamodel_builder'
+require 'rgen/serializer/json_serializer'
+require 'rgen/instantiator/json_instantiator'
+
+describe "Benchmark", :benchmark => true do
+ include PuppetSpec::Pops
+ include PuppetSpec::Scope
+
+ def code
+ 'if true
+{
+$a = 10 + 10
+}
+else
+{
+$a = "interpolate ${foo} and stuff"
+}
+' end
+
+ class StringWriter < String
+ alias write concat
+ end
+
+ class MyJSonSerializer < RGen::Serializer::JsonSerializer
+ def attributeValue(value, a)
+ x = super
+ puts "#{a.eType} value: <<#{value}>> serialize: <<#{x}>>"
+ x
+ end
+ end
+
+ def json_dump(model)
+ output = StringWriter.new
+ ser = MyJSonSerializer.new(output)
+ ser.serialize(model)
+ output
+ end
+
+ def json_load(string)
+ env = RGen::Environment.new
+ inst = RGen::Instantiator::JsonInstantiator.new(env, Puppet::Pops::Model)
+ inst.instantiate(string)
+ end
+
+ it "transformer", :profile => true do
+ parser = Puppet::Pops::Parser::Parser.new()
+ model = parser.parse_string(code).current
+ transformer = Puppet::Pops::Model::AstTransformer.new()
+ m = Benchmark.measure { 10000.times { transformer.transform(model) }}
+ puts "Transformer: #{m}"
+ end
+
+ it "validator", :profile => true do
+ parser = Puppet::Pops::Parser::EvaluatingParser.new()
+ model = parser.parse_string(code)
+ m = Benchmark.measure { 100000.times { parser.assert_and_report(model) }}
+ puts "Validator: #{m}"
+ end
+
+ it "parse transform", :profile => true do
+ parser = Puppet::Pops::Parser::Parser.new()
+ transformer = Puppet::Pops::Model::AstTransformer.new()
+ m = Benchmark.measure { 10000.times { transformer.transform(parser.parse_string(code).current) }}
+ puts "Parse and transform: #{m}"
+ end
+
+ it "parser0", :profile => true do
+ parser = Puppet::Parser::Parser.new('test')
+ m = Benchmark.measure { 10000.times { parser.parse(code) }}
+ puts "Parser 0: #{m}"
+ end
+
+ it "parser1", :profile => true do
+ parser = Puppet::Pops::Parser::EvaluatingParser.new()
+ m = Benchmark.measure { 10000.times { parser.parse_string(code) }}
+ puts "Parser1: #{m}"
+ end
+
+ it "marshal1", :profile => true do
+ parser = Puppet::Pops::Parser::EvaluatingParser.new()
+ model = parser.parse_string(code).current
+ dumped = Marshal.dump(model)
+ m = Benchmark.measure { 10000.times { Marshal.load(dumped) }}
+ puts "Marshal1: #{m}"
+ end
+
+ it "rgenjson", :profile => true do
+ parser = Puppet::Pops::Parser::EvaluatingParser.new()
+ model = parser.parse_string(code).current
+ dumped = json_dump(model)
+ m = Benchmark.measure { 10000.times { json_load(dumped) }}
+ puts "RGen Json: #{m}"
+ end
+
+ it "lexer2", :profile => true do
+ lexer = Puppet::Pops::Parser::Lexer2.new
+ m = Benchmark.measure {10000.times {lexer.string = code; lexer.fullscan }}
+ puts "Lexer2: #{m}"
+ end
+
+ it "lexer1", :profile => true do
+ lexer = Puppet::Pops::Parser::Lexer.new
+ m = Benchmark.measure {10000.times {lexer.string = code; lexer.fullscan }}
+ puts "Pops Lexer: #{m}"
+ end
+
+ it "lexer0", :profile => true do
+ lexer = Puppet::Parser::Lexer.new
+ m = Benchmark.measure {10000.times {lexer.string = code; lexer.fullscan }}
+ puts "Original Lexer: #{m}"
+ end
+
+ context "Measure Evaluator" do
+ let(:parser) { Puppet::Pops::Parser::EvaluatingParser::Transitional.new }
+ let(:node) { 'node.example.com' }
+ let(:scope) { s = create_test_scope_for_node(node); s }
+ it "evaluator", :profile => true do
+ # Do the loop in puppet code since it otherwise drowns in setup
+ puppet_loop =
+ 'Integer[0, 1000].each |$i| { if true
+{
+$a = 10 + 10
+}
+else
+{
+$a = "interpolate ${foo} and stuff"
+}}
+'
+ # parse once, only measure the evaluation
+ model = parser.parse_string(puppet_loop, __FILE__)
+ m = Benchmark.measure { parser.evaluate(create_test_scope_for_node(node), model) }
+ puts("Evaluator: #{m}")
+ end
+ end
+end
diff --git a/spec/unit/pops/binder/binder_spec.rb b/spec/unit/pops/binder/binder_spec.rb
index dfcf633e1..fec4496ff 100644
--- a/spec/unit/pops/binder/binder_spec.rb
+++ b/spec/unit/pops/binder/binder_spec.rb
@@ -1,62 +1,43 @@
require 'spec_helper'
require 'puppet/pops'
module BinderSpecModule
def factory()
Puppet::Pops::Binder::BindingsFactory
end
def injector(binder)
Puppet::Pops::Binder::Injector.new(binder)
end
def binder()
Puppet::Pops::Binder::Binder.new()
end
def test_layer_with_empty_bindings
factory.named_layer('test-layer', factory.named_bindings('test').model)
end
end
describe 'Binder' do
include BinderSpecModule
- context 'when defining categories' do
- it 'redefinition is not allowed' do
- expect do
- b = binder()
- b.define_categories(factory.categories([]))
- b.define_categories(factory.categories([]))
- end.to raise_error(/Cannot redefine/)
- end
- end
-
+ # TODO: Test binder + parent binder
context 'when defining layers' do
- it 'they must be defined after categories' do
- expect do
- binder().define_layers(factory.layered_bindings(test_layer_with_empty_bindings))
- end.to raise_error(/Categories must be defined first/)
- end
-
- it 'redefinition is not allowed' do
- expect do
- b = binder()
- b.define_categories(factory.categories([]))
- b.define_layers(factory.layered_bindings(test_layer_with_empty_bindings))
- b.define_layers(factory.layered_bindings(test_layer_with_empty_bindings))
- end.to raise_error(/Cannot redefine its content/)
- end
- end
- context 'when defining categories and layers' do
- it 'a binder should report being configured when both categories and layers have been defined' do
- b = binder()
- b.configured?().should == false
- b.define_categories(factory.categories([]))
- b.configured?().should == false
- b.define_layers(factory.layered_bindings(test_layer_with_empty_bindings))
- b.configured?().should == true
- end
+# it 'redefinition is not allowed' do
+# expect do
+# b = binder()
+# b.define_layers(factory.layered_bindings(test_layer_with_empty_bindings))
+# b.define_layers(factory.layered_bindings(test_layer_with_empty_bindings))
+# end.to raise_error(/Cannot redefine its content/)
+# end
+#
+# it 'a binder should report being configured when layers have been defined' do
+# b = binder()
+# b.configured?().should == false
+# b.define_layers(factory.layered_bindings(test_layer_with_empty_bindings))
+# b.configured?().should == true
+# end
end
end
\ No newline at end of file
diff --git a/spec/unit/pops/binder/bindings_checker_spec.rb b/spec/unit/pops/binder/bindings_checker_spec.rb
index dc3934c6c..9d8630de2 100644
--- a/spec/unit/pops/binder/bindings_checker_spec.rb
+++ b/spec/unit/pops/binder/bindings_checker_spec.rb
@@ -1,196 +1,155 @@
require 'spec_helper'
require 'puppet/pops'
require 'puppet_spec/pops'
describe 'The bindings checker' do
include PuppetSpec::Pops
Issues = Puppet::Pops::Binder::BinderIssues
Bindings = Puppet::Pops::Binder::Bindings
TypeFactory = Puppet::Pops::Types::TypeFactory
let (:acceptor) { Puppet::Pops::Validation::Acceptor.new() }
let (:binding) { Bindings::Binding.new() }
let (:ok_binding) {
b = Bindings::Binding.new()
b.producer = Bindings::ConstantProducerDescriptor.new()
b.producer.value = 'some value'
b.type = TypeFactory.string()
b
}
def validate(binding)
Puppet::Pops::Binder::BindingsValidatorFactory.new().validator(acceptor).validate(binding)
end
def bindings(*args)
b = Bindings::Bindings.new()
b.bindings = args
b
end
def named_bindings(name, *args)
b = Bindings::NamedBindings.new()
b.name = name
b.bindings = args
b
end
- def category(name, value)
- b = Bindings::Category.new()
- b.categorization = name
- b.value = value
- b
- end
-
- def categorized_bindings(bindings, *predicates)
- b = Bindings::CategorizedBindings.new()
- b.bindings = bindings
- b.predicates = predicates
- b
- end
-
def layer(name, *bindings)
l = Bindings::NamedLayer.new()
l.name = name
l.bindings = bindings
l
end
def layered_bindings(*layers)
b = Bindings::LayeredBindings.new()
b.layers = layers
b
end
def array_multibinding()
b = Bindings::Multibinding.new()
b.producer = Bindings::ArrayMultibindProducerDescriptor.new()
b.type = TypeFactory.array_of_data()
b
end
def bad_array_multibinding()
b = array_multibinding()
b.type = TypeFactory.hash_of_data() # intentionally wrong!
b
end
def hash_multibinding()
b = Bindings::Multibinding.new()
b.producer = Bindings::HashMultibindProducerDescriptor.new()
b.type = TypeFactory.hash_of_data()
b
end
def bad_hash_multibinding()
b = hash_multibinding()
b.type = TypeFactory.array_of_data() # intentionally wrong!
b
end
it 'should complain about missing producer and type' do
validate(binding())
acceptor.should have_issue(Issues::MISSING_PRODUCER)
acceptor.should have_issue(Issues::MISSING_TYPE)
end
context 'when checking array multibinding' do
it 'should complain about non array producers' do
validate(bad_array_multibinding())
acceptor.should have_issue(Issues::MULTIBIND_INCOMPATIBLE_TYPE)
end
end
context 'when checking hash multibinding' do
it 'should complain about non hash producers' do
validate(bad_hash_multibinding())
acceptor.should have_issue(Issues::MULTIBIND_INCOMPATIBLE_TYPE)
end
end
context 'when checking bindings' do
it 'should not accept zero bindings' do
validate(bindings())
acceptor.should have_issue(Issues::MISSING_BINDINGS)
end
it 'should accept non-zero bindings' do
validate(bindings(ok_binding))
acceptor.errors_or_warnings?.should() == false
end
it 'should check contained bindings' do
validate(bindings(bad_array_multibinding()))
acceptor.should have_issue(Issues::MULTIBIND_INCOMPATIBLE_TYPE)
end
end
context 'when checking named bindings' do
it 'should accept named bindings' do
validate(named_bindings('garfield', ok_binding))
acceptor.errors_or_warnings?.should() == false
end
it 'should not accept unnamed bindings' do
validate(named_bindings(nil, ok_binding))
acceptor.should have_issue(Issues::MISSING_BINDINGS_NAME)
end
it 'should do generic bindings check' do
validate(named_bindings('garfield'))
acceptor.should have_issue(Issues::MISSING_BINDINGS)
end
end
- context 'when checking categorized bindings' do
- it 'should accept non-zero predicates' do
- validate(categorized_bindings([ok_binding], category('foo', 'bar')))
- acceptor.errors_or_warnings?.should() == false
- end
-
- it 'should not accept zero predicates' do
- validate(categorized_bindings([ok_binding]))
- acceptor.should have_issue(Issues::MISSING_PREDICATES)
- end
-
- it 'should not accept predicates that has no categorization' do
- validate(categorized_bindings([ok_binding], category(nil, 'bar')))
- acceptor.should have_issue(Issues::MISSING_CATEGORIZATION)
- end
-
- it 'should not accept predicates that has no value' do
- validate(categorized_bindings([ok_binding], category('foo', nil)))
- acceptor.should have_issue(Issues::MISSING_CATEGORY_VALUE)
- end
-
- it 'should do generic bindings check' do
- validate(categorized_bindings([], category('foo', 'bar')))
- acceptor.should have_issue(Issues::MISSING_BINDINGS)
- end
- end
-
context 'when checking layered bindings' do
it 'should not accept zero layers' do
validate(layered_bindings())
acceptor.should have_issue(Issues::MISSING_LAYERS)
end
it 'should accept non-zero layers' do
validate(layered_bindings(layer('foo', named_bindings('bar', ok_binding))))
acceptor.errors_or_warnings?.should() == false
end
it 'should not accept unnamed layers' do
validate(layered_bindings(layer(nil, named_bindings('bar', ok_binding))))
acceptor.should have_issue(Issues::MISSING_LAYER_NAME)
end
it 'should accept layers without bindings' do
validate(layered_bindings(layer('foo')))
acceptor.should_not have_issue(Issues::MISSING_BINDINGS_IN_LAYER)
end
end
end
diff --git a/spec/unit/pops/binder/bindings_composer_spec.rb b/spec/unit/pops/binder/bindings_composer_spec.rb
index a0986d4dd..93bc44722 100644
--- a/spec/unit/pops/binder/bindings_composer_spec.rb
+++ b/spec/unit/pops/binder/bindings_composer_spec.rb
@@ -1,89 +1,64 @@
require 'spec_helper'
+require 'puppet/pops'
require 'puppet_spec/pops'
describe 'BinderComposer' do
include PuppetSpec::Pops
def config_dir(config_name)
my_fixture(config_name)
end
let(:acceptor) { Puppet::Pops::Validation::Acceptor.new() }
let(:diag) { Puppet::Pops::Binder::Config::DiagnosticProducer.new(acceptor) }
let(:issues) { Puppet::Pops::Binder::Config::Issues }
let(:node) { Puppet::Node.new('localhost') }
let(:compiler) { Puppet::Parser::Compiler.new(node)}
let(:scope) { Puppet::Parser::Scope.new(compiler) }
let(:parser) { Puppet::Pops::Parser::Parser.new() }
let(:factory) { Puppet::Pops::Binder::BindingsFactory }
before(:each) do
Puppet[:binder] = true
end
it 'should load default config if no config file exists' do
diagnostics = diag
composer = Puppet::Pops::Binder::BindingsComposer.new()
composer.compose(scope)
end
context "when loading a complete configuration with modules" do
let(:config_directory) { config_dir('ok') }
it 'should load everything without errors' do
Puppet.settings[:confdir] = config_directory
+ Puppet.settings[:libdir] = File.join(config_directory, 'lib')
Puppet.settings[:modulepath] = File.join(config_directory, 'modules')
+ # this ensure the binder is active at the right time
+ # (issues with getting a /dev/null path for "confdir" / "libdir")
+ raise "Binder not active" unless scope.compiler.is_binder_active?
diagnostics = diag
composer = Puppet::Pops::Binder::BindingsComposer.new()
the_scope = scope
the_scope['fqdn'] = 'localhost'
the_scope['environment'] = 'production'
layered_bindings = composer.compose(scope)
# puts Puppet::Pops::Binder::BindingsModelDumper.new().dump(layered_bindings)
- binder = Puppet::Pops::Binder::Binder.new()
- # TODO: this is cheating, the categories should come from the composer/config
- binder.define_categories(factory.categories([['node', 'localhost'], ['environment', 'production']]))
- binder.define_layers(layered_bindings)
+ binder = Puppet::Pops::Binder::Binder.new(layered_bindings)
injector = Puppet::Pops::Binder::Injector.new(binder)
-
expect(injector.lookup(scope, 'awesome_x')).to be == 'golden'
expect(injector.lookup(scope, 'good_x')).to be == 'golden'
expect(injector.lookup(scope, 'rotten_x')).to be == nil
expect(injector.lookup(scope, 'the_meaning_of_life')).to be == 42
expect(injector.lookup(scope, 'has_funny_hat')).to be == 'the pope'
expect(injector.lookup(scope, 'all your base')).to be == 'are belong to us'
expect(injector.lookup(scope, 'env_meaning_of_life')).to be == 'production thinks it is 42'
expect(injector.lookup(scope, '::quick::brown::fox')).to be == 'echo: quick brown fox'
- expect(injector.lookup(scope, 'echo::common')).to be == 'echo... awesome/common'
- expect(injector.lookup(scope, 'echo::localhost')).to be == 'echo... awesome/localhost'
- end
- end
-
- context "when loading a configuration with hiera1 hiera.yaml" do
- let(:config_directory) { config_dir('hiera1config') }
-
- it 'should load without errors by skipping the hiera.yaml' do
- Puppet.settings[:confdir] = config_directory
- Puppet.settings[:modulepath] = File.join(config_directory, 'modules')
-
- diagnostics = diag
- composer = Puppet::Pops::Binder::BindingsComposer.new()
- the_scope = scope
- the_scope['fqdn'] = 'localhost'
- the_scope['environment'] = 'production'
- layered_bindings = composer.compose(scope)
- # puts Puppet::Pops::Binder::BindingsModelDumper.new().dump(layered_bindings)
- binder = Puppet::Pops::Binder::Binder.new()
- # TODO: this is cheating, the categories should come from the composer/config
- binder.define_categories(factory.categories([['node', 'localhost'], ['environment', 'production']]))
- binder.define_layers(layered_bindings)
- injector = Puppet::Pops::Binder::Injector.new(binder)
-
- expect(injector.lookup(scope, 'the_meaning_of_life')).to be == 300
end
end
# TODO: test error conditions (see BinderConfigChecker for what to test)
end
\ No newline at end of file
diff --git a/spec/unit/pops/binder/config/binder_config_spec.rb b/spec/unit/pops/binder/config/binder_config_spec.rb
index dcd626737..67fe0bb6e 100644
--- a/spec/unit/pops/binder/config/binder_config_spec.rb
+++ b/spec/unit/pops/binder/config/binder_config_spec.rb
@@ -1,48 +1,35 @@
require 'spec_helper'
require 'puppet/pops'
require 'puppet_spec/pops'
describe 'BinderConfig' do
include PuppetSpec::Pops
let(:acceptor) { Puppet::Pops::Validation::Acceptor.new() }
let(:diag) { Puppet::Pops::Binder::Config::DiagnosticProducer.new(acceptor) }
let(:issues) { Puppet::Pops::Binder::Config::Issues }
it 'should load default config if no config file exists' do
diagnostics = diag
config = Puppet::Pops::Binder::Config::BinderConfig.new(diagnostics)
expect(acceptor.errors?()).to be == false
expect(config.layering_config[0]['name']).to be == 'site'
- expect(config.layering_config[0]['include']).to be == ['confdir-hiera:/', 'confdir:/default?optional']
+ expect(config.layering_config[0]['include']).to be == ['confdir:/default?optional']
expect(config.layering_config[1]['name']).to be == 'modules'
- expect(config.layering_config[1]['include']).to be == ['module-hiera:/*/', 'module:/*::default']
-
- expect(config.categorization.is_a?(Array)).to be == true
- expect(config.categorization.size).to be == 4
- expect(config.categorization[0][0]).to be == 'node'
- expect(config.categorization[1][0]).to be == 'osfamily'
- expect(config.categorization[2][0]).to be == 'environment'
- expect(config.categorization[3][0]).to be == 'common'
+ expect(config.layering_config[1]['include']).to be == ['module:/*::default']
end
it 'should load binder_config.yaml if it exists in confdir)' do
Puppet::Pops::Binder::Config::BinderConfig.any_instance.stubs(:confdir).returns(my_fixture("/ok/"))
config = Puppet::Pops::Binder::Config::BinderConfig.new(diag)
expect(acceptor.errors?()).to be == false
expect(config.layering_config[0]['name']).to be == 'site'
- expect(config.layering_config[0]['include']).to be == 'confdir-hiera:/'
+ expect(config.layering_config[0]['include']).to be == 'confdir:/'
expect(config.layering_config[1]['name']).to be == 'modules'
- expect(config.layering_config[1]['include']).to be == 'module-hiera:/*/'
- expect(config.layering_config[1]['exclude']).to be == 'module-hiera:/bad/'
-
- expect(config.categorization.is_a?(Array)).to be == true
- expect(config.categorization.size).to be == 3
- expect(config.categorization[0][0]).to be == 'node'
- expect(config.categorization[1][0]).to be == 'environment'
- expect(config.categorization[2][0]).to be == 'common'
+ expect(config.layering_config[1]['include']).to be == 'module:/*::test/'
+ expect(config.layering_config[1]['exclude']).to be == 'module:/bad::test/'
end
# TODO: test error conditions (see BinderConfigChecker for what to test)
end
\ No newline at end of file
diff --git a/spec/unit/pops/binder/hiera2/bindings_provider_spec.rb b/spec/unit/pops/binder/hiera2/bindings_provider_spec.rb
deleted file mode 100644
index edb4c7df2..000000000
--- a/spec/unit/pops/binder/hiera2/bindings_provider_spec.rb
+++ /dev/null
@@ -1,74 +0,0 @@
-require 'spec_helper'
-require 'puppet/pops'
-require 'puppet_spec/pops'
-
-describe 'The hiera2 bindings provider' do
-
- include PuppetSpec::Pops
-
- def config_dir(config_name)
- File.dirname(my_fixture("#{config_name}/hiera.yaml"))
- end
-
- before(:each) do
- Puppet[:binder] = true
- end
-
- context 'when loading ok bindings' do
-
- let(:node) { 'node.example.com' }
- let(:acceptor) { Puppet::Pops::Validation::Acceptor.new() }
- let(:scope) { s = Puppet::Parser::Scope.new_for_test_harness(node); s['a'] = '42'; s['node'] = node; s }
- let(:module_dir) { config_dir('ok') }
- let(:node_binder) { b = Puppet::Pops::Binder::Binder.new(); b.define_categories(Puppet::Pops::Binder::BindingsFactory.categories(['node', node])); b }
- let(:bindings) { Puppet::Pops::Binder::Hiera2::BindingsProvider.new('test', module_dir, acceptor).load_bindings(scope) }
- let(:test_layer_with_bindings) { bindings }
-
- it 'should load and validate OK bindings' do
- Puppet::Pops::Binder::BindingsValidatorFactory.new().validator(acceptor).validate(bindings)
- acceptor.errors_or_warnings?.should() == false
- end
-
- it 'should contain the expected effective categories' do
- bindings.effective_categories.categories.collect {|c| [c.categorization, c.value] }.should == [['node', 'node.example.com']]
- end
-
- it 'should produce the expected bindings model' do
- bindings.class.should() == Puppet::Pops::Binder::Bindings::ContributedBindings
- bindings.bindings.bindings.each do |cat|
- cat.class.should() == Puppet::Pops::Binder::Bindings::CategorizedBindings
- cat.predicates.length.should() == 1
- cat.predicates[0].categorization.should() == 'node'
- cat.predicates[0].value.should() == node
- cat.bindings.each do |b|
- b.class.should() == Puppet::Pops::Binder::Bindings::Binding
- ['a_number', 'a_string', 'an_eval', 'an_eval2', 'a_json_number', 'a_json_string', 'a_json_eval',
- 'a_json_eval2', 'a_json_hash', 'a_json_array'].index(b.name).should() >= 0
- b.producer.class.should() == Puppet::Pops::Binder::Bindings::EvaluatingProducerDescriptor if b.name == 'an_eval'
- end
- end
- end
-
- it 'should make the injector lookup expected constants' do
- node_binder.define_layers(Puppet::Pops::Binder::BindingsFactory.layered_bindings(test_layer_with_bindings))
- injector = Puppet::Pops::Binder::Injector.new(node_binder)
-
- injector.lookup(scope, 'a_number').should == 42
- injector.lookup(scope, 'a_string').should == 'forty two'
- injector.lookup(scope, 'a_json_number').should == 142
- injector.lookup(scope, 'a_json_string').should == 'one hundred and forty two'
- expect(injector.lookup(scope, "a_json_array")).to be == ["a", "b", 100]
- expect(injector.lookup(scope, "a_json_hash")).to be == {"a" => 1, "b" => 2}
- end
-
- it 'should make the injector lookup and evaluate expressions' do
- node_binder.define_layers(Puppet::Pops::Binder::BindingsFactory.layered_bindings(test_layer_with_bindings))
- injector = Puppet::Pops::Binder::Injector.new(node_binder)
-
- injector.lookup(scope, 'an_eval').should == 'the answer from "yaml" is 42.'
- injector.lookup(scope, 'an_eval2').should == "the answer\nfrom \\\"yaml\\\" is 42 and $a"
- injector.lookup(scope, 'a_json_eval').should == 'the answer from "json" is 42 and ${a}.'
- injector.lookup(scope, 'a_json_eval2').should == "the answer\nfrom \\\"json\\\" is 42 and $a"
- end
- end
-end
diff --git a/spec/unit/pops/binder/hiera2/config_spec.rb b/spec/unit/pops/binder/hiera2/config_spec.rb
deleted file mode 100644
index 970e72a9a..000000000
--- a/spec/unit/pops/binder/hiera2/config_spec.rb
+++ /dev/null
@@ -1,61 +0,0 @@
-require 'spec_helper'
-require 'puppet/pops'
-require 'puppet_spec/pops'
-
-# A Backend class that doesn't implement the needed API
-class Puppet::Pops::Binder::Hiera2::Bad_backend
-end
-
-describe 'The hiera2 config' do
-
- include PuppetSpec::Pops
-
- let(:acceptor) { Puppet::Pops::Validation::Acceptor.new() }
- let(:diag) { Puppet::Pops::Binder::Hiera2::DiagnosticProducer.new(acceptor) }
-
- def config_dir(config_name)
- File.dirname(my_fixture("#{config_name}/hiera.yaml"))
- end
-
- def test_config_issue(config_name, issue)
- Puppet::Pops::Binder::Hiera2::Config.new(config_dir(config_name), diag)
- acceptor.should have_issue(issue)
- end
-
- it 'should load and validate OK configuration' do
- Puppet::Pops::Binder::Hiera2::Config.new(config_dir('ok'), diag)
- acceptor.errors_or_warnings?.should() == false
- end
-
- it 'should report missing config file' do
- Puppet::Pops::Binder::Hiera2::Config.new(File.dirname(my_fixture('missing/foo.txt')), diag)
- acceptor.should have_issue(Puppet::Pops::Binder::Hiera2::Issues::CONFIG_FILE_NOT_FOUND)
- end
-
- it 'should report when config is not a hash' do
- test_config_issue('not_a_hash', Puppet::Pops::Binder::Hiera2::Issues::CONFIG_IS_NOT_HASH)
- end
-
- it 'should report when config has syntax problems' do
- if RUBY_VERSION.start_with?("1.8")
- # Yes, it is a lobotomy or 2 short of a full brain...
- # if a hash key is not in quotes it continues on the next line and gobbles what is there instead
- # of reporting an error
- test_config_issue('bad_syntax', Puppet::Pops::Binder::Hiera2::Issues::MISSING_HIERARCHY)
- else
- test_config_issue('bad_syntax', Puppet::Pops::Binder::Hiera2::Issues::CONFIG_FILE_SYNTAX_ERROR)
- end
- end
-
- it 'should report when config has no hierarchy defined' do
- test_config_issue('no_hierarchy', Puppet::Pops::Binder::Hiera2::Issues::MISSING_HIERARCHY)
- end
-
- it 'should report when config has no backends defined' do
- test_config_issue('no_backends', Puppet::Pops::Binder::Hiera2::Issues::MISSING_BACKENDS)
- end
-
- it 'should report when config hierarchy is malformed' do
- test_config_issue('malformed_hierarchy', Puppet::Pops::Binder::Hiera2::Issues::CATEGORY_MUST_BE_THREE_ELEMENT_ARRAY)
- end
-end
diff --git a/spec/unit/pops/binder/hiera2/yaml_backend_spec.rb b/spec/unit/pops/binder/hiera2/yaml_backend_spec.rb
deleted file mode 100644
index 702819c9a..000000000
--- a/spec/unit/pops/binder/hiera2/yaml_backend_spec.rb
+++ /dev/null
@@ -1,33 +0,0 @@
-require 'spec_helper'
-require 'puppet/pops'
-
-describe "Hiera2 YAML backend" do
-
- include PuppetSpec::Files
-
- def fixture_dir(config_name)
- my_fixture("#{config_name}")
- end
-
- before(:all) do
- Puppet[:binder] = true
- require 'puppetx'
- require 'puppet/pops/binder/hiera2/yaml_backend'
- end
-
- after(:all) do
- Puppet[:binder] = false
- end
-
- it "returns the expected hash from a valid yaml file" do
- Puppet::Pops::Binder::Hiera2::YamlBackend.new().read_data(fixture_dir("ok"), "common").should == {'brillig' => 'slithy'}
- end
-
- it "returns an empty hash from an empty yaml file" do
- Puppet::Pops::Binder::Hiera2::YamlBackend.new().read_data(fixture_dir("empty"), "common").should == {}
- end
-
- it "returns an empty hash from an invalid yaml file" do
- Puppet::Pops::Binder::Hiera2::YamlBackend.new().read_data(fixture_dir("invalid"), "common").should == {}
- end
-end
diff --git a/spec/unit/pops/binder/injector_spec.rb b/spec/unit/pops/binder/injector_spec.rb
index 966561ede..32eba3260 100644
--- a/spec/unit/pops/binder/injector_spec.rb
+++ b/spec/unit/pops/binder/injector_spec.rb
@@ -1,789 +1,784 @@
require 'spec_helper'
require 'puppet/pops'
module InjectorSpecModule
def injector(binder)
Puppet::Pops::Binder::Injector.new(binder)
end
def factory
Puppet::Pops::Binder::BindingsFactory
end
def test_layer_with_empty_bindings
factory.named_layer('test-layer', factory.named_bindings('test').model)
end
def test_layer_with_bindings(*bindings)
factory.named_layer('test-layer', *bindings)
end
def null_scope()
nil
end
def type_calculator
Puppet::Pops::Types::TypeCalculator
end
def type_factory
Puppet::Pops::Types::TypeFactory
end
- # Returns a binder with the effective categories highest/test, node/kermit, environment/dev (and implicit 'common')
+ # Returns a binder
#
- def binder_with_categories
+ def configured_binder
b = Puppet::Pops::Binder::Binder.new()
- b.define_categories(factory.categories(['highest', 'test', 'node', 'kermit', 'environment','dev']))
b
end
class TestDuck
end
class Daffy < TestDuck
end
class AngryDuck < TestDuck
# Supports assisted inject, returning a Donald duck as the default impl of Duck
def self.inject(injector, scope, binding, *args)
Donald.new()
end
end
class Donald < AngryDuck
end
class ArneAnka < AngryDuck
attr_reader :label
def initialize()
@label = 'A Swedish angry cartoon duck'
end
end
class ScroogeMcDuck < TestDuck
attr_reader :fortune
# Supports assisted inject, returning an ScroogeMcDuck with 1$ fortune or first arg in args
# Note that when injected (via instance producer, or implict assisted inject, the inject method
# always wins.
def self.inject(injector, scope, binding, *args)
self.new(args[0].nil? ? 1 : args[0])
end
def initialize(fortune)
@fortune = fortune
end
end
class NamedDuck < TestDuck
attr_reader :name
def initialize(name)
@name = name
end
end
# Test custom producer that on each produce returns a duck that is twice as rich as its predecessor
class ScroogeProducer < Puppet::Pops::Binder::Producers::Producer
attr_reader :next_capital
def initialize
@next_capital = 100
end
def produce(scope)
ScroogeMcDuck.new(@next_capital *= 2)
end
end
end
describe 'Injector' do
include InjectorSpecModule
let(:bindings) { factory.named_bindings('test') }
let(:scope) { null_scope()}
let(:duck_type) { type_factory.ruby(InjectorSpecModule::TestDuck) }
- let(:binder) { Puppet::Pops::Binder::Binder.new()}
-
- let(:cbinder) do
- b = Puppet::Pops::Binder::Binder.new()
- b.define_categories(factory.categories([]))
- b
- end
+ let(:binder) { Puppet::Pops::Binder::Binder }
let(:lbinder) do
- cbinder.define_layers(layered_bindings)
+ binder.new(layered_bindings)
end
let(:layered_bindings) { factory.layered_bindings(test_layer_with_bindings(bindings.model)) }
- #let(:xinjector) { Puppet::Pops::Binder::Injector.new(lbinder) }
context 'When created' do
- it 'should raise an error when given binder is not configured at all' do
- expect { Puppet::Pops::Binder::Injector.new(binder()) }.to raise_error(/Given Binder is not configured/)
- end
-
- it 'should raise an error if binder has categories, but is not completely configured' do
- expect { Puppet::Pops::Binder::Injector.new(cbinder) }.to raise_error(/Given Binder is not configured/)
- end
-
it 'should not raise an error if binder is configured' do
- lbinder.configured?().should == true # of something is very wrong
expect { injector(lbinder) }.to_not raise_error
end
it 'should create an empty injector given an empty binder' do
- expect { cbinder.define_layers(layered_bindings) }.to_not raise_exception
+ expect { binder.new(layered_bindings) }.to_not raise_exception
end
it "should be possible to reference the TypeCalculator" do
injector(lbinder).type_calculator.is_a?(Puppet::Pops::Types::TypeCalculator).should == true
end
it "should be possible to reference the KeyFactory" do
injector(lbinder).key_factory.is_a?(Puppet::Pops::Binder::KeyFactory).should == true
end
+
+ it "can be created using a model" do
+ bindings.bind.name('a_string').to('42')
+ injector = Puppet::Pops::Binder::Injector.create_from_model(layered_bindings)
+ injector.lookup(scope, 'a_string').should == '42'
+ end
+
+ it 'can be created using a block' do
+ injector = Puppet::Pops::Binder::Injector.create('test') do
+ bind.name('a_string').to('42')
+ end
+ injector.lookup(scope, 'a_string').should == '42'
+ end
+
+ it 'can be created using a hash' do
+ injector = Puppet::Pops::Binder::Injector.create_from_hash('test', 'a_string' => '42')
+ injector.lookup(scope, 'a_string').should == '42'
+ end
+
+ it 'can be created using an overriding injector with block' do
+ injector = Puppet::Pops::Binder::Injector.create('test') do
+ bind.name('a_string').to('42')
+ end
+ injector2 = injector.override('override') do
+ bind.name('a_string').to('43')
+ end
+ injector.lookup(scope, 'a_string').should == '42'
+ injector2.lookup(scope, 'a_string').should == '43'
+ end
+
+ it 'can be created using an overriding injector with hash' do
+ injector = Puppet::Pops::Binder::Injector.create_from_hash('test', 'a_string' => '42')
+ injector2 = injector.override_with_hash('override', 'a_string' => '43')
+ injector.lookup(scope, 'a_string').should == '42'
+ injector2.lookup(scope, 'a_string').should == '43'
+ end
+
+ it "can be created using an overriding injector with a model" do
+ injector = Puppet::Pops::Binder::Injector.create_from_hash('test', 'a_string' => '42')
+ bindings.bind.name('a_string').to('43')
+ injector2 = injector.override_with_model(layered_bindings)
+ injector.lookup(scope, 'a_string').should == '42'
+ injector2.lookup(scope, 'a_string').should == '43'
+ end
end
context "When looking up objects" do
it 'lookup(scope, name) finds bound object of type Data with given name' do
bindings.bind().name('a_string').to('42')
injector(lbinder).lookup(scope, 'a_string').should == '42'
end
context 'a block transforming the result can be given' do
it 'that transform a found value given scope and value' do
bindings.bind().name('a_string').to('42')
injector(lbinder).lookup(scope, 'a_string') {|zcope, val| val + '42' }.should == '4242'
end
it 'that transform a found value given only value' do
bindings.bind().name('a_string').to('42')
injector(lbinder).lookup(scope, 'a_string') {|val| val + '42' }.should == '4242'
end
it 'that produces a default value when entry is missing' do
bindings.bind().name('a_string').to('42')
injector(lbinder).lookup(scope, 'a_non_existing_string') {|val| val ? (raise Error, "Should not happen") : '4242' }.should == '4242'
end
end
context "and class is not bound" do
it "assisted inject kicks in for classes with zero args constructor" do
duck_type = type_factory.ruby(InjectorSpecModule::Daffy)
injector = injector(lbinder)
injector.lookup(scope, duck_type).is_a?(InjectorSpecModule::Daffy).should == true
injector.lookup_producer(scope, duck_type).produce(scope).is_a?(InjectorSpecModule::Daffy).should == true
end
it "assisted inject produces same instance on lookup but not on lookup producer" do
duck_type = type_factory.ruby(InjectorSpecModule::Daffy)
injector = injector(lbinder)
d1 = injector.lookup(scope, duck_type)
d2 = injector.lookup(scope, duck_type)
d1.equal?(d2).should == true
d1 = injector.lookup_producer(scope, duck_type).produce(scope)
d2 = injector.lookup_producer(scope, duck_type).produce(scope)
d1.equal?(d2).should == false
end
it "assisted inject kicks in for classes with a class inject method" do
duck_type = type_factory.ruby(InjectorSpecModule::ScroogeMcDuck)
injector = injector(lbinder)
# Do not pass any arguments, the ScroogeMcDuck :inject method should pick 1 by default
# This tests zero args passed
injector.lookup(scope, duck_type).fortune.should == 1
injector.lookup_producer(scope, duck_type).produce(scope).fortune.should == 1
end
it "assisted inject selects the inject method if it exists over a zero args constructor" do
injector = injector(lbinder)
duck_type = type_factory.ruby(InjectorSpecModule::AngryDuck)
injector.lookup(scope, duck_type).is_a?(InjectorSpecModule::Donald).should == true
injector.lookup_producer(scope, duck_type).produce(scope).is_a?(InjectorSpecModule::Donald).should == true
end
it "assisted inject selects the zero args constructor if injector is from a superclass" do
injector = injector(lbinder)
duck_type = type_factory.ruby(InjectorSpecModule::ArneAnka)
injector.lookup(scope, duck_type).is_a?(InjectorSpecModule::ArneAnka).should == true
injector.lookup_producer(scope, duck_type).produce(scope).is_a?(InjectorSpecModule::ArneAnka).should == true
end
end
- context 'and conditionals are in use' do
- let(:binder) { binder_with_categories()}
- let(:lbinder) { binder.define_layers(layered_bindings) }
-
- it "should be possible to shadow a bound value in a higher precedented category" do
- bindings.bind().name('a_string').to('42')
- bindings.when_in_category('environment', 'dev').bind().name('a_string').to('43')
- bindings.when_in_category('node', 'kermit').bind().name('a_string').to('being green')
- injector(lbinder).lookup(scope,'a_string').should == 'being green'
- end
-
- it "shadowing should not happen when not in a category" do
- bindings.bind().name('a_string').to('42')
- bindings.when_in_category('environment', 'dev').bind().name('a_string').to('43')
- bindings.when_in_category('node', 'piggy').bind().name('a_string').to('being green')
- injector(lbinder).lookup(scope,'a_string').should == '43'
- end
-
- it "multiple predicates makes binding more specific" do
- bindings.bind().name('a_string').to('42')
- bindings.when_in_category('environment', 'dev').bind().name('a_string').to('43')
- bindings.when_in_category('node', 'kermit').bind().name('a_string').to('being green')
- bindings.when_in_categories({'node'=>'kermit', 'environment'=>'dev'}).bind().name('a_string').to('being dev green')
- injector(lbinder).lookup(scope,'a_string').should == 'being dev green'
- end
-
- it "multiple predicates makes binding more specific, but not more specific than higher precedence" do
- bindings.bind().name('a_string').to('42')
- bindings.when_in_category('environment', 'dev').bind().name('a_string').to('43')
- bindings.when_in_category('node', 'kermit').bind().name('a_string').to('being green')
- bindings.when_in_categories({'node'=>'kermit', 'environment'=>'dev'}).bind().name('a_string').to('being dev green')
- bindings.when_in_category('highest', 'test').bind().name('a_string').to('bazinga')
- injector(lbinder).lookup(scope,'a_string').should == 'bazinga'
- end
- end
-
context "and multiple layers are in use" do
- let(:binder) { binder_with_categories()}
-
it "a higher layer shadows anything in a lower layer" do
bindings1 = factory.named_bindings('test1')
- bindings1.when_in_category("highest", "test").bind().name('a_string').to('bad stuff')
+ bindings1.bind().name('a_string').to('bad stuff')
lower_layer = factory.named_layer('lower-layer', bindings1.model)
bindings2 = factory.named_bindings('test2')
bindings2.bind().name('a_string').to('good stuff')
higher_layer = factory.named_layer('higher-layer', bindings2.model)
- binder.define_layers(factory.layered_bindings(higher_layer, lower_layer))
- injector = injector(binder)
+ injector = injector(binder.new(factory.layered_bindings(higher_layer, lower_layer)))
injector.lookup(scope,'a_string').should == 'good stuff'
end
+
+ it "a higher layer may not shadow a lower layer binding that is final" do
+ bindings1 = factory.named_bindings('test1')
+ bindings1.bind().final.name('a_string').to('required stuff')
+ lower_layer = factory.named_layer('lower-layer', bindings1.model)
+
+ bindings2 = factory.named_bindings('test2')
+ bindings2.bind().name('a_string').to('contraband')
+ higher_layer = factory.named_layer('higher-layer', bindings2.model)
+ expect {
+ injector = injector(binder.new(factory.layered_bindings(higher_layer, lower_layer)))
+ }.to raise_error(/Override of final binding not allowed/)
+ end
end
context "and dealing with Data types" do
- let(:binder) { binder_with_categories()}
- let(:lbinder) { binder.define_layers(layered_bindings) }
+ let(:lbinder) { binder.new(layered_bindings) }
it "should treat all data as same type w.r.t. key" do
bindings.bind().name('a_string').to('42')
bindings.bind().name('an_int').to(43)
bindings.bind().name('a_float').to(3.14)
bindings.bind().name('a_boolean').to(true)
bindings.bind().name('an_array').to([1,2,3])
bindings.bind().name('a_hash').to({'a'=>1,'b'=>2,'c'=>3})
injector = injector(lbinder)
injector.lookup(scope,'a_string').should == '42'
injector.lookup(scope,'an_int').should == 43
injector.lookup(scope,'a_float').should == 3.14
injector.lookup(scope,'a_boolean').should == true
injector.lookup(scope,'an_array').should == [1,2,3]
injector.lookup(scope,'a_hash').should == {'a'=>1,'b'=>2,'c'=>3}
end
it "should provide type-safe lookup of given type/name" do
bindings.bind().string().name('a_string').to('42')
bindings.bind().integer().name('an_int').to(43)
bindings.bind().float().name('a_float').to(3.14)
bindings.bind().boolean().name('a_boolean').to(true)
bindings.bind().array_of_data().name('an_array').to([1,2,3])
bindings.bind().hash_of_data().name('a_hash').to({'a'=>1,'b'=>2,'c'=>3})
injector = injector(lbinder)
# Check lookup using implied Data type
injector.lookup(scope,'a_string').should == '42'
injector.lookup(scope,'an_int').should == 43
injector.lookup(scope,'a_float').should == 3.14
injector.lookup(scope,'a_boolean').should == true
injector.lookup(scope,'an_array').should == [1,2,3]
injector.lookup(scope,'a_hash').should == {'a'=>1,'b'=>2,'c'=>3}
# Check lookup using expected type
injector.lookup(scope,type_factory.string(), 'a_string').should == '42'
injector.lookup(scope,type_factory.integer(), 'an_int').should == 43
injector.lookup(scope,type_factory.float(),'a_float').should == 3.14
injector.lookup(scope,type_factory.boolean(),'a_boolean').should == true
injector.lookup(scope,type_factory.array_of_data(),'an_array').should == [1,2,3]
injector.lookup(scope,type_factory.hash_of_data(),'a_hash').should == {'a'=>1,'b'=>2,'c'=>3}
# Check lookup using wrong type
expect { injector.lookup(scope,type_factory.integer(), 'a_string')}.to raise_error(/Type error/)
expect { injector.lookup(scope,type_factory.string(), 'an_int')}.to raise_error(/Type error/)
expect { injector.lookup(scope,type_factory.string(),'a_float')}.to raise_error(/Type error/)
expect { injector.lookup(scope,type_factory.string(),'a_boolean')}.to raise_error(/Type error/)
expect { injector.lookup(scope,type_factory.string(),'an_array')}.to raise_error(/Type error/)
expect { injector.lookup(scope,type_factory.string(),'a_hash')}.to raise_error(/Type error/)
end
end
end
context "When looking up producer" do
it 'the value is produced by calling produce(scope)' do
bindings.bind().name('a_string').to('42')
injector(lbinder).lookup_producer(scope, 'a_string').produce(scope).should == '42'
end
context 'a block transforming the result can be given' do
it 'that transform a found value given scope and producer' do
bindings.bind().name('a_string').to('42')
injector(lbinder).lookup_producer(scope, 'a_string') {|zcope, p| p.produce(zcope) + '42' }.should == '4242'
end
it 'that transform a found value given only producer' do
bindings.bind().name('a_string').to('42')
injector(lbinder).lookup_producer(scope, 'a_string') {|p| p.produce(scope) + '42' }.should == '4242'
end
it 'that can produce a default value when entry is not found' do
bindings.bind().name('a_string').to('42')
injector(lbinder).lookup_producer(scope, 'a_non_existing_string') {|p| p ? (raise Error,"Should not happen") : '4242' }.should == '4242'
end
end
end
context "When dealing with singleton vs. non singleton" do
it "should produce the same instance when producer is a singleton" do
bindings.bind().name('a_string').to('42')
injector = injector(lbinder)
a = injector.lookup(scope, 'a_string')
b = injector.lookup(scope, 'a_string')
a.equal?(b).should == true
end
it "should produce different instances when producer is a non singleton producer" do
bindings.bind().name('a_string').to_series_of('42')
injector = injector(lbinder)
a = injector.lookup(scope, 'a_string')
b = injector.lookup(scope, 'a_string')
a.should == '42'
b.should == '42'
a.equal?(b).should == false
end
end
context "When using the lookup producer" do
it "should lookup again to produce a value" do
bindings.bind().name('a_string').to_lookup_of('another_string')
bindings.bind().name('another_string').to('hello')
injector(lbinder).lookup(scope, 'a_string').should == 'hello'
end
it "should produce nil if looked up key does not exist" do
bindings.bind().name('a_string').to_lookup_of('non_existing')
injector(lbinder).lookup(scope, 'a_string').should == nil
end
it "should report an error if lookup loop is detected" do
bindings.bind().name('a_string').to_lookup_of('a_string')
expect { injector(lbinder).lookup(scope, 'a_string') }.to raise_error(/Lookup loop/)
end
end
context "When using the hash lookup producer" do
it "should lookup a key in looked up hash" do
data_hash = type_factory.hash_of_data()
bindings.bind().name('a_string').to_hash_lookup_of(data_hash, 'a_hash', 'huey')
bindings.bind().name('a_hash').to({'huey' => 'red', 'dewey' => 'blue', 'louie' => 'green'})
injector(lbinder).lookup(scope, 'a_string').should == 'red'
end
it "should produce nil if looked up entry does not exist" do
data_hash = type_factory.hash_of_data()
bindings.bind().name('a_string').to_hash_lookup_of(data_hash, 'non_existing_entry', 'huey')
bindings.bind().name('a_hash').to({'huey' => 'red', 'dewey' => 'blue', 'louie' => 'green'})
injector(lbinder).lookup(scope, 'a_string').should == nil
end
end
context "When using the first found producer" do
it "should lookup until it finds a value, but not further" do
bindings.bind().name('a_string').to_first_found('b_string', 'c_string', 'g_string')
bindings.bind().name('c_string').to('hello')
bindings.bind().name('g_string').to('Oh, mrs. Smith...')
injector(lbinder).lookup(scope, 'a_string').should == 'hello'
end
it "should lookup until it finds a value using mix of type and name, but not further" do
bindings.bind().name('a_string').to_first_found('b_string', [type_factory.string, 'c_string'], 'g_string')
bindings.bind().name('c_string').to('hello')
bindings.bind().name('g_string').to('Oh, mrs. Smith...')
injector(lbinder).lookup(scope, 'a_string').should == 'hello'
end
end
context "When producing instances" do
it "should lookup an instance of a class without arguments" do
bindings.bind().type(duck_type).name('the_duck').to(InjectorSpecModule::Daffy)
injector(lbinder).lookup(scope, duck_type, 'the_duck').is_a?(InjectorSpecModule::Daffy).should == true
end
it "should lookup an instance of a class with arguments" do
bindings.bind().type(duck_type).name('the_duck').to(InjectorSpecModule::ScroogeMcDuck, 1234)
injector = injector(lbinder)
the_duck = injector.lookup(scope, duck_type, 'the_duck')
the_duck.is_a?(InjectorSpecModule::ScroogeMcDuck).should == true
the_duck.fortune.should == 1234
end
it "singleton producer should not be recreated between lookups" do
bindings.bind().type(duck_type).name('the_duck').to_producer(InjectorSpecModule::ScroogeProducer)
injector = injector(lbinder)
the_duck = injector.lookup(scope, duck_type, 'the_duck')
the_duck.is_a?(InjectorSpecModule::ScroogeMcDuck).should == true
the_duck.fortune.should == 200
# singleton, do it again to get next value in series - it is the producer that is a singleton
# not the produced value
the_duck = injector.lookup(scope, duck_type, 'the_duck')
the_duck.is_a?(InjectorSpecModule::ScroogeMcDuck).should == true
the_duck.fortune.should == 400
duck_producer = injector.lookup_producer(scope, duck_type, 'the_duck')
duck_producer.produce(scope).fortune.should == 800
end
it "series of producers should recreate producer on each lookup and lookup_producer" do
bindings.bind().type(duck_type).name('the_duck').to_producer_series(InjectorSpecModule::ScroogeProducer)
injector = injector(lbinder)
duck_producer = injector.lookup_producer(scope, duck_type, 'the_duck')
duck_producer.produce(scope).fortune().should == 200
duck_producer.produce(scope).fortune().should == 400
# series, each lookup gets a new producer (initialized to produce 200)
duck_producer = injector.lookup_producer(scope, duck_type, 'the_duck')
duck_producer.produce(scope).fortune().should == 200
duck_producer.produce(scope).fortune().should == 400
injector.lookup(scope, duck_type, 'the_duck').fortune().should == 200
injector.lookup(scope, duck_type, 'the_duck').fortune().should == 200
end
end
context "When working with multibind" do
context "of hash kind" do
it "a multibind produces contributed items keyed by their bound key-name" do
hash_of_duck = type_factory.hash_of(duck_type)
multibind_id = "ducks"
bindings.multibind(multibind_id).type(hash_of_duck).name('donalds_nephews')
bindings.bind.in_multibind(multibind_id).type(duck_type).name('nephew1').to(InjectorSpecModule::NamedDuck, 'Huey')
bindings.bind.in_multibind(multibind_id).type(duck_type).name('nephew2').to(InjectorSpecModule::NamedDuck, 'Dewey')
bindings.bind.in_multibind(multibind_id).type(duck_type).name('nephew3').to(InjectorSpecModule::NamedDuck, 'Louie')
injector = injector(lbinder)
the_ducks = injector.lookup(scope, hash_of_duck, "donalds_nephews")
the_ducks.size.should == 3
the_ducks['nephew1'].name.should == 'Huey'
the_ducks['nephew2'].name.should == 'Dewey'
the_ducks['nephew3'].name.should == 'Louie'
end
it "is an error to not bind contribution with a name" do
hash_of_duck = type_factory.hash_of(duck_type)
multibind_id = "ducks"
bindings.multibind(multibind_id).type(hash_of_duck).name('donalds_nephews')
# missing name
bindings.bind.in_multibind(multibind_id).type(duck_type).to(InjectorSpecModule::NamedDuck, 'Huey')
bindings.bind.in_multibind(multibind_id).type(duck_type).to(InjectorSpecModule::NamedDuck, 'Dewey')
expect {
the_ducks = injector(lbinder).lookup(scope, hash_of_duck, "donalds_nephews")
}.to raise_error(/must have a name/)
end
it "is an error to bind with duplicate key when using default (priority) conflict resolution" do
hash_of_duck = type_factory.hash_of(duck_type)
multibind_id = "ducks"
bindings.multibind(multibind_id).type(hash_of_duck).name('donalds_nephews')
# missing name
bindings.bind.in_multibind(multibind_id).type(duck_type).name('foo').to(InjectorSpecModule::NamedDuck, 'Huey')
bindings.bind.in_multibind(multibind_id).type(duck_type).name('foo').to(InjectorSpecModule::NamedDuck, 'Dewey')
expect {
the_ducks = injector(lbinder).lookup(scope, hash_of_duck, "donalds_nephews")
}.to raise_error(/Duplicate key/)
end
it "is not an error to bind with duplicate key when using (ignore) conflict resolution" do
hash_of_duck = type_factory.hash_of(duck_type)
multibind_id = "ducks"
bindings.multibind(multibind_id).type(hash_of_duck).name('donalds_nephews').producer_options(:conflict_resolution => :ignore)
bindings.bind.in_multibind(multibind_id).type(duck_type).name('foo').to(InjectorSpecModule::NamedDuck, 'Huey')
bindings.bind.in_multibind(multibind_id).type(duck_type).name('foo').to(InjectorSpecModule::NamedDuck, 'Dewey')
expect {
the_ducks = injector(lbinder).lookup(scope, hash_of_duck, "donalds_nephews")
}.to_not raise_error(/Duplicate key/)
end
it "should produce detailed type error message" do
hash_of_integer = type_factory.hash_of(type_factory.integer())
multibind_id = "ints"
mb = bindings.multibind(multibind_id).type(hash_of_integer).name('donalds_family')
bindings.bind.in_multibind(multibind_id).name('nephew').to('Huey')
expect { ducks = injector(lbinder).lookup(scope, 'donalds_family')
}.to raise_error(%r{expected: Integer, got: String})
end
it "should be possible to combine hash multibind contributions with append on conflict" do
# This case uses a multibind of individual strings, but combines them
# into an array bound to a hash key
# (There are other ways to do this - e.g. have the multibind lookup a multibind
# of array type to which nephews are contributed).
#
hash_of_data = type_factory.hash_of_data()
multibind_id = "ducks"
mb = bindings.multibind(multibind_id).type(hash_of_data).name('donalds_family')
mb.producer_options(:conflict_resolution => :append)
bindings.bind.in_multibind(multibind_id).name('nephews').to('Huey')
bindings.bind.in_multibind(multibind_id).name('nephews').to('Dewey')
bindings.bind.in_multibind(multibind_id).name('nephews').to('Louie')
bindings.bind.in_multibind(multibind_id).name('uncles').to('Scrooge McDuck')
bindings.bind.in_multibind(multibind_id).name('uncles').to('Ludwig Von Drake')
ducks = injector(lbinder).lookup(scope, 'donalds_family')
ducks['nephews'].should == ['Huey', 'Dewey', 'Louie']
ducks['uncles'].should == ['Scrooge McDuck', 'Ludwig Von Drake']
end
it "should be possible to combine hash multibind contributions with append, flat, and uniq, on conflict" do
# This case uses a multibind of individual strings, but combines them
# into an array bound to a hash key
# (There are other ways to do this - e.g. have the multibind lookup a multibind
# of array type to which nephews are contributed).
#
hash_of_data = type_factory.hash_of_data()
multibind_id = "ducks"
mb = bindings.multibind(multibind_id).type(hash_of_data).name('donalds_family')
mb.producer_options(:conflict_resolution => :append, :flatten => true, :uniq => true)
bindings.bind.in_multibind(multibind_id).name('nephews').to('Huey')
bindings.bind.in_multibind(multibind_id).name('nephews').to('Huey')
bindings.bind.in_multibind(multibind_id).name('nephews').to('Dewey')
bindings.bind.in_multibind(multibind_id).name('nephews').to(['Huey', ['Louie'], 'Dewey'])
bindings.bind.in_multibind(multibind_id).name('uncles').to('Scrooge McDuck')
bindings.bind.in_multibind(multibind_id).name('uncles').to('Ludwig Von Drake')
ducks = injector(lbinder).lookup(scope, 'donalds_family')
ducks['nephews'].should == ['Huey', 'Dewey', 'Louie']
ducks['uncles'].should == ['Scrooge McDuck', 'Ludwig Von Drake']
end
it "should fail attempts to append, perform uniq or flatten on type incompatible multibind hash" do
hash_of_integer = type_factory.hash_of(type_factory.integer())
ids = ["ducks1", "ducks2", "ducks3"]
mb = bindings.multibind(ids[0]).type(hash_of_integer).name('broken_family0')
mb.producer_options(:conflict_resolution => :append)
mb = bindings.multibind(ids[1]).type(hash_of_integer).name('broken_family1')
mb.producer_options(:flatten => :true)
mb = bindings.multibind(ids[2]).type(hash_of_integer).name('broken_family2')
mb.producer_options(:uniq => :true)
-
- binder.define_categories(factory.categories([]))
- binder.define_layers(factory.layered_bindings(test_layer_with_bindings(bindings.model)))
- injector = injector(binder)
+ injector = injector(binder.new(factory.layered_bindings(test_layer_with_bindings(bindings.model))))
expect { injector.lookup(scope, 'broken_family0')}.to raise_error(/:conflict_resolution => :append/)
expect { injector.lookup(scope, 'broken_family1')}.to raise_error(/:flatten/)
expect { injector.lookup(scope, 'broken_family2')}.to raise_error(/:uniq/)
end
it "a higher priority contribution is selected when resolution is :priority" do
hash_of_duck = type_factory.hash_of(duck_type)
multibind_id = "ducks"
bindings.multibind(multibind_id).type(hash_of_duck).name('donalds_nephews')
- mb1 = bindings.when_in_category("highest", "test").bind.in_multibind(multibind_id)
+ mb1 = bindings.bind.in_multibind(multibind_id)
+ pending 'priority based on layers not added, and priority on category removed'
mb1.type(duck_type).name('nephew').to(InjectorSpecModule::NamedDuck, 'Huey')
mb2 = bindings.bind.in_multibind(multibind_id)
mb2.type(duck_type).name('nephew').to(InjectorSpecModule::NamedDuck, 'Dewey')
- binder.define_categories(factory.categories(['highest', 'test']))
binder.define_layers(layered_bindings)
injector(binder).lookup(scope, hash_of_duck, "donalds_nephews")['nephew'].name.should == 'Huey'
end
it "a higher priority contribution wins when resolution is :merge" do
+ # THIS TEST MAY DEPEND ON HASH ORDER SINCE PRIORITY BASED ON CATEGORY IS REMOVED
hash_of_data = type_factory.hash_of_data()
multibind_id = "hashed_ducks"
bindings.multibind(multibind_id).type(hash_of_data).name('donalds_nephews').producer_options(:conflict_resolution => :merge)
- mb1 = bindings.when_in_category("highest", "test").bind.in_multibind(multibind_id)
+ mb1 = bindings.bind.in_multibind(multibind_id)
mb1.name('nephew').to({'name' => 'Huey', 'is' => 'winner'})
mb2 = bindings.bind.in_multibind(multibind_id)
mb2.name('nephew').to({'name' => 'Dewey', 'is' => 'looser', 'has' => 'cap'})
- binder.define_categories(factory.categories(['highest', 'test']))
- binder.define_layers(layered_bindings)
-
- the_ducks = injector(binder).lookup(scope, "donalds_nephews");
+ the_ducks = injector(binder.new(layered_bindings)).lookup(scope, "donalds_nephews");
the_ducks['nephew']['name'].should == 'Huey'
the_ducks['nephew']['is'].should == 'winner'
the_ducks['nephew']['has'].should == 'cap'
end
end
context "of array kind" do
it "an array multibind produces contributed items, names are allowed but ignored" do
array_of_duck = type_factory.array_of(duck_type)
multibind_id = "ducks"
bindings.multibind(multibind_id).type(array_of_duck).name('donalds_nephews')
# one with name (ignored, expect no error)
bindings.bind.in_multibind(multibind_id).type(duck_type).name('nephew1').to(InjectorSpecModule::NamedDuck, 'Huey')
# two without name
bindings.bind.in_multibind(multibind_id).type(duck_type).to(InjectorSpecModule::NamedDuck, 'Dewey')
bindings.bind.in_multibind(multibind_id).type(duck_type).to(InjectorSpecModule::NamedDuck, 'Louie')
the_ducks = injector(lbinder).lookup(scope, array_of_duck, "donalds_nephews")
the_ducks.size.should == 3
the_ducks.collect {|d| d.name }.sort.should == ['Dewey', 'Huey', 'Louie']
end
it "should be able to make result contain only unique entries" do
# This case uses a multibind of individual strings, and combines them
# into an array of unique values
#
array_of_data = type_factory.array_of_data()
multibind_id = "ducks"
mb = bindings.multibind(multibind_id).type(array_of_data).name('donalds_family')
# turn off priority on named to not trigger conflict as all additions have the same precedence
# (could have used the default for unnamed and add unnamed entries).
mb.producer_options(:priority_on_named => false, :uniq => true)
bindings.bind.in_multibind(multibind_id).name('nephews').to('Huey')
bindings.bind.in_multibind(multibind_id).name('nephews').to('Dewey')
bindings.bind.in_multibind(multibind_id).name('nephews').to('Dewey') # duplicate
bindings.bind.in_multibind(multibind_id).name('nephews').to('Louie')
bindings.bind.in_multibind(multibind_id).name('nephews').to('Louie') # duplicate
bindings.bind.in_multibind(multibind_id).name('nephews').to('Louie') # duplicate
ducks = injector(lbinder).lookup(scope, 'donalds_family')
ducks.should == ['Huey', 'Dewey', 'Louie']
end
it "should be able to contribute elements and arrays of elements and flatten 1 level" do
# This case uses a multibind of individual strings and arrays, and combines them
# into an array of flattened
#
array_of_string = type_factory.array_of(type_factory.string())
multibind_id = "ducks"
mb = bindings.multibind(multibind_id).type(array_of_string).name('donalds_family')
# flatten one level
mb.producer_options(:flatten => 1)
bindings.bind.in_multibind(multibind_id).to('Huey')
bindings.bind.in_multibind(multibind_id).to('Dewey')
bindings.bind.in_multibind(multibind_id).to('Louie') # duplicate
bindings.bind.in_multibind(multibind_id).to(['Huey', 'Dewey', 'Louie'])
ducks = injector(lbinder).lookup(scope, 'donalds_family')
ducks.should == ['Huey', 'Dewey', 'Louie', 'Huey', 'Dewey', 'Louie']
end
it "should produce detailed type error message" do
array_of_integer = type_factory.array_of(type_factory.integer())
multibind_id = "ints"
mb = bindings.multibind(multibind_id).type(array_of_integer).name('donalds_family')
bindings.bind.in_multibind(multibind_id).to('Huey')
expect { ducks = injector(lbinder).lookup(scope, 'donalds_family')
}.to raise_error(%r{expected: Integer, or Array\[Integer\], got: String})
end
end
context "When using multibind in multibind" do
it "a hash multibind can be contributed to another" do
hash_of_data = type_factory.hash_of_data()
mb1_id = 'data1'
mb2_id = 'data2'
top = bindings.multibind(mb1_id).type(hash_of_data).name("top")
detail = bindings.multibind(mb2_id).type(hash_of_data).name("detail").in_multibind(mb1_id)
bindings.bind.in_multibind(mb1_id).name('a').to(10)
bindings.bind.in_multibind(mb1_id).name('b').to(20)
bindings.bind.in_multibind(mb2_id).name('a').to(30)
bindings.bind.in_multibind(mb2_id).name('b').to(40)
expect( injector(lbinder).lookup(scope, "top") ).to eql({'detail' => {'a' => 30, 'b' => 40}, 'a' => 10, 'b' => 20})
end
end
context "When looking up entries requiring evaluation" do
let(:node) { Puppet::Node.new('localhost') }
let(:compiler) { Puppet::Parser::Compiler.new(node)}
let(:scope) { Puppet::Parser::Scope.new(compiler) }
let(:parser) { Puppet::Pops::Parser::Parser.new() }
it "should be possible to lookup a concatenated string" do
scope['duck'] = 'Donald Fauntleroy Duck'
expr = parser.parse_string('"Hello $duck"').current()
bindings.bind.name('the_duck').to(expr)
injector(lbinder).lookup(scope, 'the_duck').should == 'Hello Donald Fauntleroy Duck'
end
it "should be possible to post process lookup with a puppet lambda" do
model = parser.parse_string('fake() |$value| {$value + 1 }').current
- bindings.bind.name('an_int').to(42).producer_options( :transformer => model.lambda)
+ bindings.bind.name('an_int').to(42).producer_options( :transformer => model.body.lambda)
injector(lbinder).lookup(scope, 'an_int').should == 43
end
it "should be possible to post process lookup with a ruby proc" do
transformer = lambda {|scope, value| value + 1 }
bindings.bind.name('an_int').to(42).producer_options( :transformer => transformer)
injector(lbinder).lookup(scope, 'an_int').should == 43
end
end
end
+
context "When there are problems with configuration" do
- let(:binder) { binder_with_categories()}
- let(:lbinder) { binder.define_layers(layered_bindings) }
+ let(:lbinder) { binder.new(layered_bindings) }
it "reports error for surfacing abstract bindings" do
bindings.bind.abstract.name('an_int')
expect{injector(lbinder).lookup(scope, 'an_int') }.to raise_error(/The abstract binding .* was not overridden/)
end
it "does not report error for abstract binding that is ovrridden" do
bindings.bind.abstract.name('an_int')
- bindings.when_in_category('highest', 'test').bind.override.name('an_int').to(142)
- expect{injector(lbinder).lookup(scope, 'an_int') }.to_not raise_error
+ bindings.bind.override.name('an_int').to(142)
+ expect{ injector(lbinder).lookup(scope, 'an_int') }.to_not raise_error
end
it "reports error for overriding binding that does not override" do
bindings.bind.override.name('an_int').to(42)
expect{injector(lbinder).lookup(scope, 'an_int') }.to raise_error(/Binding with unresolved 'override' detected/)
end
it "reports error for binding without producer" do
bindings.bind.name('an_int')
expect{injector(lbinder).lookup(scope, 'an_int') }.to raise_error(/Binding without producer/)
end
end
end
\ No newline at end of file
diff --git a/spec/unit/pops/evaluator/access_ops_spec.rb b/spec/unit/pops/evaluator/access_ops_spec.rb
new file mode 100644
index 000000000..6a0c8db33
--- /dev/null
+++ b/spec/unit/pops/evaluator/access_ops_spec.rb
@@ -0,0 +1,376 @@
+#! /usr/bin/env ruby
+require 'spec_helper'
+
+require 'puppet/pops'
+require 'puppet/pops/evaluator/evaluator_impl'
+require 'puppet/pops/types/type_factory'
+
+
+# relative to this spec file (./) does not work as this file is loaded by rspec
+require File.join(File.dirname(__FILE__), '/evaluator_rspec_helper')
+
+describe 'Puppet::Pops::Evaluator::EvaluatorImpl/AccessOperator' do
+ include EvaluatorRspecHelper
+
+ def range(from, to)
+ Puppet::Pops::Types::TypeFactory.range(from, to)
+ end
+
+ def float_range(from, to)
+ Puppet::Pops::Types::TypeFactory.float_range(from, to)
+ end
+
+ context 'The evaluator when operating on a String' do
+ it 'can get a single character using a single key index to []' do
+ expect(evaluate(literal('abc')[1])).to eql('b')
+ end
+
+ it 'can get the last character using the key -1 in []' do
+ expect(evaluate(literal('abc')[-1])).to eql('c')
+ end
+
+ it 'can get a substring by giving two keys' do
+ expect(evaluate(literal('abcd')[1,2])).to eql('bc')
+ # flattens keys
+ expect(evaluate(literal('abcd')[[1,2]])).to eql('bc')
+ end
+
+ it 'produces empty string for a substring out of range' do
+ expect(evaluate(literal('abc')[100])).to eql('')
+ end
+
+ it 'raises an error if arity is wrong for []' do
+ expect{evaluate(literal('abc')[])}.to raise_error(/String supports \[\] with one or two arguments\. Got 0/)
+ expect{evaluate(literal('abc')[1,2,3])}.to raise_error(/String supports \[\] with one or two arguments\. Got 3/)
+ end
+ end
+
+ context 'The evaluator when operating on an Array' do
+ it 'is tested with the correct assumptions' do
+ expect(literal([1,2,3])[1].current.is_a?(Puppet::Pops::Model::AccessExpression)).to eql(true)
+ end
+
+ it 'can get an element using a single key index to []' do
+ expect(evaluate(literal([1,2,3])[1])).to eql(2)
+ end
+
+ it 'can get the last element using the key -1 in []' do
+ expect(evaluate(literal([1,2,3])[-1])).to eql(3)
+ end
+
+ it 'can get a slice of elements using two keys' do
+ expect(evaluate(literal([1,2,3,4])[1,2])).to eql([2,3])
+ # flattens keys
+ expect(evaluate(literal([1,2,3,4])[[1,2]])).to eql([2,3])
+ end
+
+ it 'produces nil for a missing entry' do
+ expect(evaluate(literal([1,2,3])[100])).to eql(nil)
+ end
+
+ it 'raises an error if arity is wrong for []' do
+ expect{evaluate(literal([1,2,3,4])[])}.to raise_error(/Array supports \[\] with one or two arguments\. Got 0/)
+ expect{evaluate(literal([1,2,3,4])[1,2,3])}.to raise_error(/Array supports \[\] with one or two arguments\. Got 3/)
+ end
+ end
+
+ context 'The evaluator when operating on a Hash' do
+ it 'can get a single element giving a single key to []' do
+ expect(evaluate(literal({'a'=>1,'b'=>2,'c'=>3})['b'])).to eql(2)
+ end
+
+ it 'can lookup an array' do
+ expect(evaluate(literal({[1]=>10,[2]=>20})[[2]])).to eql(20)
+ end
+
+ it 'produces nil for a missing key' do
+ expect(evaluate(literal({'a'=>1,'b'=>2,'c'=>3})['x'])).to eql(nil)
+ end
+
+ it 'can get multiple elements by giving multiple keys to []' do
+ expect(evaluate(literal({'a'=>1,'b'=>2,'c'=>3, 'd'=>4})['b', 'd'])).to eql([2, 4])
+ end
+
+ it 'compacts the result when using multiple keys' do
+ expect(evaluate(literal({'a'=>1,'b'=>2,'c'=>3, 'd'=>4})['b', 'x'])).to eql([2])
+ end
+
+ it 'produces an empty array if none of multiple given keys were missing' do
+ expect(evaluate(literal({'a'=>1,'b'=>2,'c'=>3, 'd'=>4})['x', 'y'])).to eql([])
+ end
+
+ it 'raises an error if arity is wrong for []' do
+ expect{evaluate(literal({'a'=>1,'b'=>2,'c'=>3})[])}.to raise_error(/Hash supports \[\] with one or more arguments\. Got 0/)
+ end
+ end
+
+ context "When applied to a type it" do
+ let(:types) { Puppet::Pops::Types::TypeFactory }
+
+ # Integer
+ #
+ it 'produces an Integer[from, to]' do
+ expr = fqr('Integer')[1, 3]
+ expect(evaluate(expr)).to eql(range(1,3))
+
+ # arguments are flattened
+ expr = fqr('Integer')[[1, 3]]
+ expect(evaluate(expr)).to eql(range(1,3))
+ end
+
+ it 'produces an Integer[1]' do
+ expr = fqr('Integer')[1]
+ expect(evaluate(expr)).to eql(range(1,1))
+ end
+
+ it 'produces an Integer[from, <from]' do
+ expr = fqr('Integer')[1,0]
+ expect(evaluate(expr)).to eql(range(1,0))
+ end
+
+ it 'produces an error for Integer[] if there are more than 2 keys' do
+ expr = fqr('Integer')[1,2,3]
+ expect { evaluate(expr)}.to raise_error(/with one or two arguments/)
+ end
+
+ # Float
+ #
+ it 'produces a Float[from, to]' do
+ expr = fqr('Float')[1, 3]
+ expect(evaluate(expr)).to eql(float_range(1.0,3.0))
+
+ # arguments are flattened
+ expr = fqr('Float')[[1, 3]]
+ expect(evaluate(expr)).to eql(float_range(1.0,3.0))
+ end
+
+ it 'produces a Float[1.0]' do
+ expr = fqr('Float')[1.0]
+ expect(evaluate(expr)).to eql(float_range(1.0,1.0))
+ end
+
+ it 'produces a Float[1]' do
+ expr = fqr('Float')[1]
+ expect(evaluate(expr)).to eql(float_range(1.0,1.0))
+ end
+
+ it 'produces a Float[from, <from]' do
+ expr = fqr('Float')[1.0,0.0]
+ expect(evaluate(expr)).to eql(float_range(1.0,0.0))
+ end
+
+ it 'produces an error for Float[] if there are more than 2 keys' do
+ expr = fqr('Float')[1,2,3]
+ expect { evaluate(expr)}.to raise_error(/with one or two arguments/)
+ end
+
+ # Hash Type
+ #
+ it 'produces a Hash[Scalar,String] from the expression Hash[String]' do
+ expr = fqr('Hash')[fqr('String')]
+ expect(evaluate(expr)).to be_the_type(types.hash_of(types.string, types.scalar))
+
+ # arguments are flattened
+ expr = fqr('Hash')[[fqr('String')]]
+ expect(evaluate(expr)).to be_the_type(types.hash_of(types.string, types.scalar))
+ end
+
+ it 'produces a Hash[String,String] from the expression Hash[String, String]' do
+ expr = fqr('Hash')[fqr('String'), fqr('String')]
+ expect(evaluate(expr)).to be_the_type(types.hash_of(types.string, types.string))
+ end
+
+ it 'produces a Hash[Scalar,String] from the expression Hash[Integer][String]' do
+ expr = fqr('Hash')[fqr('Integer')][fqr('String')]
+ expect(evaluate(expr)).to be_the_type(types.hash_of(types.string, types.scalar))
+ end
+
+ it "gives an error if parameter is not a type" do
+ expr = fqr('Hash')['String']
+ expect { evaluate(expr)}.to raise_error(/Hash-Type\[\] arguments must be types/)
+ end
+
+ # Array Type
+ #
+ it 'produces an Array[String] from the expression Array[String]' do
+ expr = fqr('Array')[fqr('String')]
+ expect(evaluate(expr)).to be_the_type(types.array_of(types.string))
+
+ # arguments are flattened
+ expr = fqr('Array')[[fqr('String')]]
+ expect(evaluate(expr)).to be_the_type(types.array_of(types.string))
+ end
+
+ it 'produces an Array[String] from the expression Array[Integer][String]' do
+ expr = fqr('Array')[fqr('Integer')][fqr('String')]
+ expect(evaluate(expr)).to be_the_type(types.array_of(types.string))
+ end
+
+ it 'produces a size constrained Array when the last two arguments specify this' do
+ expr = fqr('Array')[fqr('String'), 1]
+ expected_t = types.array_of(String)
+ types.constrain_size(expected_t, 1, :default)
+ expect(evaluate(expr)).to be_the_type(expected_t)
+
+ expr = fqr('Array')[fqr('String'), 1, 2]
+ expected_t = types.array_of(String)
+ types.constrain_size(expected_t, 1, 2)
+ expect(evaluate(expr)).to be_the_type(expected_t)
+ end
+
+ it "Array parameterization gives an error if parameter is not a type" do
+ expr = fqr('Array')['String']
+ expect { evaluate(expr)}.to raise_error(/Array-Type\[\] arguments must be types/)
+ end
+
+ # Tuple Type
+ #
+ it 'produces a Tuple[String] from the expression Tuple[String]' do
+ expr = fqr('Tuple')[fqr('String')]
+ expect(evaluate(expr)).to be_the_type(types.tuple(String))
+
+ # arguments are flattened
+ expr = fqr('Tuple')[[fqr('String')]]
+ expect(evaluate(expr)).to be_the_type(types.tuple(String))
+ end
+
+ it "Tuple parameterization gives an error if parameter is not a type" do
+ expr = fqr('Tuple')['String']
+ expect { evaluate(expr)}.to raise_error(/Tuple-Type, Cannot use String where Abstract-Type is expected/)
+ end
+
+ it 'produces a varargs Tuple when the last two arguments specify size constraint' do
+ expr = fqr('Tuple')[fqr('String'), 1]
+ expected_t = types.tuple(String)
+ types.constrain_size(expected_t, 1, :default)
+ expect(evaluate(expr)).to be_the_type(expected_t)
+
+ expr = fqr('Tuple')[fqr('String'), 1, 2]
+ expected_t = types.tuple(String)
+ types.constrain_size(expected_t, 1, 2)
+ expect(evaluate(expr)).to be_the_type(expected_t)
+ end
+
+ # Pattern Type
+ #
+ it 'creates a PPatternType instance when applied to a Pattern' do
+ regexp_expr = fqr('Pattern')['foo']
+ expect(evaluate(regexp_expr)).to eql(Puppet::Pops::Types::TypeFactory.pattern('foo'))
+ end
+
+ # Regexp Type
+ #
+ it 'creates a Regexp instance when applied to a Pattern' do
+ regexp_expr = fqr('Regexp')['foo']
+ expect(evaluate(regexp_expr)).to eql(Puppet::Pops::Types::TypeFactory.regexp('foo'))
+
+ # arguments are flattened
+ regexp_expr = fqr('Regexp')[['foo']]
+ expect(evaluate(regexp_expr)).to eql(Puppet::Pops::Types::TypeFactory.regexp('foo'))
+ end
+
+ # Class
+ #
+ it 'produces a specific class from Class[classname]' do
+ expr = fqr('Class')[fqn('apache')]
+ expect(evaluate(expr)).to be_the_type(types.host_class('apache'))
+ expr = fqr('Class')[literal('apache')]
+ expect(evaluate(expr)).to be_the_type(types.host_class('apache'))
+
+ # arguments are flattened
+ expr = fqr('Class')[[fqn('apache')]]
+ expect(evaluate(expr)).to be_the_type(types.host_class('apache'))
+ end
+
+ it 'produces same class if no class name is given' do
+ expr = fqr('Class')[fqn('apache')][]
+ expect { evaluate(expr) }.to raise_error(/Evaluation Error: Class\[apache\]\[\] accepts 1 argument\. Got 0/)
+ end
+
+ it 'produces a collection of classes when multiple class names are given' do
+ expr = fqr('Class')[fqn('apache'), literal('nginx')]
+ result = evaluate(expr)
+ expect(result[0]).to be_the_type(types.host_class('apache'))
+ expect(result[1]).to be_the_type(types.host_class('nginx'))
+ end
+
+ it 'raises error if the name is not a valid name' do
+ expr = fqr('Class')['fail-whale']
+ expect { evaluate(expr) }.to raise_error(/Illegal name/)
+ end
+
+ # Resource
+ it 'produces a specific resource type from Resource[type]' do
+ expr = fqr('Resource')[fqr('File')]
+ expect(evaluate(expr)).to be_the_type(types.resource('File'))
+ expr = fqr('Resource')[literal('File')]
+ expect(evaluate(expr)).to be_the_type(types.resource('File'))
+
+ # arguments are flattened
+ expr = fqr('Resource')[[fqr('File')]]
+ expect(evaluate(expr)).to be_the_type(types.resource('File'))
+ end
+
+ it 'produces a specific resource reference type from File[title]' do
+ expr = fqr('File')[literal('/tmp/x')]
+ expect(evaluate(expr)).to be_the_type(types.resource('File', '/tmp/x'))
+ end
+
+ it 'produces a collection of specific resource references when multiple titles are used' do
+ # Using a resource type
+ expr = fqr('File')[literal('x'),literal('y')]
+ result = evaluate(expr)
+ expect(result[0]).to be_the_type(types.resource('File', 'x'))
+ expect(result[1]).to be_the_type(types.resource('File', 'y'))
+
+ # Using generic resource
+ expr = fqr('Resource')[fqr('File'), literal('x'),literal('y')]
+ result = evaluate(expr)
+ expect(result[0]).to be_the_type(types.resource('File', 'x'))
+ expect(result[1]).to be_the_type(types.resource('File', 'y'))
+ end
+
+ it 'gives an error if resource is not found' do
+ expr = fqr('File')[fqn('x')][fqn('y')]
+ expect {evaluate(expr)}.to raise_error(/Resource not found: File\['x'\]/)
+ end
+
+ # Type Type
+ #
+ it 'creates a Type instance when applied to a Type' do
+ type_expr = fqr('Type')[fqr('Integer')]
+ tf = Puppet::Pops::Types::TypeFactory
+ expect(evaluate(type_expr)).to eql(tf.type_type(tf.integer))
+
+ # arguments are flattened
+ type_expr = fqr('Type')[[fqr('Integer')]]
+ expect(evaluate(type_expr)).to eql(tf.type_type(tf.integer))
+ end
+
+ # Ruby Type
+ #
+ it 'creates a Ruby Type instance when applied to a Ruby Type' do
+ type_expr = fqr('Ruby')['String']
+ tf = Puppet::Pops::Types::TypeFactory
+ expect(evaluate(type_expr)).to eql(tf.ruby_type('String'))
+
+ # arguments are flattened
+ type_expr = fqr('Ruby')[['String']]
+ expect(evaluate(type_expr)).to eql(tf.ruby_type('String'))
+ end
+
+ end
+
+ matcher :be_the_type do |type|
+ calc = Puppet::Pops::Types::TypeCalculator.new
+
+ match do |actual|
+ calc.assignable?(actual, type) && calc.assignable?(type, actual)
+ end
+
+ failure_message_for_should do |actual|
+ "expected #{calc.string(type)}, but was #{calc.string(actual)}"
+ end
+ end
+
+end
diff --git a/spec/unit/pops/evaluator/arithmetic_ops_spec.rb b/spec/unit/pops/evaluator/arithmetic_ops_spec.rb
new file mode 100644
index 000000000..40e5d4844
--- /dev/null
+++ b/spec/unit/pops/evaluator/arithmetic_ops_spec.rb
@@ -0,0 +1,77 @@
+#! /usr/bin/env ruby
+require 'spec_helper'
+
+require 'puppet/pops'
+require 'puppet/pops/evaluator/evaluator_impl'
+
+
+# relative to this spec file (./) does not work as this file is loaded by rspec
+require File.join(File.dirname(__FILE__), '/evaluator_rspec_helper')
+
+describe 'Puppet::Pops::Evaluator::EvaluatorImpl' do
+ include EvaluatorRspecHelper
+
+ context "When the evaluator performs arithmetic" do
+ context "on Integers" do
+ it "2 + 2 == 4" do; evaluate(literal(2) + literal(2)).should == 4 ; end
+ it "7 - 3 == 4" do; evaluate(literal(7) - literal(3)).should == 4 ; end
+ it "6 * 3 == 18" do; evaluate(literal(6) * literal(3)).should == 18; end
+ it "6 / 3 == 2" do; evaluate(literal(6) / literal(3)).should == 2 ; end
+ it "6 % 3 == 0" do; evaluate(literal(6) % literal(3)).should == 0 ; end
+ it "10 % 3 == 1" do; evaluate(literal(10) % literal(3)).should == 1; end
+ it "-(6/3) == -2" do; evaluate(minus(literal(6) / literal(3))).should == -2 ; end
+ it "-6/3 == -2" do; evaluate(minus(literal(6)) / literal(3)).should == -2 ; end
+ it "8 >> 1 == 4" do; evaluate(literal(8) >> literal(1)).should == 4 ; end
+ it "8 << 1 == 16" do; evaluate(literal(8) << literal(1)).should == 16; end
+ it "8 >> -1 == 16" do; evaluate(literal(8) >> literal(-1)).should == 16 ; end
+ it "8 << -1 == 4" do; evaluate(literal(8) << literal(-1)).should == 4; end
+ end
+
+ context "on Floats" do
+ it "2.2 + 2.2 == 4.4" do; evaluate(literal(2.2) + literal(2.2)).should == 4.4 ; end
+ it "7.7 - 3.3 == 4.4" do; evaluate(literal(7.7) - literal(3.3)).should == 4.4 ; end
+ it "6.1 * 3.1 == 18.91" do; evaluate(literal(6.1) * literal(3.1)).should == 18.91; end
+ it "6.6 / 3.3 == 2.0" do; evaluate(literal(6.6) / literal(3.3)).should == 2.0 ; end
+ it "-(6.0/3.0) == -2.0" do; evaluate(minus(literal(6.0) / literal(3.0))).should == -2.0; end
+ it "-6.0/3.0 == -2.0" do; evaluate(minus(literal(6.0)) / literal(3.0)).should == -2.0; end
+ it "6.6 % 3.3 == 0.0" do; expect { evaluate(literal(6.6) % literal(3.3))}.to raise_error(Puppet::ParseError); end
+ it "10.0 % 3.0 == 1.0" do; expect { evaluate(literal(10.0) % literal(3.0))}.to raise_error(Puppet::ParseError); end
+ it "3.14 << 2 == error" do; expect { evaluate(literal(3.14) << literal(2))}.to raise_error(Puppet::ParseError); end
+ it "3.14 >> 2 == error" do; expect { evaluate(literal(3.14) >> literal(2))}.to raise_error(Puppet::ParseError); end
+ end
+
+ context "on strings requiring boxing to Numeric" do
+ it "'2' + '2' == 4" do
+ evaluate(literal('2') + literal('2')).should == 4
+ end
+
+ it "'2.2' + '2.2' == 4.4" do
+ evaluate(literal('2.2') + literal('2.2')).should == 4.4
+ end
+
+ it "'0xF7' + '0x8' == 0xFF" do
+ evaluate(literal('0xF7') + literal('0x8')).should == 0xFF
+ end
+
+ it "'0367' + '010' == 0xFF" do
+ evaluate(literal('0367') + literal('010')).should == 0xFF
+ end
+
+ it "'0888' + '010' == error" do
+ expect { evaluate(literal('0888') + literal('010'))}.to raise_error(Puppet::ParseError)
+ end
+
+ it "'0xWTF' + '010' == error" do
+ expect { evaluate(literal('0xWTF') + literal('010'))}.to raise_error(Puppet::ParseError)
+ end
+
+ it "'0x12.3' + '010' == error" do
+ expect { evaluate(literal('0x12.3') + literal('010'))}.to raise_error(Puppet::ParseError)
+ end
+
+ it "'012.3' + '010' == 20.3 (not error, floats can start with 0)" do
+ evaluate(literal('012.3') + literal('010')).should == 20.3
+ end
+ end
+ end
+end
diff --git a/spec/unit/pops/evaluator/basic_expressions_spec.rb b/spec/unit/pops/evaluator/basic_expressions_spec.rb
new file mode 100644
index 000000000..6ee46ac1a
--- /dev/null
+++ b/spec/unit/pops/evaluator/basic_expressions_spec.rb
@@ -0,0 +1,103 @@
+#! /usr/bin/env ruby
+require 'spec_helper'
+
+require 'puppet/pops'
+require 'puppet/pops/evaluator/evaluator_impl'
+
+
+# relative to this spec file (./) does not work as this file is loaded by rspec
+require File.join(File.dirname(__FILE__), '/evaluator_rspec_helper')
+
+describe 'Puppet::Pops::Evaluator::EvaluatorImpl' do
+ include EvaluatorRspecHelper
+
+ context "When the evaluator evaluates literals" do
+ it 'should evaluator numbers to numbers' do
+ evaluate(literal(1)).should == 1
+ evaluate(literal(3.14)).should == 3.14
+ end
+
+ it 'should evaluate strings to string' do
+ evaluate(literal('banana')).should == 'banana'
+ end
+
+ it 'should evaluate booleans to booleans' do
+ evaluate(literal(false)).should == false
+ evaluate(literal(true)).should == true
+ end
+
+ it 'should evaluate names to strings' do
+ evaluate(fqn('banana')).should == 'banana'
+ end
+
+ it 'should evaluator types to types' do
+ array_type = Puppet::Pops::Types::PArrayType.new()
+ array_type.element_type = Puppet::Pops::Types::PDataType.new()
+ evaluate(fqr('Array')).should == array_type
+ end
+ end
+
+ context "When the evaluator evaluates Lists" do
+ it "should create an Array when evaluating a LiteralList" do
+ evaluate(literal([1,2,3])).should == [1,2,3]
+ end
+
+ it "[...[...[]]] should create nested arrays without trouble" do
+ evaluate(literal([1,[2.0, 2.1, [2.2]],[3.0, 3.1]])).should == [1,[2.0, 2.1, [2.2]],[3.0, 3.1]]
+ end
+
+ it "[2 + 2] should evaluate expressions in entries" do
+ x = literal([literal(2) + literal(2)]);
+ Puppet::Pops::Model::ModelTreeDumper.new.dump(x).should == "([] (+ 2 2))"
+ evaluate(x)[0].should == 4
+ end
+
+ it "[1,2,3] == [1,2,3] == true" do
+ evaluate(literal([1,2,3]) == literal([1,2,3])).should == true;
+ end
+
+ it "[1,2,3] != [2,3,4] == true" do
+ evaluate(literal([1,2,3]).ne(literal([2,3,4]))).should == true;
+ end
+
+ it "[1, 2, 3][2] == 3" do
+ evaluate(literal([1,2,3])[2]).should == 3
+ end
+ end
+
+ context "When the evaluator evaluates Hashes" do
+ it "should create a Hash when evaluating a LiteralHash" do
+ evaluate(literal({'a'=>1,'b'=>2})).should == {'a'=>1,'b'=>2}
+ end
+
+ it "{...{...{}}} should create nested hashes without trouble" do
+ evaluate(literal({'a'=>1,'b'=>{'x'=>2.1,'y'=>2.2}})).should == {'a'=>1,'b'=>{'x'=>2.1,'y'=>2.2}}
+ end
+
+ it "{'a'=> 2 + 2} should evaluate values in entries" do
+ evaluate(literal({'a'=> literal(2) + literal(2)}))['a'].should == 4
+ end
+
+ it "{'a'=> 1, 'b'=>2} == {'a'=> 1, 'b'=>2} == true" do
+ evaluate(literal({'a'=> 1, 'b'=>2}) == literal({'a'=> 1, 'b'=>2})).should == true;
+ end
+
+ it "{'a'=> 1, 'b'=>2} != {'x'=> 1, 'y'=>3} == true" do
+ evaluate(literal({'a'=> 1, 'b'=>2}).ne(literal({'x'=> 1, 'y'=>3}))).should == true;
+ end
+
+ it "{'a' => 1, 'b' => 2}['b'] == 2" do
+ evaluate(literal({:a => 1, :b => 2})[:b]).should == 2
+ end
+ end
+
+ context 'When the evaluator evaluates a Block' do
+ it 'an empty block evaluates to nil' do
+ evaluate(block()).should == nil
+ end
+
+ it 'a block evaluates to its last expression' do
+ evaluate(block(literal(1), literal(2))).should == 2
+ end
+ end
+end
diff --git a/spec/unit/pops/evaluator/collections_ops_spec.rb b/spec/unit/pops/evaluator/collections_ops_spec.rb
new file mode 100644
index 000000000..1d7d642cf
--- /dev/null
+++ b/spec/unit/pops/evaluator/collections_ops_spec.rb
@@ -0,0 +1,111 @@
+#! /usr/bin/env ruby
+require 'spec_helper'
+
+require 'puppet/pops'
+require 'puppet/pops/evaluator/evaluator_impl'
+require 'puppet/pops/types/type_factory'
+
+
+# relative to this spec file (./) does not work as this file is loaded by rspec
+require File.join(File.dirname(__FILE__), '/evaluator_rspec_helper')
+
+describe 'Puppet::Pops::Evaluator::EvaluatorImpl/Concat/Delete' do
+ include EvaluatorRspecHelper
+
+ context 'The evaluator when operating on an Array' do
+ it 'concatenates another array using +' do
+ expect(evaluate(literal([1,2,3]) + literal([4,5]))).to eql([1,2,3,4,5])
+ end
+
+ it 'concatenates another nested array using +' do
+ expect(evaluate(literal([1,2,3]) + literal([[4,5]]))).to eql([1,2,3,[4,5]])
+ end
+
+ it 'concatenates a hash by converting it to array' do
+ # use match_array here since a hash does not have specified order and this
+ # hash has different order on Ruby 1.8.7, and 1.9.3.
+ expect(evaluate(literal([1,2,3]) + literal({'a' => 1, 'b'=>2}))).to match_array([1,2,3,['a',1],['b',2]])
+ end
+
+ it 'concatenates a non array value with +' do
+ expect(evaluate(literal([1,2,3]) + literal(4))).to eql([1,2,3,4])
+ end
+
+ it 'appends another array using <<' do
+ expect(evaluate(literal([1,2,3]) << literal([4,5]))).to eql([1,2,3,[4,5]])
+ end
+
+ it 'appends a hash without conversion when << operator is used' do
+ expect(evaluate(literal([1,2,3]) << literal({'a' => 1, 'b'=>2}))).to eql([1,2,3,{'a' => 1, 'b'=>2}])
+ end
+
+ it 'appends another non array using <<' do
+ expect(evaluate(literal([1,2,3]) << literal(4))).to eql([1,2,3,4])
+ end
+
+ it 'computes the difference with another array using -' do
+ expect(evaluate(literal([1,2,3,4]) - literal([2,3]))).to eql([1,4])
+ end
+
+ it 'computes the difference with a non array using -' do
+ expect(evaluate(literal([1,2,3,4]) - literal(2))).to eql([1,3,4])
+ end
+
+ it 'does not recurse into nested arrays when computing diff' do
+ expect(evaluate(literal([1,2,3,[2],4]) - literal(2))).to eql([1,3,[2],4])
+ end
+
+ it 'can compute diff with sub arrays' do
+ expect(evaluate(literal([1,2,3,[2,3],4]) - literal([[2,3]]))).to eql([1,2,3,4])
+ end
+
+ it 'computes difference by removing all matching instances' do
+ expect(evaluate(literal([1,2,3,3,2,4,2,3]) - literal([2,3]))).to eql([1,4])
+ end
+
+ it 'computes difference with a hash by converting it to an array' do
+ expect(evaluate(literal([1,2,3,['a',1],['b',2]]) - literal({'a' => 1, 'b'=>2}))).to eql([1,2,3])
+ end
+
+ it 'diffs hashes when given in an array' do
+ expect(evaluate(literal([1,2,3,{'a'=>1,'b'=>2}]) - literal([{'a' => 1, 'b'=>2}]))).to eql([1,2,3])
+ end
+
+ it 'raises and error when LHS of << is a hash' do
+ expect {
+ evaluate(literal({'a' => 1, 'b'=>2}) << literal(1))
+ }.to raise_error(/Operator '<<' is not applicable to a Hash/)
+ end
+ end
+
+ context 'The evaluator when operating on a Hash' do
+ it 'merges with another Hash using +' do
+ expect(evaluate(literal({'a' => 1, 'b'=>2}) + literal({'c' => 3}))).to eql({'a' => 1, 'b'=>2, 'c' => 3})
+ end
+
+ it 'merges RHS on top of LHS ' do
+ expect(evaluate(literal({'a' => 1, 'b'=>2}) + literal({'c' => 3, 'b'=>3}))).to eql({'a' => 1, 'b'=>3, 'c' => 3})
+ end
+
+ it 'merges a flat array of pairs converted to a hash' do
+ expect(evaluate(literal({'a' => 1, 'b'=>2}) + literal(['c', 3, 'b', 3]))).to eql({'a' => 1, 'b'=>3, 'c' => 3})
+ end
+
+ it 'merges an array converted to a hash' do
+ expect(evaluate(literal({'a' => 1, 'b'=>2}) + literal([['c', 3], ['b', 3]]))).to eql({'a' => 1, 'b'=>3, 'c' => 3})
+ end
+
+ it 'computes difference with another hash using the - operator' do
+ expect(evaluate(literal({'a' => 1, 'b'=>2}) - literal({'b' => 3}))).to eql({'a' => 1 })
+ end
+
+ it 'computes difference with an array by treating array as array of keys' do
+ expect(evaluate(literal({'a' => 1, 'b'=>2,'c'=>3}) - literal(['b', 'c']))).to eql({'a' => 1 })
+ end
+
+ it 'computes difference with a non array/hash by treating it as a key' do
+ expect(evaluate(literal({'a' => 1, 'b'=>2,'c'=>3}) - literal('c'))).to eql({'a' => 1, 'b' => 2 })
+ end
+ end
+
+end
diff --git a/spec/unit/pops/evaluator/comparison_ops_spec.rb b/spec/unit/pops/evaluator/comparison_ops_spec.rb
new file mode 100644
index 000000000..29938542e
--- /dev/null
+++ b/spec/unit/pops/evaluator/comparison_ops_spec.rb
@@ -0,0 +1,256 @@
+#! /usr/bin/env ruby
+require 'spec_helper'
+
+require 'puppet/pops'
+require 'puppet/pops/evaluator/evaluator_impl'
+
+
+# relative to this spec file (./) does not work as this file is loaded by rspec
+require File.join(File.dirname(__FILE__), '/evaluator_rspec_helper')
+
+describe 'Puppet::Pops::Evaluator::EvaluatorImpl' do
+ include EvaluatorRspecHelper
+
+ context "When the evaluator performs comparisons" do
+
+ context "of string values" do
+ it "'a' == 'a' == true" do; evaluate(literal('a') == literal('a')).should == true ; end
+ it "'a' == 'b' == false" do; evaluate(literal('a') == literal('b')).should == false ; end
+ it "'a' != 'a' == false" do; evaluate(literal('a').ne(literal('a'))).should == false ; end
+ it "'a' != 'b' == true" do; evaluate(literal('a').ne(literal('b'))).should == true ; end
+
+ it "'a' < 'b' == true" do; evaluate(literal('a') < literal('b')).should == true ; end
+ it "'a' < 'a' == false" do; evaluate(literal('a') < literal('a')).should == false ; end
+ it "'b' < 'a' == false" do; evaluate(literal('b') < literal('a')).should == false ; end
+
+ it "'a' <= 'b' == true" do; evaluate(literal('a') <= literal('b')).should == true ; end
+ it "'a' <= 'a' == true" do; evaluate(literal('a') <= literal('a')).should == true ; end
+ it "'b' <= 'a' == false" do; evaluate(literal('b') <= literal('a')).should == false ; end
+
+ it "'a' > 'b' == false" do; evaluate(literal('a') > literal('b')).should == false ; end
+ it "'a' > 'a' == false" do; evaluate(literal('a') > literal('a')).should == false ; end
+ it "'b' > 'a' == true" do; evaluate(literal('b') > literal('a')).should == true ; end
+
+ it "'a' >= 'b' == false" do; evaluate(literal('a') >= literal('b')).should == false ; end
+ it "'a' >= 'a' == true" do; evaluate(literal('a') >= literal('a')).should == true ; end
+ it "'b' >= 'a' == true" do; evaluate(literal('b') >= literal('a')).should == true ; end
+
+ context "with mixed case" do
+ it "'a' == 'A' == true" do; evaluate(literal('a') == literal('A')).should == true ; end
+ it "'a' != 'A' == false" do; evaluate(literal('a').ne(literal('A'))).should == false ; end
+ it "'a' > 'A' == false" do; evaluate(literal('a') > literal('A')).should == false ; end
+ it "'a' >= 'A' == true" do; evaluate(literal('a') >= literal('A')).should == true ; end
+ it "'A' < 'a' == false" do; evaluate(literal('A') < literal('a')).should == false ; end
+ it "'A' <= 'a' == true" do; evaluate(literal('A') <= literal('a')).should == true ; end
+ end
+ end
+
+ context "of integer values" do
+ it "1 == 1 == true" do; evaluate(literal(1) == literal(1)).should == true ; end
+ it "1 == 2 == false" do; evaluate(literal(1) == literal(2)).should == false ; end
+ it "1 != 1 == false" do; evaluate(literal(1).ne(literal(1))).should == false ; end
+ it "1 != 2 == true" do; evaluate(literal(1).ne(literal(2))).should == true ; end
+
+ it "1 < 2 == true" do; evaluate(literal(1) < literal(2)).should == true ; end
+ it "1 < 1 == false" do; evaluate(literal(1) < literal(1)).should == false ; end
+ it "2 < 1 == false" do; evaluate(literal(2) < literal(1)).should == false ; end
+
+ it "1 <= 2 == true" do; evaluate(literal(1) <= literal(2)).should == true ; end
+ it "1 <= 1 == true" do; evaluate(literal(1) <= literal(1)).should == true ; end
+ it "2 <= 1 == false" do; evaluate(literal(2) <= literal(1)).should == false ; end
+
+ it "1 > 2 == false" do; evaluate(literal(1) > literal(2)).should == false ; end
+ it "1 > 1 == false" do; evaluate(literal(1) > literal(1)).should == false ; end
+ it "2 > 1 == true" do; evaluate(literal(2) > literal(1)).should == true ; end
+
+ it "1 >= 2 == false" do; evaluate(literal(1) >= literal(2)).should == false ; end
+ it "1 >= 1 == true" do; evaluate(literal(1) >= literal(1)).should == true ; end
+ it "2 >= 1 == true" do; evaluate(literal(2) >= literal(1)).should == true ; end
+ end
+
+ context "of mixed value types" do
+ it "1 == 1.0 == true" do; evaluate(literal(1) == literal(1.0)).should == true ; end
+ it "1 < 1.1 == true" do; evaluate(literal(1) < literal(1.1)).should == true ; end
+ it "'1' < 1.1 == true" do; evaluate(literal('1') < literal(1.1)).should == true ; end
+ it "1.0 == 1 == true" do; evaluate(literal(1.0) == literal(1)).should == true ; end
+ it "1.0 < 2 == true" do; evaluate(literal(1.0) < literal(2)).should == true ; end
+ it "'1.0' < 1.1 == true" do; evaluate(literal('1.0') < literal(1.1)).should == true ; end
+
+ it "'1.0' < 'a' == true" do; evaluate(literal('1.0') < literal('a')).should == true ; end
+ it "'1.0' < '' == true" do; evaluate(literal('1.0') < literal('')).should == true ; end
+ it "'1.0' < ' ' == true" do; evaluate(literal('1.0') < literal(' ')).should == true ; end
+ it "'a' > '1.0' == true" do; evaluate(literal('a') > literal('1.0')).should == true ; end
+ end
+
+ context "of regular expressions" do
+ it "/.*/ == /.*/ == true" do; evaluate(literal(/.*/) == literal(/.*/)).should == true ; end
+ it "/.*/ != /a.*/ == true" do; evaluate(literal(/.*/).ne(literal(/a.*/))).should == true ; end
+ end
+
+ context "of booleans" do
+ it "true == true == true" do; evaluate(literal(true) == literal(true)).should == true ; end;
+ it "false == false == true" do; evaluate(literal(false) == literal(false)).should == true ; end;
+ it "true == false != true" do; evaluate(literal(true) == literal(false)).should == false ; end;
+ it "false == '' == false" do; evaluate(literal(false) == literal('')).should == false ; end;
+ end
+
+ context "of collections" do
+ it "[1,2,3] == [1,2,3] == true" do
+ evaluate(literal([1,2,3]) == literal([1,2,3])).should == true
+ evaluate(literal([1,2,3]).ne(literal([1,2,3]))).should == false
+ evaluate(literal([1,2,4]) == literal([1,2,3])).should == false
+ evaluate(literal([1,2,4]).ne(literal([1,2,3]))).should == true
+ end
+
+ it "{'a'=>1, 'b'=>2} == {'a'=>1, 'b'=>2} == true" do
+ evaluate(literal({'a'=>1, 'b'=>2}) == literal({'a'=>1, 'b'=>2})).should == true
+ evaluate(literal({'a'=>1, 'b'=>2}).ne(literal({'a'=>1, 'b'=>2}))).should == false
+ evaluate(literal({'a'=>1, 'b'=>2}) == literal({'x'=>1, 'b'=>2})).should == false
+ evaluate(literal({'a'=>1, 'b'=>2}).ne(literal({'x'=>1, 'b'=>2}))).should == true
+ end
+ end
+
+ context "of non comparable types" do
+ # TODO: Change the exception type
+ it "false < true == error" do; expect { evaluate(literal(true) < literal(false))}.to raise_error(Puppet::ParseError); end
+ it "false <= true == error" do; expect { evaluate(literal(true) <= literal(false))}.to raise_error(Puppet::ParseError); end
+ it "false > true == error" do; expect { evaluate(literal(true) > literal(false))}.to raise_error(Puppet::ParseError); end
+ it "false >= true == error" do; expect { evaluate(literal(true) >= literal(false))}.to raise_error(Puppet::ParseError); end
+
+ it "/a/ < /b/ == error" do; expect { evaluate(literal(/a/) < literal(/b/))}.to raise_error(Puppet::ParseError); end
+ it "/a/ <= /b/ == error" do; expect { evaluate(literal(/a/) <= literal(/b/))}.to raise_error(Puppet::ParseError); end
+ it "/a/ > /b/ == error" do; expect { evaluate(literal(/a/) > literal(/b/))}.to raise_error(Puppet::ParseError); end
+ it "/a/ >= /b/ == error" do; expect { evaluate(literal(/a/) >= literal(/b/))}.to raise_error(Puppet::ParseError); end
+
+ it "[1,2,3] < [1,2,3] == error" do
+ expect{ evaluate(literal([1,2,3]) < literal([1,2,3]))}.to raise_error(Puppet::ParseError)
+ end
+
+ it "[1,2,3] > [1,2,3] == error" do
+ expect{ evaluate(literal([1,2,3]) > literal([1,2,3]))}.to raise_error(Puppet::ParseError)
+ end
+
+ it "[1,2,3] >= [1,2,3] == error" do
+ expect{ evaluate(literal([1,2,3]) >= literal([1,2,3]))}.to raise_error(Puppet::ParseError)
+ end
+
+ it "[1,2,3] <= [1,2,3] == error" do
+ expect{ evaluate(literal([1,2,3]) <= literal([1,2,3]))}.to raise_error(Puppet::ParseError)
+ end
+
+ it "{'a'=>1, 'b'=>2} < {'a'=>1, 'b'=>2} == error" do
+ expect{ evaluate(literal({'a'=>1, 'b'=>2}) < literal({'a'=>1, 'b'=>2}))}.to raise_error(Puppet::ParseError)
+ end
+
+ it "{'a'=>1, 'b'=>2} > {'a'=>1, 'b'=>2} == error" do
+ expect{ evaluate(literal({'a'=>1, 'b'=>2}) > literal({'a'=>1, 'b'=>2}))}.to raise_error(Puppet::ParseError)
+ end
+
+ it "{'a'=>1, 'b'=>2} <= {'a'=>1, 'b'=>2} == error" do
+ expect{ evaluate(literal({'a'=>1, 'b'=>2}) <= literal({'a'=>1, 'b'=>2}))}.to raise_error(Puppet::ParseError)
+ end
+
+ it "{'a'=>1, 'b'=>2} >= {'a'=>1, 'b'=>2} == error" do
+ expect{ evaluate(literal({'a'=>1, 'b'=>2}) >= literal({'a'=>1, 'b'=>2}))}.to raise_error(Puppet::ParseError)
+ end
+ end
+ end
+
+ context "When the evaluator performs Regular Expression matching" do
+ it "'a' =~ /.*/ == true" do; evaluate(literal('a') =~ literal(/.*/)).should == true ; end
+ it "'a' =~ '.*' == true" do; evaluate(literal('a') =~ literal(".*")).should == true ; end
+ it "'a' !~ /b.*/ == true" do; evaluate(literal('a').mne(literal(/b.*/))).should == true ; end
+ it "'a' !~ 'b.*' == true" do; evaluate(literal('a').mne(literal("b.*"))).should == true ; end
+
+ it "'a' =~ Pattern['.*'] == true" do
+ evaluate(literal('a') =~ fqr('Pattern')[literal(".*")]).should == true
+ end
+
+ it "$a = Pattern['.*']; 'a' =~ $a == true" do
+ expr = block(var('a').set(fqr('Pattern')['foo']), literal('foo') =~ var('a'))
+ evaluate(expr).should == true
+ end
+
+ it 'should fail if LHS is not a string' do
+ expect { evaluate(literal(666) =~ literal(/6/))}.to raise_error(Puppet::ParseError)
+ end
+ end
+
+ context "When evaluator evaluates the 'in' operator" do
+ it "should find elements in an array" do
+ evaluate(literal(1).in(literal([1,2,3]))).should == true
+ evaluate(literal(4).in(literal([1,2,3]))).should == false
+ end
+
+ it "should find keys in a hash" do
+ evaluate(literal('a').in(literal({'x'=>1, 'a'=>2, 'y'=> 3}))).should == true
+ evaluate(literal('z').in(literal({'x'=>1, 'a'=>2, 'y'=> 3}))).should == false
+ end
+
+ it "should find substrings in a string" do
+ evaluate(literal('ana').in(literal('bananas'))).should == true
+ evaluate(literal('xxx').in(literal('bananas'))).should == false
+ end
+
+ it "should find substrings in a string (regexp)" do
+ evaluate(literal(/ana/).in(literal('bananas'))).should == true
+ evaluate(literal(/xxx/).in(literal('bananas'))).should == false
+ end
+
+ it "should find substrings in a string (ignoring case)" do
+ evaluate(literal('ANA').in(literal('bananas'))).should == true
+ evaluate(literal('ana').in(literal('BANANAS'))).should == true
+ evaluate(literal('xxx').in(literal('BANANAS'))).should == false
+ end
+
+ it "should find sublists in a list" do
+ evaluate(literal([2,3]).in(literal([1,[2,3],4]))).should == true
+ evaluate(literal([2,4]).in(literal([1,[2,3],4]))).should == false
+ end
+
+ it "should find sublists in a list (case insensitive)" do
+ evaluate(literal(['a','b']).in(literal(['A',['A','B'],'C']))).should == true
+ evaluate(literal(['x','y']).in(literal(['A',['A','B'],'C']))).should == false
+ end
+
+ it "should find keys in a hash" do
+ evaluate(literal('a').in(literal({'a' => 10, 'b' => 20}))).should == true
+ evaluate(literal('x').in(literal({'a' => 10, 'b' => 20}))).should == false
+ end
+
+ it "should find keys in a hash (case insensitive)" do
+ evaluate(literal('A').in(literal({'a' => 10, 'b' => 20}))).should == true
+ evaluate(literal('X').in(literal({'a' => 10, 'b' => 20}))).should == false
+ end
+
+ it "should find keys in a hash (regexp)" do
+ evaluate(literal(/xxx/).in(literal({'abcxxxabc' => 10, 'xyz' => 20}))).should == true
+ evaluate(literal(/yyy/).in(literal({'abcxxxabc' => 10, 'xyz' => 20}))).should == false
+ end
+
+ it "should find numbers as numbers" do
+ evaluate(literal(15).in(literal([1,0xf,2]))).should == true
+ end
+
+ it "should find numbers as strings" do
+ evaluate(literal(15).in(literal([1, '0xf',2]))).should == true
+ evaluate(literal('15').in(literal([1, 0xf,2]))).should == true
+ end
+
+ it "should not find numbers embedded in strings, nor digits in numbers" do
+ evaluate(literal(15).in(literal([1, '115', 2]))).should == false
+ evaluate(literal(1).in(literal([11, 111, 2]))).should == false
+ evaluate(literal('1').in(literal([11, 111, 2]))).should == false
+ end
+
+ it 'should find an entry with compatible type in an Array' do
+ evaluate(fqr('Array')[fqr('Integer')].in(literal(['a', [1,2,3], 'b']))).should == true
+ evaluate(fqr('Array')[fqr('Integer')].in(literal(['a', [1,2,'not integer'], 'b']))).should == false
+ end
+
+ it 'should find an entry with compatible type in a Hash' do
+ evaluate(fqr('Integer').in(literal({1 => 'a', 'a' => 'b'}))).should == true
+ evaluate(fqr('Integer').in(literal({'a' => 'a', 'a' => 'b'}))).should == false
+ end
+ end
+end
diff --git a/spec/unit/pops/evaluator/conditionals_spec.rb b/spec/unit/pops/evaluator/conditionals_spec.rb
new file mode 100644
index 000000000..020734a5d
--- /dev/null
+++ b/spec/unit/pops/evaluator/conditionals_spec.rb
@@ -0,0 +1,190 @@
+#! /usr/bin/env ruby
+require 'spec_helper'
+require 'puppet/pops'
+require 'puppet/pops/evaluator/evaluator_impl'
+
+# relative to this spec file (./) does not work as this file is loaded by rspec
+require File.join(File.dirname(__FILE__), '/evaluator_rspec_helper')
+
+# This file contains testing of Conditionals, if, case, unless, selector
+#
+describe 'Puppet::Pops::Evaluator::EvaluatorImpl' do
+ include EvaluatorRspecHelper
+
+ context "When the evaluator evaluates" do
+ context "an if expression" do
+ it 'should output the expected result when dumped' do
+ dump(IF(literal(true), literal(2), literal(5))).should == unindent(<<-TEXT
+ (if true
+ (then 2)
+ (else 5))
+ TEXT
+ )
+ end
+
+ it 'if true {5} == 5' do
+ evaluate(IF(literal(true), literal(5))).should == 5
+ end
+
+ it 'if false {5} == nil' do
+ evaluate(IF(literal(false), literal(5))).should == nil
+ end
+
+ it 'if false {2} else {5} == 5' do
+ evaluate(IF(literal(false), literal(2), literal(5))).should == 5
+ end
+
+ it 'if false {2} elsif true {5} == 5' do
+ evaluate(IF(literal(false), literal(2), IF(literal(true), literal(5)))).should == 5
+ end
+
+ it 'if false {2} elsif false {5} == nil' do
+ evaluate(IF(literal(false), literal(2), IF(literal(false), literal(5)))).should == nil
+ end
+ end
+
+ context "an unless expression" do
+ it 'should output the expected result when dumped' do
+ dump(UNLESS(literal(true), literal(2), literal(5))).should == unindent(<<-TEXT
+ (unless true
+ (then 2)
+ (else 5))
+ TEXT
+ )
+ end
+
+ it 'unless false {5} == 5' do
+ evaluate(UNLESS(literal(false), literal(5))).should == 5
+ end
+
+ it 'unless true {5} == nil' do
+ evaluate(UNLESS(literal(true), literal(5))).should == nil
+ end
+
+ it 'unless true {2} else {5} == 5' do
+ evaluate(UNLESS(literal(true), literal(2), literal(5))).should == 5
+ end
+
+ it 'unless true {2} elsif true {5} == 5' do
+ # not supported by concrete syntax
+ evaluate(UNLESS(literal(true), literal(2), IF(literal(true), literal(5)))).should == 5
+ end
+
+ it 'unless true {2} elsif false {5} == nil' do
+ # not supported by concrete syntax
+ evaluate(UNLESS(literal(true), literal(2), IF(literal(false), literal(5)))).should == nil
+ end
+ end
+
+ context "a case expression" do
+ it 'should output the expected result when dumped' do
+ dump(CASE(literal(2),
+ WHEN(literal(1), literal('wat')),
+ WHEN([literal(2), literal(3)], literal('w00t'))
+ )).should == unindent(<<-TEXT
+ (case 2
+ (when (1) (then 'wat'))
+ (when (2 3) (then 'w00t')))
+ TEXT
+ )
+ dump(CASE(literal(2),
+ WHEN(literal(1), literal('wat')),
+ WHEN([literal(2), literal(3)], literal('w00t'))
+ ).default(literal(4))).should == unindent(<<-TEXT
+ (case 2
+ (when (1) (then 'wat'))
+ (when (2 3) (then 'w00t'))
+ (when (:default) (then 4)))
+ TEXT
+ )
+ end
+
+ it "case 1 { 1 : { 'w00t'} } == 'w00t'" do
+ evaluate(CASE(literal(1), WHEN(literal(1), literal('w00t')))).should == 'w00t'
+ end
+
+ it "case 2 { 1,2,3 : { 'w00t'} } == 'w00t'" do
+ evaluate(CASE(literal(2), WHEN([literal(1), literal(2), literal(3)], literal('w00t')))).should == 'w00t'
+ end
+
+ it "case 2 { 1,3 : {'wat'} 2: { 'w00t'} } == 'w00t'" do
+ evaluate(CASE(literal(2),
+ WHEN([literal(1), literal(3)], literal('wat')),
+ WHEN(literal(2), literal('w00t')))).should == 'w00t'
+ end
+
+ it "case 2 { 1,3 : {'wat'} 5: { 'wat'} default: {'w00t'}} == 'w00t'" do
+ evaluate(CASE(literal(2),
+ WHEN([literal(1), literal(3)], literal('wat')),
+ WHEN(literal(5), literal('wat'))).default(literal('w00t'))
+ ).should == 'w00t'
+ end
+
+ it "case 2 { 1,3 : {'wat'} 5: { 'wat'} } == nil" do
+ evaluate(CASE(literal(2),
+ WHEN([literal(1), literal(3)], literal('wat')),
+ WHEN(literal(5), literal('wat')))
+ ).should == nil
+ end
+
+ it "case 'banana' { 1,3 : {'wat'} /.*ana.*/: { 'w00t'} } == w00t" do
+ evaluate(CASE(literal('banana'),
+ WHEN([literal(1), literal(3)], literal('wat')),
+ WHEN(literal(/.*ana.*/), literal('w00t')))
+ ).should == 'w00t'
+ end
+
+ context "with regular expressions" do
+ it "should set numeric variables from the match" do
+ evaluate(CASE(literal('banana'),
+ WHEN([literal(1), literal(3)], literal('wat')),
+ WHEN(literal(/.*(ana).*/), var(1)))
+ ).should == 'ana'
+ end
+ end
+ end
+
+ context "select expressions" do
+ it 'should output the expected result when dumped' do
+ dump(literal(2).select(
+ MAP(literal(1), literal('wat')),
+ MAP(literal(2), literal('w00t'))
+ )).should == "(? 2 (1 => 'wat') (2 => 'w00t'))"
+ end
+
+ it "1 ? {1 => 'w00t'} == 'w00t'" do
+ evaluate(literal(1).select(MAP(literal(1), literal('w00t')))).should == 'w00t'
+ end
+
+ it "2 ? {1 => 'wat', 2 => 'w00t'} == 'w00t'" do
+ evaluate(literal(2).select(
+ MAP(literal(1), literal('wat')),
+ MAP(literal(2), literal('w00t'))
+ )).should == 'w00t'
+ end
+
+ it "3 ? {1 => 'wat', 2 => 'wat', default => 'w00t'} == 'w00t'" do
+ evaluate(literal(3).select(
+ MAP(literal(1), literal('wat')),
+ MAP(literal(2), literal('wat')),
+ MAP(literal(:default), literal('w00t'))
+ )).should == 'w00t'
+ end
+
+ it "3 ? {1 => 'wat', default => 'w00t', 3 => 'wat'} == 'w00t'" do
+ evaluate(literal(3).select(
+ MAP(literal(1), literal('wat')),
+ MAP(literal(:default), literal('w00t')),
+ MAP(literal(2), literal('wat'))
+ )).should == 'w00t'
+ end
+
+ it "should set numerical variables from match" do
+ evaluate(literal('banana').select(
+ MAP(literal(1), literal('wat')),
+ MAP(literal(/.*(ana).*/), var(1))
+ )).should == 'ana'
+ end
+ end
+ end
+end
diff --git a/spec/unit/pops/evaluator/evaluating_parser_spec.rb b/spec/unit/pops/evaluator/evaluating_parser_spec.rb
new file mode 100644
index 000000000..4667de4bb
--- /dev/null
+++ b/spec/unit/pops/evaluator/evaluating_parser_spec.rb
@@ -0,0 +1,1045 @@
+require 'spec_helper'
+
+require 'puppet/pops'
+require 'puppet/pops/evaluator/evaluator_impl'
+require 'puppet_spec/pops'
+require 'puppet_spec/scope'
+require 'puppet/parser/e4_parser_adapter'
+
+
+# relative to this spec file (./) does not work as this file is loaded by rspec
+#require File.join(File.dirname(__FILE__), '/evaluator_rspec_helper')
+
+describe 'Puppet::Pops::Evaluator::EvaluatorImpl' do
+ include PuppetSpec::Pops
+ include PuppetSpec::Scope
+ before(:each) do
+ Puppet[:strict_variables] = true
+
+ # These must be set since the is 3x logic that triggers on these even if the tests are explicit
+ # about selection of parser and evaluator
+ #
+ Puppet[:parser] = 'future'
+ Puppet[:evaluator] = 'future'
+ # Puppetx cannot be loaded until the correct parser has been set (injector is turned off otherwise)
+ require 'puppetx'
+ end
+
+ let(:parser) { Puppet::Pops::Parser::EvaluatingParser::Transitional.new }
+ let(:node) { 'node.example.com' }
+ let(:scope) { s = create_test_scope_for_node(node); s }
+ types = Puppet::Pops::Types::TypeFactory
+
+ context "When evaluator evaluates literals" do
+ {
+ "1" => 1,
+ "010" => 8,
+ "0x10" => 16,
+ "3.14" => 3.14,
+ "0.314e1" => 3.14,
+ "31.4e-1" => 3.14,
+ "'1'" => '1',
+ "'banana'" => 'banana',
+ '"banana"' => 'banana',
+ "banana" => 'banana',
+ "banana::split" => 'banana::split',
+ "false" => false,
+ "true" => true,
+ "Array" => types.array_of_data(),
+ "/.*/" => /.*/
+ }.each do |source, result|
+ it "should parse and evaluate the expression '#{source}' to #{result}" do
+ parser.evaluate_string(scope, source, __FILE__).should == result
+ end
+ end
+ end
+
+ context "When the evaluator evaluates Lists and Hashes" do
+ {
+ "[]" => [],
+ "[1,2,3]" => [1,2,3],
+ "[1,[2.0, 2.1, [2.2]],[3.0, 3.1]]" => [1,[2.0, 2.1, [2.2]],[3.0, 3.1]],
+ "[2 + 2]" => [4],
+ "[1,2,3] == [1,2,3]" => true,
+ "[1,2,3] != [2,3,4]" => true,
+ "[1,2,3] == [2,2,3]" => false,
+ "[1,2,3] != [1,2,3]" => false,
+ "[1,2,3][2]" => 3,
+ "[1,2,3] + [4,5]" => [1,2,3,4,5],
+ "[1,2,3] + [[4,5]]" => [1,2,3,[4,5]],
+ "[1,2,3] + 4" => [1,2,3,4],
+ "[1,2,3] << [4,5]" => [1,2,3,[4,5]],
+ "[1,2,3] << {'a' => 1, 'b'=>2}" => [1,2,3,{'a' => 1, 'b'=>2}],
+ "[1,2,3] << 4" => [1,2,3,4],
+ "[1,2,3,4] - [2,3]" => [1,4],
+ "[1,2,3,4] - [2,5]" => [1,3,4],
+ "[1,2,3,4] - 2" => [1,3,4],
+ "[1,2,3,[2],4] - 2" => [1,3,[2],4],
+ "[1,2,3,[2,3],4] - [[2,3]]" => [1,2,3,4],
+ "[1,2,3,3,2,4,2,3] - [2,3]" => [1,4],
+ "[1,2,3,['a',1],['b',2]] - {'a' => 1, 'b'=>2}" => [1,2,3],
+ "[1,2,3,{'a'=>1,'b'=>2}] - [{'a' => 1, 'b'=>2}]" => [1,2,3],
+ }.each do |source, result|
+ it "should parse and evaluate the expression '#{source}' to #{result}" do
+ parser.evaluate_string(scope, source, __FILE__).should == result
+ end
+ end
+
+ {
+ "[1,2,3] + {'a' => 1, 'b'=>2}" => [1,2,3,['a',1],['b',2]],
+ }.each do |source, result|
+ it "should parse and evaluate the expression '#{source}' to #{result}" do
+ # This test must be done with match_array since the order of the hash
+ # is undefined and Ruby 1.8.7 and 1.9.3 produce different results.
+ expect(parser.evaluate_string(scope, source, __FILE__)).to match_array(result)
+ end
+ end
+
+ {
+ "[1,2,3][a]" => :error,
+ }.each do |source, result|
+ it "should parse and raise error for '#{source}'" do
+ expect { parser.evaluate_string(scope, source, __FILE__) }.to raise_error(Puppet::ParseError)
+ end
+ end
+
+ {
+ "{}" => {},
+ "{'a'=>1,'b'=>2}" => {'a'=>1,'b'=>2},
+ "{'a'=>1,'b'=>{'x'=>2.1,'y'=>2.2}}" => {'a'=>1,'b'=>{'x'=>2.1,'y'=>2.2}},
+ "{'a'=> 2 + 2}" => {'a'=> 4},
+ "{'a'=> 1, 'b'=>2} == {'a'=> 1, 'b'=>2}" => true,
+ "{'a'=> 1, 'b'=>2} != {'x'=> 1, 'b'=>2}" => true,
+ "{'a'=> 1, 'b'=>2} == {'a'=> 2, 'b'=>3}" => false,
+ "{'a'=> 1, 'b'=>2} != {'a'=> 1, 'b'=>2}" => false,
+ "{a => 1, b => 2}[b]" => 2,
+ "{2+2 => sum, b => 2}[4]" => 'sum',
+ "{'a'=>1, 'b'=>2} + {'c'=>3}" => {'a'=>1,'b'=>2,'c'=>3},
+ "{'a'=>1, 'b'=>2} + {'b'=>3}" => {'a'=>1,'b'=>3},
+ "{'a'=>1, 'b'=>2} + ['c', 3, 'b', 3]" => {'a'=>1,'b'=>3, 'c'=>3},
+ "{'a'=>1, 'b'=>2} + [['c', 3], ['b', 3]]" => {'a'=>1,'b'=>3, 'c'=>3},
+ "{'a'=>1, 'b'=>2} - {'b' => 3}" => {'a'=>1},
+ "{'a'=>1, 'b'=>2, 'c'=>3} - ['b', 'c']" => {'a'=>1},
+ "{'a'=>1, 'b'=>2, 'c'=>3} - 'c'" => {'a'=>1, 'b'=>2},
+ }.each do |source, result|
+ it "should parse and evaluate the expression '#{source}' to #{result}" do
+ parser.evaluate_string(scope, source, __FILE__).should == result
+ end
+ end
+
+ {
+ "{'a' => 1, 'b'=>2} << 1" => :error,
+ }.each do |source, result|
+ it "should parse and raise error for '#{source}'" do
+ expect { parser.evaluate_string(scope, source, __FILE__) }.to raise_error(Puppet::ParseError)
+ end
+ end
+ end
+
+ context "When the evaluator perform comparisons" do
+ {
+ "'a' == 'a'" => true,
+ "'a' == 'b'" => false,
+ "'a' != 'a'" => false,
+ "'a' != 'b'" => true,
+ "'a' < 'b' " => true,
+ "'a' < 'a' " => false,
+ "'b' < 'a' " => false,
+ "'a' <= 'b'" => true,
+ "'a' <= 'a'" => true,
+ "'b' <= 'a'" => false,
+ "'a' > 'b' " => false,
+ "'a' > 'a' " => false,
+ "'b' > 'a' " => true,
+ "'a' >= 'b'" => false,
+ "'a' >= 'a'" => true,
+ "'b' >= 'a'" => true,
+ "'a' == 'A'" => true,
+ "'a' != 'A'" => false,
+ "'a' > 'A'" => false,
+ "'a' >= 'A'" => true,
+ "'A' < 'a'" => false,
+ "'A' <= 'a'" => true,
+ "1 == 1" => true,
+ "1 == 2" => false,
+ "1 != 1" => false,
+ "1 != 2" => true,
+ "1 < 2 " => true,
+ "1 < 1 " => false,
+ "2 < 1 " => false,
+ "1 <= 2" => true,
+ "1 <= 1" => true,
+ "2 <= 1" => false,
+ "1 > 2 " => false,
+ "1 > 1 " => false,
+ "2 > 1 " => true,
+ "1 >= 2" => false,
+ "1 >= 1" => true,
+ "2 >= 1" => true,
+ "1 == 1.0 " => true,
+ "1 < 1.1 " => true,
+ "'1' < 1.1" => true,
+ "1.0 == 1 " => true,
+ "1.0 < 2 " => true,
+ "'1.0' < 1.1" => true,
+ "'1.0' < 'a'" => true,
+ "'1.0' < '' " => true,
+ "'1.0' < ' '" => true,
+ "'a' > '1.0'" => true,
+ "/.*/ == /.*/ " => true,
+ "/.*/ != /a.*/" => true,
+ "true == true " => true,
+ "false == false" => true,
+ "true == false" => false,
+ }.each do |source, result|
+ it "should parse and evaluate the expression '#{source}' to #{result}" do
+ parser.evaluate_string(scope, source, __FILE__).should == result
+ end
+ end
+
+ {
+ "'a' =~ /.*/" => true,
+ "'a' =~ '.*'" => true,
+ "/.*/ != /a.*/" => true,
+ "'a' !~ /b.*/" => true,
+ "'a' !~ 'b.*'" => true,
+ '$x = a; a =~ "$x.*"' => true,
+ "a =~ Pattern['a.*']" => true,
+ "a =~ Regexp['a.*']" => true,
+ "$x = /a.*/ a =~ $x" => true,
+ "$x = Pattern['a.*'] a =~ $x" => true,
+ "1 =~ Integer" => true,
+ "1 !~ Integer" => false,
+ "[1,2,3] =~ Array[Integer[1,10]]" => true,
+ }.each do |source, result|
+ it "should parse and evaluate the expression '#{source}' to #{result}" do
+ parser.evaluate_string(scope, source, __FILE__).should == result
+ end
+ end
+
+ {
+ "666 =~ /6/" => :error,
+ "[a] =~ /a/" => :error,
+ "{a=>1} =~ /a/" => :error,
+ "/a/ =~ /a/" => :error,
+ "Array =~ /A/" => :error,
+ }.each do |source, result|
+ it "should parse and raise error for '#{source}'" do
+ expect { parser.evaluate_string(scope, source, __FILE__) }.to raise_error(Puppet::ParseError)
+ end
+ end
+
+ {
+ "1 in [1,2,3]" => true,
+ "4 in [1,2,3]" => false,
+ "a in {x=>1, a=>2}" => true,
+ "z in {x=>1, a=>2}" => false,
+ "ana in bananas" => true,
+ "xxx in bananas" => false,
+ "/ana/ in bananas" => true,
+ "/xxx/ in bananas" => false,
+ "ANA in bananas" => false, # ANA is a type, not a String
+ "'ANA' in bananas" => true,
+ "ana in 'BANANAS'" => true,
+ "/ana/ in 'BANANAS'" => false,
+ "/ANA/ in 'BANANAS'" => true,
+ "xxx in 'BANANAS'" => false,
+ "[2,3] in [1,[2,3],4]" => true,
+ "[2,4] in [1,[2,3],4]" => false,
+ "[a,b] in ['A',['A','B'],'C']" => true,
+ "[x,y] in ['A',['A','B'],'C']" => false,
+ "a in {a=>1}" => true,
+ "x in {a=>1}" => false,
+ "'A' in {a=>1}" => true,
+ "'X' in {a=>1}" => false,
+ "a in {'A'=>1}" => true,
+ "x in {'A'=>1}" => false,
+ "/xxx/ in {'aaaxxxbbb'=>1}" => true,
+ "/yyy/ in {'aaaxxxbbb'=>1}" => false,
+ "15 in [1, 0xf]" => true,
+ "15 in [1, '0xf']" => true,
+ "'15' in [1, 0xf]" => true,
+ "15 in [1, 115]" => false,
+ "1 in [11, '111']" => false,
+ "'1' in [11, '111']" => false,
+ "Array[Integer] in [2, 3]" => false,
+ "Array[Integer] in [2, [3, 4]]" => true,
+ "Array[Integer] in [2, [a, 4]]" => false,
+ "Integer in { 2 =>'a'}" => true,
+ "Integer[5,10] in [1,5,3]" => true,
+ "Integer[5,10] in [1,2,3]" => false,
+ "Integer in {'a'=>'a'}" => false,
+ "Integer in {'a'=>1}" => false,
+ }.each do |source, result|
+ it "should parse and evaluate the expression '#{source}' to #{result}" do
+ parser.evaluate_string(scope, source, __FILE__).should == result
+ end
+ end
+
+ {
+ 'Object' => ['Data', 'Scalar', 'Numeric', 'Integer', 'Float', 'Boolean', 'String', 'Pattern', 'Collection',
+ 'Array', 'Hash', 'CatalogEntry', 'Resource', 'Class', 'Undef', 'File', 'NotYetKnownResourceType'],
+
+ # Note, Data > Collection is false (so not included)
+ 'Data' => ['Scalar', 'Numeric', 'Integer', 'Float', 'Boolean', 'String', 'Pattern', 'Array', 'Hash',],
+ 'Scalar' => ['Numeric', 'Integer', 'Float', 'Boolean', 'String', 'Pattern'],
+ 'Numeric' => ['Integer', 'Float'],
+ 'CatalogEntry' => ['Class', 'Resource', 'File', 'NotYetKnownResourceType'],
+ 'Integer[1,10]' => ['Integer[2,3]'],
+ }.each do |general, specials|
+ specials.each do |special |
+ it "should compute that #{general} > #{special}" do
+ parser.evaluate_string(scope, "#{general} > #{special}", __FILE__).should == true
+ end
+ it "should compute that #{special} < #{general}" do
+ parser.evaluate_string(scope, "#{special} < #{general}", __FILE__).should == true
+ end
+ it "should compute that #{general} != #{special}" do
+ parser.evaluate_string(scope, "#{special} != #{general}", __FILE__).should == true
+ end
+ end
+ end
+
+ {
+ 'Integer[1,10] > Integer[2,3]' => true,
+ 'Integer[1,10] == Integer[2,3]' => false,
+ 'Integer[1,10] > Integer[0,5]' => false,
+ 'Integer[1,10] > Integer[1,10]' => false,
+ 'Integer[1,10] >= Integer[1,10]' => true,
+ 'Integer[1,10] == Integer[1,10]' => true,
+ }.each do |source, result|
+ it "should parse and evaluate the integer range comparison expression '#{source}' to #{result}" do
+ parser.evaluate_string(scope, source, __FILE__).should == result
+ end
+ end
+
+ end
+
+ context "When the evaluator performs arithmetic" do
+ context "on Integers" do
+ { "2+2" => 4,
+ "2 + 2" => 4,
+ "7 - 3" => 4,
+ "6 * 3" => 18,
+ "6 / 3" => 2,
+ "6 % 3" => 0,
+ "10 % 3" => 1,
+ "-(6/3)" => -2,
+ "-6/3 " => -2,
+ "8 >> 1" => 4,
+ "8 << 1" => 16,
+ }.each do |source, result|
+ it "should parse and evaluate the expression '#{source}' to #{result}" do
+ parser.evaluate_string(scope, source, __FILE__).should == result
+ end
+ end
+
+ context "on Floats" do
+ {
+ "2.2 + 2.2" => 4.4,
+ "7.7 - 3.3" => 4.4,
+ "6.1 * 3.1" => 18.91,
+ "6.6 / 3.3" => 2.0,
+ "-(6.0/3.0)" => -2.0,
+ "-6.0/3.0 " => -2.0,
+ }.each do |source, result|
+ it "should parse and evaluate the expression '#{source}' to #{result}" do
+ parser.evaluate_string(scope, source, __FILE__).should == result
+ end
+ end
+
+ {
+ "3.14 << 2" => :error,
+ "3.14 >> 2" => :error,
+ "6.6 % 3.3" => 0.0,
+ "10.0 % 3.0" => 1.0,
+ }.each do |source, result|
+ it "should parse and raise error for '#{source}'" do
+ expect { parser.evaluate_string(scope, source, __FILE__) }.to raise_error(Puppet::ParseError)
+ end
+ end
+ end
+
+ context "on strings requiring boxing to Numeric" do
+ {
+ "'2' + '2'" => 4,
+ "'2.2' + '2.2'" => 4.4,
+ "'0xF7' + '010'" => 0xFF,
+ "'0xF7' + '0x8'" => 0xFF,
+ "'0367' + '010'" => 0xFF,
+ "'012.3' + '010'" => 20.3,
+ }.each do |source, result|
+ it "should parse and evaluate the expression '#{source}' to #{result}" do
+ parser.evaluate_string(scope, source, __FILE__).should == result
+ end
+ end
+
+ {
+ "'0888' + '010'" => :error,
+ "'0xWTF' + '010'" => :error,
+ "'0x12.3' + '010'" => :error,
+ "'0x12.3' + '010'" => :error,
+ }.each do |source, result|
+ it "should parse and raise error for '#{source}'" do
+ expect { parser.evaluate_string(scope, source, __FILE__) }.to raise_error(Puppet::ParseError)
+ end
+ end
+ end
+ end
+ end # arithmetic
+
+ context "When the evaluator evaluates assignment" do
+ {
+ "$a = 5" => 5,
+ "$a = 5; $a" => 5,
+ "$a = 5; $b = 6; $a" => 5,
+ "$a = $b = 5; $a == $b" => true,
+ "$a = [1,2,3]; [x].map |$x| { $a += x; $a }" => [[1,2,3,'x']],
+ "$a = [a,x,c]; [x].map |$x| { $a -= x; $a }" => [['a','c']],
+ }.each do |source, result|
+ it "should parse and evaluate the expression '#{source}' to #{result}" do
+ parser.evaluate_string(scope, source, __FILE__).should == result
+ end
+ end
+
+ {
+ "[a,b,c] = [1,2,3]; $a == 1 and $b == 2 and $c == 3" => :error,
+ "[a,b,c] = {b=>2,c=>3,a=>1}; $a == 1 and $b == 2 and $c == 3" => :error,
+ "$a = [1,2,3]; [x].collect |$x| { [a] += x; $a }" => :error,
+ "$a = [a,x,c]; [x].collect |$x| { [a] -= x; $a }" => :error,
+ }.each do |source, result|
+ it "should parse and evaluate the expression '#{source}' to #{result}" do
+ expect { parser.evaluate_string(scope, source, __FILE__)}.to raise_error(Puppet::ParseError)
+ end
+ end
+ end
+
+ context "When the evaluator evaluates conditionals" do
+ {
+ "if true {5}" => 5,
+ "if false {5}" => nil,
+ "if false {2} else {5}" => 5,
+ "if false {2} elsif true {5}" => 5,
+ "if false {2} elsif false {5}" => nil,
+ "unless false {5}" => 5,
+ "unless true {5}" => nil,
+ "unless true {2} else {5}" => 5,
+ "$a = if true {5} $a" => 5,
+ "$a = if false {5} $a" => nil,
+ "$a = if false {2} else {5} $a" => 5,
+ "$a = if false {2} elsif true {5} $a" => 5,
+ "$a = if false {2} elsif false {5} $a" => nil,
+ "$a = unless false {5} $a" => 5,
+ "$a = unless true {5} $a" => nil,
+ "$a = unless true {2} else {5} $a" => 5,
+ }.each do |source, result|
+ it "should parse and evaluate the expression '#{source}' to #{result}" do
+ parser.evaluate_string(scope, source, __FILE__).should == result
+ end
+ end
+
+ {
+ "case 1 { 1 : { yes } }" => 'yes',
+ "case 2 { 1,2,3 : { yes} }" => 'yes',
+ "case 2 { 1,3 : { no } 2: { yes} }" => 'yes',
+ "case 2 { 1,3 : { no } 5: { no } default: { yes }}" => 'yes',
+ "case 2 { 1,3 : { no } 5: { no } }" => nil,
+ "case 'banana' { 1,3 : { no } /.*ana.*/: { yes } }" => 'yes',
+ "case 'banana' { /.*(ana).*/: { $1 } }" => 'ana',
+ "case [1] { Array : { yes } }" => 'yes',
+ "case [1] {
+ Array[String] : { no }
+ Array[Integer]: { yes }
+ }" => 'yes',
+ "case 1 {
+ Integer : { yes }
+ Type[Integer] : { no } }" => 'yes',
+ "case Integer {
+ Integer : { no }
+ Type[Integer] : { yes } }" => 'yes',
+
+ }.each do |source, result|
+ it "should parse and evaluate the expression '#{source}' to #{result}" do
+ parser.evaluate_string(scope, source, __FILE__).should == result
+ end
+ end
+
+ {
+ "2 ? { 1 => no, 2 => yes}" => 'yes',
+ "3 ? { 1 => no, 2 => no}" => nil,
+ "3 ? { 1 => no, 2 => no, default => yes }" => 'yes',
+ "3 ? { 1 => no, default => yes, 3 => no }" => 'yes',
+ "'banana' ? { /.*(ana).*/ => $1 }" => 'ana',
+ "[2] ? { Array[String] => yes, Array => yes}" => 'yes',
+ }.each do |source, result|
+ it "should parse and evaluate the expression '#{source}' to #{result}" do
+ parser.evaluate_string(scope, source, __FILE__).should == result
+ end
+ end
+ end
+
+ context "When evaluator performs [] operations" do
+ {
+ "[1,2,3][0]" => 1,
+ "[1,2,3][2]" => 3,
+ "[1,2,3][3]" => nil,
+ "[1,2,3][-1]" => 3,
+ "[1,2,3][-2]" => 2,
+ "[1,2,3][-4]" => nil,
+ "[1,2,3,4][0,2]" => [1,2],
+ "[1,2,3,4][1,3]" => [2,3,4],
+ "[1,2,3,4][-2,2]" => [3,4],
+ "[1,2,3,4][-3,2]" => [2,3],
+ "[1,2,3,4][3,5]" => [4],
+ "[1,2,3,4][5,2]" => [],
+ "[1,2,3,4][0,-1]" => [1,2,3,4],
+ "[1,2,3,4][0,-2]" => [1,2,3],
+ "[1,2,3,4][0,-4]" => [1],
+ "[1,2,3,4][0,-5]" => [],
+ "[1,2,3,4][-5,2]" => [1],
+ "[1,2,3,4][-5,-3]" => [1,2],
+ "[1,2,3,4][-6,-3]" => [1,2],
+ "[1,2,3,4][2,-3]" => [],
+ }.each do |source, result|
+ it "should parse and evaluate the expression '#{source}' to #{result}" do
+ parser.evaluate_string(scope, source, __FILE__).should == result
+ end
+ end
+
+ {
+ "{a=>1, b=>2, c=>3}[a]" => 1,
+ "{a=>1, b=>2, c=>3}[c]" => 3,
+ "{a=>1, b=>2, c=>3}[x]" => nil,
+ "{a=>1, b=>2, c=>3}[c,b]" => [3,2],
+ "{a=>1, b=>2, c=>3}[a,b,c]" => [1,2,3],
+ "{a=>{b=>{c=>'it works'}}}[a][b][c]" => 'it works',
+ "$a = {undef => 10} $a[free_lunch]" => nil,
+ "$a = {undef => 10} $a[undef]" => 10,
+ "$a = {undef => 10} $a[$a[free_lunch]]" => 10,
+ "$a = {} $a[free_lunch] == undef" => true,
+ }.each do |source, result|
+ it "should parse and evaluate the expression '#{source}' to #{result}" do
+ parser.evaluate_string(scope, source, __FILE__).should == result
+ end
+ end
+
+ {
+ "'abc'[0]" => 'a',
+ "'abc'[2]" => 'c',
+ "'abc'[-1]" => 'c',
+ "'abc'[-2]" => 'b',
+ "'abc'[-3]" => 'a',
+ "'abc'[-4]" => '',
+ "'abc'[3]" => '',
+ "abc[0]" => 'a',
+ "abc[2]" => 'c',
+ "abc[-1]" => 'c',
+ "abc[-2]" => 'b',
+ "abc[-3]" => 'a',
+ "abc[-4]" => '',
+ "abc[3]" => '',
+ "'abcd'[0,2]" => 'ab',
+ "'abcd'[1,3]" => 'bcd',
+ "'abcd'[-2,2]" => 'cd',
+ "'abcd'[-3,2]" => 'bc',
+ "'abcd'[3,5]" => 'd',
+ "'abcd'[5,2]" => '',
+ "'abcd'[0,-1]" => 'abcd',
+ "'abcd'[0,-2]" => 'abc',
+ "'abcd'[0,-4]" => 'a',
+ "'abcd'[0,-5]" => '',
+ "'abcd'[-5,2]" => 'a',
+ "'abcd'[-5,-3]" => 'ab',
+ "'abcd'[-6,-3]" => 'ab',
+ "'abcd'[2,-3]" => '',
+ }.each do |source, result|
+ it "should parse and evaluate the expression '#{source}' to #{result}" do
+ parser.evaluate_string(scope, source, __FILE__).should == result
+ end
+ end
+
+ # Type operations (full set tested by tests covering type calculator)
+ {
+ "Array[Integer]" => types.array_of(types.integer),
+ "Array[Integer,1]" => types.constrain_size(types.array_of(types.integer),1, :default),
+ "Array[Integer,1,2]" => types.constrain_size(types.array_of(types.integer),1, 2),
+ "Array[Integer,Integer[1,2]]" => types.constrain_size(types.array_of(types.integer),1, 2),
+ "Array[Integer,Integer[1]]" => types.constrain_size(types.array_of(types.integer),1, :default),
+ "Hash[Integer,Integer]" => types.hash_of(types.integer, types.integer),
+ "Hash[Integer,Integer,1]" => types.constrain_size(types.hash_of(types.integer, types.integer),1, :default),
+ "Hash[Integer,Integer,1,2]" => types.constrain_size(types.hash_of(types.integer, types.integer),1, 2),
+ "Hash[Integer,Integer,Integer[1,2]]" => types.constrain_size(types.hash_of(types.integer, types.integer),1, 2),
+ "Hash[Integer,Integer,Integer[1]]" => types.constrain_size(types.hash_of(types.integer, types.integer),1, :default),
+ "Resource[File]" => types.resource('File'),
+ "Resource['File']" => types.resource(types.resource('File')),
+ "File[foo]" => types.resource('file', 'foo'),
+ "File[foo, bar]" => [types.resource('file', 'foo'), types.resource('file', 'bar')],
+ "Pattern[a, /b/, Pattern[c], Regexp[d]]" => types.pattern('a', 'b', 'c', 'd'),
+ "String[1,2]" => types.constrain_size(types.string,1, 2),
+ "String[Integer[1,2]]" => types.constrain_size(types.string,1, 2),
+ "String[Integer[1]]" => types.constrain_size(types.string,1, :default),
+ }.each do |source, result|
+ it "should parse and evaluate the expression '#{source}' to #{result}" do
+ parser.evaluate_string(scope, source, __FILE__).should == result
+ end
+ end
+
+ # LHS where [] not supported, and missing key(s)
+ {
+ "Array[]" => :error,
+ "'abc'[]" => :error,
+ "Resource[]" => :error,
+ "File[]" => :error,
+ "String[]" => :error,
+ "1[]" => :error,
+ "3.14[]" => :error,
+ "/.*/[]" => :error,
+ "$a=[1] $a[]" => :error,
+ }.each do |source, result|
+ it "should parse and evaluate the expression '#{source}' to #{result}" do
+ expect { parser.evaluate_string(scope, source, __FILE__)}.to raise_error(/Syntax error/)
+ end
+ end
+
+ # Errors when wrong number/type of keys are used
+ {
+ "Array[0]" => 'Array-Type[] arguments must be types. Got Fixnum',
+ "Hash[0]" => 'Hash-Type[] arguments must be types. Got Fixnum',
+ "Hash[Integer, 0]" => 'Hash-Type[] arguments must be types. Got Fixnum',
+ "Array[Integer,1,2,3]" => 'Array-Type[] accepts 1 to 3 arguments. Got 4',
+ "Array[Integer,String]" => "A Type's size constraint arguments must be a single Integer type, or 1-2 integers (or default). Got a String-Type",
+ "Hash[Integer,String, 1,2,3]" => 'Hash-Type[] accepts 1 to 4 arguments. Got 5',
+ "'abc'[x]" => "The value 'x' cannot be converted to Numeric",
+ "'abc'[1.0]" => "A String[] cannot use Float where Integer is expected",
+ "'abc'[1,2,3]" => "String supports [] with one or two arguments. Got 3",
+ "Resource[0]" => 'First argument to Resource[] must be a resource type or a String. Got Fixnum',
+ "Resource[a, 0]" => 'Error creating type specialization of a Resource-Type, Cannot use Fixnum where String is expected',
+ "File[0]" => 'Error creating type specialization of a File-Type, Cannot use Fixnum where String is expected',
+ "String[a]" => "A Type's size constraint arguments must be a single Integer type, or 1-2 integers (or default). Got a String",
+ "Pattern[0]" => 'Error creating type specialization of a Pattern-Type, Cannot use Fixnum where String or Regexp or Pattern-Type or Regexp-Type is expected',
+ "Regexp[0]" => 'Error creating type specialization of a Regexp-Type, Cannot use Fixnum where String or Regexp is expected',
+ "Regexp[a,b]" => 'A Regexp-Type[] accepts 1 argument. Got 2',
+ "true[0]" => "Operator '[]' is not applicable to a Boolean",
+ "1[0]" => "Operator '[]' is not applicable to an Integer",
+ "3.14[0]" => "Operator '[]' is not applicable to a Float",
+ "/.*/[0]" => "Operator '[]' is not applicable to a Regexp",
+ "[1][a]" => "The value 'a' cannot be converted to Numeric",
+ "[1][0.0]" => "An Array[] cannot use Float where Integer is expected",
+ "[1]['0.0']" => "An Array[] cannot use Float where Integer is expected",
+ "[1,2][1, 0.0]" => "An Array[] cannot use Float where Integer is expected",
+ "[1,2][1.0, -1]" => "An Array[] cannot use Float where Integer is expected",
+ "[1,2][1, -1.0]" => "An Array[] cannot use Float where Integer is expected",
+ }.each do |source, result|
+ it "should parse and evaluate the expression '#{source}' to #{result}" do
+ expect { parser.evaluate_string(scope, source, __FILE__)}.to raise_error(Regexp.new(Regexp.quote(result)))
+ end
+ end
+
+ context "on catalog types" do
+ it "[n] gets resource parameter [n]" do
+ source = "notify { 'hello': message=>'yo'} Notify[hello][message]"
+ parser.evaluate_string(scope, source, __FILE__).should == 'yo'
+ end
+
+ it "[n] gets class parameter [n]" do
+ source = "class wonka($produces='chocolate'){ }
+ include wonka
+ Class[wonka][produces]"
+
+ # This is more complicated since it needs to run like 3.x and do an import_ast
+ adapted_parser = Puppet::Parser::E4ParserAdapter.new
+ adapted_parser.file = __FILE__
+ ast = adapted_parser.parse(source)
+ scope.known_resource_types.import_ast(ast, '')
+ ast.code.safeevaluate(scope).should == 'chocolate'
+ end
+
+ # Resource default and override expressions and resource parameter access with []
+ {
+ "notify { id: message=>explicit} Notify[id][message]" => "explicit",
+ "Notify { message=>by_default} notify {foo:} Notify[foo][message]" => "by_default",
+ "notify {foo:} Notify[foo]{message =>by_override} Notify[foo][message]" => "by_override",
+ }.each do |source, result|
+ it "should parse and evaluate the expression '#{source}' to #{result}" do
+ parser.evaluate_string(scope, source, __FILE__).should == result
+ end
+ end
+
+ # Resource default and override expressions and resource parameter access error conditions
+ {
+ "notify { xid: message=>explicit} Notify[id][message]" => /Resource not found/,
+ "notify { id: message=>explicit} Notify[id][mustard]" => /does not have a parameter called 'mustard'/,
+ }.each do |source, result|
+ it "should parse '#{source}' and raise error matching #{result}" do
+ expect { parser.evaluate_string(scope, source, __FILE__)}.to raise_error(result)
+ end
+ end
+
+ context 'with errors' do
+ { "Class['fail-whale']" => /Illegal name/,
+ "Class[0]" => /An Integer cannot be used where a String is expected/,
+ "Class[/.*/]" => /A Regexp cannot be used where a String is expected/,
+ "Class[4.1415]" => /A Float cannot be used where a String is expected/,
+ "Class[Integer]" => /An Integer-Type cannot be used where a String is expected/,
+ "Class[File['tmp']]" => /A File\['tmp'\] Resource-Reference cannot be used where a String is expected/,
+ }.each do | source, error_pattern|
+ it "an error is flagged for '#{source}'" do
+ expect { parser.evaluate_string(scope, source, __FILE__)}.to raise_error(error_pattern)
+ end
+ end
+ end
+ end
+ # end [] operations
+ end
+
+ context "When the evaluator performs boolean operations" do
+ {
+ "true and true" => true,
+ "false and true" => false,
+ "true and false" => false,
+ "false and false" => false,
+ "true or true" => true,
+ "false or true" => true,
+ "true or false" => true,
+ "false or false" => false,
+ "! true" => false,
+ "!! true" => true,
+ "!! false" => false,
+ "! 'x'" => false,
+ "! ''" => true,
+ "! undef" => true,
+ "! [a]" => false,
+ "! []" => false,
+ "! {a=>1}" => false,
+ "! {}" => false,
+ "true and false and '0xwtf' + 1" => false,
+ "false or true or '0xwtf' + 1" => true,
+ }.each do |source, result|
+ it "should parse and evaluate the expression '#{source}' to #{result}" do
+ parser.evaluate_string(scope, source, __FILE__).should == result
+ end
+ end
+
+ {
+ "false || false || '0xwtf' + 1" => :error,
+ }.each do |source, result|
+ it "should parse and raise error for '#{source}'" do
+ expect { parser.evaluate_string(scope, source, __FILE__) }.to raise_error(Puppet::ParseError)
+ end
+ end
+ end
+
+ context "When evaluator performs operations on literal undef" do
+ it "computes non existing hash lookup as undef" do
+ parser.evaluate_string(scope, "{a => 1}[b] == undef", __FILE__).should == true
+ parser.evaluate_string(scope, "undef == {a => 1}[b]", __FILE__).should == true
+ end
+ end
+
+ context "When evaluator performs calls" do
+ let(:populate) do
+ parser.evaluate_string(scope, "$a = 10 $b = [1,2,3]")
+ end
+
+ {
+ 'sprintf( "x%iy", $a )' => "x10y",
+ '"x%iy".sprintf( $a )' => "x10y",
+ '$b.reduce |$memo,$x| { $memo + $x }' => 6,
+ 'reduce($b) |$memo,$x| { $memo + $x }' => 6,
+ }.each do |source, result|
+ it "should parse and evaluate the expression '#{source}' to #{result}" do
+ populate
+ parser.evaluate_string(scope, source, __FILE__).should == result
+ end
+ end
+
+ {
+ '"value is ${a*2} yo"' => :error,
+ }.each do |source, result|
+ it "should parse and raise error for '#{source}'" do
+ expect { parser.evaluate_string(scope, source, __FILE__) }.to raise_error(Puppet::ParseError)
+ end
+ end
+ end
+
+ context "When evaluator performs string interpolation" do
+ let(:populate) do
+ parser.evaluate_string(scope, "$a = 10 $b = [1,2,3]")
+ end
+
+ {
+ '"value is $a yo"' => "value is 10 yo",
+ '"value is \$a yo"' => "value is $a yo",
+ '"value is ${a} yo"' => "value is 10 yo",
+ '"value is \${a} yo"' => "value is ${a} yo",
+ '"value is ${$a} yo"' => "value is 10 yo",
+ '"value is ${$a*2} yo"' => "value is 20 yo",
+ '"value is ${sprintf("x%iy",$a)} yo"' => "value is x10y yo",
+ '"value is ${"x%iy".sprintf($a)} yo"' => "value is x10y yo",
+ '"value is ${[1,2,3]} yo"' => "value is [1, 2, 3] yo",
+ '"value is ${/.*/} yo"' => "value is /.*/ yo",
+ '$x = undef "value is $x yo"' => "value is yo",
+ '$x = default "value is $x yo"' => "value is default yo",
+ '$x = Array[Integer] "value is $x yo"' => "value is Array[Integer] yo",
+ '"value is ${Array[Integer]} yo"' => "value is Array[Integer] yo",
+ }.each do |source, result|
+ it "should parse and evaluate the expression '#{source}' to #{result}" do
+ populate
+ parser.evaluate_string(scope, source, __FILE__).should == result
+ end
+ end
+
+ it "should parse and evaluate an interpolation of a hash" do
+ source = '"value is ${{a=>1,b=>2}} yo"'
+ # This test requires testing against two options because a hash to string
+ # produces a result that is unordered
+ hashstr = {'a' => 1, 'b' => 2}.to_s
+ alt_results = ["value is {a => 1, b => 2} yo", "value is {b => 2, a => 1} yo" ]
+ populate
+ parse_result = parser.evaluate_string(scope, source, __FILE__)
+ alt_results.include?(parse_result).should == true
+ end
+
+ {
+ '"value is ${a*2} yo"' => :error,
+ }.each do |source, result|
+ it "should parse and raise error for '#{source}'" do
+ expect { parser.evaluate_string(scope, source, __FILE__) }.to raise_error(Puppet::ParseError)
+ end
+ end
+ end
+
+ context "When evaluating variables" do
+ context "that are non existing an error is raised for" do
+ it "unqualified variable" do
+ expect { parser.evaluate_string(scope, "$quantum_gravity", __FILE__) }.to raise_error(/Unknown variable/)
+ end
+
+ it "qualified variable" do
+ expect { parser.evaluate_string(scope, "$quantum_gravity::graviton", __FILE__) }.to raise_error(/Unknown variable/)
+ end
+ end
+
+ it "a lex error should be raised for '$foo::::bar'" do
+ expect { parser.evaluate_string(scope, "$foo::::bar") }.to raise_error(Puppet::LexError, /Illegal fully qualified name at line 1:7/)
+ end
+
+ { '$a = $0' => nil,
+ '$a = $1' => nil,
+ }.each do |source, value|
+ it "it is ok to reference numeric unassigned variables '#{source}'" do
+ parser.evaluate_string(scope, source, __FILE__).should == value
+ end
+ end
+
+ { '$00 = 0' => /must be a decimal value/,
+ '$0xf = 0' => /must be a decimal value/,
+ '$0777 = 0' => /must be a decimal value/,
+ '$123a = 0' => /must be a decimal value/,
+ }.each do |source, error_pattern|
+ it "should raise an error for '#{source}'" do
+ expect { parser.evaluate_string(scope, source, __FILE__) }.to raise_error(error_pattern)
+ end
+ end
+
+ context "an initial underscore in the last segment of a var name is allowed" do
+ { '$_a = 1' => 1,
+ '$__a = 1' => 1,
+ }.each do |source, value|
+ it "as in this example '#{source}'" do
+ parser.evaluate_string(scope, source, __FILE__).should == value
+ end
+ end
+ end
+ end
+
+ context "When evaluating relationships" do
+ it 'should form a relation with File[a] -> File[b]' do
+ source = "File[a] -> File[b]"
+ parser.evaluate_string(scope, source, __FILE__)
+ scope.compiler.should have_relationship(['File', 'a', '->', 'File', 'b'])
+ end
+
+ it 'should form a relation with resource -> resource' do
+ source = "notify{a:} -> notify{b:}"
+ parser.evaluate_string(scope, source, __FILE__)
+ scope.compiler.should have_relationship(['Notify', 'a', '->', 'Notify', 'b'])
+ end
+
+ it 'should form a relation with [File[a], File[b]] -> [File[x], File[y]]' do
+ source = "[File[a], File[b]] -> [File[x], File[y]]"
+ parser.evaluate_string(scope, source, __FILE__)
+ scope.compiler.should have_relationship(['File', 'a', '->', 'File', 'x'])
+ scope.compiler.should have_relationship(['File', 'b', '->', 'File', 'x'])
+ scope.compiler.should have_relationship(['File', 'a', '->', 'File', 'y'])
+ scope.compiler.should have_relationship(['File', 'b', '->', 'File', 'y'])
+ end
+
+ it 'should tolerate (eliminate) duplicates in operands' do
+ source = "[File[a], File[a]] -> File[x]"
+ parser.evaluate_string(scope, source, __FILE__)
+ scope.compiler.should have_relationship(['File', 'a', '->', 'File', 'x'])
+ scope.compiler.relationships.size.should == 1
+ end
+
+ it 'should form a relation with <-' do
+ source = "File[a] <- File[b]"
+ parser.evaluate_string(scope, source, __FILE__)
+ scope.compiler.should have_relationship(['File', 'b', '->', 'File', 'a'])
+ end
+
+ it 'should form a relation with <-' do
+ source = "File[a] <~ File[b]"
+ parser.evaluate_string(scope, source, __FILE__)
+ scope.compiler.should have_relationship(['File', 'b', '~>', 'File', 'a'])
+ end
+ end
+
+ context "When evaluating heredoc" do
+ it "evaluates plain heredoc" do
+ src = "@(END)\nThis is\nheredoc text\nEND\n"
+ parser.evaluate_string(scope, src).should == "This is\nheredoc text\n"
+ end
+
+ it "parses heredoc with margin" do
+ src = [
+ "@(END)",
+ " This is",
+ " heredoc text",
+ " | END",
+ ""
+ ].join("\n")
+ parser.evaluate_string(scope, src).should == "This is\nheredoc text\n"
+ end
+
+ it "parses heredoc with margin and right newline trim" do
+ src = [
+ "@(END)",
+ " This is",
+ " heredoc text",
+ " |- END",
+ ""
+ ].join("\n")
+ parser.evaluate_string(scope, src).should == "This is\nheredoc text"
+ end
+
+ it "parses escape specification" do
+ src = <<-CODE
+ @(END/t)
+ Tex\\tt\\n
+ |- END
+ CODE
+ parser.evaluate_string(scope, src).should == "Tex\tt\\n"
+ end
+
+ it "parses syntax checked specification" do
+ src = <<-CODE
+ @(END:json)
+ ["foo", "bar"]
+ |- END
+ CODE
+ parser.evaluate_string(scope, src).should == '["foo", "bar"]'
+ end
+
+ it "parses syntax checked specification with error and reports it" do
+ src = <<-CODE
+ @(END:json)
+ ['foo', "bar"]
+ |- END
+ CODE
+ expect { parser.evaluate_string(scope, src)}.to raise_error(/Cannot parse invalid JSON string/)
+ end
+
+ it "parses interpolated heredoc epression" do
+ src = <<-CODE
+ $name = 'Fjodor'
+ @("END")
+ Hello $name
+ |- END
+ CODE
+ parser.evaluate_string(scope, src).should == "Hello Fjodor"
+ end
+
+ end
+
+ context "Detailed Error messages are reported" do
+ it 'for illegal type references' do
+ source = '1+1 { "title": }'
+ # Error references position 5 at the opening '{'
+ # Set file to nil to make it easier to match with line number (no file name in output)
+ expect { parser.parse_string(source, nil) }.to raise_error(/Expression is not valid as a resource.*line 1:5/)
+ end
+
+ it 'for non r-value producing <| |>' do
+ expect { parser.parse_string("$a = File <| |>", nil) }.to raise_error(/A Virtual Query does not produce a value at line 1:6/)
+ end
+
+ it 'for non r-value producing <<| |>>' do
+ expect { parser.parse_string("$a = File <<| |>>", nil) }.to raise_error(/An Exported Query does not produce a value at line 1:6/)
+ end
+
+ it 'for non r-value producing define' do
+ Puppet.expects(:err).with("Invalid use of expression. A 'define' expression does not produce a value at line 1:6")
+ Puppet.expects(:err).with("Classes, definitions, and nodes may only appear at toplevel or inside other classes at line 1:6")
+ expect { parser.parse_string("$a = define foo { }", nil) }.to raise_error(/2 errors/)
+ end
+
+ it 'for non r-value producing class' do
+ Puppet.expects(:err).with("Invalid use of expression. A Host Class Definition does not produce a value at line 1:6")
+ Puppet.expects(:err).with("Classes, definitions, and nodes may only appear at toplevel or inside other classes at line 1:6")
+ expect { parser.parse_string("$a = class foo { }", nil) }.to raise_error(/2 errors/)
+ end
+
+ it 'for unclosed quote with indication of start position of string' do
+ source = <<-SOURCE.gsub(/^ {6}/,'')
+ $a = "xx
+ yyy
+ SOURCE
+ # first char after opening " reported as being in error.
+ expect { parser.parse_string(source) }.to raise_error(/Unclosed quote after '"' followed by 'xx\\nyy\.\.\.' at line 1:7/)
+ end
+
+ it 'for multiple errors with a summary exception' do
+ Puppet.expects(:err).with("Invalid use of expression. A Node Definition does not produce a value at line 1:6")
+ Puppet.expects(:err).with("Classes, definitions, and nodes may only appear at toplevel or inside other classes at line 1:6")
+ expect { parser.parse_string("$a = node x { }",nil) }.to raise_error(/2 errors/)
+ end
+
+ it 'for a bad hostname' do
+ expect {
+ parser.parse_string("node 'macbook+owned+by+name' { }", nil)
+ }.to raise_error(/The hostname 'macbook\+owned\+by\+name' contains illegal characters.*at line 1:6/)
+ end
+
+ it 'for a hostname with interpolation' do
+ source = <<-SOURCE.gsub(/^ {6}/,'')
+ $name = 'fred'
+ node "macbook-owned-by$name" { }
+ SOURCE
+ expect {
+ parser.parse_string(source, nil)
+ }.to raise_error(/An interpolated expression is not allowed in a hostname of a node at line 2:23/)
+ end
+
+ end
+
+ matcher :have_relationship do |expected|
+ calc = Puppet::Pops::Types::TypeCalculator.new
+
+ match do |compiler|
+ op_name = {'->' => :relationship, '~>' => :subscription}
+ compiler.relationships.any? do | relation |
+ relation.source.type == expected[0] &&
+ relation.source.title == expected[1] &&
+ relation.type == op_name[expected[2]] &&
+ relation.target.type == expected[3] &&
+ relation.target.title == expected[4]
+ end
+ end
+
+ failure_message_for_should do |actual|
+ "Relationship #{expected[0]}[#{expected[1]}] #{expected[2]} #{expected[3]}[#{expected[4]}] but was unknown to compiler"
+ end
+ end
+
+end
diff --git a/spec/unit/pops/evaluator/evaluator_rspec_helper.rb b/spec/unit/pops/evaluator/evaluator_rspec_helper.rb
new file mode 100644
index 000000000..eccf8aaca
--- /dev/null
+++ b/spec/unit/pops/evaluator/evaluator_rspec_helper.rb
@@ -0,0 +1,75 @@
+require 'puppet/pops'
+require 'puppet/pops/evaluator/evaluator_impl'
+
+require File.join(File.dirname(__FILE__), '/../factory_rspec_helper')
+
+module EvaluatorRspecHelper
+ include FactoryRspecHelper
+
+ # Evaluate a Factory wrapper round a model object in top scope + named scope
+ # Optionally pass two or three model objects (typically blocks) to be executed
+ # in top scope, named scope, and then top scope again. If a named_scope is used, it must
+ # be preceded by the name of the scope.
+ # The optional block is executed before the result of the last specified model object
+ # is evaluated. This block gets the top scope as an argument. The intent is to pass
+ # a block that asserts the state of the top scope after the operations.
+ #
+ def evaluate in_top_scope, scopename="x", in_named_scope = nil, in_top_scope_again = nil, &block
+ node = Puppet::Node.new('localhost')
+ compiler = Puppet::Parser::Compiler.new(node)
+
+ # Compiler must create the top scope
+# compiler.send(:evaluate_main)
+
+ # compiler creates the top scope if one is not present
+ top_scope = compiler.topscope()
+ # top_scope = Puppet::Parser::Scope.new(compiler)
+
+ evaluator = Puppet::Pops::Evaluator::EvaluatorImpl.new
+ result = evaluator.evaluate(in_top_scope.current, top_scope)
+ if in_named_scope
+ other_scope = Puppet::Parser::Scope.new(compiler)
+ other_scope.add_namespace(scopename)
+ result = evaluator.evaluate(in_named_scope.current, other_scope)
+ end
+ if in_top_scope_again
+ result = evaluator.evaluate(in_top_scope_again.current, top_scope)
+ end
+ if block_given?
+ block.call(top_scope)
+ end
+ result
+ end
+
+ # Evaluate a Factory wrapper round a model object in top scope + local scope
+ # Optionally pass two or three model objects (typically blocks) to be executed
+ # in top scope, local scope, and then top scope again
+ # The optional block is executed before the result of the last specified model object
+ # is evaluated. This block gets the top scope as an argument. The intent is to pass
+ # a block that asserts the state of the top scope after the operations.
+ #
+ def evaluate_l in_top_scope, in_local_scope = nil, in_top_scope_again = nil, &block
+ node = Puppet::Node.new('localhost')
+ compiler = Puppet::Parser::Compiler.new(node)
+
+ # compiler creates the top scope if one is not present
+ top_scope = compiler.topscope()
+
+ evaluator = Puppet::Pops::Evaluator::EvaluatorImpl.new
+ result = evaluator.evaluate(in_top_scope.current, top_scope)
+ if in_local_scope
+ # This is really bad in 3.x scope
+ elevel = top_scope.ephemeral_level
+ top_scope.new_ephemeral(true)
+ result = evaluator.evaluate(in_local_scope.current, top_scope)
+ top_scope.unset_ephemeral_var(elevel)
+ end
+ if in_top_scope_again
+ result = evaluator.evaluate(in_top_scope_again.current, top_scope)
+ end
+ if block_given?
+ block.call(top_scope)
+ end
+ result
+ end
+end
diff --git a/spec/unit/pops/evaluator/logical_ops_spec.rb b/spec/unit/pops/evaluator/logical_ops_spec.rb
new file mode 100644
index 000000000..e5cdd1f93
--- /dev/null
+++ b/spec/unit/pops/evaluator/logical_ops_spec.rb
@@ -0,0 +1,90 @@
+#! /usr/bin/env ruby
+require 'spec_helper'
+
+require 'puppet/pops'
+require 'puppet/pops/evaluator/evaluator_impl'
+
+
+# relative to this spec file (./) does not work as this file is loaded by rspec
+require File.join(File.dirname(__FILE__), '/evaluator_rspec_helper')
+
+describe 'Puppet::Pops::Evaluator::EvaluatorImpl' do
+ include EvaluatorRspecHelper
+
+ context "When the evaluator performs boolean operations" do
+ context "using operator AND" do
+ it "true && true == true" do
+ evaluate(literal(true).and(literal(true))).should == true
+ end
+
+ it "false && true == false" do
+ evaluate(literal(false).and(literal(true))).should == false
+ end
+
+ it "true && false == false" do
+ evaluate(literal(true).and(literal(false))).should == false
+ end
+
+ it "false && false == false" do
+ evaluate(literal(false).and(literal(false))).should == false
+ end
+ end
+
+ context "using operator OR" do
+ it "true || true == true" do
+ evaluate(literal(true).or(literal(true))).should == true
+ end
+
+ it "false || true == true" do
+ evaluate(literal(false).or(literal(true))).should == true
+ end
+
+ it "true || false == true" do
+ evaluate(literal(true).or(literal(false))).should == true
+ end
+
+ it "false || false == false" do
+ evaluate(literal(false).or(literal(false))).should == false
+ end
+ end
+
+ context "using operator NOT" do
+ it "!false == true" do
+ evaluate(literal(false).not()).should == true
+ end
+
+ it "!true == false" do
+ evaluate(literal(true).not()).should == false
+ end
+ end
+
+ context "on values requiring boxing to Boolean" do
+ it "'x' == true" do
+ evaluate(literal('x').not()).should == false
+ end
+
+ it "'' == false" do
+ evaluate(literal('').not()).should == true
+ end
+
+ it ":undef == false" do
+ evaluate(literal(:undef).not()).should == true
+ end
+ end
+
+ context "connectives should stop when truth is obtained" do
+ it "true && false && error == false (and no failure)" do
+ evaluate(literal(false).and(literal('0xwtf') + literal(1)).and(literal(true))).should == false
+ end
+
+ it "false || true || error == true (and no failure)" do
+ evaluate(literal(true).or(literal('0xwtf') + literal(1)).or(literal(false))).should == true
+ end
+
+ it "false || false || error == error (false positive test)" do
+ # TODO: Change the exception type
+ expect {evaluate(literal(true).and(literal('0xwtf') + literal(1)).or(literal(false)))}.to raise_error(Puppet::ParseError)
+ end
+ end
+ end
+end
diff --git a/spec/unit/pops/evaluator/string_interpolation_spec.rb b/spec/unit/pops/evaluator/string_interpolation_spec.rb
new file mode 100644
index 000000000..49f674588
--- /dev/null
+++ b/spec/unit/pops/evaluator/string_interpolation_spec.rb
@@ -0,0 +1,44 @@
+#! /usr/bin/env ruby
+require 'spec_helper'
+
+require 'puppet/pops'
+require 'puppet/pops/evaluator/evaluator_impl'
+
+
+# relative to this spec file (./) does not work as this file is loaded by rspec
+require File.join(File.dirname(__FILE__), '/evaluator_rspec_helper')
+
+describe 'Puppet::Pops::Evaluator::EvaluatorImpl' do
+ include EvaluatorRspecHelper
+
+ context "When evaluator performs string interpolation" do
+ it "should interpolate a bare word as a variable name, \"${var}\"" do
+ a_block = block(var('a').set(10), string('value is ', text(fqn('a')), ' yo'))
+ evaluate(a_block).should == "value is 10 yo"
+ end
+
+ it "should interpolate a variable in a text expression, \"${$var}\"" do
+ a_block = block(var('a').set(10), string('value is ', text(var(fqn('a'))), ' yo'))
+ evaluate(a_block).should == "value is 10 yo"
+ end
+
+ it "should interpolate a variable, \"$var\"" do
+ a_block = block(var('a').set(10), string('value is ', var(fqn('a')), ' yo'))
+ evaluate(a_block).should == "value is 10 yo"
+ end
+
+ it "should interpolate any expression in a text expression, \"${$var*2}\"" do
+ a_block = block(var('a').set(5), string('value is ', text(var(fqn('a')) * 2) , ' yo'))
+ evaluate(a_block).should == "value is 10 yo"
+ end
+
+ it "should interpolate any expression without a text expression, \"${$var*2}\"" do
+ # there is no concrete syntax for this, but the parser can generate this simpler
+ # equivalent form where the expression is not wrapped in a TextExpression
+ a_block = block(var('a').set(5), string('value is ', var(fqn('a')) * 2 , ' yo'))
+ evaluate(a_block).should == "value is 10 yo"
+ end
+
+ # TODO: Add function call tests - Pending implementation of calls in the evaluator
+ end
+end
diff --git a/spec/unit/pops/evaluator/variables_spec.rb b/spec/unit/pops/evaluator/variables_spec.rb
new file mode 100644
index 000000000..fe93842c4
--- /dev/null
+++ b/spec/unit/pops/evaluator/variables_spec.rb
@@ -0,0 +1,194 @@
+#! /usr/bin/env ruby
+require 'spec_helper'
+require 'puppet/pops'
+require 'puppet/pops/evaluator/evaluator_impl'
+
+
+# This file contains basic testing of variable references and assignments
+# using a top scope and a local scope.
+# It does not test variables and named scopes.
+#
+
+# relative to this spec file (./) does not work as this file is loaded by rspec
+require File.join(File.dirname(__FILE__), '/evaluator_rspec_helper')
+
+describe 'Puppet::Pops::Impl::EvaluatorImpl' do
+ include EvaluatorRspecHelper
+
+ context "When the evaluator deals with variables" do
+ context "it should handle" do
+ it "simple assignment and dereference" do
+ evaluate_l(block( var('a').set(literal(2)+literal(2)), var('a'))).should == 4
+ end
+
+ it "local scope shadows top scope" do
+ top_scope_block = block( var('a').set(literal(2)+literal(2)), var('a'))
+ local_scope_block = block( var('a').set(var('a') + literal(2)), var('a'))
+ evaluate_l(top_scope_block, local_scope_block).should == 6
+ end
+
+ it "shadowed in local does not affect parent scope" do
+ top_scope_block = block( var('a').set(literal(2)+literal(2)), var('a'))
+ local_scope_block = block( var('a').set(var('a') + literal(2)), var('a'))
+ top_scope_again = var('a')
+ evaluate_l(top_scope_block, local_scope_block, top_scope_again).should == 4
+ end
+
+ it "access to global names works in top scope" do
+ top_scope_block = block( var('a').set(literal(2)+literal(2)), var('::a'))
+ evaluate_l(top_scope_block).should == 4
+ end
+
+ it "access to global names works in local scope" do
+ top_scope_block = block( var('a').set(literal(2)+literal(2)))
+ local_scope_block = block( var('a').set(var('::a')+literal(2)), var('::a'))
+ evaluate_l(top_scope_block, local_scope_block).should == 6
+ end
+
+ it "can not change a variable value in same scope" do
+ expect { evaluate_l(block(var('a').set(10), var('a').set(20))) }.to raise_error(/Cannot reassign variable a/)
+ end
+
+ context "-= operations" do
+ # Also see collections_ops_spec.rb where delete via - is fully tested, here only the
+ # the -= operation itself is tested (there are many combinations)
+ #
+ it 'deleting from non existing value produces :undef, nil -= ?' do
+ top_scope_block = var('b').set([1,2,3])
+ local_scope_block = block(var('a').minus_set([4]), fqn('a').var)
+ evaluate_l(top_scope_block, local_scope_block).should == :undef
+ end
+
+ it 'deletes from a list' do
+ top_scope_block = var('a').set([1,2,3])
+ local_scope_block = block(var('a').minus_set([2]), fqn('a').var())
+ evaluate_l(top_scope_block, local_scope_block).should == [1,3]
+ end
+
+ it 'deletes from a hash' do
+ top_scope_block = var('a').set({'a'=>1,'b'=>2,'c'=>3})
+ local_scope_block = block(var('a').minus_set('b'), fqn('a').var())
+ evaluate_l(top_scope_block, local_scope_block).should == {'a'=>1,'c'=>3}
+ end
+ end
+
+ context "+= operations" do
+ # Also see collections_ops_spec.rb where concatenation via + is fully tested
+ it "appending to non existing value, nil += []" do
+ top_scope_block = var('b').set([1,2,3])
+ local_scope_block = var('a').plus_set([4])
+ evaluate_l(top_scope_block, local_scope_block).should == [4]
+ end
+
+ context "appending to list" do
+ it "from list, [] += []" do
+ top_scope_block = var('a').set([1,2,3])
+ local_scope_block = block(var('a').plus_set([4]), fqn('a').var())
+ evaluate_l(top_scope_block, local_scope_block).should == [1,2,3,4]
+ end
+
+ it "from hash, [] += {a=>b}" do
+ top_scope_block = var('a').set([1,2,3])
+ local_scope_block = block(var('a').plus_set({'a' => 1, 'b'=>2}), fqn('a').var())
+ evaluate_l(top_scope_block, local_scope_block).should satisfy {|result|
+ # hash in 1.8.7 is not insertion order preserving, hence this hoop
+ result == [1,2,3,['a',1],['b',2]] || result == [1,2,3,['b',2],['a',1]]
+ }
+ end
+
+ it "from single value, [] += x" do
+ top_scope_block = var('a').set([1,2,3])
+ local_scope_block = block(var('a').plus_set(4), fqn('a').var())
+ evaluate_l(top_scope_block, local_scope_block).should == [1,2,3,4]
+ end
+
+ it "from embedded list, [] += [[x]]" do
+ top_scope_block = var('a').set([1,2,3])
+ local_scope_block = block(var('a').plus_set([[4,5]]), fqn('a').var())
+ evaluate_l(top_scope_block, local_scope_block).should == [1,2,3,[4,5]]
+ end
+ end
+
+ context "appending to hash" do
+ it "from hash, {a=>b} += {x=>y}" do
+ top_scope_block = var('a').set({'a' => 1, 'b' => 2})
+ local_scope_block = block(var('a').plus_set({'c' => 3}), fqn('a').var())
+ evaluate_l(top_scope_block, local_scope_block) do |scope|
+ # Assert no change to top scope hash
+ scope['a'].should == {'a' =>1, 'b'=> 2}
+ end.should == {'a' => 1, 'b' => 2, 'c' => 3}
+ end
+
+ it "from list, {a=>b} += ['x', y]" do
+ top_scope_block = var('a').set({'a' => 1, 'b' => 2})
+ local_scope_block = block(var('a').plus_set(['c', 3]), fqn('a').var())
+ evaluate_l(top_scope_block, local_scope_block) do |scope|
+ # Assert no change to top scope hash
+ scope['a'].should == {'a' =>1, 'b'=> 2}
+ end.should == {'a' => 1, 'b' => 2, 'c' => 3}
+ end
+
+ it "with overwrite from hash, {a=>b} += {a=>c}" do
+ top_scope_block = var('a').set({'a' => 1, 'b' => 2})
+ local_scope_block = block(var('a').plus_set({'b' => 4, 'c' => 3}),fqn('a').var())
+ evaluate_l(top_scope_block, local_scope_block) do |scope|
+ # Assert no change to top scope hash
+ scope['a'].should == {'a' =>1, 'b'=> 2}
+ end.should == {'a' => 1, 'b' => 4, 'c' => 3}
+ end
+
+ it "with overwrite from list, {a=>b} += ['a', c]" do
+ top_scope_block = var('a').set({'a' => 1, 'b' => 2})
+ local_scope_block = block(var('a').plus_set(['b', 4, 'c', 3]), fqn('a').var())
+ evaluate_l(top_scope_block, local_scope_block) do |scope|
+ # Assert no change to topscope hash
+ scope['a'].should == {'a' =>1, 'b'=> 2}
+ end.should == {'a' => 1, 'b' => 4, 'c' => 3}
+ end
+
+ it "from odd length array - error" do
+ top_scope_block = var('a').set({'a' => 1, 'b' => 2})
+ local_scope_block = var('a').plus_set(['b', 4, 'c'])
+ expect { evaluate_l(top_scope_block, local_scope_block) }.to raise_error(/Append assignment \+= failed with error: odd number of arguments for Hash/)
+ end
+ end
+ end
+
+ context "access to numeric variables" do
+ it "without a match" do
+ evaluate_l(block(literal(2) + literal(2),
+ [var(0), var(1), var(2), var(3)])).should == [nil, nil, nil, nil]
+ end
+
+ it "after a match" do
+ evaluate_l(block(literal('abc') =~ literal(/(a)(b)(c)/),
+ [var(0), var(1), var(2), var(3)])).should == ['abc', 'a', 'b', 'c']
+ end
+
+ it "after a failed match" do
+ evaluate_l(block(literal('abc') =~ literal(/(x)(y)(z)/),
+ [var(0), var(1), var(2), var(3)])).should == [nil, nil, nil, nil]
+ end
+
+ it "a failed match does not alter previous match" do
+ evaluate_l(block(
+ literal('abc') =~ literal(/(a)(b)(c)/),
+ literal('abc') =~ literal(/(x)(y)(z)/),
+ [var(0), var(1), var(2), var(3)])).should == ['abc', 'a', 'b', 'c']
+ end
+
+ it "a new match completely shadows previous match" do
+ evaluate_l(block(
+ literal('abc') =~ literal(/(a)(b)(c)/),
+ literal('abc') =~ literal(/(a)bc/),
+ [var(0), var(1), var(2), var(3)])).should == ['abc', 'a', nil, nil]
+ end
+
+ it "after a match with variable referencing a non existing group" do
+ evaluate_l(block(literal('abc') =~ literal(/(a)(b)(c)/),
+ [var(0), var(1), var(2), var(3), var(4)])).should == ['abc', 'a', 'b', 'c', nil]
+ end
+ end
+ end
+ end
+end
diff --git a/spec/unit/pops/factory_spec.rb b/spec/unit/pops/factory_spec.rb
index 3cefbc78f..779a0630d 100644
--- a/spec/unit/pops/factory_spec.rb
+++ b/spec/unit/pops/factory_spec.rb
@@ -1,329 +1,306 @@
#! /usr/bin/env ruby
require 'spec_helper'
require 'puppet/pops'
require File.join(File.dirname(__FILE__), '/factory_rspec_helper')
# This file contains testing of the pops model factory
#
describe Puppet::Pops::Model::Factory do
include FactoryRspecHelper
context "When factory methods are invoked they should produce expected results" do
it "tests #var should create a VariableExpression" do
var('a').current.class.should == Puppet::Pops::Model::VariableExpression
end
it "tests #fqn should create a QualifiedName" do
fqn('a').current.class.should == Puppet::Pops::Model::QualifiedName
end
it "tests #QNAME should create a QualifiedName" do
QNAME('a').current.class.should == Puppet::Pops::Model::QualifiedName
end
it "tests #QREF should create a QualifiedReference" do
QREF('a').current.class.should == Puppet::Pops::Model::QualifiedReference
end
it "tests #block should create a BlockExpression" do
block().current.is_a?(Puppet::Pops::Model::BlockExpression).should == true
end
it "should create a literal undef on :undef" do
literal(:undef).current.class.should == Puppet::Pops::Model::LiteralUndef
end
it "should create a literal default on :default" do
literal(:default).current.class.should == Puppet::Pops::Model::LiteralDefault
end
end
context "When calling block_or_expression" do
it "A single expression should produce identical output" do
block_or_expression(literal(1) + literal(2)).current.is_a?(Puppet::Pops::Model::ArithmeticExpression).should == true
end
it "Multiple expressions should produce a block expression" do
built = block_or_expression(literal(1) + literal(2), literal(2) + literal(3)).current
built.is_a?(Puppet::Pops::Model::BlockExpression).should == true
built.statements.size.should == 2
end
end
context "When processing calls with CALL_NAMED" do
it "Should be possible to state that r-value is required" do
built = CALL_NAMED("foo", true, []).current
built.is_a?(Puppet::Pops::Model::CallNamedFunctionExpression).should == true
built.rval_required.should == true
end
it "Should produce a call expression without arguments" do
built = CALL_NAMED("foo", false, []).current
built.is_a?(Puppet::Pops::Model::CallNamedFunctionExpression).should == true
built.functor_expr.is_a?(Puppet::Pops::Model::QualifiedName).should == true
built.functor_expr.value.should == "foo"
built.rval_required.should == false
built.arguments.size.should == 0
end
it "Should produce a call expression with one argument" do
built = CALL_NAMED("foo", false, [literal(1) + literal(2)]).current
built.is_a?(Puppet::Pops::Model::CallNamedFunctionExpression).should == true
built.functor_expr.is_a?(Puppet::Pops::Model::QualifiedName).should == true
built.functor_expr.value.should == "foo"
built.rval_required.should == false
built.arguments.size.should == 1
built.arguments[0].is_a?(Puppet::Pops::Model::ArithmeticExpression).should == true
end
it "Should produce a call expression with two arguments" do
built = CALL_NAMED("foo", false, [literal(1) + literal(2), literal(1) + literal(2)]).current
built.is_a?(Puppet::Pops::Model::CallNamedFunctionExpression).should == true
built.functor_expr.is_a?(Puppet::Pops::Model::QualifiedName).should == true
built.functor_expr.value.should == "foo"
built.rval_required.should == false
built.arguments.size.should == 2
built.arguments[0].is_a?(Puppet::Pops::Model::ArithmeticExpression).should == true
built.arguments[1].is_a?(Puppet::Pops::Model::ArithmeticExpression).should == true
end
end
context "When creating attribute operations" do
it "Should produce an attribute operation for =>" do
built = ATTRIBUTE_OP("aname", :'=>', 'x').current
built.is_a?(Puppet::Pops::Model::AttributeOperation)
built.operator.should == :'=>'
built.attribute_name.should == "aname"
built.value_expr.is_a?(Puppet::Pops::Model::LiteralString).should == true
end
it "Should produce an attribute operation for +>" do
built = ATTRIBUTE_OP("aname", :'+>', 'x').current
built.is_a?(Puppet::Pops::Model::AttributeOperation)
built.operator.should == :'+>'
built.attribute_name.should == "aname"
built.value_expr.is_a?(Puppet::Pops::Model::LiteralString).should == true
end
end
context "When processing RESOURCE" do
it "Should create a Resource body" do
built = RESOURCE_BODY("title", [ATTRIBUTE_OP('aname', :'=>', 'x')]).current
built.is_a?(Puppet::Pops::Model::ResourceBody).should == true
built.title.is_a?(Puppet::Pops::Model::LiteralString).should == true
built.operations.size.should == 1
built.operations[0].class.should == Puppet::Pops::Model::AttributeOperation
built.operations[0].attribute_name.should == 'aname'
end
it "Should create a RESOURCE without a resource body" do
bodies = []
built = RESOURCE("rtype", bodies).current
built.class.should == Puppet::Pops::Model::ResourceExpression
built.bodies.size.should == 0
end
it "Should create a RESOURCE with 1 resource body" do
bodies = [] << RESOURCE_BODY('title', [])
built = RESOURCE("rtype", bodies).current
built.class.should == Puppet::Pops::Model::ResourceExpression
built.bodies.size.should == 1
built.bodies[0].title.value.should == 'title'
end
it "Should create a RESOURCE with 2 resource bodies" do
bodies = [] << RESOURCE_BODY('title', []) << RESOURCE_BODY('title2', [])
built = RESOURCE("rtype", bodies).current
built.class.should == Puppet::Pops::Model::ResourceExpression
built.bodies.size.should == 2
built.bodies[0].title.value.should == 'title'
built.bodies[1].title.value.should == 'title2'
end
end
context "When processing simple literals" do
it "Should produce a literal boolean from a boolean" do
built = literal(true).current
built.class.should == Puppet::Pops::Model::LiteralBoolean
built.value.should == true
built = literal(false).current
built.class.should == Puppet::Pops::Model::LiteralBoolean
built.value.should == false
end
end
context "When processing COLLECT" do
it "should produce a virtual query" do
built = VIRTUAL_QUERY(fqn('a') == literal(1)).current
built.class.should == Puppet::Pops::Model::VirtualQuery
built.expr.class.should == Puppet::Pops::Model::ComparisonExpression
built.expr.operator.should == :'=='
end
it "should produce an export query" do
built = EXPORTED_QUERY(fqn('a') == literal(1)).current
built.class.should == Puppet::Pops::Model::ExportedQuery
built.expr.class.should == Puppet::Pops::Model::ComparisonExpression
built.expr.operator.should == :'=='
end
it "should produce a collect expression" do
q = VIRTUAL_QUERY(fqn('a') == literal(1))
built = COLLECT(literal('t'), q, [ATTRIBUTE_OP('name', :'=>', 3)]).current
built.class.should == Puppet::Pops::Model::CollectExpression
built.operations.size.should == 1
end
it "should produce a collect expression without attribute operations" do
q = VIRTUAL_QUERY(fqn('a') == literal(1))
built = COLLECT(literal('t'), q, []).current
built.class.should == Puppet::Pops::Model::CollectExpression
built.operations.size.should == 0
end
end
context "When processing concatenated string(iterpolation)" do
it "should handle 'just a string'" do
built = string('blah blah').current
built.class.should == Puppet::Pops::Model::ConcatenatedString
built.segments.size == 1
built.segments[0].class.should == Puppet::Pops::Model::LiteralString
built.segments[0].value.should == "blah blah"
end
it "should handle one expression in the middle" do
built = string('blah blah', TEXT(literal(1)+literal(2)), 'blah blah').current
built.class.should == Puppet::Pops::Model::ConcatenatedString
built.segments.size == 3
built.segments[0].class.should == Puppet::Pops::Model::LiteralString
built.segments[0].value.should == "blah blah"
built.segments[1].class.should == Puppet::Pops::Model::TextExpression
built.segments[1].expr.class.should == Puppet::Pops::Model::ArithmeticExpression
built.segments[2].class.should == Puppet::Pops::Model::LiteralString
built.segments[2].value.should == "blah blah"
end
it "should handle one expression at the end" do
built = string('blah blah', TEXT(literal(1)+literal(2))).current
built.class.should == Puppet::Pops::Model::ConcatenatedString
built.segments.size == 2
built.segments[0].class.should == Puppet::Pops::Model::LiteralString
built.segments[0].value.should == "blah blah"
built.segments[1].class.should == Puppet::Pops::Model::TextExpression
built.segments[1].expr.class.should == Puppet::Pops::Model::ArithmeticExpression
end
it "should handle only one expression" do
built = string(TEXT(literal(1)+literal(2))).current
built.class.should == Puppet::Pops::Model::ConcatenatedString
built.segments.size == 1
built.segments[0].class.should == Puppet::Pops::Model::TextExpression
built.segments[0].expr.class.should == Puppet::Pops::Model::ArithmeticExpression
end
it "should handle several expressions" do
built = string(TEXT(literal(1)+literal(2)), TEXT(literal(1)+literal(2))).current
built.class.should == Puppet::Pops::Model::ConcatenatedString
built.segments.size == 2
built.segments[0].class.should == Puppet::Pops::Model::TextExpression
built.segments[0].expr.class.should == Puppet::Pops::Model::ArithmeticExpression
built.segments[1].class.should == Puppet::Pops::Model::TextExpression
built.segments[1].expr.class.should == Puppet::Pops::Model::ArithmeticExpression
end
it "should handle no expression" do
built = string().current
built.class.should == Puppet::Pops::Model::ConcatenatedString
built.segments.size == 0
end
end
- context "When processing instance / resource references" do
- it "should produce an InstanceReference without a reference" do
- built = INSTANCE(QREF('a'), []).current
- built.class.should == Puppet::Pops::Model::InstanceReferences
- built.names.size.should == 0
- end
-
- it "should produce an InstanceReference with one reference" do
- built = INSTANCE(QREF('a'), [QNAME('b')]).current
- built.class.should == Puppet::Pops::Model::InstanceReferences
- built.names.size.should == 1
- built.names[0].value.should == 'b'
- end
-
- it "should produce an InstanceReference with two references" do
- built = INSTANCE(QREF('a'), [QNAME('b'), QNAME('c')]).current
- built.class.should == Puppet::Pops::Model::InstanceReferences
- built.names.size.should == 2
- built.names[0].value.should == 'b'
- built.names[1].value.should == 'c'
- end
- end
-
context "When processing UNLESS" do
it "should create an UNLESS expression with then part" do
built = UNLESS(true, literal(1), nil).current
built.class.should == Puppet::Pops::Model::UnlessExpression
built.test.class.should == Puppet::Pops::Model::LiteralBoolean
- built.then_expr.class.should == Puppet::Pops::Model::LiteralNumber
+ built.then_expr.class.should == Puppet::Pops::Model::LiteralInteger
built.else_expr.class.should == Puppet::Pops::Model::Nop
end
it "should create an UNLESS expression with then and else parts" do
built = UNLESS(true, literal(1), literal(2)).current
built.class.should == Puppet::Pops::Model::UnlessExpression
built.test.class.should == Puppet::Pops::Model::LiteralBoolean
- built.then_expr.class.should == Puppet::Pops::Model::LiteralNumber
- built.else_expr.class.should == Puppet::Pops::Model::LiteralNumber
+ built.then_expr.class.should == Puppet::Pops::Model::LiteralInteger
+ built.else_expr.class.should == Puppet::Pops::Model::LiteralInteger
end
end
context "When processing IF" do
it "should create an IF expression with then part" do
built = IF(true, literal(1), nil).current
built.class.should == Puppet::Pops::Model::IfExpression
built.test.class.should == Puppet::Pops::Model::LiteralBoolean
- built.then_expr.class.should == Puppet::Pops::Model::LiteralNumber
+ built.then_expr.class.should == Puppet::Pops::Model::LiteralInteger
built.else_expr.class.should == Puppet::Pops::Model::Nop
end
it "should create an IF expression with then and else parts" do
built = IF(true, literal(1), literal(2)).current
built.class.should == Puppet::Pops::Model::IfExpression
built.test.class.should == Puppet::Pops::Model::LiteralBoolean
- built.then_expr.class.should == Puppet::Pops::Model::LiteralNumber
- built.else_expr.class.should == Puppet::Pops::Model::LiteralNumber
+ built.then_expr.class.should == Puppet::Pops::Model::LiteralInteger
+ built.else_expr.class.should == Puppet::Pops::Model::LiteralInteger
end
end
context "When processing a Parameter" do
it "should create a Parameter" do
# PARAM(name, expr)
# PARAM(name)
#
end
end
# LIST, HASH, KEY_ENTRY
context "When processing Definition" do
# DEFINITION(classname, arguments, statements)
# should accept empty arguments, and no statements
end
context "When processing Hostclass" do
# HOSTCLASS(classname, arguments, parent, statements)
# parent may be passed as a nop /nil - check this works, should accept empty statements (nil)
# should accept empty arguments
end
context "When processing Node" do
end
# Tested in the evaluator test already, but should be here to test factory assumptions
#
# TODO: CASE / WHEN
# TODO: MAP
end
diff --git a/spec/unit/pops/issues_spec.rb b/spec/unit/pops/issues_spec.rb
index d8650b956..93f093d61 100644
--- a/spec/unit/pops/issues_spec.rb
+++ b/spec/unit/pops/issues_spec.rb
@@ -1,26 +1,26 @@
#! /usr/bin/env ruby
require 'spec_helper'
require 'puppet/pops'
describe "Puppet::Pops::Issues" do
include Puppet::Pops::Issues
it "should have an issue called NAME_WITH_HYPHEN" do
x = Puppet::Pops::Issues::NAME_WITH_HYPHEN
x.class.should == Puppet::Pops::Issues::Issue
x.issue_code.should == :NAME_WITH_HYPHEN
end
it "should should format a message that requires an argument" do
x = Puppet::Pops::Issues::NAME_WITH_HYPHEN
x.format(:name => 'Boo-Hoo',
:label => Puppet::Pops::Model::ModelLabelProvider.new,
:semantic => "dummy"
- ).should == "A Ruby String may not have a name containing a hyphen. The name 'Boo-Hoo' is not legal"
+ ).should == "A String may not have a name containing a hyphen. The name 'Boo-Hoo' is not legal"
end
it "should should format a message that does not require an argument" do
x = Puppet::Pops::Issues::NOT_TOP_LEVEL
x.format().should == "Classes, definitions, and nodes may only appear at toplevel or inside other classes"
end
end
diff --git a/spec/unit/pops/model/ast_transformer_spec.rb b/spec/unit/pops/model/ast_transformer_spec.rb
index 969e944de..d62d2eaec 100644
--- a/spec/unit/pops/model/ast_transformer_spec.rb
+++ b/spec/unit/pops/model/ast_transformer_spec.rb
@@ -1,79 +1,76 @@
require 'spec_helper'
require File.join(File.dirname(__FILE__), '/../factory_rspec_helper')
require 'puppet/pops'
describe Puppet::Pops::Model::AstTransformer do
include FactoryRspecHelper
let(:filename) { "the-file.pp" }
let(:transformer) { Puppet::Pops::Model::AstTransformer.new(filename) }
context "literal numbers" 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.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.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.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 = transform(Puppet::Pops::Model::Factory.new(Puppet::Pops::Model::LiteralInteger, 3, 2))
ast.should be_kind_of(Puppet::Parser::AST::Name)
ast.value.should == "bad radix:3"
end
end
it "preserves the file location" do
model = literal(1)
- model.record_position(location(3, 1, 10), location(3, 2, 11))
+ adapter = Puppet::Pops::Adapters::SourcePosAdapter.adapt(model.current)
+ adapter.locator = Puppet::Pops::Parser::Locator.locator("\n\n1",filename)
+ model.record_position(location(2, 1), nil)
ast = transform(model)
ast.file.should == filename
ast.line.should == 3
ast.pos.should == 1
end
def transform(model)
transformer.transform(model)
end
- def location(line, column, offset)
- position = Puppet::Pops::Adapters::SourcePosAdapter.new
- position.line = line
- position.pos = column
- position.offset = offset
-
- position
+ def location(offset, length)
+ Puppet::Pops::Parser::Locatable::Fixed.new(offset, length)
end
end
diff --git a/spec/unit/pops/model/model_spec.rb b/spec/unit/pops/model/model_spec.rb
index 5014a64fa..3c1b22e3e 100644
--- a/spec/unit/pops/model/model_spec.rb
+++ b/spec/unit/pops/model/model_spec.rb
@@ -1,37 +1,37 @@
#! /usr/bin/env ruby
require 'spec_helper'
require 'puppet/pops'
describe Puppet::Pops::Model do
it "should be possible to create an instance of a model object" do
nop = Puppet::Pops::Model::Nop.new
nop.class.should == Puppet::Pops::Model::Nop
end
end
describe Puppet::Pops::Model::Factory do
Factory = Puppet::Pops::Model::Factory
Model = Puppet::Pops::Model
it "construct an arithmetic expression" do
x = Factory.literal(10) + Factory.literal(20)
x.is_a?(Factory).should == true
current = x.current
current.is_a?(Model::ArithmeticExpression).should == true
current.operator.should == :'+'
- current.left_expr.class.should == Model::LiteralNumber
- current.right_expr.class.should == Model::LiteralNumber
+ current.left_expr.class.should == Model::LiteralInteger
+ current.right_expr.class.should == Model::LiteralInteger
current.left_expr.value.should == 10
current.right_expr.value.should == 20
end
it "should be easy to compare using a model tree dumper" do
x = Factory.literal(10) + Factory.literal(20)
Puppet::Pops::Model::ModelTreeDumper.new.dump(x.current).should == "(+ 10 20)"
end
it "builder should apply precedence" do
x = Factory.literal(2) * Factory.literal(10) + Factory.literal(20)
Puppet::Pops::Model::ModelTreeDumper.new.dump(x.current).should == "(+ (* 2 10) 20)"
end
end
diff --git a/spec/unit/pops/parser/epp_parser_spec.rb b/spec/unit/pops/parser/epp_parser_spec.rb
new file mode 100644
index 000000000..0db4ba7d9
--- /dev/null
+++ b/spec/unit/pops/parser/epp_parser_spec.rb
@@ -0,0 +1,86 @@
+require 'spec_helper'
+require 'puppet/pops'
+
+require File.join(File.dirname(__FILE__), '/../factory_rspec_helper')
+
+module EppParserRspecHelper
+ include FactoryRspecHelper
+ def parse(code)
+ parser = Puppet::Pops::Parser::EppParser.new()
+ parser.parse_string(code)
+ end
+end
+
+describe "epp parser" do
+ include EppParserRspecHelper
+
+ it "should instantiate an epp parser" do
+ parser = Puppet::Pops::Parser::EppParser.new()
+ parser.class.should == Puppet::Pops::Parser::EppParser
+ end
+
+ it "should parse a code string and return a program with epp" do
+ parser = Puppet::Pops::Parser::EppParser.new()
+ model = parser.parse_string("Nothing to see here, move along...").current
+ model.class.should == Puppet::Pops::Model::Program
+ model.body.class.should == Puppet::Pops::Model::LambdaExpression
+ model.body.body.class.should == Puppet::Pops::Model::EppExpression
+ end
+
+ context "when facing bad input it reports" do
+ it "unbalanced tags" do
+ expect { dump(parse("<% missing end tag")) }.to raise_error(/Unbalanced/)
+ end
+
+ it "abrupt end" do
+ expect { dump(parse("dum di dum di dum <%")) }.to raise_error(/Unbalanced/)
+ end
+
+ it "nested epp tags" do
+ expect { dump(parse("<% $a = 10 <% $b = 20 %>%>")) }.to raise_error(/Syntax error/)
+ end
+
+ it "nested epp expression tags" do
+ expect { dump(parse("<%= 1+1 <%= 2+2 %>%>")) }.to raise_error(/Syntax error/)
+ end
+
+ it "rendering sequence of expressions" do
+ expect { dump(parse("<%= 1 2 3 %>")) }.to raise_error(/Syntax error/)
+ end
+ end
+
+ context "handles parsing of" do
+ it "text (and nothing else)" do
+ dump(parse("Hello World")).should == "(lambda (epp (block (render-s 'Hello World'))))"
+ end
+
+ it "template parameters" do
+ dump(parse("<%|$x|%>Hello World")).should == "(lambda (parameters x) (epp (block (render-s 'Hello World'))))"
+ end
+
+ it "template parameters with default" do
+ dump(parse("<%|$x='cigar'|%>Hello World")).should == "(lambda (parameters (= x 'cigar')) (epp (block (render-s 'Hello World'))))"
+ end
+
+ it "template parameters with and without default" do
+ dump(parse("<%|$x='cigar', $y|%>Hello World")).should == "(lambda (parameters (= x 'cigar') y) (epp (block (render-s 'Hello World'))))"
+ end
+
+ it "template parameters + additional setup" do
+ dump(parse("<%|$x| $y = 10 %>Hello World")).should == "(lambda (parameters x) (epp (block (= $y 10) (render-s 'Hello World'))))"
+ end
+
+ it "comments" do
+ dump(parse("<%#($x='cigar', $y)%>Hello World")).should == "(lambda (epp (block (render-s 'Hello World'))))"
+ end
+
+ it "verbatim epp tags" do
+ dump(parse("<%% contemplating %%>Hello World")).should == "(lambda (epp (block (render-s '<% contemplating %>Hello World'))))"
+ end
+
+ it "expressions" do
+ dump(parse("We all live in <%= 3.14 - 2.14 %> world")).should ==
+ "(lambda (epp (block (render-s 'We all live in ') (render (- 3.14 2.14)) (render-s ' world'))))"
+ end
+ end
+end
diff --git a/spec/unit/pops/parser/evaluating_parser_spec.rb b/spec/unit/pops/parser/evaluating_parser_spec.rb
index 027b86a09..8448a5af3 100644
--- a/spec/unit/pops/parser/evaluating_parser_spec.rb
+++ b/spec/unit/pops/parser/evaluating_parser_spec.rb
@@ -1,88 +1,90 @@
require 'spec_helper'
require 'puppet/pops'
require 'puppet_spec/pops'
+require 'puppet_spec/scope'
-describe 'The hiera2 string evaluator' do
+describe 'The Evaluating Parser' do
include PuppetSpec::Pops
+ include PuppetSpec::Scope
let(:acceptor) { Puppet::Pops::Validation::Acceptor.new() }
let(:diag) { Puppet::Pops::Binder::Hiera2::DiagnosticProducer.new(acceptor) }
- let(:scope) { s = Puppet::Parser::Scope.new_for_test_harness(node); s }
+ let(:scope) { s = create_test_scope_for_node(node); s }
let(:node) { 'node.example.com' }
def quote(x)
Puppet::Pops::Parser::EvaluatingParser.quote(x)
end
def evaluator()
Puppet::Pops::Parser::EvaluatingParser.new()
end
def evaluate(s)
evaluator.evaluate(scope, quote(s))
end
def test(x)
evaluator.evaluate_string(scope, quote(x)).should == x
end
def test_interpolate(x, y)
scope['a'] = 'expansion'
evaluator.evaluate_string(scope, quote(x)).should == y
end
context 'when evaluating' do
it 'should produce an empty string with no change' do
test('')
end
it 'should produce a normal string with no change' do
test('A normal string')
end
it 'should produce a string with newlines with no change' do
test("A\nnormal\nstring")
end
it 'should produce a string with escaped newlines with no change' do
test("A\\nnormal\\nstring")
end
it 'should produce a string containing quotes without change' do
test('This " should remain untouched')
end
it 'should produce a string containing escaped quotes without change' do
test('This \" should remain untouched')
end
it 'should expand ${a} variables' do
test_interpolate('This ${a} was expanded', 'This expansion was expanded')
end
it 'should expand quoted ${a} variables' do
test_interpolate('This "${a}" was expanded', 'This "expansion" was expanded')
end
it 'should not expand escaped ${a}' do
test_interpolate('This \${a} was not expanded', 'This ${a} was not expanded')
end
it 'should expand $a variables' do
test_interpolate('This $a was expanded', 'This expansion was expanded')
end
it 'should expand quoted $a variables' do
test_interpolate('This "$a" was expanded', 'This "expansion" was expanded')
end
it 'should not expand escaped $a' do
test_interpolate('This \$a was not expanded', 'This $a was not expanded')
end
it 'should produce an single space from a \s' do
test_interpolate("\\s", ' ')
end
end
end
diff --git a/spec/unit/pops/parser/lexer2_spec.rb b/spec/unit/pops/parser/lexer2_spec.rb
new file mode 100644
index 000000000..3e5a2c20b
--- /dev/null
+++ b/spec/unit/pops/parser/lexer2_spec.rb
@@ -0,0 +1,428 @@
+require 'spec_helper'
+require 'matchers/match_tokens2'
+require 'puppet/pops'
+require 'puppet/pops/parser/lexer2'
+
+module EgrammarLexer2Spec
+ def tokens_scanned_from(s)
+ lexer = Puppet::Pops::Parser::Lexer2.new
+ lexer.string = s
+ tokens = lexer.fullscan[0..-2]
+ end
+
+ def epp_tokens_scanned_from(s)
+ lexer = Puppet::Pops::Parser::Lexer2.new
+ lexer.string = s
+ tokens = lexer.fullscan_epp[0..-2]
+ end
+end
+
+describe 'Lexer2' do
+ include EgrammarLexer2Spec
+
+ {
+ :LBRACK => '[',
+ :RBRACK => ']',
+ :LBRACE => '{',
+ :RBRACE => '}',
+ :LPAREN => '(',
+ :RPAREN => ')',
+ :EQUALS => '=',
+ :ISEQUAL => '==',
+ :GREATEREQUAL => '>=',
+ :GREATERTHAN => '>',
+ :LESSTHAN => '<',
+ :LESSEQUAL => '<=',
+ :NOTEQUAL => '!=',
+ :NOT => '!',
+ :COMMA => ',',
+ :DOT => '.',
+ :COLON => ':',
+ :AT => '@',
+ :LLCOLLECT => '<<|',
+ :RRCOLLECT => '|>>',
+ :LCOLLECT => '<|',
+ :RCOLLECT => '|>',
+ :SEMIC => ';',
+ :QMARK => '?',
+ :OTHER => '\\',
+ :FARROW => '=>',
+ :PARROW => '+>',
+ :APPENDS => '+=',
+ :DELETES => '-=',
+ :PLUS => '+',
+ :MINUS => '-',
+ :DIV => '/',
+ :TIMES => '*',
+ :LSHIFT => '<<',
+ :RSHIFT => '>>',
+ :MATCH => '=~',
+ :NOMATCH => '!~',
+ :IN_EDGE => '->',
+ :OUT_EDGE => '<-',
+ :IN_EDGE_SUB => '~>',
+ :OUT_EDGE_SUB => '<~',
+ :PIPE => '|',
+ }.each do |name, string|
+ it "should lex a token named #{name.to_s}" do
+ tokens_scanned_from(string).should match_tokens2(name)
+ end
+ end
+
+ {
+ "case" => :CASE,
+ "class" => :CLASS,
+ "default" => :DEFAULT,
+ "define" => :DEFINE,
+# "import" => :IMPORT, # done as a function in egrammar
+ "if" => :IF,
+ "elsif" => :ELSIF,
+ "else" => :ELSE,
+ "inherits" => :INHERITS,
+ "node" => :NODE,
+ "and" => :AND,
+ "or" => :OR,
+ "undef" => :UNDEF,
+ "false" => :BOOLEAN,
+ "true" => :BOOLEAN,
+ "in" => :IN,
+ "unless" => :UNLESS,
+ }.each do |string, name|
+ it "should lex a keyword from '#{string}'" do
+ tokens_scanned_from(string).should match_tokens2(name)
+ end
+ end
+
+ # TODO: Complete with all edge cases
+ [ 'A', 'A::B', '::A', '::A::B',].each do |string|
+ it "should lex a CLASSREF on the form '#{string}'" do
+ tokens_scanned_from(string).should match_tokens2([:CLASSREF, string])
+ end
+ end
+
+ # TODO: Complete with all edge cases
+ [ 'a', 'a::b', '::a', '::a::b',].each do |string|
+ it "should lex a NAME on the form '#{string}'" do
+ tokens_scanned_from(string).should match_tokens2([:NAME, string])
+ end
+ end
+
+ [ 'a-b', 'a--b', 'a-b-c'].each do |string|
+ it "should lex a BARE WORD STRING on the form '#{string}'" do
+ tokens_scanned_from(string).should match_tokens2([:STRING, string])
+ end
+ end
+
+ { '-a' => [:MINUS, :NAME],
+ '--a' => [:MINUS, :MINUS, :NAME],
+ 'a-' => [:NAME, :MINUS],
+ 'a- b' => [:NAME, :MINUS, :NAME],
+ 'a--' => [:NAME, :MINUS, :MINUS],
+ 'a-$3' => [:NAME, :MINUS, :VARIABLE],
+ }.each do |source, expected|
+ it "should lex leading and trailing hyphens from #{source}" do
+ tokens_scanned_from(source).should match_tokens2(*expected)
+ end
+ end
+
+ { 'false'=>false, 'true'=>true}.each do |string, value|
+ it "should lex a BOOLEAN on the form '#{string}'" do
+ tokens_scanned_from(string).should match_tokens2([:BOOLEAN, value])
+ end
+ end
+
+ [ '0', '1', '2982383139'].each do |string|
+ it "should lex a decimal integer NUMBER on the form '#{string}'" do
+ tokens_scanned_from(string).should match_tokens2([:NUMBER, string])
+ end
+ end
+
+ { ' 1' => '1', '1 ' => '1', ' 1 ' => '1'}.each do |string, value|
+ it "should lex a NUMBER with surrounding space '#{string}'" do
+ tokens_scanned_from(string).should match_tokens2([:NUMBER, value])
+ end
+ end
+
+ [ '0.0', '0.1', '0.2982383139', '29823.235', '10e23', '10e-23', '1.234e23'].each do |string|
+ it "should lex a decimal floating point NUMBER on the form '#{string}'" do
+ tokens_scanned_from(string).should match_tokens2([:NUMBER, string])
+ end
+ end
+
+ [ '00', '01', '0123', '0777'].each do |string|
+ it "should lex an octal integer NUMBER on the form '#{string}'" do
+ tokens_scanned_from(string).should match_tokens2([:NUMBER, string])
+ end
+ end
+
+ [ '0x0', '0x1', '0xa', '0xA', '0xabcdef', '0xABCDEF'].each do |string|
+ it "should lex an hex integer NUMBER on the form '#{string}'" do
+ tokens_scanned_from(string).should match_tokens2([:NUMBER, string])
+ end
+ end
+
+ { "''" => '',
+ "'a'" => 'a',
+ "'a\\'b'" =>"a'b",
+ "'a\\rb'" =>"a\\rb",
+ "'a\\nb'" =>"a\\nb",
+ "'a\\tb'" =>"a\\tb",
+ "'a\\sb'" =>"a\\sb",
+ "'a\\$b'" =>"a\\$b",
+ "'a\\\"b'" =>"a\\\"b",
+ "'a\\\\b'" =>"a\\b",
+ "'a\\\\'" =>"a\\",
+ }.each do |source, expected|
+ it "should lex a single quoted STRING on the form #{source}" do
+ tokens_scanned_from(source).should match_tokens2([:STRING, expected])
+ end
+ end
+
+ { '""' => '',
+ '"a"' => 'a',
+ '"a\'b"' => "a'b",
+ }.each do |source, expected|
+ it "should lex a double quoted STRING on the form #{source}" do
+ tokens_scanned_from(source).should match_tokens2([:STRING, expected])
+ end
+ end
+
+ { '"a$x b"' => [[:DQPRE, 'a', {:line => 1, :pos=>1, :length=>2 }],
+ [:VARIABLE, 'x', {:line => 1, :pos=>3, :length=>2 }],
+ [:DQPOST, ' b', {:line => 1, :pos=>5, :length=>3 }]],
+
+ '"a$x.b"' => [[:DQPRE, 'a', {:line => 1, :pos=>1, :length=>2 }],
+ [:VARIABLE, 'x', {:line => 1, :pos=>3, :length=>2 }],
+ [:DQPOST, '.b', {:line => 1, :pos=>5, :length=>3 }]],
+
+ '"$x.b"' => [[:DQPRE, '', {:line => 1, :pos=>1, :length=>1 }],
+ [:VARIABLE, 'x', {:line => 1, :pos=>2, :length=>2 }],
+ [:DQPOST, '.b', {:line => 1, :pos=>4, :length=>3 }]],
+
+ '"a$x"' => [[:DQPRE, 'a', {:line => 1, :pos=>1, :length=>2 }],
+ [:VARIABLE, 'x', {:line => 1, :pos=>3, :length=>2 }],
+ [:DQPOST, '', {:line => 1, :pos=>5, :length=>1 }]],
+ }.each do |source, expected|
+ it "should lex an interpolated variable 'x' from #{source}" do
+ tokens_scanned_from(source).should match_tokens2(*expected)
+ end
+ end
+
+ it "differentiates between foo[x] and foo [x] (whitespace)" do
+ tokens_scanned_from("$a[1]").should match_tokens2(:VARIABLE, :LBRACK, :NUMBER, :RBRACK)
+ tokens_scanned_from("$a [1]").should match_tokens2(:VARIABLE, :LBRACK, :NUMBER, :RBRACK)
+ tokens_scanned_from("a[1]").should match_tokens2(:NAME, :LBRACK, :NUMBER, :RBRACK)
+ tokens_scanned_from("a [1]").should match_tokens2(:NAME, :LISTSTART, :NUMBER, :RBRACK)
+ tokens_scanned_from(" if \n\r\t\nif if ").should match_tokens2(:IF, :IF, :IF)
+ end
+
+ it "skips whitepsace" do
+ tokens_scanned_from(" if if if ").should match_tokens2(:IF, :IF, :IF)
+ tokens_scanned_from(" if \n\r\t\nif if ").should match_tokens2(:IF, :IF, :IF)
+ end
+
+ it "skips single line comments" do
+ tokens_scanned_from("if # comment\nif").should match_tokens2(:IF, :IF)
+ end
+
+ ["if /* comment */\nif",
+ "if /* comment\n */\nif",
+ "if /*\n comment\n */\nif",
+ ].each do |source|
+ it "skips multi line comments" do
+ tokens_scanned_from(source).should match_tokens2(:IF, :IF)
+ end
+ end
+
+ { "=~" => [:MATCH, "=~ /./"],
+ "!~" => [:NOMATCH, "!~ /./"],
+ "," => [:COMMA, ", /./"],
+ "(" => [:LPAREN, "( /./"],
+ "[" => [:LBRACK, "[ /./"],
+ "{" => [:LBRACE, "{ /./"],
+ "+" => [:PLUS, "+ /./"],
+ "-" => [:MINUS, "- /./"],
+ "*" => [:TIMES, "* /./"],
+ ";" => [:SEMIC, "; /./"],
+ }.each do |token, entry|
+ it "should lex regexp after '#{token}'" do
+ tokens_scanned_from(entry[1]).should match_tokens2(entry[0], :REGEX)
+ end
+ end
+
+ it "should lex a simple expression" do
+ tokens_scanned_from('1 + 1').should match_tokens2([:NUMBER, '1'], :PLUS, [:NUMBER, '1'])
+ end
+
+ { "1" => ["1 /./", [:NUMBER, :DIV, :DOT, :DIV]],
+ "'a'" => ["'a' /./", [:STRING, :DIV, :DOT, :DIV]],
+ "true" => ["true /./", [:BOOLEAN, :DIV, :DOT, :DIV]],
+ "false" => ["false /./", [:BOOLEAN, :DIV, :DOT, :DIV]],
+ "/./" => ["/./ /./", [:REGEX, :DIV, :DOT, :DIV]],
+ "a" => ["a /./", [:NAME, :DIV, :DOT, :DIV]],
+ "A" => ["A /./", [:CLASSREF, :DIV, :DOT, :DIV]],
+ ")" => [") /./", [:RPAREN, :DIV, :DOT, :DIV]],
+ "]" => ["] /./", [:RBRACK, :DIV, :DOT, :DIV]],
+ "|>" => ["|> /./", [:RCOLLECT, :DIV, :DOT, :DIV]],
+ "|>>" => ["|>> /./", [:RRCOLLECT, :DIV, :DOT, :DIV]],
+ '"a$a"' => ['"a$a" /./', [:DQPRE, :VARIABLE, :DQPOST, :DIV, :DOT, :DIV]],
+ }.each do |token, entry|
+ it "should not lex regexp after '#{token}'" do
+ tokens_scanned_from(entry[ 0 ]).should match_tokens2(*entry[ 1 ])
+ end
+ end
+
+ it 'should lex assignment' do
+ tokens_scanned_from("$a = 10").should match_tokens2([:VARIABLE, "a"], :EQUALS, [:NUMBER, '10'])
+ end
+
+# TODO: Tricky, and heredoc not supported yet
+# it "should not lex regexp after heredoc" do
+# tokens_scanned_from("1 / /./").should match_tokens2(:NUMBER, :DIV, :REGEX)
+# end
+
+ it "should lex regexp at beginning of input" do
+ tokens_scanned_from(" /./").should match_tokens2(:REGEX)
+ end
+
+ it "should lex regexp right of div" do
+ tokens_scanned_from("1 / /./").should match_tokens2(:NUMBER, :DIV, :REGEX)
+ end
+
+ context 'when lexer lexes heredoc' do
+ it 'lexes tag, syntax and escapes, margin and right trim' do
+ code = <<-CODE
+ @(END:syntax/t)
+ Tex\\tt\\n
+ |- END
+ CODE
+ tokens_scanned_from(code).should match_tokens2([:HEREDOC, 'syntax'], :SUBLOCATE, [:STRING, "Tex\tt\\n"])
+ end
+
+ it 'lexes "tag", syntax and escapes, margin, right trim and interpolation' do
+ code = <<-CODE
+ @("END":syntax/t)
+ Tex\\tt\\n$var After
+ |- END
+ CODE
+ tokens_scanned_from(code).should match_tokens2(
+ [:HEREDOC, 'syntax'],
+ :SUBLOCATE,
+ [:DQPRE, "Tex\tt\\n"],
+ [:VARIABLE, "var"],
+ [:DQPOST, " After"]
+ )
+ end
+ end
+
+ it 'should support unicode characters' do
+ code = <<-CODE
+ "x\\u2713y"
+ CODE
+ if Puppet::Pops::Parser::Locator::RUBYVER < Puppet::Pops::Parser::Locator::RUBY_1_9_3
+ # Ruby 1.8.7 reports the multibyte char as several octal characters
+ tokens_scanned_from(code).should match_tokens2([:STRING, "x\342\234\223y"])
+ else
+ # >= Ruby 1.9.3 reports \u
+ tokens_scanned_from(code).should match_tokens2([:STRING, "x\u2713y"])
+ end
+ end
+
+ context 'when lexing epp' do
+ it 'epp can contain just text' do
+ code = <<-CODE
+ This is just text
+ CODE
+ epp_tokens_scanned_from(code).should match_tokens2(:EPP_START, [:RENDER_STRING, " This is just text\n"])
+ end
+
+ it 'epp can contain text with interpolated rendered expressions' do
+ code = <<-CODE
+ This is <%= $x %> just text
+ CODE
+ epp_tokens_scanned_from(code).should match_tokens2(
+ :EPP_START,
+ [:RENDER_STRING, " This is "],
+ [:RENDER_EXPR, nil],
+ [:VARIABLE, "x"],
+ [:EPP_END, "%>"],
+ [:RENDER_STRING, " just text\n"]
+ )
+ end
+
+ it 'epp can contain text with trimmed interpolated rendered expressions' do
+ code = <<-CODE
+ This is <%= $x -%> just text
+ CODE
+ epp_tokens_scanned_from(code).should match_tokens2(
+ :EPP_START,
+ [:RENDER_STRING, " This is "],
+ [:RENDER_EXPR, nil],
+ [:VARIABLE, "x"],
+ [:EPP_END_TRIM, "-%>"],
+ [:RENDER_STRING, "just text\n"]
+ )
+ end
+
+ it 'epp can contain text with expressions that are not rendered' do
+ code = <<-CODE
+ This is <% $x=10 %> just text
+ CODE
+ epp_tokens_scanned_from(code).should match_tokens2(
+ :EPP_START,
+ [:RENDER_STRING, " This is "],
+ [:VARIABLE, "x"],
+ :EQUALS,
+ [:NUMBER, "10"],
+ [:RENDER_STRING, " just text\n"]
+ )
+ end
+
+ it 'epp can skip leading space in tail text' do
+ code = <<-CODE
+ This is <% $x=10 -%>
+ just text
+ CODE
+ epp_tokens_scanned_from(code).should match_tokens2(
+ :EPP_START,
+ [:RENDER_STRING, " This is "],
+ [:VARIABLE, "x"],
+ :EQUALS,
+ [:NUMBER, "10"],
+ [:RENDER_STRING, "just text\n"]
+ )
+ end
+
+ it 'epp can skip comments' do
+ code = <<-CODE
+ This is <% $x=10 -%>
+ <%# This is an epp comment -%>
+ just text
+ CODE
+ epp_tokens_scanned_from(code).should match_tokens2(
+ :EPP_START,
+ [:RENDER_STRING, " This is "],
+ [:VARIABLE, "x"],
+ :EQUALS,
+ [:NUMBER, "10"],
+ [:RENDER_STRING, "just text\n"]
+ )
+ end
+
+ it 'epp can escape epp tags' do
+ code = <<-CODE
+ This is <% $x=10 -%>
+ <%% this is escaped epp %%>
+ CODE
+ epp_tokens_scanned_from(code).should match_tokens2(
+ :EPP_START,
+ [:RENDER_STRING, " This is "],
+ [:VARIABLE, "x"],
+ :EQUALS,
+ [:NUMBER, "10"],
+ [:RENDER_STRING, "<% this is escaped epp %>\n"]
+ )
+ end
+ end
+end
+
diff --git a/spec/unit/pops/parser/lexer_spec.rb b/spec/unit/pops/parser/lexer_spec.rb
index 71010c033..40d9b3e51 100755
--- a/spec/unit/pops/parser/lexer_spec.rb
+++ b/spec/unit/pops/parser/lexer_spec.rb
@@ -1,901 +1,840 @@
#! /usr/bin/env ruby
require 'spec_helper'
require 'puppet/pops'
# This is a special matcher to match easily lexer output
RSpec::Matchers.define :be_like do |*expected|
match do |actual|
diffable
expected.zip(actual).all? { |e,a| !e or a[0] == e or (e.is_a? Array and a[0] == e[0] and (a[1] == e[1] or (a[1].is_a?(Hash) and a[1][:value] == e[1]))) }
end
end
__ = nil
module EgrammarLexerSpec
def self.tokens_scanned_from(s)
lexer = Puppet::Pops::Parser::Lexer.new
lexer.string = s
- lexer.fullscan[0..-2]
+ tokens = lexer.fullscan[0..-2]
+ tokens.map do |t|
+ key = t[0]
+ options = t[1]
+ if options[:locator]
+ # unresolved locations needs to be resolved for tests that check positioning
+ [key,
+ options[:locator].to_location_hash(
+ options[:offset],
+ options[:end_offset]).merge({:value => options[:value]}) ]
+ else
+ t
+ end
+ end
end
end
describe Puppet::Pops::Parser::Lexer do
include EgrammarLexerSpec
describe "when reading strings" do
before { @lexer = Puppet::Pops::Parser::Lexer.new }
it "should increment the line count for every carriage return in the string" do
@lexer.string = "'this\nis\natest'"
@lexer.fullscan[0..-2]
line = @lexer.line
line.should == 3
end
it "should not increment the line count for escapes in the string" do
@lexer.string = "'this\\nis\\natest'"
@lexer.fullscan[0..-2]
@lexer.line.should == 1
end
it "should not think the terminator is escaped, when preceeded by an even number of backslashes" do
@lexer.string = "'here\nis\nthe\nstring\\\\'with\nextra\njunk"
@lexer.fullscan[0..-2]
@lexer.line.should == 6
end
{
'r' => "\r",
'n' => "\n",
't' => "\t",
's' => " "
}.each do |esc, expected_result|
it "should recognize \\#{esc} sequence" do
@lexer.string = "\\#{esc}'"
@lexer.slurpstring("'")[0].should == expected_result
end
end
end
end
describe Puppet::Pops::Parser::Lexer::Token, "when initializing" do
it "should create a regex if the first argument is a string" do
Puppet::Pops::Parser::Lexer::Token.new("something", :NAME).regex.should == %r{something}
end
it "should set the string if the first argument is one" do
Puppet::Pops::Parser::Lexer::Token.new("something", :NAME).string.should == "something"
end
it "should set the regex if the first argument is one" do
Puppet::Pops::Parser::Lexer::Token.new(%r{something}, :NAME).regex.should == %r{something}
end
end
describe Puppet::Pops::Parser::Lexer::TokenList do
before do
@list = Puppet::Pops::Parser::Lexer::TokenList.new
end
it "should have a method for retrieving tokens by the name" do
token = @list.add_token :name, "whatever"
@list[:name].should equal(token)
end
it "should have a method for retrieving string tokens by the string" do
token = @list.add_token :name, "whatever"
@list.lookup("whatever").should equal(token)
end
it "should add tokens to the list when directed" do
token = @list.add_token :name, "whatever"
@list[:name].should equal(token)
end
it "should have a method for adding multiple tokens at once" do
@list.add_tokens "whatever" => :name, "foo" => :bar
@list[:name].should_not be_nil
@list[:bar].should_not be_nil
end
it "should fail to add tokens sharing a name with an existing token" do
@list.add_token :name, "whatever"
expect { @list.add_token :name, "whatever" }.to raise_error(ArgumentError)
end
it "should set provided options on tokens being added" do
token = @list.add_token :name, "whatever", :skip_text => true
token.skip_text.should == true
end
it "should define any provided blocks as a :convert method" do
token = @list.add_token(:name, "whatever") do "foo" end
token.convert.should == "foo"
end
it "should store all string tokens in the :string_tokens list" do
one = @list.add_token(:name, "1")
@list.string_tokens.should be_include(one)
end
it "should store all regex tokens in the :regex_tokens list" do
one = @list.add_token(:name, %r{one})
@list.regex_tokens.should be_include(one)
end
it "should not store string tokens in the :regex_tokens list" do
one = @list.add_token(:name, "1")
@list.regex_tokens.should_not be_include(one)
end
it "should not store regex tokens in the :string_tokens list" do
one = @list.add_token(:name, %r{one})
@list.string_tokens.should_not be_include(one)
end
it "should sort the string tokens inversely by length when asked" do
one = @list.add_token(:name, "1")
two = @list.add_token(:other, "12")
@list.sort_tokens
@list.string_tokens.should == [two, one]
end
end
describe Puppet::Pops::Parser::Lexer::TOKENS do
before do
@lexer = Puppet::Pops::Parser::Lexer.new
end
{
:LBRACK => '[',
:RBRACK => ']',
# :LBRACE => '{',
# :RBRACE => '}',
:LPAREN => '(',
:RPAREN => ')',
:EQUALS => '=',
:ISEQUAL => '==',
:GREATEREQUAL => '>=',
:GREATERTHAN => '>',
:LESSTHAN => '<',
:LESSEQUAL => '<=',
:NOTEQUAL => '!=',
:NOT => '!',
:COMMA => ',',
:DOT => '.',
:COLON => ':',
:AT => '@',
:LLCOLLECT => '<<|',
:RRCOLLECT => '|>>',
:LCOLLECT => '<|',
:RCOLLECT => '|>',
:SEMIC => ';',
:QMARK => '?',
:BACKSLASH => '\\',
:FARROW => '=>',
:PARROW => '+>',
:APPENDS => '+=',
+ :DELETES => '-=',
:PLUS => '+',
:MINUS => '-',
:DIV => '/',
:TIMES => '*',
:LSHIFT => '<<',
:RSHIFT => '>>',
:MATCH => '=~',
:NOMATCH => '!~',
:IN_EDGE => '->',
:OUT_EDGE => '<-',
:IN_EDGE_SUB => '~>',
:OUT_EDGE_SUB => '<~',
:PIPE => '|',
}.each do |name, string|
it "should have a token named #{name.to_s}" do
Puppet::Pops::Parser::Lexer::TOKENS[name].should_not be_nil
end
it "should match '#{string}' for the token #{name.to_s}" do
Puppet::Pops::Parser::Lexer::TOKENS[name].string.should == string
end
end
{
"case" => :CASE,
"class" => :CLASS,
"default" => :DEFAULT,
"define" => :DEFINE,
# "import" => :IMPORT, # done as a function in egrammar
"if" => :IF,
"elsif" => :ELSIF,
"else" => :ELSE,
"inherits" => :INHERITS,
"node" => :NODE,
"and" => :AND,
"or" => :OR,
"undef" => :UNDEF,
"false" => :FALSE,
"true" => :TRUE,
"in" => :IN,
"unless" => :UNLESS,
}.each do |string, name|
it "should have a keyword named #{name.to_s}" do
Puppet::Pops::Parser::Lexer::KEYWORDS[name].should_not be_nil
end
it "should have the keyword for #{name.to_s} set to #{string}" do
Puppet::Pops::Parser::Lexer::KEYWORDS[name].string.should == string
end
end
# These tokens' strings don't matter, just that the tokens exist.
[:STRING, :DQPRE, :DQMID, :DQPOST, :BOOLEAN, :NAME, :NUMBER, :COMMENT, :MLCOMMENT,
: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
end
end
end
describe Puppet::Pops::Parser::Lexer::TOKENS[:CLASSREF] do
before { @token = Puppet::Pops::Parser::Lexer::TOKENS[:CLASSREF] }
it "should match against single upper-case alpha-numeric terms" do
@token.regex.should =~ "One"
end
it "should match against upper-case alpha-numeric terms separated by double colons" do
@token.regex.should =~ "One::Two"
end
it "should match against many upper-case alpha-numeric terms separated by double colons" do
@token.regex.should =~ "One::Two::Three::Four::Five"
end
it "should match against upper-case alpha-numeric terms prefixed by double colons" do
@token.regex.should =~ "::One"
end
end
describe Puppet::Pops::Parser::Lexer::TOKENS[:NAME] do
before { @token = Puppet::Pops::Parser::Lexer::TOKENS[:NAME] }
it "should match against lower-case alpha-numeric terms" do
@token.regex.should =~ "one-two"
end
it "should return itself and the value if the matched term is not a keyword" do
Puppet::Pops::Parser::Lexer::KEYWORDS.expects(:lookup).returns(nil)
@token.convert(stub("lexer"), "myval").should == [Puppet::Pops::Parser::Lexer::TOKENS[:NAME], "myval"]
end
it "should return the keyword token and the value if the matched term is a keyword" do
keyword = stub 'keyword', :name => :testing
Puppet::Pops::Parser::Lexer::KEYWORDS.expects(:lookup).returns(keyword)
@token.convert(stub("lexer"), "myval").should == [keyword, "myval"]
end
it "should return the BOOLEAN token and 'true' if the matched term is the string 'true'" do
keyword = stub 'keyword', :name => :TRUE
Puppet::Pops::Parser::Lexer::KEYWORDS.expects(:lookup).returns(keyword)
@token.convert(stub('lexer'), "true").should == [Puppet::Pops::Parser::Lexer::TOKENS[:BOOLEAN], true]
end
it "should return the BOOLEAN token and 'false' if the matched term is the string 'false'" do
keyword = stub 'keyword', :name => :FALSE
Puppet::Pops::Parser::Lexer::KEYWORDS.expects(:lookup).returns(keyword)
@token.convert(stub('lexer'), "false").should == [Puppet::Pops::Parser::Lexer::TOKENS[:BOOLEAN], false]
end
it "should match against lower-case alpha-numeric terms separated by double colons" do
@token.regex.should =~ "one::two"
end
it "should match against many lower-case alpha-numeric terms separated by double colons" do
@token.regex.should =~ "one::two::three::four::five"
end
it "should match against lower-case alpha-numeric terms prefixed by double colons" do
@token.regex.should =~ "::one"
end
it "should match against nested terms starting with numbers" do
@token.regex.should =~ "::1one::2two::3three"
end
end
describe Puppet::Pops::Parser::Lexer::TOKENS[:NUMBER] do
before do
@token = Puppet::Pops::Parser::Lexer::TOKENS[:NUMBER]
@regex = @token.regex
end
it "should match against numeric terms" do
@regex.should =~ "2982383139"
end
it "should match against float terms" do
@regex.should =~ "29823.235"
end
it "should match against hexadecimal terms" do
@regex.should =~ "0xBEEF0023"
end
it "should match against float with exponent terms" do
@regex.should =~ "10e23"
end
it "should match against float terms with negative exponents" do
@regex.should =~ "10e-23"
end
it "should match against float terms with fractional parts and exponent" do
@regex.should =~ "1.234e23"
end
end
describe Puppet::Pops::Parser::Lexer::TOKENS[:COMMENT] do
before { @token = Puppet::Pops::Parser::Lexer::TOKENS[:COMMENT] }
it "should match against lines starting with '#'" do
@token.regex.should =~ "# this is a comment"
end
it "should be marked to get skipped" do
@token.skip?.should be_true
end
- it "'s block should return the comment without the #" do
- @token.convert(@lexer,"# this is a comment")[1].should == "this is a comment"
+ it "'s block should return the comment without any text" do
+ # This is a silly test, the original tested that the comments was processed, but
+ # all comments are skipped anyway, and never collected for documentation.
+ #
+ @token.convert(@lexer,"# this is a comment")[1].should == ""
end
end
describe Puppet::Pops::Parser::Lexer::TOKENS[:MLCOMMENT] do
before do
@token = Puppet::Pops::Parser::Lexer::TOKENS[:MLCOMMENT]
@lexer = stub 'lexer', :line => 0
end
it "should match against lines enclosed with '/*' and '*/'" do
@token.regex.should =~ "/* this is a comment */"
end
it "should match multiple lines enclosed with '/*' and '*/'" do
@token.regex.should =~ """/*
this is a comment
*/"""
end
# # TODO: REWRITE THIS TEST TO NOT BE BASED ON INTERNALS
# it "should increase the lexer current line number by the amount of lines spanned by the comment" do
# @lexer.expects(:line=).with(2)
# @token.convert(@lexer, "1\n2\n3")
# end
it "should not greedily match comments" do
match = @token.regex.match("/* first */ word /* second */")
match[1].should == " first "
end
it "'s block should return the comment without the comment marks" do
+ # This is a silly test, the original tested that the comments was processed, but
+ # all comments are skipped anyway, and never collected for documentation.
+ #
@lexer.stubs(:line=).with(0)
- @token.convert(@lexer,"/* this is a comment */")[1].should == "this is a comment"
+ @token.convert(@lexer,"/* this is a comment */")[1].should == ""
end
end
describe Puppet::Pops::Parser::Lexer::TOKENS[:RETURN] do
before { @token = Puppet::Pops::Parser::Lexer::TOKENS[:RETURN] }
it "should match against carriage returns" do
@token.regex.should =~ "\n"
end
it "should be marked to initiate text skipping" do
@token.skip_text.should be_true
end
end
shared_examples_for "handling `-` in standard variable names for egrammar" do |prefix|
# Watch out - a regex might match a *prefix* on these, not just the whole
# word, so make sure you don't have false positive or negative results based
# on that.
legal = %w{f foo f::b foo::b f::bar foo::bar 3 foo3 3foo}
illegal = %w{f- f-o -f f::-o f::o- f::o-o}
["", "::"].each do |global_scope|
legal.each do |name|
var = prefix + global_scope + name
it "should accept #{var.inspect} as a valid variable name" do
(subject.regex.match(var) || [])[0].should == var
end
end
illegal.each do |name|
var = prefix + global_scope + name
it "when `variable_with_dash` is disabled it should NOT accept #{var.inspect} as a valid variable name" do
Puppet[:allow_variables_with_dashes] = false
(subject.regex.match(var) || [])[0].should_not == var
end
it "when `variable_with_dash` is enabled it should NOT accept #{var.inspect} as a valid variable name" do
Puppet[:allow_variables_with_dashes] = true
(subject.regex.match(var) || [])[0].should_not == var
end
end
end
end
describe Puppet::Pops::Parser::Lexer::TOKENS[:DOLLAR_VAR] do
its(:skip_text) { should be_false }
it_should_behave_like "handling `-` in standard variable names for egrammar", '$'
end
describe Puppet::Pops::Parser::Lexer::TOKENS[:VARIABLE] do
its(:skip_text) { should be_false }
it_should_behave_like "handling `-` in standard variable names for egrammar", ''
end
describe "the horrible deprecation / compatibility variables with dashes" do
- ENamesWithDashes = %w{f- f-o -f f::-o f::o- f::o-o}
-
- { Puppet::Pops::Parser::Lexer::TOKENS[:DOLLAR_VAR_WITH_DASH] => '$',
- Puppet::Pops::Parser::Lexer::TOKENS[:VARIABLE_WITH_DASH] => ''
- }.each do |token, prefix|
- describe token do
- its(:skip_text) { should be_false }
-
- context "when compatibly is disabled" do
- before :each do Puppet[:allow_variables_with_dashes] = false end
- Puppet::Pops::Parser::Lexer::TOKENS.each do |name, value|
- it "should be unacceptable after #{name}" do
- token.acceptable?(:after => name).should be_false
- end
- end
-
- # Yes, this should still *match*, just not be acceptable.
- ENamesWithDashes.each do |name|
- ["", "::"].each do |global_scope|
- var = prefix + global_scope + name
- it "should match #{var.inspect}" do
- subject.regex.match(var).to_a.should == [var]
- end
- end
- end
- end
-
- context "when compatibility is enabled" do
- before :each do Puppet[:allow_variables_with_dashes] = true end
-
- it "should be acceptable after DQPRE" do
- token.acceptable?(:after => :DQPRE).should be_true
- end
-
- ENamesWithDashes.each do |name|
- ["", "::"].each do |global_scope|
- var = prefix + global_scope + name
- it "should match #{var.inspect}" do
- subject.regex.match(var).to_a.should == [var]
- end
- end
- end
- end
- end
- end
context "deprecation warnings" do
before :each do Puppet[:allow_variables_with_dashes] = true end
- it "should match a top level variable" do
- Puppet.expects(:deprecation_warning).once
-
- EgrammarLexerSpec.tokens_scanned_from('$foo-bar').should == [
- [:VARIABLE, {:value=>"foo-bar", :line=>1, :pos=>1, :offset=>0, :length=>8}]
- ]
- end
-
it "does not warn about a variable without a dash" do
Puppet.expects(:deprecation_warning).never
EgrammarLexerSpec.tokens_scanned_from('$c').should == [
[:VARIABLE, {:value=>"c", :line=>1, :pos=>1, :offset=>0, :length=>2}]
]
end
it "does not warn about referencing a class name that contains a dash" do
Puppet.expects(:deprecation_warning).never
EgrammarLexerSpec.tokens_scanned_from('foo-bar').should == [
[:NAME, {:value=>"foo-bar", :line=>1, :pos=>1, :offset=>0, :length=>7}]
]
end
-
- it "warns about reference to variable" do
- Puppet.expects(:deprecation_warning).once
-
- EgrammarLexerSpec.tokens_scanned_from('$::foo-bar::baz-quux').should == [
- [:VARIABLE, {:value=>"::foo-bar::baz-quux", :line=>1, :pos=>1, :offset=>0, :length=>20}]
- ]
- end
-
- it "warns about reference to variable interpolated in a string" do
- Puppet.expects(:deprecation_warning).once
-
- EgrammarLexerSpec.tokens_scanned_from('"$::foo-bar::baz-quux"').should == [
- [:DQPRE, {:value=>"", :line=>1, :pos=>1, :offset=>0, :length=>2}], # length since preamble includes start and terminator
- [:VARIABLE, {:value=>"::foo-bar::baz-quux", :line=>1, :pos=>3, :offset=>2, :length=>19}],
- [:DQPOST, {:value=>"", :line=>1, :pos=>22, :offset=>21, :length=>1}],
- ]
- end
-
- it "warns about reference to variable interpolated in a string as an expression" do
- Puppet.expects(:deprecation_warning).once
-
- EgrammarLexerSpec.tokens_scanned_from('"${::foo-bar::baz-quux}"').should == [
- [:DQPRE, {:value=>"", :line=>1, :pos=>1, :offset=>0, :length=>3}],
- [:VARIABLE, {:value=>"::foo-bar::baz-quux", :line=>1, :pos=>4, :offset=>3, :length=>19}],
- [:DQPOST, {:value=>"", :line=>1, :pos=>23, :offset=>22, :length=>2}],
- ]
- end
end
end
describe Puppet::Pops::Parser::Lexer,"when lexing strings" do
{
%q{'single quoted string')} => [[:STRING,'single quoted string']],
%q{"double quoted string"} => [[:STRING,'double quoted string']],
%q{'single quoted string with an escaped "\\'"'} => [[:STRING,'single quoted string with an escaped "\'"']],
%q{'single quoted string with an escaped "\$"'} => [[:STRING,'single quoted string with an escaped "\$"']],
%q{'single quoted string with an escaped "\."'} => [[:STRING,'single quoted string with an escaped "\."']],
%q{'single quoted string with an escaped "\r\n"'} => [[:STRING,'single quoted string with an escaped "\r\n"']],
%q{'single quoted string with an escaped "\n"'} => [[:STRING,'single quoted string with an escaped "\n"']],
%q{'single quoted string with an escaped "\\\\"'} => [[:STRING,'single quoted string with an escaped "\\\\"']],
%q{"string with an escaped '\\"'"} => [[:STRING,"string with an escaped '\"'"]],
%q{"string with an escaped '\\$'"} => [[:STRING,"string with an escaped '$'"]],
%Q{"string with a line ending with a backslash: \\\nfoo"} => [[:STRING,"string with a line ending with a backslash: foo"]],
%q{"string with $v (but no braces)"} => [[:DQPRE,"string with "],[:VARIABLE,'v'],[:DQPOST,' (but no braces)']],
%q["string with ${v} in braces"] => [[:DQPRE,"string with "],[:VARIABLE,'v'],[:DQPOST,' in braces']],
%q["string with ${qualified::var} in braces"] => [[:DQPRE,"string with "],[:VARIABLE,'qualified::var'],[:DQPOST,' in braces']],
%q{"string with $v and $v (but no braces)"} => [[:DQPRE,"string with "],[:VARIABLE,"v"],[:DQMID," and "],[:VARIABLE,"v"],[:DQPOST," (but no braces)"]],
%q["string with ${v} and ${v} in braces"] => [[:DQPRE,"string with "],[:VARIABLE,"v"],[:DQMID," and "],[:VARIABLE,"v"],[:DQPOST," in braces"]],
%q["string with ${'a nested single quoted string'} inside it."] => [[:DQPRE,"string with "],[:STRING,'a nested single quoted string'],[:DQPOST,' inside it.']],
%q["string with ${['an array ',$v2]} in it."] => [[:DQPRE,"string with "],:LBRACK,[:STRING,"an array "],:COMMA,[:VARIABLE,"v2"],:RBRACK,[:DQPOST," in it."]],
%q{a simple "scanner" test} => [[:NAME,"a"],[:NAME,"simple"], [:STRING,"scanner"],[:NAME,"test"]],
%q{a simple 'single quote scanner' test} => [[:NAME,"a"],[:NAME,"simple"], [:STRING,"single quote scanner"],[:NAME,"test"]],
%q{a harder 'a $b \c"'} => [[:NAME,"a"],[:NAME,"harder"], [:STRING,'a $b \c"']],
%q{a harder "scanner test"} => [[:NAME,"a"],[:NAME,"harder"], [:STRING,"scanner test"]],
%q{a hardest "scanner \"test\""} => [[:NAME,"a"],[:NAME,"hardest"],[:STRING,'scanner "test"']],
%Q{a hardestest "scanner \\"test\\"\n"} => [[:NAME,"a"],[:NAME,"hardestest"],[:STRING,%Q{scanner "test"\n}]],
%q{function("call")} => [[:NAME,"function"],[:LPAREN,"("],[:STRING,'call'],[:RPAREN,")"]],
%q["string with ${(3+5)/4} nested math."] => [[:DQPRE,"string with "],:LPAREN,[:NAME,"3"],:PLUS,[:NAME,"5"],:RPAREN,:DIV,[:NAME,"4"],[:DQPOST," nested math."]],
%q["$$$$"] => [[:STRING,"$$$$"]],
%q["$variable"] => [[:DQPRE,""],[:VARIABLE,"variable"],[:DQPOST,""]],
%q["$var$other"] => [[:DQPRE,""],[:VARIABLE,"var"],[:DQMID,""],[:VARIABLE,"other"],[:DQPOST,""]],
%q["foo$bar$"] => [[:DQPRE,"foo"],[:VARIABLE,"bar"],[:DQPOST,"$"]],
%q["foo$$bar"] => [[:DQPRE,"foo$"],[:VARIABLE,"bar"],[:DQPOST,""]],
%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"]],
# 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)
end
}
end
describe Puppet::Pops::Parser::Lexer::TOKENS[:DOLLAR_VAR] do
before { @token = Puppet::Pops::Parser::Lexer::TOKENS[:DOLLAR_VAR] }
it "should match against alpha words prefixed with '$'" do
@token.regex.should =~ '$this_var'
end
it "should return the VARIABLE token and the variable name stripped of the '$'" do
@token.convert(stub("lexer"), "$myval").should == [Puppet::Pops::Parser::Lexer::TOKENS[:VARIABLE], "myval"]
end
end
describe Puppet::Pops::Parser::Lexer::TOKENS[:REGEX] do
before { @token = Puppet::Pops::Parser::Lexer::TOKENS[:REGEX] }
it "should match against any expression enclosed in //" do
@token.regex.should =~ '/this is a regex/'
end
it 'should not match if there is \n in the regex' do
@token.regex.should_not =~ "/this is \n a regex/"
end
describe "when scanning" do
it "should not consider escaped slashes to be the end of a regex" do
EgrammarLexerSpec.tokens_scanned_from("$x =~ /this \\/ foo/").should be_like(__,__,[:REGEX,%r{this / foo}])
end
it "should not lex chained division as a regex" do
EgrammarLexerSpec.tokens_scanned_from("$x = $a/$b/$c").collect { |name, data| name }.should_not be_include( :REGEX )
end
it "should accept a regular expression after NODE" do
EgrammarLexerSpec.tokens_scanned_from("node /www.*\.mysite\.org/").should be_like(__,[:REGEX,Regexp.new("www.*\.mysite\.org")])
end
it "should accept regular expressions in a CASE" do
s = %q{case $variable {
"something": {$othervar = 4096 / 2}
/regex/: {notice("this notably sucks")}
}
}
EgrammarLexerSpec.tokens_scanned_from(s).should be_like(
:CASE,:VARIABLE,:LBRACE,:STRING,:COLON,:LBRACE,:VARIABLE,:EQUALS,:NAME,:DIV,:NAME,:RBRACE,[:REGEX,/regex/],:COLON,:LBRACE,:NAME,:LPAREN,:STRING,:RPAREN,:RBRACE,:RBRACE
)
end
end
it "should return the REGEX token and a Regexp" do
@token.convert(stub("lexer"), "/myregex/").should == [Puppet::Pops::Parser::Lexer::TOKENS[:REGEX], Regexp.new(/myregex/)]
end
end
describe Puppet::Pops::Parser::Lexer, "when lexing comments" do
before { @lexer = Puppet::Pops::Parser::Lexer.new }
it "should skip whitespace before lexing the next token after a non-token" do
EgrammarLexerSpec.tokens_scanned_from("/* 1\n\n */ \ntest").should be_like([:NAME, "test"])
end
end
# FIXME: We need to rewrite all of these tests, but I just don't want to take the time right now.
describe "Puppet::Pops::Parser::Lexer in the old tests" do
before { @lexer = Puppet::Pops::Parser::Lexer.new }
it "should do simple lexing" do
{
%q{\\} => [[:BACKSLASH,"\\"]],
%q{simplest scanner test} => [[:NAME,"simplest"],[:NAME,"scanner"],[:NAME,"test"]],
%Q{returned scanner test\n} => [[:NAME,"returned"],[:NAME,"scanner"],[:NAME,"test"]]
}.each { |source,expected|
EgrammarLexerSpec.tokens_scanned_from(source).should be_like(*expected)
}
end
it "should fail usefully" do
expect { EgrammarLexerSpec.tokens_scanned_from('^') }.to raise_error(RuntimeError)
end
it "should fail if the string is not set" do
expect { @lexer.fullscan }.to raise_error(Puppet::LexError)
end
it "should correctly identify keywords" do
EgrammarLexerSpec.tokens_scanned_from("case").should be_like([:CASE, "case"])
end
it "should correctly parse class references" do
%w{Many Different Words A Word}.each { |t| EgrammarLexerSpec.tokens_scanned_from(t).should be_like([:CLASSREF,t])}
end
# #774
it "should correctly parse namespaced class refernces token" do
%w{Foo ::Foo Foo::Bar ::Foo::Bar}.each { |t| EgrammarLexerSpec.tokens_scanned_from(t).should be_like([:CLASSREF, t]) }
end
it "should correctly parse names" do
%w{this is a bunch of names}.each { |t| EgrammarLexerSpec.tokens_scanned_from(t).should be_like([:NAME,t]) }
end
it "should correctly parse names with numerals" do
%w{1name name1 11names names11}.each { |t| EgrammarLexerSpec.tokens_scanned_from(t).should be_like([:NAME,t]) }
end
it "should correctly parse empty strings" do
expect { EgrammarLexerSpec.tokens_scanned_from('$var = ""') }.to_not raise_error
end
it "should correctly parse virtual resources" do
EgrammarLexerSpec.tokens_scanned_from("@type {").should be_like([:AT, "@"], [:NAME, "type"], [:LBRACE, "{"])
end
it "should correctly deal with namespaces" do
@lexer.string = %{class myclass}
@lexer.fullscan
@lexer.namespace.should == "myclass"
@lexer.namepop
@lexer.namespace.should == ""
@lexer.string = "class base { class sub { class more"
@lexer.fullscan
@lexer.namespace.should == "base::sub::more"
@lexer.namepop
@lexer.namespace.should == "base::sub"
end
it "should not put class instantiation on the namespace" do
@lexer.string = "class base { class sub { class { mode"
@lexer.fullscan
@lexer.namespace.should == "base::sub"
end
it "should correctly handle fully qualified names" do
@lexer.string = "class base { class sub::more {"
@lexer.fullscan
@lexer.namespace.should == "base::sub::more"
@lexer.namepop
@lexer.namespace.should == "base"
end
it "should correctly lex variables" do
["$variable", "$::variable", "$qualified::variable", "$further::qualified::variable"].each do |string|
EgrammarLexerSpec.tokens_scanned_from(string).should be_like([:VARIABLE,string.sub(/^\$/,'')])
end
end
it "should end variables at `-`" do
EgrammarLexerSpec.tokens_scanned_from('$hyphenated-variable').
should be_like([:VARIABLE, "hyphenated"], [:MINUS, '-'], [:NAME, 'variable'])
end
it "should not include whitespace in a variable" do
EgrammarLexerSpec.tokens_scanned_from("$foo bar").should_not be_like([:VARIABLE, "foo bar"])
end
it "should not include excess colons in a variable" do
EgrammarLexerSpec.tokens_scanned_from("$foo::::bar").should_not be_like([:VARIABLE, "foo::::bar"])
end
end
describe "Puppet::Pops::Parser::Lexer in the old tests when lexing example files" do
my_fixtures('*.pp') do |file|
it "should correctly lex #{file}" do
lexer = Puppet::Pops::Parser::Lexer.new
lexer.file = file
expect { lexer.fullscan }.to_not raise_error
end
end
end
describe "when trying to lex a non-existent file" do
include PuppetSpec::Files
it "should return an empty list of tokens" do
lexer = Puppet::Pops::Parser::Lexer.new
lexer.file = nofile = tmpfile('lexer')
- Puppet::FileSystem::File.exist?(nofile).should == false
+ Puppet::FileSystem.exist?(nofile).should == false
lexer.fullscan.should == [[false,false]]
end
end
describe "when string quotes are not closed" do
it "should report with message including an \" opening quote" do
expect { EgrammarLexerSpec.tokens_scanned_from('$var = "') }.to raise_error(/after '"'/)
end
it "should report with message including an \' opening quote" do
expect { EgrammarLexerSpec.tokens_scanned_from('$var = \'') }.to raise_error(/after "'"/)
end
it "should report <eof> if immediately followed by eof" do
expect { EgrammarLexerSpec.tokens_scanned_from('$var = "') }.to raise_error(/followed by '<eof>'/)
end
it "should report max 5 chars following quote" do
expect { EgrammarLexerSpec.tokens_scanned_from('$var = "123456') }.to raise_error(/followed by '12345...'/)
end
it "should escape control chars" do
expect { EgrammarLexerSpec.tokens_scanned_from('$var = "12\n3456') }.to raise_error(/followed by '12\\n3...'/)
end
it "should resport position of opening quote" do
expect { EgrammarLexerSpec.tokens_scanned_from('$var = "123456') }.to raise_error(/at line 1:8/)
expect { EgrammarLexerSpec.tokens_scanned_from('$var = "123456') }.to raise_error(/at line 1:9/)
end
end
describe "when lexing number, bad input should not go unpunished" do
it "should slap bad octal as such" do
expect { EgrammarLexerSpec.tokens_scanned_from('$var = 0778') }.to raise_error(/Not a valid octal/)
end
it "should slap bad hex as such" do
expect { EgrammarLexerSpec.tokens_scanned_from('$var = 0xFG') }.to raise_error(/Not a valid hex/)
expect { EgrammarLexerSpec.tokens_scanned_from('$var = 0xfg') }.to raise_error(/Not a valid hex/)
end
# Note, bad decimals are probably impossible to enter, as they are not recognized as complete numbers, instead,
# the error will be something else, depending on what follows some initial digit.
#
end
describe "when lexing interpolation detailed positioning should be correct" do
it "should correctly position a string without interpolation" do
EgrammarLexerSpec.tokens_scanned_from('"not interpolated"').should be_like(
[:STRING, {:value=>"not interpolated", :line=>1, :offset=>0, :pos=>1, :length=>18}])
end
it "should correctly position a string with false start in interpolation" do
EgrammarLexerSpec.tokens_scanned_from('"not $$$ rpolated"').should be_like(
[:STRING, {:value=>"not $$$ rpolated", :line=>1, :offset=>0, :pos=>1, :length=>18}])
end
it "should correctly position pre-mid-end interpolation " do
EgrammarLexerSpec.tokens_scanned_from('"pre $x mid $y end"').should be_like(
[:DQPRE, {:value=>"pre ", :line=>1, :offset=>0, :pos=>1, :length=>6}],
[:VARIABLE, {:value=>"x", :line=>1, :offset=>6, :pos=>7, :length=>1}],
[:DQMID, {:value=>" mid ", :line=>1, :offset=>7, :pos=>8, :length=>6}],
[:VARIABLE, {:value=>"y", :line=>1, :offset=>13, :pos=>14, :length=>1}],
[:DQPOST, {:value=>" end", :line=>1, :offset=>14, :pos=>15, :length=>5}]
)
end
it "should correctly position pre-mid-end interpolation using ${} " do
EgrammarLexerSpec.tokens_scanned_from('"pre ${x} mid ${y} end"').should be_like(
[:DQPRE, {:value=>"pre ", :line=>1, :offset=>0, :pos=>1, :length=>7}],
[:VARIABLE, {:value=>"x", :line=>1, :offset=>7, :pos=>8, :length=>1}],
[:DQMID, {:value=>" mid ", :line=>1, :offset=>8, :pos=>9, :length=>8}],
[:VARIABLE, {:value=>"y", :line=>1, :offset=>16, :pos=>17, :length=>1}],
[:DQPOST, {:value=>" end", :line=>1, :offset=>17, :pos=>18, :length=>6}]
)
end
it "should correctly position pre-end interpolation using ${} with f call" do
EgrammarLexerSpec.tokens_scanned_from('"pre ${x()} end"').should be_like(
[:DQPRE, {:value=>"pre ", :line=>1, :offset=>0, :pos=>1, :length=>7}],
[:NAME, {:value=>"x", :line=>1, :offset=>7, :pos=>8, :length=>1}],
[:LPAREN, {:value=>"(", :line=>1, :offset=>8, :pos=>9, :length=>1}],
[:RPAREN, {:value=>")", :line=>1, :offset=>9, :pos=>10, :length=>1}],
[:DQPOST, {:value=>" end", :line=>1, :offset=>10, :pos=>11, :length=>6}]
)
end
it "should correctly position pre-end interpolation using ${} with $x" do
EgrammarLexerSpec.tokens_scanned_from('"pre ${$x} end"').should be_like(
[:DQPRE, {:value=>"pre ", :line=>1, :offset=>0, :pos=>1, :length=>7}],
[:VARIABLE, {:value=>"x", :line=>1, :offset=>7, :pos=>8, :length=>2}],
[:DQPOST, {:value=>" end", :line=>1, :offset=>9, :pos=>10, :length=>6}]
)
end
it "should correctly position pre-end interpolation across lines" do
EgrammarLexerSpec.tokens_scanned_from(%Q["pre ${\n$x} end"]).should be_like(
[:DQPRE, {:value=>"pre ", :line=>1, :offset=>0, :pos=>1, :length=>7}],
[:VARIABLE, {:value=>"x", :line=>2, :offset=>8, :pos=>1, :length=>2}],
[:DQPOST, {:value=>" end", :line=>2, :offset=>10, :pos=>3, :length=>6}]
)
end
it "should correctly position interpolation across lines when strings have embedded newlines" do
EgrammarLexerSpec.tokens_scanned_from(%Q["pre \n\n${$x}\n mid$y"]).should be_like(
[:DQPRE, {:value=>"pre \n\n", :line=>1, :offset=>0, :pos=>1, :length=>9}],
[:VARIABLE, {:value=>"x", :line=>3, :offset=>9, :pos=>3, :length=>2}],
[:DQMID, {:value=>"\n mid", :line=>3, :offset=>11, :pos=>5, :length=>7}],
[:VARIABLE, {:value=>"y", :line=>4, :offset=>18, :pos=>6, :length=>1}]
)
end
end
diff --git a/spec/unit/pops/parser/parse_basic_expressions_spec.rb b/spec/unit/pops/parser/parse_basic_expressions_spec.rb
index 6560e62e7..f5aeb2a29 100644
--- a/spec/unit/pops/parser/parse_basic_expressions_spec.rb
+++ b/spec/unit/pops/parser/parse_basic_expressions_spec.rb
@@ -1,248 +1,273 @@
#! /usr/bin/env ruby
require 'spec_helper'
require 'puppet/pops'
# relative to this spec file (./) does not work as this file is loaded by rspec
require File.join(File.dirname(__FILE__), '/parser_rspec_helper')
describe "egrammar parsing basic expressions" do
include ParserRspecHelper
context "When the parser parses arithmetic" do
context "with Integers" do
it "$a = 2 + 2" do; dump(parse("$a = 2 + 2")).should == "(= $a (+ 2 2))" ; end
it "$a = 7 - 3" do; dump(parse("$a = 7 - 3")).should == "(= $a (- 7 3))" ; end
it "$a = 6 * 3" do; dump(parse("$a = 6 * 3")).should == "(= $a (* 6 3))" ; end
it "$a = 6 / 3" do; dump(parse("$a = 6 / 3")).should == "(= $a (/ 6 3))" ; end
it "$a = 6 % 3" do; dump(parse("$a = 6 % 3")).should == "(= $a (% 6 3))" ; end
it "$a = -(6/3)" do; dump(parse("$a = -(6/3)")).should == "(= $a (- (/ 6 3)))" ; end
it "$a = -6/3" do; dump(parse("$a = -6/3")).should == "(= $a (/ (- 6) 3))" ; end
it "$a = 8 >> 1 " do; dump(parse("$a = 8 >> 1")).should == "(= $a (>> 8 1))" ; end
it "$a = 8 << 1 " do; dump(parse("$a = 8 << 1")).should == "(= $a (<< 8 1))" ; end
end
context "with Floats" do
it "$a = 2.2 + 2.2" do; dump(parse("$a = 2.2 + 2.2")).should == "(= $a (+ 2.2 2.2))" ; end
it "$a = 7.7 - 3.3" do; dump(parse("$a = 7.7 - 3.3")).should == "(= $a (- 7.7 3.3))" ; end
it "$a = 6.1 * 3.1" do; dump(parse("$a = 6.1 - 3.1")).should == "(= $a (- 6.1 3.1))" ; end
it "$a = 6.6 / 3.3" do; dump(parse("$a = 6.6 / 3.3")).should == "(= $a (/ 6.6 3.3))" ; end
it "$a = -(6.0/3.0)" do; dump(parse("$a = -(6.0/3.0)")).should == "(= $a (- (/ 6.0 3.0)))" ; end
it "$a = -6.0/3.0" do; dump(parse("$a = -6.0/3.0")).should == "(= $a (/ (- 6.0) 3.0))" ; end
it "$a = 3.14 << 2" do; dump(parse("$a = 3.14 << 2")).should == "(= $a (<< 3.14 2))" ; end
it "$a = 3.14 >> 2" do; dump(parse("$a = 3.14 >> 2")).should == "(= $a (>> 3.14 2))" ; end
end
context "with hex and octal Integer values" do
it "$a = 0xAB + 0xCD" do; dump(parse("$a = 0xAB + 0xCD")).should == "(= $a (+ 0xAB 0xCD))" ; end
it "$a = 0777 - 0333" do; dump(parse("$a = 0777 - 0333")).should == "(= $a (- 0777 0333))" ; end
end
context "with strings requiring boxing to Numeric" do
# Test that numbers in string form does not turn into numbers
it "$a = '2' + '2'" do; dump(parse("$a = '2' + '2'")).should == "(= $a (+ '2' '2'))" ; end
it "$a = '2.2' + '0.2'" do; dump(parse("$a = '2.2' + '0.2'")).should == "(= $a (+ '2.2' '0.2'))" ; end
it "$a = '0xab' + '0xcd'" do; dump(parse("$a = '0xab' + '0xcd'")).should == "(= $a (+ '0xab' '0xcd'))" ; end
it "$a = '0777' + '0333'" do; dump(parse("$a = '0777' + '0333'")).should == "(= $a (+ '0777' '0333'))" ; end
end
context "precedence should be correct" do
it "$a = 1 + 2 * 3" do; dump(parse("$a = 1 + 2 * 3")).should == "(= $a (+ 1 (* 2 3)))"; end
it "$a = 1 + 2 % 3" do; dump(parse("$a = 1 + 2 % 3")).should == "(= $a (+ 1 (% 2 3)))"; end
it "$a = 1 + 2 / 3" do; dump(parse("$a = 1 + 2 / 3")).should == "(= $a (+ 1 (/ 2 3)))"; end
it "$a = 1 + 2 << 3" do; dump(parse("$a = 1 + 2 << 3")).should == "(= $a (<< (+ 1 2) 3))"; end
it "$a = 1 + 2 >> 3" do; dump(parse("$a = 1 + 2 >> 3")).should == "(= $a (>> (+ 1 2) 3))"; end
end
context "parentheses alter precedence" do
it "$a = (1 + 2) * 3" do; dump(parse("$a = (1 + 2) * 3")).should == "(= $a (* (+ 1 2) 3))"; end
it "$a = (1 + 2) / 3" do; dump(parse("$a = (1 + 2) / 3")).should == "(= $a (/ (+ 1 2) 3))"; end
end
end
context "When the evaluator performs boolean operations" do
context "using operators AND OR NOT" do
it "$a = true and true" do; dump(parse("$a = true and true")).should == "(= $a (&& true true))"; end
it "$a = true or true" do; dump(parse("$a = true or true")).should == "(= $a (|| true true))" ; end
it "$a = !true" do; dump(parse("$a = !true")).should == "(= $a (! true))" ; end
end
context "precedence should be correct" do
it "$a = false or true and true" do
dump(parse("$a = false or true and true")).should == "(= $a (|| false (&& true true)))"
end
it "$a = (false or true) and true" do
dump(parse("$a = (false or true) and true")).should == "(= $a (&& (|| false true) true))"
end
it "$a = !true or true and true" do
dump(parse("$a = !false or true and true")).should == "(= $a (|| (! false) (&& true true)))"
end
end
# Possibly change to check of literal expressions
context "on values requiring boxing to Boolean" do
it "'x' == true" do
dump(parse("! 'x'")).should == "(! 'x')"
end
it "'' == false" do
dump(parse("! ''")).should == "(! '')"
end
it ":undef == false" do
dump(parse("! undef")).should == "(! :undef)"
end
end
end
context "When parsing comparisons" do
context "of string values" do
it "$a = 'a' == 'a'" do; dump(parse("$a = 'a' == 'a'")).should == "(= $a (== 'a' 'a'))" ; end
it "$a = 'a' != 'a'" do; dump(parse("$a = 'a' != 'a'")).should == "(= $a (!= 'a' 'a'))" ; end
it "$a = 'a' < 'b'" do; dump(parse("$a = 'a' < 'b'")).should == "(= $a (< 'a' 'b'))" ; end
it "$a = 'a' > 'b'" do; dump(parse("$a = 'a' > 'b'")).should == "(= $a (> 'a' 'b'))" ; end
it "$a = 'a' <= 'b'" do; dump(parse("$a = 'a' <= 'b'")).should == "(= $a (<= 'a' 'b'))" ; end
it "$a = 'a' >= 'b'" do; dump(parse("$a = 'a' >= 'b'")).should == "(= $a (>= 'a' 'b'))" ; end
end
context "of integer values" do
it "$a = 1 == 1" do; dump(parse("$a = 1 == 1")).should == "(= $a (== 1 1))" ; end
it "$a = 1 != 1" do; dump(parse("$a = 1 != 1")).should == "(= $a (!= 1 1))" ; end
it "$a = 1 < 2" do; dump(parse("$a = 1 < 2")).should == "(= $a (< 1 2))" ; end
it "$a = 1 > 2" do; dump(parse("$a = 1 > 2")).should == "(= $a (> 1 2))" ; end
it "$a = 1 <= 2" do; dump(parse("$a = 1 <= 2")).should == "(= $a (<= 1 2))" ; end
it "$a = 1 >= 2" do; dump(parse("$a = 1 >= 2")).should == "(= $a (>= 1 2))" ; end
end
context "of regular expressions (parse errors)" do
# Not supported in concrete syntax
it "$a = /.*/ == /.*/" do
- expect { parse("$a = /.*/ == /.*/") }.to raise_error(Puppet::ParseError)
+ dump(parse("$a = /.*/ == /.*/")).should == "(= $a (== /.*/ /.*/))"
end
it "$a = /.*/ != /a.*/" do
- expect { parse("$a = /.*/ != /.*/") }.to raise_error(Puppet::ParseError)
+ dump(parse("$a = /.*/ != /.*/")).should == "(= $a (!= /.*/ /.*/))"
end
end
end
context "When parsing Regular Expression matching" do
it "$a = 'a' =~ /.*/" do; dump(parse("$a = 'a' =~ /.*/")).should == "(= $a (=~ 'a' /.*/))" ; end
it "$a = 'a' =~ '.*'" do; dump(parse("$a = 'a' =~ '.*'")).should == "(= $a (=~ 'a' '.*'))" ; end
it "$a = 'a' !~ /b.*/" do; dump(parse("$a = 'a' !~ /b.*/")).should == "(= $a (!~ 'a' /b.*/))" ; end
it "$a = 'a' !~ 'b.*'" do; dump(parse("$a = 'a' !~ 'b.*'")).should == "(= $a (!~ 'a' 'b.*'))" ; end
end
context "When parsing Lists" do
it "$a = []" do
dump(parse("$a = []")).should == "(= $a ([]))"
end
it "$a = [1]" do
dump(parse("$a = [1]")).should == "(= $a ([] 1))"
end
it "$a = [1,2,3]" do
dump(parse("$a = [1,2,3]")).should == "(= $a ([] 1 2 3))"
end
it "[...[...[]]] should create nested arrays without trouble" do
dump(parse("$a = [1,[2.0, 2.1, [2.2]],[3.0, 3.1]]")).should == "(= $a ([] 1 ([] 2.0 2.1 ([] 2.2)) ([] 3.0 3.1)))"
end
it "$a = [2 + 2]" do
dump(parse("$a = [2+2]")).should == "(= $a ([] (+ 2 2)))"
end
it "$a [1,2,3] == [1,2,3]" do
dump(parse("$a = [1,2,3] == [1,2,3]")).should == "(= $a (== ([] 1 2 3) ([] 1 2 3)))"
end
end
context "When parsing indexed access" do
it "$a = $b[2]" do
dump(parse("$a = $b[2]")).should == "(= $a (slice $b 2))"
end
it "$a = [1, 2, 3][2]" do
dump(parse("$a = [1,2,3][2]")).should == "(= $a (slice ([] 1 2 3) 2))"
end
it "$a = {'a' => 1, 'b' => 2}['b']" do
dump(parse("$a = {'a'=>1,'b' =>2}[b]")).should == "(= $a (slice ({} ('a' 1) ('b' 2)) b))"
end
end
context "When parsing assignments" do
it "Should allow simple assignment" do
dump(parse("$a = 10")).should == "(= $a 10)"
end
+ it "Should allow append assignment" do
+ dump(parse("$a += 10")).should == "(+= $a 10)"
+ end
+
+ it "Should allow without assignment" do
+ dump(parse("$a -= 10")).should == "(-= $a 10)"
+ end
+
it "Should allow chained assignment" do
dump(parse("$a = $b = 10")).should == "(= $a (= $b 10))"
end
it "Should allow chained assignment with expressions" do
dump(parse("$a = 1 + ($b = 10)")).should == "(= $a (+ 1 (= $b 10)))"
end
end
context "When parsing Hashes" do
it "should create a Hash when evaluating a LiteralHash" do
dump(parse("$a = {'a'=>1,'b'=>2}")).should == "(= $a ({} ('a' 1) ('b' 2)))"
end
it "$a = {...{...{}}} should create nested hashes without trouble" do
dump(parse("$a = {'a'=>1,'b'=>{'x'=>2.1,'y'=>2.2}}")).should == "(= $a ({} ('a' 1) ('b' ({} ('x' 2.1) ('y' 2.2)))))"
end
it "$a = {'a'=> 2 + 2} should evaluate values in entries" do
dump(parse("$a = {'a'=>2+2}")).should == "(= $a ({} ('a' (+ 2 2))))"
end
it "$a = {'a'=> 1, 'b'=>2} == {'a'=> 1, 'b'=>2}" do
dump(parse("$a = {'a'=>1,'b'=>2} == {'a'=>1,'b'=>2}")).should == "(= $a (== ({} ('a' 1) ('b' 2)) ({} ('a' 1) ('b' 2))))"
end
it "$a = {'a'=> 1, 'b'=>2} != {'x'=> 1, 'y'=>3}" do
dump(parse("$a = {'a'=>1,'b'=>2} != {'a'=>1,'b'=>2}")).should == "(= $a (!= ({} ('a' 1) ('b' 2)) ({} ('a' 1) ('b' 2))))"
end
end
context "When parsing the 'in' operator" do
it "with integer in a list" do
dump(parse("$a = 1 in [1,2,3]")).should == "(= $a (in 1 ([] 1 2 3)))"
end
it "with string key in a hash" do
dump(parse("$a = 'a' in {'x'=>1, 'a'=>2, 'y'=> 3}")).should == "(= $a (in 'a' ({} ('x' 1) ('a' 2) ('y' 3))))"
end
it "with substrings of a string" do
dump(parse("$a = 'ana' in 'bananas'")).should == "(= $a (in 'ana' 'bananas'))"
end
it "with sublist in a list" do
dump(parse("$a = [2,3] in [1,2,3]")).should == "(= $a (in ([] 2 3) ([] 1 2 3)))"
end
end
context "When parsing string interpolation" do
it "should interpolate a bare word as a variable name, \"${var}\"" do
dump(parse("$a = \"$var\"")).should == "(= $a (cat '' (str $var) ''))"
end
it "should interpolate a variable in a text expression, \"${$var}\"" do
dump(parse("$a = \"${$var}\"")).should == "(= $a (cat '' (str $var) ''))"
end
it "should interpolate a variable, \"yo${var}yo\"" do
dump(parse("$a = \"yo${var}yo\"")).should == "(= $a (cat 'yo' (str $var) 'yo'))"
end
- it "should interpolate any expression in a text expression, \"${var*2}\"" do
- dump(parse("$a = \"yo${var+2}yo\"")).should == "(= $a (cat 'yo' (str (+ $var 2)) 'yo'))"
+ it "should interpolate any expression in a text expression, \"${$var*2}\"" do
+ dump(parse("$a = \"yo${$var+2}yo\"")).should == "(= $a (cat 'yo' (str (+ $var 2)) 'yo'))"
+ end
+
+ it "should not interpolate names as variable in expression, \"${notvar*2}\"" do
+ dump(parse("$a = \"yo${notvar+2}yo\"")).should == "(= $a (cat 'yo' (str (+ notvar 2)) 'yo'))"
+ end
+
+ it "should interpolate name as variable in access expression, \"${var[0]}\"" do
+ dump(parse("$a = \"yo${var[0]}yo\"")).should == "(= $a (cat 'yo' (str (slice $var 0)) 'yo'))"
+ end
+
+ it "should interpolate name as variable in method call, \"${var.foo}\"" do
+ dump(parse("$a = \"yo${$var.foo}yo\"")).should == "(= $a (cat 'yo' (str (call-method (. $var foo))) 'yo'))"
+ end
+
+ it "should interpolate name as variable in method call, \"${var.foo}\"" do
+ dump(parse("$a = \"yo${var.foo}yo\"")).should == "(= $a (cat 'yo' (str (call-method (. $var foo))) 'yo'))"
+ dump(parse("$a = \"yo${var.foo.bar}yo\"")).should == "(= $a (cat 'yo' (str (call-method (. (call-method (. $var foo)) bar))) 'yo'))"
end
end
end
diff --git a/spec/unit/pops/parser/parse_calls_spec.rb b/spec/unit/pops/parser/parse_calls_spec.rb
index 800a15bec..115c160d6 100644
--- a/spec/unit/pops/parser/parse_calls_spec.rb
+++ b/spec/unit/pops/parser/parse_calls_spec.rb
@@ -1,97 +1,101 @@
#! /usr/bin/env ruby
require 'spec_helper'
require 'puppet/pops'
# relative to this spec file (./) does not work as this file is loaded by rspec
require File.join(File.dirname(__FILE__), '/parser_rspec_helper')
describe "egrammar parsing function calls" do
include ParserRspecHelper
context "When parsing calls as statements" do
context "in top level scope" do
it "foo()" do
dump(parse("foo()")).should == "(invoke foo)"
end
- it "foo bar" do
- dump(parse("foo bar")).should == "(invoke foo bar)"
+ it "notice bar" do
+ dump(parse("notice bar")).should == "(invoke notice bar)"
+ end
+
+ it "notice(bar)" do
+ dump(parse("notice bar")).should == "(invoke notice bar)"
end
it "foo(bar)" do
dump(parse("foo(bar)")).should == "(invoke foo bar)"
end
it "foo(bar,)" do
dump(parse("foo(bar,)")).should == "(invoke foo bar)"
end
it "foo(bar, fum,)" do
dump(parse("foo(bar,fum,)")).should == "(invoke foo bar fum)"
end
- it "foo fqdn_rand(30)" do
- dump(parse("foo fqdn_rand(30)")).should == '(invoke foo (call fqdn_rand 30))'
+ it "notice fqdn_rand(30)" do
+ dump(parse("notice fqdn_rand(30)")).should == '(invoke notice (call fqdn_rand 30))'
end
end
context "in nested scopes" do
it "if true { foo() }" do
dump(parse("if true {foo()}")).should == "(if true\n (then (invoke foo)))"
end
- it "if true { foo bar}" do
- dump(parse("if true {foo bar}")).should == "(if true\n (then (invoke foo bar)))"
+ it "if true { notice bar}" do
+ dump(parse("if true {notice bar}")).should == "(if true\n (then (invoke notice bar)))"
end
end
end
context "When parsing calls as expressions" do
it "$a = foo()" do
dump(parse("$a = foo()")).should == "(= $a (call foo))"
end
it "$a = foo(bar)" do
dump(parse("$a = foo()")).should == "(= $a (call foo))"
end
# # For regular grammar where a bare word can not be a "statement"
# it "$a = foo bar # illegal, must have parentheses" do
# expect { dump(parse("$a = foo bar"))}.to raise_error(Puppet::ParseError)
# end
# For egrammar where a bare word can be a "statement"
it "$a = foo bar # illegal, must have parentheses" do
dump(parse("$a = foo bar")).should == "(block (= $a foo) bar)"
end
context "in nested scopes" do
it "if true { $a = foo() }" do
dump(parse("if true { $a = foo()}")).should == "(if true\n (then (= $a (call foo))))"
end
it "if true { $a= foo(bar)}" do
dump(parse("if true {$a = foo(bar)}")).should == "(if true\n (then (= $a (call foo bar))))"
end
end
end
context "When parsing method calls" do
it "$a.foo" do
dump(parse("$a.foo")).should == "(call-method (. $a foo))"
end
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) ()))"
end
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
end
diff --git a/spec/unit/pops/parser/parse_conditionals_spec.rb b/spec/unit/pops/parser/parse_conditionals_spec.rb
index b0cb36b96..b8b8d9c8e 100644
--- a/spec/unit/pops/parser/parse_conditionals_spec.rb
+++ b/spec/unit/pops/parser/parse_conditionals_spec.rb
@@ -1,159 +1,150 @@
#! /usr/bin/env ruby
require 'spec_helper'
require 'puppet/pops'
# relative to this spec file (./) does not work as this file is loaded by rspec
require File.join(File.dirname(__FILE__), '/parser_rspec_helper')
describe "egrammar parsing conditionals" do
include ParserRspecHelper
context "When parsing if statements" do
it "if true { $a = 10 }" do
dump(parse("if true { $a = 10 }")).should == "(if true\n (then (= $a 10)))"
end
it "if true { $a = 10 } else {$a = 20}" do
dump(parse("if true { $a = 10 } else {$a = 20}")).should ==
["(if true",
" (then (= $a 10))",
" (else (= $a 20)))"].join("\n")
end
it "if true { $a = 10 } elsif false { $a = 15} else {$a = 20}" do
dump(parse("if true { $a = 10 } elsif false { $a = 15} else {$a = 20}")).should ==
["(if true",
" (then (= $a 10))",
" (else (if false",
" (then (= $a 15))",
" (else (= $a 20)))))"].join("\n")
end
it "if true { $a = 10 $b = 10 } else {$a = 20}" do
dump(parse("if true { $a = 10 $b = 20} else {$a = 20}")).should ==
["(if true",
" (then (block (= $a 10) (= $b 20)))",
" (else (= $a 20)))"].join("\n")
end
it "allows a parenthesized conditional expression" do
dump(parse("if (true) { 10 }")).should == "(if true\n (then 10))"
end
it "allows a parenthesized elsif conditional expression" do
dump(parse("if true { 10 } elsif (false) { 20 }")).should ==
["(if true",
" (then 10)",
" (else (if false",
" (then 20))))"].join("\n")
end
end
context "When parsing unless statements" do
it "unless true { $a = 10 }" do
dump(parse("unless true { $a = 10 }")).should == "(unless true\n (then (= $a 10)))"
end
it "unless true { $a = 10 } else {$a = 20}" do
dump(parse("unless true { $a = 10 } else {$a = 20}")).should ==
["(unless true",
" (then (= $a 10))",
" (else (= $a 20)))"].join("\n")
end
it "allows a parenthesized conditional expression" do
dump(parse("unless (true) { 10 }")).should == "(unless true\n (then 10))"
end
it "unless true { $a = 10 } elsif false { $a = 15} else {$a = 20} # is illegal" do
expect { parse("unless true { $a = 10 } elsif false { $a = 15} else {$a = 20}")}.to raise_error(Puppet::ParseError)
end
end
context "When parsing selector expressions" do
it "$a = $b ? banana => fruit " do
dump(parse("$a = $b ? banana => fruit")).should ==
"(= $a (? $b (banana => fruit)))"
end
it "$a = $b ? { banana => fruit}" do
dump(parse("$a = $b ? { banana => fruit }")).should ==
"(= $a (? $b (banana => fruit)))"
end
it "does not fail on a trailing blank line" do
dump(parse("$a = $b ? { banana => fruit }\n\n")).should ==
"(= $a (? $b (banana => fruit)))"
end
it "$a = $b ? { banana => fruit, grape => berry }" do
dump(parse("$a = $b ? {banana => fruit, grape => berry}")).should ==
"(= $a (? $b (banana => fruit) (grape => berry)))"
end
it "$a = $b ? { banana => fruit, grape => berry, default => wat }" do
dump(parse("$a = $b ? {banana => fruit, grape => berry, default => wat}")).should ==
"(= $a (? $b (banana => fruit) (grape => berry) (:default => wat)))"
end
it "$a = $b ? { default => wat, banana => fruit, grape => berry, }" do
dump(parse("$a = $b ? {default => wat, banana => fruit, grape => berry}")).should ==
"(= $a (? $b (:default => wat) (banana => fruit) (grape => berry)))"
end
end
context "When parsing case statements" do
it "case $a { a : {}}" do
dump(parse("case $a { a : {}}")).should ==
["(case $a",
" (when (a) (then ())))"
].join("\n")
end
it "allows a parenthesized value expression" do
dump(parse("case ($a) { a : {}}")).should ==
["(case $a",
" (when (a) (then ())))"
].join("\n")
end
it "case $a { /.*/ : {}}" do
dump(parse("case $a { /.*/ : {}}")).should ==
["(case $a",
" (when (/.*/) (then ())))"
].join("\n")
end
it "case $a { a, b : {}}" do
dump(parse("case $a { a, b : {}}")).should ==
["(case $a",
" (when (a b) (then ())))"
].join("\n")
end
it "case $a { a, b : {} default : {}}" do
dump(parse("case $a { a, b : {} default : {}}")).should ==
["(case $a",
" (when (a b) (then ()))",
" (when (:default) (then ())))"
].join("\n")
end
it "case $a { a : {$b = 10 $c = 20}}" do
dump(parse("case $a { a : {$b = 10 $c = 20}}")).should ==
["(case $a",
" (when (a) (then (block (= $b 10) (= $c 20)))))"
].join("\n")
end
end
- context "When parsing imports" do
- it "import 'foo'" do
- dump(parse("import 'foo'")).should == "(import 'foo')"
- end
-
- it "import 'foo', 'bar'" do
- dump(parse("import 'foo', 'bar'")).should == "(import 'foo' 'bar')"
- end
- end
end
diff --git a/spec/unit/pops/parser/parse_containers_spec.rb b/spec/unit/pops/parser/parse_containers_spec.rb
index a6b9f2022..57d6efee9 100644
--- a/spec/unit/pops/parser/parse_containers_spec.rb
+++ b/spec/unit/pops/parser/parse_containers_spec.rb
@@ -1,175 +1,206 @@
#! /usr/bin/env ruby
require 'spec_helper'
require 'puppet/pops'
# relative to this spec file (./) does not work as this file is loaded by rspec
require File.join(File.dirname(__FILE__), '/parser_rspec_helper')
describe "egrammar parsing containers" do
include ParserRspecHelper
context "When parsing file scope" do
it "$a = 10 $b = 20" do
dump(parse("$a = 10 $b = 20")).should == "(block (= $a 10) (= $b 20))"
end
it "$a = 10" do
dump(parse("$a = 10")).should == "(= $a 10)"
end
end
context "When parsing class" do
it "class foo {}" do
dump(parse("class foo {}")).should == "(class foo ())"
end
+ it "class foo { class bar {} }" do
+ dump(parse("class foo { class bar {}}")).should == "(class foo (block (class foo::bar ())))"
+ end
+
it "class foo::bar {}" do
dump(parse("class foo::bar {}")).should == "(class foo::bar ())"
end
it "class foo inherits bar {}" do
dump(parse("class foo inherits bar {}")).should == "(class foo (inherits bar) ())"
end
it "class foo($a) {}" do
dump(parse("class foo($a) {}")).should == "(class foo (parameters a) ())"
end
it "class foo($a, $b) {}" do
dump(parse("class foo($a, $b) {}")).should == "(class foo (parameters a b) ())"
end
it "class foo($a, $b=10) {}" do
dump(parse("class foo($a, $b=10) {}")).should == "(class foo (parameters a (= b 10)) ())"
end
it "class foo($a, $b) inherits belgo::bar {}" do
dump(parse("class foo($a, $b) inherits belgo::bar{}")).should == "(class foo (inherits belgo::bar) (parameters a b) ())"
end
it "class foo {$a = 10 $b = 20}" do
dump(parse("class foo {$a = 10 $b = 20}")).should == "(class foo (block (= $a 10) (= $b 20)))"
end
context "it should handle '3x weirdness'" do
it "class class {} # a class named 'class'" do
# Not as much weird as confusing that it is possible to name a class 'class'. Can have
# a very confusing effect when resolving relative names, getting the global hardwired "Class"
# instead of some foo::class etc.
# This is allowed in 3.x.
- dump(parse("class class {}")).should == "(class class ())"
+ expect {
+ dump(parse("class class {}")).should == "(class class ())"
+ }.to raise_error(/not a valid classname/)
end
it "class default {} # a class named 'default'" do
# The weirdness here is that a class can inherit 'default' but not declare a class called default.
# (It will work with relative names i.e. foo::default though). The whole idea with keywords as
# names is flawed to begin with - it generally just a very bad idea.
expect { dump(parse("class default {}")).should == "(class default ())" }.to raise_error(Puppet::ParseError)
end
it "class foo::default {} # a nested name 'default'" do
dump(parse("class foo::default {}")).should == "(class foo::default ())"
end
it "class class inherits default {} # inherits default", :broken => true do
- dump(parse("class class inherits default {}")).should == "(class class (inherits default) ())"
+ expect {
+ dump(parse("class class inherits default {}")).should == "(class class (inherits default) ())"
+ }.to raise_error(/not a valid classname/)
end
it "class class inherits default {} # inherits default" do
# TODO: See previous test marked as :broken=>true, it is actually this test (result) that is wacky,
# this because a class is named at parse time (since class evaluation is lazy, the model must have the
# full class name for nested classes - only, it gets this wrong when a class is named "class" - or at least
# I think it is wrong.)
- #
+ #
+ expect {
dump(parse("class class inherits default {}")).should == "(class class::class (inherits default) ())"
+ }.to raise_error(/not a valid classname/)
end
it "class foo inherits class" do
- dump(parse("class foo inherits class {}")).should == "(class foo (inherits class) ())"
+ expect {
+ dump(parse("class foo inherits class {}")).should == "(class foo (inherits class) ())"
+ }.to raise_error(/not a valid classname/)
end
end
end
context "When the parser parses define" do
it "define foo {}" do
dump(parse("define foo {}")).should == "(define foo ())"
end
+ it "class foo { define bar {}}" do
+ dump(parse("class foo {define bar {}}")).should == "(class foo (block (define foo::bar ())))"
+ end
+
+ it "define foo { define bar {}}" do
+ # This is illegal, but handled as part of validation
+ dump(parse("define foo { define bar {}}")).should == "(define foo (block (define bar ())))"
+ end
+
it "define foo::bar {}" do
dump(parse("define foo::bar {}")).should == "(define foo::bar ())"
end
it "define foo($a) {}" do
dump(parse("define foo($a) {}")).should == "(define foo (parameters a) ())"
end
it "define foo($a, $b) {}" do
dump(parse("define foo($a, $b) {}")).should == "(define foo (parameters a b) ())"
end
it "define foo($a, $b=10) {}" do
dump(parse("define foo($a, $b=10) {}")).should == "(define foo (parameters a (= b 10)) ())"
end
it "define foo {$a = 10 $b = 20}" do
dump(parse("define foo {$a = 10 $b = 20}")).should == "(define foo (block (= $a 10) (= $b 20)))"
end
context "it should handle '3x weirdness'" do
it "define class {} # a define named 'class'" do
# This is weird because Class already exists, and instantiating this define will probably not
# work
- dump(parse("define class {}")).should == "(define class ())"
+ expect {
+ dump(parse("define class {}")).should == "(define class ())"
+ }.to raise_error(/not a valid classname/)
end
it "define default {} # a define named 'default'" do
# Check unwanted ability to define 'default'.
# The expression below is not allowed (which is good).
#
expect { dump(parse("define default {}")).should == "(define default ())"}.to raise_error(Puppet::ParseError)
end
end
end
context "When parsing node" do
it "node foo {}" do
- dump(parse("node foo {}")).should == "(node (matches foo) ())"
+ dump(parse("node foo {}")).should == "(node (matches 'foo') ())"
+ end
+
+ it "node kermit.example.com {}" do
+ dump(parse("node kermit.example.com {}")).should == "(node (matches 'kermit.example.com') ())"
+ end
+
+ it "node kermit . example . com {}" do
+ dump(parse("node kermit . example . com {}")).should == "(node (matches 'kermit.example.com') ())"
end
it "node foo, x::bar, default {}" do
- dump(parse("node foo, x::bar, default {}")).should == "(node (matches foo x::bar :default) ())"
+ dump(parse("node foo, x::bar, default {}")).should == "(node (matches 'foo' 'x::bar' :default) ())"
end
it "node 'foo' {}" do
dump(parse("node 'foo' {}")).should == "(node (matches 'foo') ())"
end
it "node foo inherits x::bar {}" do
- dump(parse("node foo inherits x::bar {}")).should == "(node (matches foo) (parent x::bar) ())"
+ dump(parse("node foo inherits x::bar {}")).should == "(node (matches 'foo') (parent 'x::bar') ())"
end
it "node foo inherits 'bar' {}" do
- dump(parse("node foo inherits 'bar' {}")).should == "(node (matches foo) (parent 'bar') ())"
+ dump(parse("node foo inherits 'bar' {}")).should == "(node (matches 'foo') (parent 'bar') ())"
end
it "node foo inherits default {}" do
- dump(parse("node foo inherits default {}")).should == "(node (matches foo) (parent :default) ())"
+ dump(parse("node foo inherits default {}")).should == "(node (matches 'foo') (parent :default) ())"
end
it "node /web.*/ {}" do
dump(parse("node /web.*/ {}")).should == "(node (matches /web.*/) ())"
end
it "node /web.*/, /do\.wop.*/, and.so.on {}" do
dump(parse("node /web.*/, /do\.wop.*/, 'and.so.on' {}")).should == "(node (matches /web.*/ /do\.wop.*/ 'and.so.on') ())"
end
it "node wat inherits /apache.*/ {}" do
- expect { parse("node wat inherits /apache.*/ {}")}.to raise_error(Puppet::ParseError)
+ dump(parse("node wat inherits /apache.*/ {}")).should == "(node (matches 'wat') (parent /apache.*/) ())"
end
it "node foo inherits bar {$a = 10 $b = 20}" do
- dump(parse("node foo inherits bar {$a = 10 $b = 20}")).should == "(node (matches foo) (parent bar) (block (= $a 10) (= $b 20)))"
+ dump(parse("node foo inherits bar {$a = 10 $b = 20}")).should == "(node (matches 'foo') (parent 'bar') (block (= $a 10) (= $b 20)))"
end
end
end
diff --git a/spec/unit/pops/parser/parse_heredoc_spec.rb b/spec/unit/pops/parser/parse_heredoc_spec.rb
new file mode 100644
index 000000000..3616b9b2e
--- /dev/null
+++ b/spec/unit/pops/parser/parse_heredoc_spec.rb
@@ -0,0 +1,73 @@
+require 'spec_helper'
+require 'puppet/pops'
+
+# relative to this spec file (./) does not work as this file is loaded by rspec
+require File.join(File.dirname(__FILE__), '/parser_rspec_helper')
+
+describe "egrammar parsing heredoc" do
+ include ParserRspecHelper
+
+ it "parses plain heredoc" do
+ dump(parse("@(END)\nThis is\nheredoc text\nEND\n")).should == [
+ "(@()",
+ " (sublocated 'This is\nheredoc text\n')",
+ ")"
+ ].join("\n")
+ end
+
+ it "parses heredoc with margin" do
+ src = [
+ "@(END)",
+ " This is",
+ " heredoc text",
+ " | END",
+ ""
+ ].join("\n")
+ dump(parse(src)).should == [
+ "(@()",
+ " (sublocated 'This is\nheredoc text\n')",
+ ")"
+ ].join("\n")
+ end
+
+ it "parses heredoc with margin and right newline trim" do
+ src = [
+ "@(END)",
+ " This is",
+ " heredoc text",
+ " |- END",
+ ""
+ ].join("\n")
+ dump(parse(src)).should == [
+ "(@()",
+ " (sublocated 'This is\nheredoc text')",
+ ")"
+ ].join("\n")
+ end
+
+ it "parses syntax and escape specification" do
+ src = <<-CODE
+ @(END:syntax/t)
+ Tex\\tt\\n
+ |- END
+ CODE
+ dump(parse(src)).should == [
+ "(@(syntax)",
+ " (sublocated 'Tex\tt\\n')",
+ ")"
+ ].join("\n")
+ end
+
+ it "parses interpolated heredoc epression" do
+ src = <<-CODE
+ @("END")
+ Hello $name
+ |- END
+ CODE
+ dump(parse(src)).should == [
+ "(@()",
+ " (sublocated (cat 'Hello ' (str $name) ''))",
+ ")"
+ ].join("\n")
+ end
+end
diff --git a/spec/unit/pops/parser/parse_resource_spec.rb b/spec/unit/pops/parser/parse_resource_spec.rb
index 7d2b54d10..ee7e13445 100644
--- a/spec/unit/pops/parser/parse_resource_spec.rb
+++ b/spec/unit/pops/parser/parse_resource_spec.rb
@@ -1,228 +1,242 @@
#! /usr/bin/env ruby
require 'spec_helper'
require 'puppet/pops'
# relative to this spec file (./) does not work as this file is loaded by rspec
require File.join(File.dirname(__FILE__), '/parser_rspec_helper')
describe "egrammar parsing resource declarations" do
include ParserRspecHelper
context "When parsing regular resource" do
it "file { 'title': }" do
dump(parse("file { 'title': }")).should == [
"(resource file",
" ('title'))"
].join("\n")
end
it "file { 'title': path => '/somewhere', mode => 0777}" do
dump(parse("file { 'title': path => '/somewhere', mode => 0777}")).should == [
"(resource file",
" ('title'",
" (path => '/somewhere')",
" (mode => 0777)))"
].join("\n")
end
it "file { 'title': path => '/somewhere', }" do
dump(parse("file { 'title': path => '/somewhere', }")).should == [
"(resource file",
" ('title'",
" (path => '/somewhere')))"
].join("\n")
end
it "file { 'title': , }" do
dump(parse("file { 'title': , }")).should == [
"(resource file",
" ('title'))"
].join("\n")
end
it "file { 'title': ; }" do
dump(parse("file { 'title': ; }")).should == [
"(resource file",
" ('title'))"
].join("\n")
end
it "file { 'title': ; 'other_title': }" do
dump(parse("file { 'title': ; 'other_title': }")).should == [
"(resource file",
" ('title')",
" ('other_title'))"
].join("\n")
end
it "file { 'title1': path => 'x'; 'title2': path => 'y'}" do
dump(parse("file { 'title1': path => 'x'; 'title2': path => 'y'}")).should == [
"(resource file",
" ('title1'",
" (path => 'x'))",
" ('title2'",
" (path => 'y')))",
].join("\n")
end
end
context "When parsing resource defaults" do
it "File { }" do
dump(parse("File { }")).should == "(resource-defaults file)"
end
it "File { mode => 0777 }" do
dump(parse("File { mode => 0777}")).should == [
"(resource-defaults file",
" (mode => 0777))"
].join("\n")
end
end
context "When parsing resource override" do
it "File['x'] { }" do
dump(parse("File['x'] { }")).should == "(override (slice file 'x'))"
end
it "File['x'] { x => 1 }" do
dump(parse("File['x'] { x => 1}")).should == "(override (slice file 'x')\n (x => 1))"
end
it "File['x', 'y'] { x => 1 }" do
dump(parse("File['x', 'y'] { x => 1}")).should == "(override (slice file ('x' 'y'))\n (x => 1))"
end
it "File['x'] { x => 1, y => 2 }" do
dump(parse("File['x'] { x => 1, y=> 2}")).should == "(override (slice file 'x')\n (x => 1)\n (y => 2))"
end
it "File['x'] { x +> 1 }" do
dump(parse("File['x'] { x +> 1}")).should == "(override (slice file 'x')\n (x +> 1))"
end
end
context "When parsing virtual and exported resources" do
it "@@file { 'title': }" do
dump(parse("@@file { 'title': }")).should == "(exported-resource file\n ('title'))"
end
it "@file { 'title': }" do
dump(parse("@file { 'title': }")).should == "(virtual-resource file\n ('title'))"
end
it "@file { mode => 0777 }" do
# Defaults are not virtualizeable
expect {
dump(parse("@file { mode => 0777 }")).should == ""
}.to raise_error(Puppet::ParseError, /Defaults are not virtualizable/)
end
end
context "When parsing class resource" do
it "class { 'cname': }" do
dump(parse("class { 'cname': }")).should == [
"(resource class",
" ('cname'))"
].join("\n")
end
+ it "@class { 'cname': }" do
+ dump(parse("@class { 'cname': }")).should == [
+ "(virtual-resource class",
+ " ('cname'))"
+ ].join("\n")
+ end
+
+ it "@@class { 'cname': }" do
+ dump(parse("@@class { 'cname': }")).should == [
+ "(exported-resource class",
+ " ('cname'))"
+ ].join("\n")
+ end
+
it "class { 'cname': x => 1, y => 2}" do
dump(parse("class { 'cname': x => 1, y => 2}")).should == [
"(resource class",
" ('cname'",
" (x => 1)",
" (y => 2)))"
].join("\n")
end
it "class { 'cname1': x => 1; 'cname2': y => 2}" do
dump(parse("class { 'cname1': x => 1; 'cname2': y => 2}")).should == [
"(resource class",
" ('cname1'",
" (x => 1))",
" ('cname2'",
" (y => 2)))",
].join("\n")
end
end
context "reported issues in 3.x" do
it "should not screw up on brackets in title of resource #19632" do
dump(parse('notify { "thisisa[bug]": }')).should == [
"(resource notify",
" ('thisisa[bug]'))",
].join("\n")
end
end
context "When parsing Relationships" do
it "File[a] -> File[b]" do
dump(parse("File[a] -> File[b]")).should == "(-> (slice file a) (slice file b))"
end
it "File[a] <- File[b]" do
dump(parse("File[a] <- File[b]")).should == "(<- (slice file a) (slice file b))"
end
it "File[a] ~> File[b]" do
dump(parse("File[a] ~> File[b]")).should == "(~> (slice file a) (slice file b))"
end
it "File[a] <~ File[b]" do
dump(parse("File[a] <~ File[b]")).should == "(<~ (slice file a) (slice file b))"
end
it "Should chain relationships" do
dump(parse("a -> b -> c")).should ==
"(-> (-> a b) c)"
end
it "Should chain relationships" do
dump(parse("File[a] -> File[b] ~> File[c] <- File[d] <~ File[e]")).should ==
"(<~ (<- (~> (-> (slice file a) (slice file b)) (slice file c)) (slice file d)) (slice file e))"
end
it "should create relationships between collects" do
dump(parse("File <| mode == 0644 |> -> File <| mode == 0755 |>")).should ==
"(-> (collect file\n (<| |> (== mode 0644))) (collect file\n (<| |> (== mode 0755))))"
end
end
context "When parsing collection" do
context "of virtual resources" do
it "File <| |>" do
dump(parse("File <| |>")).should == "(collect file\n (<| |>))"
end
end
context "of exported resources" do
it "File <<| |>>" do
dump(parse("File <<| |>>")).should == "(collect file\n (<<| |>>))"
end
end
context "queries are parsed with correct precedence" do
it "File <| tag == 'foo' |>" do
dump(parse("File <| tag == 'foo' |>")).should == "(collect file\n (<| |> (== tag 'foo')))"
end
it "File <| tag == 'foo' and mode != 0777 |>" do
dump(parse("File <| tag == 'foo' and mode != 0777 |>")).should == "(collect file\n (<| |> (&& (== tag 'foo') (!= mode 0777))))"
end
it "File <| tag == 'foo' or mode != 0777 |>" do
dump(parse("File <| tag == 'foo' or mode != 0777 |>")).should == "(collect file\n (<| |> (|| (== tag 'foo') (!= mode 0777))))"
end
it "File <| tag == 'foo' or tag == 'bar' and mode != 0777 |>" do
dump(parse("File <| tag == 'foo' or tag == 'bar' and mode != 0777 |>")).should ==
"(collect file\n (<| |> (|| (== tag 'foo') (&& (== tag 'bar') (!= mode 0777)))))"
end
it "File <| (tag == 'foo' or tag == 'bar') and mode != 0777 |>" do
dump(parse("File <| (tag == 'foo' or tag == 'bar') and mode != 0777 |>")).should ==
"(collect file\n (<| |> (&& (|| (== tag 'foo') (== tag 'bar')) (!= mode 0777))))"
end
end
end
end
diff --git a/spec/unit/pops/parser/parser_spec.rb b/spec/unit/pops/parser/parser_spec.rb
index e29410681..fb44a08c9 100644
--- a/spec/unit/pops/parser/parser_spec.rb
+++ b/spec/unit/pops/parser/parser_spec.rb
@@ -1,15 +1,17 @@
require 'spec_helper'
require 'puppet/pops'
describe Puppet::Pops::Parser::Parser do
it "should instantiate a parser" do
parser = Puppet::Pops::Parser::Parser.new()
parser.class.should == Puppet::Pops::Parser::Parser
end
it "should parse a code string and return a model" do
parser = Puppet::Pops::Parser::Parser.new()
model = parser.parse_string("$a = 10").current
- model.class.should == Puppet::Pops::Model::AssignmentExpression
+ model.class.should == Puppet::Pops::Model::Program
+ model.body.class.should == Puppet::Pops::Model::AssignmentExpression
end
+
end
diff --git a/spec/unit/pops/transformer/transform_basic_expressions_spec.rb b/spec/unit/pops/transformer/transform_basic_expressions_spec.rb
index 11a2c76f5..dda35002c 100644
--- a/spec/unit/pops/transformer/transform_basic_expressions_spec.rb
+++ b/spec/unit/pops/transformer/transform_basic_expressions_spec.rb
@@ -1,243 +1,243 @@
#! /usr/bin/env ruby
require 'spec_helper'
require 'puppet/pops'
# relative to this spec file (./) does not work as this file is loaded by rspec
require File.join(File.dirname(__FILE__), '/transformer_rspec_helper')
describe "transformation to Puppet AST for basic expressions" do
include TransformerRspecHelper
context "When transforming arithmetic" do
context "with Integers" do
it "$a = 2 + 2" do; astdump(parse("$a = 2 + 2")).should == "(= $a (+ 2 2))" ; end
it "$a = 7 - 3" do; astdump(parse("$a = 7 - 3")).should == "(= $a (- 7 3))" ; end
it "$a = 6 * 3" do; astdump(parse("$a = 6 * 3")).should == "(= $a (* 6 3))" ; end
it "$a = 6 / 3" do; astdump(parse("$a = 6 / 3")).should == "(= $a (/ 6 3))" ; end
it "$a = 6 % 3" do; astdump(parse("$a = 6 % 3")).should == "(= $a (% 6 3))" ; end
it "$a = -(6/3)" do; astdump(parse("$a = -(6/3)")).should == "(= $a (- (/ 6 3)))" ; end
it "$a = -6/3" do; astdump(parse("$a = -6/3")).should == "(= $a (/ (- 6) 3))" ; end
it "$a = 8 >> 1 " do; astdump(parse("$a = 8 >> 1")).should == "(= $a (>> 8 1))" ; end
it "$a = 8 << 1 " do; astdump(parse("$a = 8 << 1")).should == "(= $a (<< 8 1))" ; end
end
context "with Floats" do
it "$a = 2.2 + 2.2" do; astdump(parse("$a = 2.2 + 2.2")).should == "(= $a (+ 2.2 2.2))" ; end
it "$a = 7.7 - 3.3" do; astdump(parse("$a = 7.7 - 3.3")).should == "(= $a (- 7.7 3.3))" ; end
it "$a = 6.1 * 3.1" do; astdump(parse("$a = 6.1 - 3.1")).should == "(= $a (- 6.1 3.1))" ; end
it "$a = 6.6 / 3.3" do; astdump(parse("$a = 6.6 / 3.3")).should == "(= $a (/ 6.6 3.3))" ; end
it "$a = -(6.0/3.0)" do; astdump(parse("$a = -(6.0/3.0)")).should == "(= $a (- (/ 6.0 3.0)))" ; end
it "$a = -6.0/3.0" do; astdump(parse("$a = -6.0/3.0")).should == "(= $a (/ (- 6.0) 3.0))" ; end
it "$a = 3.14 << 2" do; astdump(parse("$a = 3.14 << 2")).should == "(= $a (<< 3.14 2))" ; end
it "$a = 3.14 >> 2" do; astdump(parse("$a = 3.14 >> 2")).should == "(= $a (>> 3.14 2))" ; end
end
context "with hex and octal Integer values" do
it "$a = 0xAB + 0xCD" do; astdump(parse("$a = 0xAB + 0xCD")).should == "(= $a (+ 0xAB 0xCD))" ; end
it "$a = 0777 - 0333" do; astdump(parse("$a = 0777 - 0333")).should == "(= $a (- 0777 0333))" ; end
end
context "with strings requiring boxing to Numeric" do
# In AST, there is no difference, the ast dumper prints all numbers without quotes - they are still
# strings
it "$a = '2' + '2'" do; astdump(parse("$a = '2' + '2'")).should == "(= $a (+ 2 2))" ; end
it "$a = '2.2' + '0.2'" do; astdump(parse("$a = '2.2' + '0.2'")).should == "(= $a (+ 2.2 0.2))" ; end
it "$a = '0xab' + '0xcd'" do; astdump(parse("$a = '0xab' + '0xcd'")).should == "(= $a (+ 0xab 0xcd))" ; end
it "$a = '0777' + '0333'" do; astdump(parse("$a = '0777' + '0333'")).should == "(= $a (+ 0777 0333))" ; end
end
context "precedence should be correct" do
it "$a = 1 + 2 * 3" do; astdump(parse("$a = 1 + 2 * 3")).should == "(= $a (+ 1 (* 2 3)))"; end
it "$a = 1 + 2 % 3" do; astdump(parse("$a = 1 + 2 % 3")).should == "(= $a (+ 1 (% 2 3)))"; end
it "$a = 1 + 2 / 3" do; astdump(parse("$a = 1 + 2 / 3")).should == "(= $a (+ 1 (/ 2 3)))"; end
it "$a = 1 + 2 << 3" do; astdump(parse("$a = 1 + 2 << 3")).should == "(= $a (<< (+ 1 2) 3))"; end
it "$a = 1 + 2 >> 3" do; astdump(parse("$a = 1 + 2 >> 3")).should == "(= $a (>> (+ 1 2) 3))"; end
end
context "parentheses alter precedence" do
it "$a = (1 + 2) * 3" do; astdump(parse("$a = (1 + 2) * 3")).should == "(= $a (* (+ 1 2) 3))"; end
it "$a = (1 + 2) / 3" do; astdump(parse("$a = (1 + 2) / 3")).should == "(= $a (/ (+ 1 2) 3))"; end
end
end
context "When transforming boolean operations" do
context "using operators AND OR NOT" do
it "$a = true and true" do; astdump(parse("$a = true and true")).should == "(= $a (&& true true))"; end
it "$a = true or true" do; astdump(parse("$a = true or true")).should == "(= $a (|| true true))" ; end
it "$a = !true" do; astdump(parse("$a = !true")).should == "(= $a (! true))" ; end
end
context "precedence should be correct" do
it "$a = false or true and true" do
astdump(parse("$a = false or true and true")).should == "(= $a (|| false (&& true true)))"
end
it "$a = (false or true) and true" do
astdump(parse("$a = (false or true) and true")).should == "(= $a (&& (|| false true) true))"
end
it "$a = !true or true and true" do
astdump(parse("$a = !false or true and true")).should == "(= $a (|| (! false) (&& true true)))"
end
end
# Possibly change to check of literal expressions
context "on values requiring boxing to Boolean" do
it "'x' == true" do
astdump(parse("! 'x'")).should == "(! 'x')"
end
it "'' == false" do
astdump(parse("! ''")).should == "(! '')"
end
it ":undef == false" do
astdump(parse("! undef")).should == "(! :undef)"
end
end
end
context "When transforming comparisons" do
context "of string values" do
it "$a = 'a' == 'a'" do; astdump(parse("$a = 'a' == 'a'")).should == "(= $a (== 'a' 'a'))" ; end
it "$a = 'a' != 'a'" do; astdump(parse("$a = 'a' != 'a'")).should == "(= $a (!= 'a' 'a'))" ; end
it "$a = 'a' < 'b'" do; astdump(parse("$a = 'a' < 'b'")).should == "(= $a (< 'a' 'b'))" ; end
it "$a = 'a' > 'b'" do; astdump(parse("$a = 'a' > 'b'")).should == "(= $a (> 'a' 'b'))" ; end
it "$a = 'a' <= 'b'" do; astdump(parse("$a = 'a' <= 'b'")).should == "(= $a (<= 'a' 'b'))" ; end
it "$a = 'a' >= 'b'" do; astdump(parse("$a = 'a' >= 'b'")).should == "(= $a (>= 'a' 'b'))" ; end
end
context "of integer values" do
it "$a = 1 == 1" do; astdump(parse("$a = 1 == 1")).should == "(= $a (== 1 1))" ; end
it "$a = 1 != 1" do; astdump(parse("$a = 1 != 1")).should == "(= $a (!= 1 1))" ; end
it "$a = 1 < 2" do; astdump(parse("$a = 1 < 2")).should == "(= $a (< 1 2))" ; end
it "$a = 1 > 2" do; astdump(parse("$a = 1 > 2")).should == "(= $a (> 1 2))" ; end
it "$a = 1 <= 2" do; astdump(parse("$a = 1 <= 2")).should == "(= $a (<= 1 2))" ; end
it "$a = 1 >= 2" do; astdump(parse("$a = 1 >= 2")).should == "(= $a (>= 1 2))" ; end
end
context "of regular expressions (parse errors)" do
- # Not supported in concrete syntax
+ # Not supported in concrete syntax (until Lexer2)
it "$a = /.*/ == /.*/" do
- expect { parse("$a = /.*/ == /.*/") }.to raise_error(Puppet::ParseError)
+ astdump(parse("$a = /.*/ == /.*/")).should == "(= $a (== /.*/ /.*/))"
end
it "$a = /.*/ != /a.*/" do
- expect { parse("$a = /.*/ != /.*/") }.to raise_error(Puppet::ParseError)
+ astdump(parse("$a = /.*/ != /.*/")).should == "(= $a (!= /.*/ /.*/))"
end
end
end
context "When transforming Regular Expression matching" do
it "$a = 'a' =~ /.*/" do; astdump(parse("$a = 'a' =~ /.*/")).should == "(= $a (=~ 'a' /.*/))" ; end
it "$a = 'a' =~ '.*'" do; astdump(parse("$a = 'a' =~ '.*'")).should == "(= $a (=~ 'a' '.*'))" ; end
it "$a = 'a' !~ /b.*/" do; astdump(parse("$a = 'a' !~ /b.*/")).should == "(= $a (!~ 'a' /b.*/))" ; end
it "$a = 'a' !~ 'b.*'" do; astdump(parse("$a = 'a' !~ 'b.*'")).should == "(= $a (!~ 'a' 'b.*'))" ; end
end
context "When transforming Lists" do
it "$a = []" do
astdump(parse("$a = []")).should == "(= $a ([]))"
end
it "$a = [1]" do
astdump(parse("$a = [1]")).should == "(= $a ([] 1))"
end
it "$a = [1,2,3]" do
astdump(parse("$a = [1,2,3]")).should == "(= $a ([] 1 2 3))"
end
it "[...[...[]]] should create nested arrays without trouble" do
astdump(parse("$a = [1,[2.0, 2.1, [2.2]],[3.0, 3.1]]")).should == "(= $a ([] 1 ([] 2.0 2.1 ([] 2.2)) ([] 3.0 3.1)))"
end
it "$a = [2 + 2]" do
astdump(parse("$a = [2+2]")).should == "(= $a ([] (+ 2 2)))"
end
it "$a [1,2,3] == [1,2,3]" do
astdump(parse("$a = [1,2,3] == [1,2,3]")).should == "(= $a (== ([] 1 2 3) ([] 1 2 3)))"
end
end
context "When transforming indexed access" do
it "$a = $b[2]" do
astdump(parse("$a = $b[2]")).should == "(= $a (slice $b 2))"
end
it "$a = [1, 2, 3][2]" do
astdump(parse("$a = [1,2,3][2]")).should == "(= $a (slice ([] 1 2 3) 2))"
end
it "$a = {'a' => 1, 'b' => 2}['b']" do
astdump(parse("$a = {'a'=>1,'b' =>2}[b]")).should == "(= $a (slice ({} ('a' 1) ('b' 2)) b))"
end
end
context "When transforming Hashes" do
it "should create a Hash when evaluating a LiteralHash" do
astdump(parse("$a = {'a'=>1,'b'=>2}")).should == "(= $a ({} ('a' 1) ('b' 2)))"
end
it "$a = {...{...{}}} should create nested hashes without trouble" do
astdump(parse("$a = {'a'=>1,'b'=>{'x'=>2.1,'y'=>2.2}}")).should == "(= $a ({} ('a' 1) ('b' ({} ('x' 2.1) ('y' 2.2)))))"
end
it "$a = {'a'=> 2 + 2} should evaluate values in entries" do
astdump(parse("$a = {'a'=>2+2}")).should == "(= $a ({} ('a' (+ 2 2))))"
end
it "$a = {'a'=> 1, 'b'=>2} == {'a'=> 1, 'b'=>2}" do
astdump(parse("$a = {'a'=>1,'b'=>2} == {'a'=>1,'b'=>2}")).should == "(= $a (== ({} ('a' 1) ('b' 2)) ({} ('a' 1) ('b' 2))))"
end
it "$a = {'a'=> 1, 'b'=>2} != {'x'=> 1, 'y'=>3}" do
astdump(parse("$a = {'a'=>1,'b'=>2} != {'a'=>1,'b'=>2}")).should == "(= $a (!= ({} ('a' 1) ('b' 2)) ({} ('a' 1) ('b' 2))))"
end
end
context "When transforming the 'in' operator" do
it "with integer in a list" do
astdump(parse("$a = 1 in [1,2,3]")).should == "(= $a (in 1 ([] 1 2 3)))"
end
it "with string key in a hash" do
astdump(parse("$a = 'a' in {'x'=>1, 'a'=>2, 'y'=> 3}")).should == "(= $a (in 'a' ({} ('a' 2) ('x' 1) ('y' 3))))"
end
it "with substrings of a string" do
astdump(parse("$a = 'ana' in 'bananas'")).should == "(= $a (in 'ana' 'bananas'))"
end
it "with sublist in a list" do
astdump(parse("$a = [2,3] in [1,2,3]")).should == "(= $a (in ([] 2 3) ([] 1 2 3)))"
end
end
context "When transforming string interpolation" do
it "should interpolate a bare word as a variable name, \"${var}\"" do
astdump(parse("$a = \"$var\"")).should == "(= $a (cat '' (str $var) ''))"
end
it "should interpolate a variable in a text expression, \"${$var}\"" do
astdump(parse("$a = \"${$var}\"")).should == "(= $a (cat '' (str $var) ''))"
end
it "should interpolate two variables in a text expression" do
astdump(parse(%q{$a = "xxx $x and $y end"})).should == "(= $a (cat 'xxx ' (str $x) ' and ' (str $y) ' end'))"
end
it "should interpolate one variables followed by parentheses" do
astdump(parse(%q{$a = "xxx ${x} (yay)"})).should == "(= $a (cat 'xxx ' (str $x) ' (yay)'))"
end
it "should interpolate a variable, \"yo${var}yo\"" do
astdump(parse("$a = \"yo${var}yo\"")).should == "(= $a (cat 'yo' (str $var) 'yo'))"
end
it "should interpolate any expression in a text expression, \"${var*2}\"" do
- astdump(parse("$a = \"yo${var+2}yo\"")).should == "(= $a (cat 'yo' (str (+ $var 2)) 'yo'))"
+ astdump(parse("$a = \"yo${$var+2}yo\"")).should == "(= $a (cat 'yo' (str (+ $var 2)) 'yo'))"
end
end
end
diff --git a/spec/unit/pops/transformer/transform_calls_spec.rb b/spec/unit/pops/transformer/transform_calls_spec.rb
index 969a2d7b8..f58c79e1e 100644
--- a/spec/unit/pops/transformer/transform_calls_spec.rb
+++ b/spec/unit/pops/transformer/transform_calls_spec.rb
@@ -1,80 +1,115 @@
#! /usr/bin/env ruby
require 'spec_helper'
require 'puppet/pops'
# relative to this spec file (./) does not work as this file is loaded by rspec
require File.join(File.dirname(__FILE__), '/transformer_rspec_helper')
describe "transformation to Puppet AST for function calls" do
include TransformerRspecHelper
context "When transforming calls as statements" do
context "in top level scope" do
it "foo()" do
astdump(parse("foo()")).should == "(invoke foo)"
end
- it "foo bar" do
- astdump(parse("foo bar")).should == "(invoke foo bar)"
+ it "notice bar" do
+ astdump(parse("notice bar")).should == "(invoke notice bar)"
end
end
context "in nested scopes" do
it "if true { foo() }" do
astdump(parse("if true {foo()}")).should == "(if true\n (then (invoke foo)))"
end
- it "if true { foo bar}" do
- astdump(parse("if true {foo bar}")).should == "(if true\n (then (invoke foo bar)))"
+ it "if true { notice bar}" do
+ astdump(parse("if true {notice bar}")).should == "(if true\n (then (invoke notice bar)))"
end
end
+ context "in general" do
+ {
+ "require bar" => '(invoke require bar)',
+ "realize bar" => '(invoke realize bar)',
+ "contain bar" => '(invoke contain bar)',
+ "include bar" => '(invoke include bar)',
+
+ "info bar" => '(invoke info bar)',
+ "notice bar" => '(invoke notice bar)',
+ "error bar" => '(invoke error bar)',
+ "warning bar" => '(invoke warning bar)',
+ "debug bar" => '(invoke debug bar)',
+
+ "fail bar" => '(invoke fail bar)',
+
+ "notice {a => 1}" => "(invoke notice ({} ('a' 1)))",
+ "notice 1,2,3" => "(invoke notice 1 2 3)",
+ "notice(1,2,3)" => "(invoke notice 1 2 3)",
+ }.each do |source, result|
+ it "should transform #{source} to #{result}" do
+ astdump(parse(source)).should == result
+ end
+ end
+
+ {
+ "foo bar" => '(block foo bar)',
+ "tag bar" => '(block tag bar)',
+ "tag" => 'tag',
+ }.each do |source, result|
+ it "should not transform #{source}, and instead produce #{result}" do
+ astdump(parse(source)).should == result
+ end
+ end
+
+ end
end
context "When transforming calls as expressions" do
it "$a = foo()" do
astdump(parse("$a = foo()")).should == "(= $a (call foo))"
end
it "$a = foo(bar)" do
astdump(parse("$a = foo()")).should == "(= $a (call foo))"
end
# For egrammar where a bare word can be a "statement"
it "$a = foo bar # assignment followed by bare word is ok in egrammar" do
astdump(parse("$a = foo bar")).should == "(block (= $a foo) bar)"
end
context "in nested scopes" do
it "if true { $a = foo() }" do
astdump(parse("if true { $a = foo()}")).should == "(if true\n (then (= $a (call foo))))"
end
it "if true { $a= foo(bar)}" do
astdump(parse("if true {$a = foo(bar)}")).should == "(if true\n (then (= $a (call foo bar))))"
end
end
end
context "When transforming method calls" do
it "$a.foo" 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 ([]))))"
end
- it "$a.foo {|$x| }" do
+ it "$a.foo |$x| { }" do
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 ==
"(call-method (. $a foo) (lambda (parameters x) (block (= $b $x))))"
end
end
end
diff --git a/spec/unit/pops/transformer/transform_conditionals_spec.rb b/spec/unit/pops/transformer/transform_conditionals_spec.rb
index 6eefa51a3..02b0de4a8 100644
--- a/spec/unit/pops/transformer/transform_conditionals_spec.rb
+++ b/spec/unit/pops/transformer/transform_conditionals_spec.rb
@@ -1,132 +1,123 @@
#! /usr/bin/env ruby
require 'spec_helper'
require 'puppet/pops'
# relative to this spec file (./) does not work as this file is loaded by rspec
require File.join(File.dirname(__FILE__), '/transformer_rspec_helper')
describe "transformation to Puppet AST for conditionals" do
include TransformerRspecHelper
context "When transforming if statements" do
it "if true { $a = 10 }" do
astdump(parse("if true { $a = 10 }")).should == "(if true\n (then (= $a 10)))"
end
it "if true { $a = 10 } else {$a = 20}" do
astdump(parse("if true { $a = 10 } else {$a = 20}")).should ==
["(if true",
" (then (= $a 10))",
" (else (= $a 20)))"].join("\n")
end
it "if true { $a = 10 } elsif false { $a = 15} else {$a = 20}" do
astdump(parse("if true { $a = 10 } elsif false { $a = 15} else {$a = 20}")).should ==
["(if true",
" (then (= $a 10))",
" (else (if false",
" (then (= $a 15))",
" (else (= $a 20)))))"].join("\n")
end
it "if true { $a = 10 $b = 10 } else {$a = 20}" do
astdump(parse("if true { $a = 10 $b = 20} else {$a = 20}")).should ==
["(if true",
" (then (block (= $a 10) (= $b 20)))",
" (else (= $a 20)))"].join("\n")
end
end
context "When transforming unless statements" do
# Note that Puppet 3.1 does not have an "unless x", it is encoded as "if !x"
it "unless true { $a = 10 }" do
astdump(parse("unless true { $a = 10 }")).should == "(if (! true)\n (then (= $a 10)))"
end
it "unless true { $a = 10 } else {$a = 20}" do
astdump(parse("unless true { $a = 10 } else {$a = 20}")).should ==
["(if (! true)",
" (then (= $a 10))",
" (else (= $a 20)))"].join("\n")
end
it "unless true { $a = 10 } elsif false { $a = 15} else {$a = 20} # is illegal" do
expect { parse("unless true { $a = 10 } elsif false { $a = 15} else {$a = 20}")}.to raise_error(Puppet::ParseError)
end
end
context "When transforming selector expressions" do
it "$a = $b ? banana => fruit " do
astdump(parse("$a = $b ? banana => fruit")).should ==
"(= $a (? $b (banana => fruit)))"
end
it "$a = $b ? { banana => fruit}" do
astdump(parse("$a = $b ? { banana => fruit }")).should ==
"(= $a (? $b (banana => fruit)))"
end
it "$a = $b ? { banana => fruit, grape => berry }" do
astdump(parse("$a = $b ? {banana => fruit, grape => berry}")).should ==
"(= $a (? $b (banana => fruit) (grape => berry)))"
end
it "$a = $b ? { banana => fruit, grape => berry, default => wat }" do
astdump(parse("$a = $b ? {banana => fruit, grape => berry, default => wat}")).should ==
"(= $a (? $b (banana => fruit) (grape => berry) (:default => wat)))"
end
it "$a = $b ? { default => wat, banana => fruit, grape => berry, }" do
astdump(parse("$a = $b ? {default => wat, banana => fruit, grape => berry}")).should ==
"(= $a (? $b (:default => wat) (banana => fruit) (grape => berry)))"
end
end
context "When transforming case statements" do
it "case $a { a : {}}" do
astdump(parse("case $a { a : {}}")).should ==
["(case $a",
" (when (a) (then ())))"
].join("\n")
end
it "case $a { /.*/ : {}}" do
astdump(parse("case $a { /.*/ : {}}")).should ==
["(case $a",
" (when (/.*/) (then ())))"
].join("\n")
end
it "case $a { a, b : {}}" do
astdump(parse("case $a { a, b : {}}")).should ==
["(case $a",
" (when (a b) (then ())))"
].join("\n")
end
it "case $a { a, b : {} default : {}}" do
astdump(parse("case $a { a, b : {} default : {}}")).should ==
["(case $a",
" (when (a b) (then ()))",
" (when (:default) (then ())))"
].join("\n")
end
it "case $a { a : {$b = 10 $c = 20}}" do
astdump(parse("case $a { a : {$b = 10 $c = 20}}")).should ==
["(case $a",
" (when (a) (then (block (= $b 10) (= $c 20)))))"
].join("\n")
end
end
- context "When transforming imports" do
- it "import 'foo'" do
- astdump(parse("import 'foo'")).should == ":nop"
- end
-
- it "import 'foo', 'bar'" do
- astdump(parse("import 'foo', 'bar'")).should == ":nop"
- end
- end
end
diff --git a/spec/unit/pops/transformer/transform_containers_spec.rb b/spec/unit/pops/transformer/transform_containers_spec.rb
index 8c65e2bcc..c57998eed 100644
--- a/spec/unit/pops/transformer/transform_containers_spec.rb
+++ b/spec/unit/pops/transformer/transform_containers_spec.rb
@@ -1,182 +1,190 @@
#! /usr/bin/env ruby
require 'spec_helper'
require 'puppet/pops'
# relative to this spec file (./) does not work as this file is loaded by rspec
require File.join(File.dirname(__FILE__), '/transformer_rspec_helper')
describe "transformation to Puppet AST for containers" do
include TransformerRspecHelper
context "When transforming file scope" do
it "$a = 10 $b = 20" do
astdump(parse("$a = 10 $b = 20")).should == "(block (= $a 10) (= $b 20))"
end
it "$a = 10" do
astdump(parse("$a = 10")).should == "(= $a 10)"
end
end
context "When transforming class" do
it "class foo {}" do
astdump(parse("class foo {}")).should == "(class foo ())"
end
it "class foo::bar {}" do
astdump(parse("class foo::bar {}")).should == "(class foo::bar ())"
end
it "class foo inherits bar {}" do
astdump(parse("class foo inherits bar {}")).should == "(class foo (inherits bar) ())"
end
it "class foo($a) {}" do
astdump(parse("class foo($a) {}")).should == "(class foo (parameters a) ())"
end
it "class foo($a, $b) {}" do
astdump(parse("class foo($a, $b) {}")).should == "(class foo (parameters a b) ())"
end
it "class foo($a, $b=10) {}" do
astdump(parse("class foo($a, $b=10) {}")).should == "(class foo (parameters a (= b 10)) ())"
end
it "class foo($a, $b) inherits belgo::bar {}" do
astdump(parse("class foo($a, $b) inherits belgo::bar{}")).should == "(class foo (inherits belgo::bar) (parameters a b) ())"
end
it "class foo {$a = 10 $b = 20}" do
astdump(parse("class foo {$a = 10 $b = 20}")).should == "(class foo (block (= $a 10) (= $b 20)))"
end
context "it should handle '3x weirdness'" do
it "class class {} # a class named 'class'" do
# Not as much weird as confusing that it is possible to name a class 'class'. Can have
# a very confusing effect when resolving relative names, getting the global hardwired "Class"
# instead of some foo::class etc.
# This is allowed in 3.x.
+ expect {
astdump(parse("class class {}")).should == "(class class ())"
+ }.to raise_error(/is not a valid classname/)
end
it "class default {} # a class named 'default'" do
# The weirdness here is that a class can inherit 'default' but not declare a class called default.
# (It will work with relative names i.e. foo::default though). The whole idea with keywords as
# names is flawed to begin with - it generally just a very bad idea.
expect { dump(parse("class default {}")).should == "(class default ())" }.to raise_error(Puppet::ParseError)
end
it "class foo::default {} # a nested name 'default'" do
astdump(parse("class foo::default {}")).should == "(class foo::default ())"
end
it "class class inherits default {} # inherits default", :broken => true do
astdump(parse("class class inherits default {}")).should == "(class class (inherits default) ())"
end
it "class class inherits default {} # inherits default" do
# TODO: See previous test marked as :broken=>true, it is actually this test (result) that is wacky,
# this because a class is named at parse time (since class evaluation is lazy, the model must have the
# full class name for nested classes - only, it gets this wrong when a class is named "class" - or at least
# I think it is wrong.)
#
- astdump(parse("class class inherits default {}")).should == "(class class::class (inherits default) ())"
+ expect {
+ astdump(parse("class class inherits default {}")).should == "(class class::class (inherits default) ())"
+ }.to raise_error(/is not a valid classname/)
end
it "class foo inherits class" do
- astdump(parse("class foo inherits class {}")).should == "(class foo (inherits class) ())"
+ expect {
+ astdump(parse("class foo inherits class {}")).should == "(class foo (inherits class) ())"
+ }.to raise_error(/is not a valid classname/)
end
end
end
context "When transforming define" do
it "define foo {}" do
astdump(parse("define foo {}")).should == "(define foo ())"
end
it "define foo::bar {}" do
astdump(parse("define foo::bar {}")).should == "(define foo::bar ())"
end
it "define foo($a) {}" do
astdump(parse("define foo($a) {}")).should == "(define foo (parameters a) ())"
end
it "define foo($a, $b) {}" do
astdump(parse("define foo($a, $b) {}")).should == "(define foo (parameters a b) ())"
end
it "define foo($a, $b=10) {}" do
astdump(parse("define foo($a, $b=10) {}")).should == "(define foo (parameters a (= b 10)) ())"
end
it "define foo {$a = 10 $b = 20}" do
astdump(parse("define foo {$a = 10 $b = 20}")).should == "(define foo (block (= $a 10) (= $b 20)))"
end
context "it should handle '3x weirdness'" do
it "define class {} # a define named 'class'" do
# This is weird because Class already exists, and instantiating this define will probably not
# work
- astdump(parse("define class {}")).should == "(define class ())"
+ expect {
+ astdump(parse("define class {}")).should == "(define class ())"
+ }.to raise_error(/is not a valid classname/)
end
it "define default {} # a define named 'default'" do
# Check unwanted ability to define 'default'.
# The expression below is not allowed (which is good).
#
expect { dump(parse("define default {}")).should == "(define default ())"}.to raise_error(Puppet::ParseError)
end
end
end
context "When transforming node" do
it "node foo {}" do
# AST can not differentiate between bare word and string
astdump(parse("node foo {}")).should == "(node (matches 'foo') ())"
end
it "node foo, x.bar, default {}" do
# AST can not differentiate between bare word and string
astdump(parse("node foo, x_bar, default {}")).should == "(node (matches 'foo' 'x_bar' :default) ())"
end
it "node 'foo' {}" do
# AST can not differentiate between bare word and string
astdump(parse("node 'foo' {}")).should == "(node (matches 'foo') ())"
end
it "node foo inherits x::bar {}" do
# AST can not differentiate between bare word and string
astdump(parse("node foo inherits x_bar {}")).should == "(node (matches 'foo') (parent x_bar) ())"
end
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) ())"
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) ())"
end
it "node /web.*/ {}" do
astdump(parse("node /web.*/ {}")).should == "(node (matches /web.*/) ())"
end
it "node /web.*/, /do\.wop.*/, and.so.on {}" do
astdump(parse("node /web.*/, /do\.wop.*/, 'and.so.on' {}")).should == "(node (matches /web.*/ /do\.wop.*/ 'and.so.on') ())"
end
it "node wat inherits /apache.*/ {}" do
- expect { parse("node wat inherits /apache.*/ {}")}.to raise_error(Puppet::ParseError)
+ astdump(parse("node wat inherits /apache.*/ {}")).should == "(node (matches 'wat') (parent /apache.*/) ())"
end
it "node foo inherits bar {$a = 10 $b = 20}" do
# AST can not differentiate between bare word and string
astdump(parse("node foo inherits bar {$a = 10 $b = 20}")).should == "(node (matches 'foo') (parent bar) (block (= $a 10) (= $b 20)))"
end
end
end
diff --git a/spec/unit/pops/types/enumeration_spec.rb b/spec/unit/pops/types/enumeration_spec.rb
new file mode 100644
index 000000000..ecf5df596
--- /dev/null
+++ b/spec/unit/pops/types/enumeration_spec.rb
@@ -0,0 +1,50 @@
+require 'spec_helper'
+require 'puppet/pops'
+
+describe 'The enumeration support' do
+ it 'produces an enumerator for Array' do
+ expect(Puppet::Pops::Types::Enumeration.enumerator([1,2,3]).respond_to?(:next)).to eql(true)
+ end
+
+ it 'produces an enumerator for Hash' do
+ expect(Puppet::Pops::Types::Enumeration.enumerator({:a=>1}).respond_to?(:next)).to eql(true)
+ end
+
+ it 'produces a char enumerator for String' do
+ enum = Puppet::Pops::Types::Enumeration.enumerator("abc")
+ expect(enum.respond_to?(:next)).to eql(true)
+ expect(enum.next).to eql('a')
+ end
+
+ it 'produces an enumerator for integer times' do
+ enum = Puppet::Pops::Types::Enumeration.enumerator(2)
+ expect(enum.next).to eql(0)
+ expect(enum.next).to eql(1)
+ expect{enum.next}.to raise_error(StopIteration)
+ end
+
+ it 'produces an enumerator for Integer range' do
+ range = Puppet::Pops::Types::TypeFactory.range(1,2)
+ enum = Puppet::Pops::Types::Enumeration.enumerator(range)
+ expect(enum.next).to eql(1)
+ expect(enum.next).to eql(2)
+ expect{enum.next}.to raise_error(StopIteration)
+ end
+
+ it 'does not produce an enumerator for infinite Integer range' do
+ range = Puppet::Pops::Types::TypeFactory.range(1,:default)
+ enum = Puppet::Pops::Types::Enumeration.enumerator(range)
+ expect(enum).to be_nil
+ range = Puppet::Pops::Types::TypeFactory.range(:default,2)
+ enum = Puppet::Pops::Types::Enumeration.enumerator(range)
+ expect(enum).to be_nil
+ end
+
+ [3.14, /.*/, true, false, nil, :something].each do |x|
+ it "does not produce an enumerator for object of type #{x.class}" do
+ enum = Puppet::Pops::Types::Enumeration.enumerator(x)
+ expect(enum).to be_nil
+ end
+ end
+
+end
diff --git a/spec/unit/pops/types/type_calculator_spec.rb b/spec/unit/pops/types/type_calculator_spec.rb
index 7ee47d648..d3c51662f 100644
--- a/spec/unit/pops/types/type_calculator_spec.rb
+++ b/spec/unit/pops/types/type_calculator_spec.rb
@@ -1,484 +1,1459 @@
require 'spec_helper'
require 'puppet/pops'
describe 'The type calculator' do
let(:calculator) { Puppet::Pops::Types::TypeCalculator.new() }
+ def range_t(from, to)
+ t = Puppet::Pops::Types::PIntegerType.new
+ t.from = from
+ t.to = to
+ t
+ end
+
+ def pattern_t(*patterns)
+ Puppet::Pops::Types::TypeFactory.pattern(*patterns)
+ end
+
+ def regexp_t(pattern)
+ Puppet::Pops::Types::TypeFactory.regexp(pattern)
+ end
+
+ def string_t(*strings)
+ Puppet::Pops::Types::TypeFactory.string(*strings)
+ end
+
+ def enum_t(*strings)
+ Puppet::Pops::Types::TypeFactory.enum(*strings)
+ end
+
+ def variant_t(*types)
+ Puppet::Pops::Types::TypeFactory.variant(*types)
+ end
+
+ def integer_t()
+ Puppet::Pops::Types::TypeFactory.integer()
+ end
+
+ def array_t(t)
+ Puppet::Pops::Types::TypeFactory.array_of(t)
+ end
+
+ def hash_t(k,v)
+ Puppet::Pops::Types::TypeFactory.hash_of(v, k)
+ end
+
+ def data_t()
+ Puppet::Pops::Types::TypeFactory.data()
+ end
+
+ def factory()
+ Puppet::Pops::Types::TypeFactory
+ end
+
+ def collection_t()
+ Puppet::Pops::Types::TypeFactory.collection()
+ end
+
+ def tuple_t(*types)
+ Puppet::Pops::Types::TypeFactory.tuple(*types)
+ end
+
+ def struct_t(type_hash)
+ Puppet::Pops::Types::TypeFactory.struct(type_hash)
+ end
+
+ def types
+ Puppet::Pops::Types
+ end
+
+ shared_context "types_setup" do
+
+ def all_types
+ [ Puppet::Pops::Types::PObjectType,
+ Puppet::Pops::Types::PNilType,
+ Puppet::Pops::Types::PDataType,
+ Puppet::Pops::Types::PScalarType,
+ Puppet::Pops::Types::PStringType,
+ Puppet::Pops::Types::PNumericType,
+ Puppet::Pops::Types::PIntegerType,
+ Puppet::Pops::Types::PFloatType,
+ Puppet::Pops::Types::PRegexpType,
+ Puppet::Pops::Types::PBooleanType,
+ Puppet::Pops::Types::PCollectionType,
+ Puppet::Pops::Types::PArrayType,
+ Puppet::Pops::Types::PHashType,
+ Puppet::Pops::Types::PRubyType,
+ Puppet::Pops::Types::PHostClassType,
+ Puppet::Pops::Types::PResourceType,
+ Puppet::Pops::Types::PPatternType,
+ Puppet::Pops::Types::PEnumType,
+ Puppet::Pops::Types::PVariantType,
+ Puppet::Pops::Types::PStructType,
+ Puppet::Pops::Types::PTupleType,
+ ]
+ end
+
+ def scalar_types
+ # PVariantType is also scalar, if its types are all Scalar
+ [
+ Puppet::Pops::Types::PScalarType,
+ Puppet::Pops::Types::PStringType,
+ Puppet::Pops::Types::PNumericType,
+ Puppet::Pops::Types::PIntegerType,
+ Puppet::Pops::Types::PFloatType,
+ Puppet::Pops::Types::PRegexpType,
+ Puppet::Pops::Types::PBooleanType,
+ Puppet::Pops::Types::PPatternType,
+ Puppet::Pops::Types::PEnumType,
+ ]
+ end
+
+ def numeric_types
+ # PVariantType is also numeric, if its types are all numeric
+ [
+ Puppet::Pops::Types::PNumericType,
+ Puppet::Pops::Types::PIntegerType,
+ Puppet::Pops::Types::PFloatType,
+ ]
+ end
+
+ def string_types
+ # PVariantType is also string type, if its types are all compatible
+ [
+ Puppet::Pops::Types::PStringType,
+ Puppet::Pops::Types::PPatternType,
+ Puppet::Pops::Types::PEnumType,
+ ]
+ end
+
+ def collection_types
+ # PVariantType is also string type, if its types are all compatible
+ [
+ Puppet::Pops::Types::PCollectionType,
+ Puppet::Pops::Types::PHashType,
+ Puppet::Pops::Types::PArrayType,
+ Puppet::Pops::Types::PStructType,
+ Puppet::Pops::Types::PTupleType,
+ ]
+ end
+
+ def data_compatible_types
+ result = scalar_types
+ result << Puppet::Pops::Types::PDataType
+ result << array_t(types::PDataType.new)
+ result << types::TypeFactory.hash_of_data
+ result << Puppet::Pops::Types::PNilType
+ tmp = tuple_t(types::PDataType.new)
+ result << (tmp)
+ tmp.size_type = range_t(0, nil)
+ result
+ end
+
+ def type_from_class(c)
+ c.is_a?(Class) ? c.new : c
+ end
+ end
+
context 'when inferring ruby' do
it 'fixnum translates to PIntegerType' do
calculator.infer(1).class.should == Puppet::Pops::Types::PIntegerType
end
it 'large fixnum (or bignum depending on architecture) translates to PIntegerType' do
calculator.infer(2**33).class.should == Puppet::Pops::Types::PIntegerType
end
it 'float translates to PFloatType' do
calculator.infer(1.3).class.should == Puppet::Pops::Types::PFloatType
end
it 'string translates to PStringType' do
calculator.infer('foo').class.should == Puppet::Pops::Types::PStringType
end
+ it 'inferred string type knows the string value' do
+ t = calculator.infer('foo')
+ t.class.should == Puppet::Pops::Types::PStringType
+ t.values.should == ['foo']
+ end
+
it 'boolean true translates to PBooleanType' do
calculator.infer(true).class.should == Puppet::Pops::Types::PBooleanType
end
it 'boolean false translates to PBooleanType' do
calculator.infer(false).class.should == Puppet::Pops::Types::PBooleanType
end
- it 'regexp translates to PPatternType' do
- calculator.infer(/^a regular exception$/).class.should == Puppet::Pops::Types::PPatternType
+ it 'regexp translates to PRegexpType' do
+ calculator.infer(/^a regular expression$/).class.should == Puppet::Pops::Types::PRegexpType
end
it 'nil translates to PNilType' do
calculator.infer(nil).class.should == Puppet::Pops::Types::PNilType
end
+ it ':undef translates to PNilType' do
+ calculator.infer(:undef).class.should == Puppet::Pops::Types::PNilType
+ end
+
it 'an instance of class Foo translates to PRubyType[Foo]' do
class Foo
end
t = calculator.infer(Foo.new)
t.class.should == Puppet::Pops::Types::PRubyType
t.ruby_class.should == 'Foo'
end
context 'array' do
it 'translates to PArrayType' do
calculator.infer([1,2]).class.should == Puppet::Pops::Types::PArrayType
end
it 'with fixnum values translates to PArrayType[PIntegerType]' do
calculator.infer([1,2]).element_type.class.should == Puppet::Pops::Types::PIntegerType
end
it 'with 32 and 64 bit integer values translates to PArrayType[PIntegerType]' do
calculator.infer([1,2**33]).element_type.class.should == Puppet::Pops::Types::PIntegerType
end
+ it 'Range of integer values are computed' do
+ t = calculator.infer([-3,0,42]).element_type
+ t.class.should == Puppet::Pops::Types::PIntegerType
+ t.from.should == -3
+ t.to.should == 42
+ end
+
+ it "Compound string values are computed" do
+ t = calculator.infer(['a','b', 'c']).element_type
+ t.class.should == Puppet::Pops::Types::PStringType
+ t.values.should == ['a', 'b', 'c']
+ end
+
it 'with fixnum and float values translates to PArrayType[PNumericType]' do
calculator.infer([1,2.0]).element_type.class.should == Puppet::Pops::Types::PNumericType
end
- it 'with fixnum and string values translates to PArrayType[PLiteralType]' do
- calculator.infer([1,'two']).element_type.class.should == Puppet::Pops::Types::PLiteralType
+ it 'with fixnum and string values translates to PArrayType[PScalarType]' do
+ calculator.infer([1,'two']).element_type.class.should == Puppet::Pops::Types::PScalarType
end
- it 'with float and string values translates to PArrayType[PLiteralType]' do
- calculator.infer([1.0,'two']).element_type.class.should == Puppet::Pops::Types::PLiteralType
+ it 'with float and string values translates to PArrayType[PScalarType]' do
+ calculator.infer([1.0,'two']).element_type.class.should == Puppet::Pops::Types::PScalarType
end
- it 'with fixnum, float, and string values translates to PArrayType[PLiteralType]' do
- calculator.infer([1, 2.0,'two']).element_type.class.should == Puppet::Pops::Types::PLiteralType
+ it 'with fixnum, float, and string values translates to PArrayType[PScalarType]' do
+ calculator.infer([1, 2.0,'two']).element_type.class.should == Puppet::Pops::Types::PScalarType
end
- it 'with fixnum and regexp values translates to PArrayType[PLiteralType]' do
- calculator.infer([1, /two/]).element_type.class.should == Puppet::Pops::Types::PLiteralType
+ it 'with fixnum and regexp values translates to PArrayType[PScalarType]' do
+ calculator.infer([1, /two/]).element_type.class.should == Puppet::Pops::Types::PScalarType
end
- it 'with string and regexp values translates to PArrayType[PLiteralType]' do
- calculator.infer(['one', /two/]).element_type.class.should == Puppet::Pops::Types::PLiteralType
+ it 'with string and regexp values translates to PArrayType[PScalarType]' do
+ calculator.infer(['one', /two/]).element_type.class.should == Puppet::Pops::Types::PScalarType
end
it 'with string and symbol values translates to PArrayType[PObjectType]' do
calculator.infer(['one', :two]).element_type.class.should == Puppet::Pops::Types::PObjectType
end
it 'with fixnum and nil values translates to PArrayType[PIntegerType]' do
calculator.infer([1, nil]).element_type.class.should == Puppet::Pops::Types::PIntegerType
end
it 'with arrays of string values translates to PArrayType[PArrayType[PStringType]]' do
et = calculator.infer([['first' 'array'], ['second','array']])
et.class.should == Puppet::Pops::Types::PArrayType
et = et.element_type
et.class.should == Puppet::Pops::Types::PArrayType
et = et.element_type
et.class.should == Puppet::Pops::Types::PStringType
end
- it 'with array of string values and array of fixnums translates to PArrayType[PArrayType[PLiteralType]]' do
+ it 'with array of string values and array of fixnums translates to PArrayType[PArrayType[PScalarType]]' do
et = calculator.infer([['first' 'array'], [1,2]])
et.class.should == Puppet::Pops::Types::PArrayType
et = et.element_type
et.class.should == Puppet::Pops::Types::PArrayType
et = et.element_type
- et.class.should == Puppet::Pops::Types::PLiteralType
+ et.class.should == Puppet::Pops::Types::PScalarType
end
it 'with hashes of string values translates to PArrayType[PHashType[PStringType]]' do
et = calculator.infer([{:first => 'first', :second => 'second' }, {:first => 'first', :second => 'second' }])
et.class.should == Puppet::Pops::Types::PArrayType
et = et.element_type
et.class.should == Puppet::Pops::Types::PHashType
et = et.element_type
et.class.should == Puppet::Pops::Types::PStringType
end
- it 'with hash of string values and hash of fixnums translates to PArrayType[PHashType[PLiteralType]]' do
+ it 'with hash of string values and hash of fixnums translates to PArrayType[PHashType[PScalarType]]' do
et = calculator.infer([{:first => 'first', :second => 'second' }, {:first => 1, :second => 2 }])
et.class.should == Puppet::Pops::Types::PArrayType
et = et.element_type
et.class.should == Puppet::Pops::Types::PHashType
et = et.element_type
- et.class.should == Puppet::Pops::Types::PLiteralType
+ et.class.should == Puppet::Pops::Types::PScalarType
end
end
context 'hash' do
it 'translates to PHashType' do
calculator.infer({:first => 1, :second => 2}).class.should == Puppet::Pops::Types::PHashType
end
it 'with symbolic keys translates to PHashType[PRubyType[Symbol],value]' do
k = calculator.infer({:first => 1, :second => 2}).key_type
k.class.should == Puppet::Pops::Types::PRubyType
k.ruby_class.should == 'Symbol'
end
it 'with string keys translates to PHashType[PStringType,value]' do
calculator.infer({'first' => 1, 'second' => 2}).key_type.class.should == Puppet::Pops::Types::PStringType
end
it 'with fixnum values translates to PHashType[key,PIntegerType]' do
calculator.infer({:first => 1, :second => 2}).element_type.class.should == Puppet::Pops::Types::PIntegerType
end
end
+
end
- context 'when testing if x is assignable to y' do
- it 'should allow all object types to PObjectType' do
- t = Puppet::Pops::Types::PObjectType.new()
- calculator.assignable?(t, t).should() == true
- calculator.assignable?(t,Puppet::Pops::Types::PNilType.new()).should() == true
- calculator.assignable?(t,Puppet::Pops::Types::PDataType.new()).should() == true
- calculator.assignable?(t,Puppet::Pops::Types::PLiteralType.new()).should() == true
- calculator.assignable?(t,Puppet::Pops::Types::PStringType.new()).should() == true
- calculator.assignable?(t,Puppet::Pops::Types::PNumericType.new()).should() == true
- calculator.assignable?(t,Puppet::Pops::Types::PIntegerType.new()).should() == true
- calculator.assignable?(t,Puppet::Pops::Types::PFloatType.new()).should() == true
- calculator.assignable?(t,Puppet::Pops::Types::PPatternType.new()).should() == true
- calculator.assignable?(t,Puppet::Pops::Types::PBooleanType.new()).should() == true
- calculator.assignable?(t,Puppet::Pops::Types::PCollectionType.new()).should() == true
- calculator.assignable?(t,Puppet::Pops::Types::PArrayType.new()).should() == true
- calculator.assignable?(t,Puppet::Pops::Types::PHashType.new()).should() == true
- calculator.assignable?(t,Puppet::Pops::Types::PRubyType.new()).should() == true
- end
-
- it 'should reject PObjectType to less generic types' do
- t = Puppet::Pops::Types::PObjectType.new()
- calculator.assignable?(Puppet::Pops::Types::PDataType.new(), t).should() == false
- calculator.assignable?(Puppet::Pops::Types::PLiteralType.new(), t).should() == false
- calculator.assignable?(Puppet::Pops::Types::PStringType.new(), t).should() == false
- calculator.assignable?(Puppet::Pops::Types::PNumericType.new(), t).should() == false
- calculator.assignable?(Puppet::Pops::Types::PIntegerType.new(), t).should() == false
- calculator.assignable?(Puppet::Pops::Types::PFloatType.new(), t).should() == false
- calculator.assignable?(Puppet::Pops::Types::PPatternType.new(), t).should() == false
- calculator.assignable?(Puppet::Pops::Types::PBooleanType.new(), t).should() == false
- calculator.assignable?(Puppet::Pops::Types::PCollectionType.new(), t).should() == false
- calculator.assignable?(Puppet::Pops::Types::PArrayType.new(), t).should() == false
- calculator.assignable?(Puppet::Pops::Types::PHashType.new(), t).should() == false
- calculator.assignable?(Puppet::Pops::Types::PRubyType.new(), t).should() == false
- end
-
- it 'should allow all data types, array, and hash to PDataType' do
- t = Puppet::Pops::Types::PDataType.new()
- calculator.assignable?(t, t).should() == true
- calculator.assignable?(t,Puppet::Pops::Types::PLiteralType.new()).should() == true
- calculator.assignable?(t,Puppet::Pops::Types::PStringType.new()).should() == true
- calculator.assignable?(t,Puppet::Pops::Types::PNumericType.new()).should() == true
- calculator.assignable?(t,Puppet::Pops::Types::PIntegerType.new()).should() == true
- calculator.assignable?(t,Puppet::Pops::Types::PFloatType.new()).should() == true
- calculator.assignable?(t,Puppet::Pops::Types::PPatternType.new()).should() == true
- calculator.assignable?(t,Puppet::Pops::Types::PBooleanType.new()).should() == true
- calculator.assignable?(t,Puppet::Pops::Types::PArrayType.new()).should() == true
- calculator.assignable?(t,Puppet::Pops::Types::PHashType.new()).should() == true
- end
-
- it 'should reject PDataType to less generic data types' do
- t = Puppet::Pops::Types::PDataType.new()
- calculator.assignable?(Puppet::Pops::Types::PLiteralType.new(), t).should() == false
- calculator.assignable?(Puppet::Pops::Types::PStringType.new(), t).should() == false
- calculator.assignable?(Puppet::Pops::Types::PNumericType.new(), t).should() == false
- calculator.assignable?(Puppet::Pops::Types::PIntegerType.new(), t).should() == false
- calculator.assignable?(Puppet::Pops::Types::PFloatType.new(), t).should() == false
- calculator.assignable?(Puppet::Pops::Types::PPatternType.new(), t).should() == false
- calculator.assignable?(Puppet::Pops::Types::PBooleanType.new(), t).should() == false
- end
-
- it 'should reject PDataType to non data types' do
- t = Puppet::Pops::Types::PDataType.new()
- calculator.assignable?(Puppet::Pops::Types::PCollectionType.new(),t).should() == false
- calculator.assignable?(Puppet::Pops::Types::PArrayType.new(),t).should() == false
- calculator.assignable?(Puppet::Pops::Types::PHashType.new(),t).should() == false
- calculator.assignable?(Puppet::Pops::Types::PRubyType.new(), t).should() == false
- end
-
- it 'should allow all literal types to PLiteralType' do
- t = Puppet::Pops::Types::PLiteralType.new()
- calculator.assignable?(t, t).should() == true
- calculator.assignable?(t,Puppet::Pops::Types::PStringType.new()).should() == true
- calculator.assignable?(t,Puppet::Pops::Types::PNumericType.new()).should() == true
- calculator.assignable?(t,Puppet::Pops::Types::PIntegerType.new()).should() == true
- calculator.assignable?(t,Puppet::Pops::Types::PFloatType.new()).should() == true
- calculator.assignable?(t,Puppet::Pops::Types::PPatternType.new()).should() == true
- calculator.assignable?(t,Puppet::Pops::Types::PBooleanType.new()).should() == true
- end
-
- it 'should reject PLiteralType to less generic literal types' do
- t = Puppet::Pops::Types::PLiteralType.new()
- calculator.assignable?(Puppet::Pops::Types::PStringType.new(), t).should() == false
- calculator.assignable?(Puppet::Pops::Types::PNumericType.new(), t).should() == false
- calculator.assignable?(Puppet::Pops::Types::PIntegerType.new(), t).should() == false
- calculator.assignable?(Puppet::Pops::Types::PFloatType.new(), t).should() == false
- calculator.assignable?(Puppet::Pops::Types::PPatternType.new(), t).should() == false
- calculator.assignable?(Puppet::Pops::Types::PBooleanType.new(), t).should() == false
- end
-
- it 'should reject PLiteralType to non literal types' do
- t = Puppet::Pops::Types::PLiteralType.new()
- calculator.assignable?(Puppet::Pops::Types::PCollectionType.new(), t).should() == false
- calculator.assignable?(Puppet::Pops::Types::PArrayType.new(), t).should() == false
- calculator.assignable?(Puppet::Pops::Types::PHashType.new(), t).should() == false
- calculator.assignable?(Puppet::Pops::Types::PRubyType.new(), t).should() == false
- end
-
- it 'should allow all numeric types to PNumericType' do
- t = Puppet::Pops::Types::PNumericType.new()
- calculator.assignable?(t, t).should() == true
- calculator.assignable?(t, Puppet::Pops::Types::PIntegerType.new()).should() == true
- calculator.assignable?(t, Puppet::Pops::Types::PFloatType.new()).should() == true
- end
-
- it 'should reject PNumericType to less generic numeric types' do
- t = Puppet::Pops::Types::PNumericType.new()
- calculator.assignable?(Puppet::Pops::Types::PIntegerType.new(), t).should() == false
- calculator.assignable?(Puppet::Pops::Types::PFloatType.new(), t).should() == false
- end
-
- it 'should reject PNumericType to non numeric types' do
- t = Puppet::Pops::Types::PNumericType.new()
- calculator.assignable?(Puppet::Pops::Types::PStringType.new(), t).should() == false
- calculator.assignable?(Puppet::Pops::Types::PPatternType.new(), t).should() == false
- calculator.assignable?(Puppet::Pops::Types::PBooleanType.new(), t).should() == false
- calculator.assignable?(Puppet::Pops::Types::PCollectionType.new(), t).should() == false
- calculator.assignable?(Puppet::Pops::Types::PArrayType.new(), t).should() == false
- calculator.assignable?(Puppet::Pops::Types::PHashType.new(), t).should() == false
- calculator.assignable?(Puppet::Pops::Types::PRubyType.new(), t).should() == false
- end
-
- it 'should allow all collection types to PCollectionType' do
- t = Puppet::Pops::Types::PCollectionType.new()
- calculator.assignable?(t, t).should() == true
- calculator.assignable?(t, Puppet::Pops::Types::PArrayType.new()).should() == true
- calculator.assignable?(t, Puppet::Pops::Types::PHashType.new()).should() == true
- end
-
- it 'should reject PCollectionType to less generic collection types' do
- t = Puppet::Pops::Types::PCollectionType.new()
- calculator.assignable?(Puppet::Pops::Types::PArrayType.new(), t).should() == false
- calculator.assignable?(Puppet::Pops::Types::PHashType.new(), t).should() == false
- end
-
- it 'should reject PCollectionType to non collection types' do
- t = Puppet::Pops::Types::PCollectionType.new()
- calculator.assignable?(Puppet::Pops::Types::PDataType.new(), t).should() == false
- calculator.assignable?(Puppet::Pops::Types::PLiteralType.new(), t).should() == false
- calculator.assignable?(Puppet::Pops::Types::PStringType.new(), t).should() == false
- calculator.assignable?(Puppet::Pops::Types::PNumericType.new(), t).should() == false
- calculator.assignable?(Puppet::Pops::Types::PIntegerType.new(), t).should() == false
- calculator.assignable?(Puppet::Pops::Types::PFloatType.new(), t).should() == false
- calculator.assignable?(Puppet::Pops::Types::PPatternType.new(), t).should() == false
- calculator.assignable?(Puppet::Pops::Types::PBooleanType.new(), t).should() == false
- calculator.assignable?(Puppet::Pops::Types::PRubyType.new(), t).should() == false
- end
-
- it 'should reject PArrayType to non array type collections' do
- t = Puppet::Pops::Types::PArrayType.new()
- calculator.assignable?(Puppet::Pops::Types::PHashType.new(), t).should() == false
+ context 'patterns' do
+ it "constructs a PPatternType" do
+ t = pattern_t('a(b)c')
+ t.class.should == Puppet::Pops::Types::PPatternType
+ t.patterns.size.should == 1
+ t.patterns[0].class.should == Puppet::Pops::Types::PRegexpType
+ t.patterns[0].pattern.should == 'a(b)c'
+ t.patterns[0].regexp.match('abc')[1].should == 'b'
end
- it 'should reject PHashType to non hash type collections' do
- t = Puppet::Pops::Types::PHashType.new()
- calculator.assignable?(Puppet::Pops::Types::PArrayType.new(), t).should() == false
+ it "constructs a PStringType with multiple strings" do
+ t = string_t('a', 'b', 'c', 'abc')
+ t.values.should == ['a', 'b', 'c', 'abc']
+ end
+ end
+
+ # Deal with cases not covered by computing common type
+ context 'when computing common type' do
+ it 'computes given resource type commonality' do
+ r1 = Puppet::Pops::Types::PResourceType.new()
+ r1.type_name = 'File'
+ r2 = Puppet::Pops::Types::PResourceType.new()
+ r2.type_name = 'File'
+ calculator.string(calculator.common_type(r1, r2)).should == "File"
+
+ r2 = Puppet::Pops::Types::PResourceType.new()
+ r2.type_name = 'File'
+ r2.title = '/tmp/foo'
+ calculator.string(calculator.common_type(r1, r2)).should == "File"
+
+ r1 = Puppet::Pops::Types::PResourceType.new()
+ r1.type_name = 'File'
+ r1.title = '/tmp/foo'
+ calculator.string(calculator.common_type(r1, r2)).should == "File['/tmp/foo']"
+
+ r1 = Puppet::Pops::Types::PResourceType.new()
+ r1.type_name = 'File'
+ r1.title = '/tmp/bar'
+ calculator.string(calculator.common_type(r1, r2)).should == "File"
+
+ r2 = Puppet::Pops::Types::PResourceType.new()
+ r2.type_name = 'Package'
+ r2.title = 'apache'
+ calculator.string(calculator.common_type(r1, r2)).should == "Resource"
+ end
+
+ it 'computes given hostclass type commonality' do
+ r1 = Puppet::Pops::Types::PHostClassType.new()
+ r1.class_name = 'foo'
+ r2 = Puppet::Pops::Types::PHostClassType.new()
+ r2.class_name = 'foo'
+ calculator.string(calculator.common_type(r1, r2)).should == "Class[foo]"
+
+ r2 = Puppet::Pops::Types::PHostClassType.new()
+ r2.class_name = 'bar'
+ calculator.string(calculator.common_type(r1, r2)).should == "Class"
+
+ r2 = Puppet::Pops::Types::PHostClassType.new()
+ calculator.string(calculator.common_type(r1, r2)).should == "Class"
+
+ r1 = Puppet::Pops::Types::PHostClassType.new()
+ calculator.string(calculator.common_type(r1, r2)).should == "Class"
+ end
+
+ it 'computes pattern commonality' do
+ t1 = pattern_t('abc')
+ t2 = pattern_t('xyz')
+ common_t = calculator.common_type(t1,t2)
+ common_t.class.should == Puppet::Pops::Types::PPatternType
+ common_t.patterns.map { |pr| pr.pattern }.should == ['abc', 'xyz']
+ calculator.string(common_t).should == "Pattern[/abc/, /xyz/]"
+ end
+
+ it 'computes enum commonality to value set sum' do
+ t1 = enum_t('a', 'b', 'c')
+ t2 = enum_t('x', 'y', 'z')
+ common_t = calculator.common_type(t1, t2)
+ common_t.should == enum_t('a', 'b', 'c', 'x', 'y', 'z')
+ end
+
+ it 'computed variant commonality to type union where added types are not sub-types' do
+ a_t1 = integer_t()
+ a_t2 = enum_t('b')
+ v_a = variant_t(a_t1, a_t2)
+ b_t1 = enum_t('a')
+ v_b = variant_t(b_t1)
+ common_t = calculator.common_type(v_a, v_b)
+ common_t.class.should == Puppet::Pops::Types::PVariantType
+ Set.new(common_t.types).should == Set.new([a_t1, a_t2, b_t1])
+ end
+
+ it 'computed variant commonality to type union where added types are sub-types' do
+ a_t1 = integer_t()
+ a_t2 = string_t()
+ v_a = variant_t(a_t1, a_t2)
+ b_t1 = enum_t('a')
+ v_b = variant_t(b_t1)
+ common_t = calculator.common_type(v_a, v_b)
+ common_t.class.should == Puppet::Pops::Types::PVariantType
+ Set.new(common_t.types).should == Set.new([a_t1, a_t2])
+ end
+ end
+
+ context 'computes assignability' do
+ include_context "types_setup"
+
+ context "for Object, such that" do
+ it 'all types are assignable to Object' do
+ t = Puppet::Pops::Types::PObjectType.new()
+ all_types.each { |t2| t2.new.should be_assignable_to(t) }
+ end
+
+ it 'Object is not assignable to anything but Object' do
+ tested_types = all_types() - [Puppet::Pops::Types::PObjectType]
+ t = Puppet::Pops::Types::PObjectType.new()
+ tested_types.each { |t2| t.should_not be_assignable_to(t2.new) }
+ end
+ end
+
+ context "for Data, such that" do
+ it 'all scalars + array and hash are assignable to Data' do
+ t = Puppet::Pops::Types::PDataType.new()
+ data_compatible_types.each { |t2|
+ type_from_class(t2).should be_assignable_to(t)
+ }
+ end
+
+ it 'a Variant of scalar, hash, or array is assignable to Data' do
+ t = Puppet::Pops::Types::PDataType.new()
+ data_compatible_types.each { |t2| variant_t(type_from_class(t2)).should be_assignable_to(t) }
+ end
+
+ it 'Data is not assignable to any of its subtypes' do
+ t = Puppet::Pops::Types::PDataType.new()
+ types_to_test = data_compatible_types- [Puppet::Pops::Types::PDataType]
+ types_to_test.each {|t2| t.should_not be_assignable_to(type_from_class(t2)) }
+ end
+
+ it 'Data is not assignable to a Variant of Data subtype' do
+ t = Puppet::Pops::Types::PDataType.new()
+ types_to_test = data_compatible_types- [Puppet::Pops::Types::PDataType]
+ types_to_test.each { |t2| t.should_not be_assignable_to(variant_t(type_from_class(t2))) }
+ end
+
+ it 'Data is not assignable to any disjunct type' do
+ tested_types = all_types - [Puppet::Pops::Types::PObjectType, Puppet::Pops::Types::PDataType] - scalar_types
+ t = Puppet::Pops::Types::PDataType.new()
+ tested_types.each {|t2| t.should_not be_assignable_to(t2.new) }
+ end
+ end
+
+ context "for Scalar, such that" do
+ it "all scalars are assignable to Scalar" do
+ t = Puppet::Pops::Types::PScalarType.new()
+ scalar_types.each {|t2| t2.new.should be_assignable_to(t) }
+ end
+
+ it 'Scalar is not assignable to any of its subtypes' do
+ t = Puppet::Pops::Types::PScalarType.new()
+ types_to_test = scalar_types - [Puppet::Pops::Types::PScalarType]
+ types_to_test.each {|t2| t.should_not be_assignable_to(t2.new) }
+ end
+
+ it 'Scalar is not assignable to any disjunct type' do
+ tested_types = all_types - [Puppet::Pops::Types::PObjectType, Puppet::Pops::Types::PDataType] - scalar_types
+ t = Puppet::Pops::Types::PScalarType.new()
+ tested_types.each {|t2| t.should_not be_assignable_to(t2.new) }
+ end
+ end
+
+ context "for Numeric, such that" do
+ it "all numerics are assignable to Numeric" do
+ t = Puppet::Pops::Types::PNumericType.new()
+ numeric_types.each {|t2| t2.new.should be_assignable_to(t) }
+ end
+
+ it 'Numeric is not assignable to any of its subtypes' do
+ t = Puppet::Pops::Types::PNumericType.new()
+ types_to_test = numeric_types - [Puppet::Pops::Types::PNumericType]
+ types_to_test.each {|t2| t.should_not be_assignable_to(t2.new) }
+ end
+
+ it 'Numeric is not assignable to any disjunct type' do
+ tested_types = all_types - [
+ Puppet::Pops::Types::PObjectType,
+ Puppet::Pops::Types::PDataType,
+ Puppet::Pops::Types::PScalarType,
+ ] - numeric_types
+ t = Puppet::Pops::Types::PNumericType.new()
+ tested_types.each {|t2| t.should_not be_assignable_to(t2.new) }
+ end
+ end
+
+ context "for Collection, such that" do
+ it "all collections are assignable to Collection" do
+ t = Puppet::Pops::Types::PCollectionType.new()
+ collection_types.each {|t2| t2.new.should be_assignable_to(t) }
+ end
+
+ it 'Collection is not assignable to any of its subtypes' do
+ t = Puppet::Pops::Types::PCollectionType.new()
+ types_to_test = collection_types - [Puppet::Pops::Types::PCollectionType]
+ types_to_test.each {|t2| t.should_not be_assignable_to(t2.new) }
+ end
+
+ it 'Collection is not assignable to any disjunct type' do
+ tested_types = all_types - [Puppet::Pops::Types::PObjectType] - collection_types
+ t = Puppet::Pops::Types::PCollectionType.new()
+ tested_types.each {|t2| t.should_not be_assignable_to(t2.new) }
+ end
+ end
+
+ context "for Array, such that" do
+ it "Array is not assignable to non Array based Collection type" do
+ t = Puppet::Pops::Types::PArrayType.new()
+ tested_types = collection_types - [
+ Puppet::Pops::Types::PCollectionType,
+ Puppet::Pops::Types::PArrayType,
+ Puppet::Pops::Types::PTupleType]
+ tested_types.each {|t2| t.should_not be_assignable_to(t2.new) }
+ end
+
+ it 'Array is not assignable to any disjunct type' do
+ tested_types = all_types - [
+ Puppet::Pops::Types::PObjectType,
+ Puppet::Pops::Types::PDataType] - collection_types
+ t = Puppet::Pops::Types::PArrayType.new()
+ tested_types.each {|t2| t.should_not be_assignable_to(t2.new) }
+ end
+ end
+
+ context "for Hash, such that" do
+ it "Hash is not assignable to any other Collection type" do
+ t = Puppet::Pops::Types::PHashType.new()
+ tested_types = collection_types - [
+ Puppet::Pops::Types::PCollectionType,
+ Puppet::Pops::Types::PStructType,
+ Puppet::Pops::Types::PHashType]
+ tested_types.each {|t2| t.should_not be_assignable_to(t2.new) }
+ end
+
+ it 'Hash is not assignable to any disjunct type' do
+ tested_types = all_types - [
+ Puppet::Pops::Types::PObjectType,
+ Puppet::Pops::Types::PDataType] - collection_types
+ t = Puppet::Pops::Types::PHashType.new()
+ tested_types.each {|t2| t.should_not be_assignable_to(t2.new) }
+ end
+ end
+
+ context "for Tuple, such that" do
+ it "Tuple is not assignable to any other non Array based Collection type" do
+ t = Puppet::Pops::Types::PTupleType.new()
+ tested_types = collection_types - [
+ Puppet::Pops::Types::PCollectionType,
+ Puppet::Pops::Types::PTupleType,
+ Puppet::Pops::Types::PArrayType]
+ tested_types.each {|t2| t.should_not be_assignable_to(t2.new) }
+ end
+
+ it 'Tuple is not assignable to any disjunct type' do
+ tested_types = all_types - [
+ Puppet::Pops::Types::PObjectType,
+ Puppet::Pops::Types::PDataType] - collection_types
+ t = Puppet::Pops::Types::PTupleType.new()
+ tested_types.each {|t2| t.should_not be_assignable_to(t2.new) }
+ end
+ end
+
+ context "for Struct, such that" do
+ it "Struct is not assignable to any other non Hashed based Collection type" do
+ t = Puppet::Pops::Types::PStructType.new()
+ tested_types = collection_types - [
+ Puppet::Pops::Types::PCollectionType,
+ Puppet::Pops::Types::PStructType,
+ Puppet::Pops::Types::PHashType]
+ tested_types.each {|t2| t.should_not be_assignable_to(t2.new) }
+ end
+
+ it 'Struct is not assignable to any disjunct type' do
+ tested_types = all_types - [
+ Puppet::Pops::Types::PObjectType,
+ Puppet::Pops::Types::PDataType] - collection_types
+ t = Puppet::Pops::Types::PStructType.new()
+ tested_types.each {|t2| t.should_not be_assignable_to(t2.new) }
+ end
+ end
+
+ it 'should recognize mapped ruby types' do
+ { Integer => Puppet::Pops::Types::PIntegerType.new,
+ Fixnum => Puppet::Pops::Types::PIntegerType.new,
+ Bignum => Puppet::Pops::Types::PIntegerType.new,
+ Float => Puppet::Pops::Types::PFloatType.new,
+ Numeric => Puppet::Pops::Types::PNumericType.new,
+ NilClass => Puppet::Pops::Types::PNilType.new,
+ TrueClass => Puppet::Pops::Types::PBooleanType.new,
+ FalseClass => Puppet::Pops::Types::PBooleanType.new,
+ String => Puppet::Pops::Types::PStringType.new,
+ Regexp => Puppet::Pops::Types::PRegexpType.new,
+ Regexp => Puppet::Pops::Types::PRegexpType.new,
+ Array => Puppet::Pops::Types::TypeFactory.array_of_data(),
+ Hash => Puppet::Pops::Types::TypeFactory.hash_of_data()
+ }.each do |ruby_type, puppet_type |
+ ruby_type.should be_assignable_to(puppet_type)
+ end
+ end
+
+ context 'when dealing with integer ranges' do
+ it 'should accept an equal range' do
+ calculator.assignable?(range_t(2,5), range_t(2,5)).should == true
+ end
+
+ it 'should accept an equal reverse range' do
+ calculator.assignable?(range_t(2,5), range_t(5,2)).should == true
+ end
+
+ it 'should accept a narrower range' do
+ calculator.assignable?(range_t(2,10), range_t(3,5)).should == true
+ end
+
+ it 'should accept a narrower reverse range' do
+ calculator.assignable?(range_t(2,10), range_t(5,3)).should == true
+ end
+
+ it 'should reject a wider range' do
+ calculator.assignable?(range_t(3,5), range_t(2,10)).should == false
+ end
+
+ it 'should reject a wider reverse range' do
+ calculator.assignable?(range_t(3,5), range_t(10,2)).should == false
+ end
+
+ it 'should reject a partially overlapping range' do
+ calculator.assignable?(range_t(3,5), range_t(2,4)).should == false
+ calculator.assignable?(range_t(3,5), range_t(4,6)).should == false
+ end
+
+ it 'should reject a partially overlapping reverse range' do
+ calculator.assignable?(range_t(3,5), range_t(4,2)).should == false
+ calculator.assignable?(range_t(3,5), range_t(6,4)).should == false
+ end
+ end
+
+ context 'when dealing with patterns' do
+ it 'should accept a string matching a pattern' do
+ p_t = pattern_t('abc')
+ p_s = string_t('XabcY')
+ calculator.assignable?(p_t, p_s).should == true
+ end
+
+ it 'should accept a regexp matching a pattern' do
+ p_t = pattern_t(/abc/)
+ p_s = string_t('XabcY')
+ calculator.assignable?(p_t, p_s).should == true
+ end
+
+ it 'should accept a pattern matching a pattern' do
+ p_t = pattern_t(pattern_t('abc'))
+ p_s = string_t('XabcY')
+ calculator.assignable?(p_t, p_s).should == true
+ end
+
+ it 'should accept a regexp matching a pattern' do
+ p_t = pattern_t(regexp_t('abc'))
+ p_s = string_t('XabcY')
+ calculator.assignable?(p_t, p_s).should == true
+ end
+
+ it 'should accept a string matching all patterns' do
+ p_t = pattern_t('abc', 'ab', 'c')
+ p_s = string_t('XabcY')
+ calculator.assignable?(p_t, p_s).should == true
+ end
+
+ it 'should accept multiple strings if they all match any patterns' do
+ p_t = pattern_t('X', 'Y', 'abc')
+ p_s = string_t('Xa', 'aY', 'abc')
+ calculator.assignable?(p_t, p_s).should == true
+ end
+
+ it 'should reject a string not matching any patterns' do
+ p_t = pattern_t('abc', 'ab', 'c')
+ p_s = string_t('XqqqY')
+ calculator.assignable?(p_t, p_s).should == false
+ end
+
+ it 'should reject multiple strings if not all match any patterns' do
+ p_t = pattern_t('abc', 'ab', 'c', 'q')
+ p_s = string_t('X', 'Y', 'Z')
+ calculator.assignable?(p_t, p_s).should == false
+ end
+
+ it 'should accept enum matching patterns as instanceof' do
+ enum = enum_t('XS', 'S', 'M', 'L' 'XL', 'XXL')
+ pattern = pattern_t('S', 'M', 'L')
+ calculator.assignable?(pattern, enum).should == true
+ end
+
end
- it 'should recognize mapped ruby types' do
- calculator.assignable?(Puppet::Pops::Types::PIntegerType.new(), Integer).should == true
- calculator.assignable?(Puppet::Pops::Types::PIntegerType.new(), Fixnum).should == true
- calculator.assignable?(Puppet::Pops::Types::PIntegerType.new(), Bignum).should == true
- calculator.assignable?(Puppet::Pops::Types::PFloatType.new(), Float).should == true
- calculator.assignable?(Puppet::Pops::Types::PNumericType.new(), Numeric).should == true
- calculator.assignable?(Puppet::Pops::Types::PNilType.new(), NilClass).should == true
- calculator.assignable?(Puppet::Pops::Types::PBooleanType.new(), FalseClass).should == true
- calculator.assignable?(Puppet::Pops::Types::PBooleanType.new(), TrueClass).should == true
- calculator.assignable?(Puppet::Pops::Types::PStringType.new(), String).should == true
- calculator.assignable?(Puppet::Pops::Types::PPatternType.new(), Regexp).should == true
- calculator.assignable?(Puppet::Pops::Types::TypeFactory.array_of_data(), Array).should == true
- calculator.assignable?(Puppet::Pops::Types::TypeFactory.hash_of_data(), Hash).should == true
+ context 'when dealing with tuples' do
+ it 'should accept matching tuples' do
+ tuple1 = tuple_t(1,2)
+ tuple2 = tuple_t(Integer,Integer)
+ calculator.assignable?(tuple1, tuple2).should == true
+ calculator.assignable?(tuple2, tuple1).should == true
+ end
+
+ it 'should accept matching tuples where one is more general than the other' do
+ tuple1 = tuple_t(1,2)
+ tuple2 = tuple_t(Numeric,Numeric)
+ calculator.assignable?(tuple1, tuple2).should == false
+ calculator.assignable?(tuple2, tuple1).should == true
+ end
+
+ it 'should accept ranged tuples' do
+ tuple1 = tuple_t(1)
+ factory.constrain_size(tuple1, 5, 5)
+ tuple2 = tuple_t(Integer,Integer, Integer, Integer, Integer)
+ calculator.assignable?(tuple1, tuple2).should == true
+ calculator.assignable?(tuple2, tuple1).should == true
+ end
+
+ it 'should reject ranged tuples when ranges does not match' do
+ tuple1 = tuple_t(1)
+ factory.constrain_size(tuple1, 4, 5)
+ tuple2 = tuple_t(Integer,Integer, Integer, Integer, Integer)
+ calculator.assignable?(tuple1, tuple2).should == true
+ calculator.assignable?(tuple2, tuple1).should == false
+ end
+
+ it 'should reject ranged tuples when ranges does not match (using infinite upper bound)' do
+ tuple1 = tuple_t(1)
+ factory.constrain_size(tuple1, 4, :default)
+ tuple2 = tuple_t(Integer,Integer, Integer, Integer, Integer)
+ calculator.assignable?(tuple1, tuple2).should == true
+ calculator.assignable?(tuple2, tuple1).should == false
+ end
+
+ it 'should accept matching tuples with optional entries' do
+ tuple1 = tuple_t(1,2)
+ factory.constrain_size(tuple1, 0, :default)
+ tuple2 = tuple_t(Numeric,Numeric)
+ factory.constrain_size(tuple2, 0, :default)
+ calculator.assignable?(tuple1, tuple2).should == false
+ calculator.assignable?(tuple2, tuple1).should == true
+ end
+
+ it 'should accept matching array' do
+ tuple1 = tuple_t(1,2)
+ array = array_t(Integer)
+ factory.constrain_size(array, 2, 2)
+ calculator.assignable?(tuple1, array).should == true
+ calculator.assignable?(array, tuple1).should == true
+ end
+ end
+
+ context 'when dealing with structs' do
+ it 'should accept matching structs' do
+ struct1 = struct_t({'a'=>Integer, 'b'=>Integer})
+ struct2 = struct_t({'a'=>Integer, 'b'=>Integer})
+ calculator.assignable?(struct1, struct2).should == true
+ calculator.assignable?(struct2, struct1).should == true
+ end
+
+ it 'should accept matching structs where one is more general than the other' do
+ struct1 = struct_t({'a'=>Integer, 'b'=>Integer})
+ struct2 = struct_t({'a'=>Numeric, 'b'=>Numeric})
+ calculator.assignable?(struct1, struct2).should == false
+ calculator.assignable?(struct2, struct1).should == true
+ end
+
+ it 'should accept matching hash' do
+ struct1 = struct_t({'a'=>Integer, 'b'=>Integer})
+ non_empty_string = string_t()
+ non_empty_string.size_type = range_t(1, nil)
+ hsh = hash_t(non_empty_string, Integer)
+ factory.constrain_size(hsh, 2, 2)
+ calculator.assignable?(struct1, hsh).should == true
+ calculator.assignable?(hsh, struct1).should == true
+ end
end
it 'should recognize ruby type inheritance' do
class Foo
end
class Bar < Foo
end
fooType = calculator.infer(Foo.new)
barType = calculator.infer(Bar.new)
calculator.assignable?(fooType, fooType).should == true
calculator.assignable?(Foo, fooType).should == true
calculator.assignable?(fooType, barType).should == true
calculator.assignable?(Foo, barType).should == true
calculator.assignable?(barType, fooType).should == false
calculator.assignable?(Bar, fooType).should == false
end
+
+ it "should allow host class with same name" do
+ hc1 = Puppet::Pops::Types::TypeFactory.host_class('the_name')
+ hc2 = Puppet::Pops::Types::TypeFactory.host_class('the_name')
+ calculator.assignable?(hc1, hc2).should == true
+ end
+
+ it "should allow host class with name assigned to hostclass without name" do
+ hc1 = Puppet::Pops::Types::TypeFactory.host_class()
+ hc2 = Puppet::Pops::Types::TypeFactory.host_class('the_name')
+ calculator.assignable?(hc1, hc2).should == true
+ end
+
+ it "should reject host classes with different names" do
+ hc1 = Puppet::Pops::Types::TypeFactory.host_class('the_name')
+ hc2 = Puppet::Pops::Types::TypeFactory.host_class('another_name')
+ calculator.assignable?(hc1, hc2).should == false
+ end
+
+ it "should reject host classes without name assigned to host class with name" do
+ hc1 = Puppet::Pops::Types::TypeFactory.host_class('the_name')
+ hc2 = Puppet::Pops::Types::TypeFactory.host_class()
+ calculator.assignable?(hc1, hc2).should == false
+ end
+
+ it "should allow resource with same type_name and title" do
+ r1 = Puppet::Pops::Types::TypeFactory.resource('file', 'foo')
+ r2 = Puppet::Pops::Types::TypeFactory.resource('file', 'foo')
+ calculator.assignable?(r1, r2).should == true
+ end
+
+ it "should allow more specific resource assignment" do
+ r1 = Puppet::Pops::Types::TypeFactory.resource()
+ r2 = Puppet::Pops::Types::TypeFactory.resource('file')
+ calculator.assignable?(r1, r2).should == true
+ r2 = Puppet::Pops::Types::TypeFactory.resource('file', '/tmp/foo')
+ calculator.assignable?(r1, r2).should == true
+ r1 = Puppet::Pops::Types::TypeFactory.resource('file')
+ calculator.assignable?(r1, r2).should == true
+ end
+
+ it "should reject less specific resource assignment" do
+ r1 = Puppet::Pops::Types::TypeFactory.resource('file', '/tmp/foo')
+ r2 = Puppet::Pops::Types::TypeFactory.resource('file')
+ calculator.assignable?(r1, r2).should == false
+ r2 = Puppet::Pops::Types::TypeFactory.resource()
+ calculator.assignable?(r1, r2).should == false
+ end
+
end
context 'when testing if x is instance of type t' do
+ include_context "types_setup"
+
+ it 'should consider undef to be instance of Object and NilType' do
+ calculator.instance?(Puppet::Pops::Types::PNilType.new(), nil).should == true
+ calculator.instance?(Puppet::Pops::Types::PObjectType.new(), nil).should == true
+ end
+
+ it 'should not consider undef to be an instance of any other type than Object and NilType and Data' do
+ types_to_test = all_types - [
+ Puppet::Pops::Types::PObjectType,
+ Puppet::Pops::Types::PNilType,
+ Puppet::Pops::Types::PDataType]
+
+ types_to_test.each {|t| calculator.instance?(t.new, nil).should == false }
+ types_to_test.each {|t| calculator.instance?(t.new, :undef).should == false }
+ end
+
it 'should consider fixnum instanceof PIntegerType' do
- calculator.instance?(Puppet::Pops::Types::PIntegerType.new(), 1)
+ calculator.instance?(Puppet::Pops::Types::PIntegerType.new(), 1).should == true
end
it 'should consider fixnum instanceof Fixnum' do
- calculator.instance?(Fixnum, 1)
+ calculator.instance?(Fixnum, 1).should == true
+ end
+
+ it 'should consider integer in range' do
+ range = range_t(0,10)
+ calculator.instance?(range, 1).should == true
+ calculator.instance?(range, 10).should == true
+ calculator.instance?(range, -1).should == false
+ calculator.instance?(range, 11).should == false
+ end
+
+ it 'should consider string in length range' do
+ range = factory.constrain_size(string_t, 1,3)
+ calculator.instance?(range, 'a').should == true
+ calculator.instance?(range, 'abc').should == true
+ calculator.instance?(range, '').should == false
+ calculator.instance?(range, 'abcd').should == false
+ end
+
+ it 'should consider array in length range' do
+ range = factory.constrain_size(array_t(integer_t), 1,3)
+ calculator.instance?(range, [1]).should == true
+ calculator.instance?(range, [1,2,3]).should == true
+ calculator.instance?(range, []).should == false
+ calculator.instance?(range, [1,2,3,4]).should == false
+ end
+
+ it 'should consider hash in length range' do
+ range = factory.constrain_size(hash_t(integer_t, integer_t), 1,2)
+ calculator.instance?(range, {1=>1}).should == true
+ calculator.instance?(range, {1=>1, 2=>2}).should == true
+ calculator.instance?(range, {}).should == false
+ calculator.instance?(range, {1=>1, 2=>2, 3=>3}).should == false
+ end
+
+ it 'should consider collection in length range for array ' do
+ range = factory.constrain_size(collection_t, 1,3)
+ calculator.instance?(range, [1]).should == true
+ calculator.instance?(range, [1,2,3]).should == true
+ calculator.instance?(range, []).should == false
+ calculator.instance?(range, [1,2,3,4]).should == false
+ end
+
+ it 'should consider collection in length range for hash' do
+ range = factory.constrain_size(collection_t, 1,2)
+ calculator.instance?(range, {1=>1}).should == true
+ calculator.instance?(range, {1=>1, 2=>2}).should == true
+ calculator.instance?(range, {}).should == false
+ calculator.instance?(range, {1=>1, 2=>2, 3=>3}).should == false
+ end
+
+ it 'should consider string matching enum as instanceof' do
+ enum = enum_t('XS', 'S', 'M', 'L', 'XL', '0')
+ calculator.instance?(enum, 'XS').should == true
+ calculator.instance?(enum, 'S').should == true
+ calculator.instance?(enum, 'XXL').should == false
+ calculator.instance?(enum, '').should == false
+ calculator.instance?(enum, '0').should == true
+ calculator.instance?(enum, 0).should == false
+ end
+
+ it 'should consider array[string] as instance of Array[Enum] when strings are instance of Enum' do
+ enum = enum_t('XS', 'S', 'M', 'L', 'XL', '0')
+ array = array_t(enum)
+ calculator.instance?(array, ['XS', 'S', 'XL']).should == true
+ calculator.instance?(array, ['XS', 'S', 'XXL']).should == false
+ end
+
+ it 'should consider array[mixed] as instance of Variant[mixed] when mixed types are listed in Variant' do
+ enum = enum_t('XS', 'S', 'M', 'L', 'XL')
+ sizes = range_t(30, 50)
+ array = array_t(variant_t(enum, sizes))
+ calculator.instance?(array, ['XS', 'S', 30, 50]).should == true
+ calculator.instance?(array, ['XS', 'S', 'XXL']).should == false
+ calculator.instance?(array, ['XS', 'S', 29]).should == false
+ end
+
+ it 'should consider array[seq] as instance of Tuple[seq] when elements of seq are instance of' do
+ tuple = tuple_t(Integer, String, Float)
+ calculator.instance?(tuple, [1, 'a', 3.14]).should == true
+ calculator.instance?(tuple, [1.2, 'a', 3.14]).should == false
+ calculator.instance?(tuple, [1, 1, 3.14]).should == false
+ calculator.instance?(tuple, [1, 'a', 1]).should == false
+ end
+
+ it 'should consider hash[cont] as instance of Struct[cont-t]' do
+ struct = struct_t({'a'=>Integer, 'b'=>String, 'c'=>Float})
+ calculator.instance?(struct, {'a'=>1, 'b'=>'a', 'c'=>3.14}).should == true
+ calculator.instance?(struct, {'a'=>1.2, 'b'=>'a', 'c'=>3.14}).should == false
+ calculator.instance?(struct, {'a'=>1, 'b'=>1, 'c'=>3.14}).should == false
+ calculator.instance?(struct, {'a'=>1, 'b'=>'a', 'c'=>1}).should == false
+ end
+
+ context 'and t is Data' do
+ it 'undef should be considered instance of Data' do
+ calculator.instance?(data_t, :undef).should == true
+ end
+
+ it 'other symbols should not be considered instance of Data' do
+ calculator.instance?(data_t, :love).should == false
+ end
+
+ it 'an empty array should be considered instance of Data' do
+ calculator.instance?(data_t, []).should == true
+ end
+
+ it 'an empty hash should be considered instance of Data' do
+ calculator.instance?(data_t, {}).should == true
+ end
+
+ it 'a hash with nil/undef data should be considered instance of Data' do
+ calculator.instance?(data_t, {'a' => nil}).should == true
+ calculator.instance?(data_t, {'a' => :undef}).should == true
+ end
+
+ it 'a hash with nil/undef key should not considered instance of Data' do
+ calculator.instance?(data_t, {nil => 10}).should == false
+ calculator.instance?(data_t, {:undef => 10}).should == false
+ end
+
+ it 'an array with undef entries should be considered instance of Data' do
+ calculator.instance?(data_t, [:undef]).should == true
+ calculator.instance?(data_t, [nil]).should == true
+ end
+
+ it 'an array with undef / data entries should be considered instance of Data' do
+ calculator.instance?(data_t, [1, :undef, 'a']).should == true
+ calculator.instance?(data_t, [1, nil, 'a']).should == true
+ end
end
end
context 'when converting a ruby class' do
it 'should yield \'PIntegerType\' for Integer, Fixnum, and Bignum' do
[Integer,Fixnum,Bignum].each do |c|
calculator.type(c).class.should == Puppet::Pops::Types::PIntegerType
end
end
it 'should yield \'PFloatType\' for Float' do
calculator.type(Float).class.should == Puppet::Pops::Types::PFloatType
end
it 'should yield \'PBooleanType\' for FalseClass and TrueClass' do
[FalseClass,TrueClass].each do |c|
calculator.type(c).class.should == Puppet::Pops::Types::PBooleanType
end
end
it 'should yield \'PNilType\' for NilClass' do
calculator.type(NilClass).class.should == Puppet::Pops::Types::PNilType
end
it 'should yield \'PStringType\' for String' do
calculator.type(String).class.should == Puppet::Pops::Types::PStringType
end
- it 'should yield \'PPatternType\' for Regexp' do
- calculator.type(Regexp).class.should == Puppet::Pops::Types::PPatternType
+ it 'should yield \'PRegexpType\' for Regexp' do
+ calculator.type(Regexp).class.should == Puppet::Pops::Types::PRegexpType
end
it 'should yield \'PArrayType[PDataType]\' for Array' do
t = calculator.type(Array)
t.class.should == Puppet::Pops::Types::PArrayType
t.element_type.class.should == Puppet::Pops::Types::PDataType
end
- it 'should yield \'PHashType[PLiteralType,PDataType]\' for Hash' do
+ it 'should yield \'PHashType[PScalarType,PDataType]\' for Hash' do
t = calculator.type(Hash)
t.class.should == Puppet::Pops::Types::PHashType
- t.key_type.class.should == Puppet::Pops::Types::PLiteralType
+ t.key_type.class.should == Puppet::Pops::Types::PScalarType
t.element_type.class.should == Puppet::Pops::Types::PDataType
end
end
context 'when representing the type as string' do
it 'should yield \'Type\' for PType' do
calculator.string(Puppet::Pops::Types::PType.new()).should == 'Type'
end
it 'should yield \'Object\' for PObjectType' do
calculator.string(Puppet::Pops::Types::PObjectType.new()).should == 'Object'
end
- it 'should yield \'Literal\' for PLiteralType' do
- calculator.string(Puppet::Pops::Types::PLiteralType.new()).should == 'Literal'
+ it 'should yield \'Scalar\' for PScalarType' do
+ calculator.string(Puppet::Pops::Types::PScalarType.new()).should == 'Scalar'
end
it 'should yield \'Boolean\' for PBooleanType' do
calculator.string(Puppet::Pops::Types::PBooleanType.new()).should == 'Boolean'
end
it 'should yield \'Data\' for PDataType' do
calculator.string(Puppet::Pops::Types::PDataType.new()).should == 'Data'
end
it 'should yield \'Numeric\' for PNumericType' do
calculator.string(Puppet::Pops::Types::PNumericType.new()).should == 'Numeric'
end
- it 'should yield \'Integer\' for PIntegerType' do
- calculator.string(Puppet::Pops::Types::PIntegerType.new()).should == 'Integer'
+ it 'should yield \'Integer\' and from/to for PIntegerType' do
+ int_T = Puppet::Pops::Types::PIntegerType
+ calculator.string(int_T.new()).should == 'Integer'
+ int = int_T.new()
+ int.from = 1
+ int.to = 1
+ calculator.string(int).should == 'Integer[1, 1]'
+ int = int_T.new()
+ int.from = 1
+ int.to = 2
+ calculator.string(int).should == 'Integer[1, 2]'
+ int = int_T.new()
+ int.from = nil
+ int.to = 2
+ calculator.string(int).should == 'Integer[default, 2]'
+ int = int_T.new()
+ int.from = 2
+ int.to = nil
+ calculator.string(int).should == 'Integer[2, default]'
end
it 'should yield \'Float\' for PFloatType' do
calculator.string(Puppet::Pops::Types::PFloatType.new()).should == 'Float'
end
- it 'should yield \'Pattern\' for PPatternType' do
- calculator.string(Puppet::Pops::Types::PPatternType.new()).should == 'Pattern'
+ it 'should yield \'Regexp\' for PRegexpType' do
+ calculator.string(Puppet::Pops::Types::PRegexpType.new()).should == 'Regexp'
+ end
+
+ it 'should yield \'Regexp[/pat/]\' for parameterized PRegexpType' do
+ t = Puppet::Pops::Types::PRegexpType.new()
+ t.pattern = ('a/b')
+ calculator.string(Puppet::Pops::Types::PRegexpType.new()).should == 'Regexp'
end
it 'should yield \'String\' for PStringType' do
calculator.string(Puppet::Pops::Types::PStringType.new()).should == 'String'
end
+ it 'should yield \'String\' for PStringType with multiple values' do
+ calculator.string(string_t('a', 'b', 'c')).should == 'String'
+ end
+
+ it 'should yield \'String\' and from/to for PStringType' do
+ string_T = Puppet::Pops::Types::PStringType
+ calculator.string(factory.constrain_size(string_T.new(), 1,1)).should == 'String[1, 1]'
+ calculator.string(factory.constrain_size(string_T.new(), 1,2)).should == 'String[1, 2]'
+ calculator.string(factory.constrain_size(string_T.new(), :default, 2)).should == 'String[default, 2]'
+ calculator.string(factory.constrain_size(string_T.new(), 2, :default)).should == 'String[2, default]'
+ end
+
it 'should yield \'Array[Integer]\' for PArrayType[PIntegerType]' do
t = Puppet::Pops::Types::PArrayType.new()
t.element_type = Puppet::Pops::Types::PIntegerType.new()
calculator.string(t).should == 'Array[Integer]'
end
+ it 'should yield \'Collection\' and from/to for PCollectionType' do
+ col = collection_t()
+ calculator.string(factory.constrain_size(col.copy, 1,1)).should == 'Collection[1, 1]'
+ calculator.string(factory.constrain_size(col.copy, 1,2)).should == 'Collection[1, 2]'
+ calculator.string(factory.constrain_size(col.copy, :default, 2)).should == 'Collection[default, 2]'
+ calculator.string(factory.constrain_size(col.copy, 2, :default)).should == 'Collection[2, default]'
+ end
+
+ it 'should yield \'Array\' and from/to for PArrayType' do
+ arr = array_t(string_t)
+ calculator.string(factory.constrain_size(arr.copy, 1,1)).should == 'Array[String, 1, 1]'
+ calculator.string(factory.constrain_size(arr.copy, 1,2)).should == 'Array[String, 1, 2]'
+ calculator.string(factory.constrain_size(arr.copy, :default, 2)).should == 'Array[String, default, 2]'
+ calculator.string(factory.constrain_size(arr.copy, 2, :default)).should == 'Array[String, 2, default]'
+ end
+
+ it 'should yield \'Tuple[Integer]\' for PTupleType[PIntegerType]' do
+ t = Puppet::Pops::Types::PTupleType.new()
+ t.addTypes(Puppet::Pops::Types::PIntegerType.new())
+ calculator.string(t).should == 'Tuple[Integer]'
+ end
+
+ it 'should yield \'Tuple[T, T,..]\' for PTupleType[T, T, ...]' do
+ t = Puppet::Pops::Types::PTupleType.new()
+ t.addTypes(Puppet::Pops::Types::PIntegerType.new())
+ t.addTypes(Puppet::Pops::Types::PIntegerType.new())
+ t.addTypes(Puppet::Pops::Types::PStringType.new())
+ calculator.string(t).should == 'Tuple[Integer, Integer, String]'
+ end
+
+ it 'should yield \'Tuple\' and from/to for PTupleType' do
+ tuple_t = tuple_t(string_t)
+ calculator.string(factory.constrain_size(tuple_t.copy, 1,1)).should == 'Tuple[String, 1, 1]'
+ calculator.string(factory.constrain_size(tuple_t.copy, 1,2)).should == 'Tuple[String, 1, 2]'
+ calculator.string(factory.constrain_size(tuple_t.copy, :default, 2)).should == 'Tuple[String, default, 2]'
+ calculator.string(factory.constrain_size(tuple_t.copy, 2, :default)).should == 'Tuple[String, 2, default]'
+ end
+
+ it 'should yield \'Struct\' and details for PStructType' do
+ struct_t = struct_t({'a'=>Integer, 'b'=>String})
+ s = calculator.string(struct_t)
+ # Ruby 1.8.7 - noone likes you...
+ (s == "Struct[{'a'=>Integer, 'b'=>String}]" || s == "Struct[{'b'=>String, 'a'=>Integer}]").should == true
+ struct_t = struct_t({})
+ calculator.string(struct_t).should == "Struct"
+ end
+
it 'should yield \'Hash[String, Integer]\' for PHashType[PStringType, PIntegerType]' do
t = Puppet::Pops::Types::PHashType.new()
t.key_type = Puppet::Pops::Types::PStringType.new()
t.element_type = Puppet::Pops::Types::PIntegerType.new()
calculator.string(t).should == 'Hash[String, Integer]'
end
+
+ it 'should yield \'Hash\' and from/to for PHashType' do
+ hsh = hash_t(string_t, string_t)
+ calculator.string(factory.constrain_size(hsh.copy, 1,1)).should == 'Hash[String, String, 1, 1]'
+ calculator.string(factory.constrain_size(hsh.copy, 1,2)).should == 'Hash[String, String, 1, 2]'
+ calculator.string(factory.constrain_size(hsh.copy, :default, 2)).should == 'Hash[String, String, default, 2]'
+ calculator.string(factory.constrain_size(hsh.copy, 2, :default)).should == 'Hash[String, String, 2, default]'
+ end
+
+ it "should yield 'Class' for a PHostClassType" do
+ t = Puppet::Pops::Types::PHostClassType.new()
+ calculator.string(t).should == 'Class'
+ end
+
+ it "should yield 'Class[x]' for a PHostClassType[x]" do
+ t = Puppet::Pops::Types::PHostClassType.new()
+ t.class_name = 'x'
+ calculator.string(t).should == 'Class[x]'
+ end
+
+ it "should yield 'Resource' for a PResourceType" do
+ t = Puppet::Pops::Types::PResourceType.new()
+ calculator.string(t).should == 'Resource'
+ end
+
+ it 'should yield \'File\' for a PResourceType[\'File\']' do
+ t = Puppet::Pops::Types::PResourceType.new()
+ t.type_name = 'File'
+ calculator.string(t).should == 'File'
+ end
+
+ it "should yield 'File['/tmp/foo']' for a PResourceType['File', '/tmp/foo']" do
+ t = Puppet::Pops::Types::PResourceType.new()
+ t.type_name = 'File'
+ t.title = '/tmp/foo'
+ calculator.string(t).should == "File['/tmp/foo']"
+ end
+
+ it "should yield 'Enum[s,...]' for a PEnumType[s,...]" do
+ t = enum_t('a', 'b', 'c')
+ calculator.string(t).should == "Enum['a', 'b', 'c']"
+ end
+
+ it "should yield 'Pattern[/pat/,...]' for a PPatternType['pat',...]" do
+ t = pattern_t('a')
+ t2 = pattern_t('a', 'b', 'c')
+ calculator.string(t).should == "Pattern[/a/]"
+ calculator.string(t2).should == "Pattern[/a/, /b/, /c/]"
+ end
+
+ it "should escape special characters in the string for a PPatternType['pat',...]" do
+ t = pattern_t('a/b')
+ calculator.string(t).should == "Pattern[/a\\/b/]"
+ end
+
+ it "should yield 'Variant[t1,t2,...]' for a PVariantType[t1, t2,...]" do
+ t1 = string_t()
+ t2 = integer_t()
+ t3 = pattern_t('a')
+ t = variant_t(t1, t2, t3)
+ calculator.string(t).should == "Variant[String, Integer, Pattern[/a/]]"
+ end
end
context 'when processing meta type' do
it 'should infer PType as the type of all other types' do
ptype = Puppet::Pops::Types::PType
calculator.infer(Puppet::Pops::Types::PNilType.new() ).is_a?(ptype).should() == true
calculator.infer(Puppet::Pops::Types::PDataType.new() ).is_a?(ptype).should() == true
- calculator.infer(Puppet::Pops::Types::PLiteralType.new() ).is_a?(ptype).should() == true
+ calculator.infer(Puppet::Pops::Types::PScalarType.new() ).is_a?(ptype).should() == true
calculator.infer(Puppet::Pops::Types::PStringType.new() ).is_a?(ptype).should() == true
calculator.infer(Puppet::Pops::Types::PNumericType.new() ).is_a?(ptype).should() == true
calculator.infer(Puppet::Pops::Types::PIntegerType.new() ).is_a?(ptype).should() == true
calculator.infer(Puppet::Pops::Types::PFloatType.new() ).is_a?(ptype).should() == true
- calculator.infer(Puppet::Pops::Types::PPatternType.new() ).is_a?(ptype).should() == true
+ calculator.infer(Puppet::Pops::Types::PRegexpType.new() ).is_a?(ptype).should() == true
calculator.infer(Puppet::Pops::Types::PBooleanType.new() ).is_a?(ptype).should() == true
calculator.infer(Puppet::Pops::Types::PCollectionType.new()).is_a?(ptype).should() == true
calculator.infer(Puppet::Pops::Types::PArrayType.new() ).is_a?(ptype).should() == true
calculator.infer(Puppet::Pops::Types::PHashType.new() ).is_a?(ptype).should() == true
calculator.infer(Puppet::Pops::Types::PRubyType.new() ).is_a?(ptype).should() == true
+ calculator.infer(Puppet::Pops::Types::PHostClassType.new() ).is_a?(ptype).should() == true
+ calculator.infer(Puppet::Pops::Types::PResourceType.new() ).is_a?(ptype).should() == true
+ calculator.infer(Puppet::Pops::Types::PEnumType.new() ).is_a?(ptype).should() == true
+ calculator.infer(Puppet::Pops::Types::PPatternType.new() ).is_a?(ptype).should() == true
+ calculator.infer(Puppet::Pops::Types::PVariantType.new() ).is_a?(ptype).should() == true
+ calculator.infer(Puppet::Pops::Types::PTupleType.new() ).is_a?(ptype).should() == true
+ end
+
+ it 'should infer PType as the type of all other types' do
+ ptype = Puppet::Pops::Types::PType
+ calculator.string(calculator.infer(Puppet::Pops::Types::PNilType.new() )).should == "Type[Undef]"
+ calculator.string(calculator.infer(Puppet::Pops::Types::PDataType.new() )).should == "Type[Data]"
+ calculator.string(calculator.infer(Puppet::Pops::Types::PScalarType.new() )).should == "Type[Scalar]"
+ calculator.string(calculator.infer(Puppet::Pops::Types::PStringType.new() )).should == "Type[String]"
+ calculator.string(calculator.infer(Puppet::Pops::Types::PNumericType.new() )).should == "Type[Numeric]"
+ calculator.string(calculator.infer(Puppet::Pops::Types::PIntegerType.new() )).should == "Type[Integer]"
+ calculator.string(calculator.infer(Puppet::Pops::Types::PFloatType.new() )).should == "Type[Float]"
+ calculator.string(calculator.infer(Puppet::Pops::Types::PRegexpType.new() )).should == "Type[Regexp]"
+ calculator.string(calculator.infer(Puppet::Pops::Types::PBooleanType.new() )).should == "Type[Boolean]"
+ calculator.string(calculator.infer(Puppet::Pops::Types::PCollectionType.new())).should == "Type[Collection]"
+ calculator.string(calculator.infer(Puppet::Pops::Types::PArrayType.new() )).should == "Type[Array[?]]"
+ calculator.string(calculator.infer(Puppet::Pops::Types::PHashType.new() )).should == "Type[Hash[?, ?]]"
+ calculator.string(calculator.infer(Puppet::Pops::Types::PRubyType.new() )).should == "Type[Ruby[?]]"
+ calculator.string(calculator.infer(Puppet::Pops::Types::PHostClassType.new() )).should == "Type[Class]"
+ calculator.string(calculator.infer(Puppet::Pops::Types::PResourceType.new() )).should == "Type[Resource]"
+ calculator.string(calculator.infer(Puppet::Pops::Types::PEnumType.new() )).should == "Type[Enum]"
+ calculator.string(calculator.infer(Puppet::Pops::Types::PVariantType.new() )).should == "Type[Variant]"
+ calculator.string(calculator.infer(Puppet::Pops::Types::PPatternType.new() )).should == "Type[Pattern]"
+ calculator.string(calculator.infer(Puppet::Pops::Types::PTupleType.new() )).should == "Type[Tuple]"
+ end
+
+ it "computes the common type of PType's type parameter" do
+ int_t = Puppet::Pops::Types::PIntegerType.new()
+ string_t = Puppet::Pops::Types::PStringType.new()
+ calculator.string(calculator.infer([int_t])).should == "Array[Type[Integer], 1, 1]"
+ calculator.string(calculator.infer([int_t, string_t])).should == "Array[Type[Scalar], 2, 2]"
end
it 'should infer PType as the type of ruby classes' do
class Foo
end
[Object, Numeric, Integer, Fixnum, Bignum, Float, String, Regexp, Array, Hash, Foo].each do |c|
calculator.infer(c).is_a?(Puppet::Pops::Types::PType).should() == true
end
end
it 'should infer PType as the type of PType (meta regression short-circuit)' do
calculator.infer(Puppet::Pops::Types::PType.new()).is_a?(Puppet::Pops::Types::PType).should() == true
end
+
+ it 'computes instance? to be true if parameterized and type match' do
+ int_t = Puppet::Pops::Types::PIntegerType.new()
+ type_t = Puppet::Pops::Types::TypeFactory.type_type(int_t)
+ type_type_t = Puppet::Pops::Types::TypeFactory.type_type(type_t)
+ calculator.instance?(type_type_t, type_t).should == true
+ end
+
+ it 'computes instance? to be false if parameterized and type do not match' do
+ int_t = Puppet::Pops::Types::PIntegerType.new()
+ string_t = Puppet::Pops::Types::PStringType.new()
+ type_t = Puppet::Pops::Types::TypeFactory.type_type(int_t)
+ type_t2 = Puppet::Pops::Types::TypeFactory.type_type(string_t)
+ type_type_t = Puppet::Pops::Types::TypeFactory.type_type(type_t)
+ # i.e. Type[Integer] =~ Type[Type[Integer]] # false
+ calculator.instance?(type_type_t, type_t2).should == false
+ end
+
+ it 'computes instance? to be true if unparameterized and matched against a type[?]' do
+ int_t = Puppet::Pops::Types::PIntegerType.new()
+ type_t = Puppet::Pops::Types::TypeFactory.type_type(int_t)
+ calculator.instance?(Puppet::Pops::Types::PType.new, type_t).should == true
+ end
end
+
+ context "when asking for an enumerable " do
+ it "should produce an enumerable for an Integer range that is not infinite" do
+ t = Puppet::Pops::Types::PIntegerType.new()
+ t.from = 1
+ t.to = 10
+ calculator.enumerable(t).respond_to?(:each).should == true
+ end
+
+ it "should not produce an enumerable for an Integer range that has an infinite side" do
+ t = Puppet::Pops::Types::PIntegerType.new()
+ t.from = nil
+ t.to = 10
+ calculator.enumerable(t).should == nil
+
+ t = Puppet::Pops::Types::PIntegerType.new()
+ t.from = 1
+ t.to = nil
+ calculator.enumerable(t).should == nil
+ end
+
+ it "all but Integer range are not enumerable" do
+ [Object, Numeric, Float, String, Regexp, Array, Hash].each do |t|
+ calculator.enumerable(calculator.type(t)).should == nil
+ end
+ end
+ end
+
+ context "when dealing with different types of inference" do
+ it "an instance specific inference is produced by infer" do
+ calculator.infer(['a','b']).element_type.values.should == ['a', 'b']
+ end
+
+ it "a generic inference is produced using infer_generic" do
+ calculator.infer_generic(['a','b']).element_type.values.should == []
+ end
+
+ it "a generic result is created by generalize! given an instance specific result for an Array" do
+ generic = calculator.infer(['a','b'])
+ generic.element_type.values.should == ['a', 'b']
+ calculator.generalize!(generic)
+ generic.element_type.values.should == []
+ end
+
+ it "a generic result is created by generalize! given an instance specific result for a Hash" do
+ generic = calculator.infer({'a' =>1,'b' => 2})
+ generic.key_type.values.sort.should == ['a', 'b']
+ generic.element_type.from.should == 1
+ generic.element_type.to.should == 2
+ calculator.generalize!(generic)
+ generic.key_type.values.should == []
+ generic.element_type.from.should == nil
+ generic.element_type.to.should == nil
+ end
+
+ it "does not reduce by combining types when using infer_set" do
+ element_type = calculator.infer(['a','b',1,2]).element_type
+ element_type.class.should == Puppet::Pops::Types::PScalarType
+ element_type = calculator.infer_set(['a','b',1,2]).element_type
+ element_type.class.should == Puppet::Pops::Types::PVariantType
+ element_type.types[0].class.should == Puppet::Pops::Types::PStringType
+ element_type.types[1].class.should == Puppet::Pops::Types::PStringType
+ element_type.types[2].class.should == Puppet::Pops::Types::PIntegerType
+ element_type.types[3].class.should == Puppet::Pops::Types::PIntegerType
+ end
+
+ it "does not reduce by combining types when using infer_set and values are undef" do
+ element_type = calculator.infer(['a',nil]).element_type
+ element_type.class.should == Puppet::Pops::Types::PStringType
+ element_type = calculator.infer_set(['a',nil]).element_type
+ element_type.class.should == Puppet::Pops::Types::PVariantType
+ element_type.types[0].class.should == Puppet::Pops::Types::PStringType
+ element_type.types[1].class.should == Puppet::Pops::Types::PNilType
+ end
+ end
+
+ matcher :be_assignable_to do |type|
+ calc = Puppet::Pops::Types::TypeCalculator.new
+
+ match do |actual|
+ calc.assignable?(type, actual)
+ end
+
+ failure_message_for_should do |actual|
+ "#{calc.string(actual)} should be assignable to #{calc.string(type)}"
+ end
+
+ failure_message_for_should_not do |actual|
+ "#{calc.string(actual)} is assignable to #{calc.string(type)} when it should not"
+ end
+ end
+
end
diff --git a/spec/unit/pops/types/type_factory_spec.rb b/spec/unit/pops/types/type_factory_spec.rb
index be95871c4..e0f66b4e2 100644
--- a/spec/unit/pops/types/type_factory_spec.rb
+++ b/spec/unit/pops/types/type_factory_spec.rb
@@ -1,65 +1,169 @@
require 'spec_helper'
require 'puppet/pops'
describe 'The type factory' do
context 'when creating' do
it 'integer() returns PIntegerType' do
Puppet::Pops::Types::TypeFactory.integer().class().should == Puppet::Pops::Types::PIntegerType
end
it 'float() returns PFloatType' do
Puppet::Pops::Types::TypeFactory.float().class().should == Puppet::Pops::Types::PFloatType
end
it 'string() returns PStringType' do
Puppet::Pops::Types::TypeFactory.string().class().should == Puppet::Pops::Types::PStringType
end
it 'boolean() returns PBooleanType' do
Puppet::Pops::Types::TypeFactory.boolean().class().should == Puppet::Pops::Types::PBooleanType
end
it 'pattern() returns PPatternType' do
Puppet::Pops::Types::TypeFactory.pattern().class().should == Puppet::Pops::Types::PPatternType
end
- it 'literal() returns PLiteralType' do
- Puppet::Pops::Types::TypeFactory.literal().class().should == Puppet::Pops::Types::PLiteralType
+ it 'regexp() returns PRegexpType' do
+ Puppet::Pops::Types::TypeFactory.regexp().class().should == Puppet::Pops::Types::PRegexpType
+ end
+
+ it 'enum() returns PEnumType' do
+ Puppet::Pops::Types::TypeFactory.enum().class().should == Puppet::Pops::Types::PEnumType
+ end
+
+ it 'variant() returns PVariantType' do
+ Puppet::Pops::Types::TypeFactory.variant().class().should == Puppet::Pops::Types::PVariantType
+ end
+
+ it 'scalar() returns PScalarType' do
+ Puppet::Pops::Types::TypeFactory.scalar().class().should == Puppet::Pops::Types::PScalarType
end
it 'data() returns PDataType' do
Puppet::Pops::Types::TypeFactory.data().class().should == Puppet::Pops::Types::PDataType
end
+ it 'optional() returns POptionalType' do
+ Puppet::Pops::Types::TypeFactory.optional().class().should == Puppet::Pops::Types::POptionalType
+ end
+
+ it 'collection() returns PCollectionType' do
+ Puppet::Pops::Types::TypeFactory.collection().class().should == Puppet::Pops::Types::PCollectionType
+ end
+
+ it 'catalog_entry() returns PCatalogEntryType' do
+ Puppet::Pops::Types::TypeFactory.catalog_entry().class().should == Puppet::Pops::Types::PCatalogEntryType
+ end
+
+ it 'struct() returns PStructType' do
+ Puppet::Pops::Types::TypeFactory.struct().class().should == Puppet::Pops::Types::PStructType
+ end
+
+ it 'tuple() returns PTupleType' do
+ Puppet::Pops::Types::TypeFactory.tuple().class().should == Puppet::Pops::Types::PTupleType
+ end
+
+ it 'undef() returns PNilType' do
+ Puppet::Pops::Types::TypeFactory.undef().class().should == Puppet::Pops::Types::PNilType
+ end
+
+ it 'range(to, from) returns PIntegerType' do
+ t = Puppet::Pops::Types::TypeFactory.range(1,2)
+ t.class().should == Puppet::Pops::Types::PIntegerType
+ t.from.should == 1
+ t.to.should == 2
+ end
+
+ it 'range(default, default) returns PIntegerType' do
+ t = Puppet::Pops::Types::TypeFactory.range(:default,:default)
+ t.class().should == Puppet::Pops::Types::PIntegerType
+ t.from.should == nil
+ t.to.should == nil
+ end
+
+ it 'float_range(to, from) returns PFloatType' do
+ t = Puppet::Pops::Types::TypeFactory.float_range(1.0, 2.0)
+ t.class().should == Puppet::Pops::Types::PFloatType
+ t.from.should == 1.0
+ t.to.should == 2.0
+ end
+
+ it 'float_range(default, default) returns PFloatType' do
+ t = Puppet::Pops::Types::TypeFactory.float_range(:default, :default)
+ t.class().should == Puppet::Pops::Types::PFloatType
+ t.from.should == nil
+ t.to.should == nil
+ end
+
+ it 'resource() creates a generic PResourceType' do
+ pr = Puppet::Pops::Types::TypeFactory.resource()
+ pr.class().should == Puppet::Pops::Types::PResourceType
+ pr.type_name.should == nil
+ end
+
+ it 'resource(x) creates a PResourceType[x]' do
+ pr = Puppet::Pops::Types::TypeFactory.resource('x')
+ pr.class().should == Puppet::Pops::Types::PResourceType
+ pr.type_name.should == 'x'
+ end
+
+ it 'host_class() creates a generic PHostClassType' do
+ hc = Puppet::Pops::Types::TypeFactory.host_class()
+ hc.class().should == Puppet::Pops::Types::PHostClassType
+ hc.class_name.should == nil
+ end
+
+ it 'host_class(x) creates a PHostClassType[x]' do
+ hc = Puppet::Pops::Types::TypeFactory.host_class('x')
+ hc.class().should == Puppet::Pops::Types::PHostClassType
+ hc.class_name.should == 'x'
+ end
+
it 'array_of(fixnum) returns PArrayType[PIntegerType]' do
at = Puppet::Pops::Types::TypeFactory.array_of(1)
at.class().should == Puppet::Pops::Types::PArrayType
at.element_type.class.should == Puppet::Pops::Types::PIntegerType
end
it 'array_of(PIntegerType) returns PArrayType[PIntegerType]' do
at = Puppet::Pops::Types::TypeFactory.array_of(Puppet::Pops::Types::PIntegerType.new())
at.class().should == Puppet::Pops::Types::PArrayType
at.element_type.class.should == Puppet::Pops::Types::PIntegerType
end
it 'array_of_data returns PArrayType[PDataType]' do
at = Puppet::Pops::Types::TypeFactory.array_of_data
at.class().should == Puppet::Pops::Types::PArrayType
at.element_type.class.should == Puppet::Pops::Types::PDataType
end
- it 'hash_of_data returns PHashType[PLiteralType,PDataType]' do
+ it 'hash_of_data returns PHashType[PScalarType,PDataType]' do
ht = Puppet::Pops::Types::TypeFactory.hash_of_data
ht.class().should == Puppet::Pops::Types::PHashType
- ht.key_type.class.should == Puppet::Pops::Types::PLiteralType
+ ht.key_type.class.should == Puppet::Pops::Types::PScalarType
ht.element_type.class.should == Puppet::Pops::Types::PDataType
end
it 'ruby(1) returns PRubyType[\'Fixnum\']' do
ht = Puppet::Pops::Types::TypeFactory.ruby(1)
ht.class().should == Puppet::Pops::Types::PRubyType
ht.ruby_class.should == 'Fixnum'
end
+
+ it 'a size constrained collection can be created from array' do
+ t = Puppet::Pops::Types::TypeFactory.array_of_data()
+ Puppet::Pops::Types::TypeFactory.constrain_size(t, 1,2).should == t
+ t.size_type.class.should == Puppet::Pops::Types::PIntegerType
+ t.size_type.from.should == 1
+ t.size_type.to.should == 2
+ end
+
+ it 'a size constrained collection can be created from hash' do
+ t = Puppet::Pops::Types::TypeFactory.hash_of_data()
+ Puppet::Pops::Types::TypeFactory.constrain_size(t, 1,2).should == t
+ t.size_type.class.should == Puppet::Pops::Types::PIntegerType
+ t.size_type.from.should == 1
+ t.size_type.to.should == 2
+ end
end
end
diff --git a/spec/unit/pops/types/type_parser_spec.rb b/spec/unit/pops/types/type_parser_spec.rb
index f0b9ea9a4..df9194161 100644
--- a/spec/unit/pops/types/type_parser_spec.rb
+++ b/spec/unit/pops/types/type_parser_spec.rb
@@ -1,93 +1,197 @@
require 'spec_helper'
require 'puppet/pops'
describe Puppet::Pops::Types::TypeParser do
extend RSpec::Matchers::DSL
let(:parser) { Puppet::Pops::Types::TypeParser.new }
let(:types) { Puppet::Pops::Types::TypeFactory }
it "rejects a puppet expression" do
expect { parser.parse("1 + 1") }.to raise_error(Puppet::ParseError, /The expression <1 \+ 1> is not a valid type specification/)
end
it "rejects a empty type specification" do
expect { parser.parse("") }.to raise_error(Puppet::ParseError, /The expression <> is not a valid type specification/)
end
it "rejects an invalid type simple type" do
- expect { parser.parse("NotAType") }.to raise_type_error_for("NotAType")
+ expect { parser.parse("notAType") }.to raise_error(Puppet::ParseError, /The expression <notAType> is not a valid type specification/)
end
it "rejects an unknown parameterized type" do
- expect { parser.parse("NotAType[Integer]") }.to raise_type_error_for("NotAType")
+ expect { parser.parse("notAType[Integer]") }.to raise_error(Puppet::ParseError,
+ /The expression <notAType\[Integer\]> is not a valid type specification/)
end
- it "does not support types that do not make sense in the puppet language" do
- expect { parser.parse("Object") }.to raise_type_error_for("Object")
- expect { parser.parse("Collection[Integer]") }.to raise_type_error_for("Collection")
+ it "rejects an unknown type parameter" do
+ expect { parser.parse("Array[notAType]") }.to raise_error(Puppet::ParseError,
+ /The expression <Array\[notAType\]> is not a valid type specification/)
+ end
+
+ [
+ 'Object', 'Data', 'CatalogEntry', 'Boolean', 'Scalar', 'Undef', 'Numeric',
+ ].each do |name|
+ it "does not support parameterizing unparameterized type <#{name}>" do
+ expect { parser.parse("#{name}[Integer]") }.to raise_unparameterized_error_for(name)
+ end
end
it "parses a simple, unparameterized type into the type object" do
+ expect(the_type_parsed_from(types.object)).to be_the_type(types.object)
expect(the_type_parsed_from(types.integer)).to be_the_type(types.integer)
expect(the_type_parsed_from(types.float)).to be_the_type(types.float)
expect(the_type_parsed_from(types.string)).to be_the_type(types.string)
expect(the_type_parsed_from(types.boolean)).to be_the_type(types.boolean)
expect(the_type_parsed_from(types.pattern)).to be_the_type(types.pattern)
expect(the_type_parsed_from(types.data)).to be_the_type(types.data)
+ expect(the_type_parsed_from(types.catalog_entry)).to be_the_type(types.catalog_entry)
+ expect(the_type_parsed_from(types.collection)).to be_the_type(types.collection)
+ expect(the_type_parsed_from(types.tuple)).to be_the_type(types.tuple)
+ expect(the_type_parsed_from(types.struct)).to be_the_type(types.struct)
+ expect(the_type_parsed_from(types.optional)).to be_the_type(types.optional)
end
it "interprets an unparameterized Array as an Array of Data" do
expect(parser.parse("Array")).to be_the_type(types.array_of_data)
end
- it "interprets an unparameterized Hash as a Hash of Literal to Data" do
+ it "interprets an unparameterized Hash as a Hash of Scalar to Data" do
expect(parser.parse("Hash")).to be_the_type(types.hash_of_data)
end
- it "interprets a parameterized Hash[t] as a Hash of Literal to t" do
+ it "interprets a parameterized Hash[t] as a Hash of Scalar to t" do
expect(parser.parse("Hash[Integer]")).to be_the_type(types.hash_of(types.integer))
end
it "parses a parameterized type into the type object" do
parameterized_array = types.array_of(types.integer)
parameterized_hash = types.hash_of(types.integer, types.boolean)
expect(the_type_parsed_from(parameterized_array)).to be_the_type(parameterized_array)
expect(the_type_parsed_from(parameterized_hash)).to be_the_type(parameterized_hash)
end
- it "rejects an array spec with the wrong number of parameters" do
- expect { parser.parse("Array[Integer, Integer]") }.to raise_the_parameter_error("Array", 1, 2)
- expect { parser.parse("Hash[Integer, Integer, Integer]") }.to raise_the_parameter_error("Hash", "1 or 2", 3)
+ it "parses a size constrained collection using capped range" do
+ parameterized_array = types.array_of(types.integer)
+ types.constrain_size(parameterized_array, 1,2)
+ parameterized_hash = types.hash_of(types.integer, types.boolean)
+ types.constrain_size(parameterized_hash, 1,2)
+
+ expect(the_type_parsed_from(parameterized_array)).to be_the_type(parameterized_array)
+ expect(the_type_parsed_from(parameterized_hash)).to be_the_type(parameterized_hash)
+ end
+
+ it "parses a size constrained collection with open range" do
+ parameterized_array = types.array_of(types.integer)
+ types.constrain_size(parameterized_array, 1,:default)
+ parameterized_hash = types.hash_of(types.integer, types.boolean)
+ types.constrain_size(parameterized_hash, 1,:default)
+
+ expect(the_type_parsed_from(parameterized_array)).to be_the_type(parameterized_array)
+ expect(the_type_parsed_from(parameterized_hash)).to be_the_type(parameterized_hash)
+ end
+
+ it "parses optional type" do
+ opt_t = types.optional(Integer)
+ expect(the_type_parsed_from(opt_t)).to be_the_type(opt_t)
+ end
+
+ it "parses tuple type" do
+ tuple_t = types.tuple(Integer, String)
+ expect(the_type_parsed_from(tuple_t)).to be_the_type(tuple_t)
+ end
+
+ it "parses tuple type with occurence constraint" do
+ tuple_t = types.tuple(Integer, String)
+ types.constrain_size(tuple_t, 2, 5)
+ expect(the_type_parsed_from(tuple_t)).to be_the_type(tuple_t)
+ end
+
+ it "parses struct type" do
+ struct_t = types.struct({'a'=>Integer, 'b'=>String})
+ expect(the_type_parsed_from(struct_t)).to be_the_type(struct_t)
+ end
+
+ it "rejects an collection spec with the wrong number of parameters" do
+ expect { parser.parse("Array[Integer, 1,2,3]") }.to raise_the_parameter_error("Array", "1 to 3", 4)
+ expect { parser.parse("Hash[Integer, Integer, 1,2,3]") }.to raise_the_parameter_error("Hash", "1 to 4", 5)
+ end
+
+ it "interprets anything that is not a built in type to be a resource type" do
+ expect(parser.parse("File")).to be_the_type(types.resource('file'))
+ end
+
+ it "parses a resource type with title" do
+ expect(parser.parse("File['/tmp/foo']")).to be_the_type(types.resource('file', '/tmp/foo'))
+ end
+
+ it "parses a resource type using 'Resource[type]' form" do
+ expect(parser.parse("Resource[File]")).to be_the_type(types.resource('file'))
+ end
+
+ it "parses a resource type with title using 'Resource[type, title]'" do
+ expect(parser.parse("Resource[File, '/tmp/foo']")).to be_the_type(types.resource('file', '/tmp/foo'))
+ end
+
+ it "parses a host class type" do
+ expect(parser.parse("Class")).to be_the_type(types.host_class())
+ end
+
+ it "parses a parameterized host class type" do
+ expect(parser.parse("Class[foo::bar]")).to be_the_type(types.host_class('foo::bar'))
+ end
+
+ it 'parses an integer range' do
+ expect(parser.parse("Integer[1,2]")).to be_the_type(types.range(1,2))
+ end
+
+ it 'parses a float range' do
+ expect(parser.parse("Float[1.0,2.0]")).to be_the_type(types.float_range(1.0,2.0))
+ end
+
+ it 'parses a collection size range' do
+ expect(parser.parse("Collection[1,2]")).to be_the_type(types.constrain_size(types.collection,1,2))
+ end
+
+ it 'parses a type type' do
+ expect(parser.parse("Type[Integer]")).to be_the_type(types.type_type(types.integer))
+ end
+
+ it 'parses a ruby type' do
+ expect(parser.parse("Ruby['Integer']")).to be_the_type(types.ruby_type('Integer'))
end
matcher :be_the_type do |type|
calc = Puppet::Pops::Types::TypeCalculator.new
match do |actual|
calc.assignable?(actual, type) && calc.assignable?(type, actual)
end
failure_message_for_should do |actual|
"expected #{calc.string(type)}, but was #{calc.string(actual)}"
end
end
def raise_the_parameter_error(type, required, given)
raise_error(Puppet::ParseError, /#{type} requires #{required}, #{given} provided/)
end
def raise_type_error_for(type_name)
raise_error(Puppet::ParseError, /Unknown type <#{type_name}>/)
end
+ def raise_unparameterized_error_for(type_name)
+ raise_error(Puppet::ParseError, /Not a parameterized type <#{type_name}>/)
+ end
+
def the_type_parsed_from(type)
parser.parse(the_type_spec_for(type))
end
def the_type_spec_for(type)
calc = Puppet::Pops::Types::TypeCalculator.new
calc.string(type)
end
end
diff --git a/spec/unit/pops/validator/validator_spec.rb b/spec/unit/pops/validator/validator_spec.rb
index f97fd0019..1d865de5f 100644
--- a/spec/unit/pops/validator/validator_spec.rb
+++ b/spec/unit/pops/validator/validator_spec.rb
@@ -1,33 +1,68 @@
#! /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
pending "validation was too strict, now too relaxed - validation missing"
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
pending "validation was too strict, now too relaxed - validation missing"
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
+ it 'should raise error for -= assignment' do
+ expect(validate(fqn('aaa').minus_set(2))).to have_issue(Puppet::Pops::Issues::UNSUPPORTED_OPERATOR)
+ end
+
+end
+
+describe "validating 4x" do
+ include ParserRspecHelper
+ include PuppetSpec::Pops
+
+ let(:acceptor) { Puppet::Pops::Validation::Acceptor.new() }
+ let(:validator) { Puppet::Pops::Validation::ValidatorFactory_4_0.new().validator(acceptor) }
+
+ def validate(model)
+ validator.validate(model)
+ acceptor
+ end
+
+ it 'should raise error for illegal names' do
+ pending "validation was too strict, now too relaxed - validation missing"
+ 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_VAR_NAME)
+ expect(validate(fqn('AAA').var())).to have_issue(Puppet::Pops::Issues::ILLEGAL_VAR_NAME)
+ expect(validate(fqn('aaa::_aaa').var())).to have_issue(Puppet::Pops::Issues::ILLEGAL_VAR_NAME)
+ end
+
+ it 'should not raise error for variable name with underscore first in first name segment' do
+ expect(validate(fqn('_aa').var())).to_not have_issue(Puppet::Pops::Issues::ILLEGAL_VAR_NAME)
+ expect(validate(fqn('::_aa').var())).to_not have_issue(Puppet::Pops::Issues::ILLEGAL_VAR_NAME)
+ end
+
+end
diff --git a/spec/unit/provider/augeas/augeas_spec.rb b/spec/unit/provider/augeas/augeas_spec.rb
index 9b6e88ce8..3d3499626 100755
--- a/spec/unit/provider/augeas/augeas_spec.rb
+++ b/spec/unit/provider/augeas/augeas_spec.rb
@@ -1,874 +1,897 @@
#! /usr/bin/env ruby
require 'spec_helper'
require 'puppet/util/package'
provider_class = Puppet::Type.type(:augeas).provider(:augeas)
describe provider_class do
before(:each) do
@resource = Puppet::Type.type(:augeas).new(
:name => "test",
:root => my_fixture_dir,
:provider => :augeas
)
@provider = provider_class.new(@resource)
end
after(:each) do
@provider.close_augeas
end
describe "command parsing" do
it "should break apart a single line into three tokens and clean up the context" do
@resource[:context] = "/context"
tokens = @provider.parse_commands("set Jar/Jar Binks")
tokens.size.should == 1
tokens[0].size.should == 3
tokens[0][0].should == "set"
tokens[0][1].should == "/context/Jar/Jar"
tokens[0][2].should == "Binks"
end
it "should break apart a multiple line into six tokens" do
tokens = @provider.parse_commands("set /Jar/Jar Binks\nrm anakin")
tokens.size.should == 2
tokens[0].size.should == 3
tokens[1].size.should == 2
tokens[0][0].should == "set"
tokens[0][1].should == "/Jar/Jar"
tokens[0][2].should == "Binks"
tokens[1][0].should == "rm"
tokens[1][1].should == "anakin"
end
it "should strip whitespace and ignore blank lines" do
tokens = @provider.parse_commands(" set /Jar/Jar Binks \t\n \n\n rm anakin ")
tokens.size.should == 2
tokens[0].size.should == 3
tokens[1].size.should == 2
tokens[0][0].should == "set"
tokens[0][1].should == "/Jar/Jar"
tokens[0][2].should == "Binks"
tokens[1][0].should == "rm"
tokens[1][1].should == "anakin"
end
it "should handle arrays" do
@resource[:context] = "/foo/"
commands = ["set /Jar/Jar Binks", "rm anakin"]
tokens = @provider.parse_commands(commands)
tokens.size.should == 2
tokens[0].size.should == 3
tokens[1].size.should == 2
tokens[0][0].should == "set"
tokens[0][1].should == "/Jar/Jar"
tokens[0][2].should == "Binks"
tokens[1][0].should == "rm"
tokens[1][1].should == "/foo/anakin"
end
# This is not supported in the new parsing class
#it "should concat the last values" do
# provider = provider_class.new
# tokens = provider.parse_commands("set /Jar/Jar Binks is my copilot")
# tokens.size.should == 1
# tokens[0].size.should == 3
# tokens[0][0].should == "set"
# tokens[0][1].should == "/Jar/Jar"
# tokens[0][2].should == "Binks is my copilot"
#end
it "should accept spaces in the value and single ticks" do
@resource[:context] = "/foo/"
tokens = @provider.parse_commands("set JarJar 'Binks is my copilot'")
tokens.size.should == 1
tokens[0].size.should == 3
tokens[0][0].should == "set"
tokens[0][1].should == "/foo/JarJar"
tokens[0][2].should == "Binks is my copilot"
end
it "should accept spaces in the value and double ticks" do
@resource[:context] = "/foo/"
tokens = @provider.parse_commands('set /JarJar "Binks is my copilot"')
tokens.size.should == 1
tokens[0].size.should == 3
tokens[0][0].should == "set"
tokens[0][1].should == '/JarJar'
tokens[0][2].should == 'Binks is my copilot'
end
it "should accept mixed ticks" do
@resource[:context] = "/foo/"
tokens = @provider.parse_commands('set JarJar "Some \'Test\'"')
tokens.size.should == 1
tokens[0].size.should == 3
tokens[0][0].should == "set"
tokens[0][1].should == '/foo/JarJar'
tokens[0][2].should == "Some \'Test\'"
end
it "should handle predicates with literals" do
@resource[:context] = "/foo/"
tokens = @provider.parse_commands("rm */*[module='pam_console.so']")
tokens.should == [["rm", "/foo/*/*[module='pam_console.so']"]]
end
it "should handle whitespace in predicates" do
@resource[:context] = "/foo/"
tokens = @provider.parse_commands("ins 42 before /files/etc/hosts/*/ipaddr[ . = '127.0.0.1' ]")
tokens.should == [["ins", "42", "before","/files/etc/hosts/*/ipaddr[ . = '127.0.0.1' ]"]]
end
it "should handle multiple predicates" do
@resource[:context] = "/foo/"
tokens = @provider.parse_commands("clear pam.d/*/*[module = 'system-auth'][type = 'account']")
tokens.should == [["clear", "/foo/pam.d/*/*[module = 'system-auth'][type = 'account']"]]
end
it "should handle nested predicates" do
@resource[:context] = "/foo/"
args = ["clear", "/foo/pam.d/*/*[module[ ../type = 'type] = 'system-auth'][type[last()] = 'account']"]
tokens = @provider.parse_commands(args.join(" "))
tokens.should == [ args ]
end
it "should handle escaped doublequotes in doublequoted string" do
@resource[:context] = "/foo/"
tokens = @provider.parse_commands("set /foo \"''\\\"''\"")
tokens.should == [[ "set", "/foo", "''\\\"''" ]]
end
it "should allow escaped spaces and brackets in paths" do
@resource[:context] = "/foo/"
args = [ "set", "/white\\ space/\\[section", "value" ]
tokens = @provider.parse_commands(args.join(" \t "))
tokens.should == [ args ]
end
it "should allow single quoted escaped spaces in paths" do
@resource[:context] = "/foo/"
args = [ "set", "'/white\\ space/key'", "value" ]
tokens = @provider.parse_commands(args.join(" \t "))
tokens.should == [[ "set", "/white\\ space/key", "value" ]]
end
it "should allow double quoted escaped spaces in paths" do
@resource[:context] = "/foo/"
args = [ "set", '"/white\\ space/key"', "value" ]
tokens = @provider.parse_commands(args.join(" \t "))
tokens.should == [[ "set", "/white\\ space/key", "value" ]]
end
it "should remove trailing slashes" do
@resource[:context] = "/foo/"
tokens = @provider.parse_commands("set foo/ bar")
tokens.should == [[ "set", "/foo/foo", "bar" ]]
end
end
describe "get filters" do
before do
augeas = stub("augeas", :get => "value")
augeas.stubs("close")
@provider.aug = augeas
end
it "should return false for a = nonmatch" do
command = ["get", "fake value", "==", "value"]
@provider.process_get(command).should == true
end
it "should return true for a != match" do
command = ["get", "fake value", "!=", "value"]
@provider.process_get(command).should == false
end
it "should return true for a =~ match" do
command = ["get", "fake value", "=~", "val*"]
@provider.process_get(command).should == true
end
it "should return false for a == nonmatch" do
command = ["get", "fake value", "=~", "num*"]
@provider.process_get(command).should == false
end
end
describe "match filters" do
before do
augeas = stub("augeas", :match => ["set", "of", "values"])
augeas.stubs("close")
@provider = provider_class.new(@resource)
@provider.aug = augeas
end
it "should return true for size match" do
command = ["match", "fake value", "size == 3"]
@provider.process_match(command).should == true
end
it "should return false for a size non match" do
command = ["match", "fake value", "size < 3"]
@provider.process_match(command).should == false
end
it "should return true for includes match" do
command = ["match", "fake value", "include values"]
@provider.process_match(command).should == true
end
it "should return false for includes non match" do
command = ["match", "fake value", "include JarJar"]
@provider.process_match(command).should == false
end
it "should return true for includes match" do
command = ["match", "fake value", "not_include JarJar"]
@provider.process_match(command).should == true
end
it "should return false for includes non match" do
command = ["match", "fake value", "not_include values"]
@provider.process_match(command).should == false
end
it "should return true for an array match" do
command = ["match", "fake value", "== ['set', 'of', 'values']"]
@provider.process_match(command).should == true
end
it "should return false for an array non match" do
command = ["match", "fake value", "== ['this', 'should', 'not', 'match']"]
@provider.process_match(command).should == false
end
it "should return false for an array match with noteq" do
command = ["match", "fake value", "!= ['set', 'of', 'values']"]
@provider.process_match(command).should == false
end
it "should return true for an array non match with noteq" do
command = ["match", "fake value", "!= ['this', 'should', 'not', 'match']"]
@provider.process_match(command).should == true
end
end
describe "need to run" do
before(:each) do
@augeas = stub("augeas")
@augeas.stubs("close")
@provider.aug = @augeas
# These tests pretend to be an earlier version so the provider doesn't
# attempt to make the change in the need_to_run? method
@provider.stubs(:get_augeas_version).returns("0.3.5")
end
it "should handle no filters" do
@augeas.stubs("match").returns(["set", "of", "values"])
@provider.need_to_run?.should == true
end
it "should return true when a get filter matches" do
@resource[:onlyif] = "get path == value"
@augeas.stubs("get").returns("value")
@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")
@provider.need_to_run?.should == false
end
it "should return true when a match filter matches" do
@resource[:onlyif] = "match path size == 3"
@augeas.stubs("match").returns(["set", "of", "values"])
@provider.need_to_run?.should == true
end
it "should return false when a match filter does not match" do
@resource[:onlyif] = "match path size == 2"
@augeas.stubs("match").returns(["set", "of", "values"])
@provider.need_to_run?.should == false
end
# Now setting force to true
it "setting force should not change the above logic" do
@resource[:force] = true
@resource[:onlyif] = "match path size == 2"
@augeas.stubs("match").returns(["set", "of", "values"])
@provider.need_to_run?.should == false
end
#Ticket 5211 testing
it "should return true when a size != the provided value" do
@resource[:onlyif] = "match path size != 17"
@augeas.stubs("match").returns(["set", "of", "values"])
@provider.need_to_run?.should == true
end
#Ticket 5211 testing
it "should return false when a size doeas equal the provided value" do
@resource[:onlyif] = "match path size != 3"
@augeas.stubs("match").returns(["set", "of", "values"])
@provider.need_to_run?.should == false
end
# Ticket 2728 (diff files)
describe "and Puppet[:show_diff] is set" do
before(:each) do
Puppet[:show_diff] = true
@resource[:root] = ""
@provider.stubs(:get_augeas_version).returns("0.10.0")
@augeas.stubs(:set).returns(true)
@augeas.stubs(:save).returns(true)
end
it "should call diff when a file is shown to have been changed" do
file = "/etc/hosts"
File.stubs(:delete)
@resource[:context] = "/files"
@resource[:changes] = ["set #{file}/foo bar"]
@augeas.stubs(:match).with("/augeas/events/saved").returns(["/augeas/events/saved"])
@augeas.stubs(:get).with("/augeas/events/saved").returns("/files#{file}")
@augeas.expects(:set).with("/augeas/save", "newfile")
@augeas.expects(:close).never()
@provider.expects("diff").with("#{file}", "#{file}.augnew").returns("")
@provider.should be_need_to_run
end
it "should call diff for each file thats changed" do
file1 = "/etc/hosts"
file2 = "/etc/resolv.conf"
File.stubs(:delete)
@resource[:context] = "/files"
@resource[:changes] = ["set #{file1}/foo bar", "set #{file2}/baz biz"]
@augeas.stubs(:match).with("/augeas/events/saved").returns(["/augeas/events/saved[1]", "/augeas/events/saved[2]"])
@augeas.stubs(:get).with("/augeas/events/saved[1]").returns("/files#{file1}")
@augeas.stubs(:get).with("/augeas/events/saved[2]").returns("/files#{file2}")
@augeas.expects(:set).with("/augeas/save", "newfile")
@augeas.expects(:close).never()
@provider.expects(:diff).with("#{file1}", "#{file1}.augnew").returns("")
@provider.expects(:diff).with("#{file2}", "#{file2}.augnew").returns("")
@provider.should be_need_to_run
end
describe "and resource[:root] is set" do
it "should call diff when a file is shown to have been changed" do
root = "/tmp/foo"
file = "/etc/hosts"
File.stubs(:delete)
@resource[:context] = "/files"
@resource[:changes] = ["set #{file}/foo bar"]
@resource[:root] = root
@augeas.stubs(:match).with("/augeas/events/saved").returns(["/augeas/events/saved"])
@augeas.stubs(:get).with("/augeas/events/saved").returns("/files#{file}")
@augeas.expects(:set).with("/augeas/save", "newfile")
@augeas.expects(:close).never()
@provider.expects(:diff).with("#{root}#{file}", "#{root}#{file}.augnew").returns("")
@provider.should be_need_to_run
end
end
it "should not call diff if no files change" do
file = "/etc/hosts"
@resource[:context] = "/files"
@resource[:changes] = ["set #{file}/foo bar"]
@augeas.stubs(:match).with("/augeas/events/saved").returns([])
@augeas.expects(:set).with("/augeas/save", "newfile")
@augeas.expects(:get).with("/augeas/events/saved").never()
@augeas.expects(:close)
@provider.expects(:diff).never()
@provider.should_not be_need_to_run
end
it "should cleanup the .augnew file" do
file = "/etc/hosts"
@resource[:context] = "/files"
@resource[:changes] = ["set #{file}/foo bar"]
@augeas.stubs(:match).with("/augeas/events/saved").returns(["/augeas/events/saved"])
@augeas.stubs(:get).with("/augeas/events/saved").returns("/files#{file}")
@augeas.expects(:set).with("/augeas/save", "newfile")
@augeas.expects(:close)
File.expects(:delete).with(file + ".augnew")
@provider.expects(:diff).with("#{file}", "#{file}.augnew").returns("")
@provider.should be_need_to_run
end
# Workaround for Augeas bug #264 which reports filenames twice
it "should handle duplicate /augeas/events/saved filenames" do
file = "/etc/hosts"
@resource[:context] = "/files"
@resource[:changes] = ["set #{file}/foo bar"]
@augeas.stubs(:match).with("/augeas/events/saved").returns(["/augeas/events/saved[1]", "/augeas/events/saved[2]"])
@augeas.stubs(:get).with("/augeas/events/saved[1]").returns("/files#{file}")
@augeas.stubs(:get).with("/augeas/events/saved[2]").returns("/files#{file}")
@augeas.expects(:set).with("/augeas/save", "newfile")
@augeas.expects(:close)
File.expects(:delete).with(file + ".augnew").once()
@provider.expects(:diff).with("#{file}", "#{file}.augnew").returns("").once()
@provider.should be_need_to_run
end
it "should fail with an error if saving fails" do
file = "/etc/hosts"
@resource[:context] = "/files"
@resource[:changes] = ["set #{file}/foo bar"]
@augeas.stubs(:save).returns(false)
@augeas.stubs(:match).with("/augeas/events/saved").returns([])
@augeas.expects(:close)
@provider.expects(:diff).never()
- lambda { @provider.need_to_run? }.should raise_error
+ @provider.expects(:print_put_errors)
+ lambda { @provider.need_to_run? }.should raise_error(Puppet::Error)
end
end
end
describe "augeas execution integration" do
before do
@augeas = stub("augeas", :load)
@augeas.stubs("close")
@augeas.stubs(:match).with("/augeas/events/saved").returns([])
@provider.aug = @augeas
@provider.stubs(:get_augeas_version).returns("0.3.5")
end
it "should handle set commands" do
@resource[:changes] = "set JarJar Binks"
@resource[:context] = "/some/path/"
@augeas.expects(:set).with("/some/path/JarJar", "Binks").returns(true)
@augeas.expects(:save).returns(true)
@augeas.expects(:close)
@provider.execute_changes.should == :executed
end
it "should handle rm commands" do
@resource[:changes] = "rm /Jar/Jar"
@augeas.expects(:rm).with("/Jar/Jar")
@augeas.expects(:save).returns(true)
@augeas.expects(:close)
@provider.execute_changes.should == :executed
end
it "should handle remove commands" do
@resource[:changes] = "remove /Jar/Jar"
@augeas.expects(:rm).with("/Jar/Jar")
@augeas.expects(:save).returns(true)
@augeas.expects(:close)
@provider.execute_changes.should == :executed
end
it "should handle clear commands" do
@resource[:changes] = "clear Jar/Jar"
@resource[:context] = "/foo/"
@augeas.expects(:clear).with("/foo/Jar/Jar").returns(true)
@augeas.expects(:save).returns(true)
@augeas.expects(:close)
@provider.execute_changes.should == :executed
end
it "should handle ins commands with before" do
@resource[:changes] = "ins Binks before Jar/Jar"
@resource[:context] = "/foo"
@augeas.expects(:insert).with("/foo/Jar/Jar", "Binks", true)
@augeas.expects(:save).returns(true)
@augeas.expects(:close)
@provider.execute_changes.should == :executed
end
it "should handle ins commands with after" do
@resource[:changes] = "ins Binks after /Jar/Jar"
@resource[:context] = "/foo"
@augeas.expects(:insert).with("/Jar/Jar", "Binks", false)
@augeas.expects(:save).returns(true)
@augeas.expects(:close)
@provider.execute_changes.should == :executed
end
it "should handle ins with no context" do
@resource[:changes] = "ins Binks after /Jar/Jar"
@augeas.expects(:insert).with("/Jar/Jar", "Binks", false)
@augeas.expects(:save).returns(true)
@augeas.expects(:close)
@provider.execute_changes.should == :executed
end
it "should handle multiple commands" do
@resource[:changes] = ["ins Binks after /Jar/Jar", "clear Jar/Jar"]
@resource[:context] = "/foo/"
@augeas.expects(:insert).with("/Jar/Jar", "Binks", false)
@augeas.expects(:clear).with("/foo/Jar/Jar").returns(true)
@augeas.expects(:save).returns(true)
@augeas.expects(:close)
@provider.execute_changes.should == :executed
end
it "should handle defvar commands" do
@resource[:changes] = "defvar myjar Jar/Jar"
@resource[:context] = "/foo/"
@augeas.expects(:defvar).with("myjar", "/foo/Jar/Jar").returns(true)
@augeas.expects(:save).returns(true)
@augeas.expects(:close)
@provider.execute_changes.should == :executed
end
it "should pass through augeas variables without context" do
@resource[:changes] = ["defvar myjar Jar/Jar","set $myjar/Binks 1"]
@resource[:context] = "/foo/"
@augeas.expects(:defvar).with("myjar", "/foo/Jar/Jar").returns(true)
# this is the important bit, shouldn't be /foo/$myjar/Binks
@augeas.expects(:set).with("$myjar/Binks", "1").returns(true)
@augeas.expects(:save).returns(true)
@augeas.expects(:close)
@provider.execute_changes.should == :executed
end
it "should handle defnode commands" do
@resource[:changes] = "defnode newjar Jar/Jar[last()+1] Binks"
@resource[:context] = "/foo/"
@augeas.expects(:defnode).with("newjar", "/foo/Jar/Jar[last()+1]", "Binks").returns(true)
@augeas.expects(:save).returns(true)
@augeas.expects(:close)
@provider.execute_changes.should == :executed
end
it "should handle mv commands" do
@resource[:changes] = "mv Jar/Jar Binks"
@resource[:context] = "/foo/"
@augeas.expects(:mv).with("/foo/Jar/Jar", "/foo/Binks").returns(true)
@augeas.expects(:save).returns(true)
@augeas.expects(:close)
@provider.execute_changes.should == :executed
end
it "should handle setm commands" do
@resource[:changes] = ["set test[1]/Jar/Jar Foo","set test[2]/Jar/Jar Bar","setm test Jar/Jar Binks"]
@resource[:context] = "/foo/"
@augeas.expects(:respond_to?).with("setm").returns(true)
@augeas.expects(:set).with("/foo/test[1]/Jar/Jar", "Foo").returns(true)
@augeas.expects(:set).with("/foo/test[2]/Jar/Jar", "Bar").returns(true)
@augeas.expects(:setm).with("/foo/test", "Jar/Jar", "Binks").returns(true)
@augeas.expects(:save).returns(true)
@augeas.expects(:close)
@provider.execute_changes.should == :executed
end
it "should throw error if setm command not supported" do
@resource[:changes] = ["set test[1]/Jar/Jar Foo","set test[2]/Jar/Jar Bar","setm test Jar/Jar Binks"]
@resource[:context] = "/foo/"
@augeas.expects(:respond_to?).with("setm").returns(false)
@augeas.expects(:set).with("/foo/test[1]/Jar/Jar", "Foo").returns(true)
@augeas.expects(:set).with("/foo/test[2]/Jar/Jar", "Bar").returns(true)
expect { @provider.execute_changes }.to raise_error RuntimeError, /command 'setm' not supported/
end
it "should handle clearm commands" do
@resource[:changes] = ["set test[1]/Jar/Jar Foo","set test[2]/Jar/Jar Bar","clearm test Jar/Jar"]
@resource[:context] = "/foo/"
@augeas.expects(:respond_to?).with("clearm").returns(true)
@augeas.expects(:set).with("/foo/test[1]/Jar/Jar", "Foo").returns(true)
@augeas.expects(:set).with("/foo/test[2]/Jar/Jar", "Bar").returns(true)
@augeas.expects(:clearm).with("/foo/test", "Jar/Jar").returns(true)
@augeas.expects(:save).returns(true)
@augeas.expects(:close)
@provider.execute_changes.should == :executed
end
it "should throw error if clearm command not supported" do
@resource[:changes] = ["set test[1]/Jar/Jar Foo","set test[2]/Jar/Jar Bar","clearm test Jar/Jar"]
@resource[:context] = "/foo/"
@augeas.expects(:respond_to?).with("clearm").returns(false)
@augeas.expects(:set).with("/foo/test[1]/Jar/Jar", "Foo").returns(true)
@augeas.expects(:set).with("/foo/test[2]/Jar/Jar", "Bar").returns(true)
- expect { @provider.execute_changes }.to raise_error RuntimeError, /command 'clearm' not supported/
+ expect { @provider.execute_changes }.to raise_error(RuntimeError, /command 'clearm' not supported/)
+ end
+
+ it "should throw error if saving failed" do
+ @resource[:changes] = ["set test[1]/Jar/Jar Foo","set test[2]/Jar/Jar Bar","clearm test Jar/Jar"]
+ @resource[:context] = "/foo/"
+ @augeas.expects(:respond_to?).with("clearm").returns(true)
+ @augeas.expects(:set).with("/foo/test[1]/Jar/Jar", "Foo").returns(true)
+ @augeas.expects(:set).with("/foo/test[2]/Jar/Jar", "Bar").returns(true)
+ @augeas.expects(:clearm).with("/foo/test", "Jar/Jar").returns(true)
+ @augeas.expects(:save).returns(false)
+ @provider.expects(:print_put_errors)
+ @augeas.expects(:match).returns([])
+ expect { @provider.execute_changes }.to raise_error(Puppet::Error)
end
end
describe "when making changes", :if => Puppet.features.augeas? do
include PuppetSpec::Files
it "should not clobber the file if it's a symlink" do
Puppet::Util::Storage.stubs(:store)
link = tmpfile('link')
target = tmpfile('target')
FileUtils.touch(target)
- Puppet::FileSystem::File.new(target).symlink(link)
+ Puppet::FileSystem.symlink(target, link)
resource = Puppet::Type.type(:augeas).new(
:name => 'test',
:incl => link,
:lens => 'Sshd.lns',
:changes => "set PermitRootLogin no"
)
catalog = Puppet::Resource::Catalog.new
catalog.add_resource resource
catalog.apply
File.ftype(link).should == 'link'
- Puppet::FileSystem::File.new(link).readlink().should == target
+ Puppet::FileSystem.readlink(link).should == target
File.read(target).should =~ /PermitRootLogin no/
end
end
describe "load/save failure reporting" do
before do
@augeas = stub("augeas")
@augeas.stubs("close")
@provider.aug = @augeas
end
describe "should find load errors" do
before do
@augeas.expects(:match).with("/augeas//error").returns(["/augeas/files/foo/error"])
@augeas.expects(:match).with("/augeas/files/foo/error/*").returns(["/augeas/files/foo/error/path", "/augeas/files/foo/error/message"])
@augeas.expects(:get).with("/augeas/files/foo/error").returns("some_failure")
@augeas.expects(:get).with("/augeas/files/foo/error/path").returns("/foo")
@augeas.expects(:get).with("/augeas/files/foo/error/message").returns("Failed to...")
end
- it "and output to debug" do
+ it "and output only to debug when no path supplied" do
@provider.expects(:debug).times(5)
- @provider.print_load_errors
+ @provider.expects(:warning).never()
+ @provider.print_load_errors(nil)
end
- it "and output a warning and to debug" do
+ it "and output a warning and to debug when path supplied" do
+ @augeas.expects(:match).with("/augeas/files/foo//error").returns(["/augeas/files/foo/error"])
@provider.expects(:warning).once()
@provider.expects(:debug).times(4)
- @provider.print_load_errors(:warning => true)
+ @provider.print_load_errors('/augeas/files/foo//error')
+ end
+
+ it "and output only to debug when path doesn't match" do
+ @augeas.expects(:match).with("/augeas/files/foo//error").returns([])
+ @provider.expects(:warning).never()
+ @provider.expects(:debug).times(5)
+ @provider.print_load_errors('/augeas/files/foo//error')
end
end
it "should find load errors from lenses" do
- @augeas.expects(:match).with("/augeas//error").returns(["/augeas/load/Xfm/error"])
+ @augeas.expects(:match).with("/augeas//error").twice.returns(["/augeas/load/Xfm/error"])
@augeas.expects(:match).with("/augeas/load/Xfm/error/*").returns([])
@augeas.expects(:get).with("/augeas/load/Xfm/error").returns(["Could not find lens php.aug"])
@provider.expects(:warning).once()
@provider.expects(:debug).twice()
- @provider.print_load_errors(:warning => true)
+ @provider.print_load_errors('/augeas//error')
end
it "should find save errors and output to debug" do
@augeas.expects(:match).with("/augeas//error[. = 'put_failed']").returns(["/augeas/files/foo/error"])
@augeas.expects(:match).with("/augeas/files/foo/error/*").returns(["/augeas/files/foo/error/path", "/augeas/files/foo/error/message"])
@augeas.expects(:get).with("/augeas/files/foo/error").returns("some_failure")
@augeas.expects(:get).with("/augeas/files/foo/error/path").returns("/foo")
@augeas.expects(:get).with("/augeas/files/foo/error/message").returns("Failed to...")
@provider.expects(:debug).times(5)
@provider.print_put_errors
end
end
# Run initialisation tests of the real Augeas library to test our open_augeas
# method. This relies on Augeas and ruby-augeas on the host to be
# functioning.
describe "augeas lib initialisation", :if => Puppet.features.augeas? do
# Expect lenses for fstab and hosts
it "should have loaded standard files by default" do
aug = @provider.open_augeas
aug.should_not == nil
aug.match("/files/etc/fstab").should == ["/files/etc/fstab"]
aug.match("/files/etc/hosts").should == ["/files/etc/hosts"]
aug.match("/files/etc/test").should == []
end
it "should report load errors to debug only" do
- @provider.expects(:print_load_errors).with(:warning => false)
+ @provider.expects(:print_load_errors).with(nil)
aug = @provider.open_augeas
aug.should_not == nil
end
# Only the file specified should be loaded
it "should load one file if incl/lens used" do
@resource[:incl] = "/etc/hosts"
@resource[:lens] = "Hosts.lns"
- @provider.expects(:print_load_errors).with(:warning => true)
+ @provider.expects(:print_load_errors).with('/augeas//error')
aug = @provider.open_augeas
aug.should_not == nil
aug.match("/files/etc/fstab").should == []
aug.match("/files/etc/hosts").should == ["/files/etc/hosts"]
aug.match("/files/etc/test").should == []
end
it "should also load lenses from load_path" do
@resource[:load_path] = my_fixture_dir
aug = @provider.open_augeas
aug.should_not == nil
aug.match("/files/etc/fstab").should == ["/files/etc/fstab"]
aug.match("/files/etc/hosts").should == ["/files/etc/hosts"]
aug.match("/files/etc/test").should == ["/files/etc/test"]
end
it "should also load lenses from pluginsync'd path" do
Puppet[:libdir] = my_fixture_dir
aug = @provider.open_augeas
aug.should_not == nil
aug.match("/files/etc/fstab").should == ["/files/etc/fstab"]
aug.match("/files/etc/hosts").should == ["/files/etc/hosts"]
aug.match("/files/etc/test").should == ["/files/etc/test"]
end
# Optimisations added for Augeas 0.8.2 or higher is available, see #7285
describe ">= 0.8.2 optimisations", :if => Puppet.features.augeas? && Facter.value(:augeasversion) && Puppet::Util::Package.versioncmp(Facter.value(:augeasversion), "0.8.2") >= 0 do
it "should only load one file if relevant context given" do
@resource[:context] = "/files/etc/fstab"
- @provider.expects(:print_load_errors).with(:warning => true)
+ @provider.expects(:print_load_errors).with('/augeas/files/etc/fstab//error')
aug = @provider.open_augeas
aug.should_not == nil
aug.match("/files/etc/fstab").should == ["/files/etc/fstab"]
aug.match("/files/etc/hosts").should == []
end
it "should only load one lens from load_path if context given" do
@resource[:context] = "/files/etc/test"
@resource[:load_path] = my_fixture_dir
- @provider.expects(:print_load_errors).with(:warning => true)
+ @provider.expects(:print_load_errors).with('/augeas/files/etc/test//error')
aug = @provider.open_augeas
aug.should_not == nil
aug.match("/files/etc/fstab").should == []
aug.match("/files/etc/hosts").should == []
aug.match("/files/etc/test").should == ["/files/etc/test"]
end
it "should load standard files if context isn't specific" do
@resource[:context] = "/files/etc"
- @provider.expects(:print_load_errors).with(:warning => false)
+ @provider.expects(:print_load_errors).with(nil)
aug = @provider.open_augeas
aug.should_not == nil
aug.match("/files/etc/fstab").should == ["/files/etc/fstab"]
aug.match("/files/etc/hosts").should == ["/files/etc/hosts"]
end
it "should not optimise if the context is a complex path" do
@resource[:context] = "/files/*[label()='etc']"
- @provider.expects(:print_load_errors).with(:warning => false)
+ @provider.expects(:print_load_errors).with(nil)
aug = @provider.open_augeas
aug.should_not == nil
aug.match("/files/etc/fstab").should == ["/files/etc/fstab"]
aug.match("/files/etc/hosts").should == ["/files/etc/hosts"]
end
end
end
describe "get_load_path" do
it "should offer no load_path by default" do
@provider.get_load_path(@resource).should == ""
end
it "should offer one path from load_path" do
@resource[:load_path] = "/foo"
@provider.get_load_path(@resource).should == "/foo"
end
it "should offer multiple colon-separated paths from load_path" do
@resource[:load_path] = "/foo:/bar:/baz"
@provider.get_load_path(@resource).should == "/foo:/bar:/baz"
end
it "should offer multiple paths in array from load_path" do
@resource[:load_path] = ["/foo", "/bar", "/baz"]
@provider.get_load_path(@resource).should == "/foo:/bar:/baz"
end
it "should offer pluginsync augeas/lenses subdir" do
Puppet[:libdir] = my_fixture_dir
@provider.get_load_path(@resource).should == "#{my_fixture_dir}/augeas/lenses"
end
it "should offer both pluginsync and load_path paths" do
Puppet[:libdir] = my_fixture_dir
@resource[:load_path] = ["/foo", "/bar", "/baz"]
@provider.get_load_path(@resource).should == "/foo:/bar:/baz:#{my_fixture_dir}/augeas/lenses"
end
end
end
diff --git a/spec/unit/provider/cron/parsed_spec.rb b/spec/unit/provider/cron/parsed_spec.rb
index a96fa7ed8..68a29c88c 100644
--- a/spec/unit/provider/cron/parsed_spec.rb
+++ b/spec/unit/provider/cron/parsed_spec.rb
@@ -1,325 +1,321 @@
#!/usr/bin/env rspec
require 'spec_helper'
describe Puppet::Type.type(:cron).provider(:crontab) do
let :provider do
described_class.new(:command => '/bin/true')
end
let :resource do
Puppet::Type.type(:cron).new(
:minute => %w{0 15 30 45},
:hour => %w{8-18 20-22},
:monthday => %w{31},
:month => %w{12},
:weekday => %w{7},
:name => 'basic',
:command => '/bin/true',
:target => 'root',
:provider => provider
)
end
let :resource_special do
Puppet::Type.type(:cron).new(
:special => 'reboot',
:name => 'special',
:command => '/bin/true',
:target => 'nobody'
)
end
let :record_special do
{
:record_type => :crontab,
:special => 'reboot',
:command => '/bin/true',
:on_disk => true,
:target => 'nobody'
}
end
let :record do
{
:record_type => :crontab,
:minute => %w{0 15 30 45},
:hour => %w{8-18 20-22},
:monthday => %w{31},
:month => %w{12},
:weekday => %w{7},
:special => :absent,
:command => '/bin/true',
:on_disk => true,
:target => 'root'
}
end
describe "when determining the correct filetype" do
it "should use the suntab filetype on Solaris" do
Facter.stubs(:value).with(:osfamily).returns 'Solaris'
described_class.filetype.should == Puppet::Util::FileType::FileTypeSuntab
end
it "should use the aixtab filetype on AIX" do
Facter.stubs(:value).with(:osfamily).returns 'AIX'
described_class.filetype.should == Puppet::Util::FileType::FileTypeAixtab
end
it "should use the crontab filetype on other platforms" do
Facter.stubs(:value).with(:osfamily).returns 'Not a real operating system family'
described_class.filetype.should == Puppet::Util::FileType::FileTypeCrontab
end
end
# I'd use ENV.expects(:[]).with('USER') but this does not work because
# ENV["USER"] is evaluated at load time.
describe "when determining the default target" do
it "should use the current user #{ENV['USER']}", :if => ENV['USER'] do
described_class.default_target.should == ENV['USER']
end
it "should fallback to root", :unless => ENV['USER'] do
described_class.default_target.should == "root"
end
end
describe "when parsing a record" do
it "should parse a comment" do
described_class.parse_line("# This is a test").should == {
:record_type => :comment,
:line => "# This is a test",
}
end
it "should get the resource name of a PUPPET NAME comment" do
described_class.parse_line('# Puppet Name: My Fancy Cronjob').should == {
:record_type => :comment,
:name => 'My Fancy Cronjob',
:line => '# Puppet Name: My Fancy Cronjob',
}
end
it "should ignore blank lines" do
described_class.parse_line('').should == {:record_type => :blank, :line => ''}
described_class.parse_line(' ').should == {:record_type => :blank, :line => ' '}
described_class.parse_line("\t").should == {:record_type => :blank, :line => "\t"}
described_class.parse_line(" \t ").should == {:record_type => :blank, :line => " \t "}
end
it "should extract environment assignments" do
# man 5 crontab: MAILTO="" with no value can be used to surpress sending
# mails at all
described_class.parse_line('MAILTO=""').should == {:record_type => :environment, :line => 'MAILTO=""'}
described_class.parse_line('FOO=BAR').should == {:record_type => :environment, :line => 'FOO=BAR'}
described_class.parse_line('FOO_BAR=BAR').should == {:record_type => :environment, :line => 'FOO_BAR=BAR'}
end
it "should extract a cron entry" do
described_class.parse_line('* * * * * /bin/true').should == {
:record_type => :crontab,
:hour => :absent,
:minute => :absent,
:month => :absent,
:weekday => :absent,
:monthday => :absent,
:special => :absent,
:command => '/bin/true'
}
described_class.parse_line('0,15,30,45 8-18,20-22 31 12 7 /bin/true').should == {
:record_type => :crontab,
:minute => %w{0 15 30 45},
:hour => %w{8-18 20-22},
:monthday => %w{31},
:month => %w{12},
:weekday => %w{7},
:special => :absent,
:command => '/bin/true'
}
# A percent sign will cause the rest of the string to be passed as
# standard input and will also act as a newline character. Not sure
# if puppet should convert % to a \n as the command property so the
# test covers the current behaviour: Do not do any conversions
described_class.parse_line('0 22 * * 1-5 mail -s "It\'s 10pm" joe%Joe,%%Where are your kids?%').should == {
:record_type => :crontab,
:minute => %w{0},
:hour => %w{22},
:monthday => :absent,
:month => :absent,
:weekday => %w{1-5},
:special => :absent,
:command => 'mail -s "It\'s 10pm" joe%Joe,%%Where are your kids?%'
}
end
describe "it should support special strings" do
['reboot','yearly','anually','monthly', 'weekly', 'daily', 'midnight', 'hourly'].each do |special|
it "should support @#{special}" do
described_class.parse_line("@#{special} /bin/true").should == {
:record_type => :crontab,
:hour => :absent,
:minute => :absent,
:month => :absent,
:weekday => :absent,
:monthday => :absent,
:special => special,
:command => '/bin/true'
}
end
end
end
end
describe ".instances" do
before :each do
described_class.stubs(:default_target).returns 'foobar'
end
describe "on linux" do
before do
Facter.stubs(:value).with(:osfamily).returns 'Linux'
Facter.stubs(:value).with(:operatingsystem)
end
it "should be empty if user has no crontab" do
# `crontab...` does only capture stdout here. On vixie-cron-4.1
# STDERR shows "no crontab for foobar" but stderr is ignored as
# well as the exitcode.
described_class.target_object('foobar').expects(:`).with('crontab -u foobar -l 2>/dev/null').returns ""
described_class.instances.should be_empty
end
it "should be empty if user is not present" do
# `crontab...` does only capture stdout. On vixie-cron-4.1
# STDERR shows "crontab: user `foobar' unknown" but stderr is
# ignored as well as the exitcode
described_class.target_object('foobar').expects(:`).with('crontab -u foobar -l 2>/dev/null').returns ""
described_class.instances.should be_empty
end
it "should be able to create records from not-managed records" do
described_class.expects(:target_object).returns File.new(my_fixture('simple'))
- described_class.instances.map do |p|
+ parameters = described_class.instances.map do |p|
h = {:name => p.get(:name)}
Puppet::Type.type(:cron).validproperties.each do |property|
h[property] = p.get(property)
end
h
- end.should == [
- {
- :name => :absent,
- :minute => ['5'],
- :hour => ['0'],
- :weekday => :absent,
- :month => :absent,
- :monthday => :absent,
- :special => :absent,
- :command => '$HOME/bin/daily.job >> $HOME/tmp/out 2>&1',
- :ensure => :present,
- :environment => :absent,
- :user => :absent,
- :target => 'foobar'
- },
- {
- :name => :absent,
- :minute => ['15'],
- :hour => ['14'],
- :weekday => :absent,
- :month => :absent,
- :monthday => ['1'],
- :special => :absent,
- :command => '$HOME/bin/monthly',
- :ensure => :present,
- :environment => :absent,
- :user => :absent,
- :target => 'foobar'
- }
- ]
+ end
+
+ expect(parameters[0][:name]).to match(%r{unmanaged:\$HOME/bin/daily.job_>>_\$HOME/tmp/out_2>&1-\d+})
+ expect(parameters[0][:minute]).to eq(['5'])
+ expect(parameters[0][:hour]).to eq(['0'])
+ expect(parameters[0][:weekday]).to eq(:absent)
+ expect(parameters[0][:month]).to eq(:absent)
+ expect(parameters[0][:monthday]).to eq(:absent)
+ expect(parameters[0][:special]).to eq(:absent)
+ expect(parameters[0][:command]).to match(%r{\$HOME/bin/daily.job >> \$HOME/tmp/out 2>&1})
+ expect(parameters[0][:ensure]).to eq(:present)
+ expect(parameters[0][:environment]).to eq(:absent)
+ expect(parameters[0][:user]).to eq(:absent)
+
+ expect(parameters[1][:name]).to match(%r{unmanaged:\$HOME/bin/monthly-\d+})
+ expect(parameters[1][:minute]).to eq(['15'])
+ expect(parameters[1][:hour]).to eq(['14'])
+ expect(parameters[1][:weekday]).to eq(:absent)
+ expect(parameters[1][:month]).to eq(:absent)
+ expect(parameters[1][:monthday]).to eq(['1'])
+ expect(parameters[1][:special]).to eq(:absent)
+ expect(parameters[1][:command]).to match(%r{\$HOME/bin/monthly})
+ expect(parameters[1][:ensure]).to eq(:present)
+ expect(parameters[1][:environment]).to eq(:absent)
+ expect(parameters[1][:user]).to eq(:absent)
+ expect(parameters[1][:target]).to eq('foobar')
end
it "should be able to parse puppet manged cronjobs" do
described_class.expects(:target_object).returns File.new(my_fixture('managed'))
described_class.instances.map do |p|
h = {:name => p.get(:name)}
Puppet::Type.type(:cron).validproperties.each do |property|
h[property] = p.get(property)
end
h
end.should == [
{
:name => 'real_job',
:minute => :absent,
:hour => :absent,
:weekday => :absent,
:month => :absent,
:monthday => :absent,
:special => :absent,
:command => '/bin/true',
:ensure => :present,
:environment => :absent,
:user => :absent,
:target => 'foobar'
},
{
:name => 'complex_job',
:minute => :absent,
:hour => :absent,
:weekday => :absent,
:month => :absent,
:monthday => :absent,
:special => 'reboot',
:command => '/bin/true >> /dev/null 2>&1',
:ensure => :present,
:environment => [
'MAILTO=foo@example.com',
'SHELL=/bin/sh'
],
:user => :absent,
:target => 'foobar'
}
]
end
end
end
describe ".match" do
describe "normal records" do
it "should match when all fields are the same" do
described_class.match(record,{resource[:name] => resource}).must == resource
end
{
:minute => %w{0 15 31 45},
:hour => %w{8-18},
:monthday => %w{30 31},
:month => %w{12 23},
:weekday => %w{4},
:command => '/bin/false',
:target => 'nobody'
}.each_pair do |field, new_value|
it "should not match a record when #{field} does not match" do
record[field] = new_value
described_class.match(record,{resource[:name] => resource}).must be_false
end
end
end
describe "special records" do
it "should match when all fields are the same" do
described_class.match(record_special,{resource_special[:name] => resource_special}).must == resource_special
end
{
:special => 'monthly',
:command => '/bin/false',
:target => 'root'
}.each_pair do |field, new_value|
it "should not match a record when #{field} does not match" do
record_special[field] = new_value
described_class.match(record_special,{resource_special[:name] => resource_special}).must be_false
end
end
end
end
end
diff --git a/spec/unit/provider/file/posix_spec.rb b/spec/unit/provider/file/posix_spec.rb
index 48eaae30a..b4fe4b9ce 100755
--- a/spec/unit/provider/file/posix_spec.rb
+++ b/spec/unit/provider/file/posix_spec.rb
@@ -1,232 +1,232 @@
#! /usr/bin/env ruby
require 'spec_helper'
describe Puppet::Type.type(:file).provider(:posix), :if => Puppet.features.posix? do
include PuppetSpec::Files
let(:path) { tmpfile('posix_file_spec') }
let(:resource) { Puppet::Type.type(:file).new :path => path, :mode => 0777, :provider => described_class.name }
let(:provider) { resource.provider }
describe "#mode" do
it "should return a string with the higher-order bits stripped away" do
FileUtils.touch(path)
File.chmod(0644, path)
provider.mode.should == '644'
end
it "should return absent if the file doesn't exist" do
provider.mode.should == :absent
end
end
describe "#mode=" do
it "should chmod the file to the specified value" do
FileUtils.touch(path)
File.chmod(0644, path)
provider.mode = '0755'
provider.mode.should == '755'
end
it "should pass along any errors encountered" do
expect do
provider.mode = '644'
end.to raise_error(Puppet::Error, /failed to set mode/)
end
end
describe "#uid2name" do
it "should return the name of the user identified by the id" do
Etc.stubs(:getpwuid).with(501).returns(Struct::Passwd.new('jilluser', nil, 501))
provider.uid2name(501).should == 'jilluser'
end
it "should return the argument if it's already a name" do
provider.uid2name('jilluser').should == 'jilluser'
end
it "should return nil if the argument is above the maximum uid" do
provider.uid2name(Puppet[:maximum_uid] + 1).should == nil
end
it "should return nil if the user doesn't exist" do
Etc.expects(:getpwuid).raises(ArgumentError, "can't find user for 999")
provider.uid2name(999).should == nil
end
end
describe "#name2uid" do
it "should return the id of the user if it exists" do
passwd = Struct::Passwd.new('bobbo', nil, 502)
Etc.stubs(:getpwnam).with('bobbo').returns(passwd)
Etc.stubs(:getpwuid).with(502).returns(passwd)
provider.name2uid('bobbo').should == 502
end
it "should return the argument if it's already an id" do
provider.name2uid('503').should == 503
end
it "should return false if the user doesn't exist" do
Etc.stubs(:getpwnam).with('chuck').raises(ArgumentError, "can't find user for chuck")
provider.name2uid('chuck').should == false
end
end
describe "#owner" do
it "should return the uid of the file owner" do
FileUtils.touch(path)
- owner = Puppet::FileSystem::File.new(path).stat.uid
+ owner = Puppet::FileSystem.stat(path).uid
provider.owner.should == owner
end
it "should return absent if the file can't be statted" do
provider.owner.should == :absent
end
it "should warn and return :silly if the value is beyond the maximum uid" do
stat = stub('stat', :uid => Puppet[:maximum_uid] + 1)
resource.stubs(:stat).returns(stat)
provider.owner.should == :silly
@logs.should be_any {|log| log.level == :warning and log.message =~ /Apparently using negative UID/}
end
end
describe "#owner=" do
it "should set the owner but not the group of the file" do
File.expects(:lchown).with(15, nil, resource[:path])
provider.owner = 15
end
it "should chown a link if managing links" do
resource[:links] = :manage
File.expects(:lchown).with(20, nil, resource[:path])
provider.owner = 20
end
it "should chown a link target if following links" do
resource[:links] = :follow
File.expects(:chown).with(20, nil, resource[:path])
provider.owner = 20
end
it "should pass along any error encountered setting the owner" do
File.expects(:lchown).raises(ArgumentError)
expect { provider.owner = 25 }.to raise_error(Puppet::Error, /Failed to set owner to '25'/)
end
end
describe "#gid2name" do
it "should return the name of the group identified by the id" do
Etc.stubs(:getgrgid).with(501).returns(Struct::Passwd.new('unicorns', nil, nil, 501))
provider.gid2name(501).should == 'unicorns'
end
it "should return the argument if it's already a name" do
provider.gid2name('leprechauns').should == 'leprechauns'
end
it "should return nil if the argument is above the maximum gid" do
provider.gid2name(Puppet[:maximum_uid] + 1).should == nil
end
it "should return nil if the group doesn't exist" do
Etc.expects(:getgrgid).raises(ArgumentError, "can't find group for 999")
provider.gid2name(999).should == nil
end
end
describe "#name2gid" do
it "should return the id of the group if it exists" do
passwd = Struct::Passwd.new('penguins', nil, nil, 502)
Etc.stubs(:getgrnam).with('penguins').returns(passwd)
Etc.stubs(:getgrgid).with(502).returns(passwd)
provider.name2gid('penguins').should == 502
end
it "should return the argument if it's already an id" do
provider.name2gid('503').should == 503
end
it "should return false if the group doesn't exist" do
Etc.stubs(:getgrnam).with('wombats').raises(ArgumentError, "can't find group for wombats")
provider.name2gid('wombats').should == false
end
end
describe "#group" do
it "should return the gid of the file group" do
FileUtils.touch(path)
- group = Puppet::FileSystem::File.new(path).stat.gid
+ group = Puppet::FileSystem.stat(path).gid
provider.group.should == group
end
it "should return absent if the file can't be statted" do
provider.group.should == :absent
end
it "should warn and return :silly if the value is beyond the maximum gid" do
stat = stub('stat', :gid => Puppet[:maximum_uid] + 1)
resource.stubs(:stat).returns(stat)
provider.group.should == :silly
@logs.should be_any {|log| log.level == :warning and log.message =~ /Apparently using negative GID/}
end
end
describe "#group=" do
it "should set the group but not the owner of the file" do
File.expects(:lchown).with(nil, 15, resource[:path])
provider.group = 15
end
it "should change the group for a link if managing links" do
resource[:links] = :manage
File.expects(:lchown).with(nil, 20, resource[:path])
provider.group = 20
end
it "should change the group for a link target if following links" do
resource[:links] = :follow
File.expects(:chown).with(nil, 20, resource[:path])
provider.group = 20
end
it "should pass along any error encountered setting the group" do
File.expects(:lchown).raises(ArgumentError)
expect { provider.group = 25 }.to raise_error(Puppet::Error, /Failed to set group to '25'/)
end
end
describe "when validating" do
it "should not perform any validation" do
resource.validate
end
end
end
diff --git a/spec/unit/provider/group/windows_adsi_spec.rb b/spec/unit/provider/group/windows_adsi_spec.rb
index d28601172..a7de859da 100644
--- a/spec/unit/provider/group/windows_adsi_spec.rb
+++ b/spec/unit/provider/group/windows_adsi_spec.rb
@@ -1,167 +1,168 @@
#!/usr/bin/env ruby
require 'spec_helper'
describe Puppet::Type.type(:group).provider(:windows_adsi) do
let(:resource) do
Puppet::Type.type(:group).new(
:title => 'testers',
:provider => :windows_adsi
)
end
let(:provider) { resource.provider }
let(:connection) { stub 'connection' }
before :each do
Puppet::Util::ADSI.stubs(:computer_name).returns('testcomputername')
Puppet::Util::ADSI.stubs(:connect).returns connection
end
describe ".instances" do
it "should enumerate all groups" do
names = ['group1', 'group2', 'group3']
stub_groups = names.map{|n| stub(:name => n)}
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']
provider.members.should =~ ['user1', 'user2', 'user3']
end
it "should be able to set group members", :if => Puppet.features.microsoft_windows? do
provider.group.stubs(:members).returns ['user1', 'user2']
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
end
describe 'when creating groups' do
it "should be able to create a group" do
resource[:members] = ['user1', 'user2']
group = stub 'group'
Puppet::Util::ADSI::Group.expects(:create).with('testers').returns group
create = sequence('create')
group.expects(:commit).in_sequence(create)
group.expects(:set_members).with(['user1', 'user2']).in_sequence(create)
provider.create
end
it 'should not create a group if a user by the same name exists' do
Puppet::Util::ADSI::Group.expects(:create).with('testers').raises( Puppet::Error.new("Cannot create group if user 'testers' exists.") )
expect{ provider.create }.to raise_error( Puppet::Error,
/Cannot create group if user 'testers' exists./ )
end
it 'should commit a newly created group' do
provider.group.expects( :commit )
provider.flush
end
end
it "should be able to test whether a group exists" do
+ Puppet::Util::ADSI.stubs(:sid_uri_safe).returns(nil)
Puppet::Util::ADSI.stubs(:connect).returns stub('connection')
provider.should be_exists
Puppet::Util::ADSI.stubs(:connect).returns nil
provider.should_not be_exists
end
it "should be able to delete a group" do
connection.expects(:Delete).with('group', 'testers')
provider.delete
end
it "should report the group's SID as gid", :if => Puppet.features.microsoft_windows? do
Puppet::Util::Windows::Security.expects(:name_to_sid).with('testers').returns('S-1-5-32-547')
provider.gid.should == 'S-1-5-32-547'
end
it "should fail when trying to manage the gid property" 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/mount_spec.rb b/spec/unit/provider/mount_spec.rb
index 34d819c57..7c3714478 100755
--- a/spec/unit/provider/mount_spec.rb
+++ b/spec/unit/provider/mount_spec.rb
@@ -1,154 +1,165 @@
#! /usr/bin/env ruby
require 'spec_helper'
require 'puppet/provider/mount'
describe Puppet::Provider::Mount do
before :each do
@mounter = Object.new
@mounter.extend(Puppet::Provider::Mount)
@name = "/"
@resource = stub 'resource'
@resource.stubs(:[]).with(:name).returns(@name)
@mounter.stubs(:resource).returns(@resource)
end
describe Puppet::Provider::Mount, " when mounting" do
before :each do
@mounter.stubs(:get).with(:ensure).returns(:mounted)
end
it "should use the 'mountcmd' method to mount" do
@mounter.stubs(:options).returns(nil)
@mounter.expects(:mountcmd)
@mounter.mount
end
it "should add the options following '-o' on MacOS if they exist and are not set to :absent" do
Facter.expects(:value).with(:kernel).returns 'Darwin'
@mounter.stubs(:options).returns("ro")
@mounter.expects(:mountcmd).with '-o', 'ro', '/'
@mounter.mount
end
it "should not explicitly pass mount options on systems other than MacOS" do
Facter.expects(:value).with(:kernel).returns 'HP-UX'
@mounter.stubs(:options).returns("ro")
@mounter.expects(:mountcmd).with '/'
@mounter.mount
end
it "should specify the filesystem name to the mount command" do
@mounter.stubs(:options).returns(nil)
@mounter.expects(:mountcmd).with { |*ary| ary[-1] == @name }
@mounter.mount
end
it "should update the :ensure state to :mounted if it was :unmounted before" do
@mounter.expects(:mountcmd)
@mounter.stubs(:options).returns(nil)
@mounter.expects(:get).with(:ensure).returns(:unmounted)
@mounter.expects(:set).with(:ensure => :mounted)
@mounter.mount
end
it "should update the :ensure state to :ghost if it was :absent before" do
@mounter.expects(:mountcmd)
@mounter.stubs(:options).returns(nil)
@mounter.expects(:get).with(:ensure).returns(:absent)
@mounter.expects(:set).with(:ensure => :ghost)
@mounter.mount
end
end
describe Puppet::Provider::Mount, " when remounting" do
it "should use '-o remount' if the resource specifies it supports remounting" do
@mounter.stubs(:info)
@resource.stubs(:[]).with(:remounts).returns(:true)
@mounter.expects(:mountcmd).with("-o", "remount", @name)
@mounter.remount
end
+ it "should mount with '-o update' on OpenBSD" do
+ @mounter.stubs(:info)
+ @mounter.stubs(:options)
+ @resource.stubs(:[]).with(:remounts).returns(false)
+ Facter.expects(:value).with(:operatingsystem).returns 'OpenBSD'
+ @mounter.expects(:mountcmd).with("-o", "update", @name)
+ @mounter.remount
+ end
+
it "should unmount and mount if the resource does not specify it supports remounting" do
@mounter.stubs(:info)
+ @mounter.stubs(:options)
@resource.stubs(:[]).with(:remounts).returns(false)
- @mounter.expects(:unmount)
+ Facter.expects(:value).with(:operatingsystem).returns 'AIX'
@mounter.expects(:mount)
+ @mounter.expects(:unmount)
@mounter.remount
end
it "should log that it is remounting" do
@resource.stubs(:[]).with(:remounts).returns(:true)
@mounter.stubs(:mountcmd)
@mounter.expects(:info).with("Remounting")
@mounter.remount
end
end
describe Puppet::Provider::Mount, " when unmounting" do
before :each do
@mounter.stubs(:get).with(:ensure).returns(:unmounted)
end
it "should call the :umount command with the resource name" do
@mounter.expects(:umount).with(@name)
@mounter.unmount
end
it "should update the :ensure state to :absent if it was :ghost before" do
@mounter.expects(:umount).with(@name).returns true
@mounter.expects(:get).with(:ensure).returns(:ghost)
@mounter.expects(:set).with(:ensure => :absent)
@mounter.unmount
end
it "should update the :ensure state to :unmounted if it was :mounted before" do
@mounter.expects(:umount).with(@name).returns true
@mounter.expects(:get).with(:ensure).returns(:mounted)
@mounter.expects(:set).with(:ensure => :unmounted)
@mounter.unmount
end
end
describe Puppet::Provider::Mount, " when determining if it is mounted" do
it "should query the property_hash" do
@mounter.expects(:get).with(:ensure).returns(:mounted)
@mounter.mounted?
end
it "should return true if prefetched value is :mounted" do
@mounter.stubs(:get).with(:ensure).returns(:mounted)
@mounter.mounted? == true
end
it "should return true if prefetched value is :ghost" do
@mounter.stubs(:get).with(:ensure).returns(:ghost)
@mounter.mounted? == true
end
it "should return false if prefetched value is :absent" do
@mounter.stubs(:get).with(:ensure).returns(:absent)
@mounter.mounted? == false
end
it "should return false if prefetched value is :unmounted" do
@mounter.stubs(:get).with(:ensure).returns(:unmounted)
@mounter.mounted? == false
end
end
end
diff --git a/spec/unit/provider/nameservice/directoryservice_spec.rb b/spec/unit/provider/nameservice/directoryservice_spec.rb
index fbb74d0f3..2b8c29d74 100755
--- a/spec/unit/provider/nameservice/directoryservice_spec.rb
+++ b/spec/unit/provider/nameservice/directoryservice_spec.rb
@@ -1,189 +1,189 @@
#! /usr/bin/env ruby
require 'spec_helper'
# We use this as a reasonable way to obtain all the support infrastructure.
[:group].each do |type_for_this_round|
provider_class = Puppet::Type.type(type_for_this_round).provider(:directoryservice)
describe provider_class do
before do
@resource = stub("resource")
@provider = provider_class.new(@resource)
end
it "[#6009] should handle nested arrays of members" do
current = ["foo", "bar", "baz"]
desired = ["foo", ["quux"], "qorp"]
group = 'example'
@resource.stubs(:[]).with(:name).returns(group)
@resource.stubs(:[]).with(:auth_membership).returns(true)
@provider.instance_variable_set(:@property_value_cache_hash,
{ :members => current })
%w{bar baz}.each do |del|
@provider.expects(:execute).once.
with([:dseditgroup, '-o', 'edit', '-n', '.', '-d', del, group])
end
%w{quux qorp}.each do |add|
@provider.expects(:execute).once.
with([:dseditgroup, '-o', 'edit', '-n', '.', '-a', add, group])
end
expect { @provider.set(:members, desired) }.to_not raise_error
end
end
end
describe 'DirectoryService.single_report' do
it 'should fail on OS X < 10.5' do
Puppet::Provider::NameService::DirectoryService.stubs(:get_macosx_version_major).returns("10.4")
expect {
Puppet::Provider::NameService::DirectoryService.single_report('resource_name')
}.to raise_error(RuntimeError, "Puppet does not support OS X versions < 10.5")
end
it 'should use plist data on >= 10.5' do
Puppet::Provider::NameService::DirectoryService.stubs(:get_macosx_version_major).returns("10.5")
Puppet::Provider::NameService::DirectoryService.stubs(:get_ds_path).returns('Users')
Puppet::Provider::NameService::DirectoryService.stubs(:list_all_present).returns(
['root', 'user1', 'user2', 'resource_name']
)
Puppet::Provider::NameService::DirectoryService.stubs(:generate_attribute_hash)
Puppet::Provider::NameService::DirectoryService.stubs(:execute)
Puppet::Provider::NameService::DirectoryService.expects(:parse_dscl_plist_data)
Puppet::Provider::NameService::DirectoryService.single_report('resource_name')
end
end
describe 'DirectoryService.get_exec_preamble' do
it 'should fail on OS X < 10.5' do
Puppet::Provider::NameService::DirectoryService.stubs(:get_macosx_version_major).returns("10.4")
expect {
Puppet::Provider::NameService::DirectoryService.get_exec_preamble('-list')
}.to raise_error(RuntimeError, "Puppet does not support OS X versions < 10.5")
end
it 'should use plist data on >= 10.5' do
Puppet::Provider::NameService::DirectoryService.stubs(:get_macosx_version_major).returns("10.5")
Puppet::Provider::NameService::DirectoryService.stubs(:get_ds_path).returns('Users')
Puppet::Provider::NameService::DirectoryService.get_exec_preamble('-list').should include("-plist")
end
end
describe 'DirectoryService password behavior' do
# The below is a binary plist containing a ShadowHashData key which CONTAINS
# another binary plist. The nested binary plist contains a 'SALTED-SHA512'
# key that contains a base64 encoded salted-SHA512 password hash...
let (:binary_plist) { "bplist00\324\001\002\003\004\005\006\a\bXCRAM-MD5RNT]SALTED-SHA512[RECOVERABLEO\020 \231k2\3360\200GI\201\355J\216\202\215y\243\001\206J\300\363\032\031\022\006\2359\024\257\217<\361O\020\020F\353\at\377\277\226\276c\306\254\031\037J(\235O\020D\335\006{\3744g@\377z\204\322\r\332t\021\330\n\003\246K\223\356\034!P\261\305t\035\346\352p\206\003n\247MMA\310\301Z<\366\246\023\0161W3\340\357\000\317T\t\301\311+\204\246L7\276\370\320*\245O\021\002\000k\024\221\270x\353\001\237\346D}\377?\265]\356+\243\v[\350\316a\340h\376<\322\266\327\016\306n\272r\t\212A\253L\216\214\205\016\241 [\360/\335\002#\\A\372\241a\261\346\346\\\251\330\312\365\016\n\341\017\016\225&;\322\\\004*\ru\316\372\a \362?8\031\247\231\030\030\267\315\023\v\343{@\227\301s\372h\212\000a\244&\231\366\nt\277\2036,\027bZ+\223W\212g\333`\264\331N\306\307\362\257(^~ b\262\247&\231\261t\341\231%\244\247\203eOt\365\271\201\273\330\350\363C^A\327F\214!\217hgf\e\320k\260n\315u~\336\371M\t\235k\230S\375\311\303\240\351\037d\273\321y\335=K\016`_\317\230\2612_\023K\036\350\v\232\323Y\310\317_\035\227%\237\v\340\023\016\243\233\025\306:\227\351\370\364x\234\231\266\367\016w\275\333-\351\210}\375x\034\262\272kRuHa\362T/F!\347B\231O`K\304\037'k$$\245h)e\363\365mT\b\317\\2\361\026\351\254\375Jl1~\r\371\267\352\2322I\341\272\376\243^Un\266E7\230[VocUJ\220N\2116D/\025f=\213\314\325\vG}\311\360\377DT\307m\261&\263\340\272\243_\020\271rG^BW\210\030l\344\0324\335\233\300\023\272\225Im\330\n\227*Yv[\006\315\330y'\a\321\373\273A\240\305F{S\246I#/\355\2425\031\031GGF\270y\n\331\004\023G@\331\000\361\343\350\264$\032\355_\210y\000\205\342\375\212q\024\004\026W:\205 \363v?\035\270L-\270=\022\323\2003\v\336\277\t\237\356\374\n\267n\003\367\342\330;\371S\326\016`B6@Njm>\240\021%\336\345\002(P\204Yn\3279l\0228\264\254\304\2528t\372h\217\347sA\314\345\245\337)]\000\b\000\021\000\032\000\035\000+\0007\000Z\000m\000\264\000\000\000\000\000\000\002\001\000\000\000\000\000\000\000\t\000\000\000\000\000\000\000\000\000\000\000\000\000\000\002\270" }
# The below is a base64 encoded salted-SHA512 password hash.
let (:pw_string) { "\335\006{\3744g@\377z\204\322\r\332t\021\330\n\003\246K\223\356\034!P\261\305t\035\346\352p\206\003n\247MMA\310\301Z<\366\246\023\0161W3\340\357\000\317T\t\301\311+\204\246L7\276\370\320*\245" }
# The below is a salted-SHA512 password hash in hex.
let (:sha512_hash) { 'dd067bfc346740ff7a84d20dda7411d80a03a64b93ee1c2150b1c5741de6ea7086036ea74d4d41c8c15a3cf6a6130e315733e0ef00cf5409c1c92b84a64c37bef8d02aa5' }
let :plist_path do
'/var/db/dslocal/nodes/Default/users/jeff.plist'
end
let :ds_provider do
Puppet::Provider::NameService::DirectoryService
end
let :shadow_hash_data do
{'ShadowHashData' => [StringIO.new(binary_plist)]}
end
subject do
Puppet::Provider::NameService::DirectoryService
end
before :each do
subject.expects(:get_macosx_version_major).returns("10.7")
end
it 'should execute convert_binary_to_xml once when getting the password on >= 10.7' do
subject.expects(:convert_binary_to_xml).returns({'SALTED-SHA512' => StringIO.new(pw_string)})
- Puppet::FileSystem::File.expects(:exist?).with(plist_path).once.returns(true)
+ Puppet::FileSystem.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)
subject.get_password('uid', 'jeff')
end
it 'should fail if a salted-SHA512 password hash is not passed in >= 10.7' do
expect {
subject.set_password('jeff', 'uid', 'badpassword')
}.to raise_error(RuntimeError, /OS X 10.7 requires a Salted SHA512 hash password of 136 characters./)
end
it 'should convert xml-to-binary and binary-to-xml when setting the pw on >= 10.7' do
subject.expects(:convert_binary_to_xml).returns({'SALTED-SHA512' => StringIO.new(pw_string)})
subject.expects(:convert_xml_to_binary).returns(binary_plist)
- Puppet::FileSystem::File.expects(:exist?).with(plist_path).once.returns(true)
+ Puppet::FileSystem.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)
# And again back to a binary plist or DirectoryService will complain
subject.expects(:plutil).with('-convert', 'binary1', plist_path)
Plist::Emit.expects(:save_plist).with(shadow_hash_data, plist_path)
subject.set_password('jeff', 'uid', sha512_hash)
end
it '[#13686] should handle an empty ShadowHashData field in the users plist' do
subject.expects(:convert_xml_to_binary).returns(binary_plist)
- Puppet::FileSystem::File.expects(:exist?).with(plist_path).once.returns(true)
+ Puppet::FileSystem.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)
Plist::Emit.expects(:save_plist)
subject.set_password('jeff', 'uid', sha512_hash)
end
end
describe '(#4855) directoryservice group resource failure' do
let :provider_class do
Puppet::Type.type(:group).provider(:directoryservice)
end
let :group_members do
['root','jeff']
end
let :user_account do
['root']
end
let :stub_resource do
stub('resource')
end
subject do
provider_class.new(stub_resource)
end
before :each do
@resource = stub("resource")
@provider = provider_class.new(@resource)
end
it 'should delete a group member if the user does not exist' do
stub_resource.stubs(:[]).with(:name).returns('fake_group')
stub_resource.stubs(:name).returns('fake_group')
subject.expects(:execute).with([:dseditgroup, '-o', 'edit', '-n', '.',
'-d', 'jeff',
'fake_group']).raises(Puppet::ExecutionFailure,
'it broke')
subject.expects(:execute).with([:dscl, '.', '-delete',
'/Groups/fake_group', 'GroupMembership',
'jeff'])
subject.remove_unwanted_members(group_members, user_account)
end
end
diff --git a/spec/unit/provider/package/apt_spec.rb b/spec/unit/provider/package/apt_spec.rb
index 5d7b3e4e6..a60e1f206 100755
--- a/spec/unit/provider/package/apt_spec.rb
+++ b/spec/unit/provider/package/apt_spec.rb
@@ -1,146 +1,146 @@
#! /usr/bin/env ruby
require 'spec_helper'
provider = Puppet::Type.type(:package).provider(:apt)
describe provider do
before do
@resource = stub 'resource', :[] => "asdf"
@provider = provider.new(@resource)
@fakeresult = <<-EOF
install ok installed asdf 1.0 "asdf summary
asdf multiline description
with multiple lines
EOF
end
it "should be versionable" do
provider.should be_versionable
end
it "should use :install to update" do
@provider.expects(:install)
@provider.update
end
it "should use 'apt-get remove' to uninstall" do
@provider.expects(:aptget).with("-y", "-q", :remove, "asdf")
@provider.uninstall
end
it "should use 'apt-get purge' and 'dpkg purge' to purge" do
@provider.expects(:aptget).with("-y", "-q", :remove, "--purge", "asdf")
@provider.expects(:dpkg).with("--purge", "asdf")
@provider.purge
end
it "should use 'apt-cache policy' to determine the latest version of a package" do
@provider.expects(:aptcache).with(:policy, "asdf").returns "asdf:
Installed: 1:1.0
Candidate: 1:1.1
Version table:
1:1.0
650 http://ftp.osuosl.org testing/main Packages
*** 1:1.1
100 /var/lib/dpkg/status"
@provider.latest.should == "1:1.1"
end
it "should print and error and return nil if no policy is found" do
@provider.expects(:aptcache).with(:policy, "asdf").returns "asdf:"
@provider.expects(:err)
@provider.latest.should be_nil
end
it "should be able to preseed" do
@provider.should respond_to(:run_preseed)
end
it "should preseed with the provided responsefile when preseeding is called for" do
@resource.expects(:[]).with(:responsefile).returns "/my/file"
- Puppet::FileSystem::File.expects(:exist?).with("/my/file").returns true
+ Puppet::FileSystem.expects(:exist?).with("/my/file").returns true
@provider.expects(:info)
@provider.expects(:preseed).with("/my/file")
@provider.run_preseed
end
it "should not preseed if no responsefile is provided" do
@resource.expects(:[]).with(:responsefile).returns nil
@provider.expects(:info)
@provider.expects(:preseed).never
@provider.run_preseed
end
describe "when installing" do
it "should preseed if a responsefile is provided" do
@resource.expects(:[]).with(:responsefile).returns "/my/file"
@provider.expects(:run_preseed)
@provider.stubs(:aptget)
@provider.install
end
it "should check for a cdrom" do
@provider.expects(:checkforcdrom)
@provider.stubs(:aptget)
@provider.install
end
it "should use 'apt-get install' with the package name if no version is asked for" do
@resource.expects(:[]).with(:ensure).returns :installed
@provider.expects(:aptget).with { |*command| command[-1] == "asdf" and command[-2] == :install }
@provider.install
end
it "should specify the package version if one is asked for" do
@resource.expects(:[]).with(:ensure).returns "1.0"
@provider.expects(:aptget).with { |*command| command[-1] == "asdf=1.0" }
@provider.install
end
it "should use --force-yes if a package version is specified" do
@resource.expects(:[]).with(:ensure).returns "1.0"
@provider.expects(:aptget).with { |*command| command.include?("--force-yes") }
@provider.install
end
it "should do a quiet install" do
@provider.expects(:aptget).with { |*command| command.include?("-q") }
@provider.install
end
it "should default to 'yes' for all questions" do
@provider.expects(:aptget).with { |*command| command.include?("-y") }
@provider.install
end
it "should keep config files if asked" do
@resource.expects(:[]).with(:configfiles).returns :keep
@provider.expects(:aptget).with { |*command| command.include?("DPkg::Options::=--force-confold") }
@provider.install
end
it "should replace config files if asked" do
@resource.expects(:[]).with(:configfiles).returns :replace
@provider.expects(:aptget).with { |*command| command.include?("DPkg::Options::=--force-confnew") }
@provider.install
end
end
end
diff --git a/spec/unit/provider/package/aptrpm_spec.rb b/spec/unit/provider/package/aptrpm_spec.rb
index 5db0a975e..081996d25 100755
--- a/spec/unit/provider/package/aptrpm_spec.rb
+++ b/spec/unit/provider/package/aptrpm_spec.rb
@@ -1,45 +1,45 @@
#! /usr/bin/env ruby
require 'spec_helper'
describe Puppet::Type.type(:package).provider(:aptrpm) do
let :type do Puppet::Type.type(:package) end
let :pkg do
type.new(:name => 'faff', :provider => :aptrpm, :source => '/tmp/faff.rpm')
end
it { should be_versionable }
context "when retrieving ensure" do
before(:each) do
Puppet::Util.stubs(:which).with("rpm").returns("/bin/rpm")
pkg.provider.stubs(:which).with("rpm").returns("/bin/rpm")
Puppet::Util::Execution.expects(:execute).with(["/bin/rpm", "--version"], {:combine => true, :custom_environment => {}, :failonfail => true}).returns("4.10.1\n").at_most_once
end
def rpm
pkg.provider.expects(:rpm).
- with('-q', 'faff', '--nosignature', '--nodigest', '--qf',
+ with('-q', '--whatprovides', 'faff', '--nosignature', '--nodigest', '--qf',
"%{NAME} %|EPOCH?{%{EPOCH}}:{0}| %{VERSION} %{RELEASE} %{ARCH}\\n")
end
it "should report absent packages" do
rpm.raises(Puppet::ExecutionFailure, "couldn't find rpm")
pkg.property(:ensure).retrieve.should == :absent
end
it "should report present packages correctly" do
rpm.returns("faff-1.2.3-1 0 1.2.3-1 5 i686\n")
pkg.property(:ensure).retrieve.should == "1.2.3-1-5"
end
end
it "should try and install when asked" do
pkg.provider.expects(:aptget). with('-q', '-y', 'install', 'faff'). returns(0)
pkg.provider.install
end
it "should try and purge when asked" do
pkg.provider.expects(:aptget).with('-y', '-q', 'remove', '--purge', 'faff').returns(0)
pkg.provider.purge
end
end
diff --git a/spec/unit/provider/package/gem_spec.rb b/spec/unit/provider/package/gem_spec.rb
index 02cb2cabd..7cedaedac 100755
--- a/spec/unit/provider/package/gem_spec.rb
+++ b/spec/unit/provider/package/gem_spec.rb
@@ -1,150 +1,162 @@
#! /usr/bin/env ruby
require 'spec_helper'
provider_class = Puppet::Type.type(:package).provider(:gem)
describe provider_class do
let(:resource) do
Puppet::Type.type(:package).new(
:name => 'myresource',
:ensure => :installed
)
end
let(:provider) do
provider = provider_class.new
provider.resource = resource
provider
end
describe "when installing" do
it "should use the path to the gem" do
provider_class.stubs(:command).with(:gemcmd).returns "/my/gem"
provider.expects(:execute).with { |args| args[0] == "/my/gem" }.returns ""
provider.install
end
it "should specify that the gem is being installed" do
provider.expects(:execute).with { |args| args[1] == "install" }.returns ""
provider.install
end
it "should specify that documentation should not be included" do
provider.expects(:execute).with { |args| args[2] == "--no-rdoc" }.returns ""
provider.install
end
it "should specify that RI should not be included" do
provider.expects(:execute).with { |args| args[3] == "--no-ri" }.returns ""
provider.install
end
it "should specify the package name" do
provider.expects(:execute).with { |args| args[4] == "myresource" }.returns ""
provider.install
end
describe "when a source is specified" do
describe "as a normal file" do
it "should use the file name instead of the gem name" do
resource[:source] = "/my/file"
provider.expects(:execute).with { |args| args[2] == "/my/file" }.returns ""
provider.install
end
end
describe "as a file url" do
it "should use the file name instead of the gem name" do
resource[:source] = "file:///my/file"
provider.expects(:execute).with { |args| args[2] == "/my/file" }.returns ""
provider.install
end
end
describe "as a puppet url" do
it "should fail" do
resource[:source] = "puppet://my/file"
lambda { provider.install }.should raise_error(Puppet::Error)
end
end
describe "as a non-file and non-puppet url" do
it "should treat the source as a gem repository" do
resource[:source] = "http://host/my/file"
provider.expects(:execute).with { |args| args[2..4] == ["--source", "http://host/my/file", "myresource"] }.returns ""
provider.install
end
end
describe "with an invalid uri" do
it "should fail" do
URI.expects(:parse).raises(ArgumentError)
resource[:source] = "http:::::uppet:/:/my/file"
lambda { provider.install }.should raise_error(Puppet::Error)
end
end
end
end
describe "#latest" do
it "should return a single value for 'latest'" do
#gemlist is used for retrieving both local and remote version numbers, and there are cases
# (particularly local) where it makes sense for it to return an array. That doesn't make
# sense for '#latest', though.
provider.class.expects(:gemlist).with({ :justme => 'myresource'}).returns({
:name => 'myresource',
:ensure => ["3.0"],
:provider => :gem,
})
provider.latest.should == "3.0"
end
it "should list from the specified source repository" do
resource[:source] = "http://foo.bar.baz/gems"
provider.class.expects(:gemlist).
with({:justme => 'myresource', :source => "http://foo.bar.baz/gems"}).
returns({
:name => 'myresource',
:ensure => ["3.0"],
:provider => :gem,
})
provider.latest.should == "3.0"
end
end
describe "#instances" do
before do
provider_class.stubs(:command).with(:gemcmd).returns "/my/gem"
end
it "should return an empty array when no gems installed" do
provider_class.expects(:execute).with(%w{/my/gem list --local}).returns("\n")
provider_class.instances.should == []
end
it "should return ensure values as an array of installed versions" do
provider_class.expects(:execute).with(%w{/my/gem list --local}).returns <<-HEREDOC.gsub(/ /, '')
systemu (1.2.0)
vagrant (0.8.7, 0.6.9)
HEREDOC
provider_class.instances.map {|p| p.properties}.should == [
{:ensure => ["1.2.0"], :provider => :gem, :name => 'systemu'},
{:ensure => ["0.8.7", "0.6.9"], :provider => :gem, :name => 'vagrant'}
]
end
+ it "should ignore platform specifications" do
+ provider_class.expects(:execute).with(%w{/my/gem list --local}).returns <<-HEREDOC.gsub(/ /, '')
+ systemu (1.2.0)
+ nokogiri (1.6.1 ruby java x86-mingw32 x86-mswin32-60, 1.4.4.1 x86-mswin32)
+ HEREDOC
+
+ provider_class.instances.map {|p| p.properties}.should == [
+ {:ensure => ["1.2.0"], :provider => :gem, :name => 'systemu'},
+ {:ensure => ["1.6.1", "1.4.4.1"], :provider => :gem, :name => 'nokogiri'}
+ ]
+ end
+
it "should not fail when an unmatched line is returned" do
provider_class.expects(:execute).with(%w{/my/gem list --local}).
returns(File.read(my_fixture('line-with-1.8.5-warning')))
provider_class.instances.map {|p| p.properties}.
should == [{:provider=>:gem, :ensure=>["0.3.2"], :name=>"columnize"},
{:provider=>:gem, :ensure=>["1.1.3"], :name=>"diff-lcs"},
{:provider=>:gem, :ensure=>["0.0.1"], :name=>"metaclass"},
{:provider=>:gem, :ensure=>["0.10.5"], :name=>"mocha"},
{:provider=>:gem, :ensure=>["0.8.7"], :name=>"rake"},
{:provider=>:gem, :ensure=>["2.9.0"], :name=>"rspec-core"},
{:provider=>:gem, :ensure=>["2.9.1"], :name=>"rspec-expectations"},
{:provider=>:gem, :ensure=>["2.9.0"], :name=>"rspec-mocks"},
{:provider=>:gem, :ensure=>["0.9.0"], :name=>"rubygems-bundler"},
{:provider=>:gem, :ensure=>["1.11.3.3"], :name=>"rvm"}]
end
end
end
diff --git a/spec/unit/provider/package/msi_spec.rb b/spec/unit/provider/package/msi_spec.rb
index 192b0a749..9c325bb49 100755
--- a/spec/unit/provider/package/msi_spec.rb
+++ b/spec/unit/provider/package/msi_spec.rb
@@ -1,225 +1,229 @@
#! /usr/bin/env ruby
require 'spec_helper'
describe Puppet::Type.type(:package).provider(:msi) do
let (:name) { 'mysql-5.1.58-win-x64' }
let (:source) { 'E:\mysql-5.1.58-win-x64.msi' }
let (:productcode) { '{E437FFB6-5C49-4DAC-ABAE-33FF065FE7CC}' }
let (:packagecode) { '{5A6FD560-763A-4BC1-9E03-B18DFFB7C72C}' }
let (:resource) { Puppet::Type.type(:package).new(:name => name, :provider => :msi, :source => source) }
let (:provider) { resource.provider }
let (:execute_options) do {:failonfail => false, :combine => true} end
def installer(productcodes)
installer = mock
installer.expects(:UILevel=).with(2)
installer.stubs(:ProductState).returns(5)
installer.stubs(:Products).returns(productcodes)
productcodes.each do |guid|
installer.stubs(:ProductInfo).with(guid, 'ProductName').returns("name-#{guid}")
installer.stubs(:ProductInfo).with(guid, 'PackageCode').returns("package-#{guid}")
end
MsiPackage.stubs(:installer).returns(installer)
end
def expect_execute(command, status)
provider.expects(:execute).with(command, execute_options).returns(Puppet::Util::Execution::ProcessOutput.new('',status))
end
describe 'provider features' do
it { should be_installable }
it { should be_uninstallable }
it { should be_install_options }
it { should be_uninstall_options }
end
describe 'on Windows', :as_platform => :windows do
+ after :each do
+ Puppet::Type.type(:package).defaultprovider = nil
+ end
+
it 'should not be the default provider' do
# provider.expects(:execute).never
Puppet::Type.type(:package).defaultprovider.should_not == subject.class
end
end
context '::instances' do
it 'should return an empty array' do
described_class.instances.should == []
end
end
context '#initialize' do
it 'should issue a deprecation warning' do
Puppet.expects(:deprecation_warning).with("The `:msi` package provider is deprecated, use the `:windows` provider instead.")
Puppet::Type.type(:package).new(:name => name, :provider => :msi, :source => source)
end
end
context '#query' do
let (:package) do {
:name => name,
:ensure => :installed,
:provider => :msi,
:productcode => productcode,
:packagecode => packagecode.upcase
}
end
before :each do
MsiPackage.stubs(:each).yields(package)
end
it 'should match package codes case-insensitively' do
resource[:name] = packagecode.downcase
provider.query.should == package
end
it 'should match product name' do
resource[:name] = name
provider.query.should == package
end
it 'should return nil if none found' do
resource[:name] = 'not going to find it'
provider.query.should be_nil
end
end
context '#install' do
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)
expect do
resource.provider.install
end.to raise_error(Puppet::Error, /The source parameter is required when using the MSI provider/)
end
it 'should install using the source and install_options' do
resource[:install_options] = { 'INSTALLDIR' => 'C:\mysql-5.1' }
expect_execute("#{command} INSTALLDIR=C:\\mysql-5.1", 0)
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.')
provider.install
end
it 'should warn if reboot required' do
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
expect_execute(command, 5)
expect do
provider.install
end.to raise_error(Puppet::Util::Windows::Error, /Access is denied/)
end
end
context '#uninstall' do
let (:command) { "msiexec.exe /qn /norestart /x #{productcode}" }
before :each do
resource[:ensure] = :absent
provider.set(:productcode => productcode)
end
it 'should require the productcode' do
provider.set(:productcode => nil)
expect do
provider.uninstall
end.to raise_error(Puppet::Error, /The productcode property is missing./)
end
it 'should uninstall using the productcode' do
expect_execute(command, 0)
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.')
provider.uninstall
end
it 'should warn if reboot required' do
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
expect_execute(command, 5)
expect do
provider.uninstall
end.to raise_error(Puppet::Util::Windows::Error, /Failed to uninstall.*Access is denied/)
end
end
context '#validate_source' do
it 'should fail if the source parameter is empty' do
expect do
resource[:source] = ''
end.to raise_error(Puppet::Error, /The source parameter cannot be empty when using the MSI provider/)
end
it 'should accept a source' do
resource[:source] = source
end
end
context '#install_options' do
it 'should return nil by default' do
provider.install_options.should be_nil
end
it 'should return the options' do
resource[:install_options] = { 'INSTALLDIR' => 'C:\mysql-here' }
provider.install_options.should == ['INSTALLDIR=C:\mysql-here']
end
it 'should only quote if needed' do
resource[:install_options] = { 'INSTALLDIR' => 'C:\mysql here' }
provider.install_options.should == ['INSTALLDIR="C:\mysql here"']
end
it 'should escape embedded quotes in install_options values with spaces' do
resource[:install_options] = { 'INSTALLDIR' => 'C:\mysql "here"' }
provider.install_options.should == ['INSTALLDIR="C:\mysql \"here\""']
end
end
context '#uninstall_options' do
it 'should return nil by default' do
provider.uninstall_options.should be_nil
end
it 'should return the options' do
resource[:uninstall_options] = { 'INSTALLDIR' => 'C:\mysql-here' }
provider.uninstall_options.should == ['INSTALLDIR=C:\mysql-here']
end
end
end
diff --git a/spec/unit/provider/package/openbsd_spec.rb b/spec/unit/provider/package/openbsd_spec.rb
index 48ec4bedb..8d4f079fe 100755
--- a/spec/unit/provider/package/openbsd_spec.rb
+++ b/spec/unit/provider/package/openbsd_spec.rb
@@ -1,312 +1,312 @@
#! /usr/bin/env ruby
require 'spec_helper'
require 'stringio'
provider_class = Puppet::Type.type(:package).provider(:openbsd)
describe provider_class do
let(:package) { Puppet::Type.type(:package).new(:name => 'bash', :provider => 'openbsd') }
let(:provider) { provider_class.new(package) }
def expect_read_from_pkgconf(lines)
pkgconf = stub(:readlines => lines)
- Puppet::FileSystem::File.expects(:exist?).with('/etc/pkg.conf').returns(true)
+ Puppet::FileSystem.expects(:exist?).with('/etc/pkg.conf').returns(true)
File.expects(:open).with('/etc/pkg.conf', 'rb').returns(pkgconf)
end
def expect_pkgadd_with_source(source)
provider.expects(:pkgadd).with do |fullname|
- ENV.should_not be_key 'PKG_PATH'
+ ENV.should_not be_key('PKG_PATH')
fullname.should == source
end
end
def expect_pkgadd_with_env_and_name(source, &block)
- ENV.should_not be_key 'PKG_PATH'
+ ENV.should_not be_key('PKG_PATH')
provider.expects(:pkgadd).with do |fullname|
- ENV.should be_key 'PKG_PATH'
+ ENV.should be_key('PKG_PATH')
ENV['PKG_PATH'].should == source
fullname.should == provider.resource[:name]
end
provider.expects(:execpipe).with(['/bin/pkg_info', '-I', provider.resource[:name]]).yields('')
yield
- ENV.should_not be_key 'PKG_PATH'
+ ENV.should_not be_key('PKG_PATH')
end
before :each do
# Stub some provider methods to avoid needing the actual software
# installed, so we can test on whatever platform we want.
provider_class.stubs(:command).with(:pkginfo).returns('/bin/pkg_info')
provider_class.stubs(:command).with(:pkgadd).returns('/bin/pkg_add')
provider_class.stubs(:command).with(:pkgdelete).returns('/bin/pkg_delete')
end
context "::instances" do
it "should return nil if execution failed" do
provider_class.expects(:execpipe).raises(Puppet::ExecutionFailure, 'wawawa')
provider_class.instances.should be_nil
end
it "should return the empty set if no packages are listed" do
provider_class.expects(:execpipe).with(%w{/bin/pkg_info -a}).yields(StringIO.new(''))
provider_class.instances.should be_empty
end
it "should return all packages when invoked" do
fixture = File.read(my_fixture('pkginfo.list'))
provider_class.expects(:execpipe).with(%w{/bin/pkg_info -a}).yields(fixture)
provider_class.instances.map(&:name).sort.should ==
%w{bash bzip2 expat gettext libiconv lzo openvpn python vim wget}.sort
end
it "should return all flavors if set" do
fixture = File.read(my_fixture('pkginfo_flavors.list'))
provider_class.expects(:execpipe).with(%w{/bin/pkg_info -a}).yields(fixture)
instances = provider_class.instances.map {|p| {:name => p.get(:name),
:ensure => p.get(:ensure), :flavor => p.get(:flavor)}}
instances.size.should == 2
instances[0].should == {:name => 'bash', :ensure => '3.1.17', :flavor => 'static'}
instances[1].should == {:name => 'vim', :ensure => '7.0.42', :flavor => 'no_x11'}
end
end
context "#install" do
it "should fail if the resource doesn't have a source" do
- Puppet::FileSystem::File.expects(:exist?).with('/etc/pkg.conf').returns(false)
+ Puppet::FileSystem.expects(:exist?).with('/etc/pkg.conf').returns(false)
expect {
provider.install
- }.to raise_error Puppet::Error, /must specify a package source/
+ }.to raise_error(Puppet::Error, /must specify a package source/)
end
it "should fail if /etc/pkg.conf exists, but is not readable" do
- Puppet::FileSystem::File.expects(:exist?).with('/etc/pkg.conf').returns(true)
+ Puppet::FileSystem.expects(:exist?).with('/etc/pkg.conf').returns(true)
File.expects(:open).with('/etc/pkg.conf', 'rb').raises(Errno::EACCES)
expect {
provider.install
- }.to raise_error Errno::EACCES, /Permission denied/
+ }.to raise_error(Errno::EACCES, /Permission denied/)
end
it "should fail if /etc/pkg.conf exists, but there is no installpath" do
expect_read_from_pkgconf([])
expect {
provider.install
- }.to raise_error Puppet::Error, /No valid installpath found in \/etc\/pkg\.conf and no source was set/
+ }.to raise_error(Puppet::Error, /No valid installpath found in \/etc\/pkg\.conf and no source was set/)
end
it "should install correctly when given a directory-unlike source" do
source = '/whatever.pkg'
provider.resource[:source] = source
expect_pkgadd_with_source(source)
provider.install
end
it "should install correctly when given a directory-like source" do
source = '/whatever/'
provider.resource[:source] = source
expect_pkgadd_with_env_and_name(source) do
provider.install
end
end
it "should install correctly when given a CDROM installpath" do
dir = '/mnt/cdrom/5.2/packages/amd64/'
expect_read_from_pkgconf(["installpath = #{dir}"])
expect_pkgadd_with_env_and_name(dir) do
provider.install
end
end
it "should install correctly when given a ftp mirror" do
url = 'ftp://your.ftp.mirror/pub/OpenBSD/5.2/packages/amd64/'
expect_read_from_pkgconf(["installpath = #{url}"])
expect_pkgadd_with_env_and_name(url) do
provider.install
end
end
it "should set the resource's source parameter" do
url = 'ftp://your.ftp.mirror/pub/OpenBSD/5.2/packages/amd64/'
expect_read_from_pkgconf(["installpath = #{url}"])
expect_pkgadd_with_env_and_name(url) do
provider.install
end
provider.resource[:source].should == url
end
it "should strip leading whitespace in installpath" do
dir = '/one/'
lines = ["# Notice the extra spaces after the ='s\n",
"installpath = #{dir}\n",
"# And notice how each line ends with a newline\n"]
expect_read_from_pkgconf(lines)
expect_pkgadd_with_env_and_name(dir) do
provider.install
end
end
it "should not require spaces around the equals" do
dir = '/one/'
lines = ["installpath=#{dir}"]
expect_read_from_pkgconf(lines)
expect_pkgadd_with_env_and_name(dir) do
provider.install
end
end
it "should be case-insensitive" do
dir = '/one/'
lines = ["INSTALLPATH = #{dir}"]
expect_read_from_pkgconf(lines)
expect_pkgadd_with_env_and_name(dir) do
provider.install
end
end
it "should ignore unknown keywords" do
dir = '/one/'
lines = ["foo = bar\n",
"installpath = #{dir}\n"]
expect_read_from_pkgconf(lines)
expect_pkgadd_with_env_and_name(dir) do
provider.install
end
end
it "should preserve trailing spaces" do
dir = '/one/ '
lines = ["installpath = #{dir}"]
expect_read_from_pkgconf(lines)
expect_pkgadd_with_source(dir)
provider.install
end
it "should append installpath" do
urls = ["ftp://your.ftp.mirror/pub/OpenBSD/5.2/packages/amd64/",
"http://another.ftp.mirror/pub/OpenBSD/5.2/packages/amd64/"]
lines = ["installpath = #{urls[0]}\n",
"installpath += #{urls[1]}\n"]
expect_read_from_pkgconf(lines)
expect_pkgadd_with_env_and_name(urls.join(":")) do
provider.install
end
end
it "should handle append on first installpath" do
url = "ftp://your.ftp.mirror/pub/OpenBSD/5.2/packages/amd64/"
lines = ["installpath += #{url}\n"]
expect_read_from_pkgconf(lines)
expect_pkgadd_with_env_and_name(url) do
provider.install
end
end
%w{ installpath installpath= installpath+=}.each do |line|
it "should reject '#{line}'" do
expect_read_from_pkgconf([line])
expect {
provider.install
}.to raise_error(Puppet::Error, /No valid installpath found in \/etc\/pkg\.conf and no source was set/)
end
end
end
context "#get_version" do
it "should return nil if execution fails" do
provider.expects(:execpipe).raises(Puppet::ExecutionFailure, 'wawawa')
provider.get_version.should be_nil
end
it "should return the package version if in the output" do
fixture = File.read(my_fixture('pkginfo.list'))
provider.expects(:execpipe).with(%w{/bin/pkg_info -I bash}).yields(fixture)
provider.get_version.should == '3.1.17'
end
it "should return the empty string if the package is not present" do
provider.resource[:name] = 'zsh'
provider.expects(:execpipe).with(%w{/bin/pkg_info -I zsh}).yields(StringIO.new(''))
provider.get_version.should == ''
end
end
context "#query" do
it "should return the installed version if present" do
fixture = File.read(my_fixture('pkginfo.detail'))
provider.expects(:pkginfo).with('bash').returns(fixture)
provider.query.should == { :ensure => '3.1.17' }
end
it "should return nothing if not present" do
provider.resource[:name] = 'zsh'
provider.expects(:pkginfo).with('zsh').returns('')
provider.query.should be_nil
end
end
context "#install_options" do
it "should return nill by default" do
provider.install_options.should be_nil
end
it "should return install_options when set" do
provider.resource[:install_options] = ['-n']
provider.resource[:install_options].should == ['-n']
end
it "should return multiple install_options when set" do
provider.resource[:install_options] = ['-L', '/opt/puppet']
provider.resource[: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
end
context "#uninstall_options" do
it "should return nill by default" do
provider.uninstall_options.should be_nil
end
it "should return uninstall_options when set" do
provider.resource[:uninstall_options] = ['-n']
provider.resource[:uninstall_options].should == ['-n']
end
it "should return multiple uninstall_options when set" do
provider.resource[:uninstall_options] = ['-q', '-c']
provider.resource[:uninstall_options].should == ['-q', '-c']
end
it 'should return uninstall_options when set as hash' do
provider.resource[:uninstall_options] = { '-Dbaddepend' => '1' }
provider.uninstall_options.should == ['-Dbaddepend=1']
end
end
context "#uninstall" do
describe 'when uninstalling' do
it 'should use erase to purge' do
provider.expects(:pkgdelete).with('-c', '-q', 'bash')
provider.purge
end
end
end
end
diff --git a/spec/unit/provider/package/pacman_spec.rb b/spec/unit/provider/package/pacman_spec.rb
index 21de2af7e..789fd88fb 100755
--- a/spec/unit/provider/package/pacman_spec.rb
+++ b/spec/unit/provider/package/pacman_spec.rb
@@ -1,266 +1,295 @@
#! /usr/bin/env ruby
require 'spec_helper'
require 'stringio'
provider = Puppet::Type.type(:package).provider(:pacman)
describe provider do
let(:no_extra_options) { { :failonfail => true, :combine => true, :custom_environment => {} } }
let(:executor) { Puppet::Util::Execution }
let(:resolver) { Puppet::Util }
before do
resolver.stubs(:which).with('/usr/bin/pacman').returns('/usr/bin/pacman')
provider.stubs(:which).with('/usr/bin/pacman').returns('/usr/bin/pacman')
+ resolver.stubs(:which).with('/usr/bin/yaourt').returns('/usr/bin/yaourt')
+ provider.stubs(:which).with('/usr/bin/yaourt').returns('/usr/bin/yaourt')
@resource = Puppet::Type.type(:package).new(:name => 'package')
@provider = provider.new(@resource)
end
describe "when installing" do
before do
@provider.stubs(:query).returns({
:ensure => '1.0'
})
end
it "should call pacman to install the right package quietly" do
+
+ if @provider.yaourt?
+ args = ['/usr/bin/yaourt', '--noconfirm', '-S', @resource[:name]]
+ else
+ args = ['/usr/bin/pacman', '--noconfirm', '--noprogressbar', '-Sy', @resource[:name]]
+ end
+
executor.
expects(:execute).
at_least_once.
- with(["/usr/bin/pacman", "--noconfirm", "--noprogressbar", "-Sy", @resource[:name]], no_extra_options).
- returns ""
+ with(args, no_extra_options).
+ returns ''
@provider.install
end
it "should raise an ExecutionFailure if the installation failed" do
executor.stubs(:execute).returns("")
@provider.expects(:query).returns(nil)
lambda { @provider.install }.should raise_exception(Puppet::ExecutionFailure)
end
context "when :source is specified" do
before :each do
@install = sequence("install")
end
context "recognizable by pacman" do
%w{
/some/package/file
http://some.package.in/the/air
ftp://some.package.in/the/air
}.each do |source|
it "should install #{source} directly" do
@resource[:source] = source
executor.expects(:execute).
with(all_of(includes("-Sy"), includes("--noprogressbar")), no_extra_options).
in_sequence(@install).
returns("")
executor.expects(:execute).
with(all_of(includes("-U"), includes(source)), no_extra_options).
in_sequence(@install).
returns("")
@provider.install
end
end
end
context "as a file:// URL" do
before do
@package_file = "file:///some/package/file"
@actual_file_path = "/some/package/file"
@resource[:source] = @package_file
end
it "should install from the path segment of the URL" do
executor.expects(:execute).
with(all_of(includes("-Sy"),
includes("--noprogressbar"),
includes("--noconfirm")),
no_extra_options).
in_sequence(@install).
returns("")
executor.expects(:execute).
with(all_of(includes("-U"), includes(@actual_file_path)), no_extra_options).
in_sequence(@install).
returns("")
@provider.install
end
end
context "as a puppet URL" do
before do
@resource[:source] = "puppet://server/whatever"
end
it "should fail" do
lambda { @provider.install }.should raise_error(Puppet::Error)
end
end
context "as a malformed URL" do
before do
@resource[:source] = "blah://"
end
it "should fail" do
lambda { @provider.install }.should raise_error(Puppet::Error)
end
end
end
end
describe "when updating" do
it "should call install" do
@provider.expects(:install).returns("install return value")
@provider.update.should == "install return value"
end
end
describe "when uninstalling" do
it "should call pacman to remove the right package quietly" do
executor.
expects(:execute).
with(["/usr/bin/pacman", "--noconfirm", "--noprogressbar", "-R", @resource[:name]], no_extra_options).
returns ""
@provider.uninstall
end
end
describe "when querying" do
it "should query pacman" do
executor.
expects(:execute).
with(["/usr/bin/pacman", "-Qi", @resource[:name]], no_extra_options)
@provider.query
end
it "should return the version" do
query_output = <<EOF
Name : package
Version : 1.01.3-2
URL : http://www.archlinux.org/pacman/
Licenses : GPL
Groups : base
Provides : None
Depends On : bash libarchive>=2.7.1 libfetch>=2.25 pacman-mirrorlist
Optional Deps : fakeroot: for makepkg usage as normal user
curl: for rankmirrors usage
Required By : None
Conflicts With : None
Replaces : None
Installed Size : 2352.00 K
Packager : Dan McGee <dan@archlinux.org>
Architecture : i686
Build Date : Sat 22 Jan 2011 03:56:41 PM EST
Install Date : Thu 27 Jan 2011 06:45:49 AM EST
Install Reason : Explicitly installed
Install Script : Yes
Description : A library-based package manager with dependency support
EOF
executor.expects(:execute).returns(query_output)
@provider.query.should == {:ensure => "1.01.3-2"}
end
it "should return a nil if the package isn't found" do
executor.expects(:execute).returns("")
@provider.query.should be_nil
end
it "should return a hash indicating that the package is missing on error" do
executor.expects(:execute).raises(Puppet::ExecutionFailure.new("ERROR!"))
@provider.query.should == {
:ensure => :purged,
:status => 'missing',
:name => @resource[:name],
:error => 'ok',
}
end
end
+
+
describe "when fetching a package list" do
- it "should query pacman" do
+ it "should retrieve installed packages" do
provider.expects(:execpipe).with(["/usr/bin/pacman", '-Q'])
- provider.instances
+ provider.installedpkgs
+ end
+
+ it "should retrieve installed package groups" do
+ provider.expects(:execpipe).with(["/usr/bin/pacman", '-Qg'])
+ provider.installedgroups
end
it "should return installed packages with their versions" do
provider.expects(:execpipe).yields(StringIO.new("package1 1.23-4\npackage2 2.00\n"))
- packages = provider.instances
+ packages = provider.installedpkgs
packages.length.should == 2
packages[0].properties.should == {
:provider => :pacman,
:ensure => '1.23-4',
:name => 'package1'
}
packages[1].properties.should == {
:provider => :pacman,
:ensure => '2.00',
:name => 'package2'
}
end
+ it "should return installed groups with a dummy version" do
+ provider.expects(:execpipe).yields(StringIO.new("group1 pkg1\ngroup1 pkg2"))
+ groups = provider.installedgroups
+
+ groups.length.should == 1
+
+ groups[0].properties.should == {
+ :provider => :pacman,
+ :ensure => '1',
+ :name => 'group1'
+ }
+ end
+
it "should return nil on error" do
- provider.expects(:execpipe).raises(Puppet::ExecutionFailure.new("ERROR!"))
+ provider.expects(:execpipe).twice.raises(Puppet::ExecutionFailure.new("ERROR!"))
provider.instances.should be_nil
end
it "should warn on invalid input" do
provider.expects(:execpipe).yields(StringIO.new("blah"))
provider.expects(:warning).with("Failed to match line blah")
- provider.instances.should == []
+ provider.installedpkgs == []
end
end
describe "when determining the latest version" do
it "should refresh package list" do
get_latest_version = sequence("get_latest_version")
executor.
expects(:execute).
in_sequence(get_latest_version).
with(['/usr/bin/pacman', '-Sy'], no_extra_options)
executor.
stubs(:execute).
in_sequence(get_latest_version).
returns("")
@provider.latest
end
it "should get query pacman for the latest version" do
get_latest_version = sequence("get_latest_version")
executor.
stubs(:execute).
in_sequence(get_latest_version)
executor.
expects(:execute).
in_sequence(get_latest_version).
with(['/usr/bin/pacman', '-Sp', '--print-format', '%v', @resource[:name]], no_extra_options).
returns("")
@provider.latest
end
it "should return the version number from pacman" do
executor.
expects(:execute).
at_least_once().
returns("1.00.2-3\n")
@provider.latest.should == "1.00.2-3"
end
end
end
diff --git a/spec/unit/provider/package/pkgin_spec.rb b/spec/unit/provider/package/pkgin_spec.rb
index c62ab685a..8b6d4e51d 100644
--- a/spec/unit/provider/package/pkgin_spec.rb
+++ b/spec/unit/provider/package/pkgin_spec.rb
@@ -1,176 +1,178 @@
require "spec_helper"
provider_class = Puppet::Type.type(:package).provider(:pkgin)
describe provider_class do
- let(:resource) { Puppet::Type.type(:package).new(:name => "vim") }
- subject { provider_class.new(resource) }
+ let(:resource) { Puppet::Type.type(:package).new(:name => "vim", :provider => :pkgin) }
+ subject { resource.provider }
describe "Puppet provider interface" do
it "can return the list of all packages" do
provider_class.should respond_to(:instances)
end
end
describe "#install" do
- before { resource[:ensure] = :absent }
+ describe "a package not installed" do
+ before { resource[:ensure] = :absent }
it "uses pkgin install to install" do
- subject.expects(:pkgin).with("-y", :install, "vim")
+ subject.expects(:pkgin).with("-y", :install, "vim").once()
+ subject.install
+ end
+ end
+
+ describe "a package with a fixed version" do
+ before { resource[:ensure] = '7.2.446' }
+ it "uses pkgin install to install a fixed version" do
+ subject.expects(:pkgin).with("-y", :install, "vim-7.2.446").once()
subject.install
end
+ end
+
end
describe "#uninstall" do
- before { resource[:ensure] = :present }
-
it "uses pkgin remove to uninstall" do
- subject.expects(:pkgin).with("-y", :remove, "vim")
+ subject.expects(:pkgin).with("-y", :remove, "vim").once()
subject.uninstall
end
end
describe "#instances" do
let(:pkgin_ls_output) do
"zlib-1.2.3 General purpose data compression library\nzziplib-0.13.59 Library for ZIP archive handling\n"
end
before do
provider_class.stubs(:pkgin).with(:list).returns(pkgin_ls_output)
end
it "returns an array of providers for each package" do
instances = provider_class.instances
instances.should have(2).items
instances.each do |instance|
instance.should be_a(provider_class)
end
end
it "populates each provider with an installed package" do
zlib_provider, zziplib_provider = provider_class.instances
zlib_provider.get(:name).should == "zlib"
- zlib_provider.get(:ensure).should == :present
+ zlib_provider.get(:ensure).should == "1.2.3"
zziplib_provider.get(:name).should == "zziplib"
- zziplib_provider.get(:ensure).should == :present
+ zziplib_provider.get(:ensure).should == "0.13.59"
end
end
- describe "#query" do
+ describe "#latest" do
before do
provider_class.stubs(:pkgin).with(:search, "vim").returns(pkgin_search_output)
end
context "when the package is installed" do
let(:pkgin_search_output) do
"vim-7.2.446 = Vim editor (vi clone) without GUI\nvim-share-7.2.446 = Data files for the vim editor (vi clone)\n\n=: package is installed and up-to-date\n<: package is installed but newer version is available\n>: installed package has a greater version than available package\n"
end
- it "returns a hash stating the package is present" do
- result = subject.query
- result[:ensure].should == :present
- result[:name].should == "vim"
- result[:provider].should == :pkgin
+ it "returns installed version" do
+ subject.expects(:properties).returns( { :ensure => "7.2.446" } )
+ subject.latest.should == "7.2.446"
end
end
context "when the package is out of date" do
let(:pkgin_search_output) do
- "vim-7.2.446 < Vim editor (vi clone) without GUI\nvim-share-7.2.446 = Data files for the vim editor (vi clone)\n\n=: package is installed and up-to-date\n<: package is installed but newer version is available\n>: installed package has a greater version than available package\n"
+ "vim-7.2.447 < Vim editor (vi clone) without GUI\nvim-share-7.2.447 < Data files for the vim editor (vi clone)\n\n=: package is installed and up-to-date\n<: package is installed but newer version is available\n>: installed package has a greater version than available package\n"
end
- it "returns a hash stating the package is present" do
- result = subject.query
- result[:ensure].should == :present
- result[:name].should == "vim"
- result[:provider].should == :pkgin
+ it "returns the version to be installed" do
+ subject.latest.should == "7.2.447"
end
end
context "when the package is ahead of date" do
let(:pkgin_search_output) do
- "vim-7.2.446 > Vim editor (vi clone) without GUI\nvim-share-7.2.446 = Data files for the vim editor (vi clone)\n\n=: package is installed and up-to-date\n<: package is installed but newer version is available\n>: installed package has a greater version than available package\n"
+ "vim-7.2.446 > Vim editor (vi clone) without GUI\nvim-share-7.2.446 > Data files for the vim editor (vi clone)\n\n=: package is installed and up-to-date\n<: package is installed but newer version is available\n>: installed package has a greater version than available package\n"
end
- it "returns a hash stating the package is present" do
- result = subject.query
- result[:ensure].should == :present
- result[:name].should == "vim"
- result[:provider].should == :pkgin
+ it "returns current version" do
+ subject.expects(:properties).returns( { :ensure => "7.2.446" } )
+ subject.latest.should == "7.2.446"
end
end
- context "when the package is not installed" do
+ context "when multiple candidates do exists" do
let(:pkgin_search_output) do
- "vim-7.2.446 Vim editor (vi clone) without GUI\nvim-share-7.2.446 = Data files for the vim editor (vi clone)\n\n=: package is installed and up-to-date\n<: package is installed but newer version is available\n>: installed package has a greater version than available package\n"
+ <<-SEARCH
+vim-7.1 > Vim editor (vi clone) without GUI
+vim-share-7.1 > Data files for the vim editor (vi clone)
+vim-7.2.446 = Vim editor (vi clone) without GUI
+vim-share-7.2.446 = Data files for the vim editor (vi clone)
+vim-7.3 < Vim editor (vi clone) without GUI
+vim-share-7.3 < Data files for the vim editor (vi clone)
+
+=: package is installed and up-to-date
+<: package is installed but newer version is available
+>: installed package has a greater version than available package
+SEARCH
end
- it "returns a hash stating the package is present" do
- result = subject.query
- result[:ensure].should == :absent
- result[:name].should == "vim"
- result[:provider].should == :pkgin
+ it "returns the newest available version" do
+ provider_class.stubs(:pkgin).with(:search, "vim").returns(pkgin_search_output)
+ subject.latest.should == "7.3"
end
end
context "when the package cannot be found" do
let(:pkgin_search_output) do
- "\n=: package is installed and up-to-date\n<: package is installed but newer version is available\n>: installed package has a greater version than available package\n"
+ "No results found for is-puppet"
end
it "returns nil" do
- subject.query.should be_nil
+ expect { subject.latest }.to raise_error(Puppet::Error, "No candidate to be installed")
end
end
end
describe "#parse_pkgin_line" do
context "with an installed package" do
let(:package) { "vim-7.2.446 = Vim editor (vi clone) without GUI" }
it "extracts the name and status" do
- hash = provider_class.parse_pkgin_line(package)
- hash[:name].should == "vim"
- hash[:ensure].should == :present
- hash[:provider].should == :pkgin
+ provider_class.parse_pkgin_line(package).should == { :name => "vim" ,
+ :status => "=" ,
+ :ensure => "7.2.446" }
end
end
context "with an installed package with a hyphen in the name" do
- let(:package) { "ruby18-puppet-0.25.5nb1 = Configuration management framework written in Ruby" }
+ let(:package) { "ruby18-puppet-0.25.5nb1 > Configuration management framework written in Ruby" }
it "extracts the name and status" do
- hash = provider_class.parse_pkgin_line(package)
- hash[:name].should == "ruby18-puppet"
- hash[:ensure].should == :present
- hash[:provider].should == :pkgin
+ provider_class.parse_pkgin_line(package).should == { :name => "ruby18-puppet",
+ :status => ">" ,
+ :ensure => "0.25.5nb1" }
end
end
context "with a package not yet installed" do
let(:package) { "vim-7.2.446 Vim editor (vi clone) without GUI" }
it "extracts the name and status" do
- hash = provider_class.parse_pkgin_line(package)
- hash[:name].should == "vim"
- hash[:ensure].should == :absent
- hash[:provider].should == :pkgin
+ provider_class.parse_pkgin_line(package).should == { :name => "vim" ,
+ :status => nil ,
+ :ensure => "7.2.446" }
end
- it "extracts the name and an overridden status" do
- hash = provider_class.parse_pkgin_line(package, :present)
- hash[:name].should == "vim"
- hash[:ensure].should == :present
- hash[:provider].should == :pkgin
- end
end
context "with an invalid package" do
let(:package) { "" }
it "returns nil" do
provider_class.parse_pkgin_line(package).should be_nil
end
end
end
end
diff --git a/spec/unit/provider/package/rpm_spec.rb b/spec/unit/provider/package/rpm_spec.rb
index 221a77e50..b599c6333 100755
--- a/spec/unit/provider/package/rpm_spec.rb
+++ b/spec/unit/provider/package/rpm_spec.rb
@@ -1,322 +1,367 @@
#! /usr/bin/env ruby
require 'spec_helper'
provider_class = Puppet::Type.type(:package).provider(:rpm)
describe provider_class do
let (:packages) do
<<-RPM_OUTPUT
cracklib-dicts 0 2.8.9 3.3 x86_64
basesystem 0 8.0 5.1.1.el5.centos noarch
chkconfig 0 1.3.30.2 2.el5 x86_64
myresource 0 1.2.3.4 5.el4 noarch
mysummaryless 0 1.2.3.4 5.el4 noarch
RPM_OUTPUT
end
let(:resource_name) { 'myresource' }
let(:resource) do
Puppet::Type.type(:package).new(
:name => resource_name,
:ensure => :installed,
:provider => 'rpm'
)
end
let(:provider) do
provider = provider_class.new
provider.resource = resource
provider
end
let(:nevra_format) { %Q{%{NAME} %|EPOCH?{%{EPOCH}}:{0}| %{VERSION} %{RELEASE} %{ARCH}\\n} }
let(:execute_options) do
{:failonfail => true, :combine => true, :custom_environment => {}}
end
let(:rpm_version) { "RPM version 5.0.0\n" }
before(:each) do
Puppet::Util.stubs(:which).with("rpm").returns("/bin/rpm")
provider_class.stubs(:which).with("rpm").returns("/bin/rpm")
provider_class.instance_variable_set("@current_version", nil)
Puppet::Type::Package::ProviderRpm.expects(:execute).with(["/bin/rpm", "--version"]).returns(rpm_version).at_most_once
Puppet::Util::Execution.expects(:execute).with(["/bin/rpm", "--version"], execute_options).returns(rpm_version).at_most_once
end
describe "self.instances" do
describe "with a modern version of RPM" do
it "includes all the modern flags" do
Puppet::Util::Execution.expects(:execpipe).with("/bin/rpm -qa --nosignature --nodigest --qf '#{nevra_format}'").yields(packages)
installed_packages = provider_class.instances
end
end
describe "with a version of RPM < 4.1" do
let(:rpm_version) { "RPM version 4.0.2\n" }
it "excludes the --nosignature flag" do
Puppet::Util::Execution.expects(:execpipe).with("/bin/rpm -qa --nodigest --qf '#{nevra_format}'").yields(packages)
installed_packages = provider_class.instances
end
end
describe "with a version of RPM < 4.0.2" do
let(:rpm_version) { "RPM version 3.0.5\n" }
it "excludes the --nodigest flag" do
Puppet::Util::Execution.expects(:execpipe).with("/bin/rpm -qa --qf '#{nevra_format}'").yields(packages)
installed_packages = provider_class.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)
installed_packages = provider_class.instances
expect(installed_packages[0].properties).to eq(
{
:provider => :rpm,
:name => "cracklib-dicts",
:epoch => "0",
:version => "2.8.9",
:release => "3.3",
:arch => "x86_64",
:ensure => "2.8.9-3.3",
}
)
expect(installed_packages[1].properties).to eq(
{
:provider => :rpm,
:name => "basesystem",
:epoch => "0",
:version => "8.0",
:release => "5.1.1.el5.centos",
:arch => "noarch",
:ensure => "8.0-5.1.1.el5.centos",
}
)
expect(installed_packages[2].properties).to eq(
{
:provider => :rpm,
:name => "chkconfig",
:epoch => "0",
:version => "1.3.30.2",
:release => "2.el5",
:arch => "x86_64",
:ensure => "1.3.30.2-2.el5",
}
)
expect(installed_packages.last.properties).to eq(
{
:provider => :rpm,
:name => "mysummaryless",
:epoch => "0",
:version => "1.2.3.4",
:release => "5.el4",
:arch => "noarch",
:ensure => "1.2.3.4-5.el4",
}
)
end
end
describe "#install" do
let(:resource) do
Puppet::Type.type(:package).new(
:name => 'myresource',
:ensure => :installed,
:source => '/path/to/package'
)
end
describe "when not already installed" do
it "only includes the '-i' flag" do
Puppet::Util::Execution.expects(:execute).with(["/bin/rpm", ["-i"], '/path/to/package'], execute_options)
provider.install
end
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 "includes 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
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 "includes 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
it "retrieves version string after querying rpm for version from source file" do
resource.expects(:[]).with(:source).returns('source-string')
Puppet::Util::Execution.expects(:execfail).with(["/bin/rpm", "-q", "--qf", nevra_format, "-p", "source-string"], Puppet::Error).returns("myresource 0 1.2.3.4 5.el4 noarch\n")
expect(provider.latest).to eq("1.2.3.4-5.el4")
end
end
describe "#uninstall" do
let(:resource) do
Puppet::Type.type(:package).new(
:name => 'myresource',
:ensure => :installed
)
end
describe "on a modern RPM" do
before(:each) do
- Puppet::Util::Execution.expects(:execute).with(["/bin/rpm", "-q", "myresource", '--nosignature', '--nodigest', "--qf", nevra_format], execute_options).returns("myresource 0 1.2.3.4 5.el4 noarch\n")
+ Puppet::Util::Execution.expects(:execute).with(["/bin/rpm", "-q", "--whatprovides", "myresource", '--nosignature', '--nodigest', "--qf", nevra_format], execute_options).returns("myresource 0 1.2.3.4 5.el4 noarch\n")
end
let(:rpm_version) { "RPM version 4.10.0\n" }
it "includes the architecture in the package name" do
- Puppet::Util::Execution.expects(:execute).with(["/bin/rpm", "-e", 'myresource-1.2.3.4-5.el4.noarch'], execute_options).returns('').at_most_once
+ Puppet::Util::Execution.expects(:execute).with(["/bin/rpm", ["-e"], 'myresource-1.2.3.4-5.el4.noarch'], execute_options).returns('').at_most_once
provider.uninstall
end
end
describe "on an ancient RPM" do
before(:each) do
- Puppet::Util::Execution.expects(:execute).with(["/bin/rpm", "-q", "myresource", '', '', '--qf', nevra_format], execute_options).returns("myresource 0 1.2.3.4 5.el4 noarch\n")
+ Puppet::Util::Execution.expects(:execute).with(["/bin/rpm", "-q", "--whatprovides", "myresource", '', '', '--qf', nevra_format], execute_options).returns("myresource 0 1.2.3.4 5.el4 noarch\n")
end
let(:rpm_version) { "RPM version 3.0.6\n" }
it "excludes the architecture from the package name" do
- Puppet::Util::Execution.expects(:execute).with(["/bin/rpm", "-e", 'myresource-1.2.3.4-5.el4'], execute_options).returns('').at_most_once
+ Puppet::Util::Execution.expects(:execute).with(["/bin/rpm", ["-e"], 'myresource-1.2.3.4-5.el4'], execute_options).returns('').at_most_once
provider.uninstall
end
end
+ describe "when uninstalled with options" do
+ before(:each) do
+ Puppet::Util::Execution.expects(:execute).with(["/bin/rpm", "-q", "--whatprovides", "myresource", '--nosignature', '--nodigest', "--qf", nevra_format], execute_options).returns("myresource 0 1.2.3.4 5.el4 noarch\n")
+ end
+
+ let(:resource) do
+ Puppet::Type.type(:package).new(
+ :name => resource_name,
+ :ensure => :absent,
+ :provider => 'rpm',
+ :uninstall_options => ['--nodeps']
+ )
+ end
+
+ it "includes the options" do
+ Puppet::Util::Execution.expects(:execute).with(["/bin/rpm", ["-e", "--nodeps"], 'myresource-1.2.3.4-5.el4.noarch'], execute_options)
+ provider.uninstall
+ end
+ end
end
describe "parsing" do
def parser_test(rpm_output_string, gold_hash, number_of_debug_logs = 0)
Puppet.expects(:debug).times(number_of_debug_logs)
- Puppet::Util::Execution.expects(:execute).with(["/bin/rpm", "-q", resource_name, "--nosignature", "--nodigest", "--qf", nevra_format], execute_options).returns(rpm_output_string)
+ Puppet::Util::Execution.expects(:execute).with(["/bin/rpm", "-q", "--whatprovides", resource_name, "--nosignature", "--nodigest", "--qf", nevra_format], execute_options).returns(rpm_output_string)
expect(provider.query).to eq(gold_hash)
end
let(:resource_name) { 'name' }
let('delimiter') { ':DESC:' }
let(:package_hash) do
{
:name => 'name',
:epoch => 'epoch',
:version => 'version',
:release => 'release',
:arch => 'arch',
:provider => :rpm,
:ensure => 'version-release',
}
end
let(:line) { 'name epoch version release arch' }
['name', 'epoch', 'version', 'release', 'arch'].each do |field|
it "still parses if #{field} is replaced by delimiter" do
parser_test(
line.gsub(field, delimiter),
package_hash.merge(
field.to_sym => delimiter,
:ensure => 'version-release'.gsub(field, delimiter)
)
)
end
end
it "does not fail if line is unparseable, but issues a debug log" do
parser_test('bad data', {}, 1)
end
it "does not log or fail if rpm returns package not found" do
Puppet.expects(:debug).never
- Puppet::Util::Execution.expects(:execute).with(["/bin/rpm", "-q", resource_name, "--nosignature", "--nodigest", "--qf", nevra_format], execute_options).raises Puppet::ExecutionFailure.new('package not found')
+ Puppet::Util::Execution.expects(:execute).with(["/bin/rpm", "-q", "--whatprovides", resource_name, "--nosignature", "--nodigest", "--qf", nevra_format], execute_options).raises Puppet::ExecutionFailure.new('package not found')
expect(provider.query).to be_nil
end
end
describe "#install_options" do
it "returns empty array by default" do
expect(provider.install_options).to eq([])
end
it "returns install_options when set" do
provider.resource[:install_options] = ['-n']
expect(provider.install_options).to eq(['-n'])
end
it "returns multiple install_options when set" do
provider.resource[:install_options] = ['-L', '/opt/puppet']
expect(provider.install_options).to eq(['-L', '/opt/puppet'])
end
it 'returns install_options when set as hash' do
provider.resource[:install_options] = { '-Darch' => 'vax' }
expect(provider.install_options).to eq(['-Darch=vax'])
end
+
it 'returns install_options when an array with hashes' do
provider.resource[:install_options] = [ '-L', { '-Darch' => 'vax' }]
expect(provider.install_options).to eq(['-L', '-Darch=vax'])
end
end
+ describe "#uninstall_options" do
+ it "returns empty array by default" do
+ expect(provider.uninstall_options).to eq([])
+ end
+
+ it "returns uninstall_options when set" do
+ provider.resource[:uninstall_options] = ['-n']
+ expect(provider.uninstall_options).to eq(['-n'])
+ end
+
+ it "returns multiple uninstall_options when set" do
+ provider.resource[:uninstall_options] = ['-L', '/opt/puppet']
+ expect(provider.uninstall_options).to eq(['-L', '/opt/puppet'])
+ end
+
+ it 'returns uninstall_options when set as hash' do
+ provider.resource[:uninstall_options] = { '-Darch' => 'vax' }
+ expect(provider.uninstall_options).to eq(['-Darch=vax'])
+ end
+ it 'returns uninstall_options when an array with hashes' do
+ provider.resource[:uninstall_options] = [ '-L', { '-Darch' => 'vax' }]
+ expect(provider.uninstall_options).to eq(['-L', '-Darch=vax'])
+ end
+ end
+
describe ".nodigest" do
{ '4.0' => nil,
'4.0.1' => nil,
'4.0.2' => '--nodigest',
'4.0.3' => '--nodigest',
'4.1' => '--nodigest',
'5' => '--nodigest',
}.each do |version, expected|
describe "when current version is #{version}" do
it "returns #{expected.inspect}" do
provider_class.stubs(:current_version).returns(version)
expect(provider_class.nodigest).to eq(expected)
end
end
end
end
describe ".nosignature" do
{ '4.0.3' => nil,
'4.1' => '--nosignature',
'4.1.1' => '--nosignature',
'4.2' => '--nosignature',
'5' => '--nosignature',
}.each do |version, expected|
describe "when current version is #{version}" do
it "returns #{expected.inspect}" do
provider_class.stubs(:current_version).returns(version)
expect(provider_class.nosignature).to eq(expected)
end
end
end
end
end
diff --git a/spec/unit/provider/service/base_spec.rb b/spec/unit/provider/service/base_spec.rb
index 9d6a7fdcc..eb8c65034 100755
--- a/spec/unit/provider/service/base_spec.rb
+++ b/spec/unit/provider/service/base_spec.rb
@@ -1,77 +1,77 @@
#! /usr/bin/env ruby
require 'spec_helper'
require 'rbconfig'
require 'fileutils'
provider_class = Puppet::Type.type(:service).provider(:init)
describe "base service provider" do
include PuppetSpec::Files
let :type do Puppet::Type.type(:service) end
let :provider do type.provider(:base) end
subject { provider }
context "basic operations" do
# Cross-platform file interactions. Fun times.
Ruby = File.join(RbConfig::CONFIG["bindir"],
RbConfig::CONFIG["RUBY_INSTALL_NAME"] +
RbConfig::CONFIG["EXEEXT"])
Start = [Ruby, '-rfileutils', '-e', 'FileUtils.touch(ARGV[0])']
Status = [Ruby, '-e' 'exit File.file?(ARGV[0])']
Stop = [Ruby, '-e', 'File.exist?(ARGV[0]) and File.unlink(ARGV[0])']
let :flag do tmpfile('base-service-test') end
subject do
type.new(:name => "test", :provider => :base,
:start => Start + [flag],
:status => Status + [flag],
:stop => Stop + [flag]
).provider
end
before :each do
- Puppet::FileSystem::File.unlink(flag) if Puppet::FileSystem::File.exist?(flag)
+ Puppet::FileSystem.unlink(flag) if Puppet::FileSystem.exist?(flag)
end
it { should be }
it "should invoke the start command if not running" do
- File.should_not be_file flag
+ File.should_not be_file(flag)
subject.start
- File.should be_file flag
+ File.should be_file(flag)
end
it "should be stopped before being started" do
subject.status.should == :stopped
end
it "should be running after being started" do
subject.start
subject.status.should == :running
end
it "should invoke the stop command when asked" do
subject.start
subject.status.should == :running
subject.stop
subject.status.should == :stopped
- File.should_not be_file flag
+ File.should_not be_file(flag)
end
it "should start again even if already running" do
subject.start
subject.expects(:ucommand).with(:start)
subject.start
end
it "should stop again even if already stopped" do
subject.stop
subject.expects(:ucommand).with(:stop)
subject.stop
end
end
end
diff --git a/spec/unit/provider/service/daemontools_spec.rb b/spec/unit/provider/service/daemontools_spec.rb
index 4c35e0586..57bf632c2 100755
--- a/spec/unit/provider/service/daemontools_spec.rb
+++ b/spec/unit/provider/service/daemontools_spec.rb
@@ -1,176 +1,171 @@
#! /usr/bin/env ruby
#
# Unit testing for the Daemontools service Provider
#
# author Brice Figureau
#
require 'spec_helper'
provider_class = Puppet::Type.type(:service).provider(:daemontools)
describe provider_class do
before(:each) do
# Create a mock resource
@resource = stub 'resource'
@provider = provider_class.new
@servicedir = "/etc/service"
@provider.servicedir=@servicedir
@daemondir = "/var/lib/service"
@provider.class.defpath=@daemondir
# A catch all; no parameters set
@resource.stubs(:[]).returns(nil)
# But set name, source and path (because we won't run
# the thing that will fetch the resource path from the provider)
@resource.stubs(:[]).with(:name).returns "myservice"
@resource.stubs(:[]).with(:ensure).returns :enabled
@resource.stubs(:[]).with(:path).returns @daemondir
@resource.stubs(:ref).returns "Service[myservice]"
@provider.resource = @resource
@provider.stubs(:command).with(:svc).returns "svc"
@provider.stubs(:command).with(:svstat).returns "svstat"
@provider.stubs(:svc)
@provider.stubs(:svstat)
end
it "should have a restart method" do
@provider.should respond_to(:restart)
end
it "should have a start method" do
@provider.should respond_to(:start)
end
it "should have a stop method" do
@provider.should respond_to(:stop)
end
it "should have an enabled? method" do
@provider.should respond_to(:enabled?)
end
it "should have an enable method" do
@provider.should respond_to(:enable)
end
it "should have a disable method" do
@provider.should respond_to(:disable)
end
describe "when starting" do
it "should use 'svc' to start the service" do
@provider.stubs(:enabled?).returns :true
@provider.expects(:svc).with("-u", "/etc/service/myservice")
@provider.start
end
it "should enable the service if it is not enabled" do
@provider.stubs(:svc)
@provider.expects(:enabled?).returns :false
@provider.expects(:enable)
@provider.start
end
end
describe "when stopping" do
it "should use 'svc' to stop the service" do
@provider.stubs(:disable)
@provider.expects(:svc).with("-d", "/etc/service/myservice")
@provider.stop
end
end
describe "when restarting" do
it "should use 'svc' to restart the service" do
@provider.expects(:svc).with("-t", "/etc/service/myservice")
@provider.restart
end
end
describe "when enabling" do
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)
+ Puppet::FileSystem.expects(:symlink?).with(service_path).returns(false)
+ Puppet::FileSystem.expects(:symlink).with(daemon_path, service_path).returns(0)
+
@provider.enable
end
end
describe "when disabling" do
it "should remove the symlink between daemon dir and service dir" do
FileTest.stubs(:directory?).returns(false)
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)
+ Puppet::FileSystem.expects(:symlink?).with(path).returns(true)
+ Puppet::FileSystem.expects(:unlink).with(path)
@provider.stubs(:texecute).returns("")
@provider.disable
end
it "should stop the service" do
FileTest.stubs(:directory?).returns(false)
- mocked_file = mock('anything', :symlink? => true)
- Puppet::FileSystem::File.expects(:new).returns(mocked_file)
- Puppet::FileSystem::File.stubs(:unlink)
+ Puppet::FileSystem.expects(:symlink?).returns(true)
+ Puppet::FileSystem.stubs(:unlink)
@provider.expects(:stop)
@provider.disable
end
end
describe "when checking if the service is enabled?" do
it "should return true if it is running" do
@provider.stubs(:status).returns(:running)
@provider.enabled?.should == :true
end
[true, false].each do |t|
it "should return #{t} if the symlink exists" do
@provider.stubs(:status).returns(:stopped)
path = File.join(@servicedir,"myservice")
- mocked_file = mock(path, :symlink? => t)
- Puppet::FileSystem::File.expects(:new).with(path).returns(mocked_file)
+ Puppet::FileSystem.expects(:symlink?).with(path).returns(t)
@provider.enabled?.should == "#{t}".to_sym
end
end
end
describe "when checking status" do
it "should call the external command 'svstat /etc/service/myservice'" do
@provider.expects(:svstat).with(File.join(@servicedir,"myservice"))
@provider.status
end
end
describe "when checking status" do
it "and svstat fails, properly raise a Puppet::Error" do
@provider.expects(:svstat).with(File.join(@servicedir,"myservice")).raises(Puppet::ExecutionFailure, "failure")
lambda { @provider.status }.should raise_error(Puppet::Error, 'Could not get status for service Service[myservice]: failure')
end
it "and svstat returns up, then return :running" do
@provider.expects(:svstat).with(File.join(@servicedir,"myservice")).returns("/etc/service/myservice: up (pid 454) 954326 seconds")
@provider.status.should == :running
end
it "and svstat returns not running, then return :stopped" do
@provider.expects(:svstat).with(File.join(@servicedir,"myservice")).returns("/etc/service/myservice: supervise not running")
@provider.status.should == :stopped
end
end
end
diff --git a/spec/unit/provider/service/freebsd_spec.rb b/spec/unit/provider/service/freebsd_spec.rb
index 69b923f9a..8ef5fee78 100755
--- a/spec/unit/provider/service/freebsd_spec.rb
+++ b/spec/unit/provider/service/freebsd_spec.rb
@@ -1,75 +1,75 @@
#! /usr/bin/env ruby
require 'spec_helper'
provider_class = Puppet::Type.type(:service).provider(:freebsd)
describe provider_class do
before :each do
@provider = provider_class.new
@provider.stubs(:initscript)
end
it "should correctly parse rcvar for FreeBSD < 7" do
@provider.stubs(:execute).returns <<OUTPUT
# ntpd
$ntpd_enable=YES
OUTPUT
@provider.rcvar.should == ['# ntpd', 'ntpd_enable=YES']
end
it "should correctly parse rcvar for FreeBSD 7 to 8" do
@provider.stubs(:execute).returns <<OUTPUT
# ntpd
ntpd_enable=YES
OUTPUT
@provider.rcvar.should == ['# ntpd', 'ntpd_enable=YES']
end
it "should correctly parse rcvar for FreeBSD >= 8.1" do
@provider.stubs(:execute).returns <<OUTPUT
# ntpd
#
ntpd_enable="YES"
# (default: "")
OUTPUT
@provider.rcvar.should == ['# ntpd', 'ntpd_enable="YES"', '# (default: "")']
end
it "should correctly parse rcvar for DragonFly BSD" do
@provider.stubs(:execute).returns <<OUTPUT
# ntpd
$ntpd=YES
OUTPUT
@provider.rcvar.should == ['# ntpd', 'ntpd=YES']
end
it "should find the right rcvar_value for FreeBSD < 7" do
@provider.stubs(:rcvar).returns(['# ntpd', 'ntpd_enable=YES'])
@provider.rcvar_value.should == "YES"
end
it "should find the right rcvar_value for FreeBSD >= 7" do
@provider.stubs(:rcvar).returns(['# ntpd', 'ntpd_enable="YES"', '# (default: "")'])
@provider.rcvar_value.should == "YES"
end
it "should find the right rcvar_name" do
@provider.stubs(:rcvar).returns(['# ntpd', 'ntpd_enable="YES"'])
@provider.rcvar_name.should == "ntpd"
end
it "should enable only the selected service" do
- Puppet::FileSystem::File.stubs(:exist?).with('/etc/rc.conf').returns(true)
+ Puppet::FileSystem.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")
- 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)
+ Puppet::FileSystem.stubs(:exist?).with('/etc/rc.conf.local').returns(false)
+ Puppet::FileSystem.stubs(:exist?).with('/etc/rc.conf.d/ntpd').returns(false)
@provider.rc_replace('ntpd', 'ntpd', 'YES')
end
end
diff --git a/spec/unit/provider/service/gentoo_spec.rb b/spec/unit/provider/service/gentoo_spec.rb
index aa802430e..2bca67937 100755
--- a/spec/unit/provider/service/gentoo_spec.rb
+++ b/spec/unit/provider/service/gentoo_spec.rb
@@ -1,241 +1,241 @@
#! /usr/bin/env ruby
require 'spec_helper'
describe Puppet::Type.type(:service).provider(:gentoo) do
before :each do
Puppet::Type.type(:service).stubs(:defaultprovider).returns described_class
FileTest.stubs(:file?).with('/sbin/rc-update').returns true
FileTest.stubs(:executable?).with('/sbin/rc-update').returns true
Facter.stubs(:value).with(:operatingsystem).returns 'Gentoo'
# 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.
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
+# stub_file = stub(sshd_path, :stat => stub('stat'))
+ Puppet::FileSystem.stubs(:stat).with(sshd_path).returns stub('stat')
end
let :initscripts do
[
'alsasound',
'bootmisc',
'functions.sh',
'hwclock',
'reboot.sh',
'rsyncd',
'shutdown.sh',
'sshd',
'vixie-cron',
'wpa_supplicant',
'xdm-setup'
]
end
let :helperscripts do
[
'functions.sh',
'reboot.sh',
'shutdown.sh'
]
end
describe ".instances" do
it "should have an instances method" do
- described_class.should respond_to :instances
+ described_class.should respond_to(:instances)
end
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
Dir.expects(:entries).with('/etc/init.d').returns initscripts
(initscripts - helperscripts).each do |script|
FileTest.expects(:executable?).with("/etc/init.d/#{script}").returns true
end
helperscripts.each do |script|
FileTest.expects(:executable?).with("/etc/init.d/#{script}").never
end
- Puppet::FileSystem::File.stubs(:new).returns stub('file', :symlink? => false)
+ Puppet::FileSystem.stubs(:symlink?).returns false # stub('file', :symlink? => false)
described_class.instances.map(&:name).should == [
'alsasound',
'bootmisc',
'hwclock',
'rsyncd',
'sshd',
'vixie-cron',
'wpa_supplicant',
'xdm-setup'
]
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.expects(:execute).with(['/bin/foo'], :failonfail => true, :override_locale => false, :squelch => false, :combine => true)
provider.start
end
it "should start the service with <initscript> start otherwise" do
provider = described_class.new(Puppet::Type.type(:service).new(:name => 'sshd'))
- provider.expects(:execute).with(['/etc/init.d/sshd',:start], :failonfail => true, :override_locale => false, :squelch => true)
+ provider.expects(:execute).with(['/etc/init.d/sshd',:start], :failonfail => true, :override_locale => false, :squelch => false, :combine => true)
provider.expects(:search).with('sshd').returns('/etc/init.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.expects(:execute).with(['/bin/foo'], :failonfail => true, :override_locale => false, :squelch => false, :combine => true)
provider.stop
end
it "should stop the service with <initscript> stop otherwise" do
provider = described_class.new(Puppet::Type.type(:service).new(:name => 'sshd'))
- provider.expects(:execute).with(['/etc/init.d/sshd',:stop], :failonfail => true, :override_locale => false, :squelch => true)
+ provider.expects(:execute).with(['/etc/init.d/sshd',:stop], :failonfail => true, :override_locale => false, :squelch => false, :combine => true)
provider.expects(:search).with('sshd').returns('/etc/init.d/sshd')
provider.stop
end
end
describe "#enabled?" do
before :each do
described_class.any_instance.stubs(:update).with(:show).returns File.read(my_fixture('rc_update_show'))
end
it "should run rc-update show to get a list of enabled services" do
provider = described_class.new(Puppet::Type.type(:service).new(:name => 'sshd'))
provider.expects(:update).with(:show).returns "\n"
provider.enabled?
end
['hostname', 'net.lo', 'procfs'].each do |service|
it "should consider service #{service} in runlevel boot as enabled" do
provider = described_class.new(Puppet::Type.type(:service).new(:name => service))
provider.enabled?.should == :true
end
end
['alsasound', 'xdm', 'netmount'].each do |service|
it "should consider service #{service} in runlevel default as enabled" do
provider = described_class.new(Puppet::Type.type(:service).new(:name => service))
provider.enabled?.should == :true
end
end
['rsyncd', 'lighttpd', 'mysql'].each do |service|
it "should consider unused service #{service} as disabled" do
provider = described_class.new(Puppet::Type.type(:service).new(:name => service))
provider.enabled?.should == :false
end
end
end
describe "#enable" do
it "should run rc-update add to enable a service" do
provider = described_class.new(Puppet::Type.type(:service).new(:name => 'sshd'))
provider.expects(:update).with(:add, 'sshd', :default)
provider.enable
end
end
describe "#disable" do
it "should run rc-update del to disable a service" do
provider = described_class.new(Puppet::Type.type(:service).new(:name => 'sshd'))
provider.expects(:update).with(:del, 'sshd', :default)
provider.disable
end
end
describe "#status" do
describe "when a special status command is specified" 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/init.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.expects(:execute).with(['/etc/init.d/sshd',:status], :failonfail => false, :override_locale => false, :squelch => false, :combine => true).never
+ provider.expects(:execute).with(['/bin/foo'], :failonfail => false, :override_locale => false, :squelch => false, :combine => true)
provider.status
end
it "should return :stopped when the 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/init.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.expects(:execute).with(['/etc/init.d/sshd',:status], :failonfail => false, :override_locale => false, :squelch => false, :combine => true).never
+ provider.expects(:execute).with(['/bin/foo'], :failonfail => false, :override_locale => false, :squelch => false, :combine => true)
$CHILD_STATUS.stubs(:exitstatus).returns 3
provider.status.should == :stopped
end
it "should return :running when the 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/init.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.expects(:execute).with(['/etc/init.d/sshd',:status], :failonfail => false, :override_locale => false, :squelch => false, :combine => true).never
+ provider.expects(:execute).with(['/bin/foo'], :failonfail => false, :override_locale => false, :squelch => false, :combine => true)
$CHILD_STATUS.stubs(:exitstatus).returns 0
provider.status.should == :running
end
end
describe "when hasstatus is false" do
it "should return running if a pid can be found" do
provider = described_class.new(Puppet::Type.type(:service).new(:name => 'sshd', :hasstatus => false))
- provider.expects(:execute).with(['/etc/init.d/sshd',:status], :failonfail => false, :override_locale => false, :squelch => true).never
+ provider.expects(:execute).with(['/etc/init.d/sshd',:status], :failonfail => false, :override_locale => false, :squelch => false, :combine => true).never
provider.expects(:getpid).returns 1000
provider.status.should == :running
end
it "should return stopped if no pid can be found" do
provider = described_class.new(Puppet::Type.type(:service).new(:name => 'sshd', :hasstatus => false))
- provider.expects(:execute).with(['/etc/init.d/sshd',:status], :failonfail => false, :override_locale => false, :squelch => true).never
+ provider.expects(:execute).with(['/etc/init.d/sshd',:status], :failonfail => false, :override_locale => false, :squelch => false, :combine => true).never
provider.expects(:getpid).returns nil
provider.status.should == :stopped
end
end
describe "when hasstatus is true" do
it "should return running if <initscript> status exits with a zero exitcode" do
provider = described_class.new(Puppet::Type.type(:service).new(:name => 'sshd', :hasstatus => true))
provider.expects(:search).with('sshd').returns('/etc/init.d/sshd')
- provider.expects(:execute).with(['/etc/init.d/sshd',:status], :failonfail => false, :override_locale => false, :squelch => true)
+ provider.expects(:execute).with(['/etc/init.d/sshd',:status], :failonfail => false, :override_locale => false, :squelch => false, :combine => true)
$CHILD_STATUS.stubs(:exitstatus).returns 0
provider.status.should == :running
end
it "should return stopped if <initscript> status exits with a non-zero exitcode" do
provider = described_class.new(Puppet::Type.type(:service).new(:name => 'sshd', :hasstatus => true))
provider.expects(:search).with('sshd').returns('/etc/init.d/sshd')
- provider.expects(:execute).with(['/etc/init.d/sshd',:status], :failonfail => false, :override_locale => false, :squelch => true)
+ provider.expects(:execute).with(['/etc/init.d/sshd',:status], :failonfail => false, :override_locale => false, :squelch => false, :combine => true)
$CHILD_STATUS.stubs(:exitstatus).returns 3
provider.status.should == :stopped
end
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/init.d/sshd',:restart], :failonfail => true, :override_locale => false, :squelch => true).never
- provider.expects(:execute).with(['/bin/foo'], :failonfail => true, :override_locale => false, :squelch => true)
+ provider.expects(:execute).with(['/etc/init.d/sshd',:restart], :failonfail => true, :override_locale => false, :squelch => false, :combine => true).never
+ provider.expects(:execute).with(['/bin/foo'], :failonfail => true, :override_locale => false, :squelch => false, :combine => true)
provider.restart
end
it "should restart the service with <initscript> restart if hasrestart is true" do
provider = described_class.new(Puppet::Type.type(:service).new(:name => 'sshd', :hasrestart => true))
provider.expects(:search).with('sshd').returns('/etc/init.d/sshd')
- provider.expects(:execute).with(['/etc/init.d/sshd',:restart], :failonfail => true, :override_locale => false, :squelch => true)
+ provider.expects(:execute).with(['/etc/init.d/sshd',:restart], :failonfail => true, :override_locale => false, :squelch => false, :combine => true)
provider.restart
end
it "should restart the service with <initscript> stop/start if hasrestart is false" do
provider = described_class.new(Puppet::Type.type(:service).new(:name => 'sshd', :hasrestart => false))
provider.expects(:search).with('sshd').returns('/etc/init.d/sshd')
- provider.expects(:execute).with(['/etc/init.d/sshd',:restart], :failonfail => true, :override_locale => false, :squelch => true).never
- provider.expects(:execute).with(['/etc/init.d/sshd',:stop], :failonfail => true, :override_locale => false, :squelch => true)
- provider.expects(:execute).with(['/etc/init.d/sshd',:start], :failonfail => true, :override_locale => false, :squelch => true)
+ provider.expects(:execute).with(['/etc/init.d/sshd',:restart], :failonfail => true, :override_locale => false, :squelch => false, :combine => true).never
+ provider.expects(:execute).with(['/etc/init.d/sshd',:stop], :failonfail => true, :override_locale => false, :squelch => false, :combine => true)
+ provider.expects(:execute).with(['/etc/init.d/sshd',:start], :failonfail => true, :override_locale => false, :squelch => false, :combine => true)
provider.restart
end
end
end
diff --git a/spec/unit/provider/service/init_spec.rb b/spec/unit/provider/service/init_spec.rb
index 896f13ac1..7890ba9ab 100755
--- a/spec/unit/provider/service/init_spec.rb
+++ b/spec/unit/provider/service/init_spec.rb
@@ -1,212 +1,212 @@
#! /usr/bin/env ruby
#
# Unit testing for the Init service Provider
#
require 'spec_helper'
describe Puppet::Type.type(:service).provider(:init) do
let :provider do
provider = described_class.new(:name => 'myservice')
provider.resource = resource
provider
end
let :resource do
Puppet::Type.type(:service).new(
:name => 'myservice',
:ensure => :running,
:path => paths
)
end
let :paths do
["/service/path","/alt/service/path"]
end
let :excludes do
# Taken from redhat, gentoo, and debian
%w{functions.sh reboot.sh shutdown.sh functions halt killall single linuxconf reboot boot wait-for-state rcS module-init-tools}
end
describe "when getting all service instances" do
before :each do
described_class.stubs(:defpath).returns('tmp')
@services = ['one', 'two', 'three', 'four']
Dir.stubs(:entries).with('tmp').returns @services
FileTest.expects(:directory?).with('tmp').returns(true)
FileTest.stubs(:executable?).returns(true)
end
it "should return instances for all services" do
described_class.instances.map(&:name).should == @services
end
it "should omit an array of services from exclude list" do
exclude = ['two', 'four']
described_class.get_services(described_class.defpath, exclude).map(&:name).should == (@services - exclude)
end
it "should omit a single service from the exclude list" do
exclude = 'two'
described_class.get_services(described_class.defpath, exclude).map(&:name).should == @services - [exclude]
end
it "should use defpath" do
described_class.instances.should be_all { |provider| provider.get(:path) == described_class.defpath }
end
it "should set hasstatus to true for providers" do
described_class.instances.should be_all { |provider| provider.get(:hasstatus) == true }
end
it "should discard upstart jobs", :if => Puppet.features.manages_symlinks? do
not_init_service, *valid_services = @services
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)
+ Puppet::FileSystem.expects(:symlink?).at_least_once.returns false
+ Puppet::FileSystem.expects(:symlink?).with(Puppet::FileSystem.pathname(path)).returns(true)
+ Puppet::FileSystem.expects(:readlink).with(Puppet::FileSystem.pathname(path)).returns("/lib/init/upstart-job")
described_class.instances.map(&:name).should == valid_services
end
it "should discard non-initscript scripts" do
valid_services = @services
all_services = valid_services + excludes
Dir.expects(:entries).with('tmp').returns all_services
described_class.instances.map(&:name).should =~ valid_services
end
end
describe "when checking valid paths" do
it "should discard paths that do not exist" do
File.expects(:directory?).with(paths[0]).returns false
- Puppet::FileSystem::File.expects(:exist?).with(paths[0]).returns false
+ Puppet::FileSystem.expects(:exist?).with(paths[0]).returns false
File.expects(:directory?).with(paths[1]).returns true
provider.paths.should == [paths[1]]
end
it "should discard paths that are not directories" do
paths.each do |path|
- Puppet::FileSystem::File.expects(:exist?).with(path).returns true
+ Puppet::FileSystem.expects(:exist?).with(path).returns true
File.expects(:directory?).with(path).returns false
end
provider.paths.should be_empty
end
end
describe "when searching for the init script" do
before :each do
paths.each {|path| File.expects(:directory?).with(path).returns true }
end
it "should be able to find the init script in the service path" do
- Puppet::FileSystem::File.expects(:exist?).with("#{paths[0]}/myservice").returns true
- Puppet::FileSystem::File.expects(:exist?).with("#{paths[1]}/myservice").never # first one wins
+ Puppet::FileSystem.expects(:exist?).with("#{paths[0]}/myservice").returns true
+ Puppet::FileSystem.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
- Puppet::FileSystem::File.expects(:exist?).with("#{paths[0]}/myservice").returns false
- Puppet::FileSystem::File.expects(:exist?).with("#{paths[1]}/myservice").returns true
+ Puppet::FileSystem.expects(:exist?).with("#{paths[0]}/myservice").returns false
+ Puppet::FileSystem.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
- 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
+ Puppet::FileSystem.expects(:exist?).with("#{paths[0]}/myservice").returns false
+ Puppet::FileSystem.expects(:exist?).with("#{paths[1]}/myservice").returns false
+ Puppet::FileSystem.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|
- Puppet::FileSystem::File.expects(:exist?).with("#{path}/myservice").returns false
- Puppet::FileSystem::File.expects(:exist?).with("#{path}/myservice.sh").returns false
+ Puppet::FileSystem.expects(:exist?).with("#{path}/myservice").returns false
+ Puppet::FileSystem.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
end
describe "if the init script is present" do
before :each do
File.stubs(:directory?).with("/service/path").returns true
File.stubs(:directory?).with("/alt/service/path").returns true
- Puppet::FileSystem::File.stubs(:exist?).with("/service/path/myservice").returns true
+ Puppet::FileSystem.stubs(:exist?).with("/service/path/myservice").returns true
end
[:start, :stop, :status, :restart].each do |method|
it "should have a #{method} method" do
provider.should respond_to(method)
end
describe "when running #{method}" do
it "should use any provided explicit command" do
resource[method] = "/user/specified/command"
provider.expects(:execute).with { |command, *args| command == ["/user/specified/command"] }
provider.send(method)
end
it "should pass #{method} to the init script when no explicit command is provided" do
resource[:hasrestart] = :true
resource[:hasstatus] = :true
provider.expects(:execute).with { |command, *args| command == ["/service/path/myservice",method]}
provider.send(method)
end
end
end
describe "when checking status" do
describe "when hasstatus is :true" do
before :each do
resource[:hasstatus] = :true
end
it "should execute the command" do
provider.expects(:texecute).with(:status, ['/service/path/myservice', :status], false).returns("")
provider.status
end
it "should consider the process running if the command returns 0" do
provider.expects(:texecute).with(:status, ['/service/path/myservice', :status], false).returns("")
$CHILD_STATUS.stubs(:exitstatus).returns(0)
provider.status.should == :running
end
[-10,-1,1,10].each { |ec|
it "should consider the process stopped if the command returns something non-0" do
provider.expects(:texecute).with(:status, ['/service/path/myservice', :status], false).returns("")
$CHILD_STATUS.stubs(:exitstatus).returns(ec)
provider.status.should == :stopped
end
}
end
describe "when hasstatus is not :true" do
before :each do
resource[:hasstatus] = :false
end
it "should consider the service :running if it has a pid" do
provider.expects(:getpid).returns "1234"
provider.status.should == :running
end
it "should consider the service :stopped if it doesn't have a pid" do
provider.expects(:getpid).returns nil
provider.status.should == :stopped
end
end
end
describe "when restarting and hasrestart is not :true" do
before :each do
resource[:hasrestart] = :false
end
it "should stop and restart the process" do
provider.expects(:texecute).with(:stop, ['/service/path/myservice', :stop ], true).returns("")
provider.expects(:texecute).with(:start,['/service/path/myservice', :start], true).returns("")
$CHILD_STATUS.stubs(:exitstatus).returns(0)
provider.restart
end
end
end
end
diff --git a/spec/unit/provider/service/openbsd_spec.rb b/spec/unit/provider/service/openbsd_spec.rb
index 8c417986d..5bed8862a 100644
--- a/spec/unit/provider/service/openbsd_spec.rb
+++ b/spec/unit/provider/service/openbsd_spec.rb
@@ -1,125 +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.expects(:execute).with(['/bin/foo'], :failonfail => true, :override_locale => false, :squelch => false, :combine => 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(:execute).with(['/etc/rc.d/sshd', '-f', :start], :failonfail => true, :override_locale => false, :squelch => false, :combine => 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.expects(:execute).with(['/bin/foo'], :failonfail => true, :override_locale => false, :squelch => false, :combine => 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(:execute).with(['/etc/rc.d/sshd', :stop], :failonfail => true, :override_locale => false, :squelch => false, :combine => 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.expects(:execute).with(['/etc/rc.d/sshd', :status], :failonfail => false, :override_locale => false, :squelch => false, :combine => true).never
+ provider.expects(:execute).with(['/bin/foo'], :failonfail => false, :override_locale => false, :squelch => false, :combine => 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)
+ provider.expects(:execute).with(['/etc/rc.d/sshd', :status], :failonfail => false, :override_locale => false, :squelch => false, :combine => true).never
+ provider.expects(:execute).with(['/bin/foo'], :failonfail => false, :override_locale => false, :squelch => false, :combine => 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)
+ provider.expects(:execute).with(['/etc/rc.d/sshd', :status], :failonfail => false, :override_locale => false, :squelch => false, :combine => true).never
+ provider.expects(:execute).with(['/bin/foo'], :failonfail => false, :override_locale => false, :squelch => false, :combine => 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.expects(:execute).with(['/etc/rc.d/sshd', '-f', :restart], :failonfail => true, :override_locale => false, :squelch => false, :combine => true).never
+ provider.expects(:execute).with(['/bin/foo'], :failonfail => true, :override_locale => false, :squelch => false, :combine => 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(:execute).with(['/etc/rc.d/sshd', '-f', :restart], :failonfail => true, :override_locale => false, :squelch => false, :combine => 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(:execute).with(['/etc/rc.d/sshd', '-f', :restart], :failonfail => true, :override_locale => false, :squelch => false, :combine => true).never
+ provider.expects(:execute).with(['/etc/rc.d/sshd', :stop], :failonfail => true, :override_locale => false, :squelch => false, :combine => true)
+ provider.expects(:execute).with(['/etc/rc.d/sshd', '-f', :start], :failonfail => true, :override_locale => false, :squelch => false, :combine => true)
provider.expects(:search).with('sshd').returns('/etc/rc.d/sshd')
provider.restart
end
end
end
diff --git a/spec/unit/provider/service/openrc_spec.rb b/spec/unit/provider/service/openrc_spec.rb
index 1949d3328..038340ed6 100755
--- a/spec/unit/provider/service/openrc_spec.rb
+++ b/spec/unit/provider/service/openrc_spec.rb
@@ -1,225 +1,225 @@
#! /usr/bin/env ruby
require 'spec_helper'
describe Puppet::Type.type(:service).provider(:openrc) do
before :each do
Puppet::Type.type(:service).stubs(:defaultprovider).returns described_class
['/sbin/rc-service', '/bin/rc-status', '/sbin/rc-update'].each do |command|
# Puppet::Util is both mixed in to providers and is also invoked directly
# by Puppet::Provider::CommandDefiner, so we have to stub both out.
described_class.stubs(:which).with(command).returns(command)
Puppet::Util.stubs(:which).with(command).returns(command)
end
end
describe ".instances" do
it "should have an instances method" do
described_class.should respond_to :instances
end
it "should get a list of services from rc-service --list" do
described_class.expects(:rcservice).with('-C','--list').returns File.read(my_fixture('rcservice_list'))
described_class.instances.map(&:name).should == [
'alsasound',
'consolefont',
'lvm-monitoring',
'pydoc-2.7',
'pydoc-3.2',
'wpa_supplicant',
'xdm',
'xdm-setup'
]
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.expects(:execute).with(['/bin/foo'], :failonfail => true, :override_locale => false, :squelch => false, :combine => true)
provider.start
end
it "should start the service with rc-service start otherwise" do
provider = described_class.new(Puppet::Type.type(:service).new(:name => 'sshd'))
- provider.expects(:execute).with(['/sbin/rc-service','sshd',:start], :failonfail => true, :override_locale => false, :squelch => true)
+ provider.expects(:execute).with(['/sbin/rc-service','sshd',:start], :failonfail => true, :override_locale => false, :squelch => false, :combine => true)
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.expects(:execute).with(['/bin/foo'], :failonfail => true, :override_locale => false, :squelch => false, :combine => true)
provider.stop
end
it "should stop the service with rc-service stop otherwise" do
provider = described_class.new(Puppet::Type.type(:service).new(:name => 'sshd'))
- provider.expects(:execute).with(['/sbin/rc-service','sshd',:stop], :failonfail => true, :override_locale => false, :squelch => true)
+ provider.expects(:execute).with(['/sbin/rc-service','sshd',:stop], :failonfail => true, :override_locale => false, :squelch => false, :combine => true)
provider.stop
end
end
describe 'when invoking `rc-status`' do
subject { described_class.new(Puppet::Type.type(:service).new(:name => 'urandom')) }
it "clears the RC_SVCNAME environment variable" do
Puppet::Util.withenv(:RC_SVCNAME => 'puppet') do
Puppet::Util::Execution.expects(:execute).with(
includes('/bin/rc-status'),
has_entry(:custom_environment, {:RC_SVCNAME => nil})
).returns ''
subject.enabled?
end
end
end
describe "#enabled?" do
before :each do
described_class.any_instance.stubs(:rcstatus).with('-C','-a').returns File.read(my_fixture('rcstatus'))
end
it "should run rc-status to get a list of enabled services" do
provider = described_class.new(Puppet::Type.type(:service).new(:name => 'sshd'))
provider.expects(:rcstatus).with('-C','-a').returns "\n"
provider.enabled?
end
['hwclock', 'modules', 'urandom'].each do |service|
it "should consider service #{service} in runlevel boot as enabled" do
provider = described_class.new(Puppet::Type.type(:service).new(:name => service))
provider.enabled?.should == :true
end
end
['netmount', 'xdm', 'local', 'foo_with_very_very_long_servicename_no_still_not_the_end_wait_for_it_almost_there_almost_there_now_finally_the_end'].each do |service|
it "should consider service #{service} in runlevel default as enabled" do
provider = described_class.new(Puppet::Type.type(:service).new(:name => service))
provider.enabled?.should == :true
end
end
['net.eth0', 'pcscd'].each do |service|
it "should consider service #{service} in dynamic runlevel: hotplugged as disabled" do
provider = described_class.new(Puppet::Type.type(:service).new(:name => service))
provider.enabled?.should == :false
end
end
['sysfs', 'udev-mount'].each do |service|
it "should consider service #{service} in dynamic runlevel: needed as disabled" do
provider = described_class.new(Puppet::Type.type(:service).new(:name => service))
provider.enabled?.should == :false
end
end
['sshd'].each do |service|
it "should consider service #{service} in dynamic runlevel: manual as disabled" do
provider = described_class.new(Puppet::Type.type(:service).new(:name => service))
provider.enabled?.should == :false
end
end
end
describe "#enable" do
it "should run rc-update add to enable a service" do
provider = described_class.new(Puppet::Type.type(:service).new(:name => 'sshd'))
provider.expects(:rcupdate).with('-C', :add, 'sshd')
provider.enable
end
end
describe "#disable" do
it "should run rc-update del to disable a service" do
provider = described_class.new(Puppet::Type.type(:service).new(:name => 'sshd'))
provider.expects(:rcupdate).with('-C', :del, 'sshd')
provider.disable
end
end
describe "#status" do
describe "when a special status command if specified" 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(['/sbin/rc-service','sshd',:status], :failonfail => false, :override_locale => false, :squelch => true).never
- provider.expects(:execute).with(['/bin/foo'], :failonfail => false, :override_locale => false, :squelch => true)
+ provider.expects(:execute).with(['/sbin/rc-service','sshd',:status], :failonfail => false, :override_locale => false, :squelch => false, :combine => true).never
+ provider.expects(:execute).with(['/bin/foo'], :failonfail => false, :override_locale => false, :squelch => false, :combine => 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(['/sbin/rc-service','sshd',:status], :failonfail => false, :override_locale => false, :squelch => true).never
- provider.expects(:execute).with(['/bin/foo'], :failonfail => false, :override_locale => false, :squelch => true)
+ provider.expects(:execute).with(['/sbin/rc-service','sshd',:status], :failonfail => false, :override_locale => false, :squelch => false, :combine => true).never
+ provider.expects(:execute).with(['/bin/foo'], :failonfail => false, :override_locale => false, :squelch => false, :combine => 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(['/sbin/rc-service','sshd',:status], :failonfail => false, :override_locale => false, :squelch => true).never
- provider.expects(:execute).with(['/bin/foo'], :failonfail => false, :override_locale => false, :squelch => true)
+ provider.expects(:execute).with(['/sbin/rc-service','sshd',:status], :failonfail => false, :override_locale => false, :squelch => false, :combine => true).never
+ provider.expects(:execute).with(['/bin/foo'], :failonfail => false, :override_locale => false, :squelch => false, :combine => true)
$CHILD_STATUS.stubs(:exitstatus).returns 0
provider.status.should == :running
end
end
describe "when hasstatus is false" do
it "should return running if a pid can be found" do
provider = described_class.new(Puppet::Type.type(:service).new(:name => 'sshd', :hasstatus => false))
- provider.expects(:execute).with(['/sbin/rc-service','sshd',:status], :failonfail => false, :override_locale => false, :squelch => true).never
+ provider.expects(:execute).with(['/sbin/rc-service','sshd',:status], :failonfail => false, :override_locale => false, :squelch => false, :combine => true).never
provider.expects(:getpid).returns 1000
provider.status.should == :running
end
it "should return stopped if no pid can be found" do
provider = described_class.new(Puppet::Type.type(:service).new(:name => 'sshd', :hasstatus => false))
- provider.expects(:execute).with(['/sbin/rc-service','sshd',:status], :failonfail => false, :override_locale => false, :squelch => true).never
+ provider.expects(:execute).with(['/sbin/rc-service','sshd',:status], :failonfail => false, :override_locale => false, :squelch => false, :combine => true).never
provider.expects(:getpid).returns nil
provider.status.should == :stopped
end
end
describe "when hasstatus is true" do
it "should return running if rc-service status exits with a zero exitcode" do
provider = described_class.new(Puppet::Type.type(:service).new(:name => 'sshd', :hasstatus => true))
- provider.expects(:execute).with(['/sbin/rc-service','sshd',:status], :failonfail => false, :override_locale => false, :squelch => true)
+ provider.expects(:execute).with(['/sbin/rc-service','sshd',:status], :failonfail => false, :override_locale => false, :squelch => false, :combine => true)
$CHILD_STATUS.stubs(:exitstatus).returns 0
provider.status.should == :running
end
it "should return stopped if rc-service status exits with a non-zero exitcode" do
provider = described_class.new(Puppet::Type.type(:service).new(:name => 'sshd', :hasstatus => true))
- provider.expects(:execute).with(['/sbin/rc-service','sshd',:status], :failonfail => false, :override_locale => false, :squelch => true)
+ provider.expects(:execute).with(['/sbin/rc-service','sshd',:status], :failonfail => false, :override_locale => false, :squelch => false, :combine => true)
$CHILD_STATUS.stubs(:exitstatus).returns 3
provider.status.should == :stopped
end
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(['/sbin/rc-service','sshd',:restart], :failonfail => true, :override_locale => false, :squelch => true).never
- provider.expects(:execute).with(['/bin/foo'], :failonfail => true, :override_locale => false, :squelch => true)
+ provider.expects(:execute).with(['/sbin/rc-service','sshd',:restart], :failonfail => true, :override_locale => false, :squelch => false, :combine => true).never
+ provider.expects(:execute).with(['/bin/foo'], :failonfail => true, :override_locale => false, :squelch => false, :combine => 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(['/sbin/rc-service','sshd',:restart], :failonfail => true, :override_locale => false, :squelch => true)
+ provider.expects(:execute).with(['/sbin/rc-service','sshd',:restart], :failonfail => true, :override_locale => false, :squelch => false, :combine => true)
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(['/sbin/rc-service','sshd',:restart], :failonfail => true, :override_locale => false, :squelch => true).never
- provider.expects(:execute).with(['/sbin/rc-service','sshd',:stop], :failonfail => true, :override_locale => false, :squelch => true)
- provider.expects(:execute).with(['/sbin/rc-service','sshd',:start], :failonfail => true, :override_locale => false, :squelch => true)
+ provider.expects(:execute).with(['/sbin/rc-service','sshd',:restart], :failonfail => true, :override_locale => false, :squelch => false, :combine => true).never
+ provider.expects(:execute).with(['/sbin/rc-service','sshd',:stop], :failonfail => true, :override_locale => false, :squelch => false, :combine => true)
+ provider.expects(:execute).with(['/sbin/rc-service','sshd',:start], :failonfail => true, :override_locale => false, :squelch => false, :combine => true)
provider.restart
end
end
end
diff --git a/spec/unit/provider/service/openwrt_spec.rb b/spec/unit/provider/service/openwrt_spec.rb
index 2113aceb4..8d385d175 100755
--- a/spec/unit/provider/service/openwrt_spec.rb
+++ b/spec/unit/provider/service/openwrt_spec.rb
@@ -1,109 +1,109 @@
#! /usr/bin/env ruby
#
# Unit testing for the OpenWrt service Provider
#
require 'spec_helper'
describe Puppet::Type.type(:service).provider(:openwrt), :as_platform => :posix do
let(:resource) do
resource = stub 'resource'
resource.stubs(:[]).returns(nil)
resource.stubs(:[]).with(:name).returns "myservice"
resource.stubs(:[]).with(:path).returns ["/etc/init.d"]
resource
end
let(:provider) do
provider = described_class.new
provider.stubs(:get).with(:hasstatus).returns false
provider
end
before :each do
resource.stubs(:provider).returns provider
provider.resource = resource
FileTest.stubs(:file?).with('/etc/rc.common').returns true
FileTest.stubs(:executable?).with('/etc/rc.common').returns true
# All OpenWrt tests operate on the init script directly. It must exist.
File.stubs(:directory?).with('/etc/init.d').returns true
- Puppet::FileSystem::File.stubs(:exist?).with('/etc/init.d/myservice').returns true
+ Puppet::FileSystem.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
operatingsystem = 'openwrt'
it "should be the default provider on #{operatingsystem}" do
Facter.expects(:value).with(:operatingsystem).returns(operatingsystem)
described_class.default?.should be_true
end
# test self.instances
describe "when getting all service instances" do
let(:services) {['dnsmasq', 'dropbear', 'firewall', 'led', 'puppet', 'uhttpd' ]}
before :each do
Dir.stubs(:entries).returns services
FileTest.stubs(:directory?).returns(true)
FileTest.stubs(:executable?).returns(true)
end
it "should return instances for all services" do
services.each do |inst|
described_class.expects(:new).with{|hash| hash[:name] == inst && hash[:path] == '/etc/init.d'}.returns("#{inst}_instance")
end
results = services.collect {|x| "#{x}_instance"}
described_class.instances.should == results
end
end
it "should have an enabled? method" do
provider.should respond_to(:enabled?)
end
it "should have an enable method" do
provider.should respond_to(:enable)
end
it "should have a disable method" do
provider.should respond_to(:disable)
end
[:start, :stop, :restart].each do |method|
it "should have a #{method} method" do
provider.should respond_to(method)
end
describe "when running #{method}" do
it "should use any provided explicit command" do
resource.stubs(:[]).with(method).returns "/user/specified/command"
provider.expects(:execute).with { |command, *args| command == ["/user/specified/command"] }
provider.send(method)
end
it "should execute the init script with #{method} when no explicit command is provided" do
resource.stubs(:[]).with("has#{method}".intern).returns :true
provider.expects(:execute).with { |command, *args| command == ['/etc/init.d/myservice', method ]}
provider.send(method)
end
end
end
describe "when checking status" do
it "should consider the service :running if it has a pid" do
provider.expects(:getpid).returns "1234"
provider.status.should == :running
end
it "should consider the service :stopped if it doesn't have a pid" do
provider.expects(:getpid).returns nil
provider.status.should == :stopped
end
end
end
diff --git a/spec/unit/provider/service/runit_spec.rb b/spec/unit/provider/service/runit_spec.rb
index cbdc19ba6..58701309f 100755
--- a/spec/unit/provider/service/runit_spec.rb
+++ b/spec/unit/provider/service/runit_spec.rb
@@ -1,148 +1,145 @@
#! /usr/bin/env ruby
#
# Unit testing for the Runit service Provider
#
# author Brice Figureau
#
require 'spec_helper'
provider_class = Puppet::Type.type(:service).provider(:runit)
describe provider_class do
before(:each) do
# Create a mock resource
@resource = stub 'resource'
@provider = provider_class.new
@servicedir = "/etc/service"
@provider.servicedir=@servicedir
@daemondir = "/etc/sv"
@provider.class.defpath=@daemondir
# A catch all; no parameters set
@resource.stubs(:[]).returns(nil)
# But set name, source and path (because we won't run
# the thing that will fetch the resource path from the provider)
@resource.stubs(:[]).with(:name).returns "myservice"
@resource.stubs(:[]).with(:ensure).returns :enabled
@resource.stubs(:[]).with(:path).returns @daemondir
@resource.stubs(:ref).returns "Service[myservice]"
@provider.stubs(:sv)
@provider.stubs(:resource).returns @resource
end
it "should have a restart method" do
@provider.should respond_to(:restart)
end
it "should have a restartcmd method" do
@provider.should respond_to(:restartcmd)
end
it "should have a start method" do
@provider.should respond_to(:start)
end
it "should have a stop method" do
@provider.should respond_to(:stop)
end
it "should have an enabled? method" do
@provider.should respond_to(:enabled?)
end
it "should have an enable method" do
@provider.should respond_to(:enable)
end
it "should have a disable method" do
@provider.should respond_to(:disable)
end
describe "when starting" do
it "should enable the service if it is not enabled" do
@provider.stubs(:sv)
@provider.expects(:enabled?).returns :false
@provider.expects(:enable)
@provider.stubs(:sleep)
@provider.start
end
it "should execute external command 'sv start /etc/service/myservice'" do
@provider.stubs(:enabled?).returns :true
@provider.expects(:sv).with("start", "/etc/service/myservice")
@provider.start
end
end
describe "when stopping" do
it "should execute external command 'sv stop /etc/service/myservice'" do
@provider.expects(:sv).with("stop", "/etc/service/myservice")
@provider.stop
end
end
describe "when restarting" do
it "should call 'sv restart /etc/service/myservice'" do
@provider.expects(:sv).with("restart","/etc/service/myservice")
@provider.restart
end
end
describe "when enabling" do
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)
+ Puppet::FileSystem.expects(:symlink?).with(service_path).returns(false)
+ Puppet::FileSystem.expects(:symlink).with(daemon_path, 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)
+# mocked_file = mock(path, :symlink? => true)
FileTest.stubs(:directory?).returns(false)
- Puppet::FileSystem::File.expects(:new).with(path).returns(mocked_file)
- Puppet::FileSystem::File.expects(:unlink).with(path).returns(0)
+ Puppet::FileSystem.expects(:symlink?).with(path).returns(true) # mocked_file)
+ Puppet::FileSystem.expects(:unlink).with(path).returns(0)
@provider.disable
end
end
describe "when checking status" do
it "should call the external command 'sv status /etc/sv/myservice'" do
@provider.expects(:sv).with('status',File.join(@daemondir,"myservice"))
@provider.status
end
end
describe "when checking status" do
it "and sv status fails, properly raise a Puppet::Error" do
@provider.expects(:sv).with('status',File.join(@daemondir,"myservice")).raises(Puppet::ExecutionFailure, "fail: /etc/sv/myservice: file not found")
lambda { @provider.status }.should raise_error(Puppet::Error, 'Could not get status for service Service[myservice]: fail: /etc/sv/myservice: file not found')
end
it "and sv status returns up, then return :running" do
@provider.expects(:sv).with('status',File.join(@daemondir,"myservice")).returns("run: /etc/sv/myservice: (pid 9029) 6s")
@provider.status.should == :running
end
it "and sv status returns not running, then return :stopped" do
@provider.expects(:sv).with('status',File.join(@daemondir,"myservice")).returns("fail: /etc/sv/myservice: runsv not running")
@provider.status.should == :stopped
end
it "and sv status returns a warning, then return :stopped" do
@provider.expects(:sv).with('status',File.join(@daemondir,"myservice")).returns("warning: /etc/sv/myservice: unable to open supervise/ok: file does not exist")
@provider.status.should == :stopped
end
end
end
diff --git a/spec/unit/provider/service/src_spec.rb b/spec/unit/provider/service/src_spec.rb
index 1f0b928fd..f168e3844 100755
--- a/spec/unit/provider/service/src_spec.rb
+++ b/spec/unit/provider/service/src_spec.rb
@@ -1,173 +1,173 @@
#! /usr/bin/env ruby
#
# Unit testing for the AIX System Resource Controller (src) provider
#
require 'spec_helper'
provider_class = Puppet::Type.type(:service).provider(:src)
describe provider_class do
before :each do
@resource = stub 'resource'
@resource.stubs(:[]).returns(nil)
@resource.stubs(:[]).with(:name).returns "myservice"
@provider = provider_class.new
@provider.resource = @resource
@provider.stubs(:command).with(:stopsrc).returns "/usr/bin/stopsrc"
@provider.stubs(:command).with(:startsrc).returns "/usr/bin/startsrc"
@provider.stubs(:command).with(:lssrc).returns "/usr/bin/lssrc"
@provider.stubs(:command).with(:refresh).returns "/usr/bin/refresh"
@provider.stubs(:command).with(:lsitab).returns "/usr/sbin/lsitab"
@provider.stubs(:command).with(:mkitab).returns "/usr/sbin/mkitab"
@provider.stubs(:command).with(:rmitab).returns "/usr/sbin/rmitab"
@provider.stubs(:command).with(:chitab).returns "/usr/sbin/chitab"
@provider.stubs(:stopsrc)
@provider.stubs(:startsrc)
@provider.stubs(:lssrc)
@provider.stubs(:refresh)
@provider.stubs(:lsitab)
@provider.stubs(:mkitab)
@provider.stubs(:rmitab)
@provider.stubs(:chitab)
end
describe ".instances" do
it "should has a .instances method" do
provider_class.should respond_to :instances
end
it "should get a list of running services" do
sample_output = <<_EOF_
#subsysname:synonym:cmdargs:path:uid:auditid:standin:standout:standerr:action:multi:contact:svrkey:svrmtype:priority:signorm:sigforce:display:waittime:grpname:
myservice.1:::/usr/sbin/inetd:0:0:/dev/console:/dev/console:/dev/console:-O:-Q:-K:0:0:20:0:0:-d:20:tcpip:
myservice.2:::/usr/sbin/inetd:0:0:/dev/console:/dev/console:/dev/console:-O:-Q:-K:0:0:20:0:0:-d:20:tcpip:
myservice.3:::/usr/sbin/inetd:0:0:/dev/console:/dev/console:/dev/console:-O:-Q:-K:0:0:20:0:0:-d:20:tcpip:
myservice.4:::/usr/sbin/inetd:0:0:/dev/console:/dev/console:/dev/console:-O:-Q:-K:0:0:20:0:0:-d:20:tcpip:
_EOF_
provider_class.stubs(:lssrc).returns sample_output
provider_class.instances.map(&:name).should == [
'myservice.1',
'myservice.2',
'myservice.3',
'myservice.4'
]
end
end
describe "when starting a service" do
it "should execute the startsrc command" do
- @provider.expects(:execute).with(['/usr/bin/startsrc', '-s', "myservice"], {:override_locale => false, :squelch => true, :failonfail => true})
+ @provider.expects(:execute).with(['/usr/bin/startsrc', '-s', "myservice"], {:override_locale => false, :squelch => false, :combine => true, :failonfail => true})
@provider.start
end
end
describe "when stopping a service" do
it "should execute the stopsrc command" do
- @provider.expects(:execute).with(['/usr/bin/stopsrc', '-s', "myservice"], {:override_locale => false, :squelch => true, :failonfail => true})
+ @provider.expects(:execute).with(['/usr/bin/stopsrc', '-s', "myservice"], {:override_locale => false, :squelch => false, :combine => true, :failonfail => true})
@provider.stop
end
end
describe "should have a set of methods" do
[:enabled?, :enable, :disable, :start, :stop, :status, :restart].each do |method|
it "should have a #{method} method" do
@provider.should respond_to(method)
end
end
end
describe "when enabling" do
it "should execute the mkitab command" do
@provider.expects(:mkitab).with("myservice:2:once:/usr/bin/startsrc -s myservice").once
@provider.enable
end
end
describe "when disabling" do
it "should execute the rmitab command" do
@provider.expects(:rmitab).with("myservice")
@provider.disable
end
end
describe "when checking if it is enabled" do
it "should execute the lsitab command" do
@provider.expects(:execute).with(['/usr/sbin/lsitab', 'myservice'], {:combine => true, :failonfail => false})
@provider.enabled?
end
it "should return false when lsitab returns non-zero" do
@provider.stubs(:execute)
$CHILD_STATUS.stubs(:exitstatus).returns(1)
@provider.enabled?.should == :false
end
it "should return true when lsitab returns zero" do
@provider.stubs(:execute)
$CHILD_STATUS.stubs(:exitstatus).returns(0)
@provider.enabled?.should == :true
end
end
describe "when checking a subsystem's status" do
it "should execute status and return running if the subsystem is active" do
sample_output = <<_EOF_
Subsystem Group PID Status
myservice tcpip 1234 active
_EOF_
@provider.expects(:execute).with(['/usr/bin/lssrc', '-s', "myservice"]).returns sample_output
@provider.status.should == :running
end
it "should execute status and return stopped if the subsystem is inoperative" do
sample_output = <<_EOF_
Subsystem Group PID Status
myservice tcpip inoperative
_EOF_
@provider.expects(:execute).with(['/usr/bin/lssrc', '-s', "myservice"]).returns sample_output
@provider.status.should == :stopped
end
it "should execute status and return nil if the status is not known" do
sample_output = <<_EOF_
Subsystem Group PID Status
myservice tcpip randomdata
_EOF_
@provider.expects(:execute).with(['/usr/bin/lssrc', '-s', "myservice"]).returns sample_output
@provider.status.should == nil
end
end
describe "when restarting a service" do
it "should execute restart which runs refresh" do
sample_output = <<_EOF_
#subsysname:synonym:cmdargs:path:uid:auditid:standin:standout:standerr:action:multi:contact:svrkey:svrmtype:priority:signorm:sigforce:display:waittime:grpname:
myservice:::/usr/sbin/inetd:0:0:/dev/console:/dev/console:/dev/console:-O:-Q:-K:0:0:20:0:0:-d:20:tcpip:
_EOF_
@provider.expects(:execute).with(['/usr/bin/lssrc', '-Ss', "myservice"]).returns sample_output
@provider.expects(:execute).with(['/usr/bin/refresh', '-s', "myservice"])
@provider.restart
end
it "should execute restart which runs stopsrc then startsrc" do
sample_output = <<_EOF_
#subsysname:synonym:cmdargs:path:uid:auditid:standin:standout:standerr:action:multi:contact:svrkey:svrmtype:priority:signorm:sigforce:display:waittime:grpname:
myservice::--no-daemonize:/usr/sbin/puppetd:0:0:/dev/null:/var/log/puppet.log:/var/log/puppet.log:-O:-Q:-S:0:0:20:15:9:-d:20::"
_EOF_
@provider.expects(:execute).with(['/usr/bin/lssrc', '-Ss', "myservice"]).returns sample_output
- @provider.expects(:execute).with(['/usr/bin/stopsrc', '-s', "myservice"], {:override_locale => false, :squelch => true, :failonfail => true})
- @provider.expects(:execute).with(['/usr/bin/startsrc', '-s', "myservice"], {:override_locale => false, :squelch => true, :failonfail => true})
+ @provider.expects(:execute).with(['/usr/bin/stopsrc', '-s', "myservice"], {:override_locale => false, :squelch => false, :combine => true, :failonfail => true})
+ @provider.expects(:execute).with(['/usr/bin/startsrc', '-s', "myservice"], {:override_locale => false, :squelch => false, :combine => true, :failonfail => true})
@provider.restart
end
end
end
diff --git a/spec/unit/provider/service/systemd_spec.rb b/spec/unit/provider/service/systemd_spec.rb
index 6ed8658f9..108454a77 100755
--- a/spec/unit/provider/service/systemd_spec.rb
+++ b/spec/unit/provider/service/systemd_spec.rb
@@ -1,152 +1,162 @@
#! /usr/bin/env ruby
#
# Unit testing for the systemd service Provider
#
require 'spec_helper'
describe Puppet::Type.type(:service).provider(:systemd) do
before :each do
Puppet::Type.type(:service).stubs(:defaultprovider).returns described_class
described_class.stubs(:which).with('systemctl').returns '/bin/systemctl'
end
-
+
let :provider do
described_class.new(:name => 'sshd.service')
end
-
+
osfamily = [ 'archlinux' ]
osfamily.each do |osfamily|
it "should be the default provider on #{osfamily}" do
Facter.expects(:value).with(:osfamily).returns(osfamily)
described_class.default?.should be_true
end
- end
+ end
+
+ it "should be the default provider on rhel7" do
+ Facter.expects(:value).with(:osfamily).at_least_once.returns(:redhat)
+ Facter.expects(:value).with(:operatingsystemmajrelease).returns("7")
+ described_class.default?.should be_true
+ end
+
+ it "should not be the default provider on rhel6" do
+ Facter.expects(:value).with(:osfamily).at_least_once.returns(:redhat)
+ Facter.expects(:value).with(:operatingsystemmajrelease).returns("6")
+ described_class.default?.should_not be_true
+ end
[:enabled?, :enable, :disable, :start, :stop, :status, :restart].each do |method|
it "should have a #{method} method" do
provider.should respond_to(method)
end
end
describe ".instances" do
it "should have an instances method" do
described_class.should respond_to :instances
end
- it "should get a list of services from systemctl list-units" do
- pending('A service that has been killed (ACTIVE=failed) is not recognized')
- described_class.expects(:systemctl).with('list-units', '--full', '--all', '--no-pager').returns File.read(my_fixture('list_units'))
+ it "should return only services" do
+ described_class.expects(:systemctl).with('list-units', '--type', 'service', '--full', '--all', '--no-pager').returns File.read(my_fixture('list_units_services'))
described_class.instances.map(&:name).should =~ %w{
auditd.service
crond.service
dbus.service
display-manager.service
ebtables.service
fedora-readonly.service
initrd-switch-root.service
ip6tables.service
puppet.service
- sshd.service
}
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.service', :start => '/bin/foo'))
- provider.expects(:execute).with(['/bin/foo'], :failonfail => true, :override_locale => false, :squelch => true)
+ provider.expects(:execute).with(['/bin/foo'], :failonfail => true, :override_locale => false, :squelch => false, :combine => true)
provider.start
end
it "should start the service with systemctl start otherwise" do
provider = described_class.new(Puppet::Type.type(:service).new(:name => 'sshd.service'))
- provider.expects(:execute).with(['/bin/systemctl','start','sshd.service'], :failonfail => true, :override_locale => false, :squelch => true)
+ provider.expects(:execute).with(['/bin/systemctl','start','sshd.service'], :failonfail => true, :override_locale => false, :squelch => false, :combine => true)
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.service', :stop => '/bin/foo'))
- provider.expects(:execute).with(['/bin/foo'], :failonfail => true, :override_locale => false, :squelch => true)
+ provider.expects(:execute).with(['/bin/foo'], :failonfail => true, :override_locale => false, :squelch => false, :combine => true)
provider.stop
end
it "should stop the service with systemctl stop otherwise" do
provider = described_class.new(Puppet::Type.type(:service).new(:name => 'sshd.service'))
- provider.expects(:execute).with(['/bin/systemctl','stop','sshd.service'], :failonfail => true, :override_locale => false, :squelch => true)
+ provider.expects(:execute).with(['/bin/systemctl','stop','sshd.service'], :failonfail => true, :override_locale => false, :squelch => false, :combine => true)
provider.stop
end
end
describe "#enabled?" do
it "should return :true if the service is enabled" do
provider = described_class.new(Puppet::Type.type(:service).new(:name => 'sshd.service'))
provider.expects(:systemctl).with('is-enabled', 'sshd.service').returns 'enabled'
provider.enabled?.should == :true
end
it "should return :false if the service is disabled" do
provider = described_class.new(Puppet::Type.type(:service).new(:name => 'sshd.service'))
provider.expects(:systemctl).with('is-enabled', 'sshd.service').raises Puppet::ExecutionFailure, "Execution of '/bin/systemctl is-enabled sshd.service' returned 1: disabled"
provider.enabled?.should == :false
end
end
describe "#enable" do
it "should run systemctl enable to enable a service" do
provider = described_class.new(Puppet::Type.type(:service).new(:name => 'sshd.service'))
provider.expects(:systemctl).with('enable', 'sshd.service')
provider.enable
end
end
describe "#disable" do
it "should run systemctl disable to disable a service" do
provider = described_class.new(Puppet::Type.type(:service).new(:name => 'sshd.service'))
provider.expects(:systemctl).with(:disable, 'sshd.service')
provider.disable
end
end
# Note: systemd provider does not care about hasstatus or a custom status
# command. I just assume that it does not make sense for systemd.
describe "#status" do
it "should return running if active" do
provider = described_class.new(Puppet::Type.type(:service).new(:name => 'sshd.service'))
provider.expects(:systemctl).with('is-active', 'sshd.service').returns 'active'
provider.status.should == :running
end
it "should return stopped if inactive" do
provider = described_class.new(Puppet::Type.type(:service).new(:name => 'sshd.service'))
provider.expects(:systemctl).with('is-active', 'sshd.service').raises Puppet::ExecutionFailure, "Execution of '/bin/systemctl is-active sshd.service' returned 3: inactive"
provider.status.should == :stopped
end
end
# Note: systemd provider does not care about hasrestart. I just assume it
# does not make sense for systemd
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(['/bin/systemctl','restart','sshd.service'], :failonfail => true, :override_locale => false, :squelch => true).never
- provider.expects(:execute).with(['/bin/foo'], :failonfail => true, :override_locale => false, :squelch => true)
+ provider.expects(:execute).with(['/bin/systemctl','restart','sshd.service'], :failonfail => true, :override_locale => false, :squelch => false, :combine => true).never
+ provider.expects(:execute).with(['/bin/foo'], :failonfail => true, :override_locale => false, :squelch => false, :combine => true)
provider.restart
end
it "should restart the service with systemctl restart" do
provider = described_class.new(Puppet::Type.type(:service).new(:name => 'sshd.service'))
- provider.expects(:execute).with(['/bin/systemctl','restart','sshd.service'], :failonfail => true, :override_locale => false, :squelch => true)
+ provider.expects(:execute).with(['/bin/systemctl','restart','sshd.service'], :failonfail => true, :override_locale => false, :squelch => false, :combine => true)
provider.restart
end
end
it "(#16451) has command systemctl without being fully qualified" do
described_class.instance_variable_get(:@commands).
should include(:systemctl => 'systemctl')
end
end
diff --git a/spec/unit/provider/service/upstart_spec.rb b/spec/unit/provider/service/upstart_spec.rb
index ed93386b3..69c02b6f9 100755
--- a/spec/unit/provider/service/upstart_spec.rb
+++ b/spec/unit/provider/service/upstart_spec.rb
@@ -1,514 +1,522 @@
#! /usr/bin/env ruby
require 'spec_helper'
describe Puppet::Type.type(:service).provider(:upstart) do
let(:manual) { "\nmanual" }
let(:start_on_default_runlevels) { "\nstart on runlevel [2,3,4,5]" }
let(:provider_class) { Puppet::Type.type(:service).provider(:upstart) }
def given_contents_of(file, content)
File.open(file, 'w') do |file|
file.write(content)
end
end
def then_contents_of(file)
File.open(file).read
end
def lists_processes_as(output)
Puppet::Util::Execution.stubs(:execpipe).with("/sbin/initctl list").yields(output)
provider_class.stubs(:which).with("/sbin/initctl").returns("/sbin/initctl")
end
+ describe "excluding services" do
+ it "ignores tty and serial on Redhat systems" do
+ Facter.stubs(:value).with(:osfamily).returns('RedHat')
+ expect(described_class.excludes).to include 'serial'
+ expect(described_class.excludes).to include 'tty'
+ end
+ end
+
describe "#instances" do
it "should be able to find all instances" do
lists_processes_as("rc stop/waiting\nssh start/running, process 712")
provider_class.instances.map {|provider| provider.name}.should =~ ["rc","ssh"]
end
it "should attach the interface name for network interfaces" do
lists_processes_as("network-interface (eth0)")
provider_class.instances.first.name.should == "network-interface INTERFACE=eth0"
end
it "should attach the job name for network interface security" do
processes = "network-interface-security (network-interface/eth0)"
provider_class.stubs(:execpipe).yields(processes)
provider_class.instances.first.name.should == "network-interface-security JOB=network-interface/eth0"
end
it "should not find excluded services" do
processes = "wait-for-state stop/waiting\nportmap-wait start/running"
provider_class.stubs(:execpipe).yields(processes)
provider_class.instances.should be_empty
end
end
describe "#search" do
it "searches through paths to find a matching conf file" do
File.stubs(:directory?).returns(true)
- Puppet::FileSystem::File.stubs(:exist?).returns(false)
- Puppet::FileSystem::File.expects(:exist?).with("/etc/init/foo-bar.conf").returns(true)
+ Puppet::FileSystem.stubs(:exist?).returns(false)
+ Puppet::FileSystem.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)
provider.initscript.should == "/etc/init/foo-bar.conf"
end
it "searches for just the name of a compound named service" do
File.stubs(:directory?).returns(true)
- Puppet::FileSystem::File.stubs(:exist?).returns(false)
- Puppet::FileSystem::File.expects(:exist?).with("/etc/init/network-interface.conf").returns(true)
+ Puppet::FileSystem.stubs(:exist?).returns(false)
+ Puppet::FileSystem.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)
provider.initscript.should == "/etc/init/network-interface.conf"
end
end
describe "#status" do
it "should use the default status command if none is specified" do
resource = Puppet::Type.type(:service).new(:name => "foo", :provider => :upstart)
provider = provider_class.new(resource)
provider.stubs(:is_upstart?).returns(true)
provider.expects(:status_exec).with(["foo"]).returns("foo start/running, process 1000")
Process::Status.any_instance.stubs(:exitstatus).returns(0)
provider.status.should == :running
end
it "should properly handle services with 'start' in their name" do
resource = Puppet::Type.type(:service).new(:name => "foostartbar", :provider => :upstart)
provider = provider_class.new(resource)
provider.stubs(:is_upstart?).returns(true)
provider.expects(:status_exec).with(["foostartbar"]).returns("foostartbar stop/waiting")
Process::Status.any_instance.stubs(:exitstatus).returns(0)
provider.status.should == :stopped
end
end
describe "inheritance" do
let :resource do
resource = Puppet::Type.type(:service).new(:name => "foo", :provider => :upstart)
end
let :provider do
provider = provider_class.new(resource)
end
describe "when upstart job" do
before(:each) do
provider.stubs(:is_upstart?).returns(true)
end
["start", "stop"].each do |command|
it "should return the #{command}cmd of its parent provider" do
provider.send("#{command}cmd".to_sym).should == [provider.command(command.to_sym), resource.name]
end
end
it "should return nil for the statuscmd" do
provider.statuscmd.should be_nil
end
end
end
describe "should be enableable" do
let :resource do
Puppet::Type.type(:service).new(:name => "foo", :provider => :upstart)
end
let :provider do
provider_class.new(resource)
end
let :init_script do
PuppetSpec::Files.tmpfile("foo.conf")
end
let :over_script do
PuppetSpec::Files.tmpfile("foo.override")
end
let :disabled_content do
"\t # \t start on\nother file stuff"
end
let :multiline_disabled do
"# \t start on other file stuff (\n" +
"# more stuff ( # )))))inline comment\n" +
"# finishing up )\n" +
"# and done )\n" +
"this line shouldn't be touched\n"
end
let :multiline_disabled_bad do
"# \t start on other file stuff (\n" +
"# more stuff ( # )))))inline comment\n" +
"# finishing up )\n" +
"# and done )\n" +
"# this is a comment i want to be a comment\n" +
"this line shouldn't be touched\n"
end
let :multiline_enabled_bad do
" \t start on other file stuff (\n" +
" more stuff ( # )))))inline comment\n" +
" finishing up )\n" +
" and done )\n" +
"# this is a comment i want to be a comment\n" +
"this line shouldn't be touched\n"
end
let :multiline_enabled do
" \t start on other file stuff (\n" +
" more stuff ( # )))))inline comment\n" +
" finishing up )\n" +
" and done )\n" +
"this line shouldn't be touched\n"
end
let :multiline_enabled_standalone do
" \t start on other file stuff (\n" +
" more stuff ( # )))))inline comment\n" +
" finishing up )\n" +
" and done )\n"
end
let :enabled_content do
"\t \t start on\nother file stuff"
end
let :content do
"just some text"
end
describe "Upstart version < 0.6.7" do
before(:each) do
provider.stubs(:is_upstart?).returns(true)
provider.stubs(:upstart_version).returns("0.6.5")
provider.stubs(:search).returns(init_script)
end
[:enabled?,:enable,:disable].each do |enableable|
it "should respond to #{enableable}" do
provider.should respond_to(enableable)
end
end
describe "when enabling" do
it "should open and uncomment the '#start on' line" do
given_contents_of(init_script, disabled_content)
provider.enable
then_contents_of(init_script).should == enabled_content
end
it "should add a 'start on' line if none exists" do
given_contents_of(init_script, "this is a file")
provider.enable
then_contents_of(init_script).should == "this is a file" + start_on_default_runlevels
end
it "should handle multiline 'start on' stanzas" do
given_contents_of(init_script, multiline_disabled)
provider.enable
then_contents_of(init_script).should == multiline_enabled
end
it "should leave not 'start on' comments alone" do
given_contents_of(init_script, multiline_disabled_bad)
provider.enable
then_contents_of(init_script).should == multiline_enabled_bad
end
end
describe "when disabling" do
it "should open and comment the 'start on' line" do
given_contents_of(init_script, enabled_content)
provider.disable
then_contents_of(init_script).should == "#" + enabled_content
end
it "should handle multiline 'start on' stanzas" do
given_contents_of(init_script, multiline_enabled)
provider.disable
then_contents_of(init_script).should == multiline_disabled
end
end
describe "when checking whether it is enabled" do
it "should consider 'start on ...' to be enabled" do
given_contents_of(init_script, enabled_content)
provider.enabled?.should == :true
end
it "should consider '#start on ...' to be disabled" do
given_contents_of(init_script, disabled_content)
provider.enabled?.should == :false
end
it "should consider no start on line to be disabled" do
given_contents_of(init_script, content)
provider.enabled?.should == :false
end
end
end
describe "Upstart version < 0.9.0" do
before(:each) do
provider.stubs(:is_upstart?).returns(true)
provider.stubs(:upstart_version).returns("0.7.0")
provider.stubs(:search).returns(init_script)
end
[:enabled?,:enable,:disable].each do |enableable|
it "should respond to #{enableable}" do
provider.should respond_to(enableable)
end
end
describe "when enabling" do
it "should open and uncomment the '#start on' line" do
given_contents_of(init_script, disabled_content)
provider.enable
then_contents_of(init_script).should == enabled_content
end
it "should add a 'start on' line if none exists" do
given_contents_of(init_script, "this is a file")
provider.enable
then_contents_of(init_script).should == "this is a file" + start_on_default_runlevels
end
it "should handle multiline 'start on' stanzas" do
given_contents_of(init_script, multiline_disabled)
provider.enable
then_contents_of(init_script).should == multiline_enabled
end
it "should remove manual stanzas" do
given_contents_of(init_script, multiline_enabled + manual)
provider.enable
then_contents_of(init_script).should == multiline_enabled
end
it "should leave not 'start on' comments alone" do
given_contents_of(init_script, multiline_disabled_bad)
provider.enable
then_contents_of(init_script).should == multiline_enabled_bad
end
end
describe "when disabling" do
it "should add a manual stanza" do
given_contents_of(init_script, enabled_content)
provider.disable
then_contents_of(init_script).should == enabled_content + manual
end
it "should remove manual stanzas before adding new ones" do
given_contents_of(init_script, multiline_enabled + manual + "\n" + multiline_enabled)
provider.disable
then_contents_of(init_script).should == multiline_enabled + "\n" + multiline_enabled + manual
end
it "should handle multiline 'start on' stanzas" do
given_contents_of(init_script, multiline_enabled)
provider.disable
then_contents_of(init_script).should == multiline_enabled + manual
end
end
describe "when checking whether it is enabled" do
describe "with no manual stanza" do
it "should consider 'start on ...' to be enabled" do
given_contents_of(init_script, enabled_content)
provider.enabled?.should == :true
end
it "should consider '#start on ...' to be disabled" do
given_contents_of(init_script, disabled_content)
provider.enabled?.should == :false
end
it "should consider no start on line to be disabled" do
given_contents_of(init_script, content)
provider.enabled?.should == :false
end
end
describe "with manual stanza" do
it "should consider 'start on ...' to be disabled if there is a trailing manual stanza" do
given_contents_of(init_script, enabled_content + manual + "\nother stuff")
provider.enabled?.should == :false
end
it "should consider two start on lines with a manual in the middle to be enabled" do
given_contents_of(init_script, enabled_content + manual + "\n" + enabled_content)
provider.enabled?.should == :true
end
end
end
end
describe "Upstart version > 0.9.0" do
before(:each) do
provider.stubs(:is_upstart?).returns(true)
provider.stubs(:upstart_version).returns("0.9.5")
provider.stubs(:search).returns(init_script)
provider.stubs(:overscript).returns(over_script)
end
[:enabled?,:enable,:disable].each do |enableable|
it "should respond to #{enableable}" do
provider.should respond_to(enableable)
end
end
describe "when enabling" do
it "should add a 'start on' line if none exists" do
given_contents_of(init_script, "this is a file")
provider.enable
then_contents_of(init_script).should == "this is a file"
then_contents_of(over_script).should == start_on_default_runlevels
end
it "should handle multiline 'start on' stanzas" do
given_contents_of(init_script, multiline_disabled)
provider.enable
then_contents_of(init_script).should == multiline_disabled
then_contents_of(over_script).should == start_on_default_runlevels
end
it "should remove any manual stanzas from the override file" do
given_contents_of(over_script, manual)
given_contents_of(init_script, enabled_content)
provider.enable
then_contents_of(init_script).should == enabled_content
then_contents_of(over_script).should == ""
end
it "should copy existing start on from conf file if conf file is disabled" do
given_contents_of(init_script, multiline_enabled_standalone + manual)
provider.enable
then_contents_of(init_script).should == multiline_enabled_standalone + manual
then_contents_of(over_script).should == multiline_enabled_standalone
end
it "should leave not 'start on' comments alone" do
given_contents_of(init_script, multiline_disabled_bad)
given_contents_of(over_script, "")
provider.enable
then_contents_of(init_script).should == multiline_disabled_bad
then_contents_of(over_script).should == start_on_default_runlevels
end
end
describe "when disabling" do
it "should add a manual stanza to the override file" do
given_contents_of(init_script, enabled_content)
provider.disable
then_contents_of(init_script).should == enabled_content
then_contents_of(over_script).should == manual
end
it "should handle multiline 'start on' stanzas" do
given_contents_of(init_script, multiline_enabled)
provider.disable
then_contents_of(init_script).should == multiline_enabled
then_contents_of(over_script).should == manual
end
end
describe "when checking whether it is enabled" do
describe "with no override file" do
it "should consider 'start on ...' to be enabled" do
given_contents_of(init_script, enabled_content)
provider.enabled?.should == :true
end
it "should consider '#start on ...' to be disabled" do
given_contents_of(init_script, disabled_content)
provider.enabled?.should == :false
end
it "should consider no start on line to be disabled" do
given_contents_of(init_script, content)
provider.enabled?.should == :false
end
end
describe "with override file" do
it "should consider 'start on ...' to be disabled if there is manual in override file" do
given_contents_of(init_script, enabled_content)
given_contents_of(over_script, manual + "\nother stuff")
provider.enabled?.should == :false
end
it "should consider '#start on ...' to be enabled if there is a start on in the override file" do
given_contents_of(init_script, disabled_content)
given_contents_of(over_script, "start on stuff")
provider.enabled?.should == :true
end
end
end
end
end
end
diff --git a/spec/unit/provider/service/windows_spec.rb b/spec/unit/provider/service/windows_spec.rb
index 86ab266f4..bb883bdf5 100755
--- a/spec/unit/provider/service/windows_spec.rb
+++ b/spec/unit/provider/service/windows_spec.rb
@@ -1,183 +1,183 @@
#! /usr/bin/env ruby
#
# Unit testing for the Windows service Provider
#
require 'spec_helper'
require 'win32/service' if Puppet.features.microsoft_windows?
describe Puppet::Type.type(:service).provider(:windows), :if => Puppet.features.microsoft_windows? do
let(:name) { 'nonexistentservice' }
let(:resource) { Puppet::Type.type(:service).new(:name => name, :provider => :windows) }
let(:provider) { resource.provider }
let(:config) { Struct::ServiceConfigInfo.new }
let(:status) { Struct::ServiceStatus.new }
before :each do
# make sure we never actually execute anything (there are two execute methods)
provider.class.expects(:execute).never
provider.expects(:execute).never
Win32::Service.stubs(:config_info).with(name).returns(config)
Win32::Service.stubs(:status).with(name).returns(status)
end
describe ".instances" do
it "should enumerate all services" do
list_of_services = ['snmptrap', 'svchost', 'sshd'].map { |s| stub('service', :service_name => s) }
Win32::Service.expects(:services).returns(list_of_services)
described_class.instances.map(&:name).should =~ ['snmptrap', 'svchost', 'sshd']
end
end
describe "#start" do
before :each do
config.start_type = Win32::Service.get_start_type(Win32::Service::SERVICE_AUTO_START)
end
it "should start the service" do
provider.expects(:net).with(:start, name)
provider.start
end
it "should raise an error if the start command fails" do
provider.expects(:net).with(:start, name).raises(Puppet::ExecutionFailure, "The service name is invalid.")
expect {
provider.start
}.to raise_error(Puppet::Error, /Cannot start #{name}, error was: The service name is invalid./)
end
describe "when the service is disabled" do
before :each do
config.start_type = Win32::Service.get_start_type(Win32::Service::SERVICE_DISABLED)
end
it "should refuse to start if not managing enable" do
expect { provider.start }.to raise_error(Puppet::Error, /Will not start disabled service/)
end
it "should enable if managing enable and enable is true" do
resource[:enable] = :true
provider.expects(:net).with(:start, name)
Win32::Service.expects(:configure).with('service_name' => name, 'start_type' => Win32::Service::SERVICE_AUTO_START).returns(Win32::Service)
provider.start
end
it "should manual start if managing enable and enable is false" do
resource[:enable] = :false
provider.expects(:net).with(:start, name)
Win32::Service.expects(:configure).with('service_name' => name, 'start_type' => Win32::Service::SERVICE_DEMAND_START).returns(Win32::Service)
provider.start
end
end
end
describe "#stop" do
it "should stop a running service" do
provider.expects(:net).with(:stop, name)
provider.stop
end
it "should raise an error if the stop command fails" do
provider.expects(:net).with(:stop, name).raises(Puppet::ExecutionFailure, 'The service name is invalid.')
expect {
provider.stop
}.to raise_error(Puppet::Error, /Cannot stop #{name}, error was: The service name is invalid./)
end
end
describe "#status" do
['stopped', 'paused', 'stop pending', 'pause pending'].each do |state|
it "should report a #{state} service as stopped" do
status.current_state = state
provider.status.should == :stopped
end
end
["running", "continue pending", "start pending" ].each do |state|
it "should report a #{state} service as running" do
status.current_state = state
provider.status.should == :running
end
end
end
describe "#restart" do
it "should use the supplied restart command if specified" do
resource[:restart] = 'c:/bin/foo'
provider.expects(:execute).never
- provider.expects(:execute).with(['c:/bin/foo'], :failonfail => true, :override_locale => false, :squelch => true)
+ provider.expects(:execute).with(['c:/bin/foo'], :failonfail => true, :override_locale => false, :squelch => false, :combine => true)
provider.restart
end
it "should restart the service" do
seq = sequence("restarting")
provider.expects(:stop).in_sequence(seq)
provider.expects(:start).in_sequence(seq)
provider.restart
end
end
describe "#enabled?" do
it "should report a service with a startup type of manual as manual" do
config.start_type = Win32::Service.get_start_type(Win32::Service::SERVICE_DEMAND_START)
provider.enabled?.should == :manual
end
it "should report a service with a startup type of disabled as false" do
config.start_type = Win32::Service.get_start_type(Win32::Service::SERVICE_DISABLED)
provider.enabled?.should == :false
end
# We need to guard this section explicitly since rspec will always
# construct all examples, even if it isn't going to run them.
if Puppet.features.microsoft_windows?
[Win32::Service::SERVICE_AUTO_START, Win32::Service::SERVICE_BOOT_START, Win32::Service::SERVICE_SYSTEM_START].each do |start_type_const|
start_type = Win32::Service.get_start_type(start_type_const)
it "should report a service with a startup type of '#{start_type}' as true" do
config.start_type = start_type
provider.enabled?.should == :true
end
end
end
end
describe "#enable" do
it "should set service start type to Service_Auto_Start when enabled" do
Win32::Service.expects(:configure).with('service_name' => name, 'start_type' => Win32::Service::SERVICE_AUTO_START).returns(Win32::Service)
provider.enable
end
end
describe "#disable" do
it "should set service start type to Service_Disabled when disabled" do
Win32::Service.expects(:configure).with('service_name' => name, 'start_type' => Win32::Service::SERVICE_DISABLED).returns(Win32::Service)
provider.disable
end
end
describe "#manual_start" do
it "should set service start type to Service_Demand_Start (manual) when manual" do
Win32::Service.expects(:configure).with('service_name' => name, 'start_type' => Win32::Service::SERVICE_DEMAND_START).returns(Win32::Service)
provider.manual_start
end
end
end
diff --git a/spec/unit/provider/ssh_authorized_key/parsed_spec.rb b/spec/unit/provider/ssh_authorized_key/parsed_spec.rb
index 4d17ffe51..d0cd4e850 100755
--- a/spec/unit/provider/ssh_authorized_key/parsed_spec.rb
+++ b/spec/unit/provider/ssh_authorized_key/parsed_spec.rb
@@ -1,257 +1,255 @@
#! /usr/bin/env ruby
require 'spec_helper'
require 'shared_behaviours/all_parsedfile_providers'
require 'puppet_spec/files'
provider_class = Puppet::Type.type(:ssh_authorized_key).provider(:parsed)
describe provider_class, :unless => Puppet.features.microsoft_windows? do
include PuppetSpec::Files
before :each do
@keyfile = tmpfile('authorized_keys')
@provider_class = provider_class
@provider_class.initvars
@provider_class.any_instance.stubs(:target).returns @keyfile
@user = 'random_bob'
Puppet::Util.stubs(:uid).with(@user).returns 12345
end
def mkkey(args)
args[:target] = @keyfile
args[:user] = @user
resource = Puppet::Type.type(:ssh_authorized_key).new(args)
key = @provider_class.new(resource)
args.each do |p,v|
key.send(p.to_s + "=", v)
end
key
end
def genkey(key)
@provider_class.stubs(:filetype).returns(Puppet::Util::FileType::FileTypeRam)
File.stubs(:chown)
File.stubs(:chmod)
Puppet::Util::SUIDManager.stubs(:asuser).yields
key.flush
@provider_class.target_object(@keyfile).read
end
it_should_behave_like "all parsedfile providers", provider_class
it "should be able to generate a basic authorized_keys file" do
key = mkkey(:name => "Just_Testing",
:key => "AAAAfsfddsjldjgksdflgkjsfdlgkj",
:type => "ssh-dss",
:ensure => :present,
:options => [:absent]
)
genkey(key).should == "ssh-dss AAAAfsfddsjldjgksdflgkjsfdlgkj Just_Testing\n"
end
it "should be able to generate an authorized_keys file with options" do
key = mkkey(:name => "root@localhost",
:key => "AAAAfsfddsjldjgksdflgkjsfdlgkj",
:type => "ssh-rsa",
:ensure => :present,
:options => ['from="192.168.1.1"', "no-pty", "no-X11-forwarding"]
)
genkey(key).should == "from=\"192.168.1.1\",no-pty,no-X11-forwarding ssh-rsa AAAAfsfddsjldjgksdflgkjsfdlgkj root@localhost\n"
end
it "should parse comments" do
result = [{ :record_type => :comment, :line => "# hello" }]
@provider_class.parse("# hello\n").should == result
end
it "should parse comments with leading whitespace" do
result = [{ :record_type => :comment, :line => " # hello" }]
@provider_class.parse(" # hello\n").should == result
end
it "should skip over lines with only whitespace" do
result = [{ :record_type => :comment, :line => "#before" },
{ :record_type => :blank, :line => " " },
{ :record_type => :comment, :line => "#after" }]
@provider_class.parse("#before\n \n#after\n").should == result
end
it "should skip over completely empty lines" do
result = [{ :record_type => :comment, :line => "#before"},
{ :record_type => :blank, :line => ""},
{ :record_type => :comment, :line => "#after"}]
@provider_class.parse("#before\n\n#after\n").should == result
end
it "should be able to parse name if it includes whitespace" do
@provider_class.parse_line('ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAAgQC7pHZ1XRj3tXbFpPFhMGU1bVwz7jr13zt/wuE+pVIJA8GlmHYuYtIxHPfDHlkixdwLachCpSQUL9NbYkkRFRn9m6PZ7125ohE4E4m96QS6SGSQowTiRn4Lzd9LV38g93EMHjPmEkdSq7MY4uJEd6DUYsLvaDYdIgBiLBIWPA3OrQ== fancy user')[:name].should == 'fancy user'
@provider_class.parse_line('from="host1.reductlivelabs.com,host.reductivelabs.com",command="/usr/local/bin/run",ssh-pty ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAAgQC7pHZ1XRj3tXbFpPFhMGU1bVwz7jr13zt/wuE+pVIJA8GlmHYuYtIxHPfDHlkixdwLachCpSQUL9NbYkkRFRn9m6PZ7125ohE4E4m96QS6SGSQowTiRn4Lzd9LV38g93EMHjPmEkdSq7MY4uJEd6DUYsLvaDYdIgBiLBIWPA3OrQ== fancy user')[:name].should == 'fancy user'
end
it "should be able to parse options containing commas via its parse_options method" do
options = %w{from="host1.reductlivelabs.com,host.reductivelabs.com" command="/usr/local/bin/run" ssh-pty}
optionstr = options.join(", ")
@provider_class.parse_options(optionstr).should == options
end
it "should use '' as name for entries that lack a comment" do
line = "ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEAut8aOSxenjOqF527dlsdHWV4MNoAsX14l9M297+SQXaQ5Z3BedIxZaoQthkDALlV/25A1COELrg9J2MqJNQc8Xe9XQOIkBQWWinUlD/BXwoOTWEy8C8zSZPHZ3getMMNhGTBO+q/O+qiJx3y5cA4MTbw2zSxukfWC87qWwcZ64UUlegIM056vPsdZWFclS9hsROVEa57YUMrehQ1EGxT4Z5j6zIopufGFiAPjZigq/vqgcAqhAKP6yu4/gwO6S9tatBeEjZ8fafvj1pmvvIplZeMr96gHE7xS3pEEQqnB3nd4RY7AF6j9kFixnsytAUO7STPh/M3pLiVQBN89TvWPQ=="
@provider_class.parse(line)[0][:name].should == ""
end
- ['ssh-dss', 'ssh-rsa', 'ecdsa-sha2-nistp256', 'ecdsa-sha2-nistp384', 'ecdsa-sha2-nistp521'].each do |keytype|
+ {
+ # ssh-keygen -t dsa -b 1024
+ 'ssh-dss' => 'AAAAB3NzaC1kc3MAAACBANGTefWMXS780qLMMgysq3GNMKzg55LXZODif6Tqv1vtTh4Wuk3J5X5u644jTyNdAIn1RiBI9MnwnZMZ6nXKvucMcMQWMibYS9W2MhkRj3oqsLWMMsdGXJL18SWM5A6oC3oIRC4JHJZtkm0OctR2trKxmX+MGhdCd+Xpsh9CNK8XAAAAFQD4olFiwv+QQUFdaZbWUy1CLEG9xQAAAIByCkXKgoriZF8bQ0OX1sKuR69M/6n5ngmQGVBKB7BQkpUjbK/OggB6iJgst5utKkDcaqYRnrTYG9q3jJ/flv7yYePuoSreS0nCMMx9gpEYuq+7Sljg9IecmN/IHrNd9qdYoASy5iuROQMvEZM7KFHA8vBv0tWdBOsp4hZKyiL1DAAAAIEAjkZlOps9L+cD/MTzxDj7toYYypdLOvjlcPBaglkPZoFZ0MAKTI0zXlVX1cWAnkd0Yfo4EpP+6XAjlZkod+QXKXM4Tb4PnR34ASMeU6sEjM61Na24S7JD3gpPKataFU/oH3hzXsBdK2ttKYmoqvf61h32IA/3Z5PjCCD9pPLPpAY',
+ # ssh-keygen -t rsa -b 2048
+ 'ssh-rsa' => 'AAAAB3NzaC1yc2EAAAADAQABAAABAQDYtEaWa1mlxaAh9vtiz6RCVKDiJHDY15nsqqWU7F7A1+U1498+sWDyRDkZ8vXWQpzyOMBzBSHIxhsprlKhkjomy8BuJP+bHDBIKx4zgSFDrklrPIf467Iuug8J0qqDLxO4rOOjeAiLEyC0t2ZGnsTEea+rmat0bJ2cv3g5L4gH/OFz2pI4ZLp1HGN83ipl5UH8CjXQKwo3Db1E3WJCqKgszVX0Z4/qjnBRxFMoqky/1mGb/mX1eoT9JyQ8OhU9uENZOShkksSpgUqjlrjpj0Yd14hBlnE3M18pE4ivxjzectA/XRKNZaxOL1YREtU8sXusAwmlEY4aJ64aR0JrXfgx',
+ # ssh-keygen -t ecdsa -b 256
+ 'ecdsa-sha2-nistp256' => 'AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBBO5PfBf0c2jAuqD+Lj3j+SuXOXNT2uqESLVOn5jVQfEF9GzllOw+CMOpUvV1CiOOn+F1ET15vcsfmD7z05WUTA=',
+ # ssh-keygen -t ecdsa -b 384
+ 'ecdsa-sha2-nistp384' => 'AAAAE2VjZHNhLXNoYTItbmlzdHAzODQAAAAIbmlzdHAzODQAAABhBJIfxNoVK4FX3RuMlkHOwwxXwAh6Fqx5uAp4ftXrJ+64qYuIzb+/zSAkJV698Sre1b1lb0G4LyDdVAvXwaYK9kN25vy8umV3WdfZeHKXJGCcrplMCbbOERWARlpiPNEblg==',
+ # ssh-keygen -t ecdsa -b 521
+ 'ecdsa-sha2-nistp521' => 'AAAAE2VjZHNhLXNoYTItbmlzdHA1MjEAAAAIbmlzdHA1MjEAAACFBADLK+u12xwB0JOwpmaxYXv8KnPK4p+SE2405qoo+vpAQ569fMwPMgKzltd770amdeuFogw/MJu17PN9LDdrD3o0uwHMjWee6TpHQDkuEetaxiou6K0WAzgbxx9QsY0MsJgXf1BuMLqdK+xT183wOSXwwumv99G7T32dOJZ5tYrH0y4XMw==',
+ # ssh-keygen -t ed25519
+ 'ssh-ed25519' => 'AAAAC3NzaC1lZDI1NTE5AAAAIBWvu7D1KHBPaNXQcEuBsp48+JyPelXAq8ds6K5Du9gd',
+ }.each_pair do |keytype, keydata|
it "should be able to parse a #{keytype} key entry" do
- # use some real world examples generated with ssh-keygen
- key = case keytype
- when 'ssh-dss' # ssh-keygen -t dsa -b 1024
- 'AAAAB3NzaC1kc3MAAACBANGTefWMXS780qLMMgysq3GNMKzg55LXZODif6Tqv1vtTh4Wuk3J5X5u644jTyNdAIn1RiBI9MnwnZMZ6nXKvucMcMQWMibYS9W2MhkRj3oqsLWMMsdGXJL18SWM5A6oC3oIRC4JHJZtkm0OctR2trKxmX+MGhdCd+Xpsh9CNK8XAAAAFQD4olFiwv+QQUFdaZbWUy1CLEG9xQAAAIByCkXKgoriZF8bQ0OX1sKuR69M/6n5ngmQGVBKB7BQkpUjbK/OggB6iJgst5utKkDcaqYRnrTYG9q3jJ/flv7yYePuoSreS0nCMMx9gpEYuq+7Sljg9IecmN/IHrNd9qdYoASy5iuROQMvEZM7KFHA8vBv0tWdBOsp4hZKyiL1DAAAAIEAjkZlOps9L+cD/MTzxDj7toYYypdLOvjlcPBaglkPZoFZ0MAKTI0zXlVX1cWAnkd0Yfo4EpP+6XAjlZkod+QXKXM4Tb4PnR34ASMeU6sEjM61Na24S7JD3gpPKataFU/oH3hzXsBdK2ttKYmoqvf61h32IA/3Z5PjCCD9pPLPpAY'
- when 'ssh-rsa' # ssh-keygen -t rsa -b 2048
- 'AAAAB3NzaC1yc2EAAAADAQABAAABAQDYtEaWa1mlxaAh9vtiz6RCVKDiJHDY15nsqqWU7F7A1+U1498+sWDyRDkZ8vXWQpzyOMBzBSHIxhsprlKhkjomy8BuJP+bHDBIKx4zgSFDrklrPIf467Iuug8J0qqDLxO4rOOjeAiLEyC0t2ZGnsTEea+rmat0bJ2cv3g5L4gH/OFz2pI4ZLp1HGN83ipl5UH8CjXQKwo3Db1E3WJCqKgszVX0Z4/qjnBRxFMoqky/1mGb/mX1eoT9JyQ8OhU9uENZOShkksSpgUqjlrjpj0Yd14hBlnE3M18pE4ivxjzectA/XRKNZaxOL1YREtU8sXusAwmlEY4aJ64aR0JrXfgx'
- when 'ecdsa-sha2-nistp256' # ssh-keygen -t ecdsa -b 256
- 'AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBBO5PfBf0c2jAuqD+Lj3j+SuXOXNT2uqESLVOn5jVQfEF9GzllOw+CMOpUvV1CiOOn+F1ET15vcsfmD7z05WUTA='
- when 'ecdsa-sha2-nistp384' # ssh-keygen -t ecdsa -b 384
- 'AAAAE2VjZHNhLXNoYTItbmlzdHAzODQAAAAIbmlzdHAzODQAAABhBJIfxNoVK4FX3RuMlkHOwwxXwAh6Fqx5uAp4ftXrJ+64qYuIzb+/zSAkJV698Sre1b1lb0G4LyDdVAvXwaYK9kN25vy8umV3WdfZeHKXJGCcrplMCbbOERWARlpiPNEblg=='
- when 'ecdsa-sha2-nistp521' #ssh-keygen -t ecdsa -b 521
- 'AAAAE2VjZHNhLXNoYTItbmlzdHA1MjEAAAAIbmlzdHA1MjEAAACFBADLK+u12xwB0JOwpmaxYXv8KnPK4p+SE2405qoo+vpAQ569fMwPMgKzltd770amdeuFogw/MJu17PN9LDdrD3o0uwHMjWee6TpHQDkuEetaxiou6K0WAzgbxx9QsY0MsJgXf1BuMLqdK+xT183wOSXwwumv99G7T32dOJZ5tYrH0y4XMw=='
- else
- pending("No sample key for #{keytype} yet")
- end
comment = 'sample_key'
- record = @provider_class.parse_line("#{keytype} #{key} #{comment}")
+ record = @provider_class.parse_line("#{keytype} #{keydata} #{comment}")
record.should_not be_nil
record[:name].should == comment
- record[:key].should == key
+ record[:key].should == keydata
record[:type].should == keytype
end
end
end
describe provider_class, :unless => Puppet.features.microsoft_windows? do
before :each do
@resource = Puppet::Type.type(:ssh_authorized_key).new(:name => "foo", :user => "random_bob")
@provider = provider_class.new(@resource)
provider_class.stubs(:filetype).returns(Puppet::Util::FileType::FileTypeRam)
Puppet::Util::SUIDManager.stubs(:asuser).yields
provider_class.initvars
end
describe "when flushing" do
before :each do
# Stub file and directory operations
Dir.stubs(:mkdir)
File.stubs(:chmod)
File.stubs(:chown)
end
describe "and both a user and a target have been specified" do
before :each do
Puppet::Util.stubs(:uid).with("random_bob").returns 12345
@resource[:user] = "random_bob"
target = "/tmp/.ssh_dir/place_to_put_authorized_keys"
@resource[:target] = target
end
it "should create the directory" do
- Puppet::FileSystem::File.stubs(:exist?).with("/tmp/.ssh_dir").returns false
+ Puppet::FileSystem.stubs(:exist?).with("/tmp/.ssh_dir").returns false
Dir.expects(:mkdir).with("/tmp/.ssh_dir", 0700)
@provider.flush
end
it "should absolutely not chown the directory to the user" do
uid = Puppet::Util.uid("random_bob")
File.expects(:chown).never
@provider.flush
end
it "should absolutely not chown the key file to the user" do
uid = Puppet::Util.uid("random_bob")
File.expects(:chown).never
@provider.flush
end
it "should chmod the key file to 0600" do
File.expects(:chmod).with(0600, "/tmp/.ssh_dir/place_to_put_authorized_keys")
@provider.flush
end
end
describe "and a user has been specified with no target" do
before :each do
@resource[:user] = "nobody"
#
# I'd like to use random_bob here and something like
#
# File.stubs(:expand_path).with("~random_bob/.ssh").returns "/users/r/random_bob/.ssh"
#
# but mocha objects strenuously to stubbing File.expand_path
# so I'm left with using nobody.
@dir = File.expand_path("~nobody/.ssh")
end
it "should create the directory if it doesn't exist" do
- Puppet::FileSystem::File.stubs(:exist?).with(@dir).returns false
+ Puppet::FileSystem.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
- Puppet::FileSystem::File.stubs(:exist?).with(@dir).returns false
+ Puppet::FileSystem.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
- Puppet::FileSystem::File.stubs(:exist?).with(@dir).returns false
+ Puppet::FileSystem.stubs(:exist?).with(@dir).returns false
Dir.stubs(:mkdir).with(@dir,0700)
uid = Puppet::Util.uid("nobody")
File.expects(:chown).never
@provider.flush
end
it "should not create or chown the directory if it already exist" do
- Puppet::FileSystem::File.stubs(:exist?).with(@dir).returns false
+ Puppet::FileSystem.stubs(:exist?).with(@dir).returns false
Dir.expects(:mkdir).never
File.expects(:chown).never
@provider.flush
end
it "should absolutely not chown the key file to the user" do
uid = Puppet::Util.uid("nobody")
File.expects(:chown).never
@provider.flush
end
it "should chmod the key file to 0600" do
File.expects(:chmod).with(0600, File.expand_path("~nobody/.ssh/authorized_keys"))
@provider.flush
end
end
describe "and a target has been specified with no user" do
it "should raise an error" do
@resource = Puppet::Type.type(:ssh_authorized_key).new(:name => "foo", :target => "/tmp/.ssh_dir/place_to_put_authorized_keys")
@provider = provider_class.new(@resource)
proc { @provider.flush }.should raise_error
end
end
describe "and an invalid user has been specified with no target" do
it "should catch an exception and raise a Puppet error" do
@resource[:user] = "thisusershouldnotexist"
lambda { @provider.flush }.should raise_error(Puppet::Error)
end
end
end
end
diff --git a/spec/unit/provider/user/directoryservice_spec.rb b/spec/unit/provider/user/directoryservice_spec.rb
index fe72bfc4f..ccf37742a 100755
--- a/spec/unit/provider/user/directoryservice_spec.rb
+++ b/spec/unit/provider/user/directoryservice_spec.rb
@@ -1,1063 +1,1063 @@
#! /usr/bin/env ruby -S rspec
# encoding: ASCII-8BIT
require 'spec_helper'
require 'facter/util/plist'
describe Puppet::Type.type(:user).provider(:directoryservice) do
let(:username) { 'nonexistant_user' }
let(:user_path) { "/Users/#{username}" }
let(:resource) do
Puppet::Type.type(:user).new(
:name => username,
:provider => :directoryservice
)
end
let(:provider) { resource.provider }
let(:users_plist_dir) { '/var/db/dslocal/nodes/Default/users' }
let(:stringio_object) { StringIO.new('new_stringio_object') }
# This is the output of doing `dscl -plist . read /Users/<username>` which
# will return a hash of keys whose values are all arrays.
let(:user_plist_xml) do
'<?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>dsAttrTypeStandard:NFSHomeDirectory</key>
<array>
<string>/Users/nonexistant_user</string>
</array>
<key>dsAttrTypeStandard:RealName</key>
<array>
<string>nonexistant_user</string>
</array>
<key>dsAttrTypeStandard:PrimaryGroupID</key>
<array>
<string>22</string>
</array>
<key>dsAttrTypeStandard:UniqueID</key>
<array>
<string>1000</string>
</array>
<key>dsAttrTypeStandard:RecordName</key>
<array>
<string>nonexistant_user</string>
</array>
</dict>
</plist>'
end
# This is the same as above, however in a native Ruby hash instead
# of XML
let(:user_plist_hash) do
{
"dsAttrTypeStandard:RealName" => [username],
"dsAttrTypeStandard:NFSHomeDirectory" => [user_path],
"dsAttrTypeStandard:PrimaryGroupID" => ["22"],
"dsAttrTypeStandard:UniqueID" => ["1000"],
"dsAttrTypeStandard:RecordName" => [username]
}
end
# The below value is the result of executing
# `dscl -plist . read /Users/<username> ShadowHashData` on a 10.7
# system and converting it to a native Ruby Hash with Plist.parse_xml
let(:sha512_shadowhashdata_hash) do
{
'dsAttrTypeNative:ShadowHashData' => ['62706c69 73743030 d101025d 53414c54 45442d53 48413531 324f1044 7ea7d592 131f57b2 c8f8bdbc ec8d9df1 2128a386 393a4f00 c7619bac 2622a44d 451419d1 1da512d5 915ab98e 39718ac9 4083fe2e fd6bf710 a54d477f 8ff735b1 2587192d 080b1900 00000000 00010100 00000000 00000300 00000000 00000000 00000000 000060']
}
end
# The below is a binary plist that is stored in the ShadowHashData key
# on a 10.7 system.
let(:sha512_embedded_bplist) do
"bplist00\321\001\002]SALTED-SHA512O\020D~\247\325\222\023\037W\262\310\370\275\274\354\215\235\361!(\243\2069:O\000\307a\233\254&\"\244ME\024\031\321\035\245\022\325\221Z\271\2169q\212\311@\203\376.\375k\367\020\245MG\177\217\3675\261%\207\031-\b\v\031\000\000\000\000\000\000\001\001\000\000\000\000\000\000\000\003\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000`"
end
# The below is a Base64 encoded string representing a salted-SHA512 password
# hash.
let(:sha512_pw_string) do
"~\247\325\222\023\037W\262\310\370\275\274\354\215\235\361!(\243\2069:O\000\307a\233\254&\"\244ME\024\031\321\035\245\022\325\221Z\271\2169q\212\311@\203\376.\375k\367\020\245MG\177\217\3675\261%\207\031-"
end
# The below is the result of converting sha512_embedded_bplist to XML and
# parsing it with Plist.parse_xml. It is a Ruby Hash whose value is a
# StringIO object holding a Base64 encoded salted-SHA512 password hash.
let(:sha512_embedded_bplist_hash) do
{ 'SALTED-SHA512' => StringIO.new(sha512_pw_string) }
end
# The value below is the result of converting sha512_pw_string to Hex.
let(:sha512_password_hash) do
'7ea7d592131f57b2c8f8bdbcec8d9df12128a386393a4f00c7619bac2622a44d451419d11da512d5915ab98e39718ac94083fe2efd6bf710a54d477f8ff735b12587192d'
end
# The below value is the result of executing
# `dscl -plist . read /Users/<username> ShadowHashData` on a 10.8
# system and converting it to a native Ruby Hash with Plist.parse_xml
let(:pbkdf2_shadowhashdata_hash) do
{
"dsAttrTypeNative:ShadowHashData"=>["62706c69 73743030 d101025f 10145341 4c544544 2d534841 3531322d 50424b44 4632d303 04050607 0857656e 74726f70 79547361 6c745a69 74657261 74696f6e 734f1080 0590ade1 9e6953c1 35ae872a e7761823 5df7d46c 63de7f9a 0fcdf2cd 9e7d85e4 b7ca8681 01235b61 58e05a30 9805ee48 14b027a4 be9c23ec 2926bc81 72269aff ba5c9a59 85e81091 fa689807 6d297f1f aa75fa61 7551ef16 71d75200 55c4a0d9 7b9b9c58 05aa322b aedbcd8e e9c52381 1653ac2e a9e9c8d8 f1ac519a 0f2b595e 4f102093 77c46908 a1c8ac2c 3e45c0d4 4da8ad0f cd85ec5c 14d9a59f fc40c9da 31f0ec11 60b0080b 22293136 41c4e700 00000000 00010100 00000000 00000900 00000000 00000000 00000000 0000ea"]
}
end
# The below value is the result of converting pbkdf2_embedded_bplist to XML and
# parsing it with Plist.parse_xml.
let(:pbkdf2_embedded_bplist_hash) do
{
'SALTED-SHA512-PBKDF2' => {
'entropy' => StringIO.new(pbkdf2_pw_string),
'salt' => StringIO.new(pbkdf2_salt_string),
'iterations' => pbkdf2_iterations_value
}
}
end
# The value below is the result of converting pbkdf2_pw_string to Hex.
let(:pbkdf2_password_hash) do
'0590ade19e6953c135ae872ae77618235df7d46c63de7f9a0fcdf2cd9e7d85e4b7ca868101235b6158e05a309805ee4814b027a4be9c23ec2926bc8172269affba5c9a5985e81091fa6898076d297f1faa75fa617551ef1671d7520055c4a0d97b9b9c5805aa322baedbcd8ee9c523811653ac2ea9e9c8d8f1ac519a0f2b595e'
end
# The below is a binary plist that is stored in the ShadowHashData key
# of a 10.8 system.
let(:pbkdf2_embedded_plist) do
"bplist00\321\001\002_\020\024SALTED-SHA512-PBKDF2\323\003\004\005\006\a\bWentropyTsaltZiterationsO\020\200\005\220\255\341\236iS\3015\256\207*\347v\030#]\367\324lc\336\177\232\017\315\362\315\236}\205\344\267\312\206\201\001#[aX\340Z0\230\005\356H\024\260'\244\276\234#\354)&\274\201r&\232\377\272\\\232Y\205\350\020\221\372h\230\am)\177\037\252u\372auQ\357\026q\327R\000U\304\240\331{\233\234X\005\2522+\256\333\315\216\351\305#\201\026S\254.\251\351\310\330\361\254Q\232\017+Y^O\020 \223w\304i\b\241\310\254,>E\300\324M\250\255\017\315\205\354\\\024\331\245\237\374@\311\3321\360\354\021`\260\b\v\")16A\304\347\000\000\000\000\000\000\001\001\000\000\000\000\000\000\000\t\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\352"
end
# The below value is a Base64 encoded string representing a PBKDF2 password
# hash.
let(:pbkdf2_pw_string) do
"\005\220\255\341\236iS\3015\256\207*\347v\030#]\367\324lc\336\177\232\017\315\362\315\236}\205\344\267\312\206\201\001#[aX\340Z0\230\005\356H\024\260'\244\276\234#\354)&\274\201r&\232\377\272\\\232Y\205\350\020\221\372h\230\am)\177\037\252u\372auQ\357\026q\327R\000U\304\240\331{\233\234X\005\2522+\256\333\315\216\351\305#\201\026S\254.\251\351\310\330\361\254Q\232\017+Y^"
end
# The below value is a Base64 encoded string representing a PBKDF2 salt
# string.
let(:pbkdf2_salt_string) do
"\223w\304i\b\241\310\254,>E\300\324M\250\255\017\315\205\354\\\024\331\245\237\374@\311\3321\360\354"
end
# The below value represents the Hex value of a PBKDF2 salt string
let(:pbkdf2_salt_value) do
"9377c46908a1c8ac2c3e45c0d44da8ad0fcd85ec5c14d9a59ffc40c9da31f0ec"
end
# The below value is a Fixnum iterations value used in the PBKDF2
# key stretching algorithm
let(:pbkdf2_iterations_value) do
24752
end
# The below represents output of 'dscl -plist . readall /Users' converted to
# a native Ruby hash if only one user were installed on the system.
# This lets us check the behavior of all the methods necessary to return a
# user's groups property by controlling the data provided by dscl
let(:testuser_hash) do
[{"dsAttrTypeStandard:RecordName" =>["nonexistant_user"],
"dsAttrTypeStandard:UniqueID" =>["1000"],
"dsAttrTypeStandard:AuthenticationAuthority"=>
[";Kerberosv5;;testuser@LKDC:SHA1.4383E152D9D394AA32D13AE98F6F6E1FE8D00F81;LKDC:SHA1.4383E152D9D394AA32D13AE98F6F6E1FE8D00F81",
";ShadowHash;HASHLIST:<SALTED-SHA512>"],
"dsAttrTypeStandard:AppleMetaNodeLocation" =>["/Local/Default"],
"dsAttrTypeStandard:NFSHomeDirectory" =>["/Users/nonexistant_user"],
"dsAttrTypeStandard:RecordType" =>["dsRecTypeStandard:Users"],
"dsAttrTypeStandard:RealName" =>["nonexistant_user"],
"dsAttrTypeStandard:Password" =>["********"],
"dsAttrTypeStandard:PrimaryGroupID" =>["22"],
"dsAttrTypeStandard:GeneratedUID" =>["0A7D5B63-3AD4-4CA7-B03E-85876F1D1FB3"],
"dsAttrTypeStandard:AuthenticationHint" =>[""],
"dsAttrTypeNative:KerberosKeys" =>
["30820157 a1030201 02a08201 4e308201 4a3074a1 2b3029a0 03020112 a1220420 54af3992 1c198bf8 94585a6b 2fba445b c8482228 0dcad666 ea62e038 99e59c45 a2453043 a0030201 03a13c04 3a4c4b44 433a5348 41312e34 33383345 31353244 39443339 34414133 32443133 41453938 46364636 45314645 38443030 46383174 65737475 73657230 64a11b30 19a00302 0111a112 04106375 7d97b2ce ca8343a6 3b0f73d5 1001a245 3043a003 020103a1 3c043a4c 4b44433a 53484131 2e343338 33453135 32443944 33393441 41333244 31334145 39384636 46364531 46453844 30304638 31746573 74757365 72306ca1 233021a0 03020110 a11a0418 67b09be3 5131b670 f8e9265e 62459b4c 19435419 fe918519 a2453043 a0030201 03a13c04 3a4c4b44 433a5348 41312e34 33383345 31353244 39443339 34414133 32443133 41453938 46364636 45314645 38443030 46383174 65737475 736572"],
"dsAttrTypeStandard:PasswordPolicyOptions" =>
["<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n <!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n <plist version=\"1.0\">\n <dict>\n <key>failedLoginCount</key>\n <integer>0</integer>\n <key>failedLoginTimestamp</key>\n <date>2001-01-01T00:00:00Z</date>\n <key>lastLoginTimestamp</key>\n <date>2001-01-01T00:00:00Z</date>\n <key>passwordTimestamp</key>\n <date>2012-08-10T23:53:50Z</date>\n </dict>\n </plist>\n "],
"dsAttrTypeStandard:UserShell" =>["/bin/bash"],
"dsAttrTypeNative:ShadowHashData" =>
["62706c69 73743030 d101025d 53414c54 45442d53 48413531 324f1044 7ea7d592 131f57b2 c8f8bdbc ec8d9df1 2128a386 393a4f00 c7619bac 2622a44d 451419d1 1da512d5 915ab98e 39718ac9 4083fe2e fd6bf710 a54d477f 8ff735b1 2587192d 080b1900 00000000 00010100 00000000 00000300 00000000 00000000 00000000 000060"]}]
end
# The below represents the result of running Plist.parse_xml on XML
# data returned from the `dscl -plist . readall /Groups` command.
# (AKA: What the get_list_of_groups method returns)
let(:group_plist_hash_guid) do
[{
'dsAttrTypeStandard:RecordName' => ['testgroup'],
'dsAttrTypeStandard:GroupMembership' => [
username,
'jeff',
'zack'
],
'dsAttrTypeStandard:GroupMembers' => [
"guid#{username}",
'guidtestuser',
'guidjeff',
'guidzack'
],
},
{
'dsAttrTypeStandard:RecordName' => ['second'],
'dsAttrTypeStandard:GroupMembership' => [
'jeff',
'zack'
],
'dsAttrTypeStandard:GroupMembers' => [
"guid#{username}",
'guidjeff',
'guidzack'
],
},
{
'dsAttrTypeStandard:RecordName' => ['third'],
'dsAttrTypeStandard:GroupMembership' => [
username,
'jeff',
'zack'
],
'dsAttrTypeStandard:GroupMembers' => [
"guid#{username}",
'guidtestuser',
'guidjeff',
'guidzack'
],
}]
end
describe 'Creating a user that does not exist' do
# These are the defaults that the provider will use if a user does
# not provide a value
let(:defaults) do
{
'UniqueID' => '1000',
'RealName' => resource[:name],
'PrimaryGroupID' => 20,
'UserShell' => '/bin/bash',
'NFSHomeDirectory' => "/Users/#{resource[:name]}"
}
end
before :each do
# Stub out all calls to dscl with default values from above
defaults.each do |key, val|
provider.stubs(:merge_attribute_with_dscl).with('Users', username, key, val)
end
# Mock the rest of the dscl calls. We can't assume that our Linux
# build system will have the dscl binary
provider.stubs(:create_new_user).with(username)
provider.class.stubs(:get_attribute_from_dscl).with('Users', username, 'GeneratedUID').returns({'dsAttrTypeStandard:GeneratedUID' => ['GUID']})
provider.stubs(:next_system_id).returns('1000')
end
it 'should not raise any errors when creating a user with default values' do
provider.create
end
%w{password iterations salt}.each do |value|
it "should call ##{value}= if a #{value} attribute is specified" do
resource[value.intern] = 'somevalue'
setter = (value << '=').intern
provider.expects(setter).with('somevalue')
provider.create
end
end
it 'should merge the GroupMembership and GroupMembers dscl values if a groups attribute is specified' do
resource[:groups] = 'somegroup'
provider.expects(:merge_attribute_with_dscl).with('Groups', 'somegroup', 'GroupMembership', username)
provider.expects(:merge_attribute_with_dscl).with('Groups', 'somegroup', 'GroupMembers', 'GUID')
provider.create
end
it 'should convert group names into integers' do
resource[:gid] = 'somegroup'
Puppet::Util.expects(:gid).with('somegroup').returns(21)
provider.expects(:merge_attribute_with_dscl).with('Users', username, 'PrimaryGroupID', 21)
provider.create
end
end
describe 'self#instances' do
it 'should create an array of provider instances' do
provider.class.expects(:get_all_users).returns(['foo', 'bar'])
['foo', 'bar'].each do |user|
provider.class.expects(:generate_attribute_hash).with(user).returns({})
end
instances = provider.class.instances
instances.should be_a_kind_of Array
instances.each do |instance|
instance.should be_a_kind_of Puppet::Provider
end
end
end
describe 'self#get_all_users' do
let(:empty_plist) do
'<?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>
</dict>
</plist>'
end
it 'should return a hash of user attributes' do
provider.class.expects(:dscl).with('-plist', '.', 'readall', '/Users').returns(user_plist_xml)
provider.class.get_all_users.should == user_plist_hash
end
it 'should return a hash when passed an empty plist' do
provider.class.expects(:dscl).with('-plist', '.', 'readall', '/Users').returns(empty_plist)
provider.class.get_all_users.should == {}
end
end
describe 'self#generate_attribute_hash' do
let(:user_plist_resource) do
{
:ensure => :present,
:provider => :directoryservice,
:groups => 'testgroup,third',
:comment => username,
:password => sha512_password_hash,
:shadowhashdata => sha512_shadowhashdata_hash,
:name => username,
:uid => 1000,
:gid => 22,
:home => user_path
}
end
before :each do
provider.class.stubs(:get_os_version).returns('10.7')
provider.class.stubs(:get_all_users).returns(testuser_hash)
provider.class.stubs(:get_attribute_from_dscl).with('Users', username, 'ShadowHashData').returns(sha512_shadowhashdata_hash)
provider.class.stubs(:get_list_of_groups).returns(group_plist_hash_guid)
provider.class.stubs(:convert_binary_to_xml).with(sha512_embedded_bplist).returns(sha512_embedded_bplist_hash)
provider.class.prefetch({})
end
it 'should return :uid values as a Fixnum' do
provider.class.generate_attribute_hash(user_plist_hash)[:uid].should be_a_kind_of Fixnum
end
it 'should return :gid values as a Fixnum' do
provider.class.generate_attribute_hash(user_plist_hash)[:gid].should be_a_kind_of Fixnum
end
it 'should return a hash of resource attributes' do
provider.class.generate_attribute_hash(user_plist_hash).should == user_plist_resource
end
end
describe '#exists?' do
# This test expects an error to be raised
# I'm PROBABLY doing this wrong...
it 'should return false if the dscl command errors out' do
provider.expects(:dscl).with('.', 'read', user_path).raises(Puppet::ExecutionFailure, 'Dscl Fails')
provider.exists?.should == false
end
it 'should return true if the dscl command does not error' do
provider.expects(:dscl).with('.', 'read', user_path).returns(user_plist_xml)
provider.exists?.should == true
end
end
describe '#delete' do
it 'should call dscl when destroying/deleting a resource' do
provider.expects(:dscl).with('.', '-delete', user_path)
provider.delete
end
end
describe 'the groups property' do
# The below represents the result of running Plist.parse_xml on XML
# data returned from the `dscl -plist . readall /Groups` command.
# (AKA: What the get_list_of_groups method returns)
let(:group_plist_hash) do
[{
'dsAttrTypeStandard:RecordName' => ['testgroup'],
'dsAttrTypeStandard:GroupMembership' => [
'testuser',
username,
'jeff',
'zack'
],
'dsAttrTypeStandard:GroupMembers' => [
'guidtestuser',
'guidjeff',
'guidzack'
],
},
{
'dsAttrTypeStandard:RecordName' => ['second'],
'dsAttrTypeStandard:GroupMembership' => [
username,
'testuser',
'jeff',
],
'dsAttrTypeStandard:GroupMembers' => [
'guidtestuser',
'guidjeff',
],
},
{
'dsAttrTypeStandard:RecordName' => ['third'],
'dsAttrTypeStandard:GroupMembership' => [
'jeff',
'zack'
],
'dsAttrTypeStandard:GroupMembers' => [
'guidjeff',
'guidzack'
],
}]
end
before :each do
provider.class.stubs(:get_all_users).returns(testuser_hash)
provider.class.stubs(:get_attribute_from_dscl).with('Users', username, 'ShadowHashData').returns([])
provider.class.stubs(:get_os_version).returns('10.7')
end
it "should return a list of groups if the user's name matches GroupMembership" do
provider.class.expects(:get_list_of_groups).returns(group_plist_hash)
provider.class.prefetch({}).first.groups.should == 'second,testgroup'
end
it "should return a list of groups if the user's GUID matches GroupMembers" do
provider.class.expects(:get_list_of_groups).returns(group_plist_hash_guid)
provider.class.prefetch({}).first.groups.should == 'testgroup,third'
end
end
describe '#groups=' do
let(:group_plist_one_two_three) do
[{
'dsAttrTypeStandard:RecordName' => ['one'],
'dsAttrTypeStandard:GroupMembership' => [
'jeff',
'zack'
],
'dsAttrTypeStandard:GroupMembers' => [
'guidjeff',
'guidzack'
],
},
{
'dsAttrTypeStandard:RecordName' => ['two'],
'dsAttrTypeStandard:GroupMembership' => [
'jeff',
'zack',
username
],
'dsAttrTypeStandard:GroupMembers' => [
'guidjeff',
'guidzack'
],
},
{
'dsAttrTypeStandard:RecordName' => ['three'],
'dsAttrTypeStandard:GroupMembership' => [
'jeff',
'zack',
username
],
'dsAttrTypeStandard:GroupMembers' => [
'guidjeff',
'guidzack'
],
}]
end
before :each do
provider.class.stubs(:get_all_users).returns(testuser_hash)
provider.class.stubs(:get_list_of_groups).returns(group_plist_one_two_three)
end
it 'should call dscl to add necessary groups' do
provider.class.expects(:get_os_version).returns('10.7')
provider.class.expects(:get_attribute_from_dscl).with('Users', username, 'ShadowHashData').returns([])
provider.class.expects(:get_attribute_from_dscl).with('Users', username, 'GeneratedUID').returns({'dsAttrTypeStandard:GeneratedUID' => ['guidnonexistant_user']})
provider.expects(:groups).returns('two,three')
provider.expects(:dscl).with('.', '-merge', '/Groups/one', 'GroupMembership', 'nonexistant_user')
provider.expects(:dscl).with('.', '-merge', '/Groups/one', 'GroupMembers', 'guidnonexistant_user')
provider.class.prefetch({})
provider.groups= 'one,two,three'
end
#describe how passwords are fetched in 10.5 and 10.6
['10.5', '10.6'].each do |os_ver|
it "should call the get_sha1 method on #{os_ver}" do
provider.class.expects(:get_os_version).returns(os_ver)
provider.class.expects(:get_attribute_from_dscl).with('Users', username, 'ShadowHashData').returns([])
provider.class.expects(:get_sha1).with('0A7D5B63-3AD4-4CA7-B03E-85876F1D1FB3').returns('password')
provider.class.prefetch({}).first.password.should == 'password'
end
end
it 'should call the get_salted_sha512 method on 10.7 and return the correct hash' do
provider.class.expects(:get_os_version).returns('10.7')
provider.class.expects(:convert_binary_to_xml).with(sha512_embedded_bplist).returns(sha512_embedded_bplist_hash)
provider.class.expects(:get_attribute_from_dscl).with('Users', username, 'ShadowHashData').returns(sha512_shadowhashdata_hash)
provider.class.prefetch({}).first.password.should == sha512_password_hash
end
it 'should call the get_salted_sha512_pbkdf2 method on 10.8 and return the correct hash' do
provider.class.expects(:get_os_version).returns('10.8')
provider.class.expects(:get_attribute_from_dscl).with('Users', username,'ShadowHashData').returns(pbkdf2_shadowhashdata_hash)
provider.class.expects(:convert_binary_to_xml).with(pbkdf2_embedded_plist).returns(pbkdf2_embedded_bplist_hash)
provider.class.prefetch({}).first.password.should == pbkdf2_password_hash
end
end
describe '#password=' do
before :each do
provider.stubs(:sleep)
provider.stubs(:flush_dscl_cache)
end
['10.5', '10.6'].each do |os_ver|
it "should call write_sha1_hash when setting the password on #{os_ver}" do
provider.class.stubs(:get_os_version).returns(os_ver)
provider.expects(:write_sha1_hash).with('password')
provider.password = 'password'
end
end
it 'should call write_password_to_users_plist when setting the password on 10.7' do
provider.class.stubs(:get_os_version).returns('10.7')
provider.expects(:write_password_to_users_plist).with(sha512_password_hash)
provider.password = sha512_password_hash
end
it 'should call write_password_to_users_plist when setting the password on 10.8' do
provider.class.stubs(:get_os_version).returns('10.8')
provider.expects(:write_password_to_users_plist).with(pbkdf2_password_hash)
provider.password = pbkdf2_password_hash
end
it "should raise an error on 10.7 if a password hash that doesn't contain 136 characters is passed" do
provider.class.stubs(:get_os_version).returns('10.7')
expect { provider.password = 'password' }.to raise_error Puppet::Error, /OS X 10\.7 requires a Salted SHA512 hash password of 136 characters\. Please check your password and try again/
end
it "should raise an error on 10.8 if a password hash that doesn't contain 256 characters is passed" do
provider.class.stubs(:get_os_version).returns('10.8')
expect { provider.password = 'password' }.to raise_error Puppet::Error, /OS X versions > 10\.7 require a Salted SHA512 PBKDF2 password hash of 256 characters\. Please check your password and try again\./
end
end
describe '#get_list_of_groups' do
# The below value is the result of running `dscl -plist . readall /Groups`
# on an OS X system.
let(:groups_xml) do
'<?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">
<array>
<dict>
<key>dsAttrTypeStandard:AppleMetaNodeLocation</key>
<array>
<string>/Local/Default</string>
</array>
<key>dsAttrTypeStandard:GeneratedUID</key>
<array>
<string>ABCDEFAB-CDEF-ABCD-EFAB-CDEF00000053</string>
</array>
<key>dsAttrTypeStandard:Password</key>
<array>
<string>*</string>
</array>
<key>dsAttrTypeStandard:PrimaryGroupID</key>
<array>
<string>83</string>
</array>
<key>dsAttrTypeStandard:RealName</key>
<array>
<string>SPAM Assassin Group 2</string>
</array>
<key>dsAttrTypeStandard:RecordName</key>
<array>
<string>_amavisd</string>
<string>amavisd</string>
</array>
<key>dsAttrTypeStandard:RecordType</key>
<array>
<string>dsRecTypeStandard:Groups</string>
</array>
</dict>
</array>
</plist>'
end
# The below value is the result of executing Plist.parse_xml on
# groups_xml
let(:groups_hash) do
[{ 'dsAttrTypeStandard:AppleMetaNodeLocation' => ['/Local/Default'],
'dsAttrTypeStandard:GeneratedUID' => ['ABCDEFAB-CDEF-ABCD-EFAB-CDEF00000053'],
'dsAttrTypeStandard:Password' => ['*'],
'dsAttrTypeStandard:PrimaryGroupID' => ['83'],
'dsAttrTypeStandard:RealName' => ['SPAM Assassin Group 2'],
'dsAttrTypeStandard:RecordName' => ['_amavisd', 'amavisd'],
'dsAttrTypeStandard:RecordType' => ['dsRecTypeStandard:Groups']
}]
end
it 'should return an array of hashes containing group data' do
provider.class.expects(:dscl).with('-plist', '.', 'readall', '/Groups').returns(groups_xml)
provider.class.get_list_of_groups.should == groups_hash
end
end
describe '#get_attribute_from_dscl' do
# The below value is the result of executing
# `dscl -plist . read /Users/<username/ GeneratedUID`
# on an OS X system.
let(:user_guid_xml) do
'<?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>dsAttrTypeStandard:GeneratedUID</key>
<array>
<string>DCC660C6-F5A9-446D-B9FF-3C0258AB5BA0</string>
</array>
</dict>
</plist>'
end
# The below value is the result of parsing user_guid_xml with
# Plist.parse_xml
let(:user_guid_hash) do
{ 'dsAttrTypeStandard:GeneratedUID' => ['DCC660C6-F5A9-446D-B9FF-3C0258AB5BA0'] }
end
it 'should return a hash containing a user\'s dscl attribute data' do
provider.class.expects(:dscl).with('-plist', '.', 'read', user_path, 'GeneratedUID').returns(user_guid_xml)
provider.class.get_attribute_from_dscl('Users', username, 'GeneratedUID').should == user_guid_hash
end
end
describe '#convert_xml_to_binary' do
# Because this method relies on a binary that only exists on OS X, a stub
# object is needed to expect the calls. This makes testing somewhat...uneventful
let(:stub_io_object) { stub('connection') }
it 'should use plutil to successfully convert an xml plist to a binary plist' do
IO.expects(:popen).with('plutil -convert binary1 -o - -', 'r+').yields stub_io_object
Plist::Emit.expects(:dump).with('ruby_hash').returns('xml_plist_data')
stub_io_object.expects(:write).with('xml_plist_data')
stub_io_object.expects(:close_write)
stub_io_object.expects(:read).returns('binary_plist_data')
provider.class.convert_xml_to_binary('ruby_hash').should == 'binary_plist_data'
end
end
describe '#convert_binary_to_xml' do
let(:stub_io_object) { stub('connection') }
it 'should accept a binary plist and return a ruby hash containing the plist data' do
IO.expects(:popen).with('plutil -convert xml1 -o - -', 'r+').yields stub_io_object
stub_io_object.expects(:write).with('binary_plist_data')
stub_io_object.expects(:close_write)
stub_io_object.expects(:read).returns(user_plist_xml)
provider.class.convert_binary_to_xml('binary_plist_data').should == user_plist_hash
end
end
describe '#next_system_id' do
it 'should return the next available UID number that is not in the list obtained from dscl and is greater than the passed integer value' do
provider.expects(:dscl).with('.', '-list', '/Users', 'uid').returns("kathee 312\ngary 11\ntanny 33\njohn 9\nzach 5")
provider.next_system_id(30).should == 34
end
end
describe '#get_salted_sha512' do
it "should accept a hash whose 'SALTED-SHA512' key contains a StringIO object with a base64 encoded salted-SHA512 password hash and return the hex value of that password hash" do
provider.class.get_salted_sha512(sha512_embedded_bplist_hash).should == sha512_password_hash
end
end
describe '#get_salted_sha512_pbkdf2' do
it "should accept a hash containing a PBKDF2 password hash, salt, and iterations value and return the correct password hash" do
provider.class.get_salted_sha512_pbkdf2('entropy', pbkdf2_embedded_bplist_hash).should == pbkdf2_password_hash
end
it "should accept a hash containing a PBKDF2 password hash, salt, and iterations value and return the correct salt value" do
provider.class.get_salted_sha512_pbkdf2('salt', pbkdf2_embedded_bplist_hash).should == pbkdf2_salt_value
end
it "should accept a hash containing a PBKDF2 password hash, salt, and iterations value and return the correct iterations value" do
provider.class.get_salted_sha512_pbkdf2('iterations', pbkdf2_embedded_bplist_hash).should == pbkdf2_iterations_value
end
it "should return a Fixnum value when looking up the PBKDF2 iterations value" do
- provider.class.get_salted_sha512_pbkdf2('iterations', pbkdf2_embedded_bplist_hash).should be_a_kind_of Fixnum
+ provider.class.get_salted_sha512_pbkdf2('iterations', pbkdf2_embedded_bplist_hash).should be_a_kind_of(Fixnum)
end
it "should raise an error if a field other than 'entropy', 'salt', or 'iterations' is passed" do
- expect { provider.class.get_salted_sha512_pbkdf2('othervalue', pbkdf2_embedded_bplist_hash) }.to raise_error Puppet::Error, /Puppet has tried to read an incorrect value from the SALTED-SHA512-PBKDF2 hash. Acceptable fields are 'salt', 'entropy', or 'iterations'/
+ expect { provider.class.get_salted_sha512_pbkdf2('othervalue', pbkdf2_embedded_bplist_hash) }.to raise_error(Puppet::Error, /Puppet has tried to read an incorrect value from the SALTED-SHA512-PBKDF2 hash. Acceptable fields are 'salt', 'entropy', or 'iterations'/)
end
end
describe '#get_sha1' do
let(:password_hash_file) { '/var/db/shadow/hash/user_guid' }
let(:stub_password_file) { stub('connection') }
it 'should return a sha1 hash read from disk' do
- Puppet::FileSystem::File.expects(:exist?).with(password_hash_file).returns(true)
+ Puppet::FileSystem.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)
stub_password_file.expects(:read).returns('sha1_password_hash')
stub_password_file.expects(:close)
provider.class.get_sha1('user_guid').should == 'sha1_password_hash'
end
it 'should return nil if the password_hash_file does not exist' do
- Puppet::FileSystem::File.expects(:exist?).with(password_hash_file).returns(false)
+ Puppet::FileSystem.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
- Puppet::FileSystem::File.expects(:exist?).with(password_hash_file).returns(true)
+ Puppet::FileSystem.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
- Puppet::FileSystem::File.expects(:exist?).with(password_hash_file).returns(true)
+ Puppet::FileSystem.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}/
+ expect { provider.class.get_sha1('user_guid').should == nil }.to raise_error(Puppet::Error, /Could not read password hash file at #{password_hash_file}/)
end
end
describe '#write_password_to_users_plist' do
let(:sha512_plist_xml) do
"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n<plist version=\"1.0\">\n<dict>\n\t<key>KerberosKeys</key>\n\t<array>\n\t\t<data>\n\t\tMIIBS6EDAgEBoIIBQjCCAT4wcKErMCmgAwIBEqEiBCCS/0Im7BAps/YhX/ED\n\t\tKOpDeSMFkUsu3UzEa6gqDu35BKJBMD+gAwIBA6E4BDZMS0RDOlNIQTEuNDM4\n\t\tM0UxNTJEOUQzOTRBQTMyRDEzQUU5OEY2RjZFMUZFOEQwMEY4MWplZmYwYKEb\n\t\tMBmgAwIBEaESBBAk8a3rrFk5mHAdEU5nRgFwokEwP6ADAgEDoTgENkxLREM6\n\t\tU0hBMS40MzgzRTE1MkQ5RDM5NEFBMzJEMTNBRTk4RjZGNkUxRkU4RDAwRjgx\n\t\tamVmZjBooSMwIaADAgEQoRoEGFg71irsV+9ddRNPSn9houo3Q6jZuj55XaJB\n\t\tMD+gAwIBA6E4BDZMS0RDOlNIQTEuNDM4M0UxNTJEOUQzOTRBQTMyRDEzQUU5\n\t\tOEY2RjZFMUZFOEQwMEY4MWplZmY=\n\t\t</data>\n\t</array>\n\t<key>ShadowHashData</key>\n\t<array>\n\t\t<data>\n\t\tYnBsaXN0MDDRAQJdU0FMVEVELVNIQTUxMk8QRFNL0iuruijP6becUWe43GTX\n\t\t5WTgOTi2emx41DMnwnB4vbKieVOE4eNHiyocX5c0GX1LWJ6VlZqZ9EnDLsuA\n\t\tNC5Ga9qlCAsZAAAAAAAAAQEAAAAAAAAAAwAAAAAAAAAAAAAAAAAAAGA=\n\t\t</data>\n\t</array>\n\t<key>authentication_authority</key>\n\t<array>\n\t\t<string>;Kerberosv5;;jeff@LKDC:SHA1.4383E152D9D394AA32D13AE98F6F6E1FE8D00F81;LKDC:SHA1.4383E152D9D394AA32D13AE98F6F6E1FE8D00F81</string>\n\t\t<string>;ShadowHash;HASHLIST:&lt;SALTED-SHA512&gt;</string>\n\t</array>\n\t<key>dsAttrTypeStandard:ShadowHashData</key>\n\t<array>\n\t\t<data>\n\t\tYnBsaXN0MDDRAQJdU0FMVEVELVNIQTUxMk8QRH6n1ZITH1eyyPi9vOyNnfEh\n\t\tKKOGOTpPAMdhm6wmIqRNRRQZ0R2lEtWRWrmOOXGKyUCD/i79a/cQpU1Hf4/3\n\t\tNbElhxktCAsZAAAAAAAAAQEAAAAAAAAAAwAAAAAAAAAAAAAAAAAAAGA=\n\t\t</data>\n\t</array>\n\t<key>generateduid</key>\n\t<array>\n\t\t<string>3AC74939-C14F-45DD-B6A9-D1A82373F0B0</string>\n\t</array>\n\t<key>name</key>\n\t<array>\n\t\t<string>jeff</string>\n\t</array>\n\t<key>passwd</key>\n\t<array>\n\t\t<string>********</string>\n\t</array>\n\t<key>passwordpolicyoptions</key>\n\t<array>\n\t\t<data>\n\t\tPD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz4KPCFET0NU\n\t\tWVBFIHBsaXN0IFBVQkxJQyAiLS8vQXBwbGUvL0RURCBQTElTVCAxLjAvL0VO\n\t\tIiAiaHR0cDovL3d3dy5hcHBsZS5jb20vRFREcy9Qcm9wZXJ0eUxpc3QtMS4w\n\t\tLmR0ZCI+CjxwbGlzdCB2ZXJzaW9uPSIxLjAiPgo8ZGljdD4KCTxrZXk+ZmFp\n\t\tbGVkTG9naW5Db3VudDwva2V5PgoJPGludGVnZXI+MDwvaW50ZWdlcj4KCTxr\n\t\tZXk+ZmFpbGVkTG9naW5UaW1lc3RhbXA8L2tleT4KCTxkYXRlPjIwMDEtMDEt\n\t\tMDFUMDA6MDA6MDBaPC9kYXRlPgoJPGtleT5sYXN0TG9naW5UaW1lc3RhbXA8\n\t\tL2tleT4KCTxkYXRlPjIwMDEtMDEtMDFUMDA6MDA6MDBaPC9kYXRlPgoJPGtl\n\t\teT5wYXNzd29yZFRpbWVzdGFtcDwva2V5PgoJPGRhdGU+MjAxMi0wOC0xMVQw\n\t\tMDozNTo1MFo8L2RhdGU+CjwvZGljdD4KPC9wbGlzdD4K\n\t\t</data>\n\t</array>\n\t<key>uid</key>\n\t<array>\n\t\t<string>28</string>\n\t</array>\n</dict>\n</plist>"
end
let(:pbkdf2_plist_xml) do
"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n<plist version=\"1.0\">\n<dict>\n\t<key>KerberosKeys</key>\n\t<array>\n\t\t<data>\n\t\tMIIBS6EDAgEBoIIBQjCCAT4wcKErMCmgAwIBEqEiBCDrboPy0gxu7oTZR/Pc\n\t\tYdCBC9ivXo1k05gt036/aNe5VqJBMD+gAwIBA6E4BDZMS0RDOlNIQTEuNDEz\n\t\tQTMwRjU5MEVFREM3ODdENTMyOTgxODUwQTk3NTI0NUIwQTcyM2plZmYwYKEb\n\t\tMBmgAwIBEaESBBCm02SYYdsxo2fiDP4KuPtmokEwP6ADAgEDoTgENkxLREM6\n\t\tU0hBMS40MTNBMzBGNTkwRUVEQzc4N0Q1MzI5ODE4NTBBOTc1MjQ1QjBBNzIz\n\t\tamVmZjBooSMwIaADAgEQoRoEGHPBc7Dg7zjaE8g+YXObwupiBLMIlCrN5aJB\n\t\tMD+gAwIBA6E4BDZMS0RDOlNIQTEuNDEzQTMwRjU5MEVFREM3ODdENTMyOTgx\n\t\tODUwQTk3NTI0NUIwQTcyM2plZmY=\n\t\t</data>\n\t</array>\n\t<key>ShadowHashData</key>\n\t<array>\n\t\t<data>\n\t\tYnBsaXN0MDDRAQJfEBRTQUxURUQtU0hBNTEyLVBCS0RGMtMDBAUGBwhXZW50\n\t\tcm9weVRzYWx0Wml0ZXJhdGlvbnNPEIAFkK3hnmlTwTWuhyrndhgjXffUbGPe\n\t\tf5oPzfLNnn2F5LfKhoEBI1thWOBaMJgF7kgUsCekvpwj7CkmvIFyJpr/ulya\n\t\tWYXoEJH6aJgHbSl/H6p1+mF1Ue8WcddSAFXEoNl7m5xYBaoyK67bzY7pxSOB\n\t\tFlOsLqnpyNjxrFGaDytZXk8QIJN3xGkIocisLD5FwNRNqK0PzYXsXBTZpZ/8\n\t\tQMnaMfDsEWCwCAsiKTE2QcTnAAAAAAAAAQEAAAAAAAAACQAAAAAAAAAAAAAA\n\t\tAAAAAOo=\n\t\t</data>\n\t</array>\n\t<key>authentication_authority</key>\n\t<array>\n\t\t<string>;Kerberosv5;;jeff@LKDC:SHA1.413A30F590EEDC787D532981850A975245B0A723;LKDC:SHA1.413A30F590EEDC787D532981850A975245B0A723</string>\n\t\t<string>;ShadowHash;HASHLIST:&lt;SALTED-SHA512-PBKDF2&gt;</string>\n\t</array>\n\t<key>generateduid</key>\n\t<array>\n\t\t<string>1CB825D1-2DF7-43CC-B874-DB6BBB76C402</string>\n\t</array>\n\t<key>gid</key>\n\t<array>\n\t\t<string>21</string>\n\t</array>\n\t<key>name</key>\n\t<array>\n\t\t<string>jeff</string>\n\t</array>\n\t<key>passwd</key>\n\t<array>\n\t\t<string>********</string>\n\t</array>\n\t<key>passwordpolicyoptions</key>\n\t<array>\n\t\t<data>\n\t\tPD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz4KPCFET0NU\n\t\tWVBFIHBsaXN0IFBVQkxJQyAiLS8vQXBwbGUvL0RURCBQTElTVCAxLjAvL0VO\n\t\tIiAiaHR0cDovL3d3dy5hcHBsZS5jb20vRFREcy9Qcm9wZXJ0eUxpc3QtMS4w\n\t\tLmR0ZCI+CjxwbGlzdCB2ZXJzaW9uPSIxLjAiPgo8ZGljdD4KCTxrZXk+ZmFp\n\t\tbGVkTG9naW5Db3VudDwva2V5PgoJPGludGVnZXI+MDwvaW50ZWdlcj4KCTxr\n\t\tZXk+ZmFpbGVkTG9naW5UaW1lc3RhbXA8L2tleT4KCTxkYXRlPjIwMDEtMDEt\n\t\tMDFUMDA6MDA6MDBaPC9kYXRlPgoJPGtleT5sYXN0TG9naW5UaW1lc3RhbXA8\n\t\tL2tleT4KCTxkYXRlPjIwMDEtMDEtMDFUMDA6MDA6MDBaPC9kYXRlPgoJPGtl\n\t\teT5wYXNzd29yZExhc3RTZXRUaW1lPC9rZXk+Cgk8ZGF0ZT4yMDEyLTA3LTI1\n\t\tVDE4OjQ3OjU5WjwvZGF0ZT4KPC9kaWN0Pgo8L3BsaXN0Pgo=\n\t\t</data>\n\t</array>\n\t<key>uid</key>\n\t<array>\n\t\t<string>28</string>\n\t</array>\n</dict>\n</plist>"
end
let(:sha512_shadowhashdata) do
{
'SALTED-SHA512' => StringIO.new('blankvalue')
}
end
let(:pbkdf2_shadowhashdata) do
{
'SALTED-SHA512-PBKDF2' => {
'entropy' => StringIO.new('blank_entropy'),
'salt' => StringIO.new('blank_salt'),
'iterations' => 100
}
}
end
let(:sample_users_plist) do
{
"shell" => ["/bin/zsh"],
"passwd" => ["********"],
"picture" => ["/Library/User Pictures/Animals/Eagle.tif"],
"_writers_LinkedIdentity" => ["puppet"], "name"=>["puppet"],
"home" => ["/Users/puppet"],
"_writers_UserCertificate" => ["puppet"],
"_writers_passwd" => ["puppet"],
"gid" => ["20"],
"generateduid" => ["DA8A0E67-E9BE-4B4F-B34E-8977BAE0D3D4"],
"realname" => ["Puppet"],
"_writers_picture" => ["puppet"],
"uid" => ["501"],
"hint" => [""],
"authentication_authority" => [";ShadowHash;HASHLIST:<SALTED-SHA512>",
";Kerberosv5;;puppet@LKDC:S HA1.35580B1D6366D2890A35D430373FF653297F377D;LKDC:SHA1.35580B1D6366D2890A35D430373FF653297F377D"],
"_writers_realname" => ["puppet"],
"_writers_hint" => ["puppet"],
"ShadowHashData" => [StringIO.new('blank')]
}
end
it 'should call set_salted_sha512 on 10.7 when given a salted-SHA512 password hash' do
provider.expects(:get_users_plist).returns(sample_users_plist)
provider.expects(:get_shadow_hash_data).with(sample_users_plist).returns(sha512_shadowhashdata)
provider.class.expects(:get_os_version).returns('10.7')
provider.expects(:set_salted_sha512).with(sample_users_plist, sha512_shadowhashdata, sha512_password_hash)
provider.write_password_to_users_plist(sha512_password_hash)
end
it 'should call set_salted_pbkdf2 on 10.8 when given a PBKDF2 password hash' do
provider.expects(:get_users_plist).returns(sample_users_plist)
provider.expects(:get_shadow_hash_data).with(sample_users_plist).returns(pbkdf2_shadowhashdata)
provider.class.expects(:get_os_version).returns('10.8')
provider.expects(:set_salted_pbkdf2).with(sample_users_plist, pbkdf2_shadowhashdata, 'entropy', pbkdf2_password_hash)
provider.write_password_to_users_plist(pbkdf2_password_hash)
end
it "should delete the SALTED-SHA512 key in the shadow_hash_data hash if it exists on a 10.8 system and write_password_to_users_plist has been called to set the user's password" do
provider.expects(:get_users_plist).returns('users_plist')
provider.expects(:get_shadow_hash_data).with('users_plist').returns(sha512_shadowhashdata)
provider.class.expects(:get_os_version).returns('10.8')
provider.expects(:set_salted_pbkdf2).with('users_plist', {}, 'entropy', pbkdf2_password_hash)
provider.write_password_to_users_plist(pbkdf2_password_hash)
end
end
describe '#set_salted_sha512' do
let(:users_plist) { {'ShadowHashData' => [StringIO.new('string_data')] } }
let(:sha512_shadow_hash_data) do
{
'SALTED-SHA512' => stringio_object
}
end
it 'should set the SALTED-SHA512 password hash for a user in 10.7 and call the set_shadow_hash_data method to write the plist to disk' do
provider.class.expects(:convert_xml_to_binary).with(sha512_embedded_bplist_hash).returns(sha512_embedded_bplist)
provider.expects(:set_shadow_hash_data).with(users_plist, sha512_embedded_bplist)
provider.set_salted_sha512(users_plist, sha512_embedded_bplist_hash, sha512_password_hash)
end
it 'should set the salted-SHA512 password, even if a blank shadow_hash_data hash is passed' do
provider.expects(:new_stringio_object).returns(stringio_object)
provider.class.expects(:convert_xml_to_binary).with(sha512_shadow_hash_data).returns(sha512_embedded_bplist)
provider.expects(:set_shadow_hash_data).with(users_plist, sha512_embedded_bplist)
provider.set_salted_sha512(users_plist, false, sha512_password_hash)
end
end
describe '#set_salted_pbkdf2' do
let(:users_plist) { {'ShadowHashData' => [StringIO.new('string_data')] } }
let(:entropy_shadow_hash_data) do
{
'SALTED-SHA512-PBKDF2' =>
{
'entropy' => stringio_object
}
}
end
# This will also catch the edge-case where a 10.6-style user exists on
# a 10.8 system and Puppet attempts to set a password
it 'should not fail if shadow_hash_data is not a Hash' do
provider.expects(:new_stringio_object).returns(stringio_object)
provider.expects(:base64_decode_string).with(pbkdf2_password_hash).returns('binary_string')
provider.class.expects(:convert_xml_to_binary).with(entropy_shadow_hash_data).returns('binary_plist')
provider.expects(:set_shadow_hash_data).with({'passwd' => '********'}, 'binary_plist')
provider.set_salted_pbkdf2({}, false, 'entropy', pbkdf2_password_hash)
end
it "should set the PBKDF2 password hash when the 'entropy' field is passed with a valid password hash" do
provider.class.expects(:convert_xml_to_binary).with(pbkdf2_embedded_bplist_hash).returns(pbkdf2_embedded_plist)
provider.expects(:set_shadow_hash_data).with(users_plist, pbkdf2_embedded_plist)
users_plist.expects(:[]=).with('passwd', '********')
provider.set_salted_pbkdf2(users_plist, pbkdf2_embedded_bplist_hash, 'entropy', pbkdf2_password_hash)
end
it "should set the PBKDF2 password hash when the 'salt' field is passed with a valid password hash" do
provider.class.expects(:convert_xml_to_binary).with(pbkdf2_embedded_bplist_hash).returns(pbkdf2_embedded_plist)
provider.expects(:set_shadow_hash_data).with(users_plist, pbkdf2_embedded_plist)
users_plist.expects(:[]=).with('passwd', '********')
provider.set_salted_pbkdf2(users_plist, pbkdf2_embedded_bplist_hash, 'salt', pbkdf2_salt_value)
end
it "should set the PBKDF2 password hash when the 'iterations' field is passed with a valid password hash" do
provider.class.expects(:convert_xml_to_binary).with(pbkdf2_embedded_bplist_hash).returns(pbkdf2_embedded_plist)
provider.expects(:set_shadow_hash_data).with(users_plist, pbkdf2_embedded_plist)
users_plist.expects(:[]=).with('passwd', '********')
provider.set_salted_pbkdf2(users_plist, pbkdf2_embedded_bplist_hash, 'iterations', pbkdf2_iterations_value)
end
end
describe '#write_users_plist_to_disk' do
it 'should save the passed plist to disk and convert it to a binary plist' do
Plist::Emit.expects(:save_plist).with(user_plist_xml, "#{users_plist_dir}/nonexistant_user.plist")
provider.expects(:plutil).with('-convert', 'binary1', "#{users_plist_dir}/nonexistant_user.plist")
provider.write_users_plist_to_disk(user_plist_xml)
end
end
describe '#write_sha1_hash' do
let(:password_hash_dir) { '/var/db/shadow/hash' }
it "should write the sha1 hash to a file on disk named after the user's GUID and also ensure that ':ShadowHash;' is included in the user's AuthenticationAuthority" do
provider.class.expects(:get_attribute_from_dscl).with('Users', username, 'GeneratedUID').returns({'dsAttrTypeStandard:GeneratedUID' => ['GUID']})
provider.expects(:write_to_file).with("#{password_hash_dir}/GUID", 'sha1_password')
provider.expects(:dscl).with('.', '-merge', user_path, 'AuthenticationAuthority', ';ShadowHash;').returns(true)
provider.write_sha1_hash('sha1_password')
end
it "should raise an error if Puppet cannot write to the file in /var/db/shadow/hash named after the user's GUID" do
File.expects(:open).with('filename', 'w').raises(Errno::EACCES, 'boom')
expect { provider.write_to_file('filename', 'sha1_password') }.to raise_error Puppet::Error, /Could not write to file filename: Permission denied - boom/
end
it "should raise an error if dscl cannot merge ';ShadowHash;' into the user's AuthenticationAuthority" do
provider.class.expects(:get_attribute_from_dscl).with('Users', username, 'GeneratedUID').returns({'dsAttrTypeStandard:GeneratedUID' => ['GUID']})
provider.expects(:write_to_file).with("#{password_hash_dir}/GUID", 'sha1_password')
provider.expects(:dscl).with('.', '-merge', user_path, 'AuthenticationAuthority', ';ShadowHash;').raises(Puppet::ExecutionFailure, 'boom')
expect { provider.write_sha1_hash('sha1_password') }.to raise_error Puppet::Error, /Could not set the dscl AuthenticationAuthority key with value: ;ShadowHash;/
end
end
describe '#merge_attribute_with_dscl' do
it 'should raise an error if a dscl command raises an error' do
provider.expects(:dscl).with('.', '-merge', user_path, 'GeneratedUID', 'GUID').raises(Puppet::ExecutionFailure, 'boom')
expect { provider.merge_attribute_with_dscl('Users', username, 'GeneratedUID', 'GUID') }.to raise_error Puppet::Error, /Could not set the dscl GeneratedUID key with value: GUID/
end
end
describe '#get_users_plist' do
let(:test_plist) do
"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple Computer//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n<plist version=\"1.0\">\n<dict>\n\t<key>shell</key>\n\t<string>/bin/bash</string>\n\t<key>user</key>\n\t<string>puppet</string>\n</dict>\n</plist>\n"
end
let(:test_hash) do
{
'user' => 'puppet',
'shell' => '/bin/bash'
}
end
it 'should convert a plist to a valid Ruby hash' do
provider.expects(:plutil).with('-convert', 'xml1', '-o', '/dev/stdout', "#{users_plist_dir}/#{username}.plist").returns(test_plist)
provider.get_users_plist(username).should == test_hash
end
end
describe '#get_shadow_hash_data' do
let(:shadow_hash) do
{
'ShadowHashData' => [StringIO.new('test')]
}
end
let(:no_shadow_hash) do
{
'no' => 'Shadow Hash Data'
}
end
it 'should return false if the passed users_plist does NOT have a ShadowHashData key' do
provider.get_shadow_hash_data(no_shadow_hash).should == false
end
it 'should call convert_binary_to_xml() with the contents of the StringIO Object ' +
'located in the first element of the array of the ShadowHashData key if the ' +
'passed users_plist contains a ShadowHashData key' do
provider.class.expects(:convert_binary_to_xml).with('test').returns('returnvalue')
provider.get_shadow_hash_data(shadow_hash).should == 'returnvalue'
end
end
describe 'self#get_os_version' do
it 'should call Facter.value(:macosx_productversion_major) ONLY ONCE no matter how ' +
'many times get_os_version() is called' do
Facter.expects(:value).with(:macosx_productversion_major).once.returns('10.8')
provider.class.get_os_version.should == '10.8'
provider.class.get_os_version.should == '10.8'
provider.class.get_os_version.should == '10.8'
provider.class.get_os_version.should == '10.8'
end
end
describe '#base64_decode_string' do
it 'should return a Base64-decoded string appropriate for use in a user\'s plist' do
provider.base64_decode_string(sha512_password_hash).should == sha512_pw_string
end
end
describe '(#12833) 10.6-style users on 10.8' do
# The below represents output of 'dscl -plist . readall /Users'
# converted to a Ruby hash if only one user were installed on the system.
# This lets us check the behavior of all the methods necessary to return
# a user's groups property by controlling the data provided by dscl. The
# differentiating aspect about this plist is that it's from a 10.6-style
# user. There's an edge case whereby a user that was created in 10.6, but
# who hasn't attempted to login to the system until after it's been
# upgraded to 10.8, will experience errors due to assumptions in Puppet
# based solely on operatingsystem.
let(:all_users_hash) do
[
{
"dsAttrTypeNative:_writers_UserCertificate" => ["testuser"],
"dsAttrTypeStandard:RealName" => ["testuser"],
"dsAttrTypeStandard:NFSHomeDirectory" => ["/Users/testuser"],
"dsAttrTypeNative:_writers_realname" => ["testuser"],
"dsAttrTypeNative:_writers_picture" => ["testuser"],
"dsAttrTypeStandard:AppleMetaNodeLocation" => ["/Local/Default"],
"dsAttrTypeStandard:PrimaryGroupID" => ["20"],
"dsAttrTypeNative:_writers_LinkedIdentity" => ["testuser"],
"dsAttrTypeStandard:UserShell" => ["/bin/bash"],
"dsAttrTypeStandard:UniqueID" => ["1234"],
"dsAttrTypeStandard:RecordName" => ["testuser"],
"dsAttrTypeStandard:Password" => ["********"],
"dsAttrTypeNative:_writers_jpegphoto" => ["testuser"],
"dsAttrTypeNative:_writers_hint" => ["testuser"],
"dsAttrTypeNative:_writers_passwd" => ["testuser"],
"dsAttrTypeStandard:RecordType" => ["dsRecTypeStandard:Users"],
"dsAttrTypeStandard:AuthenticationAuthority" => [
";ShadowHash;",
";Kerberosv5;;testuser@LKDC:SHA1.48AC4BCFEFE9 D66847B5E7D813BC4B12C5513A07;LKDC:SHA1.48AC4BCFEFE9D66847B5E7D813BC4B12C5513A07;"
],
"dsAttrTypeStandard:GeneratedUID" => ["D1AC2ECC-F177-4B45-8B18-59CF002F97FF"]
}
]
end
let(:username) { 'testuser' }
let(:user_path) { "/Users/#{username}" }
let(:resource) do
Puppet::Type.type(:user).new(
:name => username,
:provider => :directoryservice
)
end
let(:provider) { resource.provider }
# The below represents the result of get_users_plist on the testuser
# account from the 'all_users_hash' helper method. The get_users_plist
# method calls the `plutil` binary to do its work, so we want to stub
# that out
let(:user_plist_hash) do
{
'realname' => ['testuser'],
'authentication_authority' => [';ShadowHash;', ';Kerberosv5;;testuser@LKDC:SHA1.48AC4BCFEFE9D66847B5E7D813BC4B12C5513A07;LKDC:SHA1.48AC4BCFEFE9D66847B5E7D813BC4B12C5513A07;'],
'home' => ['/Users/testuser'],
'_writers_realname' => ['testuser'],
'passwd' => '********',
'_writers_LinkedIdentity' => ['testuser'],
'_writers_picture' => ['testuser'],
'gid' => ['20'],
'_writers_passwd' => ['testuser'],
'_writers_hint' => ['testuser'],
'_writers_UserCertificate' => ['testuser'],
'_writers_jpegphoto' => ['testuser'],
'shell' => ['/bin/bash'],
'uid' => ['1234'],
'generateduid' => ['D1AC2ECC-F177-4B45-8B18-59CF002F97FF'],
'name' => ['testuser']
}
end
before :each do
provider.class.stubs(:get_all_users).returns(all_users_hash)
provider.class.stubs(:get_list_of_groups).returns(group_plist_hash_guid)
provider.class.stubs(:get_attribute_from_dscl).with('Users', 'testuser', 'ShadowHashData').returns({})
provider.class.prefetch({})
end
it 'should not raise an error if the password=() method is called on ' +
'a user without a ShadowHashData key in their user\'s plist on OS X ' +
'version 10.8' do
provider.class.stubs(:get_os_version).returns('10.8')
provider.stubs(:sleep)
provider.stubs(:flush_dscl_cache)
provider.expects(:get_users_plist).with('testuser').returns(user_plist_hash)
provider.expects(:set_salted_pbkdf2).with(user_plist_hash, false, 'entropy', pbkdf2_password_hash)
provider.password = pbkdf2_password_hash
end
end
end
diff --git a/spec/unit/provider/user/useradd_spec.rb b/spec/unit/provider/user/useradd_spec.rb
index 98183758b..215fd6cf4 100755
--- a/spec/unit/provider/user/useradd_spec.rb
+++ b/spec/unit/provider/user/useradd_spec.rb
@@ -1,402 +1,430 @@
#! /usr/bin/env ruby
require 'spec_helper'
describe Puppet::Type.type(:user).provider(:useradd) do
before :each do
described_class.stubs(:command).with(:password).returns '/usr/bin/chage'
described_class.stubs(:command).with(:add).returns '/usr/sbin/useradd'
described_class.stubs(:command).with(:localadd).returns '/usr/sbin/luseradd'
described_class.stubs(:command).with(:modify).returns '/usr/sbin/usermod'
described_class.stubs(:command).with(:delete).returns '/usr/sbin/userdel'
end
let(:resource) do
Puppet::Type.type(:user).new(
:name => 'myuser',
:managehome => :false,
:system => :false,
:provider => provider
)
end
let(:provider) { described_class.new(:name => 'myuser') }
let(:shadow_entry) {
return unless Puppet.features.libshadow?
- Struct::PasswdEntry.new(
- 'myuser', # login name
- '$6$FvW8Ib8h$qQMI/CR9m.QzIicZKutLpBgCBBdrch1IX0rTnxuI32K1pD9.RXZrmeKQlaC.RzODNuoUtPPIyQDufunvLOQWF0', # encrypted password
- 15573, # date of last password change
- 10, # minimum password age
- 20, # maximum password age
- 7, # password warning period
- -1, # password inactivity period
- 15706, # account expiration date
- -1 # reserved field
- )
+ entry = Struct::PasswdEntry.new
+ entry[:sp_namp] = 'myuser' # login name
+ entry[:sp_pwdp] = '$6$FvW8Ib8h$qQMI/CR9m.QzIicZKutLpBgCBBdrch1IX0rTnxuI32K1pD9.RXZrmeKQlaC.RzODNuoUtPPIyQDufunvLOQWF0' # encrypted password
+ entry[:sp_lstchg] = 15573 # date of last password change
+ entry[:sp_min] = 10 # minimum password age
+ entry[:sp_max] = 20 # maximum password age
+ entry[:sp_warn] = 7 # password warning period
+ entry[:sp_inact] = -1 # password inactivity period
+ entry[:sp_expire] = 15706 # account expiration date
+ entry
}
describe "#create" do
before do
provider.stubs(:exists?).returns(false)
end
it "should add -g when no gid is specified and group already exists" do
Puppet::Util.stubs(:gid).returns(true)
resource[:ensure] = :present
provider.expects(:execute).with(includes('-g'), kind_of(Hash))
provider.create
end
it "should add -o when allowdupe is enabled and the user is being created" do
resource[:allowdupe] = true
provider.expects(:execute).with(includes('-o'), kind_of(Hash))
provider.create
end
describe "on systems that support has_system", :if => described_class.system_users? do
it "should add -r when system is enabled" do
resource[:system] = :true
provider.should be_system_users
provider.expects(:execute).with(includes('-r'), kind_of(Hash))
provider.create
end
end
describe "on systems that do not support has_system", :unless => described_class.system_users? do
it "should not add -r when system is enabled" do
resource[:system] = :true
provider.should_not be_system_users
provider.expects(:execute).with(['/usr/sbin/useradd', 'myuser'], kind_of(Hash))
provider.create
end
end
it "should set password age rules" do
described_class.has_feature :manages_password_age
resource[:password_min_age] = 5
resource[:password_max_age] = 10
provider.expects(:execute).with(includes('/usr/sbin/useradd'), kind_of(Hash))
provider.expects(:execute).with(['/usr/bin/chage', '-m', 5, '-M', 10, 'myuser'])
provider.create
end
describe "on systems with the libuser and forcelocal=true" do
before do
described_class.has_feature :libuser
resource[:forcelocal] = true
end
it "should use luseradd instead of useradd" do
provider.expects(:execute).with(includes('/usr/sbin/luseradd'), has_entry(:custom_environment, has_key('LIBUSER_CONF')))
provider.create
end
-
+
it "should NOT use -o when allowdupe=true" do
- resource[:allowdupe] = :true
+ resource[:allowdupe] = :true
provider.expects(:execute).with(Not(includes('-o')), has_entry(:custom_environment, has_key('LIBUSER_CONF')))
provider.create
end
it "should raise an exception for duplicate UIDs" do
resource[:uid] = 505
provider.stubs(:finduser).returns(true)
lambda { provider.create }.should raise_error(Puppet::Error, "UID 505 already exists, use allowdupe to force user creation")
end
it "should not use -G for luseradd and should call usermod with -G after luseradd when groups property is set" do
resource[:groups] = ['group1', 'group2']
provider.expects(:execute).with(Not(includes("-G")), has_entry(:custom_environment, has_key('LIBUSER_CONF')))
provider.expects(:execute).with(includes('/usr/sbin/usermod'))
- provider.create
+ provider.create
end
it "should not use -m when managehome set" do
resource[:managehome] = :true
provider.expects(:execute).with(Not(includes('-m')), has_entry(:custom_environment, has_key('LIBUSER_CONF')))
- provider.create
+ provider.create
end
it "should not use -e with luseradd, should call usermod with -e after luseradd when expiry is set" do
resource[:expiry] = '2038-01-24'
provider.expects(:execute).with(all_of(includes('/usr/sbin/luseradd'), Not(includes('-e'))), has_entry(:custom_environment, has_key('LIBUSER_CONF')))
provider.expects(:execute).with(all_of(includes('/usr/sbin/usermod'), includes('-e')))
provider.create
end
+
+ it "should use userdel to delete users" do
+ resource[:ensure] = :absent
+ provider.stubs(:exists?).returns(true)
+ provider.expects(:execute).with(includes('/usr/sbin/userdel'))
+ provider.delete
+ end
+ end
+
+ describe "on systems that allow to set shell" do
+ it "should trigger shell validation" do
+ resource[:shell] = '/bin/bash'
+ provider.expects(:check_valid_shell)
+ provider.expects(:execute).with(includes('-s'), kind_of(Hash))
+ provider.create
+ end
end
end
describe "#uid=" do
it "should add -o when allowdupe is enabled and the uid is being modified" do
resource[:allowdupe] = :true
provider.expects(:execute).with(['/usr/sbin/usermod', '-u', 150, '-o', 'myuser'])
provider.uid = 150
end
end
describe "#expiry=" do
it "should pass expiry to usermod as MM/DD/YY when on Solaris" do
Facter.expects(:value).with(:operatingsystem).returns 'Solaris'
resource[:expiry] = '2012-10-31'
provider.expects(:execute).with(['/usr/sbin/usermod', '-e', '10/31/2012', 'myuser'])
provider.expiry = '2012-10-31'
end
it "should pass expiry to usermod as YYYY-MM-DD when not on Solaris" do
Facter.expects(:value).with(:operatingsystem).returns 'not_solaris'
resource[:expiry] = '2012-10-31'
provider.expects(:execute).with(['/usr/sbin/usermod', '-e', '2012-10-31', 'myuser'])
provider.expiry = '2012-10-31'
end
it "should use -e with an empty string when the expiry property is removed" do
resource[:expiry] = :absent
provider.expects(:execute).with(['/usr/sbin/usermod', '-e', '', 'myuser'])
provider.expiry = :absent
end
end
describe "#check_allow_dup" do
it "should return an array with a flag if dup is allowed" do
resource[:allowdupe] = :true
provider.check_allow_dup.must == ["-o"]
end
it "should return an empty array if no dup is allowed" do
resource[:allowdupe] = :false
provider.check_allow_dup.must == []
end
end
describe "#check_system_users" do
it "should check system users" do
described_class.expects(:system_users?).returns true
resource.expects(:system?)
provider.check_system_users
end
it "should return an array with a flag if it's a system user" do
described_class.expects(:system_users?).returns true
resource[:system] = :true
provider.check_system_users.must == ["-r"]
end
it "should return an empty array if it's not a system user" do
described_class.expects(:system_users?).returns true
resource[:system] = :false
provider.check_system_users.must == []
end
it "should return an empty array if system user is not featured" do
described_class.expects(:system_users?).returns false
resource[:system] = :true
provider.check_system_users.must == []
end
end
describe "#check_manage_home" do
it "should return an array with -m flag if home is managed" do
resource[:managehome] = :true
provider.expects(:execute).with(includes('-m'), kind_of(Hash))
provider.create
end
it "should return an array with -r flag if home is managed" do
resource[:managehome] = :true
resource[:ensure] = :absent
provider.stubs(:exists?).returns(true)
provider.expects(:execute).with(includes('-r'))
provider.delete
end
it "should use -M flag if home is not managed and on Redhat" do
Facter.stubs(:value).with(:osfamily).returns("RedHat")
resource[:managehome] = :false
provider.expects(:execute).with(includes('-M'), kind_of(Hash))
provider.create
end
it "should not use -M flag if home is not managed and not on Redhat" do
Facter.stubs(:value).with(:osfamily).returns("not RedHat")
resource[:managehome] = :false
provider.expects(:execute).with(Not(includes('-M')), kind_of(Hash))
provider.create
end
end
describe "#addcmd" do
before do
resource[:allowdupe] = :true
resource[:managehome] = :true
resource[:system] = :true
resource[:groups] = [ 'somegroup' ]
end
it "should call command with :add" do
provider.expects(:command).with(:add)
provider.addcmd
end
it "should add properties" do
provider.expects(:add_properties).returns(['-foo_add_properties'])
provider.addcmd.should include '-foo_add_properties'
end
it "should check and add if dup allowed" do
provider.expects(:check_allow_dup).returns(['-allow_dup_flag'])
provider.addcmd.should include '-allow_dup_flag'
end
it "should check and add if home is managed" do
provider.expects(:check_manage_home).returns(['-manage_home_flag'])
provider.addcmd.should include '-manage_home_flag'
end
it "should add the resource :name" do
provider.addcmd.should include 'myuser'
end
describe "on systems featuring system_users", :if => described_class.system_users? do
it "should return an array with -r if system? is true" do
resource[:system] = :true
provider.addcmd.should include("-r")
end
it "should return an array without -r if system? is false" do
resource[:system] = :false
provider.addcmd.should_not include("-r")
end
end
describe "on systems not featuring system_users", :unless => described_class.system_users? do
[:false, :true].each do |system|
it "should return an array without -r if system? is #{system}" do
resource[:system] = system
provider.addcmd.should_not include("-r")
end
end
end
it "should return an array with the full command and expiry as MM/DD/YY when on Solaris" do
Facter.stubs(:value).with(:operatingsystem).returns 'Solaris'
described_class.expects(:system_users?).returns true
resource[:expiry] = "2012-08-18"
provider.addcmd.must == ['/usr/sbin/useradd', '-e', '08/18/2012', '-G', 'somegroup', '-o', '-m', '-r', 'myuser']
end
it "should return an array with the full command and expiry as YYYY-MM-DD when not on Solaris" do
Facter.stubs(:value).with(:operatingsystem).returns 'not_solaris'
described_class.expects(:system_users?).returns true
resource[:expiry] = "2012-08-18"
provider.addcmd.must == ['/usr/sbin/useradd', '-e', '2012-08-18', '-G', 'somegroup', '-o', '-m', '-r', 'myuser']
end
it "should return an array without -e if expiry is undefined full command" do
described_class.expects(:system_users?).returns true
provider.addcmd.must == ["/usr/sbin/useradd", "-G", "somegroup", "-o", "-m", "-r", "myuser"]
end
it "should pass -e \"\" if the expiry has to be removed" do
described_class.expects(:system_users?).returns true
resource[:expiry] = :absent
provider.addcmd.must == ['/usr/sbin/useradd', '-e', '', '-G', 'somegroup', '-o', '-m', '-r', 'myuser']
end
end
{
:password_min_age => 10,
:password_max_age => 20,
:password => '$6$FvW8Ib8h$qQMI/CR9m.QzIicZKutLpBgCBBdrch1IX0rTnxuI32K1pD9.RXZrmeKQlaC.RzODNuoUtPPIyQDufunvLOQWF0'
}.each_pair do |property, expected_value|
describe "##{property}" do
before :each do
resource # just to link the resource to the provider
end
it "should return absent if libshadow feature is not present" do
Puppet.features.stubs(:libshadow?).returns false
# Shadow::Passwd.expects(:getspnam).never # if we really don't have libshadow we dont have Shadow::Passwd either
provider.send(property).should == :absent
end
it "should return absent if user cannot be found", :if => Puppet.features.libshadow? do
Shadow::Passwd.expects(:getspnam).with('myuser').returns nil
provider.send(property).should == :absent
end
it "should return the correct value if libshadow is present", :if => Puppet.features.libshadow? do
Shadow::Passwd.expects(:getspnam).with('myuser').returns shadow_entry
provider.send(property).should == expected_value
end
end
end
describe '#expiry' do
before :each do
resource # just to link the resource to the provider
end
it "should return absent if libshadow feature is not present" do
Puppet.features.stubs(:libshadow?).returns false
provider.expiry.should == :absent
end
it "should return absent if user cannot be found", :if => Puppet.features.libshadow? do
Shadow::Passwd.expects(:getspnam).with('myuser').returns nil
provider.expiry.should == :absent
end
it "should return absent if expiry is -1", :if => Puppet.features.libshadow? do
shadow_entry.sp_expire = -1
Shadow::Passwd.expects(:getspnam).with('myuser').returns shadow_entry
provider.expiry.should == :absent
end
it "should convert to YYYY-MM-DD", :if => Puppet.features.libshadow? do
Shadow::Passwd.expects(:getspnam).with('myuser').returns shadow_entry
provider.expiry.should == '2013-01-01'
end
end
describe "#passcmd" do
before do
resource[:allowdupe] = :true
resource[:managehome] = :true
resource[:system] = :true
described_class.has_feature :manages_password_age
end
it "should call command with :pass" do
# command(:password) is only called inside passcmd if
# password_min_age or password_max_age is set
resource[:password_min_age] = 123
provider.expects(:command).with(:password)
provider.passcmd
end
it "should return nil if neither min nor max is set" do
provider.passcmd.must be_nil
end
it "should return a chage command array with -m <value> and the user name if password_min_age is set" do
resource[:password_min_age] = 123
provider.passcmd.must == ['/usr/bin/chage','-m',123,'myuser']
end
it "should return a chage command array with -M <value> if password_max_age is set" do
resource[:password_max_age] = 999
provider.passcmd.must == ['/usr/bin/chage','-M',999,'myuser']
end
it "should return a chage command array with -M <value> -m <value> if both password_min_age and password_max_age are set" do
resource[:password_min_age] = 123
resource[:password_max_age] = 999
provider.passcmd.must == ['/usr/bin/chage','-m',123,'-M',999,'myuser']
end
end
+
+ describe "#check_valid_shell" do
+ it "should raise an error if shell does not exist" do
+ resource[:shell] = 'foo/bin/bash'
+ lambda { provider.check_valid_shell }.should raise_error(Puppet::Error, /Shell foo\/bin\/bash must exist/)
+ end
+
+ it "should raise an error if the shell is not executable" do
+ resource[:shell] = 'LICENSE'
+ lambda { provider.check_valid_shell }.should raise_error(Puppet::Error, /Shell LICENSE must be executable/)
+ end
+ end
+
end
diff --git a/spec/unit/provider/user/windows_adsi_spec.rb b/spec/unit/provider/user/windows_adsi_spec.rb
index c25ccaf95..8d3ed1d0a 100755
--- a/spec/unit/provider/user/windows_adsi_spec.rb
+++ b/spec/unit/provider/user/windows_adsi_spec.rb
@@ -1,172 +1,173 @@
#!/usr/bin/env ruby
require 'spec_helper'
describe Puppet::Type.type(:user).provider(:windows_adsi) do
let(:resource) do
Puppet::Type.type(:user).new(
:title => 'testuser',
:comment => 'Test J. User',
:provider => :windows_adsi
)
end
let(:provider) { resource.provider }
let(:connection) { stub 'connection' }
before :each do
Puppet::Util::ADSI.stubs(:computer_name).returns('testcomputername')
Puppet::Util::ADSI.stubs(:connect).returns connection
end
describe ".instances" 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 where localaccount = "TRUE"').returns(stub_users)
described_class.instances.map(&:name).should =~ names
end
end
it "should provide access to a Puppet::Util::ADSI::User object" do
provider.user.should be_a(Puppet::Util::ADSI::User)
end
describe "when managing groups" do
it 'should return the list of groups as a comma-separated list' do
provider.user.stubs(:groups).returns ['group1', 'group2', 'group3']
provider.groups.should == 'group1,group2,group3'
end
it "should return absent if there are no groups" do
provider.user.stubs(:groups).returns []
provider.groups.should == ''
end
it 'should be able to add a user to a set of groups' do
resource[:membership] = :minimum
provider.user.expects(:set_groups).with('group1,group2', true)
provider.groups = 'group1,group2'
resource[:membership] = :inclusive
provider.user.expects(:set_groups).with('group1,group2', false)
provider.groups = 'group1,group2'
end
end
describe "when creating a user" do
it "should create the user on the system and set its other properties" do
resource[:groups] = ['group1', 'group2']
resource[:membership] = :inclusive
resource[:comment] = 'a test user'
resource[:home] = 'C:\Users\testuser'
user = stub 'user'
Puppet::Util::ADSI::User.expects(:create).with('testuser').returns user
user.stubs(:groups).returns(['group2', 'group3'])
create = sequence('create')
user.expects(:password=).in_sequence(create)
user.expects(:commit).in_sequence(create)
user.expects(:set_groups).with('group1,group2', false).in_sequence(create)
user.expects(:[]=).with('Description', 'a test user')
user.expects(:[]=).with('HomeDirectory', 'C:\Users\testuser')
provider.create
end
it "should load the profile if managehome is set", :if => Puppet.features.microsoft_windows? do
resource[:password] = '0xDeadBeef'
resource[:managehome] = true
user = stub_everything 'user'
Puppet::Util::ADSI::User.expects(:create).with('testuser').returns user
Puppet::Util::Windows::User.expects(:load_profile).with('testuser', '0xDeadBeef')
provider.create
end
it "should set a user's password" do
provider.user.expects(:password=).with('plaintextbad')
provider.password = "plaintextbad"
end
it "should test a valid user password" do
resource[:password] = 'plaintext'
provider.user.expects(:password_is?).with('plaintext').returns true
provider.password.should == 'plaintext'
end
it "should test a bad user password" do
resource[:password] = 'plaintext'
provider.user.expects(:password_is?).with('plaintext').returns false
provider.password.should == :absent
end
it 'should not create a user if a group by the same name exists' do
Puppet::Util::ADSI::User.expects(:create).with('testuser').raises( Puppet::Error.new("Cannot create user if group 'testuser' exists.") )
expect{ provider.create }.to raise_error( Puppet::Error,
/Cannot create user if group 'testuser' exists./ )
end
end
it 'should be able to test whether a user exists' do
+ Puppet::Util::ADSI.stubs(:sid_uri_safe).returns(nil)
Puppet::Util::ADSI.stubs(:connect).returns stub('connection')
provider.should be_exists
Puppet::Util::ADSI.stubs(:connect).returns nil
provider.should_not be_exists
end
it 'should be able to delete a user' do
connection.expects(:Delete).with('user', 'testuser')
provider.delete
end
it 'should delete the profile if managehome is set', :if => Puppet.features.microsoft_windows? do
resource[:managehome] = true
sid = 'S-A-B-C'
Puppet::Util::Windows::Security.expects(:name_to_sid).with('testuser').returns(sid)
Puppet::Util::ADSI::UserProfile.expects(:delete).with(sid)
connection.expects(:Delete).with('user', 'testuser')
provider.delete
end
it "should commit the user when flushed" do
provider.user.expects(:commit)
provider.flush
end
it "should return the user's SID as uid", :if => Puppet.features.microsoft_windows? do
Puppet::Util::Windows::Security.expects(:name_to_sid).with('testuser').returns('S-1-5-21-1362942247-2130103807-3279964888-1111')
provider.uid.should == 'S-1-5-21-1362942247-2130103807-3279964888-1111'
end
it "should fail when trying to manage the uid property" do
provider.expects(:fail).with { |msg| msg =~ /uid is read-only/ }
provider.send(:uid=, 500)
end
[:gid, :shell].each do |prop|
it "should fail when trying to manage the #{prop} property" do
provider.expects(:fail).with { |msg| msg =~ /No support for managing property #{prop}/ }
provider.send("#{prop}=", 'foo')
end
end
end
diff --git a/spec/unit/provider/yumrepo/inifile_spec.rb b/spec/unit/provider/yumrepo/inifile_spec.rb
new file mode 100644
index 000000000..ef3beb8a1
--- /dev/null
+++ b/spec/unit/provider/yumrepo/inifile_spec.rb
@@ -0,0 +1,105 @@
+require 'spec_helper'
+
+describe Puppet::Type.type(:yumrepo).provider(:inifile) do
+
+ let(:virtual_inifile) { stub('virtual inifile') }
+
+ before :each do
+ described_class.stubs(:virtual_inifile).returns(virtual_inifile)
+ end
+
+ describe 'self.instances' do
+ let(:updates_section) do
+ stub('inifile updates section',
+ :name => 'updates',
+ :entries => {'name' => 'updates', 'enabled' => '1', 'descr' => 'test updates'})
+ end
+
+ it 'finds any existing sections' do
+ virtual_inifile.expects(:each_section).yields(updates_section)
+
+ providers = described_class.instances
+ providers.should have(1).items
+ providers[0].name.should == 'updates'
+ providers[0].enabled.should == '1'
+ end
+ end
+
+ describe "methods used by ensurable" do
+
+ let(:type) do
+ Puppet::Type.type(:yumrepo).new(
+ :name => 'puppetlabs-products',
+ :ensure => :present,
+ :baseurl => 'http://yum.puppetlabs.com/el/6/products/$basearch',
+ :descr => 'Puppet Labs Products El 6 - $basearch',
+ :enabled => '1',
+ :gpgcheck => '1',
+ :gpgkey => 'file:///etc/pki/rpm-gpg/RPM-GPG-KEY-puppetlabs'
+ )
+ end
+
+ let(:provider) { type.provider }
+
+ let(:puppetlabs_section) { stub('inifile puppetlabs section', :name => 'puppetlabs-products') }
+
+ it "#create sets the yumrepo properties on the according section" do
+ described_class.expects(:section).returns(puppetlabs_section)
+ puppetlabs_section.expects(:[]=).with('baseurl', 'http://yum.puppetlabs.com/el/6/products/$basearch')
+ puppetlabs_section.expects(:[]=).with('descr', 'Puppet Labs Products El 6 - $basearch')
+ puppetlabs_section.expects(:[]=).with('enabled', '1')
+ puppetlabs_section.expects(:[]=).with('gpgcheck', '1')
+ puppetlabs_section.expects(:[]=).with('gpgkey', 'file:///etc/pki/rpm-gpg/RPM-GPG-KEY-puppetlabs')
+
+ provider.create
+ end
+
+ it "#exists? checks if the repo has been marked as present" do
+ described_class.stubs(:section).returns(stub(:[]= => nil))
+ provider.create
+ expect(provider).to be_exist
+ end
+
+ it "#destroy deletes the associated ini file section" do
+ described_class.expects(:section).returns(puppetlabs_section)
+ puppetlabs_section.expects(:destroy=).with(true)
+ provider.destroy
+ end
+ end
+
+ describe 'reposdir' do
+ let(:defaults) { ['/etc/yum.repos.d', '/etc/yum/repos.d'] }
+
+ before do
+ Puppet::FileSystem.stubs(:exist?).with('/etc/yum.repos.d').returns(true)
+ Puppet::FileSystem.stubs(:exist?).with('/etc/yum/repos.d').returns(true)
+ end
+
+ it "returns the default directories if yum.conf doesn't contain a `reposdir` entry" do
+ described_class.stubs(:find_conf_value).with('reposdir', '/etc/yum.conf')
+ described_class.reposdir('/etc/yum.conf').should == defaults
+ end
+
+ it "includes the directory specified by the yum.conf 'reposdir' entry when the directory is present" do
+ Puppet::FileSystem.expects(:exist?).with("/etc/yum/extra.repos.d").returns(true)
+
+ described_class.expects(:find_conf_value).with('reposdir', '/etc/yum.conf').returns "/etc/yum/extra.repos.d"
+ described_class.reposdir('/etc/yum.conf').should include("/etc/yum/extra.repos.d")
+ end
+
+ it "doesn't the directory specified by the yum.conf 'reposdir' entry when the directory is absent" do
+ Puppet::FileSystem.expects(:exist?).with("/etc/yum/extra.repos.d").returns(false)
+
+ described_class.expects(:find_conf_value).with('reposdir', '/etc/yum.conf').returns "/etc/yum/extra.repos.d"
+ described_class.reposdir('/etc/yum.conf').should_not include("/etc/yum/extra.repos.d")
+ end
+
+ it "raises an entry if none of the specified repo directories exist" do
+ Puppet::FileSystem.unstub(:exist?)
+ Puppet::FileSystem.stubs(:exist?).returns false
+
+ described_class.stubs(:find_conf_value).with('reposdir', '/etc/yum.conf')
+ expect { described_class.reposdir('/etc/yum.conf') }.to raise_error('No yum directories were found on the local filesystem')
+ end
+ end
+end
diff --git a/spec/unit/provider/zone/solaris_spec.rb b/spec/unit/provider/zone/solaris_spec.rb
index c143cabd1..393df6b51 100755
--- a/spec/unit/provider/zone/solaris_spec.rb
+++ b/spec/unit/provider/zone/solaris_spec.rb
@@ -1,195 +1,195 @@
#! /usr/bin/env ruby
require 'spec_helper'
describe Puppet::Type.type(:zone).provider(:solaris) do
let(:resource) { Puppet::Type.type(:zone).new(:name => 'dummy', :path => '/', :provider => :solaris) }
let(:provider) { described_class.new(resource) }
context "#configure" do
it "should add the create args to the create str" do
resource.stubs(:properties).returns([])
resource[:create_args] = "create_args"
provider.expects(:setconfig).with("create -b create_args")
provider.configure
end
it "should add the create args to the create str" do
iptype = stub "property"
iptype.stubs(:name).with().returns(:iptype)
iptype.stubs(:safe_insync?).with(iptype).returns(false)
provider.stubs(:properties).returns({:iptype => iptype})
resource.stubs(:properties).with().returns([iptype])
resource[:create_args] = "create_args"
provider.expects(:setconfig).with("create -b create_args\nset ip-type=shared")
provider.configure
end
end
context "#install" do
context "clone" do
it "should call zoneadm" do
provider.expects(:zoneadm).with(:install)
provider.install
end
it "with the resource's clone attribute" do
resource[:clone] = :clone_argument
provider.expects(:zoneadm).with(:clone, :clone_argument)
provider.install
end
end
context "not clone" do
it "should just install if there are no install args" do
# there is a nil check in type.rb:[]= so we cannot directly set nil.
resource.stubs(:[]).with(:clone).returns(nil)
resource.stubs(:[]).with(:install_args).returns(nil)
provider.expects(:zoneadm).with(:install)
provider.install
end
it "should add the install args to the command if they exist" do
# there is a nil check in type.rb:[]= so we cannot directly set nil.
resource.stubs(:[]).with(:clone).returns(nil)
resource.stubs(:[]).with(:install_args).returns('install args')
provider.expects(:zoneadm).with(:install, ["install", "args"])
provider.install
end
end
end
context "#instances" do
it "should list the instances correctly" do
described_class.expects(:adm).with(:list, "-cp").returns("0:dummy:running:/::native:shared")
instances = described_class.instances.map { |p| {:name => p.get(:name), :ensure => p.get(:ensure)} }
instances.size.should == 1
instances[0].should == {
:name=>"dummy",
:ensure=>:running,
}
end
end
context "#setconfig" do
it "should correctly set configuration" do
provider.expects(:command).with(:cfg).returns('/usr/sbin/zonecfg')
provider.expects(:exec_cmd).with(:input => "set zonepath=/\ncommit\nexit", :cmd => '/usr/sbin/zonecfg -z dummy -f -').returns({:out=>'', :exit => 0})
provider.setconfig("set zonepath=\/")
provider.flush
end
it "should correctly warn on 'not allowed'" do
provider.expects(:command).with(:cfg).returns('/usr/sbin/zonecfg')
provider.expects(:exec_cmd).with(:input => "set zonepath=/\ncommit\nexit", :cmd => '/usr/sbin/zonecfg -z dummy -f -').returns({:out=>"Zone z2 already installed; set zonepath not allowed.\n", :exit => 0})
provider.setconfig("set zonepath=\/")
expect {
provider.flush
}.to raise_error(ArgumentError, /Failed to apply configuration/)
end
end
context "#getconfig" do
zone_info =<<-EOF
zonename: dummy
zonepath: /dummy/z
brand: native
autoboot: true
bootargs:
pool:
limitpriv:
scheduling-class:
ip-type: shared
hostid:
net:
address: 1.1.1.1
physical: ex0001
defrouter not specified
net:
address: 1.1.1.2
physical: ex0002
defrouter not specified
EOF
it "should correctly parse zone info" do
provider.expects(:zonecfg).with(:info).returns(zone_info)
provider.getconfig.should == {
:brand=>"native",
:autoboot=>"true",
:"ip-type"=>"shared",
:zonename=>"dummy",
"net"=>[{:physical=>"ex0001", :address=>"1.1.1.1"}, {:physical=>"ex0002", :address=>"1.1.1.2"}],
:zonepath=>"/dummy/z"
}
end
end
context "#flush" do
it "should correctly execute pending commands" do
provider.expects(:command).with(:cfg).returns('/usr/sbin/zonecfg')
provider.expects(:exec_cmd).with(:input => "set iptype=shared\ncommit\nexit", :cmd => '/usr/sbin/zonecfg -z dummy -f -').returns({:out=>'', :exit => 0})
provider.setconfig("set iptype=shared")
provider.flush
end
it "should correctly raise error on failure" do
provider.expects(:command).with(:cfg).returns('/usr/sbin/zonecfg')
provider.expects(:exec_cmd).with(:input => "set iptype=shared\ncommit\nexit", :cmd => '/usr/sbin/zonecfg -z dummy -f -').returns({:out=>'', :exit => 1})
provider.setconfig("set iptype=shared")
expect {
provider.flush
}.to raise_error(ArgumentError, /Failed to apply/)
end
end
context "#start" do
it "should not require path if sysidcfg is specified" do
resource[:path] = '/mypath'
resource[:sysidcfg] = 'dummy'
- Puppet::FileSystem::File.stubs(:exist?).with('/mypath/root/etc/sysidcfg').returns true
+ Puppet::FileSystem.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
end
it "should require path if sysidcfg is specified" do
resource.stubs(:[]).with(:path).returns nil
resource.stubs(:[]).with(:sysidcfg).returns 'dummy'
expect {
provider.start
}.to raise_error(Puppet::Error, /Path is required/)
end
end
context "#line2hash" do
it "should parse lines correctly" do
described_class.line2hash('0:dummy:running:/z::native:shared').should == {:ensure=>:running, :iptype=>"shared", :path=>"/z", :name=>"dummy", :id=>"0"}
end
it "should parse lines correctly(2)" do
described_class.line2hash('0:dummy:running:/z:ipkg:native:shared').should == {:ensure=>:running, :iptype=>"shared", :path=>"/z", :name=>"dummy", :id=>"0"}
end
it "should parse lines correctly(3)" do
described_class.line2hash('-:dummy:running:/z:ipkg:native:shared').should == {:ensure=>:running, :iptype=>"shared", :path=>"/z", :name=>"dummy"}
end
it "should parse lines correctly(3)" do
described_class.line2hash('-:dummy:running:/z:ipkg:native:exclusive').should == {:ensure=>:running, :iptype=>"exclusive", :path=>"/z", :name=>"dummy"}
end
end
context "#multi_conf" do
it "should correctly add and remove properties" do
provider.stubs(:properties).with().returns({:ip => ['1.1.1.1', '2.2.2.2']})
should = ['1.1.1.1', '3.3.3.3']
p = Proc.new do |a, str|
case a
when :add; 'add:' + str
when :rm; 'rm:' + str
end
end
provider.multi_conf(:ip, should, &p).should == "rm:2.2.2.2\nadd:3.3.3.3"
end
end
context "single props" do
{:iptype => /set ip-type/, :autoboot => /set autoboot/, :path => /set zonepath/, :pool => /set pool/, :shares => /add rctl/}.each do |p, v|
it "#{p.to_s}: should correctly return conf string" do
provider.send(p.to_s + '_conf', 'dummy').should =~ v
end
it "#{p.to_s}: should correctly set property string" do
provider.expects((p.to_s + '_conf').intern).returns('dummy')
provider.expects(:setconfig).with('dummy').returns('dummy2')
provider.send(p.to_s + '=', 'dummy').should == 'dummy2'
end
end
end
end
diff --git a/spec/unit/provider_spec.rb b/spec/unit/provider_spec.rb
index 31e69f799..cd673df81 100755
--- a/spec/unit/provider_spec.rb
+++ b/spec/unit/provider_spec.rb
@@ -1,656 +1,714 @@
#! /usr/bin/env ruby
require 'spec_helper'
def existing_command
Puppet.features.microsoft_windows? ? "cmd" : "echo"
end
describe Puppet::Provider do
before :each do
Puppet::Type.newtype(:test) do
newparam(:name) { isnamevar }
end
end
after :each do
Puppet::Type.rmtype(:test)
end
let :type do Puppet::Type.type(:test) end
let :provider do type.provide(:default) {} end
subject { provider }
describe "has command" do
it "installs a method to run the command specified by the path" do
echo_command = expect_command_executed(:echo, "/bin/echo", "an argument")
allow_creation_of(echo_command)
provider = provider_of do
has_command(:echo, "/bin/echo")
end
provider.echo("an argument")
end
it "installs a command that is run with a given environment" do
echo_command = expect_command_executed(:echo, "/bin/echo", "an argument")
allow_creation_of(echo_command, {
:EV => "value",
:OTHER => "different"
})
provider = provider_of do
has_command(:echo, "/bin/echo") do
environment :EV => "value", :OTHER => "different"
end
end
provider.echo("an argument")
end
it "is required by default" do
provider = provider_of do
has_command(:does_not_exist, "/does/not/exist")
end
provider.should_not be_suitable
end
it "is required by default" do
provider = provider_of do
has_command(:does_exist, File.expand_path("/exists/somewhere"))
end
file_exists_and_is_executable(File.expand_path("/exists/somewhere"))
provider.should be_suitable
end
it "can be specified as optional" do
provider = provider_of do
has_command(:does_not_exist, "/does/not/exist") do
is_optional
end
end
provider.should be_suitable
end
end
describe "has required commands" do
it "installs methods to run executables by path" do
echo_command = expect_command_executed(:echo, "/bin/echo", "an argument")
ls_command = expect_command_executed(:ls, "/bin/ls")
allow_creation_of(echo_command)
allow_creation_of(ls_command)
provider = provider_of do
commands :echo => "/bin/echo", :ls => "/bin/ls"
end
provider.echo("an argument")
provider.ls
end
it "allows the provider to be suitable if the executable is present" do
provider = provider_of do
commands :always_exists => File.expand_path("/this/command/exists")
end
file_exists_and_is_executable(File.expand_path("/this/command/exists"))
provider.should be_suitable
end
it "does not allow the provider to be suitable if the executable is not present" do
provider = provider_of do
commands :does_not_exist => "/this/command/does/not/exist"
end
provider.should_not be_suitable
end
end
describe "has optional commands" do
it "installs methods to run executables" do
echo_command = expect_command_executed(:echo, "/bin/echo", "an argument")
ls_command = expect_command_executed(:ls, "/bin/ls")
allow_creation_of(echo_command)
allow_creation_of(ls_command)
provider = provider_of do
optional_commands :echo => "/bin/echo", :ls => "/bin/ls"
end
provider.echo("an argument")
provider.ls
end
it "allows the provider to be suitable even if the executable is not present" do
provider = provider_of do
optional_commands :does_not_exist => "/this/command/does/not/exist"
end
provider.should be_suitable
end
end
it "makes command methods on demand (deprecated)" do
Puppet::Util.expects(:which).with("/not/a/command").returns("/not/a/command")
Puppet::Util::Execution.expects(:execute).with(["/not/a/command"], {})
provider = provider_of do
@commands[:echo] = "/not/a/command"
end
provider.stubs(:which).with("/not/a/command").returns("/not/a/command")
provider.make_command_methods(:echo)
provider.echo
end
it "should have a specifity class method" do
Puppet::Provider.should respond_to(:specificity)
end
- it "should consider two defaults to be higher specificity than one default" do
- one = provider_of do
- defaultfor :osfamily => "solaris"
- end
-
- two = provider_of do
- defaultfor :osfamily => "solaris", :operatingsystemrelease => "5.10"
- end
-
- two.specificity.should > one.specificity
- end
-
-
it "should be Comparable" do
res = Puppet::Type.type(:notify).new(:name => "res")
# Normally I wouldn't like the stubs, but the only way to name a class
# otherwise is to assign it to a constant, and that hurts more here in
# testing world. --daniel 2012-01-29
a = Class.new(Puppet::Provider).new(res)
a.class.stubs(:name).returns "Puppet::Provider::Notify::A"
b = Class.new(Puppet::Provider).new(res)
b.class.stubs(:name).returns "Puppet::Provider::Notify::B"
c = Class.new(Puppet::Provider).new(res)
c.class.stubs(:name).returns "Puppet::Provider::Notify::C"
[[a, b, c], [a, c, b], [b, a, c], [b, c, a], [c, a, b], [c, b, a]].each do |this|
this.sort.should == [a, b, c]
end
a.should be < b
a.should be < c
b.should be > a
b.should be < c
c.should be > a
c.should be > b
[a, b, c].each {|x| a.should be <= x }
[a, b, c].each {|x| c.should be >= x }
b.should be_between(a, c)
end
context "when creating instances" do
context "with a resource" do
let :resource do type.new(:name => "fred") end
subject { provider.new(resource) }
it "should set the resource correctly" do
subject.resource.must equal resource
end
it "should set the name from the resource" do
subject.name.should == resource.name
end
end
context "with a hash" do
subject { provider.new(:name => "fred") }
it "should set the name" do
subject.name.should == "fred"
end
it "should not have a resource" do subject.resource.should be_nil end
end
context "with no arguments" do
subject { provider.new }
it "should raise an internal error if asked for the name" do
expect { subject.name }.to raise_error Puppet::DevError
end
it "should not have a resource" do subject.resource.should be_nil end
end
end
context "when confining" do
it "should be suitable by default" do
subject.should be_suitable
end
it "should not be default by default" do
subject.should_not be_default
end
{ { :true => true } => true,
{ :true => false } => false,
{ :false => false } => true,
{ :false => true } => false,
{ :operatingsystem => Facter.value(:operatingsystem) } => true,
{ :operatingsystem => :yayness } => false,
{ :nothing => :yayness } => false,
{ :exists => Puppet::Util.which(existing_command) } => true,
{ :exists => "/this/file/does/not/exist" } => false,
{ :true => true, :exists => Puppet::Util.which(existing_command) } => true,
{ :true => true, :exists => "/this/file/does/not/exist" } => false,
{ :operatingsystem => Facter.value(:operatingsystem),
:exists => Puppet::Util.which(existing_command) } => true,
{ :operatingsystem => :yayness,
:exists => Puppet::Util.which(existing_command) } => false,
{ :operatingsystem => Facter.value(:operatingsystem),
:exists => "/this/file/does/not/exist" } => false,
{ :operatingsystem => :yayness,
:exists => "/this/file/does/not/exist" } => false,
}.each do |confines, result|
it "should confine #{confines.inspect} to #{result}" do
confines.each {|test, value| subject.confine test => value }
subject.send(result ? :should : :should_not, be_suitable)
end
end
it "should not override a confine even if a second has the same type" do
subject.confine :true => false
subject.should_not be_suitable
subject.confine :true => true
subject.should_not be_suitable
end
it "should not be suitable if any confine fails" do
subject.confine :true => false
subject.should_not be_suitable
10.times do
subject.confine :true => true
subject.should_not be_suitable
end
end
end
context "default providers" do
let :os do Facter.value(:operatingsystem) end
it { should respond_to :specificity }
it "should find the default provider" do
type.provide(:nondefault) {}
subject.defaultfor :operatingsystem => os
subject.name.should == type.defaultprovider.name
end
+ describe "when there are multiple defaultfor's of equal specificity" do
+ before :each do
+ subject.defaultfor :operatingsystem => :os1
+ subject.defaultfor :operatingsystem => :os2
+ end
+
+ let(:alternate) { type.provide(:alternate) {} }
+
+ it "should be default for the first defaultfor" do
+ Facter.expects(:value).with(:operatingsystem).at_least_once.returns :os1
+
+ provider.should be_default
+ alternate.should_not be_default
+ end
+
+ it "should be default for the last defaultfor" do
+ Facter.expects(:value).with(:operatingsystem).at_least_once.returns :os2
+
+ provider.should be_default
+ alternate.should_not be_default
+ end
+ end
+
+ describe "when there are multiple defaultfor's with different specificity" do
+ before :each do
+ subject.defaultfor :operatingsystem => :os1
+ subject.defaultfor :operatingsystem => :os2, :operatingsystemmajrelease => "42"
+ end
+
+ let(:alternate) { type.provide(:alternate) {} }
+
+ it "should be default for a more specific, but matching, defaultfor" do
+ Facter.expects(:value).with(:operatingsystem).at_least_once.returns :os2
+ Facter.expects(:value).with(:operatingsystemmajrelease).at_least_once.returns "42"
+
+ provider.should be_default
+ alternate.should_not be_default
+ end
+
+ it "should be default for a less specific, but matching, defaultfor" do
+ Facter.expects(:value).with(:operatingsystem).at_least_once.returns :os1
+
+ provider.should be_default
+ alternate.should_not be_default
+ end
+ end
+
it "should consider any true value enough to be default" do
alternate = type.provide(:alternate) {}
subject.defaultfor :operatingsystem => [:one, :two, :three, os]
subject.name.should == type.defaultprovider.name
subject.should be_default
alternate.should_not be_default
end
- it "should not be default if the confine doesn't match" do
+ it "should not be default if the defaultfor doesn't match" do
subject.should_not be_default
subject.defaultfor :operatingsystem => :one
subject.should_not be_default
end
it "should consider two defaults to be higher specificity than one default" do
+ Facter.expects(:value).with(:osfamily).at_least_once.returns "solaris"
+ Facter.expects(:value).with(:operatingsystemrelease).at_least_once.returns "5.10"
+
one = type.provide(:one) do
defaultfor :osfamily => "solaris"
end
two = type.provide(:two) do
defaultfor :osfamily => "solaris", :operatingsystemrelease => "5.10"
end
two.specificity.should > one.specificity
end
it "should consider a subclass more specific than its parent class" do
parent = type.provide(:parent)
child = type.provide(:child, :parent => parent)
child.specificity.should > parent.specificity
end
+
+ describe "using a :feature key" do
+ before :each do
+ Puppet.features.add(:yay) do true end
+ Puppet.features.add(:boo) do false end
+ end
+
+ it "is default for an available feature" do
+ one = type.provide(:one) do
+ defaultfor :feature => :yay
+ end
+
+ one.should be_default
+ end
+
+ it "is not default for a missing feature" do
+ two = type.provide(:two) do
+ defaultfor :feature => :boo
+ end
+
+ two.should_not be_default
+ end
+ end
end
context "provider commands" do
it "should raise for unknown commands" do
- expect { subject.command(:something) }.to raise_error Puppet::DevError
+ expect { subject.command(:something) }.to raise_error(Puppet::DevError)
end
it "should handle command inheritance" do
parent = type.provide("parent")
child = type.provide("child", :parent => parent.name)
command = Puppet::Util.which('sh') || Puppet::Util.which('cmd.exe')
parent.commands :sh => command
- Puppet::FileSystem::File.exist?(parent.command(:sh)).should be_true
+ Puppet::FileSystem.exist?(parent.command(:sh)).should be_true
parent.command(:sh).should =~ /#{Regexp.escape(command)}$/
- Puppet::FileSystem::File.exist?(child.command(:sh)).should be_true
+ Puppet::FileSystem.exist?(child.command(:sh)).should be_true
child.command(:sh).should =~ /#{Regexp.escape(command)}$/
end
it "#1197: should find commands added in the same run" do
subject.commands :testing => "puppet-bug-1197"
subject.command(:testing).should be_nil
subject.stubs(:which).with("puppet-bug-1197").returns("/puppet-bug-1197")
subject.command(:testing).should == "/puppet-bug-1197"
# Ideally, we would also test that `suitable?` returned the right thing
# here, but it is impossible to get access to the methods that do that
# without digging way down into the implementation. --daniel 2012-03-20
end
context "with optional commands" do
before :each do
subject.optional_commands :cmd => "/no/such/binary/exists"
end
it { should be_suitable }
it "should not be suitable if a mandatory command is also missing" do
subject.commands :foo => "/no/such/binary/either"
subject.should_not be_suitable
end
it "should define a wrapper for the command" do
- subject.should respond_to :cmd
+ subject.should respond_to(:cmd)
end
it "should return nil if the command is requested" do
subject.command(:cmd).should be_nil
end
it "should raise if the command is invoked" do
- expect { subject.cmd }.to raise_error Puppet::Error, /Command cmd is missing/
+ expect { subject.cmd }.to raise_error(Puppet::Error, /Command cmd is missing/)
end
end
end
context "execution" do
before :each do
Puppet.expects(:deprecation_warning).never
end
it "delegates instance execute to Puppet::Util::Execution" do
Puppet::Util::Execution.expects(:execute).with("a_command", { :option => "value" })
provider.new.send(:execute, "a_command", { :option => "value" })
end
it "delegates class execute to Puppet::Util::Execution" do
Puppet::Util::Execution.expects(:execute).with("a_command", { :option => "value" })
provider.send(:execute, "a_command", { :option => "value" })
end
it "delegates instance execpipe to Puppet::Util::Execution" do
block = Proc.new { }
Puppet::Util::Execution.expects(:execpipe).with("a_command", true, block)
provider.new.send(:execpipe, "a_command", true, block)
end
it "delegates class execpipe to Puppet::Util::Execution" do
block = Proc.new { }
Puppet::Util::Execution.expects(:execpipe).with("a_command", true, block)
provider.send(:execpipe, "a_command", true, block)
end
it "delegates instance execfail to Puppet::Util::Execution" do
Puppet::Util::Execution.expects(:execfail).with("a_command", "an exception to raise")
provider.new.send(:execfail, "a_command", "an exception to raise")
end
it "delegates class execfail to Puppet::Util::Execution" do
Puppet::Util::Execution.expects(:execfail).with("a_command", "an exception to raise")
provider.send(:execfail, "a_command", "an exception to raise")
end
end
context "mk_resource_methods" do
before :each do
- type.newproperty(:prop1)
- type.newproperty(:prop2)
- type.newparam(:param1)
- type.newparam(:param2)
+ type.newproperty(:prop)
+ type.newparam(:param)
+ provider.mk_resource_methods
end
- fields = %w{prop1 prop2 param1 param2}
+ let(:instance) { provider.new(nil) }
- fields.each do |name|
- it "should add getter methods for #{name}" do
- expect { subject.mk_resource_methods }.
- to change { subject.method_defined?(name) }.
- from(false).to(true)
- end
+ it "defaults to :absent" do
+ expect(instance.prop).to eq(:absent)
+ expect(instance.param).to eq(:absent)
+ end
- it "should add setter methods for #{name}" do
- method = name + '='
- expect { subject.mk_resource_methods }.
- to change { subject.method_defined?(name) }.
- from(false).to(true)
- end
+ it "should update when set" do
+ instance.prop = 'hello'
+ instance.param = 'goodbye'
+
+ expect(instance.prop).to eq('hello')
+ expect(instance.param).to eq('goodbye')
end
- context "with an instance" do
- subject { provider.mk_resource_methods; provider.new(nil) }
+ it "treats nil the same as absent" do
+ instance.prop = "value"
+ instance.param = "value"
- fields.each do |name|
- context name do
- it "should default to :absent" do
- subject.send(name).should == :absent
- end
+ instance.prop = nil
+ instance.param = nil
- it "should update when set" do
- expect { subject.send(name + '=', "hello") }.
- to change { subject.send(name) }.
- from(:absent).to("hello")
- end
- end
- end
+ expect(instance.prop).to eq(:absent)
+ expect(instance.param).to eq(:absent)
+ end
+
+ it "preserves false as false" do
+ instance.prop = false
+ instance.param = false
+
+ expect(instance.prop).to eq(false)
+ expect(instance.param).to eq(false)
end
end
context "source" do
it "should default to the provider name" do
subject.source.should == :default
end
it "should default to the provider name for a child provider" do
type.provide(:sub, :parent => subject.name).source.should == :sub
end
it "should override if requested" do
provider = type.provide(:sub, :parent => subject.name, :source => subject.source)
provider.source.should == subject.source
end
it "should override to anything you want" do
expect { subject.source = :banana }.to change { subject.source }.
from(:default).to(:banana)
end
end
context "features" do
before :each do
type.feature :numeric, '', :methods => [:one, :two]
type.feature :alpha, '', :methods => [:a, :b]
type.feature :nomethods, ''
end
{ :no => { :alpha => false, :numeric => false, :methods => [] },
:numeric => { :alpha => false, :numeric => true, :methods => [:one, :two] },
:alpha => { :alpha => true, :numeric => false, :methods => [:a, :b] },
:all => {
:alpha => true, :numeric => true,
:methods => [:a, :b, :one, :two]
},
:alpha_and_partial => {
:alpha => true, :numeric => false,
:methods => [:a, :b, :one]
},
:numeric_and_partial => {
:alpha => false, :numeric => true,
:methods => [:a, :one, :two]
},
:all_partial => { :alpha => false, :numeric => false, :methods => [:a, :one] },
:other_and_none => { :alpha => false, :numeric => false, :methods => [:foo, :bar] },
:other_and_alpha => {
:alpha => true, :numeric => false,
:methods => [:foo, :bar, :a, :b]
},
}.each do |name, setup|
context "with #{name.to_s.gsub('_', ' ')} features" do
let :provider do
provider = type.provide(name)
setup[:methods].map do |method|
provider.send(:define_method, method) do true end
end
type.provider(name)
end
let :numeric? do setup[:numeric] ? :should : :should_not end
let :alpha? do setup[:alpha] ? :should : :should_not end
subject { provider }
- it { should respond_to :has_features }
- it { should respond_to :has_feature }
+ it { should respond_to(:has_features) }
+ it { should respond_to(:has_feature) }
context "provider class" do
- it { should respond_to :nomethods? }
+ it { should respond_to(:nomethods?) }
it { should_not be_nomethods }
- it { should respond_to :numeric? }
+ it { should respond_to(:numeric?) }
it { subject.send(numeric?, be_numeric) }
it { subject.send(numeric?, be_satisfies(:numeric)) }
- it { should respond_to :alpha? }
+ it { should respond_to(:alpha?) }
it { subject.send(alpha?, be_alpha) }
it { subject.send(alpha?, be_satisfies(:alpha)) }
end
context "provider instance" do
subject { provider.new }
- it { should respond_to :numeric? }
+ it { should respond_to(:numeric?) }
it { subject.send(numeric?, be_numeric) }
it { subject.send(numeric?, be_satisfies(:numeric)) }
- it { should respond_to :alpha? }
+ it { should respond_to(:alpha?) }
it { subject.send(alpha?, be_alpha) }
it { subject.send(alpha?, be_satisfies(:alpha)) }
end
end
end
context "feature with no methods" do
before :each do
type.feature :undemanding, ''
end
- it { should respond_to :undemanding? }
+ it { should respond_to(:undemanding?) }
context "when the feature is not declared" do
it { should_not be_undemanding }
- it { should_not be_satisfies :undemanding }
+ it { should_not be_satisfies(:undemanding) }
end
context "when the feature is declared" do
before :each do
subject.has_feature :undemanding
end
it { should be_undemanding }
- it { should be_satisfies :undemanding }
+ it { should be_satisfies(:undemanding) }
end
end
context "supports_parameter?" do
before :each do
type.newparam(:no_feature)
type.newparam(:one_feature, :required_features => :alpha)
type.newparam(:two_features, :required_features => [:alpha, :numeric])
end
let :providers do
{
:zero => type.provide(:zero),
:one => type.provide(:one) do has_features :alpha end,
:two => type.provide(:two) do has_features :alpha, :numeric end
}
end
{ :zero => { :yes => [:no_feature], :no => [:one_feature, :two_features] },
:one => { :yes => [:no_feature, :one_feature], :no => [:two_features] },
:two => { :yes => [:no_feature, :one_feature, :two_features], :no => [] }
}.each do |name, data|
data[:yes].each do |param|
it "should support #{param} with provider #{name}" do
- providers[name].should be_supports_parameter param
+ providers[name].should be_supports_parameter(param)
end
end
data[:no].each do |param|
it "should not support #{param} with provider #{name}" do
- providers[name].should_not be_supports_parameter param
+ providers[name].should_not be_supports_parameter(param)
end
end
end
end
end
def provider_of(options = {}, &block)
type = Puppet::Type.newtype(:dummy) do
provide(:dummy, options, &block)
end
type.provider(:dummy)
end
def expect_command_executed(name, path, *args)
command = Puppet::Provider::Command.new(name, path, Puppet::Util, Puppet::Util::Execution)
command.expects(:execute).with(*args)
command
end
def allow_creation_of(command, environment = {})
Puppet::Provider::Command.stubs(:new).with(command.name, command.executable, Puppet::Util, Puppet::Util::Execution, { :failonfail => true, :combine => true, :custom_environment => environment }).returns(command)
end
def file_exists_and_is_executable(path)
FileTest.expects(:file?).with(path).returns(true)
FileTest.expects(:executable?).with(path).returns(true)
end
end
diff --git a/spec/unit/rails/host_spec.rb b/spec/unit/rails/host_spec.rb
index 81d048ef1..746c3a075 100755
--- a/spec/unit/rails/host_spec.rb
+++ b/spec/unit/rails/host_spec.rb
@@ -1,162 +1,162 @@
#! /usr/bin/env ruby
require 'spec_helper'
require 'puppet/node/environment'
describe "Puppet::Rails::Host", :if => can_use_scratch_database? do
before do
require 'puppet/rails/host'
setup_scratch_database
@node = Puppet::Node.new("foo")
@node.environment = "production"
@node.ipaddress = "127.0.0.1"
@host = stub 'host', :environment= => nil, :ip= => nil
end
describe "when converting a Puppet::Node instance into a Rails instance" do
it "should modify any existing instance in the database" do
Puppet::Rails::Host.expects(:find_by_name).with("foo").returns @host
Puppet::Rails::Host.from_puppet(@node)
end
it "should create a new instance in the database if none can be found" do
Puppet::Rails::Host.expects(:find_by_name).with("foo").returns nil
Puppet::Rails::Host.expects(:new).with(:name => "foo").returns @host
Puppet::Rails::Host.from_puppet(@node)
end
it "should copy the environment from the Puppet instance" do
Puppet::Rails::Host.expects(:find_by_name).with("foo").returns @host
@node.environment = "production"
@host.expects(:environment=).with {|x| x.name.to_s == 'production' }
Puppet::Rails::Host.from_puppet(@node)
end
it "should stringify the environment" do
host = Puppet::Rails::Host.new
- host.environment = Puppet::Node::Environment.new("production")
+ host.environment = Puppet::Node::Environment.create(:production, [], '')
host.environment.class.should == String
end
it "should copy the ipaddress from the Puppet instance" do
Puppet::Rails::Host.expects(:find_by_name).with("foo").returns @host
@node.ipaddress = "192.168.0.1"
@host.expects(:ip=).with "192.168.0.1"
Puppet::Rails::Host.from_puppet(@node)
end
it "should not save the Rails instance" do
Puppet::Rails::Host.expects(:find_by_name).with("foo").returns @host
@host.expects(:save).never
Puppet::Rails::Host.from_puppet(@node)
end
end
describe "when converting a Puppet::Rails::Host instance into a Puppet::Node instance" do
before do
@host = Puppet::Rails::Host.new(:name => "foo", :environment => "production", :ip => "127.0.0.1")
@node = Puppet::Node.new("foo")
Puppet::Node.stubs(:new).with("foo").returns @node
end
it "should create a new instance with the correct name" do
Puppet::Node.expects(:new).with("foo").returns @node
@host.to_puppet
end
it "should copy the environment from the Rails instance" do
@host.environment = "prod"
@node.expects(:environment=).with "prod"
@host.to_puppet
end
it "should copy the ipaddress from the Rails instance" do
@host.ip = "192.168.0.1"
@node.expects(:ipaddress=).with "192.168.0.1"
@host.to_puppet
end
end
describe "when merging catalog resources and database resources" do
before :each do
Puppet[:thin_storeconfigs] = false
@resource1 = stub_everything 'res1'
@resource2 = stub_everything 'res2'
@resources = [ @resource1, @resource2 ]
@dbresource1 = stub_everything 'dbres1'
@dbresource2 = stub_everything 'dbres2'
@dbresources = { 1 => @dbresource1, 2 => @dbresource2 }
@host = Puppet::Rails::Host.new(:name => "foo", :environment => "production", :ip => "127.0.0.1")
@host.stubs(:find_resources).returns(@dbresources)
@host.stubs(:find_resources_parameters_tags)
@host.stubs(:compare_to_catalog)
@host.stubs(:id).returns(1)
end
it "should find all database resources" do
@host.expects(:find_resources)
@host.merge_resources(@resources)
end
it "should find all paramaters and tags for those database resources" do
@host.expects(:find_resources_parameters_tags).with(@dbresources)
@host.merge_resources(@resources)
end
it "should compare all database resources to catalog" do
@host.expects(:compare_to_catalog).with(@dbresources, @resources)
@host.merge_resources(@resources)
end
it "should compare only exported resources in thin_storeconfigs mode" do
Puppet[:thin_storeconfigs] = true
@resource1.stubs(:exported?).returns(true)
@host.expects(:compare_to_catalog).with(@dbresources, [ @resource1 ])
@host.merge_resources(@resources)
end
end
describe "when searching the database for host resources" do
before :each do
Puppet[:thin_storeconfigs] = false
@resource1 = stub_everything 'res1', :id => 1
@resource2 = stub_everything 'res2', :id => 2
@resources = [ @resource1, @resource2 ]
@dbresources = stub 'resources'
@dbresources.stubs(:find).returns(@resources)
@host = Puppet::Rails::Host.new(:name => "foo", :environment => "production", :ip => "127.0.0.1")
@host.stubs(:resources).returns(@dbresources)
end
it "should return a hash keyed by id of all resources" do
@host.find_resources.should == { 1 => @resource1, 2 => @resource2 }
end
it "should return a hash keyed by id of only exported resources in thin_storeconfigs mode" do
Puppet[:thin_storeconfigs] = true
@dbresources.expects(:find).with { |*h| h[1][:conditions] == { :exported => true } }.returns([])
@host.find_resources
end
end
end
diff --git a/spec/unit/rails/param_value_spec.rb b/spec/unit/rails/param_value_spec.rb
index d29d17fbb..ace121be3 100755
--- a/spec/unit/rails/param_value_spec.rb
+++ b/spec/unit/rails/param_value_spec.rb
@@ -1,42 +1,46 @@
#! /usr/bin/env ruby
require 'spec_helper'
require 'puppet/rails'
describe "Puppet::Rails::ParamValue", :if => can_use_scratch_database? do
before do
require 'puppet/rails/param_value'
setup_scratch_database
# Stub this so we don't need access to the DB.
name = stub 'param_name', :name => "foo"
Puppet::Rails::ParamName.stubs(:find_or_create_by_name).returns(name)
end
+ after do
+ Puppet::Rails.teardown
+ end
+
describe "when creating initial parameter values" do
it "should return an array of hashes" do
Puppet::Rails::ParamValue.from_parser_param(:myparam, %w{a b})[0].should be_instance_of(Hash)
end
it "should return hashes for each value with the parameter name set as the ParamName instance" do
name = stub 'param_name', :name => "foo"
Puppet::Rails::ParamName.expects(:find_or_create_by_name).returns(name)
result = Puppet::Rails::ParamValue.from_parser_param(:myparam, "a")[0]
result[:value].should == "a"
result[:param_name].should == name
end
it "should return an array of hashes even when only one parameter is provided" do
Puppet::Rails::ParamValue.from_parser_param(:myparam, "a")[0].should be_instance_of(Hash)
end
it "should convert all arguments into strings" do
Puppet::Rails::ParamValue.from_parser_param(:myparam, 50)[0][:value].should == "50"
end
it "should not convert Resource References into strings" do
ref = Puppet::Resource.new(:file, "/file")
Puppet::Rails::ParamValue.from_parser_param(:myparam, ref)[0][:value].should == ref
end
end
end
diff --git a/spec/unit/relationship_spec.rb b/spec/unit/relationship_spec.rb
index a6aeffc4f..66cd38183 100755
--- a/spec/unit/relationship_spec.rb
+++ b/spec/unit/relationship_spec.rb
@@ -1,228 +1,228 @@
#! /usr/bin/env ruby
require 'spec_helper'
require 'puppet/relationship'
describe Puppet::Relationship do
before do
@edge = Puppet::Relationship.new(:a, :b)
end
it "should have a :source attribute" do
@edge.should respond_to(:source)
end
it "should have a :target attribute" do
@edge.should respond_to(:target)
end
it "should have a :callback attribute" do
@edge.callback = :foo
@edge.callback.should == :foo
end
it "should have an :event attribute" do
@edge.event = :NONE
@edge.event.should == :NONE
end
it "should require a callback if a non-NONE event is specified" do
proc { @edge.event = :something }.should raise_error(ArgumentError)
end
it "should have a :label attribute" do
@edge.should respond_to(:label)
end
it "should provide a :ref method that describes the edge" do
@edge = Puppet::Relationship.new("a", "b")
@edge.ref.should == "a => b"
end
it "should be able to produce a label as a hash with its event and callback" do
@edge.callback = :foo
@edge.event = :bar
@edge.label.should == {:callback => :foo, :event => :bar}
end
it "should work if nil options are provided" do
lambda { Puppet::Relationship.new("a", "b", nil) }.should_not raise_error
end
end
describe Puppet::Relationship, " when initializing" do
before do
@edge = Puppet::Relationship.new(:a, :b)
end
it "should use the first argument as the source" do
@edge.source.should == :a
end
it "should use the second argument as the target" do
@edge.target.should == :b
end
it "should set the rest of the arguments as the event and callback" do
@edge = Puppet::Relationship.new(:a, :b, :callback => :foo, :event => :bar)
@edge.callback.should == :foo
@edge.event.should == :bar
end
it "should accept events specified as strings" do
@edge = Puppet::Relationship.new(:a, :b, "event" => :NONE)
@edge.event.should == :NONE
end
it "should accept callbacks specified as strings" do
@edge = Puppet::Relationship.new(:a, :b, "callback" => :foo)
@edge.callback.should == :foo
end
end
describe Puppet::Relationship, " when matching edges with no specified event" do
before do
@edge = Puppet::Relationship.new(:a, :b)
end
it "should not match :NONE" do
@edge.should_not be_match(:NONE)
end
it "should not match :ALL_EVENTS" do
@edge.should_not be_match(:NONE)
end
it "should not match any other events" do
@edge.should_not be_match(:whatever)
end
end
describe Puppet::Relationship, " when matching edges with :NONE as the event" do
before do
@edge = Puppet::Relationship.new(:a, :b, :event => :NONE)
end
it "should not match :NONE" do
@edge.should_not be_match(:NONE)
end
it "should not match :ALL_EVENTS" do
@edge.should_not be_match(:ALL_EVENTS)
end
it "should not match other events" do
@edge.should_not be_match(:yayness)
end
end
describe Puppet::Relationship, " when matching edges with :ALL as the event" do
before do
@edge = Puppet::Relationship.new(:a, :b, :event => :ALL_EVENTS, :callback => :whatever)
end
it "should not match :NONE" do
@edge.should_not be_match(:NONE)
end
it "should match :ALL_EVENTS" do
@edge.should be_match(:ALLEVENTS)
end
it "should match all other events" do
@edge.should be_match(:foo)
end
end
describe Puppet::Relationship, " when matching edges with a non-standard event" do
before do
@edge = Puppet::Relationship.new(:a, :b, :event => :random, :callback => :whatever)
end
it "should not match :NONE" do
@edge.should_not be_match(:NONE)
end
it "should not match :ALL_EVENTS" do
@edge.should_not be_match(:ALL_EVENTS)
end
it "should match events with the same name" do
@edge.should be_match(:random)
end
end
describe Puppet::Relationship, "when converting to pson" do
before do
@edge = Puppet::Relationship.new(:a, :b, :event => :random, :callback => :whatever)
end
it "should store the stringified source as the source in the data" do
PSON.parse(@edge.to_pson)["source"].should == "a"
end
it "should store the stringified target as the target in the data" do
PSON.parse(@edge.to_pson)['target'].should == "b"
end
it "should store the psonified event as the event in the data" do
PSON.parse(@edge.to_pson)["event"].should == "random"
end
it "should not store an event when none is set" do
@edge.event = nil
PSON.parse(@edge.to_pson)["event"].should be_nil
end
it "should store the psonified callback as the callback in the data" do
@edge.callback = "whatever"
PSON.parse(@edge.to_pson)["callback"].should == "whatever"
end
it "should not store a callback when none is set in the edge" do
@edge.callback = nil
PSON.parse(@edge.to_pson)["callback"].should be_nil
end
end
describe Puppet::Relationship, "when converting from pson" do
before do
@event = "random"
@callback = "whatever"
@data = {
"source" => "mysource",
"target" => "mytarget",
"event" => @event,
"callback" => @callback
}
@pson = {
"type" => "Puppet::Relationship",
"data" => @data
}
end
def pson_result_should
Puppet::Relationship.expects(:new).with { |*args| yield args }
end
it "should be extended with the PSON utility module" do
Puppet::Relationship.singleton_class.ancestors.should be_include(Puppet::Util::Pson)
end
# LAK:NOTE For all of these tests, we convert back to the edge so we can
# trap the actual data structure then.
it "should pass the source in as the first argument" do
- Puppet::Relationship.from_pson("source" => "mysource", "target" => "mytarget").source.should == "mysource"
+ Puppet::Relationship.from_data_hash("source" => "mysource", "target" => "mytarget").source.should == "mysource"
end
it "should pass the target in as the second argument" do
- Puppet::Relationship.from_pson("source" => "mysource", "target" => "mytarget").target.should == "mytarget"
+ Puppet::Relationship.from_data_hash("source" => "mysource", "target" => "mytarget").target.should == "mytarget"
end
it "should pass the event as an argument if it's provided" do
- Puppet::Relationship.from_pson("source" => "mysource", "target" => "mytarget", "event" => "myevent", "callback" => "eh").event.should == "myevent"
+ Puppet::Relationship.from_data_hash("source" => "mysource", "target" => "mytarget", "event" => "myevent", "callback" => "eh").event.should == "myevent"
end
it "should pass the callback as an argument if it's provided" do
- Puppet::Relationship.from_pson("source" => "mysource", "target" => "mytarget", "callback" => "mycallback").callback.should == "mycallback"
+ Puppet::Relationship.from_data_hash("source" => "mysource", "target" => "mytarget", "callback" => "mycallback").callback.should == "mycallback"
end
end
diff --git a/spec/unit/reports/http_spec.rb b/spec/unit/reports/http_spec.rb
index 1a99761d8..6efb72751 100755
--- a/spec/unit/reports/http_spec.rb
+++ b/spec/unit/reports/http_spec.rb
@@ -1,89 +1,100 @@
#! /usr/bin/env ruby
require 'spec_helper'
require 'puppet/reports'
processor = Puppet::Reports.report(:http)
describe processor do
subject { Puppet::Transaction::Report.new("apply").extend(processor) }
describe "when setting up the connection" do
let(:http) { stub_everything "http" }
let(:httpok) { Net::HTTPOK.new('1.1', 200, '') }
before :each do
http.expects(:post).returns(httpok)
end
it "configures the connection for ssl when using https" do
Puppet[:reporturl] = 'https://testing:8080/the/path'
Puppet::Network::HttpPool.expects(:http_instance).with(
'testing', 8080, true
).returns http
subject.process
end
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
subject.process
end
end
describe "when making a request" do
let(:connection) { stub_everything "connection" }
let(:httpok) { Net::HTTPOK.new('1.1', 200, '') }
before :each do
Puppet::Network::HttpPool.expects(:http_instance).returns(connection)
end
it "should use the path specified by the 'reporturl' setting" do
report_path = URI.parse(Puppet[:reporturl]).path
- connection.expects(:post).with(report_path, anything, anything).returns(httpok)
+ connection.expects(:post).with(report_path, anything, anything, {}).returns(httpok)
+
+ subject.process
+ end
+
+ it "should use the username and password specified by the 'reporturl' setting" do
+ Puppet[:reporturl] = "https://user:pass@myhost.mydomain:1234/report/upload"
+
+ connection.expects(:post).with(anything, anything, anything, :basic_auth => {
+ :user => 'user',
+ :password => 'pass'
+ }).returns(httpok)
subject.process
end
it "should give the body as the report as YAML" do
- connection.expects(:post).with(anything, subject.to_yaml, anything).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
- connection.expects(:post).with(anything, anything, has_entry("Content-Type" => "application/x-yaml")).returns(httpok)
+ connection.expects(:post).with(anything, anything, has_entry("Content-Type" => "application/x-yaml"), {}).returns(httpok)
subject.process
end
Net::HTTPResponse::CODE_TO_OBJ.each do |code, klass|
if code.to_i >= 200 and code.to_i < 300
it "should succeed on http code #{code}" do
response = klass.new('1.1', code, '')
connection.expects(:post).returns(response)
Puppet.expects(:err).never
subject.process
end
end
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, '')
connection.expects(:post).returns(response)
Puppet.expects(:err)
subject.process
end
end
end
end
end
diff --git a/spec/unit/reports/rrdgraph_spec.rb b/spec/unit/reports/rrdgraph_spec.rb
index 0085667ac..ea5258ea6 100755
--- a/spec/unit/reports/rrdgraph_spec.rb
+++ b/spec/unit/reports/rrdgraph_spec.rb
@@ -1,30 +1,29 @@
#! /usr/bin/env ruby
require 'spec_helper'
require 'puppet/reports'
processor = Puppet::Reports.report(:rrdgraph)
describe processor do
include PuppetSpec::Files
before do
Puppet[:rrddir] = tmpdir('rrdgraph')
- Puppet.settings.use :master
end
after do
FileUtils.rm_rf(Puppet[:rrddir])
end
it "should not error on 0.25.x report format" do
report = YAML.load_file(File.join(PuppetSpec::FIXTURE_DIR, 'yaml/report0.25.x.yaml')).extend processor
report.expects(:mkhtml)
lambda{ report.process }.should_not raise_error
end
it "should not error on 2.6.x report format" do
report = YAML.load_file(File.join(PuppetSpec::FIXTURE_DIR, 'yaml/report2.6.x.yaml')).extend processor
report.expects(:mkhtml)
lambda{ report.process }.should_not raise_error
end
end
diff --git a/spec/unit/reports/store_spec.rb b/spec/unit/reports/store_spec.rb
index 421f8404d..7866571a1 100755
--- a/spec/unit/reports/store_spec.rb
+++ b/spec/unit/reports/store_spec.rb
@@ -1,76 +1,76 @@
#! /usr/bin/env ruby
require 'spec_helper'
require 'puppet/reports'
require 'time'
require 'pathname'
require 'tempfile'
require 'fileutils'
processor = Puppet::Reports.report(:store)
describe processor do
describe "#process" do
include PuppetSpec::Files
before :each do
Puppet[:reportdir] = File.join(tmpdir('reports'), 'reports')
@report = YAML.load_file(File.join(PuppetSpec::FIXTURE_DIR, 'yaml/report2.6.x.yaml')).extend processor
end
it "should create a report directory for the client if one doesn't exist" do
@report.process
File.should be_directory(File.join(Puppet[:reportdir], @report.host))
end
it "should write the report to the file in YAML" do
Time.stubs(:now).returns(Time.parse("2011-01-06 12:00:00 UTC"))
@report.process
File.read(File.join(Puppet[:reportdir], @report.host, "201101061200.yaml")).should == @report.to_yaml
end
it "should write to the report directory in the correct sequence" do
# By doing things in this sequence we should protect against race
# conditions
Time.stubs(:now).returns(Time.parse("2011-01-06 12:00:00 UTC"))
writeseq = sequence("write")
file = mock "file"
Tempfile.expects(:new).in_sequence(writeseq).returns(file)
file.expects(:chmod).in_sequence(writeseq).with(0640)
file.expects(:print).with(@report.to_yaml).in_sequence(writeseq)
file.expects(:close).in_sequence(writeseq)
file.stubs(:path).returns(File.join(Dir.tmpdir, "foo123"))
FileUtils.expects(:mv).in_sequence(writeseq).with(File.join(Dir.tmpdir, "foo123"), File.join(Puppet[:reportdir], @report.host, "201101061200.yaml"))
@report.process
end
it "rejects invalid hostnames" do
@report.host = ".."
- Puppet::FileSystem::File.expects(:exist?).never
+ Puppet::FileSystem.expects(:exist?).never
Tempfile.expects(:new).never
expect { @report.process }.to raise_error(ArgumentError, /Invalid node/)
end
end
describe "::destroy" do
it "rejects invalid hostnames" do
- Puppet::FileSystem::File.expects(:unlink).never
+ Puppet::FileSystem.expects(:unlink).never
expect { processor.destroy("..") }.to raise_error(ArgumentError, /Invalid node/)
end
end
describe "::validate_host" do
['..', 'hello/', '/hello', 'he/llo', 'hello/..', '.'].each do |node|
it "rejects #{node.inspect}" do
expect { processor.validate_host(node) }.to raise_error(ArgumentError, /Invalid node/)
end
end
['.hello', 'hello.', '..hi', 'hi..'].each do |node|
it "accepts #{node.inspect}" do
processor.validate_host(node)
end
end
end
end
diff --git a/spec/unit/resource/catalog_spec.rb b/spec/unit/resource/catalog_spec.rb
index dd88e8e20..8b042c8fb 100755
--- a/spec/unit/resource/catalog_spec.rb
+++ b/spec/unit/resource/catalog_spec.rb
@@ -1,985 +1,973 @@
#! /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
+require 'matchers/json'
describe Puppet::Resource::Catalog, "when compiling" do
+ include JSONMatchers
include PuppetSpec::Files
before do
@basepath = make_absolute("/somepath")
# stub this to not try to create state.yaml
Puppet::Util::Storage.stubs(:store)
end
# audit only resources are unmanaged
# as are resources without properties with should values
it "should write its managed resources' types, namevars" do
catalog = Puppet::Resource::Catalog.new("host")
resourcefile = tmpfile('resourcefile')
Puppet[:resourcefile] = resourcefile
res = Puppet::Type.type('file').new(:title => File.expand_path('/tmp/sam'), :ensure => 'present')
res.file = 'site.pp'
res.line = 21
res2 = Puppet::Type.type('exec').new(:title => 'bob', :command => "#{File.expand_path('/bin/rm')} -rf /")
res2.file = File.expand_path('/modules/bob/manifests/bob.pp')
res2.line = 42
res3 = Puppet::Type.type('file').new(:title => File.expand_path('/tmp/susan'), :audit => 'all')
res3.file = 'site.pp'
res3.line = 63
res4 = Puppet::Type.type('file').new(:title => File.expand_path('/tmp/lilly'))
res4.file = 'site.pp'
res4.line = 84
comp_res = Puppet::Type.type('component').new(:title => 'Class[Main]')
catalog.add_resource(res, res2, res3, res4, comp_res)
catalog.write_resource_file
File.readlines(resourcefile).map(&:chomp).should =~ [
"file[#{File.expand_path('/tmp/sam')}]",
"exec[#{File.expand_path('/bin/rm')} -rf /]"
]
end
it "should log an error if unable to write to the resource file" do
catalog = Puppet::Resource::Catalog.new("host")
Puppet[:resourcefile] = File.expand_path('/not/writable/file')
catalog.add_resource(Puppet::Type.type('file').new(:title => File.expand_path('/tmp/foo')))
catalog.write_resource_file
@logs.size.should == 1
@logs.first.message.should =~ /Could not create resource file/
@logs.first.level.should == :err
end
it "should be able to write its list of classes to the class file" do
@catalog = Puppet::Resource::Catalog.new("host")
@catalog.add_class "foo", "bar"
Puppet[:classfile] = File.expand_path("/class/file")
fh = mock 'filehandle'
File.expects(:open).with(Puppet[:classfile], "w").yields fh
fh.expects(:puts).with "foo\nbar"
@catalog.write_class_file
end
it "should have a client_version attribute" do
@catalog = Puppet::Resource::Catalog.new("host")
@catalog.client_version = 5
@catalog.client_version.should == 5
end
it "should have a server_version attribute" do
@catalog = Puppet::Resource::Catalog.new("host")
@catalog.server_version = 5
@catalog.server_version.should == 5
end
describe "when compiling" do
it "should accept tags" do
config = Puppet::Resource::Catalog.new("mynode")
config.tag("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.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.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.should be_tagged("one")
config.should be_tagged("one::two")
end
it "should accept classes" do
config = Puppet::Resource::Catalog.new("mynode")
config.add_class("one")
config.classes.should == %w{one}
config.add_class("two", "three")
config.classes.should == %w{one two three}
end
it "should tag itself with passed class names" do
config = Puppet::Resource::Catalog.new("mynode")
config.add_class("one")
config.should be_tagged("one")
end
end
describe "when converting to a RAL catalog" do
before do
@original = Puppet::Resource::Catalog.new("mynode")
@original.tag(*%w{one two three})
@original.add_class *%w{four five six}
@top = Puppet::Resource.new :class, 'top'
@topobject = Puppet::Resource.new :file, @basepath+'/topobject'
@middle = Puppet::Resource.new :class, 'middle'
@middleobject = Puppet::Resource.new :file, @basepath+'/middleobject'
@bottom = Puppet::Resource.new :class, 'bottom'
@bottomobject = Puppet::Resource.new :file, @basepath+'/bottomobject'
@resources = [@top, @topobject, @middle, @middleobject, @bottom, @bottomobject]
@original.add_resource(*@resources)
@original.add_edge(@top, @topobject)
@original.add_edge(@top, @middle)
@original.add_edge(@middle, @middleobject)
@original.add_edge(@middle, @bottom)
@original.add_edge(@bottom, @bottomobject)
@catalog = @original.to_ral
end
it "should add all resources as RAL instances" do
@resources.each do |resource|
# Warning: a failure here will result in "global resource iteration is
# deprecated" being raised, because the rspec rendering to get the
# result tries to call `each` on the resource, and that raises.
@catalog.resource(resource.ref).must be_a_kind_of(Puppet::Type)
end
end
it "should copy the tag list to the new catalog" do
@catalog.tags.sort.should == @original.tags.sort
end
it "should copy the class list to the new catalog" do
@catalog.classes.should == @original.classes
end
it "should duplicate the original edges" do
@original.edges.each do |edge|
@catalog.edge?(@catalog.resource(edge.source.ref), @catalog.resource(edge.target.ref)).should be_true
end
end
it "should set itself as the catalog for each converted resource" do
@catalog.vertices.each { |v| v.catalog.object_id.should equal(@catalog.object_id) }
end
# This tests #931.
it "should not lose track of resources whose names vary" do
changer = Puppet::Resource.new :file, @basepath+'/test/', :parameters => {:ensure => :directory}
config = Puppet::Resource::Catalog.new('test')
config.add_resource(changer)
config.add_resource(@top)
config.add_edge(@top, changer)
catalog = config.to_ral
catalog.resource("File[#{@basepath}/test/]").must equal(catalog.resource("File[#{@basepath}/test]"))
end
after do
# Remove all resource instances.
@catalog.clear(true)
end
end
describe "when filtering" do
before :each do
@original = Puppet::Resource::Catalog.new("mynode")
@original.tag(*%w{one two three})
@original.add_class *%w{four five six}
@r1 = stub_everything 'r1', :ref => "File[/a]"
@r1.stubs(:respond_to?).with(:ref).returns(true)
@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(:copy_as_resource).returns(@r2)
@r2.stubs(:is_a?).with(Puppet::Resource).returns(true)
@resources = [@r1,@r2]
@original.add_resource(@r1,@r2)
end
it "should transform the catalog to a resource catalog" do
@original.expects(:to_catalog).with { |h,b| h == :to_resource }
@original.filter
end
it "should scan each catalog resource in turn and apply filtering block" do
@resources.each { |r| r.expects(:test?) }
@original.filter do |r|
r.test?
end
end
it "should filter out resources which produce true when the filter block is evaluated" do
@original.filter do |r|
r == @r1
end.resource("File[/a]").should be_nil
end
it "should not consider edges against resources that were filtered out" do
@original.add_edge(@r1,@r2)
@original.filter do |r|
r == @r1
end.edge?(@r1,@r2).should_not be
end
end
describe "when functioning as a resource container" do
before do
@catalog = Puppet::Resource::Catalog.new("host")
@one = Puppet::Type.type(:notify).new :name => "one"
@two = Puppet::Type.type(:notify).new :name => "two"
@dupe = Puppet::Type.type(:notify).new :name => "one"
end
it "should provide a method to add one or more resources" do
@catalog.add_resource @one, @two
@catalog.resource(@one.ref).must equal(@one)
@catalog.resource(@two.ref).must equal(@two)
end
it "should add resources to the relationship graph if it exists" do
relgraph = @catalog.relationship_graph
@catalog.add_resource @one
relgraph.should be_vertex(@one)
end
it "should set itself as the resource's catalog if it is not a relationship graph" do
@one.expects(:catalog=).with(@catalog)
@catalog.add_resource @one
end
it "should make all vertices available by resource reference" do
@catalog.add_resource(@one)
@catalog.resource(@one.ref).must equal(@one)
@catalog.vertices.find { |r| r.ref == @one.ref }.must equal(@one)
end
it "tracks the container through edges" do
@catalog.add_resource(@two)
@catalog.add_resource(@one)
@catalog.add_edge(@one, @two)
@catalog.container_of(@two).must == @one
end
it "a resource without a container is contained in nil" do
@catalog.add_resource(@one)
@catalog.container_of(@one).must be_nil
end
it "should canonize how resources are referred to during retrieval when both type and title are provided" do
@catalog.add_resource(@one)
@catalog.resource("notify", "one").must equal(@one)
end
it "should canonize how resources are referred to during retrieval when just the title is provided" do
@catalog.add_resource(@one)
@catalog.resource("notify[one]", nil).must equal(@one)
end
describe 'with a duplicate resource' do
def resource_at(type, name, file, line)
resource = Puppet::Resource.new(type, name)
resource.file = file
resource.line = line
Puppet::Type.type(type).new(resource)
end
let(:orig) { resource_at(:notify, 'duplicate-title', '/path/to/orig/file', 42) }
let(:dupe) { resource_at(:notify, 'duplicate-title', '/path/to/dupe/file', 314) }
it "should print the locations of the original duplicated resource" do
@catalog.add_resource(orig)
expect { @catalog.add_resource(dupe) }.to raise_error { |error|
error.should be_a Puppet::Resource::Catalog::DuplicateResourceError
error.message.should match %r[Duplicate declaration: Notify\[duplicate-title\] is already declared]
error.message.should match %r[in file /path/to/orig/file:42]
error.message.should match %r[cannot redeclare]
error.message.should match %r[at /path/to/dupe/file:314]
}
end
end
it "should remove all resources when asked" do
@catalog.add_resource @one
@catalog.add_resource @two
@one.expects :remove
@two.expects :remove
@catalog.clear(true)
end
it "should support a mechanism for finishing resources" do
@one.expects :finish
@two.expects :finish
@catalog.add_resource @one
@catalog.add_resource @two
@catalog.finalize
end
it "should make default resources when finalizing" do
@catalog.expects(:make_default_resources)
@catalog.finalize
end
it "should add default resources to the catalog upon creation" do
@catalog.make_default_resources
@catalog.resource(:schedule, "daily").should_not be_nil
end
it "should optionally support an initialization block and should finalize after such blocks" do
@one.expects :finish
@two.expects :finish
config = Puppet::Resource::Catalog.new("host") do |conf|
conf.add_resource @one
conf.add_resource @two
end
end
it "should inform the resource that it is the resource's catalog" do
@one.expects(:catalog=).with(@catalog)
@catalog.add_resource @one
end
it "should be able to find resources by reference" do
@catalog.add_resource @one
@catalog.resource(@one.ref).must equal(@one)
end
it "should be able to find resources by reference or by type/title tuple" do
@catalog.add_resource @one
@catalog.resource("notify", "one").must equal(@one)
end
it "should have a mechanism for removing resources" do
@catalog.add_resource(@one)
@catalog.resource(@one.ref).must be
@catalog.vertex?(@one).must be_true
@catalog.remove_resource(@one)
@catalog.resource(@one.ref).must be_nil
@catalog.vertex?(@one).must be_false
end
it "should have a method for creating aliases for resources" do
@catalog.add_resource @one
@catalog.alias(@one, "other")
@catalog.resource("notify", "other").must equal(@one)
end
it "should ignore conflicting aliases that point to the aliased resource" do
@catalog.alias(@one, "other")
lambda { @catalog.alias(@one, "other") }.should_not raise_error
end
it "should create aliases for isomorphic resources whose names do not match their titles" do
resource = Puppet::Type::File.new(:title => "testing", :path => @basepath+"/something")
@catalog.add_resource(resource)
@catalog.resource(:file, @basepath+"/something").must equal(resource)
end
it "should not create aliases for non-isomorphic resources whose names do not match their titles" do
resource = Puppet::Type.type(:exec).new(:title => "testing", :command => "echo", :path => %w{/bin /usr/bin /usr/local/bin})
@catalog.add_resource(resource)
# Yay, I've already got a 'should' method
@catalog.resource(:exec, "echo").object_id.should == nil.object_id
end
# This test is the same as the previous, but the behaviour should be explicit.
it "should alias using the class name from the resource reference, not the resource class name" do
@catalog.add_resource @one
@catalog.alias(@one, "other")
@catalog.resource("notify", "other").must equal(@one)
end
it "should fail to add an alias if the aliased name already exists" do
@catalog.add_resource @one
proc { @catalog.alias @two, "one" }.should raise_error(ArgumentError)
end
it "should not fail when a resource has duplicate aliases created" do
@catalog.add_resource @one
proc { @catalog.alias @one, "one" }.should_not raise_error
end
it "should not create aliases that point back to the resource" do
@catalog.alias(@one, "one")
@catalog.resource(:notify, "one").must be_nil
end
it "should be able to look resources up by their aliases" do
@catalog.add_resource @one
@catalog.alias @one, "two"
@catalog.resource(:notify, "two").must equal(@one)
end
it "should remove resource aliases when the target resource is removed" do
@catalog.add_resource @one
@catalog.alias(@one, "other")
@one.expects :remove
@catalog.remove_resource(@one)
@catalog.resource("notify", "other").must be_nil
end
it "should add an alias for the namevar when the title and name differ on isomorphic resource types" do
resource = Puppet::Type.type(:file).new :path => @basepath+"/something", :title => "other", :content => "blah"
resource.expects(:isomorphic?).returns(true)
@catalog.add_resource(resource)
@catalog.resource(:file, "other").must equal(resource)
@catalog.resource(:file, @basepath+"/something").ref.should == resource.ref
end
it "should not add an alias for the namevar when the title and name differ on non-isomorphic resource types" do
resource = Puppet::Type.type(:file).new :path => @basepath+"/something", :title => "other", :content => "blah"
resource.expects(:isomorphic?).returns(false)
@catalog.add_resource(resource)
@catalog.resource(:file, resource.title).must equal(resource)
# We can't use .should here, because the resources respond to that method.
raise "Aliased non-isomorphic resource" if @catalog.resource(:file, resource.name)
end
it "should provide a method to create additional resources that also registers the resource" do
args = {:name => "/yay", :ensure => :file}
resource = stub 'file', :ref => "File[/yay]", :catalog= => @catalog, :title => "/yay", :[] => "/yay"
Puppet::Type.type(:file).expects(:new).with(args).returns(resource)
@catalog.create_resource :file, args
@catalog.resource("File[/yay]").must equal(resource)
end
describe "when adding resources with multiple namevars" do
before :each do
Puppet::Type.newtype(:multiple) do
newparam(:color, :namevar => true)
newparam(:designation, :namevar => true)
def self.title_patterns
[ [
/^(\w+) (\w+)$/,
[
[:color, lambda{|x| x}],
[:designation, lambda{|x| x}]
]
] ]
end
end
end
it "should add an alias using the uniqueness key" do
@resource = Puppet::Type.type(:multiple).new(:title => "some resource", :color => "red", :designation => "5")
@catalog.add_resource(@resource)
@catalog.resource(:multiple, "some resource").must == @resource
@catalog.resource("Multiple[some resource]").must == @resource
@catalog.resource("Multiple[red 5]").must == @resource
end
it "should conflict with a resource with the same uniqueness key" do
@resource = Puppet::Type.type(:multiple).new(:title => "some resource", :color => "red", :designation => "5")
@other = Puppet::Type.type(:multiple).new(:title => "another resource", :color => "red", :designation => "5")
@catalog.add_resource(@resource)
expect { @catalog.add_resource(@other) }.to raise_error(ArgumentError, /Cannot alias Multiple\[another resource\] to \["red", "5"\].*resource \["Multiple", "red", "5"\] already declared/)
end
it "should conflict when its uniqueness key matches another resource's title" do
path = make_absolute("/tmp/foo")
@resource = Puppet::Type.type(:file).new(:title => path)
@other = Puppet::Type.type(:file).new(:title => "another file", :path => path)
@catalog.add_resource(@resource)
expect { @catalog.add_resource(@other) }.to raise_error(ArgumentError, /Cannot alias File\[another file\] to \["#{Regexp.escape(path)}"\].*resource \["File", "#{Regexp.escape(path)}"\] already declared/)
end
it "should conflict when its uniqueness key matches the uniqueness key derived from another resource's title" do
@resource = Puppet::Type.type(:multiple).new(:title => "red leader")
@other = Puppet::Type.type(:multiple).new(:title => "another resource", :color => "red", :designation => "leader")
@catalog.add_resource(@resource)
expect { @catalog.add_resource(@other) }.to raise_error(ArgumentError, /Cannot alias Multiple\[another resource\] to \["red", "leader"\].*resource \["Multiple", "red", "leader"\] already declared/)
end
end
end
describe "when applying" do
before :each do
@catalog = Puppet::Resource::Catalog.new("host")
@transaction = Puppet::Transaction.new(@catalog, nil, Puppet::Graph::RandomPrioritizer.new)
Puppet::Transaction.stubs(:new).returns(@transaction)
@transaction.stubs(:evaluate)
@transaction.stubs(:for_network_device=)
Puppet.settings.stubs(:use)
end
it "should create and evaluate a transaction" do
@transaction.expects(:evaluate)
@catalog.apply
end
it "should return the transaction" do
@catalog.apply.should equal(@transaction)
end
it "should yield the transaction if a block is provided" do
@catalog.apply do |trans|
trans.should equal(@transaction)
end
end
it "should default to being a host catalog" do
@catalog.host_config.should be_true
end
it "should be able to be set to a non-host_config" do
@catalog.host_config = false
@catalog.host_config.should be_false
end
it "should pass supplied tags on to the transaction" do
@transaction.expects(:tags=).with(%w{one two})
@catalog.apply(:tags => %w{one two})
end
it "should set ignoreschedules on the transaction if specified in apply()" do
@transaction.expects(:ignoreschedules=).with(true)
@catalog.apply(:ignoreschedules => true)
end
describe "host catalogs" do
# super() doesn't work in the setup method for some reason
before do
@catalog.host_config = true
Puppet::Util::Storage.stubs(:store)
end
it "should initialize the state database before applying a catalog" do
Puppet::Util::Storage.expects(:load)
# Short-circuit the apply, so we know we're loading before the transaction
Puppet::Transaction.expects(:new).raises ArgumentError
proc { @catalog.apply }.should raise_error(ArgumentError)
end
it "should sync the state database after applying" do
Puppet::Util::Storage.expects(:store)
@transaction.stubs :any_failed? => false
@catalog.apply
end
after { Puppet.settings.clear }
end
describe "non-host catalogs" do
before do
@catalog.host_config = false
end
it "should never send reports" do
Puppet[:report] = true
Puppet[:summarize] = true
@catalog.apply
end
it "should never modify the state database" do
Puppet::Util::Storage.expects(:load).never
Puppet::Util::Storage.expects(:store).never
@catalog.apply
end
after { Puppet.settings.clear }
end
end
describe "when creating a relationship graph" do
before do
@catalog = Puppet::Resource::Catalog.new("host")
end
it "should get removed when the catalog is cleaned up" do
@catalog.relationship_graph.expects(:clear)
@catalog.clear
@catalog.instance_variable_get("@relationship_graph").should be_nil
end
end
describe "when writing dot files" do
before do
@catalog = Puppet::Resource::Catalog.new("host")
@name = :test
@file = File.join(Puppet[:graphdir], @name.to_s + ".dot")
end
it "should only write when it is a host catalog" do
File.expects(:open).with(@file).never
@catalog.host_config = false
Puppet[:graph] = true
@catalog.write_graph(@name)
end
after do
Puppet.settings.clear
end
end
describe "when indirecting" do
before do
@real_indirection = Puppet::Resource::Catalog.indirection
@indirection = stub 'indirection', :name => :catalog
end
it "should use the value of the 'catalog_terminus' setting to determine its terminus class" do
# Puppet only checks the terminus setting the first time you ask
# so this returns the object to the clean state
# at the expense of making this test less pure
Puppet::Resource::Catalog.indirection.reset_terminus_class
Puppet.settings[:catalog_terminus] = "rest"
Puppet::Resource::Catalog.indirection.terminus_class.should == :rest
end
it "should allow the terminus class to be set manually" do
Puppet::Resource::Catalog.indirection.terminus_class = :rest
Puppet::Resource::Catalog.indirection.terminus_class.should == :rest
end
after do
@real_indirection.reset_terminus_class
end
end
describe "when converting to yaml" do
before do
@catalog = Puppet::Resource::Catalog.new("me")
@catalog.add_edge("one", "two")
end
it "should be able to be dumped to yaml" do
YAML.dump(@catalog).should be_instance_of(String)
end
end
describe "when converting from yaml" do
before do
@catalog = Puppet::Resource::Catalog.new("me")
@catalog.add_edge("one", "two")
text = YAML.dump(@catalog)
@newcatalog = YAML.load(text)
end
it "should get converted back to a catalog" do
@newcatalog.should be_instance_of(Puppet::Resource::Catalog)
end
it "should have all vertices" do
@newcatalog.vertex?("one").should be_true
@newcatalog.vertex?("two").should be_true
end
it "should have all edges" do
@newcatalog.edge?("one", "two").should be_true
end
end
end
describe Puppet::Resource::Catalog, "when converting a resource catalog to pson" do
+ include JSONMatchers
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
+ it "should validate an empty catalog against the schema" do
empty_catalog = compile_to_catalog("")
- validate_json_for_catalog(empty_catalog)
+ expect(empty_catalog.to_pson).to validate_against('api/schemas/catalog.json')
end
- it "should validate a noop catalog against the schema", :unless => Puppet.features.microsoft_windows? do
+ it "should validate a noop catalog against the schema" do
noop_catalog = compile_to_catalog("create_resources('file', {})")
- validate_json_for_catalog(noop_catalog)
+ expect(noop_catalog.to_pson).to validate_against('api/schemas/catalog.json')
end
- it "should validate a single resource catalog against the schema", :unless => Puppet.features.microsoft_windows? do
+ it "should validate a single resource catalog against the schema" do
catalog = compile_to_catalog("create_resources('file', {'/etc/foo'=>{'ensure'=>'present'}})")
- validate_json_for_catalog(catalog)
+ expect(catalog.to_pson).to validate_against('api/schemas/catalog.json')
end
- it "should validate a virtual resource catalog against the schema", :unless => Puppet.features.microsoft_windows? do
+ it "should validate a virtual resource catalog against the schema" do
catalog = compile_to_catalog("create_resources('@file', {'/etc/foo'=>{'ensure'=>'present'}})\nrealize(File['/etc/foo'])")
- validate_json_for_catalog(catalog)
+ expect(catalog.to_pson).to validate_against('api/schemas/catalog.json')
end
- it "should validate a single exported resource catalog against the schema", :unless => Puppet.features.microsoft_windows? do
+ it "should validate a single exported resource catalog against the schema" do
catalog = compile_to_catalog("create_resources('@@file', {'/etc/foo'=>{'ensure'=>'present'}})")
- validate_json_for_catalog(catalog)
+ expect(catalog.to_pson).to validate_against('api/schemas/catalog.json')
end
- it "should validate a two resource catalog against the schema", :unless => Puppet.features.microsoft_windows? do
+ it "should validate a two resource catalog against the schema" do
catalog = compile_to_catalog("create_resources('notify', {'foo'=>{'message'=>'one'}, 'bar'=>{'message'=>'two'}})")
- validate_json_for_catalog(catalog)
+ expect(catalog.to_pson).to validate_against('api/schemas/catalog.json')
end
- it "should validate a two parameter class catalog against the schema", :unless => Puppet.features.microsoft_windows? do
+ it "should validate a two parameter class catalog against the schema" 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)
+ expect(catalog.to_pson).to validate_against('api/schemas/catalog.json')
end
end
describe Puppet::Resource::Catalog, "when converting to pson" do
before do
@catalog = Puppet::Resource::Catalog.new("myhost")
end
def pson_output_should
@catalog.class.expects(:pson_create).with { |hash| yield hash }.returns(:something)
end
# LAK:NOTE For all of these tests, we convert back to the resource so we can
# trap the actual data structure then.
it "should set its document_type to 'Catalog'" do
pson_output_should { |hash| hash['document_type'] == "Catalog" }
PSON.parse @catalog.to_pson
end
it "should set its data as a hash" do
pson_output_should { |hash| hash['data'].is_a?(Hash) }
PSON.parse @catalog.to_pson
end
[: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].should == @catalog.send(param) }
PSON.parse @catalog.to_pson
end
end
it "should convert its resources to a PSON-encoded array and store it as the 'resources' data" do
one = stub 'one', :to_pson_data_hash => "one_resource", :ref => "Foo[one]"
two = stub 'two', :to_pson_data_hash => "two_resource", :ref => "Foo[two]"
@catalog.add_resource(one)
@catalog.add_resource(two)
# TODO this should really guarantee sort order
PSON.parse(@catalog.to_pson,:create_additions => false)['data']['resources'].sort.should == ["one_resource", "two_resource"].sort
end
it "should convert its edges to a PSON-encoded array and store it as the 'edges' data" do
one = stub 'one', :to_pson_data_hash => "one_resource", :ref => 'Foo[one]'
two = stub 'two', :to_pson_data_hash => "two_resource", :ref => 'Foo[two]'
three = stub 'three', :to_pson_data_hash => "three_resource", :ref => 'Foo[three]'
@catalog.add_edge(one, two)
@catalog.add_edge(two, three)
@catalog.edges_between(one, two )[0].expects(:to_pson_data_hash).returns "one_two_pson"
@catalog.edges_between(two, three)[0].expects(:to_pson_data_hash).returns "two_three_pson"
PSON.parse(@catalog.to_pson,:create_additions => false)['data']['edges'].sort.should == %w{one_two_pson two_three_pson}.sort
end
end
describe Puppet::Resource::Catalog, "when converting from pson" do
def pson_result_should
Puppet::Resource::Catalog.expects(:new).with { |hash| yield hash }
end
before do
@data = {
'name' => "myhost"
}
@pson = {
'document_type' => 'Puppet::Resource::Catalog',
'data' => @data,
'metadata' => {}
}
@catalog = Puppet::Resource::Catalog.new("myhost")
Puppet::Resource::Catalog.stubs(:new).returns @catalog
end
it "should be extended with the PSON utility module" do
Puppet::Resource::Catalog.singleton_class.ancestors.should be_include(Puppet::Util::Pson)
end
it "should create it with the provided name" do
Puppet::Resource::Catalog.expects(:new).with('myhost').returns @catalog
PSON.parse @pson.to_pson
end
it "should set the provided version on the catalog if one is set" do
@data['version'] = 50
PSON.parse @pson.to_pson
@catalog.version.should == @data['version']
end
it "should set any provided tags on the catalog" do
@data['tags'] = %w{one two}
PSON.parse @pson.to_pson
@catalog.should be_tagged("one")
@catalog.should be_tagged("two")
end
it "should set any provided classes on the catalog" do
@data['classes'] = %w{one two}
PSON.parse @pson.to_pson
@catalog.classes.should == @data['classes']
end
it 'should convert the resources list into resources and add each of them' do
@data['resources'] = [Puppet::Resource.new(:file, "/foo"), Puppet::Resource.new(:file, "/bar")]
catalog = PSON.parse @pson.to_pson
catalog.resources.collect(&:ref) == ["File[/foo]", "File[/bar]"]
end
it 'should convert resources even if they do not include "type" information' do
@data['resources'] = [Puppet::Resource.new(:file, "/foo")]
@data['resources'][0].expects(:to_pson).returns '{"title":"/foo","tags":["file"],"type":"File"}'
@catalog.expects(:add_resource).with { |res| res.type == "File" }
PSON.parse @pson.to_pson
end
it 'should convert the edges list into edges and add each of them' do
one = Puppet::Relationship.new("osource", "otarget", :event => "one", :callback => "refresh")
two = Puppet::Relationship.new("tsource", "ttarget", :event => "two", :callback => "refresh")
@data['edges'] = [one, two]
@catalog.stubs(:resource).returns("eh")
@catalog.expects(:add_edge).with { |edge| edge.event == "one" }
@catalog.expects(:add_edge).with { |edge| edge.event == "two" }
PSON.parse @pson.to_pson
end
it "should be able to convert relationships that do not include 'type' information" do
one = Puppet::Relationship.new("osource", "otarget", :event => "one", :callback => "refresh")
one.expects(:to_pson).returns "{\"event\":\"one\",\"callback\":\"refresh\",\"source\":\"osource\",\"target\":\"otarget\"}"
@data['edges'] = [one]
@catalog.stubs(:resource).returns("eh")
@catalog.expects(:add_edge).with { |edge| edge.event == "one" }
PSON.parse @pson.to_pson
end
it "should set the source and target for each edge to the actual resource" do
edge = Puppet::Relationship.new("source", "target")
@data['edges'] = [edge]
@catalog.expects(:resource).with("source").returns("source_resource")
@catalog.expects(:resource).with("target").returns("target_resource")
@catalog.expects(:add_edge).with { |edge| edge.source == "source_resource" and edge.target == "target_resource" }
PSON.parse @pson.to_pson
end
it "should fail if the source resource cannot be found" do
edge = Puppet::Relationship.new("source", "target")
@data['edges'] = [edge]
@catalog.expects(:resource).with("source").returns(nil)
@catalog.stubs(:resource).with("target").returns("target_resource")
lambda { PSON.parse @pson.to_pson }.should raise_error(ArgumentError)
end
it "should fail if the target resource cannot be found" do
edge = Puppet::Relationship.new("source", "target")
@data['edges'] = [edge]
@catalog.stubs(:resource).with("source").returns("source_resource")
@catalog.expects(:resource).with("target").returns(nil)
lambda { PSON.parse @pson.to_pson }.should raise_error(ArgumentError)
end
describe "#title_key_for_ref" do
it "should parse a resource ref string into a pair" do
@catalog.title_key_for_ref("Title[name]").should == ["Title", "name"]
end
it "should parse a resource ref string into a pair, even if there's a newline inside the name" do
@catalog.title_key_for_ref("Title[na\nme]").should == ["Title", "na\nme"]
end
end
end
diff --git a/spec/unit/resource/status_spec.rb b/spec/unit/resource/status_spec.rb
index d50abdce3..271554325 100755
--- a/spec/unit/resource/status_spec.rb
+++ b/spec/unit/resource/status_spec.rb
@@ -1,220 +1,220 @@
#! /usr/bin/env ruby
require 'spec_helper'
require 'puppet/resource/status'
describe Puppet::Resource::Status do
include PuppetSpec::Files
before do
@resource = Puppet::Type.type(:file).new :path => make_absolute("/my/file")
@containment_path = ["foo", "bar", "baz"]
@resource.stubs(:pathbuilder).returns @containment_path
@status = Puppet::Resource::Status.new(@resource)
end
it "should compute type and title correctly" do
@status.resource_type.should == "File"
@status.title.should == make_absolute("/my/file")
end
[:node, :file, :line, :current_values, :status, :evaluation_time].each do |attr|
it "should support #{attr}" do
@status.send(attr.to_s + "=", "foo")
@status.send(attr).should == "foo"
end
end
[:skipped, :failed, :restarted, :failed_to_restart, :changed, :out_of_sync, :scheduled].each do |attr|
it "should support #{attr}" do
@status.send(attr.to_s + "=", "foo")
@status.send(attr).should == "foo"
end
it "should have a boolean method for determining whehter it was #{attr}" do
@status.send(attr.to_s + "=", "foo")
@status.should send("be_#{attr}")
end
end
it "should accept a resource at initialization" do
Puppet::Resource::Status.new(@resource).resource.should_not be_nil
end
it "should set its source description to the resource's path" do
@resource.expects(:path).returns "/my/path"
Puppet::Resource::Status.new(@resource).source_description.should == "/my/path"
end
it "should set its containment path" do
Puppet::Resource::Status.new(@resource).containment_path.should == @containment_path
end
[:file, :line].each do |attr|
it "should copy the resource's #{attr}" do
@resource.expects(attr).returns "foo"
Puppet::Resource::Status.new(@resource).send(attr).should == "foo"
end
end
it "should copy the resource's tags" do
@resource.expects(:tags).returns %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
@resource.expects(:to_s).returns "foo"
Puppet::Resource::Status.new(@resource).resource.should == "foo"
end
it "should support tags" do
Puppet::Resource::Status.ancestors.should include(Puppet::Util::Tagging)
end
it "should create a timestamp at its creation time" do
@status.time.should be_instance_of(Time)
end
describe "when sending logs" do
before do
Puppet::Util::Log.stubs(:new)
end
it "should set the tags to the event tags" do
Puppet::Util::Log.expects(:new).with { |args| args[:tags] == %w{one two} }
@status.stubs(:tags).returns %w{one two}
@status.send_log :notice, "my message"
end
[:file, :line].each do |attr|
it "should pass the #{attr}" do
Puppet::Util::Log.expects(:new).with { |args| args[attr] == "my val" }
@status.send(attr.to_s + "=", "my val")
@status.send_log :notice, "my message"
end
end
it "should use the source description as the source" do
Puppet::Util::Log.expects(:new).with { |args| args[:source] == "my source" }
@status.stubs(:source_description).returns "my source"
@status.send_log :notice, "my message"
end
end
it "should support adding events" do
event = Puppet::Transaction::Event.new(:name => :foobar)
@status.add_event(event)
@status.events.should == [event]
end
it "should use '<<' to add events" do
event = Puppet::Transaction::Event.new(:name => :foobar)
(@status << event).should equal(@status)
@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
@status.changed.should == true
@status.out_of_sync.should == true
end
it "should not start with any changes" do
@status.change_count.should == 0
@status.changed.should == false
@status.out_of_sync.should == false
end
it "should not treat failure, audit, or noop events as changed" do
['failure', 'audit', 'noop'].each do |s| @status << Puppet::Transaction::Event.new(:status => s) end
@status.change_count.should == 0
@status.changed.should == false
end
it "should not treat audit events as out of sync" do
@status << Puppet::Transaction::Event.new(:status => 'audit')
@status.out_of_sync_count.should == 0
@status.out_of_sync.should == false
end
['failure', 'noop', 'success'].each do |event_status|
it "should treat #{event_status} events as out of sync" do
3.times do @status << Puppet::Transaction::Event.new(:status => event_status) end
@status.out_of_sync_count.should == 3
@status.out_of_sync.should == true
end
end
describe "When converting to YAML" do
it "should include only documented attributes" do
@status.file = "/foo.rb"
@status.line = 27
@status.evaluation_time = 2.7
@status.tags = %w{one two}
@status.to_yaml_properties.should =~ Puppet::Resource::Status::YAML_ATTRIBUTES
end
end
it "should round trip through pson" do
@status.file = "/foo.rb"
@status.line = 27
@status.evaluation_time = 2.7
@status.tags = %w{one two}
@status << Puppet::Transaction::Event.new(:name => :mode_changed, :status => 'audit')
@status.failed = false
@status.changed = true
@status.out_of_sync = true
@status.skipped = false
@status.containment_path.should == @containment_path
- tripped = Puppet::Resource::Status.from_pson(PSON.parse(@status.to_pson))
+ tripped = Puppet::Resource::Status.from_data_hash(PSON.parse(@status.to_pson))
tripped.title.should == @status.title
tripped.containment_path.should == @status.containment_path
tripped.file.should == @status.file
tripped.line.should == @status.line
tripped.resource.should == @status.resource
tripped.resource_type.should == @status.resource_type
tripped.evaluation_time.should == @status.evaluation_time
tripped.tags.should == @status.tags
tripped.time.should == @status.time
tripped.failed.should == @status.failed
tripped.changed.should == @status.changed
tripped.out_of_sync.should == @status.out_of_sync
tripped.skipped.should == @status.skipped
tripped.change_count.should == @status.change_count
tripped.out_of_sync_count.should == @status.out_of_sync_count
events_as_hashes(tripped).should == events_as_hashes(@status)
end
def events_as_hashes(report)
report.events.collect do |e|
{
:audited => e.audited,
:property => e.property,
:previous_value => e.previous_value,
:desired_value => e.desired_value,
:historical_value => e.historical_value,
:message => e.message,
:name => e.name,
:status => e.status,
:time => e.time,
}
end
end
end
diff --git a/spec/unit/resource/type_collection_spec.rb b/spec/unit/resource/type_collection_spec.rb
index c3a335ce2..4cd5b339e 100755
--- a/spec/unit/resource/type_collection_spec.rb
+++ b/spec/unit/resource/type_collection_spec.rb
@@ -1,436 +1,417 @@
#! /usr/bin/env ruby
require 'spec_helper'
require 'puppet/resource/type_collection'
require 'puppet/resource/type'
describe Puppet::Resource::TypeCollection do
include PuppetSpec::Files
+ let(:environment) { Puppet::Node::Environment.create(:testing, [], '') }
+
before do
@instance = Puppet::Resource::Type.new(:hostclass, "foo")
- @code = Puppet::Resource::TypeCollection.new("env")
- end
-
- it "should require an environment at initialization" do
- env = Puppet::Node::Environment.new("testing")
- Puppet::Resource::TypeCollection.new(env).environment.should equal(env)
- end
-
- it "should convert the environment into an environment instance if a string is provided" do
- env = Puppet::Node::Environment.new("testing")
- Puppet::Resource::TypeCollection.new("testing").environment.should equal(env)
- end
-
- it "should create a 'loader' at initialization" do
- Puppet::Resource::TypeCollection.new("testing").loader.should be_instance_of(Puppet::Parser::TypeLoader)
- end
-
- it "should be able to add a resource type" do
- Puppet::Resource::TypeCollection.new("env").should respond_to(:add)
+ @code = Puppet::Resource::TypeCollection.new(environment)
end
it "should consider '<<' to be an alias to 'add' but should return self" do
- loader = Puppet::Resource::TypeCollection.new("env")
- loader.expects(:add).with "foo"
- loader.expects(:add).with "bar"
- loader << "foo" << "bar"
+ @code.expects(:add).with "foo"
+ @code.expects(:add).with "bar"
+ @code << "foo" << "bar"
end
it "should set itself as the code collection for added resource types" do
- loader = Puppet::Resource::TypeCollection.new("env")
-
node = Puppet::Resource::Type.new(:node, "foo")
@code.add(node)
@code.node("foo").should equal(node)
node.resource_type_collection.should equal(@code)
end
it "should store node resource types as nodes" do
node = Puppet::Resource::Type.new(:node, "foo")
@code.add(node)
@code.node("foo").should equal(node)
end
it "should fail if a duplicate node is added" do
@code.add(Puppet::Resource::Type.new(:node, "foo"))
expect do
@code.add(Puppet::Resource::Type.new(:node, "foo"))
end.to raise_error(Puppet::ParseError, /cannot redefine/)
end
it "should store hostclasses as hostclasses" do
klass = Puppet::Resource::Type.new(:hostclass, "foo")
@code.add(klass)
@code.hostclass("foo").should equal(klass)
end
it "merge together hostclasses of the same name" do
klass1 = Puppet::Resource::Type.new(:hostclass, "foo", :doc => "first")
klass2 = Puppet::Resource::Type.new(:hostclass, "foo", :doc => "second")
@code.add(klass1)
@code.add(klass2)
@code.hostclass("foo").doc.should == "firstsecond"
end
it "should store definitions as definitions" do
define = Puppet::Resource::Type.new(:definition, "foo")
@code.add(define)
@code.definition("foo").should equal(define)
end
it "should fail if a duplicate definition is added" do
@code.add(Puppet::Resource::Type.new(:definition, "foo"))
expect do
@code.add(Puppet::Resource::Type.new(:definition, "foo"))
end.to raise_error(Puppet::ParseError, /cannot be redefined/)
end
it "should remove all nodes, classes, and definitions when cleared" do
- loader = Puppet::Resource::TypeCollection.new("env")
+ loader = Puppet::Resource::TypeCollection.new(environment)
loader.add Puppet::Resource::Type.new(:hostclass, "class")
loader.add Puppet::Resource::Type.new(:definition, "define")
loader.add Puppet::Resource::Type.new(:node, "node")
watched_file = tmpfile('watched_file')
loader.watch_file(watched_file)
loader.clear
loader.hostclass("class").should be_nil
loader.definition("define").should be_nil
loader.node("node").should be_nil
loader.should_not be_watching_file(watched_file)
end
describe "when resolving namespaces" do
[ ['', '::foo', ['foo']],
['a', '::foo', ['foo']],
['a::b', '::foo', ['foo']],
[['a::b'], '::foo', ['foo']],
[['a::b', 'c'], '::foo', ['foo']],
[['A::B', 'C'], '::Foo', ['foo']],
['', '', ['']],
['a', '', ['']],
['a::b', '', ['']],
[['a::b'], '', ['']],
[['a::b', 'c'], '', ['']],
[['A::B', 'C'], '', ['']],
['', 'foo', ['foo']],
['a', 'foo', ['a::foo', 'foo']],
['a::b', 'foo', ['a::b::foo', 'a::foo', 'foo']],
['A::B', 'Foo', ['a::b::foo', 'a::foo', 'foo']],
[['a::b'], 'foo', ['a::b::foo', 'a::foo', 'foo']],
[['a', 'b'], 'foo', ['a::foo', 'foo', 'b::foo']],
[['a::b', 'c::d'], 'foo', ['a::b::foo', 'a::foo', 'foo', 'c::d::foo', 'c::foo']],
[['a::b', 'a::c'], 'foo', ['a::b::foo', 'a::foo', 'foo', 'a::c::foo']],
].each do |namespaces, name, expected_result|
it "should resolve #{name.inspect} in namespaces #{namespaces.inspect} correctly" do
@code.instance_eval { resolve_namespaces(namespaces, name) }.should == expected_result
end
end
end
describe "when looking up names" do
before do
@type = Puppet::Resource::Type.new(:hostclass, "ns::klass")
end
it "should support looking up with multiple namespaces" do
@code.add @type
@code.find_hostclass(%w{boo baz ns}, "klass").should equal(@type)
end
it "should not attempt to import anything when the type is already defined" do
@code.add @type
@code.loader.expects(:import).never
@code.find_hostclass(%w{ns}, "klass").should equal(@type)
end
describe "that need to be loaded" do
it "should use the loader to load the files" do
@code.loader.expects(:try_load_fqname).with(:hostclass, "ns::klass")
@code.loader.expects(:try_load_fqname).with(:hostclass, "klass")
@code.find_hostclass(["ns"], "klass")
end
it "should downcase the name and downcase and array-fy the namespaces before passing to the loader" do
@code.loader.expects(:try_load_fqname).with(:hostclass, "ns::klass")
@code.loader.expects(:try_load_fqname).with(:hostclass, "klass")
@code.find_hostclass("Ns", "Klass")
end
it "should use the class returned by the loader" do
@code.loader.expects(:try_load_fqname).returns(:klass)
@code.expects(:hostclass).with("ns::klass").returns(false)
@code.find_hostclass("ns", "klass").should == :klass
end
it "should return nil if the name isn't found" do
@code.loader.stubs(:try_load_fqname).returns(nil)
@code.find_hostclass("Ns", "Klass").should be_nil
end
it "already-loaded names at broader scopes should not shadow autoloaded names" do
@code.add Puppet::Resource::Type.new(:hostclass, "bar")
@code.loader.expects(:try_load_fqname).with(:hostclass, "foo::bar").returns(:foobar)
@code.find_hostclass("foo", "bar").should == :foobar
end
it "should not try to autoload names that we couldn't autoload in a previous step if ignoremissingtypes is enabled" do
Puppet[:ignoremissingtypes] = true
@code.loader.expects(:try_load_fqname).with(:hostclass, "ns::klass").returns(nil)
@code.loader.expects(:try_load_fqname).with(:hostclass, "klass").returns(nil)
@code.find_hostclass("Ns", "Klass").should be_nil
Puppet.expects(:debug).at_least_once.with {|msg| msg =~ /Not attempting to load hostclass/}
@code.find_hostclass("Ns", "Klass").should be_nil
end
end
end
%w{hostclass node definition}.each do |data|
describe "behavior of add for #{data}" do
it "should return the added #{data}" do
- loader = Puppet::Resource::TypeCollection.new("env")
+ loader = Puppet::Resource::TypeCollection.new(environment)
instance = Puppet::Resource::Type.new(data, "foo")
loader.add(instance).should equal(instance)
end
it "should retrieve #{data} insensitive to case" do
- loader = Puppet::Resource::TypeCollection.new("env")
+ loader = Puppet::Resource::TypeCollection.new(environment)
instance = Puppet::Resource::Type.new(data, "Bar")
loader.add instance
loader.send(data, "bAr").should equal(instance)
end
it "should return nil when asked for a #{data} that has not been added" do
- Puppet::Resource::TypeCollection.new("env").send(data, "foo").should be_nil
+ Puppet::Resource::TypeCollection.new(environment).send(data, "foo").should be_nil
end
end
end
describe "when finding a qualified instance" do
it "should return any found instance if the instance name is fully qualified" do
- loader = Puppet::Resource::TypeCollection.new("env")
+ loader = Puppet::Resource::TypeCollection.new(environment)
instance = Puppet::Resource::Type.new(:hostclass, "foo::bar")
loader.add instance
loader.find_hostclass("namespace", "::foo::bar").should equal(instance)
end
it "should return nil if the instance name is fully qualified and no such instance exists" do
- loader = Puppet::Resource::TypeCollection.new("env")
+ loader = Puppet::Resource::TypeCollection.new(environment)
loader.find_hostclass("namespace", "::foo::bar").should be_nil
end
it "should be able to find classes in the base namespace" do
- loader = Puppet::Resource::TypeCollection.new("env")
+ loader = Puppet::Resource::TypeCollection.new(environment)
instance = Puppet::Resource::Type.new(:hostclass, "foo")
loader.add instance
loader.find_hostclass("", "foo").should equal(instance)
end
it "should return the partially qualified object if it exists in a provided namespace" do
- loader = Puppet::Resource::TypeCollection.new("env")
+ loader = Puppet::Resource::TypeCollection.new(environment)
instance = Puppet::Resource::Type.new(:hostclass, "foo::bar::baz")
loader.add instance
loader.find_hostclass("foo", "bar::baz").should equal(instance)
end
it "should be able to find partially qualified objects in any of the provided namespaces" do
- loader = Puppet::Resource::TypeCollection.new("env")
+ loader = Puppet::Resource::TypeCollection.new(environment)
instance = Puppet::Resource::Type.new(:hostclass, "foo::bar::baz")
loader.add instance
loader.find_hostclass(["nons", "foo", "otherns"], "bar::baz").should equal(instance)
end
it "should return the unqualified object if it exists in a provided namespace" do
- loader = Puppet::Resource::TypeCollection.new("env")
+ loader = Puppet::Resource::TypeCollection.new(environment)
instance = Puppet::Resource::Type.new(:hostclass, "foo::bar")
loader.add instance
loader.find_hostclass("foo", "bar").should equal(instance)
end
it "should return the unqualified object if it exists in the parent namespace" do
- loader = Puppet::Resource::TypeCollection.new("env")
+ loader = Puppet::Resource::TypeCollection.new(environment)
instance = Puppet::Resource::Type.new(:hostclass, "foo::bar")
loader.add instance
loader.find_hostclass("foo::bar::baz", "bar").should equal(instance)
end
it "should should return the partially qualified object if it exists in the parent namespace" do
- loader = Puppet::Resource::TypeCollection.new("env")
+ loader = Puppet::Resource::TypeCollection.new(environment)
instance = Puppet::Resource::Type.new(:hostclass, "foo::bar::baz")
loader.add instance
loader.find_hostclass("foo::bar", "bar::baz").should equal(instance)
end
it "should return the qualified object if it exists in the root namespace" do
- loader = Puppet::Resource::TypeCollection.new("env")
+ loader = Puppet::Resource::TypeCollection.new(environment)
instance = Puppet::Resource::Type.new(:hostclass, "foo::bar::baz")
loader.add instance
loader.find_hostclass("foo::bar", "foo::bar::baz").should equal(instance)
end
it "should return nil if the object cannot be found" do
- loader = Puppet::Resource::TypeCollection.new("env")
+ loader = Puppet::Resource::TypeCollection.new(environment)
instance = Puppet::Resource::Type.new(:hostclass, "foo::bar::baz")
loader.add instance
loader.find_hostclass("foo::bar", "eh").should be_nil
end
describe "when topscope has a class that has the same name as a local class" do
before do
- @loader = Puppet::Resource::TypeCollection.new("env")
+ @loader = Puppet::Resource::TypeCollection.new(environment)
[ "foo::bar", "bar" ].each do |name|
@loader.add Puppet::Resource::Type.new(:hostclass, name)
end
end
it "should favor the local class, if the name is unqualified" do
@loader.find_hostclass("foo", "bar").name.should == 'foo::bar'
end
it "should only look in the topclass, if the name is qualified" do
@loader.find_hostclass("foo", "::bar").name.should == 'bar'
end
it "should only look in the topclass, if we assume the name is fully qualified" do
@loader.find_hostclass("foo", "bar", :assume_fqname => true).name.should == 'bar'
end
end
it "should not look in the local scope for classes when the name is qualified" do
- @loader = Puppet::Resource::TypeCollection.new("env")
+ @loader = Puppet::Resource::TypeCollection.new(environment)
@loader.add Puppet::Resource::Type.new(:hostclass, "foo::bar")
@loader.find_hostclass("foo", "::bar").should == nil
end
end
it "should be able to find nodes" do
node = Puppet::Resource::Type.new(:node, "bar")
- loader = Puppet::Resource::TypeCollection.new("env")
+ loader = Puppet::Resource::TypeCollection.new(environment)
loader.add(node)
loader.find_node(stub("ignored"), "bar").should == node
end
it "should indicate whether any nodes are defined" do
- loader = Puppet::Resource::TypeCollection.new("env")
+ loader = Puppet::Resource::TypeCollection.new(environment)
loader.add_node(Puppet::Resource::Type.new(:node, "foo"))
loader.should be_nodes
end
it "should indicate whether no nodes are defined" do
- Puppet::Resource::TypeCollection.new("env").should_not be_nodes
+ Puppet::Resource::TypeCollection.new(environment).should_not be_nodes
end
describe "when finding nodes" do
before :each do
- @loader = Puppet::Resource::TypeCollection.new("env")
+ @loader = Puppet::Resource::TypeCollection.new(environment)
end
it "should return any node whose name exactly matches the provided node name" do
node = Puppet::Resource::Type.new(:node, "foo")
@loader << node
@loader.node("foo").should equal(node)
end
it "should return the first regex node whose regex matches the provided node name" do
node1 = Puppet::Resource::Type.new(:node, /\w/)
node2 = Puppet::Resource::Type.new(:node, /\d/)
@loader << node1 << node2
@loader.node("foo10").should equal(node1)
end
it "should preferentially return a node whose name is string-equal over returning a node whose regex matches a provided name" do
node1 = Puppet::Resource::Type.new(:node, /\w/)
node2 = Puppet::Resource::Type.new(:node, "foo")
@loader << node1 << node2
@loader.node("foo").should equal(node2)
end
end
describe "when managing files" do
before do
- @loader = Puppet::Resource::TypeCollection.new("env")
+ @loader = Puppet::Resource::TypeCollection.new(environment)
Puppet::Util::WatchedFile.stubs(:new).returns stub("watched_file")
end
it "should have a method for specifying a file should be watched" do
@loader.should respond_to(:watch_file)
end
it "should have a method for determining if a file is being watched" do
@loader.watch_file("/foo/bar")
@loader.should be_watching_file("/foo/bar")
end
it "should use WatchedFile to watch files" do
Puppet::Util::WatchedFile.expects(:new).with("/foo/bar").returns stub("watched_file")
@loader.watch_file("/foo/bar")
end
it "should be considered stale if any files have changed" do
file1 = stub 'file1', :changed? => false
file2 = stub 'file2', :changed? => true
Puppet::Util::WatchedFile.expects(:new).times(2).returns(file1).then.returns(file2)
@loader.watch_file("/foo/bar")
@loader.watch_file("/other/bar")
@loader.should be_stale
end
it "should not be considered stable if no files have changed" do
file1 = stub 'file1', :changed? => false
file2 = stub 'file2', :changed? => false
Puppet::Util::WatchedFile.expects(:new).times(2).returns(file1).then.returns(file2)
@loader.watch_file("/foo/bar")
@loader.watch_file("/other/bar")
@loader.should_not be_stale
end
end
describe "when determining the configuration version" do
before do
- @code = Puppet::Resource::TypeCollection.new("env")
+ @code = Puppet::Resource::TypeCollection.new(environment)
end
it "should default to the current time" do
time = Time.now
Time.stubs(:now).returns time
@code.version.should == time.to_i
end
it "should use the output of the environment's config_version setting if one is provided" do
@code.environment.stubs(:[]).with(:config_version).returns("/my/foo")
Puppet::Util::Execution.expects(:execute).with(["/my/foo"]).returns "output\n"
@code.version.should == "output"
end
it "should raise a puppet parser error if executing config_version fails" do
@code.environment.stubs(:[]).with(:config_version).returns("test")
Puppet::Util::Execution.expects(:execute).raises(Puppet::ExecutionFailure.new("msg"))
lambda { @code.version }.should raise_error(Puppet::ParseError)
end
end
end
diff --git a/spec/unit/resource/type_spec.rb b/spec/unit/resource/type_spec.rb
index 1b5a4727b..b3a4aab78 100755
--- a/spec/unit/resource/type_spec.rb
+++ b/spec/unit/resource/type_spec.rb
@@ -1,788 +1,777 @@
#! /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
+require 'matchers/json'
describe Puppet::Resource::Type do
+ include JSONMatchers
+
it "should have a 'name' attribute" do
Puppet::Resource::Type.new(:hostclass, "foo").name.should == "foo"
end
[:code, :doc, :line, :file, :resource_type_collection, :ruby_code].each do |attr|
it "should have a '#{attr}' attribute" do
type = Puppet::Resource::Type.new(:hostclass, "foo")
type.send(attr.to_s + "=", "yay")
type.send(attr).should == "yay"
end
end
[:hostclass, :node, :definition].each do |type|
it "should know when it is a #{type}" do
Puppet::Resource::Type.new(type, "foo").send("#{type}?").should be_true
end
end
it "should indirect 'resource_type'" do
Puppet::Resource::Type.indirection.name.should == :resource_type
end
it "should default to 'parser' for its terminus class" do
Puppet::Resource::Type.indirection.terminus_class.should == :parser
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
def from_json(json)
- Puppet::Resource::Type.from_pson(json)
+ Puppet::Resource::Type.from_data_hash(json)
end
def double_convert
- Puppet::Resource::Type.from_pson(PSON.parse(@type.to_pson))
+ Puppet::Resource::Type.from_data_hash(PSON.parse(@type.to_pson))
end
it "should include the name and type" do
double_convert.name.should == @type.name
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)
+ it "should validate with only name and kind" do
+ expect(@type.to_pson).to validate_against('api/schemas/resource_type.json')
end
- it "should validate with all fields set", :unless => Puppet.features.microsoft_windows? do
+ it "should validate with all fields set" 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)
+ expect(@type.to_pson).to validate_against('api/schemas/resource_type.json')
end
it "should include any arguments" do
@type.set_arguments("one" => nil, "two" => "foo")
double_convert.arguments.should == {"one" => nil, "two" => "foo"}
end
it "should not include arguments if none are present" do
@type.to_pson["arguments"].should be_nil
end
[:line, :doc, :file, :parent].each do |attr|
it "should include #{attr} when set" do
@type.send(attr.to_s + "=", "value")
double_convert.send(attr).should == "value"
end
it "should not include #{attr} when not set" do
@type.to_pson[attr.to_s].should be_nil
end
end
it "should not include docs if they are empty" do
@type.doc = ""
@type.to_pson["doc"].should be_nil
end
end
describe "when a node" do
it "should allow a regex as its name" do
lambda { Puppet::Resource::Type.new(:node, /foo/) }.should_not raise_error
end
it "should allow an AST::HostName instance as its name" do
regex = Puppet::Parser::AST::Regex.new(:value => /foo/)
name = Puppet::Parser::AST::HostName.new(:value => regex)
lambda { Puppet::Resource::Type.new(:node, name) }.should_not raise_error
end
it "should match against the regexp in the AST::HostName when a HostName instance is provided" do
regex = Puppet::Parser::AST::Regex.new(:value => /\w/)
name = Puppet::Parser::AST::HostName.new(:value => regex)
node = Puppet::Resource::Type.new(:node, name)
node.match("foo").should be_true
end
it "should return the value of the hostname if provided a string-form AST::HostName instance as the name" do
name = Puppet::Parser::AST::HostName.new(:value => "foo")
node = Puppet::Resource::Type.new(:node, name)
node.name.should == "foo"
end
describe "and the name is a regex" do
it "should have a method that indicates that this is the case" do
Puppet::Resource::Type.new(:node, /w/).should be_name_is_regex
end
it "should set its namespace to ''" do
Puppet::Resource::Type.new(:node, /w/).namespace.should == ""
end
it "should return the regex converted to a string when asked for its name" do
Puppet::Resource::Type.new(:node, /ww/).name.should == "ww"
end
it "should downcase the regex when returning the name as a string" do
Puppet::Resource::Type.new(:node, /W/).name.should == "w"
end
it "should remove non-alpha characters when returning the name as a string" do
Puppet::Resource::Type.new(:node, /w*w/).name.should_not include("*")
end
it "should remove leading dots when returning the name as a string" do
Puppet::Resource::Type.new(:node, /.ww/).name.should_not =~ /^\./
end
it "should have a method for matching its regex name against a provided name" do
Puppet::Resource::Type.new(:node, /.ww/).should respond_to(:match)
end
it "should return true when its regex matches the provided name" do
Puppet::Resource::Type.new(:node, /\w/).match("foo").should be_true
end
it "should return true when its regex matches the provided name" do
Puppet::Resource::Type.new(:node, /\w/).match("foo").should be_true
end
it "should return false when its regex does not match the provided name" do
(!!Puppet::Resource::Type.new(:node, /\d/).match("foo")).should be_false
end
it "should return true when its name, as a string, is matched against an equal string" do
Puppet::Resource::Type.new(:node, "foo").match("foo").should be_true
end
it "should return false when its name is matched against an unequal string" do
Puppet::Resource::Type.new(:node, "foo").match("bar").should be_false
end
it "should match names insensitive to case" do
Puppet::Resource::Type.new(:node, "fOo").match("foO").should be_true
end
end
end
describe "when initializing" do
it "should require a resource super type" do
Puppet::Resource::Type.new(:hostclass, "foo").type.should == :hostclass
end
it "should fail if provided an invalid resource super type" do
lambda { Puppet::Resource::Type.new(:nope, "foo") }.should raise_error(ArgumentError)
end
it "should set its name to the downcased, stringified provided name" do
Puppet::Resource::Type.new(:hostclass, "Foo::Bar".intern).name.should == "foo::bar"
end
it "should set its namespace to the downcased, stringified qualified name for classes" do
Puppet::Resource::Type.new(:hostclass, "Foo::Bar::Baz".intern).namespace.should == "foo::bar::baz"
end
[:definition, :node].each do |type|
it "should set its namespace to the downcased, stringified qualified portion of the name for #{type}s" do
Puppet::Resource::Type.new(type, "Foo::Bar::Baz".intern).namespace.should == "foo::bar"
end
end
%w{code line file doc}.each do |arg|
it "should set #{arg} if provided" do
type = Puppet::Resource::Type.new(:hostclass, "foo", arg.to_sym => "something")
type.send(arg).should == "something"
end
end
it "should set any provided arguments with the keys as symbols" do
type = Puppet::Resource::Type.new(:hostclass, "foo", :arguments => {:foo => "bar", :baz => "biz"})
type.should be_valid_parameter("foo")
type.should be_valid_parameter("baz")
end
it "should set any provided arguments with they keys as strings" do
type = Puppet::Resource::Type.new(:hostclass, "foo", :arguments => {"foo" => "bar", "baz" => "biz"})
type.should be_valid_parameter(:foo)
type.should be_valid_parameter(:baz)
end
it "should function if provided no arguments" do
type = Puppet::Resource::Type.new(:hostclass, "foo")
type.should_not be_valid_parameter(:foo)
end
end
describe "when testing the validity of an attribute" do
it "should return true if the parameter was typed at initialization" do
Puppet::Resource::Type.new(:hostclass, "foo", :arguments => {"foo" => "bar"}).should be_valid_parameter("foo")
end
it "should return true if it is a metaparam" do
Puppet::Resource::Type.new(:hostclass, "foo").should be_valid_parameter("require")
end
it "should return true if the parameter is named 'name'" do
Puppet::Resource::Type.new(:hostclass, "foo").should be_valid_parameter("name")
end
it "should return false if it is not a metaparam and was not provided at initialization" do
Puppet::Resource::Type.new(:hostclass, "foo").should_not be_valid_parameter("yayness")
end
end
describe "when setting its parameters in the scope" do
before do
@scope = Puppet::Parser::Scope.new(Puppet::Parser::Compiler.new(Puppet::Node.new("foo")), :source => stub("source"))
@resource = Puppet::Parser::Resource.new(:foo, "bar", :scope => @scope)
@type = Puppet::Resource::Type.new(:definition, "foo")
@resource.environment.known_resource_types.add @type
end
['module_name', 'name', 'title'].each do |variable|
it "should allow #{variable} to be evaluated as param default" do
@type.instance_eval { @module_name = "bar" }
var = Puppet::Parser::AST::Variable.new({'value' => variable})
@type.set_arguments :foo => var
@type.set_resource_parameters(@resource, @scope)
@scope['foo'].should == 'bar'
end
end
# this test is to clarify a crazy edge case
# if you specify these special names as params, the resource
# will override the special variables
it "should allow the resource to override defaults" do
@type.set_arguments :name => nil
@resource[:name] = 'foobar'
var = Puppet::Parser::AST::Variable.new({'value' => 'name'})
@type.set_arguments :foo => var
@type.set_resource_parameters(@resource, @scope)
@scope['foo'].should == 'foobar'
end
it "should set each of the resource's parameters as variables in the scope" do
@type.set_arguments :foo => nil, :boo => nil
@resource[:foo] = "bar"
@resource[:boo] = "baz"
@type.set_resource_parameters(@resource, @scope)
@scope['foo'].should == "bar"
@scope['boo'].should == "baz"
end
it "should set the variables as strings" do
@type.set_arguments :foo => nil
@resource[:foo] = "bar"
@type.set_resource_parameters(@resource, @scope)
@scope['foo'].should == "bar"
end
it "should fail if any of the resource's parameters are not valid attributes" do
@type.set_arguments :foo => nil
@resource[:boo] = "baz"
lambda { @type.set_resource_parameters(@resource, @scope) }.should raise_error(Puppet::ParseError)
end
it "should evaluate and set its default values as variables for parameters not provided by the resource" do
@type.set_arguments :foo => Puppet::Parser::AST::String.new(:value => "something")
@type.set_resource_parameters(@resource, @scope)
@scope['foo'].should == "something"
end
it "should set all default values as parameters in the resource" do
@type.set_arguments :foo => Puppet::Parser::AST::String.new(:value => "something")
@type.set_resource_parameters(@resource, @scope)
@resource[:foo].should == "something"
end
it "should fail if the resource does not provide a value for a required argument" do
@type.set_arguments :foo => nil
lambda { @type.set_resource_parameters(@resource, @scope) }.should raise_error(Puppet::ParseError)
end
it "should set the resource's title as a variable if not otherwise provided" do
@type.set_resource_parameters(@resource, @scope)
@scope['title'].should == "bar"
end
it "should set the resource's name as a variable if not otherwise provided" do
@type.set_resource_parameters(@resource, @scope)
@scope['name'].should == "bar"
end
it "should set its module name in the scope if available" do
@type.instance_eval { @module_name = "mymod" }
@type.set_resource_parameters(@resource, @scope)
@scope["module_name"].should == "mymod"
end
it "should set its caller module name in the scope if available" do
@scope.expects(:parent_module_name).returns "mycaller"
@type.set_resource_parameters(@resource, @scope)
@scope["caller_module_name"].should == "mycaller"
end
end
describe "when describing and managing parent classes" do
before do
- @krt = Puppet::Node::Environment.new.known_resource_types
+ environment = Puppet::Node::Environment.create(:testing, [], '')
+ @krt = environment.known_resource_types
@parent = Puppet::Resource::Type.new(:hostclass, "bar")
@krt.add @parent
@child = Puppet::Resource::Type.new(:hostclass, "foo", :parent => "bar")
@krt.add @child
- @scope = Puppet::Parser::Scope.new(Puppet::Parser::Compiler.new(Puppet::Node.new("foo")))
+ @scope = Puppet::Parser::Scope.new(Puppet::Parser::Compiler.new(Puppet::Node.new("foo", :environment => environment)))
end
it "should be able to define a parent" do
Puppet::Resource::Type.new(:hostclass, "foo", :parent => "bar")
end
it "should use the code collection to find the parent resource type" do
@child.parent_type(@scope).should equal(@parent)
end
it "should be able to find parent nodes" do
parent = Puppet::Resource::Type.new(:node, "bar")
@krt.add parent
child = Puppet::Resource::Type.new(:node, "foo", :parent => "bar")
@krt.add child
child.parent_type(@scope).should equal(parent)
end
it "should cache a reference to the parent type" do
@krt.stubs(:hostclass).with("foo::bar").returns nil
@krt.expects(:hostclass).with("bar").once.returns @parent
@child.parent_type(@scope)
@child.parent_type
end
it "should correctly state when it is another type's child" do
@child.parent_type(@scope)
@child.should be_child_of(@parent)
end
it "should be considered the child of a parent's parent" do
@grandchild = Puppet::Resource::Type.new(:hostclass, "baz", :parent => "foo")
@krt.add @grandchild
@child.parent_type(@scope)
@grandchild.parent_type(@scope)
@grandchild.should be_child_of(@parent)
end
it "should correctly state when it is not another type's child" do
@notchild = Puppet::Resource::Type.new(:hostclass, "baz")
@krt.add @notchild
@notchild.should_not be_child_of(@parent)
end
end
describe "when evaluating its code" do
before do
@compiler = Puppet::Parser::Compiler.new(Puppet::Node.new("mynode"))
@scope = Puppet::Parser::Scope.new @compiler
@resource = Puppet::Parser::Resource.new(:class, "foo", :scope => @scope)
# This is so the internal resource lookup works, yo.
@compiler.catalog.add_resource @resource
@type = Puppet::Resource::Type.new(:hostclass, "foo")
@resource.environment.known_resource_types.add @type
end
it "should add node regex captures to its scope" do
@type = Puppet::Resource::Type.new(:node, /f(\w)o(.*)$/)
match = @type.match('foo')
code = stub 'code'
@type.stubs(:code).returns code
subscope = stub 'subscope', :compiler => @compiler
@scope.expects(:newscope).with(:source => @type, :namespace => '', :resource => @resource).returns subscope
elevel = 876
subscope.expects(:ephemeral_level).returns elevel
subscope.expects(:ephemeral_from).with(match, nil, nil).returns subscope
code.expects(:safeevaluate).with(subscope)
subscope.expects(:unset_ephemeral_var).with(elevel)
# Just to keep the stub quiet about intermediate calls
@type.expects(:set_resource_parameters).with(@resource, subscope)
@type.evaluate_code(@resource)
end
it "should add hostclass names to the classes list" do
@type.evaluate_code(@resource)
@compiler.catalog.classes.should be_include("foo")
end
it "should not add defined resource names to the classes list" do
@type = Puppet::Resource::Type.new(:definition, "foo")
@type.evaluate_code(@resource)
@compiler.catalog.classes.should_not be_include("foo")
end
it "should set all of its parameters in a subscope" do
subscope = stub 'subscope', :compiler => @compiler
@scope.expects(:newscope).with(:source => @type, :namespace => 'foo', :resource => @resource).returns subscope
@type.expects(:set_resource_parameters).with(@resource, subscope)
@type.evaluate_code(@resource)
end
it "should not create a subscope for the :main class" do
@resource.stubs(:title).returns(:main)
@type.expects(:subscope).never
@type.expects(:set_resource_parameters).with(@resource, @scope)
@type.evaluate_code(@resource)
end
it "should store the class scope" do
@type.evaluate_code(@resource)
@scope.class_scope(@type).should be_instance_of(@scope.class)
end
it "should still create a scope but not store it if the type is a definition" do
@type = Puppet::Resource::Type.new(:definition, "foo")
@type.evaluate_code(@resource)
@scope.class_scope(@type).should be_nil
end
it "should evaluate the AST code if any is provided" do
code = stub 'code'
@type.stubs(:code).returns code
subscope = stub_everything("subscope", :compiler => @compiler)
@scope.stubs(:newscope).returns subscope
code.expects(:safeevaluate).with subscope
@type.evaluate_code(@resource)
end
describe "and ruby code is provided" do
it "should create a DSL Resource API and evaluate it" do
@type.stubs(:ruby_code).returns(proc { "foo" })
@api = stub 'api'
Puppet::DSL::ResourceAPI.expects(:new).with { |res, scope, code| code == @type.ruby_code }.returns @api
@api.expects(:evaluate)
@type.evaluate_code(@resource)
end
end
it "should noop if there is no code" do
@type.expects(:code).returns nil
@type.evaluate_code(@resource)
end
describe "and it has a parent class" do
before do
@parent_type = Puppet::Resource::Type.new(:hostclass, "parent")
@type.parent = "parent"
@parent_resource = Puppet::Parser::Resource.new(:class, "parent", :scope => @scope)
@compiler.add_resource @scope, @parent_resource
@type.resource_type_collection = @scope.known_resource_types
@type.resource_type_collection.add @parent_type
end
it "should evaluate the parent's resource" do
@type.parent_type(@scope)
@type.evaluate_code(@resource)
@scope.class_scope(@parent_type).should_not be_nil
end
it "should not evaluate the parent's resource if it has already been evaluated" do
@parent_resource.evaluate
@type.parent_type(@scope)
@parent_resource.expects(:evaluate).never
@type.evaluate_code(@resource)
end
it "should use the parent's scope as its base scope" do
@type.parent_type(@scope)
@type.evaluate_code(@resource)
@scope.class_scope(@type).parent.object_id.should == @scope.class_scope(@parent_type).object_id
end
end
describe "and it has a parent node" do
before do
@type = Puppet::Resource::Type.new(:node, "foo")
@parent_type = Puppet::Resource::Type.new(:node, "parent")
@type.parent = "parent"
@parent_resource = Puppet::Parser::Resource.new(:node, "parent", :scope => @scope)
@compiler.add_resource @scope, @parent_resource
@type.resource_type_collection = @scope.known_resource_types
@type.resource_type_collection.add(@parent_type)
end
it "should evaluate the parent's resource" do
@type.parent_type(@scope)
@type.evaluate_code(@resource)
@scope.class_scope(@parent_type).should_not be_nil
end
it "should not evaluate the parent's resource if it has already been evaluated" do
@parent_resource.evaluate
@type.parent_type(@scope)
@parent_resource.expects(:evaluate).never
@type.evaluate_code(@resource)
end
it "should use the parent's scope as its base scope" do
@type.parent_type(@scope)
@type.evaluate_code(@resource)
@scope.class_scope(@type).parent.object_id.should == @scope.class_scope(@parent_type).object_id
end
end
end
describe "when creating a resource" do
before do
@node = Puppet::Node.new("foo", :environment => 'env')
@compiler = Puppet::Parser::Compiler.new(@node)
@scope = Puppet::Parser::Scope.new(@compiler)
@top = Puppet::Resource::Type.new :hostclass, "top"
@middle = Puppet::Resource::Type.new :hostclass, "middle", :parent => "top"
@code = Puppet::Resource::TypeCollection.new("env")
@code.add @top
@code.add @middle
@node.environment.stubs(:known_resource_types).returns(@code)
end
it "should create a resource instance" do
@top.ensure_in_catalog(@scope).should be_instance_of(Puppet::Parser::Resource)
end
it "should set its resource type to 'class' when it is a hostclass" do
Puppet::Resource::Type.new(:hostclass, "top").ensure_in_catalog(@scope).type.should == "Class"
end
it "should set its resource type to 'node' when it is a node" do
Puppet::Resource::Type.new(:node, "top").ensure_in_catalog(@scope).type.should == "Node"
end
it "should fail when it is a definition" do
lambda { Puppet::Resource::Type.new(:definition, "top").ensure_in_catalog(@scope) }.should raise_error(ArgumentError)
end
it "should add the created resource to the scope's catalog" do
@top.ensure_in_catalog(@scope)
@compiler.catalog.resource(:class, "top").should be_instance_of(Puppet::Parser::Resource)
end
it "should add specified parameters to the resource" do
@top.ensure_in_catalog(@scope, {'one'=>'1', 'two'=>'2'})
@compiler.catalog.resource(:class, "top")['one'].should == '1'
@compiler.catalog.resource(:class, "top")['two'].should == '2'
end
it "should not require params for a param class" do
@top.ensure_in_catalog(@scope, {})
@compiler.catalog.resource(:class, "top").should be_instance_of(Puppet::Parser::Resource)
end
it "should evaluate the parent class if one exists" do
@middle.ensure_in_catalog(@scope)
@compiler.catalog.resource(:class, "top").should be_instance_of(Puppet::Parser::Resource)
end
it "should evaluate the parent class if one exists" do
@middle.ensure_in_catalog(@scope, {})
@compiler.catalog.resource(:class, "top").should be_instance_of(Puppet::Parser::Resource)
end
it "should fail if you try to create duplicate class resources" do
othertop = Puppet::Parser::Resource.new(:class, 'top',:source => @source, :scope => @scope )
# add the same class resource to the catalog
@compiler.catalog.add_resource(othertop)
lambda { @top.ensure_in_catalog(@scope, {}) }.should raise_error(Puppet::Resource::Catalog::DuplicateResourceError)
end
it "should fail to evaluate if a parent class is defined but cannot be found" do
othertop = Puppet::Resource::Type.new :hostclass, "something", :parent => "yay"
@code.add othertop
lambda { othertop.ensure_in_catalog(@scope) }.should raise_error(Puppet::ParseError)
end
it "should not create a new resource if one already exists" do
@compiler.catalog.expects(:resource).with(:class, "top").returns("something")
@compiler.catalog.expects(:add_resource).never
@top.ensure_in_catalog(@scope)
end
it "should return the existing resource when not creating a new one" do
@compiler.catalog.expects(:resource).with(:class, "top").returns("something")
@compiler.catalog.expects(:add_resource).never
@top.ensure_in_catalog(@scope).should == "something"
end
it "should not create a new parent resource if one already exists and it has a parent class" do
@top.ensure_in_catalog(@scope)
top_resource = @compiler.catalog.resource(:class, "top")
@middle.ensure_in_catalog(@scope)
@compiler.catalog.resource(:class, "top").should equal(top_resource)
end
# #795 - tag before evaluation.
it "should tag the catalog with the resource tags when it is evaluated" do
@middle.ensure_in_catalog(@scope)
@compiler.catalog.should be_tagged("middle")
end
it "should tag the catalog with the parent class tags when it is evaluated" do
@middle.ensure_in_catalog(@scope)
@compiler.catalog.should be_tagged("top")
end
end
describe "when merging code from another instance" do
def code(str)
Puppet::Parser::AST::Leaf.new :value => str
end
it "should fail unless it is a class" do
lambda { Puppet::Resource::Type.new(:node, "bar").merge("foo") }.should raise_error(Puppet::Error)
end
it "should fail unless the source instance is a class" do
dest = Puppet::Resource::Type.new(:hostclass, "bar")
source = Puppet::Resource::Type.new(:node, "foo")
lambda { dest.merge(source) }.should raise_error(Puppet::Error)
end
it "should fail if both classes have different parent classes" do
code = Puppet::Resource::TypeCollection.new("env")
{"a" => "b", "c" => "d"}.each do |parent, child|
code.add Puppet::Resource::Type.new(:hostclass, parent)
code.add Puppet::Resource::Type.new(:hostclass, child, :parent => parent)
end
lambda { code.hostclass("b").merge(code.hostclass("d")) }.should raise_error(Puppet::Error)
end
it "should fail if it's named 'main' and 'freeze_main' is enabled" do
Puppet.settings[:freeze_main] = true
code = Puppet::Resource::TypeCollection.new("env")
code.add Puppet::Resource::Type.new(:hostclass, "")
other = Puppet::Resource::Type.new(:hostclass, "")
lambda { code.hostclass("").merge(other) }.should raise_error(Puppet::Error)
end
it "should copy the other class's parent if it has not parent" do
dest = Puppet::Resource::Type.new(:hostclass, "bar")
parent = Puppet::Resource::Type.new(:hostclass, "parent")
source = Puppet::Resource::Type.new(:hostclass, "foo", :parent => "parent")
dest.merge(source)
dest.parent.should == "parent"
end
it "should copy the other class's documentation as its docs if it has no docs" do
dest = Puppet::Resource::Type.new(:hostclass, "bar")
source = Puppet::Resource::Type.new(:hostclass, "foo", :doc => "yayness")
dest.merge(source)
dest.doc.should == "yayness"
end
it "should append the other class's docs to its docs if it has any" do
dest = Puppet::Resource::Type.new(:hostclass, "bar", :doc => "fooness")
source = Puppet::Resource::Type.new(:hostclass, "foo", :doc => "yayness")
dest.merge(source)
dest.doc.should == "foonessyayness"
end
it "should set the other class's code as its code if it has none" do
dest = Puppet::Resource::Type.new(:hostclass, "bar")
source = Puppet::Resource::Type.new(:hostclass, "foo", :code => code("bar"))
dest.merge(source)
dest.code.value.should == "bar"
end
it "should append the other class's code to its code if it has any" do
dcode = Puppet::Parser::AST::BlockExpression.new(:children => [code("dest")])
dest = Puppet::Resource::Type.new(:hostclass, "bar", :code => dcode)
scode = Puppet::Parser::AST::BlockExpression.new(:children => [code("source")])
source = Puppet::Resource::Type.new(:hostclass, "foo", :code => scode)
dest.merge(source)
dest.code.children.collect { |l| l.value }.should == %w{dest source}
end
end
end
diff --git a/spec/unit/resource_spec.rb b/spec/unit/resource_spec.rb
index a8f21d205..42d0a7ec8 100755
--- a/spec/unit/resource_spec.rb
+++ b/spec/unit/resource_spec.rb
@@ -1,1018 +1,979 @@
#! /usr/bin/env ruby
require 'spec_helper'
require 'puppet/resource'
describe Puppet::Resource do
include PuppetSpec::Files
- let :basepath do make_absolute("/somepath") end
+ let(:basepath) { make_absolute("/somepath") }
+ let(:environment) { Puppet::Node::Environment.create(:testing, [], '') }
[:catalog, :file, :line].each do |attr|
it "should have an #{attr} attribute" do
resource = Puppet::Resource.new("file", "/my/file")
resource.should respond_to(attr)
resource.should respond_to(attr.to_s + "=")
end
end
it "should have a :title attribute" do
Puppet::Resource.new(:user, "foo").title.should == "foo"
end
it "should require the type and title" do
expect { Puppet::Resource.new }.to raise_error(ArgumentError)
end
it "should canonize types to capitalized strings" do
Puppet::Resource.new(:user, "foo").type.should == "User"
end
it "should canonize qualified types so all strings are capitalized" do
Puppet::Resource.new("foo::bar", "foo").type.should == "Foo::Bar"
end
it "should tag itself with its type" do
Puppet::Resource.new("file", "/f").should be_tagged("file")
end
it "should tag itself with its title if the title is a valid tag" do
Puppet::Resource.new("user", "bar").should be_tagged("bar")
end
it "should not tag itself with its title if the title is a not valid tag" do
Puppet::Resource.new("file", "/bar").should_not be_tagged("/bar")
end
it "should allow setting of attributes" do
Puppet::Resource.new("file", "/bar", :file => "/foo").file.should == "/foo"
Puppet::Resource.new("file", "/bar", :exported => true).should be_exported
end
it "should set its type to 'Class' and its title to the passed title if the passed type is :component and the title has no square brackets in it" do
ref = Puppet::Resource.new(:component, "foo")
ref.type.should == "Class"
ref.title.should == "Foo"
end
it "should interpret the title as a reference and assign appropriately if the type is :component and the title contains square brackets" do
ref = Puppet::Resource.new(:component, "foo::bar[yay]")
ref.type.should == "Foo::Bar"
ref.title.should == "yay"
end
it "should set the type to 'Class' if it is nil and the title contains no square brackets" do
ref = Puppet::Resource.new(nil, "yay")
ref.type.should == "Class"
ref.title.should == "Yay"
end
it "should interpret the title as a reference and assign appropriately if the type is nil and the title contains square brackets" do
ref = Puppet::Resource.new(nil, "foo::bar[yay]")
ref.type.should == "Foo::Bar"
ref.title.should == "yay"
end
it "should interpret the title as a reference and assign appropriately if the type is nil and the title contains nested square brackets" do
ref = Puppet::Resource.new(nil, "foo::bar[baz[yay]]")
ref.type.should == "Foo::Bar"
ref.title.should =="baz[yay]"
end
it "should interpret the type as a reference and assign appropriately if the title is nil and the type contains square brackets" do
ref = Puppet::Resource.new("foo::bar[baz]")
ref.type.should == "Foo::Bar"
ref.title.should =="baz"
end
it "should be able to extract its information from a Puppet::Type instance" do
ral = Puppet::Type.type(:file).new :path => basepath+"/foo"
ref = Puppet::Resource.new(ral)
ref.type.should == "File"
ref.title.should == basepath+"/foo"
end
it "should fail if the title is nil and the type is not a valid resource reference string" do
expect { Puppet::Resource.new("resource-spec-foo") }.to raise_error(ArgumentError)
end
it 'should fail if strict is set and type does not exist' do
expect { Puppet::Resource.new('resource-spec-foo', 'title', {:strict=>true}) }.to raise_error(ArgumentError, 'Invalid resource type resource-spec-foo')
end
it 'should fail if strict is set and class does not exist' do
expect { Puppet::Resource.new('Class', 'resource-spec-foo', {:strict=>true}) }.to raise_error(ArgumentError, 'Could not find declared class resource-spec-foo')
end
it "should fail if the title is a hash and the type is not a valid resource reference string" do
expect { Puppet::Resource.new({:type => "resource-spec-foo", :title => "bar"}) }.
to raise_error ArgumentError, /Puppet::Resource.new does not take a hash/
end
it "should be taggable" do
Puppet::Resource.ancestors.should be_include(Puppet::Util::Tagging)
end
it "should have an 'exported' attribute" do
resource = Puppet::Resource.new("file", "/f")
resource.exported = true
resource.exported.should == true
resource.should be_exported
end
- it "should support an environment attribute" do
- Puppet::Resource.new("file", "/my/file", :environment => :foo).environment.name.should == :foo
- end
-
describe "and munging its type and title" do
describe "when modeling a builtin resource" do
it "should be able to find the resource type" do
Puppet::Resource.new("file", "/my/file").resource_type.should equal(Puppet::Type.type(:file))
end
it "should set its type to the capitalized type name" do
Puppet::Resource.new("file", "/my/file").type.should == "File"
end
end
describe "when modeling a defined resource" do
describe "that exists" do
before do
@type = Puppet::Resource::Type.new(:definition, "foo::bar")
- Puppet::Node::Environment.new.known_resource_types.add @type
+ environment.known_resource_types.add @type
end
it "should set its type to the capitalized type name" do
- Puppet::Resource.new("foo::bar", "/my/file").type.should == "Foo::Bar"
+ Puppet::Resource.new("foo::bar", "/my/file", :environment => environment).type.should == "Foo::Bar"
end
it "should be able to find the resource type" do
- Puppet::Resource.new("foo::bar", "/my/file").resource_type.should equal(@type)
+ Puppet::Resource.new("foo::bar", "/my/file", :environment => environment).resource_type.should equal(@type)
end
it "should set its title to the provided title" do
- Puppet::Resource.new("foo::bar", "/my/file").title.should == "/my/file"
+ Puppet::Resource.new("foo::bar", "/my/file", :environment => environment).title.should == "/my/file"
end
end
describe "that does not exist" do
it "should set its resource type to the capitalized resource type name" do
Puppet::Resource.new("foo::bar", "/my/file").type.should == "Foo::Bar"
end
end
end
describe "when modeling a node" do
# Life's easier with nodes, because they can't be qualified.
it "should set its type to 'Node' and its title to the provided title" do
node = Puppet::Resource.new("node", "foo")
node.type.should == "Node"
node.title.should == "foo"
end
end
describe "when modeling a class" do
it "should set its type to 'Class'" do
Puppet::Resource.new("class", "foo").type.should == "Class"
end
describe "that exists" do
before do
@type = Puppet::Resource::Type.new(:hostclass, "foo::bar")
- Puppet::Node::Environment.new.known_resource_types.add @type
+ environment.known_resource_types.add @type
end
it "should set its title to the capitalized, fully qualified resource type" do
- Puppet::Resource.new("class", "foo::bar").title.should == "Foo::Bar"
+ Puppet::Resource.new("class", "foo::bar", :environment => environment).title.should == "Foo::Bar"
end
it "should be able to find the resource type" do
- Puppet::Resource.new("class", "foo::bar").resource_type.should equal(@type)
+ Puppet::Resource.new("class", "foo::bar", :environment => environment).resource_type.should equal(@type)
end
end
describe "that does not exist" do
it "should set its type to 'Class' and its title to the capitalized provided name" do
klass = Puppet::Resource.new("class", "foo::bar")
klass.type.should == "Class"
klass.title.should == "Foo::Bar"
end
end
describe "and its name is set to the empty string" do
it "should set its title to :main" do
Puppet::Resource.new("class", "").title.should == :main
end
describe "and a class exists whose name is the empty string" do # this was a bit tough to track down
it "should set its title to :main" do
@type = Puppet::Resource::Type.new(:hostclass, "")
- Puppet::Node::Environment.new.known_resource_types.add @type
+ environment.known_resource_types.add @type
- Puppet::Resource.new("class", "").title.should == :main
+ Puppet::Resource.new("class", "", :environment => environment).title.should == :main
end
end
end
describe "and its name is set to :main" do
it "should set its title to :main" do
Puppet::Resource.new("class", :main).title.should == :main
end
describe "and a class exists whose name is the empty string" do # this was a bit tough to track down
it "should set its title to :main" do
@type = Puppet::Resource::Type.new(:hostclass, "")
- Puppet::Node::Environment.new.known_resource_types.add @type
+ environment.known_resource_types.add @type
- Puppet::Resource.new("class", :main).title.should == :main
+ Puppet::Resource.new("class", :main, :environment => environment).title.should == :main
end
end
end
end
end
it "should return nil when looking up resource types that don't exist" do
Puppet::Resource.new("foobar", "bar").resource_type.should be_nil
end
it "should not fail when an invalid parameter is used and strict mode is disabled" do
type = Puppet::Resource::Type.new(:definition, "foobar")
- Puppet::Node::Environment.new.known_resource_types.add type
- resource = Puppet::Resource.new("foobar", "/my/file")
+ environment.known_resource_types.add type
+ resource = Puppet::Resource.new("foobar", "/my/file", :environment => environment)
resource[:yay] = true
end
it "should be considered equivalent to another resource if their type and title match and no parameters are set" do
Puppet::Resource.new("file", "/f").should == Puppet::Resource.new("file", "/f")
end
it "should be considered equivalent to another resource if their type, title, and parameters are equal" do
Puppet::Resource.new("file", "/f", :parameters => {:foo => "bar"}).should == Puppet::Resource.new("file", "/f", :parameters => {:foo => "bar"})
end
it "should not be considered equivalent to another resource if their type and title match but parameters are different" do
Puppet::Resource.new("file", "/f", :parameters => {:fee => "baz"}).should_not == Puppet::Resource.new("file", "/f", :parameters => {:foo => "bar"})
end
it "should not be considered equivalent to a non-resource" do
Puppet::Resource.new("file", "/f").should_not == "foo"
end
it "should not be considered equivalent to another resource if their types do not match" do
Puppet::Resource.new("file", "/f").should_not == Puppet::Resource.new("exec", "/f")
end
it "should not be considered equivalent to another resource if their titles do not match" do
Puppet::Resource.new("file", "/foo").should_not == Puppet::Resource.new("file", "/f")
end
describe "when setting default parameters" do
- let(:foo_node) { Puppet::Node.new('foo') }
+ let(:foo_node) { Puppet::Node.new('foo', :environment => environment) }
let(:compiler) { Puppet::Parser::Compiler.new(foo_node) }
let(:scope) { Puppet::Parser::Scope.new(compiler) }
def ast_string(value)
Puppet::Parser::AST::String.new({:value => value})
end
it "should fail when asked to set default values and it is not a parser resource" do
- Puppet::Node::Environment.new.known_resource_types.add(
+ environment.known_resource_types.add(
Puppet::Resource::Type.new(:definition, "default_param", :arguments => {"a" => ast_string("default")})
)
- resource = Puppet::Resource.new("default_param", "name")
+ resource = Puppet::Resource.new("default_param", "name", :environment => environment)
lambda { resource.set_default_parameters(scope) }.should raise_error(Puppet::DevError)
end
it "should evaluate and set any default values when no value is provided" do
- Puppet::Node::Environment.new.known_resource_types.add(
+ environment.known_resource_types.add(
Puppet::Resource::Type.new(:definition, "default_param", :arguments => {"a" => ast_string("a_default_value")})
)
resource = Puppet::Parser::Resource.new("default_param", "name", :scope => scope)
resource.set_default_parameters(scope)
resource["a"].should == "a_default_value"
end
it "should skip attributes with no default value" do
- Puppet::Node::Environment.new.known_resource_types.add(
+ environment.known_resource_types.add(
Puppet::Resource::Type.new(:definition, "no_default_param", :arguments => {"a" => ast_string("a_default_value")})
)
resource = Puppet::Parser::Resource.new("no_default_param", "name", :scope => scope)
lambda { resource.set_default_parameters(scope) }.should_not raise_error
end
it "should return the list of default parameters set" do
- Puppet::Node::Environment.new.known_resource_types.add(
+ environment.known_resource_types.add(
Puppet::Resource::Type.new(:definition, "default_param", :arguments => {"a" => ast_string("a_default_value")})
)
resource = Puppet::Parser::Resource.new("default_param", "name", :scope => scope)
resource.set_default_parameters(scope).should == ["a"]
end
describe "when the resource type is :hostclass" do
let(:environment_name) { "testing env" }
let(:fact_values) { { :a => 1 } }
let(:port) { Puppet::Parser::AST::String.new(:value => '80') }
let(:apache) { Puppet::Resource::Type.new(:hostclass, 'apache', :arguments => { 'port' => port }) }
before do
- environment = Puppet::Node::Environment.new(environment_name)
environment.known_resource_types.add(apache)
scope.stubs(:host).returns('host')
- scope.stubs(:environment).returns(Puppet::Node::Environment.new(environment_name))
+ scope.stubs(:environment).returns(environment)
scope.stubs(:facts).returns(Puppet::Node::Facts.new("facts", fact_values))
end
context "when no value is provided" do
before(:each) do
Puppet[:binder] = true
end
let(:resource) do
Puppet::Parser::Resource.new("class", "apache", :scope => scope)
end
it "should query the data_binding terminus using a namespaced key" do
Puppet::DataBinding.indirection.expects(:find).with(
'apache::port', all_of(has_key(:environment), has_key(:variables)))
resource.set_default_parameters(scope)
end
- it "should query the injector using a namespaced key" do
- 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
Puppet::DataBinding.indirection.expects(:find).returns('443')
resource.set_default_parameters(scope)
resource[:port].should == '443'
end
- it "should use the value from the injector" do
- compiler.injector.expects(:lookup).with(scope, 'apache::port').returns('443')
-
- resource.set_default_parameters(scope)
-
- resource[:port].should == '443'
- end
-
- it "should not call the DataBinding terminus when injector produces a value" do
- compiler.injector.expects(:lookup).with(scope, 'apache::port').returns('443')
- Puppet::DataBinding.indirection.expects(:find).never()
-
- resource.set_default_parameters(scope)
-
- resource[:port].should == '443'
- end
-
it "should use the default value if the data_binding terminus returns nil" do
Puppet::DataBinding.indirection.expects(:find).returns(nil)
resource.set_default_parameters(scope)
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)
-
- resource[:port].should == '80'
- end
end
context "when a value is provided" do
let(:port_parameter) do
Puppet::Parser::Resource::Param.new(
{ :name => 'port', :value => '8080' }
)
end
let(:resource) do
Puppet::Parser::Resource.new("class", "apache", :scope => scope,
:parameters => [port_parameter])
end
it "should not query the data_binding terminus" do
Puppet::DataBinding.indirection.expects(:find).never
resource.set_default_parameters(scope)
end
it "should not query the injector" do
# enable the injector
Puppet[:binder] = true
compiler.injector.expects(:find).never
resource.set_default_parameters(scope)
end
it "should use the value provided" do
Puppet::DataBinding.indirection.expects(:find).never
resource.set_default_parameters(scope).should == []
resource[:port].should == '8080'
end
end
end
end
describe "when validating all required parameters are present" do
it "should be able to validate that all required parameters are present" do
- Puppet::Node::Environment.new.known_resource_types.add(
+ environment.known_resource_types.add(
Puppet::Resource::Type.new(:definition, "required_param", :arguments => {"a" => nil})
)
- lambda { Puppet::Resource.new("required_param", "name").validate_complete }.should raise_error(Puppet::ParseError)
+ lambda { Puppet::Resource.new("required_param", "name", :environment => environment).validate_complete }.should raise_error(Puppet::ParseError)
end
it "should not fail when all required parameters are present" do
- Puppet::Node::Environment.new.known_resource_types.add(
+ environment.known_resource_types.add(
Puppet::Resource::Type.new(:definition, "no_required_param")
)
- resource = Puppet::Resource.new("no_required_param", "name")
+ resource = Puppet::Resource.new("no_required_param", "name", :environment => environment)
resource["a"] = "meh"
lambda { resource.validate_complete }.should_not raise_error
end
it "should not validate against builtin types" do
lambda { Puppet::Resource.new("file", "/bar").validate_complete }.should_not raise_error
end
end
describe "when referring to a resource with name canonicalization" do
it "should canonicalize its own name" do
res = Puppet::Resource.new("file", "/path/")
res.uniqueness_key.should == ["/path"]
res.ref.should == "File[/path/]"
end
end
describe "when running in strict mode" do
it "should be strict" do
Puppet::Resource.new("file", "/path", :strict => true).should be_strict
end
it "should fail if invalid parameters are used" do
expect { Puppet::Resource.new("file", "/path", :strict => true, :parameters => {:nosuchparam => "bar"}) }.to raise_error
end
it "should fail if the resource type cannot be resolved" do
expect { Puppet::Resource.new("nosuchtype", "/path", :strict => true) }.to raise_error
end
end
describe "when managing parameters" do
before do
@resource = Puppet::Resource.new("file", "/my/file")
end
it "should correctly detect when provided parameters are not valid for builtin types" do
Puppet::Resource.new("file", "/my/file").should_not be_valid_parameter("foobar")
end
it "should correctly detect when provided parameters are valid for builtin types" do
Puppet::Resource.new("file", "/my/file").should be_valid_parameter("mode")
end
it "should correctly detect when provided parameters are not valid for defined resource types" do
type = Puppet::Resource::Type.new(:definition, "foobar")
- Puppet::Node::Environment.new.known_resource_types.add type
- Puppet::Resource.new("foobar", "/my/file").should_not be_valid_parameter("myparam")
+ environment.known_resource_types.add type
+ Puppet::Resource.new("foobar", "/my/file", :environment => environment).should_not be_valid_parameter("myparam")
end
it "should correctly detect when provided parameters are valid for defined resource types" do
type = Puppet::Resource::Type.new(:definition, "foobar", :arguments => {"myparam" => nil})
- Puppet::Node::Environment.new.known_resource_types.add type
- Puppet::Resource.new("foobar", "/my/file").should be_valid_parameter("myparam")
+ environment.known_resource_types.add type
+ Puppet::Resource.new("foobar", "/my/file", :environment => environment).should be_valid_parameter("myparam")
end
it "should allow setting and retrieving of parameters" do
@resource[:foo] = "bar"
@resource[:foo].should == "bar"
end
it "should allow setting of parameters at initialization" do
Puppet::Resource.new("file", "/my/file", :parameters => {:foo => "bar"})[:foo].should == "bar"
end
it "should canonicalize retrieved parameter names to treat symbols and strings equivalently" do
@resource[:foo] = "bar"
@resource["foo"].should == "bar"
end
it "should canonicalize set parameter names to treat symbols and strings equivalently" do
@resource["foo"] = "bar"
@resource[:foo].should == "bar"
end
it "should set the namevar when asked to set the name" do
resource = Puppet::Resource.new("user", "bob")
Puppet::Type.type(:user).stubs(:key_attributes).returns [:myvar]
resource[:name] = "bob"
resource[:myvar].should == "bob"
end
it "should return the namevar when asked to return the name" do
resource = Puppet::Resource.new("user", "bob")
Puppet::Type.type(:user).stubs(:key_attributes).returns [:myvar]
resource[:myvar] = "test"
resource[:name].should == "test"
end
it "should be able to set the name for non-builtin types" do
resource = Puppet::Resource.new(:foo, "bar")
resource[:name] = "eh"
expect { resource[:name] = "eh" }.to_not raise_error
end
it "should be able to return the name for non-builtin types" do
resource = Puppet::Resource.new(:foo, "bar")
resource[:name] = "eh"
resource[:name].should == "eh"
end
it "should be able to iterate over parameters" do
@resource[:foo] = "bar"
@resource[:fee] = "bare"
params = {}
@resource.each do |key, value|
params[key] = value
end
params.should == {:foo => "bar", :fee => "bare"}
end
it "should include Enumerable" do
@resource.class.ancestors.should be_include(Enumerable)
end
it "should have a method for testing whether a parameter is included" do
@resource[:foo] = "bar"
@resource.should be_has_key(:foo)
@resource.should_not be_has_key(:eh)
end
it "should have a method for providing the list of parameters" do
@resource[:foo] = "bar"
@resource[:bar] = "foo"
keys = @resource.keys
keys.should be_include(:foo)
keys.should be_include(:bar)
end
it "should have a method for providing the number of parameters" do
@resource[:foo] = "bar"
@resource.length.should == 1
end
it "should have a method for deleting parameters" do
@resource[:foo] = "bar"
@resource.delete(:foo)
@resource[:foo].should be_nil
end
it "should have a method for testing whether the parameter list is empty" do
@resource.should be_empty
@resource[:foo] = "bar"
@resource.should_not be_empty
end
it "should be able to produce a hash of all existing parameters" do
@resource[:foo] = "bar"
@resource[:fee] = "yay"
hash = @resource.to_hash
hash[:foo].should == "bar"
hash[:fee].should == "yay"
end
it "should not provide direct access to the internal parameters hash when producing a hash" do
hash = @resource.to_hash
hash[:foo] = "bar"
@resource[:foo].should be_nil
end
it "should use the title as the namevar to the hash if no namevar is present" do
resource = Puppet::Resource.new("user", "bob")
Puppet::Type.type(:user).stubs(:key_attributes).returns [:myvar]
resource.to_hash[:myvar].should == "bob"
end
it "should set :name to the title if :name is not present for non-builtin types" do
krt = Puppet::Resource::TypeCollection.new("myenv")
krt.add Puppet::Resource::Type.new(:definition, :foo)
resource = Puppet::Resource.new :foo, "bar"
resource.stubs(:known_resource_types).returns krt
resource.to_hash[:name].should == "bar"
end
end
describe "when serializing a native type" do
before do
@resource = Puppet::Resource.new("file", "/my/file")
@resource["one"] = "test"
@resource["two"] = "other"
end
it "should produce 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 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
+ environment.known_resource_types.add type
- before :each do
- @resource = Puppet::Resource.new('foo::bar', 'xyzzy')
+ @resource = Puppet::Resource.new('foo::bar', 'xyzzy', :environment => environment)
@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")
result = resource.to_ral
result.must be_instance_of(Puppet::Type.type(:file))
result[:path].should == basepath+"/my/file"
end
it "should convert to a component instance if the resource type is not of a builtin type" do
resource = Puppet::Resource.new("foobar", "somename")
result = resource.to_ral
result.must be_instance_of(Puppet::Type.type(:component))
result.title.should == "Foobar[somename]"
end
end
describe "when converting to puppet code" do
before do
@resource = Puppet::Resource.new("one::two", "/my/file",
:parameters => {
:noop => true,
:foo => %w{one two},
:ensure => 'present',
}
)
end
it "should align, sort and add trailing commas to attributes with ensure first" do
@resource.to_manifest.should == <<-HEREDOC.gsub(/^\s{8}/, '').gsub(/\n$/, '')
one::two { '/my/file':
ensure => 'present',
foo => ['one', 'two'],
noop => 'true',
}
HEREDOC
end
end
describe "when converting to pson" do
def pson_output_should
@resource.class.expects(:pson_create).with { |hash| yield hash }
end
it "should include the pson util module" do
Puppet::Resource.singleton_class.ancestors.should be_include(Puppet::Util::Pson)
end
# LAK:NOTE For all of these tests, we convert back to the resource so we can
# trap the actual data structure then.
it "should set its type to the provided type" do
- Puppet::Resource.from_pson(PSON.parse(Puppet::Resource.new("File", "/foo").to_pson)).type.should == "File"
+ Puppet::Resource.from_data_hash(PSON.parse(Puppet::Resource.new("File", "/foo").to_pson)).type.should == "File"
end
it "should set its title to the provided title" do
- Puppet::Resource.from_pson(PSON.parse(Puppet::Resource.new("File", "/foo").to_pson)).title.should == "/foo"
+ Puppet::Resource.from_data_hash(PSON.parse(Puppet::Resource.new("File", "/foo").to_pson)).title.should == "/foo"
end
it "should include all tags from the resource" do
resource = Puppet::Resource.new("File", "/foo")
resource.tag("yay")
- Puppet::Resource.from_pson(PSON.parse(resource.to_pson)).tags.should == resource.tags
+ Puppet::Resource.from_data_hash(PSON.parse(resource.to_pson)).tags.should == resource.tags
end
it "should include the file if one is set" do
resource = Puppet::Resource.new("File", "/foo")
resource.file = "/my/file"
- Puppet::Resource.from_pson(PSON.parse(resource.to_pson)).file.should == "/my/file"
+ Puppet::Resource.from_data_hash(PSON.parse(resource.to_pson)).file.should == "/my/file"
end
it "should include the line if one is set" do
resource = Puppet::Resource.new("File", "/foo")
resource.line = 50
- Puppet::Resource.from_pson(PSON.parse(resource.to_pson)).line.should == 50
+ Puppet::Resource.from_data_hash(PSON.parse(resource.to_pson)).line.should == 50
end
it "should include the 'exported' value if one is set" do
resource = Puppet::Resource.new("File", "/foo")
resource.exported = true
- Puppet::Resource.from_pson(PSON.parse(resource.to_pson)).exported?.should be_true
+ Puppet::Resource.from_data_hash(PSON.parse(resource.to_pson)).exported?.should be_true
end
it "should set 'exported' to false if no value is set" do
resource = Puppet::Resource.new("File", "/foo")
- Puppet::Resource.from_pson(PSON.parse(resource.to_pson)).exported?.should be_false
+ Puppet::Resource.from_data_hash(PSON.parse(resource.to_pson)).exported?.should be_false
end
it "should set all of its parameters as the 'parameters' entry" do
resource = Puppet::Resource.new("File", "/foo")
resource[:foo] = %w{bar eh}
resource[:fee] = %w{baz}
- result = Puppet::Resource.from_pson(PSON.parse(resource.to_pson))
+ result = Puppet::Resource.from_data_hash(PSON.parse(resource.to_pson))
result["foo"].should == %w{bar eh}
result["fee"].should == %w{baz}
end
it "should serialize relationships as reference strings" do
resource = Puppet::Resource.new("File", "/foo")
resource[:requires] = Puppet::Resource.new("File", "/bar")
- result = Puppet::Resource.from_pson(PSON.parse(resource.to_pson))
+ result = Puppet::Resource.from_data_hash(PSON.parse(resource.to_pson))
result[:requires].should == "File[/bar]"
end
it "should serialize multiple relationships as arrays of reference strings" do
resource = Puppet::Resource.new("File", "/foo")
resource[:requires] = [Puppet::Resource.new("File", "/bar"), Puppet::Resource.new("File", "/baz")]
- result = Puppet::Resource.from_pson(PSON.parse(resource.to_pson))
+ result = Puppet::Resource.from_data_hash(PSON.parse(resource.to_pson))
result[:requires].should == [ "File[/bar]", "File[/baz]" ]
end
end
describe "when converting from pson" do
def pson_result_should
Puppet::Resource.expects(:new).with { |hash| yield hash }
end
before do
@data = {
'type' => "file",
'title' => basepath+"/yay",
}
end
it "should set its type to the provided type" do
- Puppet::Resource.from_pson(@data).type.should == "File"
+ Puppet::Resource.from_data_hash(@data).type.should == "File"
end
it "should set its title to the provided title" do
- Puppet::Resource.from_pson(@data).title.should == basepath+"/yay"
+ Puppet::Resource.from_data_hash(@data).title.should == basepath+"/yay"
end
it "should tag the resource with any provided tags" do
@data['tags'] = %w{foo bar}
- resource = Puppet::Resource.from_pson(@data)
+ resource = Puppet::Resource.from_data_hash(@data)
resource.tags.should be_include("foo")
resource.tags.should be_include("bar")
end
it "should set its file to the provided file" do
@data['file'] = "/foo/bar"
- Puppet::Resource.from_pson(@data).file.should == "/foo/bar"
+ Puppet::Resource.from_data_hash(@data).file.should == "/foo/bar"
end
it "should set its line to the provided line" do
@data['line'] = 50
- Puppet::Resource.from_pson(@data).line.should == 50
+ Puppet::Resource.from_data_hash(@data).line.should == 50
end
it "should 'exported' to true if set in the pson data" do
@data['exported'] = true
- Puppet::Resource.from_pson(@data).exported.should be_true
+ Puppet::Resource.from_data_hash(@data).exported.should be_true
end
it "should 'exported' to false if not set in the pson data" do
- Puppet::Resource.from_pson(@data).exported.should be_false
+ Puppet::Resource.from_data_hash(@data).exported.should be_false
end
it "should fail if no title is provided" do
@data.delete('title')
- expect { Puppet::Resource.from_pson(@data) }.to raise_error(ArgumentError)
+ expect { Puppet::Resource.from_data_hash(@data) }.to raise_error(ArgumentError)
end
it "should fail if no type is provided" do
@data.delete('type')
- expect { Puppet::Resource.from_pson(@data) }.to raise_error(ArgumentError)
+ expect { Puppet::Resource.from_data_hash(@data) }.to raise_error(ArgumentError)
end
it "should set each of the provided parameters" do
@data['parameters'] = {'foo' => %w{one two}, 'fee' => %w{three four}}
- resource = Puppet::Resource.from_pson(@data)
+ resource = Puppet::Resource.from_data_hash(@data)
resource['foo'].should == %w{one two}
resource['fee'].should == %w{three four}
end
it "should convert single-value array parameters to normal values" do
@data['parameters'] = {'foo' => %w{one}}
- resource = Puppet::Resource.from_pson(@data)
+ resource = Puppet::Resource.from_data_hash(@data)
resource['foo'].should == %w{one}
end
end
- describe "it should implement copy_as_resource" do
+ it "implements copy_as_resource" do
resource = Puppet::Resource.new("file", "/my/file")
resource.copy_as_resource.should == resource
end
describe "because it is an indirector model" do
it "should include Puppet::Indirector" do
Puppet::Resource.should be_is_a(Puppet::Indirector)
end
it "should have a default terminus" do
Puppet::Resource.indirection.terminus_class.should be
end
it "should have a name" do
Puppet::Resource.new("file", "/my/file").name.should == "File//my/file"
end
end
describe "when resolving resources with a catalog" do
it "should resolve all resources using the catalog" do
catalog = mock 'catalog'
resource = Puppet::Resource.new("foo::bar", "yay")
resource.catalog = catalog
catalog.expects(:resource).with("Foo::Bar[yay]").returns(:myresource)
resource.resolve.should == :myresource
end
end
describe "when generating the uniqueness key" do
it "should include all of the key_attributes in alphabetical order by attribute name" do
Puppet::Type.type(:file).stubs(:key_attributes).returns [:myvar, :owner, :path]
Puppet::Type.type(:file).stubs(:title_patterns).returns(
[ [ /(.*)/, [ [:path, lambda{|x| x} ] ] ] ]
)
res = Puppet::Resource.new("file", "/my/file", :parameters => {:owner => 'root', :content => 'hello'})
res.uniqueness_key.should == [ nil, 'root', '/my/file']
end
end
describe '#parse_title' do
describe 'with a composite namevar' do
before do
Puppet::Type.newtype(:composite) do
newparam(:name)
newparam(:value)
# Configure two title patterns to match a title that is either
# separated with a colon or exclamation point. The first capture
# will be used for the :name param, and the second capture will be
# used for the :value param.
def self.title_patterns
identity = lambda {|x| x }
reverse = lambda {|x| x.reverse }
[
[
/^(.*?):(.*?)$/,
[
[:name, identity],
[:value, identity],
]
],
[
/^(.*?)!(.*?)$/,
[
[:name, reverse],
[:value, reverse],
]
],
]
end
end
end
describe "with no matching title patterns" do
subject { Puppet::Resource.new(:composite, 'unmatching title')}
it "should raise an exception if no title patterns match" do
expect do
subject.to_hash
end.to raise_error(Puppet::Error, /No set of title patterns matched/)
end
end
describe "with a matching title pattern" do
subject { Puppet::Resource.new(:composite, 'matching:title') }
it "should not raise an exception if there was a match" do
expect do
subject.to_hash
end.to_not raise_error
end
it "should set the resource parameters from the parsed title values" do
h = subject.to_hash
h[:name].should == 'matching'
h[:value].should == 'title'
end
end
describe "and multiple title patterns" do
subject { Puppet::Resource.new(:composite, 'matching!title') }
it "should use the first title pattern that matches" do
h = subject.to_hash
h[:name].should == 'gnihctam'
h[:value].should == 'eltit'
end
end
end
end
describe "#prune_parameters" do
before do
Puppet.newtype('blond') do
newproperty(:ensure)
newproperty(:height)
newproperty(:weight)
newproperty(:sign)
newproperty(:friends)
newparam(:admits_to_dying_hair)
newparam(:admits_to_age)
newparam(:name)
end
end
it "should strip all parameters and strip properties that are nil, empty or absent except for ensure" do
resource = Puppet::Resource.new("blond", "Bambi", :parameters => {
:ensure => 'absent',
:height => '',
:weight => 'absent',
:friends => [],
:admits_to_age => true,
:admits_to_dying_hair => false
})
pruned_resource = resource.prune_parameters
pruned_resource.should == Puppet::Resource.new("blond", "Bambi", :parameters => {:ensure => 'absent'})
end
it "should leave parameters alone if in parameters_to_include" do
resource = Puppet::Resource.new("blond", "Bambi", :parameters => {
:admits_to_age => true,
:admits_to_dying_hair => false
})
pruned_resource = resource.prune_parameters(:parameters_to_include => [:admits_to_dying_hair])
pruned_resource.should == Puppet::Resource.new("blond", "Bambi", :parameters => {:admits_to_dying_hair => false})
end
it "should leave properties if not nil, absent or empty" do
resource = Puppet::Resource.new("blond", "Bambi", :parameters => {
:ensure => 'silly',
:height => '7 ft 5 in',
:friends => ['Oprah'],
})
pruned_resource = resource.prune_parameters
pruned_resource.should ==
resource = Puppet::Resource.new("blond", "Bambi", :parameters => {
:ensure => 'silly',
:height => '7 ft 5 in',
:friends => ['Oprah'],
})
end
end
end
diff --git a/spec/unit/run_spec.rb b/spec/unit/run_spec.rb
index 266f65334..c1b1d8a93 100755
--- a/spec/unit/run_spec.rb
+++ b/spec/unit/run_spec.rb
@@ -1,175 +1,175 @@
#! /usr/bin/env ruby
require 'spec_helper'
require 'puppet/agent'
require 'puppet/run'
describe Puppet::Run do
before do
@runner = Puppet::Run.new
end
it "should indirect :run" do
Puppet::Run.indirection.name.should == :run
end
it "should use a configurer agent as its agent" do
agent = mock 'agent'
Puppet::Agent.expects(:new).with(Puppet::Configurer, anything).returns agent
@runner.agent.should equal(agent)
end
it "should accept options at initialization" do
expect do
Puppet::Run.new(
:background => true,
:tags => 'tag',
:ignoreschedules => false,
:pluginsync => true)
end.not_to raise_error
end
it "should not accept arbitrary options" do
lambda { Puppet::Run.new(:foo => true) }.should raise_error(ArgumentError)
end
it "should default to running in the foreground" do
Puppet::Run.new.should_not be_background
end
it "should default to its options being an empty hash" do
Puppet::Run.new.options.should == {}
end
it "should accept :tags for the agent" do
Puppet::Run.new(:tags => "foo").options[:tags].should == "foo"
end
it "should accept :ignoreschedules for the agent" do
Puppet::Run.new(:ignoreschedules => true).options[:ignoreschedules].should be_true
end
it "should accept an option to configure it to run in the background" do
Puppet::Run.new(:background => true).should be_background
end
it "should retain the background option" do
Puppet::Run.new(:background => true).options[:background].should be_nil
end
describe "when asked to run" do
before do
@agent = stub 'agent', :run => nil, :running? => false
@runner.stubs(:agent).returns @agent
end
it "should run its agent" do
agent = stub 'agent2', :running? => false
@runner.stubs(:agent).returns agent
agent.expects(:run)
@runner.run
end
it "should pass any of its options on to the agent" do
@runner.stubs(:options).returns(:foo => :bar)
@agent.expects(:run).with(:foo => :bar)
@runner.run
end
it "should log its run using the provided options" do
@runner.expects(:log_run)
@runner.run
end
it "should set its status to 'already_running' if the agent is already running" do
@agent.expects(:running?).returns true
@runner.run
@runner.status.should == "running"
end
it "should set its status to 'success' if the agent is run" do
@agent.expects(:running?).returns false
@runner.run
@runner.status.should == "success"
end
it "should run the agent in a thread if asked to run it in the background" do
Thread.expects(:new)
@runner.expects(:background?).returns true
@agent.expects(:run).never # because our thread didn't yield
@runner.run
end
it "should run the agent directly if asked to run it in the foreground" do
Thread.expects(:new).never
@runner.expects(:background?).returns false
@agent.expects(:run)
@runner.run
end
end
- describe ".from_pson" do
+ describe ".from_data_hash" do
it "should read from a hash that represents the 'options' to initialize" do
options = {
"tags" => "whatever",
"background" => true,
"ignoreschedules" => false,
}
- run = Puppet::Run.from_pson(options)
+ run = Puppet::Run.from_data_hash(options)
run.options.should == {
:tags => "whatever",
:pluginsync => Puppet[:pluginsync],
:ignoreschedules => false,
}
run.background.should be_true
end
it "should read from a hash that follows the actual object structure" do
hash = {"background" => true,
"options" => {
"pluginsync" => true,
"tags" => [],
"ignoreschedules" => false},
"status" => "success"}
- run = Puppet::Run.from_pson(hash)
+ run = Puppet::Run.from_data_hash(hash)
run.options.should == {
:pluginsync => true,
:tags => [],
:ignoreschedules => false
}
run.background.should be_true
run.status.should == 'success'
end
it "should round trip through pson" do
run = Puppet::Run.new(
:tags => ['a', 'b', 'c'],
:ignoreschedules => true,
:pluginsync => false,
:background => true
)
run.instance_variable_set(:@status, true)
tripped = Puppet::Run.convert_from(:pson, run.render(:pson))
tripped.options.should == run.options
tripped.status.should == run.status
tripped.background.should == run.background
end
end
end
diff --git a/spec/unit/settings/autosign_setting_spec.rb b/spec/unit/settings/autosign_setting_spec.rb
index 5a7105153..0c8184c8a 100644
--- a/spec/unit/settings/autosign_setting_spec.rb
+++ b/spec/unit/settings/autosign_setting_spec.rb
@@ -1,103 +1,103 @@
require 'spec_helper'
require 'puppet/settings'
require 'puppet/settings/autosign_setting'
describe Puppet::Settings::AutosignSetting do
let(:settings) do
s = stub('settings')
s.stubs(:[]).with(:mkusers).returns true
s.stubs(:[]).with(:user).returns 'puppet'
s.stubs(:[]).with(:group).returns 'puppet'
s.stubs(:[]).with(:manage_internal_file_permissions).returns true
s
end
let(:setting) { described_class.new(:name => 'autosign', :section => 'section', :settings => settings, :desc => "test") }
it "is of type :file" do
expect(setting.type).to eq :file
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
describe "setting additional setting values" do
it "can set the file mode" do
setting.mode = '0664'
expect(setting.mode).to eq '0664'
end
it "can set the file owner" do
setting.owner = 'service'
expect(setting.owner).to eq 'puppet'
end
it "can set the file group" do
setting.group = 'service'
expect(setting.group).to eq 'puppet'
end
end
describe "converting the setting to a resource" do
it "converts the file path to a file resource" do
path = File.expand_path('/path/to/autosign.conf')
settings.stubs(:value).with('autosign').returns(path)
- Puppet::FileSystem::File.stubs(:exist?).with(path).returns true
+ Puppet::FileSystem.stubs(:exist?).with(path).returns true
Puppet.stubs(:features).returns(stub(:root? => true, :microsoft_windows? => false))
setting.mode = '0664'
setting.owner = 'service'
setting.group = 'service'
resource = setting.to_resource
expect(resource.title).to eq path
expect(resource[:ensure]).to eq :file
expect(resource[:mode]).to eq '664'
expect(resource[:owner]).to eq 'puppet'
expect(resource[:group]).to eq 'puppet'
end
it "returns nil when the setting is a boolean" do
settings.stubs(:value).with('autosign').returns 'true'
setting.mode = '0664'
setting.owner = 'service'
setting.group = 'service'
expect(setting.to_resource).to be_nil
end
end
end
diff --git a/spec/unit/settings/config_file_spec.rb b/spec/unit/settings/config_file_spec.rb
index 0d3112097..bfd56c9a0 100644
--- a/spec/unit/settings/config_file_spec.rb
+++ b/spec/unit/settings/config_file_spec.rb
@@ -1,100 +1,153 @@
#! /usr/bin/env ruby -S rspec
require 'spec_helper'
require 'puppet/settings/config_file'
describe Puppet::Settings::ConfigFile do
NOTHING = {}
def section_containing(data)
meta = data[:meta] || {}
values = data.reject { |key, _| key == :meta }
values.merge({ :_meta => Hash[values.keys.collect { |key| [key, meta[key] || {}] }] })
end
def the_parse_of(*lines)
config.parse_file(filename, lines.join("\n"))
end
let(:identity_transformer) { Proc.new { |value| value } }
let(:config) { Puppet::Settings::ConfigFile.new(identity_transformer) }
let(:filename) { "a/fake/filename.conf" }
+ Conf = Puppet::Settings::ConfigFile::Conf
+ Section = Puppet::Settings::ConfigFile::Section
+ Meta = Puppet::Settings::ConfigFile::Meta
+ NO_META = Puppet::Settings::ConfigFile::NO_META
+
it "interprets an empty file to contain a main section with no entries" do
- the_parse_of("").should == { :main => section_containing(NOTHING) }
+ result = the_parse_of("")
+
+ expect(result).to eq(Conf.new.with_section(Section.new(:main)))
end
it "interprets an empty main section the same as an empty file" do
the_parse_of("").should == config.parse_file(filename, "[main]")
end
it "places an entry in no section in main" do
- the_parse_of("var = value").should == { :main => section_containing(:var => "value") }
+ result = the_parse_of("var = value")
+
+ expect(result).to eq(Conf.new.with_section(Section.new(:main).with_setting(:var, "value", NO_META)))
end
it "places an entry after a section header in that section" do
- the_parse_of("[section]", "var = value").should == { :main => section_containing(NOTHING),
- :section => section_containing(:var => "value") }
+ result = the_parse_of("[section]", "var = value")
+
+ expect(result).to eq(Conf.new.
+ with_section(Section.new(:main)).
+ with_section(Section.new(:section).
+ with_setting(:var, "value", NO_META)))
end
it "does not include trailing whitespace in the value" do
- the_parse_of("var = value\t ").should == { :main => section_containing(:var => "value") }
+ result = the_parse_of("var = value\t ")
+
+ expect(result).to eq(Conf.new.
+ with_section(Section.new(:main).
+ with_setting(:var, "value", NO_META)))
end
it "does not include leading whitespace in the name" do
- the_parse_of(" \t var=value").should == { :main => section_containing(:var => "value") }
+ result = the_parse_of(" \t var=value")
+
+ expect(result).to eq(Conf.new.
+ with_section(Section.new(:main).
+ with_setting(:var, "value", NO_META)))
end
it "skips lines that are commented out" do
- the_parse_of("#var = value").should == { :main => section_containing(NOTHING) }
+ result = the_parse_of("#var = value")
+
+ expect(result).to eq(Conf.new.with_section(Section.new(:main)))
end
it "skips lines that are entirely whitespace" do
- the_parse_of(" \t ").should == { :main => section_containing(NOTHING) }
+ result = the_parse_of(" \t ")
+
+ expect(result).to eq(Conf.new.with_section(Section.new(:main)))
end
it "errors when a line is not a known form" do
expect { the_parse_of("unknown") }.to raise_error Puppet::Settings::ParseError, /Could not match line/
end
+ it "errors providing correct line number when line is not a known form" do
+ multi_line_config = <<-EOF
+[main]
+foo=bar
+badline
+ EOF
+ expect { the_parse_of(multi_line_config) }.to(
+ raise_error(Puppet::Settings::ParseError, /Could not match line/) do |exception|
+ expect(exception.line).to eq(3)
+ end
+ )
+ end
+
it "stores file meta information in the _meta section" do
- the_parse_of("var = value { owner = me, group = you, mode = 0666 }").should ==
- { :main => section_containing(:var => "value", :meta => { :var => { :owner => "me",
- :group => "you",
- :mode => "0666" } }) }
+ result = the_parse_of("var = value { owner = me, group = you, mode = 0666 }")
+
+ expect(result).to eq(Conf.new.with_section(Section.new(:main).
+ with_setting(:var, "value",
+ Meta.new("me", "you", "0666"))))
end
it "errors when there is unknown meta information" do
expect { the_parse_of("var = value { unknown = no }") }.
to raise_error ArgumentError, /Invalid file option 'unknown'/
end
it "errors when the mode is not numeric" do
expect { the_parse_of("var = value { mode = no }") }.
to raise_error ArgumentError, "File modes must be numbers"
end
it "errors when the options are not key-value pairs" do
expect { the_parse_of("var = value { mode }") }.
to raise_error ArgumentError, "Could not parse 'value { mode }'"
end
it "errors when an application_defaults section is created" do
expect { the_parse_of("[application_defaults]") }.
to raise_error Puppet::Error,
- "Illegal section 'application_defaults' in config file #{filename} at line [application_defaults]"
+ "Illegal section 'application_defaults' in config file #{filename} at line 1"
+ end
+
+ it "errors when a global_defaults section is created" do
+ expect { the_parse_of("[main]\n[global_defaults]") }.
+ to raise_error Puppet::Error,
+ "Illegal section 'global_defaults' in config file #{filename} at line 2"
end
it "transforms values with the given function" do
config = Puppet::Settings::ConfigFile.new(Proc.new { |value| value + " changed" })
- config.parse_file(filename, "var = value").should == { :main => section_containing(:var => "value changed") }
+ result = config.parse_file(filename, "var = value")
+
+ expect(result).to eq(Conf.new.
+ with_section(Section.new(:main).
+ with_setting(:var, "value changed", NO_META)))
end
it "does not try to transform an entry named 'mode'" do
config = Puppet::Settings::ConfigFile.new(Proc.new { raise "Should not transform" })
- config.parse_file(filename, "mode = value").should == { :main => section_containing(:mode => "value") }
+ result = config.parse_file(filename, "mode = value")
+
+ expect(result).to eq(Conf.new.
+ with_section(Section.new(:main).
+ with_setting(:mode, "value", NO_META)))
end
end
diff --git a/spec/unit/settings/file_setting_spec.rb b/spec/unit/settings/file_setting_spec.rb
index d9c6f5629..b31d0ccb3 100755
--- a/spec/unit/settings/file_setting_spec.rb
+++ b/spec/unit/settings/file_setting_spec.rb
@@ -1,297 +1,297 @@
#! /usr/bin/env ruby
require 'spec_helper'
require 'puppet/settings'
require 'puppet/settings/file_setting'
describe Puppet::Settings::FileSetting do
FileSetting = Puppet::Settings::FileSetting
include PuppetSpec::Files
describe "when controlling permissions" do
def settings(wanted_values = {})
real_values = {
:user => 'root',
:group => 'root',
:mkusers => false,
:service_user_available? => false,
:service_group_available? => false
}.merge(wanted_values)
settings = mock("settings")
settings.stubs(:[]).with(:user).returns real_values[:user]
settings.stubs(:[]).with(:group).returns real_values[:group]
settings.stubs(:[]).with(:mkusers).returns real_values[:mkusers]
settings.stubs(:service_user_available?).returns real_values[:service_user_available?]
settings.stubs(:service_group_available?).returns real_values[:service_group_available?]
settings
end
context "owner" do
it "can always be root" do
settings = settings(:user => "the_service", :mkusers => true)
setting = FileSetting.new(:settings => settings, :owner => "root", :desc => "a setting")
setting.owner.should == "root"
end
it "is the service user if we are making users" do
settings = settings(:user => "the_service", :mkusers => true, :service_user_available? => false)
setting = FileSetting.new(:settings => settings, :owner => "service", :desc => "a setting")
setting.owner.should == "the_service"
end
it "is the service user if the user is available on the system" do
settings = settings(:user => "the_service", :mkusers => false, :service_user_available? => true)
setting = FileSetting.new(:settings => settings, :owner => "service", :desc => "a setting")
setting.owner.should == "the_service"
end
it "is root when the setting specifies service and the user is not available on the system" do
settings = settings(:user => "the_service", :mkusers => false, :service_user_available? => false)
setting = FileSetting.new(:settings => settings, :owner => "service", :desc => "a setting")
setting.owner.should == "root"
end
it "is unspecified when no specific owner is wanted" do
FileSetting.new(:settings => settings(), :desc => "a setting").owner.should be_nil
end
it "does not allow other owners" do
expect { FileSetting.new(:settings => settings(), :desc => "a setting", :name => "testing", :default => "the default", :owner => "invalid") }.
to raise_error(FileSetting::SettingError, /The :owner parameter for the setting 'testing' must be either 'root' or 'service'/)
end
end
context "group" do
it "is unspecified when no specific group is wanted" do
setting = FileSetting.new(:settings => settings(), :desc => "a setting")
setting.group.should be_nil
end
it "is root if root is requested" do
settings = settings(:group => "the_group")
setting = FileSetting.new(:settings => settings, :group => "root", :desc => "a setting")
setting.group.should == "root"
end
it "is the service group if we are making users" do
settings = settings(:group => "the_service", :mkusers => true)
setting = FileSetting.new(:settings => settings, :group => "service", :desc => "a setting")
setting.group.should == "the_service"
end
it "is the service user if the group is available on the system" do
settings = settings(:group => "the_service", :mkusers => false, :service_group_available? => true)
setting = FileSetting.new(:settings => settings, :group => "service", :desc => "a setting")
setting.group.should == "the_service"
end
it "is unspecified when the setting specifies service and the group is not available on the system" do
settings = settings(:group => "the_service", :mkusers => false, :service_group_available? => false)
setting = FileSetting.new(:settings => settings, :group => "service", :desc => "a setting")
setting.group.should be_nil
end
it "does not allow other groups" do
expect { FileSetting.new(:settings => settings(), :group => "invalid", :name => 'testing', :desc => "a setting") }.
to raise_error(FileSetting::SettingError, /The :group parameter for the setting 'testing' must be either 'root' or 'service'/)
end
end
end
it "should be able to be converted into a resource" do
FileSetting.new(:settings => mock("settings"), :desc => "eh").should respond_to(:to_resource)
end
describe "when being converted to a resource" do
before do
@basepath = make_absolute("/somepath")
@settings = mock 'settings'
@file = Puppet::Settings::FileSetting.new(:settings => @settings, :desc => "eh", :name => :myfile, :section => "mysect")
@file.stubs(:create_files?).returns true
@settings.stubs(:value).with(:myfile).returns @basepath
end
it "should return :file as its type" do
@file.type.should == :file
end
it "should skip non-existent files if 'create_files' is not enabled" do
@file.expects(:create_files?).returns false
@file.expects(:type).returns :file
- Puppet::FileSystem::File.expects(:exist?).with(@basepath).returns false
+ Puppet::FileSystem.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
- Puppet::FileSystem::File.expects(:exist?).with(@basepath).returns true
+ Puppet::FileSystem.expects(:exist?).with(@basepath).returns true
@file.to_resource.should be_instance_of(Puppet::Resource)
end
describe "on POSIX systems", :if => Puppet.features.posix? do
it "should skip files in /dev" do
@settings.stubs(:value).with(:myfile).returns "/dev/file"
@file.to_resource.should be_nil
end
end
it "should skip files whose paths are not strings" do
@settings.stubs(:value).with(:myfile).returns :foo
@file.to_resource.should be_nil
end
it "should return a file resource with the path set appropriately" do
resource = @file.to_resource
resource.type.should == "File"
resource.title.should == @basepath
end
it "should fully qualified returned files if necessary (#795)" do
@settings.stubs(:value).with(:myfile).returns "myfile"
path = File.expand_path('myfile')
@file.to_resource.title.should == path
end
it "should set the mode on the file if a mode is provided as an octal number" do
@file.mode = 0755
@file.to_resource[:mode].should == '755'
end
it "should set the mode on the file if a mode is provided as a string" do
@file.mode = '0755'
@file.to_resource[:mode].should == '755'
end
it "should not set the mode on a the file if manage_internal_file_permissions is disabled" do
Puppet[:manage_internal_file_permissions] = false
@file.stubs(:mode).returns(0755)
@file.to_resource[:mode].should == nil
end
it "should set the owner if running as root and the owner is provided" do
Puppet.features.expects(:root?).returns true
Puppet.features.stubs(:microsoft_windows?).returns false
@file.stubs(:owner).returns "foo"
@file.to_resource[:owner].should == "foo"
end
it "should not set the owner if manage_internal_file_permissions is disabled" do
Puppet[:manage_internal_file_permissions] = false
Puppet.features.stubs(:root?).returns true
@file.stubs(:owner).returns "foo"
@file.to_resource[:owner].should == nil
end
it "should set the group if running as root and the group is provided" do
Puppet.features.expects(:root?).returns true
Puppet.features.stubs(:microsoft_windows?).returns false
@file.stubs(:group).returns "foo"
@file.to_resource[:group].should == "foo"
end
it "should not set the group if manage_internal_file_permissions is disabled" do
Puppet[:manage_internal_file_permissions] = false
Puppet.features.stubs(:root?).returns true
@file.stubs(:group).returns "foo"
@file.to_resource[:group].should == nil
end
it "should not set owner if not running as root" do
Puppet.features.expects(:root?).returns false
Puppet.features.stubs(:microsoft_windows?).returns false
@file.stubs(:owner).returns "foo"
@file.to_resource[:owner].should be_nil
end
it "should not set group if not running as root" do
Puppet.features.expects(:root?).returns false
Puppet.features.stubs(:microsoft_windows?).returns false
@file.stubs(:group).returns "foo"
@file.to_resource[:group].should be_nil
end
describe "on Microsoft Windows systems" do
before :each do
Puppet.features.stubs(:microsoft_windows?).returns true
end
it "should not set owner" do
@file.stubs(:owner).returns "foo"
@file.to_resource[:owner].should be_nil
end
it "should not set group" do
@file.stubs(:group).returns "foo"
@file.to_resource[:group].should be_nil
end
end
it "should set :ensure to the file type" do
@file.expects(:type).returns :directory
@file.to_resource[:ensure].should == :directory
end
it "should set the loglevel to :debug" do
@file.to_resource[:loglevel].should == :debug
end
it "should set the backup to false" do
@file.to_resource[:backup].should be_false
end
it "should tag the resource with the settings section" do
@file.expects(:section).returns "mysect"
@file.to_resource.should be_tagged("mysect")
end
it "should tag the resource with the setting name" do
@file.to_resource.should be_tagged("myfile")
end
it "should tag the resource with 'settings'" do
@file.to_resource.should be_tagged("settings")
end
it "should set links to 'follow'" do
@file.to_resource[:links].should == :follow
end
end
describe "#munge" do
it 'does not expand the path of the special value :memory: so we can set dblocation to an in-memory database' do
filesetting = FileSetting.new(:settings => mock("settings"), :desc => "eh")
filesetting.munge(':memory:').should == ':memory:'
end
end
end
diff --git a/spec/unit/settings/ini_file_spec.rb b/spec/unit/settings/ini_file_spec.rb
new file mode 100644
index 000000000..11e698a52
--- /dev/null
+++ b/spec/unit/settings/ini_file_spec.rb
@@ -0,0 +1,184 @@
+require 'spec_helper'
+require 'stringio'
+
+require 'puppet/settings/ini_file'
+
+describe Puppet::Settings::IniFile do
+ it "preserves the file when no changes are made" do
+ original_config = <<-CONFIG
+ # comment
+ [section]
+ name = value
+ CONFIG
+ config_fh = a_config_file_containing(original_config)
+
+ Puppet::Settings::IniFile.update(config_fh) do; end
+
+ expect(config_fh.string).to eq original_config
+ end
+
+ it "adds a set name and value to an empty file" do
+ config_fh = a_config_file_containing("")
+
+ Puppet::Settings::IniFile.update(config_fh) do |config|
+ config.set("the_section", "name", "value")
+ end
+
+ expect(config_fh.string).to eq "[the_section]\nname = value\n"
+ end
+
+ it "does not add a [main] section to a file when it isn't needed" do
+ config_fh = a_config_file_containing(<<-CONF)
+ [section]
+ name = different value
+ CONF
+
+
+ Puppet::Settings::IniFile.update(config_fh) do |config|
+ config.set("main", "name", "value")
+ end
+
+ expect(config_fh.string).to eq(<<-CONF)
+name = value
+ [section]
+ name = different value
+ CONF
+ end
+
+ it "preserves comments when writing a new name and value" do
+ config_fh = a_config_file_containing("# this is a comment")
+
+ Puppet::Settings::IniFile.update(config_fh) do |config|
+ config.set("the_section", "name", "value")
+ end
+
+ expect(config_fh.string).to eq "# this is a comment\n[the_section]\nname = value\n"
+ end
+
+ it "updates existing names and values in place" do
+ config_fh = a_config_file_containing(<<-CONFIG)
+ # this is the preceeding comment
+ [section]
+ name = original value
+ # this is the trailing comment
+ CONFIG
+
+ Puppet::Settings::IniFile.update(config_fh) do |config|
+ config.set("section", "name", "changed value")
+ end
+
+ expect(config_fh.string).to eq <<-CONFIG
+ # this is the preceeding comment
+ [section]
+ name = changed value
+ # this is the trailing comment
+ CONFIG
+ end
+
+ it "updates only the value in the selected section" do
+ config_fh = a_config_file_containing(<<-CONFIG)
+ [other_section]
+ name = does not change
+ [section]
+ name = original value
+ CONFIG
+
+ Puppet::Settings::IniFile.update(config_fh) do |config|
+ config.set("section", "name", "changed value")
+ end
+
+ expect(config_fh.string).to eq <<-CONFIG
+ [other_section]
+ name = does not change
+ [section]
+ name = changed value
+ CONFIG
+ end
+
+ it "considers settings outside a section to be in section 'main'" do
+ config_fh = a_config_file_containing(<<-CONFIG)
+ name = original value
+ CONFIG
+
+ Puppet::Settings::IniFile.update(config_fh) do |config|
+ config.set("main", "name", "changed value")
+ end
+
+ expect(config_fh.string).to eq <<-CONFIG
+ name = changed value
+ CONFIG
+ end
+
+ it "adds new settings to an existing section" do
+ config_fh = a_config_file_containing(<<-CONFIG)
+ [section]
+ original = value
+
+ # comment about 'other' section
+ [other]
+ dont = change
+ CONFIG
+
+ Puppet::Settings::IniFile.update(config_fh) do |config|
+ config.set("section", "updated", "new")
+ end
+
+ expect(config_fh.string).to eq <<-CONFIG
+ [section]
+ original = value
+updated = new
+
+ # comment about 'other' section
+ [other]
+ dont = change
+ CONFIG
+ end
+
+ it "adds a new setting into an existing, yet empty section" do
+ config_fh = a_config_file_containing(<<-CONFIG)
+ [section]
+ [other]
+ dont = change
+ CONFIG
+
+ Puppet::Settings::IniFile.update(config_fh) do |config|
+ config.set("section", "updated", "new")
+ end
+
+ expect(config_fh.string).to eq <<-CONFIG
+ [section]
+updated = new
+ [other]
+ dont = change
+ CONFIG
+ end
+
+ it "finds settings when the section is split up" do
+ config_fh = a_config_file_containing(<<-CONFIG)
+ [section]
+ name = original value
+ [different]
+ name = other value
+ [section]
+ other_name = different original value
+ CONFIG
+
+ Puppet::Settings::IniFile.update(config_fh) do |config|
+ config.set("section", "name", "changed value")
+ config.set("section", "other_name", "other changed value")
+ end
+
+ expect(config_fh.string).to eq <<-CONFIG
+ [section]
+ name = changed value
+ [different]
+ name = other value
+ [section]
+ other_name = other changed value
+ CONFIG
+ end
+
+ def a_config_file_containing(text)
+ StringIO.new(text)
+ end
+end
diff --git a/spec/unit/settings_spec.rb b/spec/unit/settings_spec.rb
index b3dc31f3d..df4784a59 100755
--- a/spec/unit/settings_spec.rb
+++ b/spec/unit/settings_spec.rb
@@ -1,1677 +1,1735 @@
#! /usr/bin/env ruby
require 'spec_helper'
require 'ostruct'
require 'puppet/settings/errors'
describe Puppet::Settings do
include PuppetSpec::Files
let(:main_config_file_default_location) do
File.join(Puppet::Util::RunMode[:master].conf_dir, "puppet.conf")
end
let(:user_config_file_default_location) do
File.join(Puppet::Util::RunMode[:user].conf_dir, "puppet.conf")
end
describe "when specifying defaults" do
before do
@settings = Puppet::Settings.new
end
it "should start with no defined parameters" do
@settings.params.length.should == 0
end
it "should not allow specification of default values associated with a section as an array" do
expect {
@settings.define_settings(:section, :myvalue => ["defaultval", "my description"])
}.to raise_error
end
it "should not allow duplicate parameter specifications" do
@settings.define_settings(:section, :myvalue => { :default => "a", :desc => "b" })
lambda { @settings.define_settings(:section, :myvalue => { :default => "c", :desc => "d" }) }.should raise_error(ArgumentError)
end
it "should allow specification of default values associated with a section as a hash" do
@settings.define_settings(:section, :myvalue => {:default => "defaultval", :desc => "my description"})
end
it "should consider defined parameters to be valid" do
@settings.define_settings(:section, :myvalue => { :default => "defaultval", :desc => "my description" })
@settings.valid?(:myvalue).should be_true
end
it "should require a description when defaults are specified with a hash" do
lambda { @settings.define_settings(:section, :myvalue => {:default => "a value"}) }.should raise_error(ArgumentError)
end
it "should support specifying owner, group, and mode when specifying files" do
@settings.define_settings(:section, :myvalue => {:type => :file, :default => "/some/file", :owner => "service", :mode => "boo", :group => "service", :desc => "whatever"})
end
it "should support specifying a short name" do
@settings.define_settings(:section, :myvalue => {:default => "w", :desc => "b", :short => "m"})
end
it "should support specifying the setting type" do
@settings.define_settings(:section, :myvalue => {:default => "/w", :desc => "b", :type => :string})
@settings.setting(:myvalue).should be_instance_of(Puppet::Settings::StringSetting)
end
it "should fail if an invalid setting type is specified" do
lambda { @settings.define_settings(:section, :myvalue => {:default => "w", :desc => "b", :type => :foo}) }.should raise_error(ArgumentError)
end
it "should fail when short names conflict" do
@settings.define_settings(:section, :myvalue => {:default => "w", :desc => "b", :short => "m"})
lambda { @settings.define_settings(:section, :myvalue => {:default => "w", :desc => "b", :short => "m"}) }.should raise_error(ArgumentError)
end
end
describe "when initializing application defaults do" do
let(:default_values) do
values = {}
PuppetSpec::Settings::TEST_APP_DEFAULT_DEFINITIONS.keys.each do |key|
values[key] = 'default value'
end
values
end
before do
@settings = Puppet::Settings.new
@settings.define_settings(:main, PuppetSpec::Settings::TEST_APP_DEFAULT_DEFINITIONS)
end
it "should fail if the app defaults hash is missing any required values" do
incomplete_default_values = default_values.reject { |key, _| key == :confdir }
expect {
@settings.initialize_app_defaults(default_values.reject { |key, _| key == :confdir })
}.to raise_error(Puppet::Settings::SettingsError)
end
# ultimately I'd like to stop treating "run_mode" as a normal setting, because it has so many special
# case behaviors / uses. However, until that time... we need to make sure that our private run_mode=
# setter method gets properly called during app initialization.
it "sets the preferred run mode when initializing the app defaults" do
@settings.initialize_app_defaults(default_values.merge(:run_mode => :master))
@settings.preferred_run_mode.should == :master
end
end
describe "#call_hooks_deferred_to_application_initialization" do
- let (:good_default) { "yay" }
- let (:bad_default) { "$doesntexist" }
+ let(:good_default) { "yay" }
+ let(:bad_default) { "$doesntexist" }
before(:each) do
@settings = Puppet::Settings.new
end
describe "when ignoring dependency interpolation errors" do
let(:options) { {:ignore_interpolation_dependency_errors => true} }
describe "if interpolation error" do
it "should not raise an error" do
hook_values = []
@settings.define_settings(:section, :badhook => {:default => bad_default, :desc => "boo", :call_hook => :on_initialize_and_write, :hook => lambda { |v| hook_values << v }})
expect do
@settings.send(:call_hooks_deferred_to_application_initialization, options)
end.to_not raise_error
end
end
describe "if no interpolation error" do
it "should not raise an error" do
hook_values = []
@settings.define_settings(:section, :goodhook => {:default => good_default, :desc => "boo", :call_hook => :on_initialize_and_write, :hook => lambda { |v| hook_values << v }})
expect do
@settings.send(:call_hooks_deferred_to_application_initialization, options)
end.to_not raise_error
end
end
end
describe "when not ignoring dependency interpolation errors" do
[ {}, {:ignore_interpolation_dependency_errors => false}].each do |options|
describe "if interpolation error" do
it "should raise an error" do
hook_values = []
@settings.define_settings(
:section,
:badhook => {
:default => bad_default,
:desc => "boo",
:call_hook => :on_initialize_and_write,
:hook => lambda { |v| hook_values << v }
}
)
expect do
@settings.send(:call_hooks_deferred_to_application_initialization, options)
- end.to raise_error Puppet::Settings::InterpolationError
+ end.to raise_error(Puppet::Settings::InterpolationError)
end
it "should contain the setting name in error message" do
hook_values = []
@settings.define_settings(
:section,
:badhook => {
:default => bad_default,
:desc => "boo",
:call_hook => :on_initialize_and_write,
:hook => lambda { |v| hook_values << v }
}
)
expect do
@settings.send(:call_hooks_deferred_to_application_initialization, options)
- end.to raise_error Puppet::Settings::InterpolationError, /badhook/
+ end.to raise_error(Puppet::Settings::InterpolationError, /badhook/)
end
end
describe "if no interpolation error" do
it "should not raise an error" do
hook_values = []
@settings.define_settings(
:section,
:goodhook => {
:default => good_default,
:desc => "boo",
:call_hook => :on_initialize_and_write,
:hook => lambda { |v| hook_values << v }
}
)
expect do
@settings.send(:call_hooks_deferred_to_application_initialization, options)
end.to_not raise_error
end
end
end
end
end
describe "when setting values" do
before do
@settings = Puppet::Settings.new
@settings.define_settings :main, :myval => { :default => "val", :desc => "desc" }
@settings.define_settings :main, :bool => { :type => :boolean, :default => true, :desc => "desc" }
end
it "should provide a method for setting values from other objects" do
@settings[:myval] = "something else"
@settings[:myval].should == "something else"
end
it "should support a getopt-specific mechanism for setting values" do
@settings.handlearg("--myval", "newval")
@settings[:myval].should == "newval"
end
it "should support a getopt-specific mechanism for turning booleans off" do
- @settings[:bool] = true
+ @settings.override_default(:bool, true)
@settings.handlearg("--no-bool", "")
@settings[:bool].should == false
end
it "should support a getopt-specific mechanism for turning booleans on" do
# Turn it off first
- @settings[:bool] = false
+ @settings.override_default(:bool, false)
@settings.handlearg("--bool", "")
@settings[:bool].should == true
end
it "should consider a cli setting with no argument to be a boolean" do
# Turn it off first
- @settings[:bool] = false
+ @settings.override_default(:bool, false)
@settings.handlearg("--bool")
@settings[:bool].should == true
end
- it "should consider a cli setting with an empty string as an argument to be a boolean, if the setting itself is a boolean" do
- # Turn it off first
- @settings[:bool] = false
- @settings.handlearg("--bool", "")
- @settings[:bool].should == true
- end
-
it "should consider a cli setting with an empty string as an argument to be an empty argument, if the setting itself is not a boolean" do
- @settings[:myval] = "bob"
+ @settings.override_default(:myval, "bob")
@settings.handlearg("--myval", "")
@settings[:myval].should == ""
end
it "should consider a cli setting with a boolean as an argument to be a boolean" do
# Turn it off first
- @settings[:bool] = false
+ @settings.override_default(:bool, false)
@settings.handlearg("--bool", "true")
@settings[:bool].should == true
end
it "should not consider a cli setting of a non boolean with a boolean as an argument to be a boolean" do
- # Turn it off first
- @settings[:myval] = "bob"
+ @settings.override_default(:myval, "bob")
@settings.handlearg("--no-myval", "")
@settings[:myval].should == ""
end
it "should flag string settings from the CLI" do
@settings.handlearg("--myval", "12")
@settings.set_by_cli?(:myval).should be_true
end
it "should flag bool settings from the CLI" do
- @settings[:bool] = false
@settings.handlearg("--bool")
@settings.set_by_cli?(:bool).should be_true
end
it "should not flag settings memory as from CLI" do
@settings[:myval] = "12"
@settings.set_by_cli?(:myval).should be_false
end
describe "setbycli" do
it "should generate a deprecation warning" do
- Puppet.expects(:deprecation_warning)
+ Puppet.expects(:deprecation_warning).at_least(1)
@settings.setting(:myval).setbycli = true
end
it "should set the value" do
@settings[:myval] = "blah"
@settings.setting(:myval).setbycli = true
@settings.set_by_cli?(:myval).should be_true
end
it "should raise error if trying to unset value" do
@settings.handlearg("--myval", "blah")
expect do
@settings.setting(:myval).setbycli = nil
- end.to raise_error ArgumentError, /unset/
+ end.to raise_error(ArgumentError, /unset/)
end
end
it "should clear the cache when setting getopt-specific values" do
@settings.define_settings :mysection,
:one => { :default => "whah", :desc => "yay" },
:two => { :default => "$one yay", :desc => "bah" }
@settings.expects(:unsafe_flush_cache)
@settings[:two].should == "whah yay"
@settings.handlearg("--one", "else")
@settings[:two].should == "else yay"
end
it "should clear the cache when the preferred_run_mode is changed" do
@settings.expects(:flush_cache)
@settings.preferred_run_mode = :master
end
it "should not clear other values when setting getopt-specific values" do
@settings[:myval] = "yay"
@settings.handlearg("--no-bool", "")
@settings[:myval].should == "yay"
end
it "should clear the list of used sections" do
@settings.expects(:clearused)
@settings[:myval] = "yay"
end
describe "call_hook" do
Puppet::Settings::StringSetting.available_call_hook_values.each do |val|
describe "when :#{val}" do
describe "and definition invalid" do
it "should raise error if no hook defined" do
expect do
@settings.define_settings(:section, :hooker => {:default => "yay", :desc => "boo", :call_hook => val})
- end.to raise_error ArgumentError, /no :hook/
+ end.to raise_error(ArgumentError, /no :hook/)
end
it "should include the setting name in the error message" do
expect do
@settings.define_settings(:section, :hooker => {:default => "yay", :desc => "boo", :call_hook => val})
- end.to raise_error ArgumentError, /for :hooker/
+ end.to raise_error(ArgumentError, /for :hooker/)
end
end
describe "and definition valid" do
before(:each) do
hook_values = []
@settings.define_settings(:section, :hooker => {:default => "yay", :desc => "boo", :call_hook => val, :hook => lambda { |v| hook_values << v }})
end
it "should call the hook when value written" do
@settings.setting(:hooker).expects(:handle).with("something").once
@settings[:hooker] = "something"
end
end
end
end
it "should have a default value of :on_write_only" do
@settings.define_settings(:section, :hooker => {:default => "yay", :desc => "boo", :hook => lambda { |v| hook_values << v }})
@settings.setting(:hooker).call_hook.should == :on_write_only
end
describe "when nil" do
it "should generate a warning" do
Puppet.expects(:warning)
@settings.define_settings(:section, :hooker => {:default => "yay", :desc => "boo", :call_hook => nil, :hook => lambda { |v| hook_values << v }})
end
it "should use default" do
@settings.define_settings(:section, :hooker => {:default => "yay", :desc => "boo", :call_hook => nil, :hook => lambda { |v| hook_values << v }})
@settings.setting(:hooker).call_hook.should == :on_write_only
end
end
describe "when invalid" do
it "should raise an error" do
expect do
@settings.define_settings(:section, :hooker => {:default => "yay", :desc => "boo", :call_hook => :foo, :hook => lambda { |v| hook_values << v }})
- end.to raise_error ArgumentError, /invalid.*call_hook/i
+ end.to raise_error(ArgumentError, /invalid.*call_hook/i)
end
end
describe "when :on_define_and_write" do
it "should call the hook at definition" do
hook_values = []
@settings.define_settings(:section, :hooker => {:default => "yay", :desc => "boo", :call_hook => :on_define_and_write, :hook => lambda { |v| hook_values << v }})
@settings.setting(:hooker).call_hook.should == :on_define_and_write
hook_values.should == %w{yay}
end
end
describe "when :on_initialize_and_write" do
before(:each) do
@hook_values = []
@settings.define_settings(:section, :hooker => {:default => "yay", :desc => "boo", :call_hook => :on_initialize_and_write, :hook => lambda { |v| @hook_values << v }})
end
it "should not call the hook at definition" do
@hook_values.should == []
@hook_values.should_not == %w{yay}
end
it "should call the hook at initialization" do
app_defaults = {}
Puppet::Settings::REQUIRED_APP_SETTINGS.each do |key|
app_defaults[key] = "foo"
end
app_defaults[:run_mode] = :user
@settings.define_settings(:main, PuppetSpec::Settings::TEST_APP_DEFAULT_DEFINITIONS)
@settings.setting(:hooker).expects(:handle).with("yay").once
@settings.initialize_app_defaults app_defaults
end
end
end
describe "call_on_define" do
[true, false].each do |val|
describe "to #{val}" do
it "should generate a deprecation warning" do
Puppet.expects(:deprecation_warning)
values = []
@settings.define_settings(:section, :hooker => {:default => "yay", :desc => "boo", :call_on_define => val, :hook => lambda { |v| values << v }})
end
it "should should set call_hook" do
values = []
name = "hooker_#{val}".to_sym
@settings.define_settings(:section, name => {:default => "yay", :desc => "boo", :call_on_define => val, :hook => lambda { |v| values << v }})
@settings.setting(name).call_hook.should == :on_define_and_write if val
@settings.setting(name).call_hook.should == :on_write_only unless val
end
end
end
end
it "should call passed blocks when values are set" do
values = []
@settings.define_settings(:section, :hooker => {:default => "yay", :desc => "boo", :hook => lambda { |v| values << v }})
values.should == []
@settings[:hooker] = "something"
values.should == %w{something}
end
it "should call passed blocks when values are set via the command line" do
values = []
@settings.define_settings(:section, :hooker => {:default => "yay", :desc => "boo", :hook => lambda { |v| values << v }})
values.should == []
@settings.handlearg("--hooker", "yay")
values.should == %w{yay}
end
it "should provide an option to call passed blocks during definition" do
values = []
@settings.define_settings(:section, :hooker => {:default => "yay", :desc => "boo", :call_hook => :on_define_and_write, :hook => lambda { |v| values << v }})
values.should == %w{yay}
end
it "should pass the fully interpolated value to the hook when called on definition" do
values = []
@settings.define_settings(:section, :one => { :default => "test", :desc => "a" })
@settings.define_settings(:section, :hooker => {:default => "$one/yay", :desc => "boo", :call_hook => :on_define_and_write, :hook => lambda { |v| values << v }})
values.should == %w{test/yay}
end
it "should munge values using the setting-specific methods" do
@settings[:bool] = "false"
@settings[:bool].should == false
end
- it "should prefer cli values to values set in Ruby code" do
- @settings.handlearg("--myval", "cliarg")
+ it "should prefer values set in ruby to values set on the cli" do
@settings[:myval] = "memarg"
- @settings[:myval].should == "cliarg"
+ @settings.handlearg("--myval", "cliarg")
+
+ @settings[:myval].should == "memarg"
end
it "should clear the list of environments" do
Puppet::Node::Environment.expects(:clear).at_least(1)
@settings[:myval] = "memarg"
end
it "should raise an error if we try to set a setting that hasn't been defined'" do
lambda{
@settings[:why_so_serious] = "foo"
- }.should raise_error(ArgumentError, /unknown configuration parameter/)
+ }.should raise_error(ArgumentError, /unknown setting/)
+ end
+
+ it "allows overriding cli args based on the cli-set value" do
+ @settings.handlearg("--myval", "cliarg")
+ @settings.set_value(:myval, "modified #{@settings[:myval]}", :cli)
+ expect(@settings[:myval]).to eq("modified cliarg")
end
end
describe "when returning values" do
before do
@settings = Puppet::Settings.new
@settings.define_settings :section,
:config => { :type => :file, :default => "/my/file", :desc => "eh" },
:one => { :default => "ONE", :desc => "a" },
:two => { :default => "$one TWO", :desc => "b"},
:three => { :default => "$one $two THREE", :desc => "c"},
:four => { :default => "$two $three FOUR", :desc => "d"},
:five => { :default => nil, :desc => "e" }
- Puppet::FileSystem::File.stubs(:exist?).returns true
+ Puppet::FileSystem.stubs(:exist?).returns true
end
describe "call_on_define" do
it "should generate a deprecation warning" do
Puppet.expects(:deprecation_warning)
@settings.define_settings(:section, :hooker => {:default => "yay", :desc => "boo", :hook => lambda { |v| hook_values << v }})
@settings.setting(:hooker).call_on_define
end
Puppet::Settings::StringSetting.available_call_hook_values.each do |val|
it "should match value for call_hook => :#{val}" do
hook_values = []
@settings.define_settings(:section, :hooker => {:default => "yay", :desc => "boo", :call_hook => val, :hook => lambda { |v| hook_values << v }})
@settings.setting(:hooker).call_on_define.should == @settings.setting(:hooker).call_hook_on_define?
end
end
end
it "should provide a mechanism for returning set values" do
@settings[:one] = "other"
@settings[:one].should == "other"
end
+ it "setting a value to nil causes it to return to its default" do
+ default_values = { :one => "skipped value" }
+ [:logdir, :confdir, :vardir].each do |key|
+ default_values[key] = 'default value'
+ end
+ @settings.define_settings :main, PuppetSpec::Settings::TEST_APP_DEFAULT_DEFINITIONS
+ @settings.initialize_app_defaults(default_values)
+ @settings[:one] = "value will disappear"
+
+ @settings[:one] = nil
+
+ @settings[:one].should == "ONE"
+ end
+
it "should interpolate default values for other parameters into returned parameter values" do
@settings[:one].should == "ONE"
@settings[:two].should == "ONE TWO"
@settings[:three].should == "ONE ONE TWO THREE"
end
it "should interpolate default values that themselves need to be interpolated" do
@settings[:four].should == "ONE TWO ONE ONE TWO THREE FOUR"
end
it "should provide a method for returning uninterpolated values" do
@settings[:two] = "$one tw0"
@settings.uninterpolated_value(:two).should == "$one tw0"
@settings.uninterpolated_value(:four).should == "$two $three FOUR"
end
it "should interpolate set values for other parameters into returned parameter values" do
@settings[:one] = "on3"
@settings[:two] = "$one tw0"
@settings[:three] = "$one $two thr33"
@settings[:four] = "$one $two $three f0ur"
@settings[:one].should == "on3"
@settings[:two].should == "on3 tw0"
@settings[:three].should == "on3 on3 tw0 thr33"
@settings[:four].should == "on3 on3 tw0 on3 on3 tw0 thr33 f0ur"
end
it "should not cache interpolated values such that stale information is returned" do
@settings[:two].should == "ONE TWO"
@settings[:one] = "one"
@settings[:two].should == "one TWO"
end
- describe "caching values that evaluate to false" do
- it "caches nil" do
- @settings.expects(:convert).once.returns nil
- @settings[:five].should be_nil
- @settings[:five].should be_nil
- end
-
- it "caches false" do
- @settings.expects(:convert).once.returns false
- @settings[:five].should == false
- @settings[:five].should == false
- end
- end
-
it "should not cache values such that information from one environment is returned for another environment" do
text = "[env1]\none = oneval\n[env2]\none = twoval\n"
@settings.stubs(:read_file).returns(text)
@settings.send(:parse_config_files)
@settings.value(:one, "env1").should == "oneval"
@settings.value(:one, "env2").should == "twoval"
end
it "should have a run_mode that defaults to user" do
@settings.preferred_run_mode.should == :user
end
+ it "interpolates a boolean false without raising an error" do
+ @settings.define_settings(:section,
+ :trip_wire => { :type => :boolean, :default => false, :desc => "a trip wire" },
+ :tripping => { :default => '$trip_wire', :desc => "once tripped if interpolated was false" })
+ @settings[:tripping].should == "false"
+ end
+
describe "setbycli" do
it "should generate a deprecation warning" do
@settings.handlearg("--one", "blah")
Puppet.expects(:deprecation_warning)
@settings.setting(:one).setbycli
end
it "should be true" do
@settings.handlearg("--one", "blah")
@settings.setting(:one).setbycli.should be_true
end
end
end
describe "when choosing which value to return" do
before do
@settings = Puppet::Settings.new
@settings.define_settings :section,
:config => { :type => :file, :default => "/my/file", :desc => "a" },
:one => { :default => "ONE", :desc => "a" },
:two => { :default => "TWO", :desc => "b" }
- Puppet::FileSystem::File.stubs(:exist?).returns true
+ Puppet::FileSystem.stubs(:exist?).returns true
@settings.preferred_run_mode = :agent
end
it "should return default values if no values have been set" do
@settings[:one].should == "ONE"
end
it "should return values set on the cli before values set in the configuration file" do
text = "[main]\none = fileval\n"
@settings.stubs(:read_file).returns(text)
@settings.handlearg("--one", "clival")
@settings.send(:parse_config_files)
@settings[:one].should == "clival"
end
- it "should return values set on the cli before values set in Ruby" do
- @settings[:one] = "rubyval"
- @settings.handlearg("--one", "clival")
- @settings[:one].should == "clival"
- end
-
it "should return values set in the mode-specific section before values set in the main section" do
text = "[main]\none = mainval\n[agent]\none = modeval\n"
@settings.stubs(:read_file).returns(text)
@settings.send(:parse_config_files)
@settings[:one].should == "modeval"
end
it "should not return values outside of its search path" do
text = "[other]\none = oval\n"
file = "/some/file"
@settings.stubs(:read_file).returns(text)
@settings.send(:parse_config_files)
@settings[:one].should == "ONE"
end
it "should return values in a specified environment" do
text = "[env]\none = envval\n"
@settings.stubs(:read_file).returns(text)
@settings.send(:parse_config_files)
@settings.value(:one, "env").should == "envval"
end
it 'should use the current environment for $environment' do
@settings.define_settings :main, :myval => { :default => "$environment/foo", :desc => "mydocs" }
@settings.value(:myval, "myenv").should == "myenv/foo"
end
it "should interpolate found values using the current environment" do
text = "[main]\none = mainval\n[myname]\none = nameval\ntwo = $one/two\n"
@settings.stubs(:read_file).returns(text)
@settings.send(:parse_config_files)
@settings.value(:two, "myname").should == "nameval/two"
end
it "should return values in a specified environment before values in the main or name sections" do
text = "[env]\none = envval\n[main]\none = mainval\n[myname]\none = nameval\n"
@settings.stubs(:read_file).returns(text)
@settings.send(:parse_config_files)
@settings.value(:one, "env").should == "envval"
end
end
describe "when locating config files" do
before do
@settings = Puppet::Settings.new
end
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)
- 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
+ Puppet::FileSystem.expects(:exist?).with(main_config_file_default_location).returns(false)
+ Puppet::FileSystem.expects(:exist?).with(user_config_file_default_location).never
@settings.send(:parse_config_files)
end
end
describe "when not root" do
it "should look for user config file default location if config settings haven't been overridden'" do
Puppet.features.stubs(:root?).returns(false)
seq = sequence "load config files"
- Puppet::FileSystem::File.expects(:exist?).with(user_config_file_default_location).returns(false).in_sequence(seq)
+ Puppet::FileSystem.expects(:exist?).with(user_config_file_default_location).returns(false).in_sequence(seq)
@settings.send(:parse_config_files)
end
end
end
describe "when parsing its configuration" do
before do
@settings = Puppet::Settings.new
@settings.stubs(:service_user_available?).returns true
@settings.stubs(:service_group_available?).returns true
@file = make_absolute("/some/file")
@userconfig = make_absolute("/test/userconfigfile")
@settings.define_settings :section, :user => { :default => "suser", :desc => "doc" }, :group => { :default => "sgroup", :desc => "doc" }
@settings.define_settings :section,
:config => { :type => :file, :default => @file, :desc => "eh" },
:one => { :default => "ONE", :desc => "a" },
:two => { :default => "$one TWO", :desc => "b" },
:three => { :default => "$one $two THREE", :desc => "c" }
@settings.stubs(:user_config_file).returns(@userconfig)
- Puppet::FileSystem::File.stubs(:exist?).with(@file).returns true
- Puppet::FileSystem::File.stubs(:exist?).with(@userconfig).returns false
+ Puppet::FileSystem.stubs(:exist?).with(@file).returns true
+ Puppet::FileSystem.stubs(:exist?).with(@userconfig).returns false
end
it "should not ignore the report setting" do
@settings.define_settings :section, :report => { :default => "false", :desc => "a" }
# This is needed in order to make sure we pass on windows
myfile = File.expand_path(@file)
@settings[:config] = myfile
text = <<-CONF
[puppetd]
report=true
CONF
- Puppet::FileSystem::File.expects(:exist?).with(myfile).returns(true)
+ Puppet::FileSystem.expects(:exist?).with(myfile).returns(true)
@settings.expects(:read_file).returns(text)
@settings.send(:parse_config_files)
@settings[:report].should be_true
end
it "should use its current ':config' value for the file to parse" 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
- Puppet::FileSystem::File.expects(:exist?).with(myfile).returns(true)
+ Puppet::FileSystem.expects(:exist?).with(myfile).returns(true)
File.expects(:read).with(myfile).returns "[main]"
@settings.send(:parse_config_files)
end
it "should not try to parse non-existent files" do
- Puppet::FileSystem::File.expects(:exist?).with(@file).returns false
+ Puppet::FileSystem.expects(:exist?).with(@file).returns false
File.expects(:read).with(@file).never
@settings.send(:parse_config_files)
end
it "should return values set in the configuration file" do
text = "[main]
one = fileval
"
@settings.expects(:read_file).returns(text)
@settings.send(:parse_config_files)
@settings[:one].should == "fileval"
end
#484 - this should probably be in the regression area
it "should not throw an exception on unknown parameters" do
text = "[main]\nnosuchparam = mval\n"
@settings.expects(:read_file).returns(text)
lambda { @settings.send(:parse_config_files) }.should_not raise_error
end
it "should convert booleans in the configuration file into Ruby booleans" do
text = "[main]
one = true
two = false
"
@settings.expects(:read_file).returns(text)
@settings.send(:parse_config_files)
@settings[:one].should == true
@settings[:two].should == false
end
it "should convert integers in the configuration file into Ruby Integers" do
text = "[main]
one = 65
"
@settings.expects(:read_file).returns(text)
@settings.send(:parse_config_files)
@settings[:one].should == 65
end
it "should support specifying all metadata (owner, group, mode) in the configuration file" do
@settings.define_settings :section, :myfile => { :type => :file, :default => make_absolute("/myfile"), :desc => "a" }
otherfile = make_absolute("/other/file")
- text = "[main]
+ @settings.parse_config(<<-CONF)
+ [main]
myfile = #{otherfile} {owner = service, group = service, mode = 644}
- "
- @settings.expects(:read_file).returns(text)
- @settings.send(:parse_config_files)
+ CONF
+
@settings[:myfile].should == otherfile
@settings.metadata(:myfile).should == {:owner => "suser", :group => "sgroup", :mode => "644"}
end
it "should support specifying a single piece of metadata (owner, group, or mode) in the configuration file" do
@settings.define_settings :section, :myfile => { :type => :file, :default => make_absolute("/myfile"), :desc => "a" }
otherfile = make_absolute("/other/file")
- text = "[main]
+ @settings.parse_config(<<-CONF)
+ [main]
myfile = #{otherfile} {owner = service}
- "
- @settings.expects(:read_file).returns(text)
- @settings.send(:parse_config_files)
+ CONF
+
@settings[:myfile].should == otherfile
@settings.metadata(:myfile).should == {:owner => "suser"}
end
it "should support loading metadata (owner, group, or mode) from a run_mode section in the configuration file" do
default_values = {}
PuppetSpec::Settings::TEST_APP_DEFAULT_DEFINITIONS.keys.each do |key|
default_values[key] = 'default value'
end
@settings.define_settings :main, PuppetSpec::Settings::TEST_APP_DEFAULT_DEFINITIONS
@settings.define_settings :master, :myfile => { :type => :file, :default => make_absolute("/myfile"), :desc => "a" }
otherfile = make_absolute("/other/file")
text = "[master]
myfile = #{otherfile} {mode = 664}
"
@settings.expects(:read_file).returns(text)
# will start initialization as user
@settings.preferred_run_mode.should == :user
@settings.send(:parse_config_files)
# change app run_mode to master
@settings.initialize_app_defaults(default_values.merge(:run_mode => :master))
@settings.preferred_run_mode.should == :master
# initializing the app should have reloaded the metadata based on run_mode
@settings[:myfile].should == otherfile
@settings.metadata(:myfile).should == {:mode => "664"}
end
+ it "does not use the metadata from the same setting in a different section" do
+ default_values = {}
+ PuppetSpec::Settings::TEST_APP_DEFAULT_DEFINITIONS.keys.each do |key|
+ default_values[key] = 'default value'
+ end
+
+ file = make_absolute("/file")
+ default_mode = "0600"
+ @settings.define_settings :main, PuppetSpec::Settings::TEST_APP_DEFAULT_DEFINITIONS
+ @settings.define_settings :master, :myfile => { :type => :file, :default => file, :desc => "a", :mode => default_mode }
+
+ text = "[master]
+ myfile = #{file}/foo
+ [agent]
+ myfile = #{file} {mode = 664}
+ "
+ @settings.expects(:read_file).returns(text)
+
+ # will start initialization as user
+ @settings.preferred_run_mode.should == :user
+ @settings.send(:parse_config_files)
+
+ # change app run_mode to master
+ @settings.initialize_app_defaults(default_values.merge(:run_mode => :master))
+ @settings.preferred_run_mode.should == :master
+
+ # initializing the app should have reloaded the metadata based on run_mode
+ @settings[:myfile].should == "#{file}/foo"
+ @settings.metadata(:myfile).should == { :mode => default_mode }
+ end
+
it "should call hooks associated with values set in the configuration file" do
values = []
@settings.define_settings :section, :mysetting => {:default => "defval", :desc => "a", :hook => proc { |v| values << v }}
text = "[main]
mysetting = setval
"
@settings.expects(:read_file).returns(text)
@settings.send(:parse_config_files)
values.should == ["setval"]
end
it "should not call the same hook for values set multiple times in the configuration file" do
values = []
@settings.define_settings :section, :mysetting => {:default => "defval", :desc => "a", :hook => proc { |v| values << v }}
text = "[user]
mysetting = setval
[main]
mysetting = other
"
@settings.expects(:read_file).returns(text)
@settings.send(:parse_config_files)
values.should == ["setval"]
end
it "should pass the environment-specific value to the hook when one is available" do
values = []
@settings.define_settings :section, :mysetting => {:default => "defval", :desc => "a", :hook => proc { |v| values << v }}
@settings.define_settings :section, :environment => { :default => "yay", :desc => "a" }
@settings.define_settings :section, :environments => { :default => "yay,foo", :desc => "a" }
text = "[main]
mysetting = setval
[yay]
mysetting = other
"
@settings.expects(:read_file).returns(text)
@settings.send(:parse_config_files)
values.should == ["other"]
end
it "should pass the interpolated value to the hook when one is available" do
values = []
@settings.define_settings :section, :base => {:default => "yay", :desc => "a", :hook => proc { |v| values << v }}
@settings.define_settings :section, :mysetting => {:default => "defval", :desc => "a", :hook => proc { |v| values << v }}
text = "[main]
mysetting = $base/setval
"
@settings.expects(:read_file).returns(text)
@settings.send(:parse_config_files)
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')
+ @settings[:deferred].should eq(File.expand_path('/path/to/confdir/goose'))
+ end
+
+ it "does not require the value for a setting without a hook to resolve during global setup" do
+ hook_invoked = false
+ @settings.define_settings :section, :can_cause_problems => {:desc => '' }
+
+ @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]
+ can_cause_problems=$confdir/goose
+ EOD
+
+ @settings.stubs(:read_file).returns(text)
+ @settings.initialize_global_settings
+ @settings.initialize_app_defaults(:logdir => '/path/to/logdir', :confdir => '/path/to/confdir', :vardir => '/path/to/vardir')
+
+ @settings[:can_cause_problems].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" }
text = "[main]
myarg =
"
@settings.stubs(:read_file).returns(text)
@settings.send(:parse_config_files)
@settings[:myarg].should == ""
end
describe "and when reading a non-positive filetimeout value from the config file" do
before do
@settings.define_settings :foo, :filetimeout => { :default => 5, :desc => "eh" }
somefile = "/some/file"
text = "[main]
filetimeout = -1
"
File.expects(:read).with(somefile).returns(text)
File.expects(:expand_path).with(somefile).returns somefile
@settings[:config] = somefile
end
end
end
describe "when there are multiple config files" do
let(:main_config_text) { "[main]\none = main\ntwo = main2" }
let(:user_config_text) { "[main]\none = user\n" }
let(:seq) { sequence "config_file_sequence" }
before :each do
@settings = Puppet::Settings.new
@settings.define_settings(:section,
{ :confdir => { :default => nil, :desc => "Conf dir" },
:config => { :default => "$confdir/puppet.conf", :desc => "Config" },
:one => { :default => "ONE", :desc => "a" },
:two => { :default => "TWO", :desc => "b" }, })
end
context "running non-root without explicit config file" do
before :each do
Puppet.features.stubs(:root?).returns(false)
- Puppet::FileSystem::File.expects(:exist?).
+ Puppet::FileSystem.expects(:exist?).
with(user_config_file_default_location).
returns(true).in_sequence(seq)
@settings.expects(:read_file).
with(user_config_file_default_location).
returns(user_config_text).in_sequence(seq)
end
it "should return values from the user config file" do
@settings.send(:parse_config_files)
@settings[:one].should == "user"
end
it "should not return values from the main config file" do
@settings.send(:parse_config_files)
@settings[:two].should == "TWO"
end
end
context "running as root without explicit config file" do
before :each do
Puppet.features.stubs(:root?).returns(true)
- Puppet::FileSystem::File.expects(:exist?).
+ Puppet::FileSystem.expects(:exist?).
with(main_config_file_default_location).
returns(true).in_sequence(seq)
@settings.expects(:read_file).
with(main_config_file_default_location).
returns(main_config_text).in_sequence(seq)
end
it "should return values from the main config file" do
@settings.send(:parse_config_files)
@settings[:one].should == "main"
end
it "should not return values from the user config file" do
@settings.send(:parse_config_files)
@settings[:two].should == "main2"
end
end
context "running with an explicit config file as a user (e.g. Apache + Passenger)" do
before :each do
Puppet.features.stubs(:root?).returns(false)
@settings[:confdir] = File.dirname(main_config_file_default_location)
- Puppet::FileSystem::File.expects(:exist?).
+ Puppet::FileSystem.expects(:exist?).
with(main_config_file_default_location).
returns(true).in_sequence(seq)
@settings.expects(:read_file).
with(main_config_file_default_location).
returns(main_config_text).in_sequence(seq)
end
it "should return values from the main config file" do
@settings.send(:parse_config_files)
@settings[:one].should == "main"
end
it "should not return values from the user config file" do
@settings.send(:parse_config_files)
@settings[:two].should == "main2"
end
end
end
describe "when reparsing its configuration" do
before do
@file = make_absolute("/test/file")
@userconfig = make_absolute("/test/userconfigfile")
@settings = Puppet::Settings.new
@settings.define_settings :section,
:config => { :type => :file, :default => @file, :desc => "a" },
:one => { :default => "ONE", :desc => "a" },
:two => { :default => "$one TWO", :desc => "b" },
:three => { :default => "$one $two THREE", :desc => "c" }
- Puppet::FileSystem::File.stubs(:exist?).with(@file).returns true
- Puppet::FileSystem::File.stubs(:exist?).with(@userconfig).returns false
+ Puppet::FileSystem.stubs(:exist?).with(@file).returns true
+ Puppet::FileSystem.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
- Puppet::FileSystem::File.expects(:exist?).with(@file).returns false
+ Puppet::FileSystem.expects(:exist?).with(@file).returns false
Puppet::Util::WatchedFile.expects(:new).never
@settings.expects(:parse_config_files).never
@settings.reparse_config_files
end
context "and watched file exists" do
before do
@watched_file = Puppet::Util::WatchedFile.new(@file)
Puppet::Util::WatchedFile.expects(:new).with(@file).returns @watched_file
end
it "uses a WatchedFile instance to determine if the file has changed" do
@watched_file.expects(:changed?)
@settings.reparse_config_files
end
it "does not reparse if the file has not changed" do
@watched_file.expects(:changed?).returns false
@settings.expects(:parse_config_files).never
@settings.reparse_config_files
end
it "reparses if the file has changed" do
@watched_file.expects(:changed?).returns true
- @settings.expects(:unsafe_parse).with(@file)
+ @settings.expects(:parse_config_files)
@settings.reparse_config_files
end
it "replaces in-memory values with on-file values" do
@watched_file.stubs(:changed?).returns(true)
@settings[:one] = "init"
# Now replace the value
text = "[main]\none = disk-replace\n"
@settings.stubs(:read_file).returns(text)
@settings.reparse_config_files
@settings[:one].should == "disk-replace"
end
end
it "should retain parameters set by cli when configuration files are reparsed" do
@settings.handlearg("--one", "clival")
text = "[main]\none = on-disk\n"
@settings.stubs(:read_file).returns(text)
@settings.send(:parse_config_files)
@settings[:one].should == "clival"
end
it "should remove in-memory values that are no longer set in the file" do
# Init the value
text = "[main]\none = disk-init\n"
@settings.expects(:read_file).returns(text)
@settings.send(:parse_config_files)
@settings[:one].should == "disk-init"
# Now replace the value
text = "[main]\ntwo = disk-replace\n"
@settings.expects(:read_file).returns(text)
@settings.send(:parse_config_files)
# The originally-overridden value should be replaced with the default
@settings[:one].should == "ONE"
# and we should now have the new value in memory
@settings[:two].should == "disk-replace"
end
it "should retain in-memory values if the file has a syntax error" do
# Init the value
text = "[main]\none = initial-value\n"
@settings.expects(:read_file).with(@file).returns(text)
@settings.send(:parse_config_files)
@settings[:one].should == "initial-value"
# Now replace the value with something bogus
text = "[main]\nkenny = killed-by-what-follows\n1 is 2, blah blah florp\n"
@settings.expects(:read_file).with(@file).returns(text)
@settings.send(:parse_config_files)
# The originally-overridden value should not be replaced with the default
@settings[:one].should == "initial-value"
# and we should not have the new value in memory
@settings[:kenny].should be_nil
end
end
it "should provide a method for creating a catalog of resources from its configuration" do
Puppet::Settings.new.should respond_to(:to_catalog)
end
describe "when creating a catalog" do
before do
@settings = Puppet::Settings.new
@settings.stubs(:service_user_available?).returns true
@prefix = Puppet.features.posix? ? "" : "C:"
end
it "should add all file resources to the catalog if no sections have been specified" do
@settings.define_settings :main,
:maindir => { :type => :directory, :default => @prefix+"/maindir", :desc => "a"},
:seconddir => { :type => :directory, :default => @prefix+"/seconddir", :desc => "a"}
@settings.define_settings :other,
:otherdir => { :type => :directory, :default => @prefix+"/otherdir", :desc => "a" }
catalog = @settings.to_catalog
[@prefix+"/maindir", @prefix+"/seconddir", @prefix+"/otherdir"].each do |path|
catalog.resource(:file, path).should be_instance_of(Puppet::Resource)
end
end
it "should add only files in the specified sections if section names are provided" do
@settings.define_settings :main, :maindir => { :type => :directory, :default => @prefix+"/maindir", :desc => "a" }
@settings.define_settings :other, :otherdir => { :type => :directory, :default => @prefix+"/otherdir", :desc => "a" }
catalog = @settings.to_catalog(:main)
catalog.resource(:file, @prefix+"/otherdir").should be_nil
catalog.resource(:file, @prefix+"/maindir").should be_instance_of(Puppet::Resource)
end
it "should not try to add the same file twice" do
@settings.define_settings :main, :maindir => { :type => :directory, :default => @prefix+"/maindir", :desc => "a" }
@settings.define_settings :other, :otherdir => { :type => :directory, :default => @prefix+"/maindir", :desc => "a" }
lambda { @settings.to_catalog }.should_not raise_error
end
it "should ignore files whose :to_resource method returns nil" do
@settings.define_settings :main, :maindir => { :type => :directory, :default => @prefix+"/maindir", :desc => "a" }
@settings.setting(:maindir).expects(:to_resource).returns nil
Puppet::Resource::Catalog.any_instance.expects(:add_resource).never
@settings.to_catalog
end
describe "on Microsoft Windows" do
before :each do
Puppet.features.stubs(:root?).returns true
Puppet.features.stubs(:microsoft_windows?).returns true
@settings.define_settings :foo,
:mkusers => { :type => :boolean, :default => true, :desc => "e" },
:user => { :default => "suser", :desc => "doc" },
:group => { :default => "sgroup", :desc => "doc" }
@settings.define_settings :other,
:otherdir => { :type => :directory, :default => "/otherdir", :desc => "a", :owner => "service", :group => "service"}
@catalog = @settings.to_catalog
end
it "it should not add users and groups to the catalog" do
@catalog.resource(:user, "suser").should be_nil
@catalog.resource(:group, "sgroup").should be_nil
end
end
describe "when adding users and groups to the catalog" do
before do
Puppet.features.stubs(:root?).returns true
Puppet.features.stubs(:microsoft_windows?).returns false
@settings.define_settings :foo,
:mkusers => { :type => :boolean, :default => true, :desc => "e" },
:user => { :default => "suser", :desc => "doc" },
:group => { :default => "sgroup", :desc => "doc" }
@settings.define_settings :other, :otherdir => {:type => :directory, :default => "/otherdir", :desc => "a", :owner => "service", :group => "service"}
@catalog = @settings.to_catalog
end
it "should add each specified user and group to the catalog if :mkusers is a valid setting, is enabled, and we're running as root" do
@catalog.resource(:user, "suser").should be_instance_of(Puppet::Resource)
@catalog.resource(:group, "sgroup").should be_instance_of(Puppet::Resource)
end
it "should only add users and groups to the catalog from specified sections" do
@settings.define_settings :yay, :yaydir => { :type => :directory, :default => "/yaydir", :desc => "a", :owner => "service", :group => "service"}
catalog = @settings.to_catalog(:other)
catalog.resource(:user, "jane").should be_nil
catalog.resource(:group, "billy").should be_nil
end
it "should not add users or groups to the catalog if :mkusers not running as root" do
Puppet.features.stubs(:root?).returns false
catalog = @settings.to_catalog
catalog.resource(:user, "suser").should be_nil
catalog.resource(:group, "sgroup").should be_nil
end
it "should not add users or groups to the catalog if :mkusers is not a valid setting" do
Puppet.features.stubs(:root?).returns true
settings = Puppet::Settings.new
settings.define_settings :other, :otherdir => {:type => :directory, :default => "/otherdir", :desc => "a", :owner => "service", :group => "service"}
catalog = settings.to_catalog
catalog.resource(:user, "suser").should be_nil
catalog.resource(:group, "sgroup").should be_nil
end
it "should not add users or groups to the catalog if :mkusers is a valid setting but is disabled" do
@settings[:mkusers] = false
catalog = @settings.to_catalog
catalog.resource(:user, "suser").should be_nil
catalog.resource(:group, "sgroup").should be_nil
end
it "should not try to add users or groups to the catalog twice" do
@settings.define_settings :yay, :yaydir => {:type => :directory, :default => "/yaydir", :desc => "a", :owner => "service", :group => "service"}
# This would fail if users/groups were added twice
lambda { @settings.to_catalog }.should_not raise_error
end
it "should set :ensure to :present on each created user and group" do
@catalog.resource(:user, "suser")[:ensure].should == :present
@catalog.resource(:group, "sgroup")[:ensure].should == :present
end
it "should set each created user's :gid to the service group" do
@settings.to_catalog.resource(:user, "suser")[:gid].should == "sgroup"
end
it "should not attempt to manage the root user" do
Puppet.features.stubs(:root?).returns true
@settings.define_settings :foo, :foodir => {:type => :directory, :default => "/foodir", :desc => "a", :owner => "root", :group => "service"}
@settings.to_catalog.resource(:user, "root").should be_nil
end
end
end
it "should be able to be converted to a manifest" do
Puppet::Settings.new.should respond_to(:to_manifest)
end
describe "when being converted to a manifest" do
it "should produce a string with the code for each resource joined by two carriage returns" do
@settings = Puppet::Settings.new
@settings.define_settings :main,
:maindir => { :type => :directory, :default => "/maindir", :desc => "a"},
:seconddir => { :type => :directory, :default => "/seconddir", :desc => "a"}
main = stub 'main_resource', :ref => "File[/maindir]"
main.expects(:to_manifest).returns "maindir"
second = stub 'second_resource', :ref => "File[/seconddir]"
second.expects(:to_manifest).returns "seconddir"
@settings.setting(:maindir).expects(:to_resource).returns main
@settings.setting(:seconddir).expects(:to_resource).returns second
@settings.to_manifest.split("\n\n").sort.should == %w{maindir seconddir}
end
end
describe "when using sections of the configuration to manage the local host" do
before do
@settings = Puppet::Settings.new
@settings.stubs(:service_user_available?).returns true
@settings.stubs(:service_group_available?).returns true
@settings.define_settings :main, :noop => { :default => false, :desc => "", :type => :boolean }
@settings.define_settings :main,
:maindir => { :type => :directory, :default => make_absolute("/maindir"), :desc => "a" },
:seconddir => { :type => :directory, :default => make_absolute("/seconddir"), :desc => "a"}
@settings.define_settings :main, :user => { :default => "suser", :desc => "doc" }, :group => { :default => "sgroup", :desc => "doc" }
@settings.define_settings :other, :otherdir => {:type => :directory, :default => make_absolute("/otherdir"), :desc => "a", :owner => "service", :group => "service", :mode => 0755}
@settings.define_settings :third, :thirddir => { :type => :directory, :default => make_absolute("/thirddir"), :desc => "b"}
@settings.define_settings :files, :myfile => {:type => :file, :default => make_absolute("/myfile"), :desc => "a", :mode => 0755}
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)
@settings.mkdir(:otherdir)
end
it "should create a catalog with the specified sections" do
@settings.expects(:to_catalog).with(:main, :other).returns Puppet::Resource::Catalog.new("foo")
@settings.use(:main, :other)
end
it "should canonicalize the sections" do
@settings.expects(:to_catalog).with(:main, :other).returns Puppet::Resource::Catalog.new("foo")
@settings.use("main", "other")
end
it "should ignore sections that have already been used" do
@settings.expects(:to_catalog).with(:main).returns Puppet::Resource::Catalog.new("foo")
@settings.use(:main)
@settings.expects(:to_catalog).with(:other).returns Puppet::Resource::Catalog.new("foo")
@settings.use(:main, :other)
end
it "should convert the created catalog to a RAL catalog" do
@catalog = Puppet::Resource::Catalog.new("foo")
@settings.expects(:to_catalog).with(:main).returns @catalog
@catalog.expects(:to_ral).returns @catalog
@settings.use(:main)
end
it "should specify that it is not managing a host catalog" do
catalog = Puppet::Resource::Catalog.new("foo")
catalog.expects(:apply)
@settings.expects(:to_catalog).returns catalog
catalog.stubs(:to_ral).returns catalog
catalog.expects(:host_config=).with false
@settings.use(:main)
end
it "should support a method for re-using all currently used sections" do
@settings.expects(:to_catalog).with(:main, :third).times(2).returns Puppet::Resource::Catalog.new("foo")
@settings.use(:main, :third)
@settings.reuse
end
it "should fail with an appropriate message if any resources fail" do
@catalog = Puppet::Resource::Catalog.new("foo")
@catalog.stubs(:to_ral).returns @catalog
@settings.expects(:to_catalog).returns @catalog
@trans = mock("transaction")
@catalog.expects(:apply).yields(@trans)
@trans.expects(:any_failed?).returns(true)
- report = mock 'report'
- @trans.expects(:report).returns report
+ resource = Puppet::Type.type(:notify).new(:title => 'failed')
+ status = Puppet::Resource::Status.new(resource)
+ event = Puppet::Transaction::Event.new(
+ :name => 'failure',
+ :status => 'failure',
+ :message => 'My failure')
+ status.add_event(event)
- log = mock 'log', :to_s => "My failure", :level => :err
- report.expects(:logs).returns [log]
+ report = Puppet::Transaction::Report.new('apply')
+ report.add_resource_status(status)
+
+ @trans.expects(:report).returns report
- @settings.expects(:raise).with { |msg| msg.include?("My failure") }
+ @settings.expects(:raise).with(includes("My failure"))
@settings.use(:whatever)
end
end
describe "when dealing with printing configs" do
before do
@settings = Puppet::Settings.new
#these are the magic default values
@settings.stubs(:value).with(:configprint).returns("")
@settings.stubs(:value).with(:genconfig).returns(false)
@settings.stubs(:value).with(:genmanifest).returns(false)
@settings.stubs(:value).with(:environment).returns(nil)
end
describe "when checking print_config?" do
it "should return false when the :configprint, :genconfig and :genmanifest are not set" do
@settings.print_configs?.should be_false
end
it "should return true when :configprint has a value" do
@settings.stubs(:value).with(:configprint).returns("something")
@settings.print_configs?.should be_true
end
it "should return true when :genconfig has a value" do
@settings.stubs(:value).with(:genconfig).returns(true)
@settings.print_configs?.should be_true
end
it "should return true when :genmanifest has a value" do
@settings.stubs(:value).with(:genmanifest).returns(true)
@settings.print_configs?.should be_true
end
end
describe "when printing configs" do
describe "when :configprint has a value" do
it "should call print_config_options" do
@settings.stubs(:value).with(:configprint).returns("something")
@settings.expects(:print_config_options)
@settings.print_configs
end
it "should get the value of the option using the environment" do
@settings.stubs(:value).with(:configprint).returns("something")
@settings.stubs(:include?).with("something").returns(true)
@settings.expects(:value).with(:environment).returns("env")
@settings.expects(:value).with("something", "env").returns("foo")
@settings.stubs(:puts).with("foo")
@settings.print_configs
end
it "should print the value of the option" do
@settings.stubs(:value).with(:configprint).returns("something")
@settings.stubs(:include?).with("something").returns(true)
@settings.stubs(:value).with("something", nil).returns("foo")
@settings.expects(:puts).with("foo")
@settings.print_configs
end
it "should print the value pairs if there are multiple options" do
@settings.stubs(:value).with(:configprint).returns("bar,baz")
@settings.stubs(:include?).with("bar").returns(true)
@settings.stubs(:include?).with("baz").returns(true)
@settings.stubs(:value).with("bar", nil).returns("foo")
@settings.stubs(:value).with("baz", nil).returns("fud")
@settings.expects(:puts).with("bar = foo")
@settings.expects(:puts).with("baz = fud")
@settings.print_configs
end
it "should return true after printing" do
@settings.stubs(:value).with(:configprint).returns("something")
@settings.stubs(:include?).with("something").returns(true)
@settings.stubs(:value).with("something", nil).returns("foo")
@settings.stubs(:puts).with("foo")
@settings.print_configs.should be_true
end
it "should return false if a config param is not found" do
@settings.stubs :puts
@settings.stubs(:value).with(:configprint).returns("something")
@settings.stubs(:include?).with("something").returns(false)
@settings.print_configs.should be_false
end
end
describe "when genconfig is true" do
before do
@settings.stubs :puts
end
it "should call to_config" do
@settings.stubs(:value).with(:genconfig).returns(true)
@settings.expects(:to_config)
@settings.print_configs
end
it "should return true from print_configs" do
@settings.stubs(:value).with(:genconfig).returns(true)
@settings.stubs(:to_config)
@settings.print_configs.should be_true
end
end
describe "when genmanifest is true" do
before do
@settings.stubs :puts
end
it "should call to_config" do
@settings.stubs(:value).with(:genmanifest).returns(true)
@settings.expects(:to_manifest)
@settings.print_configs
end
it "should return true from print_configs" do
@settings.stubs(:value).with(:genmanifest).returns(true)
@settings.stubs(:to_manifest)
@settings.print_configs.should be_true
end
end
end
end
describe "when determining if the service user is available" do
let(:settings) do
settings = Puppet::Settings.new
settings.define_settings :main, :user => { :default => nil, :desc => "doc" }
settings
end
def a_user_type_for(username)
user = mock 'user'
Puppet::Type.type(:user).expects(:new).with { |args| args[:name] == username }.returns user
user
end
it "should return false if there is no user setting" do
settings.should_not be_service_user_available
end
it "should return false if the user provider says the user is missing" do
settings[:user] = "foo"
a_user_type_for("foo").expects(:exists?).returns false
settings.should_not be_service_user_available
end
it "should return true if the user provider says the user is present" do
settings[:user] = "foo"
a_user_type_for("foo").expects(:exists?).returns true
settings.should be_service_user_available
end
it "caches the result of determining if the user is present" do
settings[:user] = "foo"
a_user_type_for("foo").expects(:exists?).returns true
settings.should be_service_user_available
settings.should be_service_user_available
end
end
describe "when determining if the service group is available" do
let(:settings) do
settings = Puppet::Settings.new
settings.define_settings :main, :group => { :default => nil, :desc => "doc" }
settings
end
def a_group_type_for(groupname)
group = mock 'group'
Puppet::Type.type(:group).expects(:new).with { |args| args[:name] == groupname }.returns group
group
end
it "should return false if there is no group setting" do
settings.should_not be_service_group_available
end
it "should return false if the group provider says the group is missing" do
settings[:group] = "foo"
a_group_type_for("foo").expects(:exists?).returns false
settings.should_not be_service_group_available
end
it "should return true if the group provider says the group is present" do
settings[:group] = "foo"
a_group_type_for("foo").expects(:exists?).returns true
settings.should be_service_group_available
end
it "caches the result of determining if the group is present" do
settings[:group] = "foo"
a_group_type_for("foo").expects(:exists?).returns true
settings.should be_service_group_available
settings.should be_service_group_available
end
end
describe "when dealing with command-line options" do
let(:settings) { Puppet::Settings.new }
it "should get options from Puppet.settings.optparse_addargs" do
settings.expects(:optparse_addargs).returns([])
settings.send(:parse_global_options, [])
end
it "should add options to OptionParser" do
settings.stubs(:optparse_addargs).returns( [["--option","-o", "Funny Option", :NONE]])
settings.expects(:handlearg).with("--option", true)
settings.send(:parse_global_options, ["--option"])
end
it "should not die if it sees an unrecognized option, because the app/face may handle it later" do
expect { settings.send(:parse_global_options, ["--topuppet", "value"]) } .to_not raise_error
end
it "should not pass an unrecognized option to handleargs" do
settings.expects(:handlearg).with("--topuppet", "value").never
expect { settings.send(:parse_global_options, ["--topuppet", "value"]) } .to_not raise_error
end
it "should pass valid puppet settings options to handlearg even if they appear after an unrecognized option" do
settings.stubs(:optparse_addargs).returns( [["--option","-o", "Funny Option", :NONE]])
settings.expects(:handlearg).with("--option", true)
settings.send(:parse_global_options, ["--invalidoption", "--option"])
end
it "should transform boolean option to normal form" do
Puppet::Settings.clean_opt("--[no-]option", true).should == ["--option", true]
end
it "should transform boolean option to no- form" do
Puppet::Settings.clean_opt("--[no-]option", false).should == ["--no-option", false]
end
it "should set preferred run mode from --run_mode <foo> string without error" do
args = ["--run_mode", "master"]
settings.expects(:handlearg).with("--run_mode", "master").never
expect { settings.send(:parse_global_options, args) } .to_not raise_error
Puppet.settings.preferred_run_mode.should == :master
args.empty?.should == true
end
it "should set preferred run mode from --run_mode=<foo> string without error" do
args = ["--run_mode=master"]
settings.expects(:handlearg).with("--run_mode", "master").never
expect { settings.send(:parse_global_options, args) } .to_not raise_error
Puppet.settings.preferred_run_mode.should == :master
args.empty?.should == true
end
end
describe "default_certname" do
describe "using hostname and domainname" do
before :each do
Puppet::Settings.stubs(:hostname_fact).returns("testhostname")
Puppet::Settings.stubs(:domain_fact).returns("domain.test.")
end
it "should use both to generate fqdn" do
Puppet::Settings.default_certname.should =~ /testhostname\.domain\.test/
end
it "should remove trailing dots from fqdn" do
Puppet::Settings.default_certname.should == 'testhostname.domain.test'
end
end
describe "using just hostname" do
before :each do
Puppet::Settings.stubs(:hostname_fact).returns("testhostname")
Puppet::Settings.stubs(:domain_fact).returns("")
end
it "should use only hostname to generate fqdn" do
Puppet::Settings.default_certname.should == "testhostname"
end
it "should removing trailing dots from fqdn" do
Puppet::Settings.default_certname.should == "testhostname"
end
end
end
end
diff --git a/spec/unit/ssl/certificate_authority/interface_spec.rb b/spec/unit/ssl/certificate_authority/interface_spec.rb
index 9e55dd30d..61110b2a9 100755
--- a/spec/unit/ssl/certificate_authority/interface_spec.rb
+++ b/spec/unit/ssl/certificate_authority/interface_spec.rb
@@ -1,364 +1,366 @@
#! /usr/bin/env ruby
require 'spec_helper'
require 'puppet/ssl/certificate_authority'
shared_examples_for "a normal interface method" do
it "should call the method on the CA for each host specified if an array was provided" do
@ca.expects(@method).with("host1")
@ca.expects(@method).with("host2")
@applier = Puppet::SSL::CertificateAuthority::Interface.new(@method, :to => %w{host1 host2})
@applier.apply(@ca)
end
it "should call the method on the CA for all existing certificates if :all was provided" do
@ca.expects(:list).returns %w{host1 host2}
@ca.expects(@method).with("host1")
@ca.expects(@method).with("host2")
@applier = Puppet::SSL::CertificateAuthority::Interface.new(@method, :to => :all)
@applier.apply(@ca)
end
end
describe Puppet::SSL::CertificateAuthority::Interface do
before do
@class = Puppet::SSL::CertificateAuthority::Interface
end
describe "when initializing" do
it "should set its method using its settor" do
instance = @class.new(:generate, :to => :all)
instance.method.should == :generate
end
it "should set its subjects using the settor" do
instance = @class.new(:generate, :to => :all)
instance.subjects.should == :all
end
it "should set the digest if given" do
interface = @class.new(:generate, :to => :all, :digest => :digest)
interface.digest.should == :digest
end
end
describe "when setting the method" do
it "should set the method" do
instance = @class.new(:generate, :to => :all)
instance.method = :list
instance.method.should == :list
end
it "should fail if the method isn't a member of the INTERFACE_METHODS array" do
lambda { @class.new(:thing, :to => :all) }.should raise_error(ArgumentError, /Invalid method thing to apply/)
end
end
describe "when setting the subjects" do
it "should set the subjects" do
instance = @class.new(:generate, :to => :all)
instance.subjects = :signed
instance.subjects.should == :signed
end
it "should fail if the subjects setting isn't :all or an array" do
lambda { @class.new(:generate, :to => "other") }.should raise_error(ArgumentError, /Subjects must be an array or :all; not other/)
end
end
it "should have a method for triggering the application" do
@class.new(:generate, :to => :all).should respond_to(:apply)
end
describe "when applying" do
before do
# We use a real object here, because :verify can't be stubbed, apparently.
@ca = Object.new
end
describe "with an empty array specified and the method is not list" do
it "should fail" do
@applier = @class.new(:sign, :to => [])
lambda { @applier.apply(@ca) }.should raise_error(ArgumentError)
end
end
describe ":generate" do
it "should fail if :all was specified" do
@applier = @class.new(:generate, :to => :all)
lambda { @applier.apply(@ca) }.should raise_error(ArgumentError)
end
it "should call :generate on the CA for each host specified" do
@applier = @class.new(:generate, :to => %w{host1 host2})
@ca.expects(:generate).with("host1", {})
@ca.expects(:generate).with("host2", {})
@applier.apply(@ca)
end
end
describe ":verify" do
before { @method = :verify }
#it_should_behave_like "a normal interface method"
it "should call the method on the CA for each host specified if an array was provided" do
# LAK:NOTE Mocha apparently doesn't allow you to mock :verify, but I'm confident this works in real life.
end
it "should call the method on the CA for all existing certificates if :all was provided" do
# LAK:NOTE Mocha apparently doesn't allow you to mock :verify, but I'm confident this works in real life.
end
end
describe ":destroy" do
before { @method = :destroy }
it_should_behave_like "a normal interface method"
end
describe ":revoke" do
before { @method = :revoke }
it_should_behave_like "a normal interface method"
end
describe ":sign" do
describe "and an array of names was provided" do
let(:applier) { @class.new(:sign, @options.merge(:to => %w{host1 host2})) }
it "should sign the specified waiting certificate requests" do
@options = {:allow_dns_alt_names => false}
@ca.expects(:sign).with("host1", false)
@ca.expects(:sign).with("host2", false)
applier.apply(@ca)
end
it "should sign the certificate requests with alt names if specified" do
@options = {:allow_dns_alt_names => true}
@ca.expects(:sign).with("host1", true)
@ca.expects(:sign).with("host2", true)
applier.apply(@ca)
end
end
describe "and :all was provided" do
it "should sign all waiting certificate requests" do
@ca.stubs(:waiting?).returns(%w{cert1 cert2})
@ca.expects(:sign).with("cert1", nil)
@ca.expects(:sign).with("cert2", nil)
@applier = @class.new(:sign, :to => :all)
@applier.apply(@ca)
end
it "should fail if there are no waiting certificate requests" do
@ca.stubs(:waiting?).returns([])
@applier = @class.new(:sign, :to => :all)
lambda { @applier.apply(@ca) }.should raise_error(Puppet::SSL::CertificateAuthority::Interface::InterfaceError)
end
end
end
describe ":list" do
before :each do
@cert = Puppet::SSL::Certificate.new 'foo'
@csr = Puppet::SSL::CertificateRequest.new 'bar'
@cert.stubs(:subject_alt_names).returns []
@csr.stubs(:subject_alt_names).returns []
Puppet::SSL::Certificate.indirection.stubs(:find).returns @cert
Puppet::SSL::CertificateRequest.indirection.stubs(:find).returns @csr
@digest = mock("digest")
@digest.stubs(:to_s).returns("(fingerprint)")
@ca.expects(:waiting?).returns %w{host1 host2 host3}
- @ca.expects(:list).returns %w{host4 host5 host6}
+ @ca.expects(:list).returns(%w{host4 host5 host6}).at_most(1)
@csr.stubs(:digest).returns @digest
@cert.stubs(:digest).returns @digest
@ca.stubs(:verify)
end
describe "and an empty array was provided" do
it "should print all certificate requests" do
applier = @class.new(:list, :to => [])
applier.expects(:puts).with(<<-OUTPUT.chomp)
"host1" (fingerprint)
"host2" (fingerprint)
"host3" (fingerprint)
OUTPUT
applier.apply(@ca)
end
end
describe "and :all was provided" do
it "should print a string containing all certificate requests and certificates" do
+ @ca.expects(:list).returns %w{host4 host5 host6}
@ca.stubs(:verify).with("host4").raises(Puppet::SSL::CertificateAuthority::CertificateVerificationError.new(23), "certificate revoked")
applier = @class.new(:list, :to => :all)
applier.expects(:puts).with(<<-OUTPUT.chomp)
"host1" (fingerprint)
"host2" (fingerprint)
"host3" (fingerprint)
+ "host5" (fingerprint)
+ "host6" (fingerprint)
- "host4" (fingerprint) (certificate revoked)
OUTPUT
applier.apply(@ca)
end
end
describe "and :signed was provided" do
it "should print a string containing all signed certificate requests and certificates" do
+ @ca.expects(:list).returns %w{host4 host5 host6}
applier = @class.new(:list, :to => :signed)
applier.expects(:puts).with(<<-OUTPUT.chomp)
+ "host4" (fingerprint)
+ "host5" (fingerprint)
+ "host6" (fingerprint)
OUTPUT
applier.apply(@ca)
end
it "should include subject alt names if they are on the certificate request" do
@csr.stubs(:subject_alt_names).returns ["DNS:foo", "DNS:bar"]
applier = @class.new(:list, :to => ['host1'])
applier.expects(:puts).with(<<-OUTPUT.chomp)
"host1" (fingerprint) (alt names: "DNS:foo", "DNS:bar")
OUTPUT
applier.apply(@ca)
end
end
describe "and an array of names was provided" do
it "should print all named hosts" do
applier = @class.new(:list, :to => %w{host1 host2 host4 host5})
applier.expects(:puts).with(<<-OUTPUT.chomp)
"host1" (fingerprint)
"host2" (fingerprint)
+ "host4" (fingerprint)
+ "host5" (fingerprint)
OUTPUT
applier.apply(@ca)
end
end
end
describe ":print" do
describe "and :all was provided" do
it "should print all certificates" do
@ca.expects(:list).returns %w{host1 host2}
@applier = @class.new(:print, :to => :all)
@ca.expects(:print).with("host1").returns "h1"
@applier.expects(:puts).with "h1"
@ca.expects(:print).with("host2").returns "h2"
@applier.expects(:puts).with "h2"
@applier.apply(@ca)
end
end
describe "and an array of names was provided" do
it "should print each named certificate if found" do
@applier = @class.new(:print, :to => %w{host1 host2})
@ca.expects(:print).with("host1").returns "h1"
@applier.expects(:puts).with "h1"
@ca.expects(:print).with("host2").returns "h2"
@applier.expects(:puts).with "h2"
@applier.apply(@ca)
end
it "should log any named but not found certificates" do
@applier = @class.new(:print, :to => %w{host1 host2})
@ca.expects(:print).with("host1").returns "h1"
@applier.expects(:puts).with "h1"
@ca.expects(:print).with("host2").returns nil
Puppet.expects(:err).with { |msg| msg.include?("host2") }
@applier.apply(@ca)
end
end
end
describe ":fingerprint" do
before(:each) do
@cert = Puppet::SSL::Certificate.new 'foo'
@csr = Puppet::SSL::CertificateRequest.new 'bar'
Puppet::SSL::Certificate.indirection.stubs(:find)
Puppet::SSL::CertificateRequest.indirection.stubs(:find)
Puppet::SSL::Certificate.indirection.stubs(:find).with('host1').returns(@cert)
Puppet::SSL::CertificateRequest.indirection.stubs(:find).with('host2').returns(@csr)
end
it "should fingerprint with the set digest algorithm" do
@applier = @class.new(:fingerprint, :to => %w{host1}, :digest => :shaonemillion)
@cert.expects(:digest).with(:shaonemillion).returns("fingerprint1")
@applier.expects(:puts).with "host1 fingerprint1"
@applier.apply(@ca)
end
describe "and :all was provided" do
it "should fingerprint all certificates (including waiting ones)" do
@ca.expects(:list).returns %w{host1}
@ca.expects(:waiting?).returns %w{host2}
@applier = @class.new(:fingerprint, :to => :all)
@cert.expects(:digest).returns("fingerprint1")
@applier.expects(:puts).with "host1 fingerprint1"
@csr.expects(:digest).returns("fingerprint2")
@applier.expects(:puts).with "host2 fingerprint2"
@applier.apply(@ca)
end
end
describe "and an array of names was provided" do
it "should print each named certificate if found" do
@applier = @class.new(:fingerprint, :to => %w{host1 host2})
@cert.expects(:digest).returns("fingerprint1")
@applier.expects(:puts).with "host1 fingerprint1"
@csr.expects(:digest).returns("fingerprint2")
@applier.expects(:puts).with "host2 fingerprint2"
@applier.apply(@ca)
end
end
end
end
end
diff --git a/spec/unit/ssl/certificate_authority_spec.rb b/spec/unit/ssl/certificate_authority_spec.rb
index 13c169a0a..69114a403 100755
--- a/spec/unit/ssl/certificate_authority_spec.rb
+++ b/spec/unit/ssl/certificate_authority_spec.rb
@@ -1,1103 +1,1105 @@
#! /usr/bin/env ruby
# encoding: ASCII-8BIT
require 'spec_helper'
require 'puppet/ssl/certificate_authority'
describe Puppet::SSL::CertificateAuthority do
after do
Puppet::SSL::CertificateAuthority.instance_variable_set(:@singleton_instance, nil)
Puppet.settings.clearused
end
def stub_ca_host
@key = mock 'key'
@key.stubs(:content).returns "cakey"
@cacert = mock 'certificate'
@cacert.stubs(:content).returns "cacertificate"
@host = stub 'ssl_host', :key => @key, :certificate => @cacert, :name => Puppet::SSL::Host.ca_name
end
it "should have a class method for returning a singleton instance" do
Puppet::SSL::CertificateAuthority.should respond_to(:instance)
end
describe "when finding an existing instance" do
describe "and the host is a CA host and the run_mode is master" do
before do
Puppet[:ca] = true
Puppet.run_mode.stubs(:master?).returns true
@ca = mock('ca')
Puppet::SSL::CertificateAuthority.stubs(:new).returns @ca
end
it "should return an instance" do
Puppet::SSL::CertificateAuthority.instance.should equal(@ca)
end
it "should always return the same instance" do
Puppet::SSL::CertificateAuthority.instance.should equal(Puppet::SSL::CertificateAuthority.instance)
end
end
describe "and the host is not a CA host" do
it "should return nil" do
Puppet[:ca] = false
Puppet.run_mode.stubs(:master?).returns true
ca = mock('ca')
Puppet::SSL::CertificateAuthority.expects(:new).never
Puppet::SSL::CertificateAuthority.instance.should be_nil
end
end
describe "and the run_mode is not master" do
it "should return nil" do
Puppet[:ca] = true
Puppet.run_mode.stubs(:master?).returns false
ca = mock('ca')
Puppet::SSL::CertificateAuthority.expects(:new).never
Puppet::SSL::CertificateAuthority.instance.should be_nil
end
end
end
describe "when initializing" do
before do
Puppet.settings.stubs(:use)
Puppet::SSL::CertificateAuthority.any_instance.stubs(:setup)
end
it "should always set its name to the value of :certname" do
Puppet[:certname] = "ca_testing"
Puppet::SSL::CertificateAuthority.new.name.should == "ca_testing"
end
it "should create an SSL::Host instance whose name is the 'ca_name'" do
Puppet::SSL::Host.expects(:ca_name).returns "caname"
host = stub 'host'
Puppet::SSL::Host.expects(:new).with("caname").returns host
Puppet::SSL::CertificateAuthority.new
end
it "should use the :main, :ca, and :ssl settings sections" do
Puppet.settings.expects(:use).with(:main, :ssl, :ca)
Puppet::SSL::CertificateAuthority.new
end
it "should make sure the CA is set up" do
Puppet::SSL::CertificateAuthority.any_instance.expects(:setup)
Puppet::SSL::CertificateAuthority.new
end
end
describe "when setting itself up" do
it "should generate the CA certificate if it does not have one" do
Puppet.settings.stubs :use
host = stub 'host'
Puppet::SSL::Host.stubs(:new).returns host
host.expects(:certificate).returns nil
Puppet::SSL::CertificateAuthority.any_instance.expects(:generate_ca_certificate)
Puppet::SSL::CertificateAuthority.new
end
end
describe "when retrieving the certificate revocation list" do
before do
Puppet.settings.stubs(:use)
Puppet[:cacrl] = "/my/crl"
cert = stub("certificate", :content => "real_cert")
key = stub("key", :content => "real_key")
@host = stub 'host', :certificate => cert, :name => "hostname", :key => key
Puppet::SSL::CertificateAuthority.any_instance.stubs(:setup)
@ca = Puppet::SSL::CertificateAuthority.new
@ca.stubs(:host).returns @host
end
it "should return any found CRL instance" do
crl = mock 'crl'
Puppet::SSL::CertificateRevocationList.indirection.expects(:find).returns crl
@ca.crl.should equal(crl)
end
it "should create, generate, and save a new CRL instance of no CRL can be found" do
crl = Puppet::SSL::CertificateRevocationList.new("fakename")
Puppet::SSL::CertificateRevocationList.indirection.expects(:find).returns nil
Puppet::SSL::CertificateRevocationList.expects(:new).returns crl
crl.expects(:generate).with(@ca.host.certificate.content, @ca.host.key.content)
Puppet::SSL::CertificateRevocationList.indirection.expects(:save).with(crl)
@ca.crl.should equal(crl)
end
end
describe "when generating a self-signed CA certificate" do
before do
Puppet.settings.stubs(:use)
Puppet::SSL::CertificateAuthority.any_instance.stubs(:setup)
Puppet::SSL::CertificateAuthority.any_instance.stubs(:crl)
@ca = Puppet::SSL::CertificateAuthority.new
@host = stub 'host', :key => mock("key"), :name => "hostname", :certificate => mock('certificate')
Puppet::SSL::CertificateRequest.any_instance.stubs(:generate)
@ca.stubs(:host).returns @host
end
it "should create and store a password at :capass" do
Puppet[:capass] = File.expand_path("/path/to/pass")
- Puppet::FileSystem::File.expects(:exist?).with(Puppet[:capass]).returns false
+ Puppet::FileSystem.expects(:exist?).with(Puppet[:capass]).returns false
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
@ca.stubs :generate_password
@ca.stubs :sign
@ca.host.expects(:key).returns nil
@ca.host.expects(:generate_key)
@ca.generate_ca_certificate
end
it "should create and sign a self-signed cert using the CA name" do
request = mock 'request'
Puppet::SSL::CertificateRequest.expects(:new).with(@ca.host.name).returns request
request.expects(:generate).with(@ca.host.key)
request.stubs(:request_extensions => [])
@ca.expects(:sign).with(@host.name, false, request)
@ca.stubs :generate_password
@ca.generate_ca_certificate
end
it "should generate its CRL" do
@ca.stubs :generate_password
@ca.stubs :sign
@ca.host.expects(:key).returns nil
@ca.host.expects(:generate_key)
@ca.expects(:crl)
@ca.generate_ca_certificate
end
end
describe "when signing" do
before do
Puppet.settings.stubs(:use)
Puppet::SSL::CertificateAuthority.any_instance.stubs(:password?).returns true
stub_ca_host
Puppet::SSL::Host.expects(:new).with(Puppet::SSL::Host.ca_name).returns @host
@ca = Puppet::SSL::CertificateAuthority.new
@name = "myhost"
@real_cert = stub 'realcert', :sign => nil
@cert = Puppet::SSL::Certificate.new(@name)
@cert.content = @real_cert
Puppet::SSL::Certificate.stubs(:new).returns @cert
Puppet::SSL::Certificate.indirection.stubs(:save)
# Stub out the factory
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
@request_content.stubs(:verify).returns(true)
# And the inventory
@inventory = stub 'inventory', :add => nil
@ca.stubs(:inventory).returns @inventory
Puppet::SSL::CertificateRequest.indirection.stubs(:destroy)
end
describe "its own certificate" do
before do
@serial = 10
@ca.stubs(:next_serial).returns @serial
end
it "should not look up a certificate request for the host" do
Puppet::SSL::CertificateRequest.indirection.expects(:find).never
@ca.sign(@name, true, @request)
end
it "should use a certificate type of :ca" do
Puppet::SSL::CertificateFactory.expects(:build).with do |*args|
args[0].should == :ca
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 @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 @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 @cert.content
@ca.sign(@name, :ca, @request)
end
it "should sign the certificate request even if it contains alt names" do
@request.stubs(:subject_alt_names).returns %w[DNS:foo DNS:bar DNS:baz]
expect do
@ca.sign(@name, false, @request)
end.not_to raise_error
end
it "should save the resulting certificate" do
Puppet::SSL::Certificate.indirection.expects(:save).with(@cert)
@ca.sign(@name, :ca, @request)
end
end
describe "another host's certificate" do
before do
@serial = 10
@ca.stubs(:next_serial).returns @serial
Puppet::SSL::CertificateRequest.indirection.stubs(:find).with(@name).returns @request
Puppet::SSL::CertificateRequest.indirection.stubs :save
end
it "should use a certificate type of :server" do
Puppet::SSL::CertificateFactory.expects(:build).with do |*args|
args[0] == :server
end.returns @cert.content
@ca.sign(@name)
end
it "should use look up a CSR for the host in the :ca_file terminus" do
Puppet::SSL::CertificateRequest.indirection.expects(:find).with(@name).returns @request
@ca.sign(@name)
end
it "should fail if no CSR can be found for the host" do
Puppet::SSL::CertificateRequest.indirection.expects(:find).with(@name).returns nil
expect { @ca.sign(@name) }.to raise_error(ArgumentError)
end
it "should fail if an unknown request extension is present" do
@request.stubs :request_extensions => [{ "oid" => "bananas",
"value" => "delicious" }]
expect {
@ca.sign(@name)
}.to raise_error(/CSR has request extensions that are not permitted/)
end
it "should fail if the CSR contains alt names and they are not expected" do
@request.stubs(:subject_alt_names).returns %w[DNS:foo DNS:bar DNS:baz]
expect do
@ca.sign(@name, false)
end.to raise_error(Puppet::SSL::CertificateAuthority::CertificateSigningError, /CSR '#{@name}' contains subject alternative names \(.*?\), which are disallowed. Use `puppet cert --allow-dns-alt-names sign #{@name}` to sign this request./)
end
it "should not fail if the CSR does not contain alt names and they are expected" do
@request.stubs(:subject_alt_names).returns []
expect { @ca.sign(@name, true) }.to_not raise_error
end
it "should reject alt names by default" do
@request.stubs(:subject_alt_names).returns %w[DNS:foo DNS:bar DNS:baz]
expect do
@ca.sign(@name)
end.to raise_error(Puppet::SSL::CertificateAuthority::CertificateSigningError, /CSR '#{@name}' contains subject alternative names \(.*?\), which are disallowed. Use `puppet cert --allow-dns-alt-names sign #{@name}` to sign this request./)
end
it "should use the CA certificate as the issuer" do
Puppet::SSL::CertificateFactory.expects(:build).with do |*args|
args[2] == @cacert.content
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 @cert.content
@ca.sign(@name)
end
it "should sign the resulting certificate using its real key and a digest" do
digest = mock 'digest'
OpenSSL::Digest::SHA256.expects(:new).returns digest
key = stub 'key', :content => "real_key"
@ca.host.stubs(:key).returns key
@cert.content.expects(:sign).with("real_key", digest)
@ca.sign(@name)
end
it "should save the resulting certificate" do
Puppet::SSL::Certificate.indirection.stubs(:save).with(@cert)
@ca.sign(@name)
end
it "should remove the host's certificate request" do
Puppet::SSL::CertificateRequest.indirection.expects(:destroy).with(@name)
@ca.sign(@name)
end
it "should check the internal signing policies" do
@ca.expects(:check_internal_signing_policies).returns true
@ca.sign(@name)
end
end
context "#check_internal_signing_policies" do
before do
@serial = 10
@ca.stubs(:next_serial).returns @serial
Puppet::SSL::CertificateRequest.indirection.stubs(:find).with(@name).returns @request
@cert.stubs :save
end
it "should reject CSRs whose CN doesn't match the name for which we're signing them" do
# Shorten this so the test doesn't take too long
Puppet[:keylength] = 1024
key = Puppet::SSL::Key.new('the_certname')
key.generate
csr = Puppet::SSL::CertificateRequest.new('the_certname')
csr.generate(key)
expect do
@ca.check_internal_signing_policies('not_the_certname', csr, false)
end.to raise_error(
Puppet::SSL::CertificateAuthority::CertificateSigningError,
/common name "the_certname" does not match expected certname "not_the_certname"/
)
end
describe "when validating the CN" do
before :all do
Puppet[:keylength] = 1024
Puppet[:passfile] = '/f00'
@signing_key = Puppet::SSL::Key.new('my_signing_key')
@signing_key.generate
end
[
'completely_okay',
'sure, why not? :)',
'so+many(things)-are=allowed.',
'this"is#just&madness%you[see]',
'and even a (an?) \\!',
'waltz, nymph, for quick jigs vex bud.',
'{552c04ca-bb1b-11e1-874b-60334b04494e}'
].each do |name|
it "should accept #{name.inspect}" do
csr = Puppet::SSL::CertificateRequest.new(name)
csr.generate(@signing_key)
@ca.check_internal_signing_policies(name, csr, false)
end
end
[
'super/bad',
"not\neven\tkind\rof",
"ding\adong\a",
"hidden\b\b\b\b\b\bmessage",
"\xE2\x98\x83 :("
].each do |name|
it "should reject #{name.inspect}" do
# We aren't even allowed to make objects with these names, so let's
# stub that to simulate an invalid one coming from outside Puppet
Puppet::SSL::CertificateRequest.stubs(:validate_certname)
csr = Puppet::SSL::CertificateRequest.new(name)
csr.generate(@signing_key)
expect do
@ca.check_internal_signing_policies(name, csr, false)
end.to raise_error(
Puppet::SSL::CertificateAuthority::CertificateSigningError,
/subject contains unprintable or non-ASCII characters/
)
end
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",
"critical" => true }]
expect { @ca.check_internal_signing_policies(@name, @request, false) }.to raise_error(
Puppet::SSL::CertificateAuthority::CertificateSigningError,
/request extensions that are not permitted/
)
end
it "should reject a non-critical extension that isn't on the whitelist" do
@request.stubs(:request_extensions).returns [{ "oid" => "peach",
"value" => "meh",
"critical" => false }]
expect { @ca.check_internal_signing_policies(@name, @request, false) }.to raise_error(
Puppet::SSL::CertificateAuthority::CertificateSigningError,
/request extensions that are not permitted/
)
end
it "should reject non-whitelist extensions even if a valid extension is present" do
@request.stubs(:request_extensions).returns [{ "oid" => "peach",
"value" => "meh",
"critical" => false },
{ "oid" => "subjectAltName",
"value" => "DNS:foo",
"critical" => true }]
expect { @ca.check_internal_signing_policies(@name, @request, false) }.to raise_error(
Puppet::SSL::CertificateAuthority::CertificateSigningError,
/request extensions that are not permitted/
)
end
it "should reject a subjectAltName for a non-DNS value" do
@request.stubs(:subject_alt_names).returns ['DNS:foo', 'email:bar@example.com']
expect { @ca.check_internal_signing_policies(@name, @request, true) }.to raise_error(
Puppet::SSL::CertificateAuthority::CertificateSigningError,
/subjectAltName outside the DNS label space/
)
end
it "should reject a wildcard subject" do
@request.content.stubs(:subject).
returns(OpenSSL::X509::Name.new([["CN", "*.local"]]))
expect { @ca.check_internal_signing_policies('*.local', @request, false) }.to raise_error(
Puppet::SSL::CertificateAuthority::CertificateSigningError,
/subject contains a wildcard/
)
end
it "should reject a wildcard subjectAltName" do
@request.stubs(:subject_alt_names).returns ['DNS:foo', 'DNS:*.bar']
expect { @ca.check_internal_signing_policies(@name, @request, true) }.to raise_error(
Puppet::SSL::CertificateAuthority::CertificateSigningError,
/subjectAltName contains a wildcard/
)
end
end
it "should create a certificate instance with the content set to the newly signed x509 certificate" do
@serial = 10
@ca.stubs(:next_serial).returns @serial
Puppet::SSL::CertificateRequest.indirection.stubs(:find).with(@name).returns @request
Puppet::SSL::Certificate.indirection.stubs :save
Puppet::SSL::Certificate.expects(:new).with(@name).returns @cert
@ca.sign(@name)
end
it "should return the certificate instance" do
@ca.stubs(:next_serial).returns @serial
Puppet::SSL::CertificateRequest.indirection.stubs(:find).with(@name).returns @request
Puppet::SSL::Certificate.indirection.stubs :save
@ca.sign(@name).should equal(@cert)
end
it "should add the certificate to its inventory" do
@ca.stubs(:next_serial).returns @serial
@inventory.expects(:add).with(@cert)
Puppet::SSL::CertificateRequest.indirection.stubs(:find).with(@name).returns @request
Puppet::SSL::Certificate.indirection.stubs :save
@ca.sign(@name)
end
it "should have a method for triggering autosigning of available CSRs" do
@ca.should respond_to(:autosign)
end
describe "when autosigning certificates" do
let(:csr) { Puppet::SSL::CertificateRequest.new("host") }
describe "using the autosign setting" do
let(:autosign) { File.expand_path("/auto/sign") }
it "should do nothing if autosign is disabled" do
Puppet[:autosign] = false
@ca.expects(:sign).never
@ca.autosign(csr)
end
it "should do nothing if no autosign.conf exists" do
Puppet[:autosign] = autosign
non_existent_file = Puppet::FileSystem::MemoryFile.a_missing_file(autosign)
- Puppet::FileSystem::File.overlay(non_existent_file) do
+ Puppet::FileSystem.overlay(non_existent_file) do
@ca.expects(:sign).never
@ca.autosign(csr)
end
end
describe "and autosign is enabled and the autosign.conf file exists" do
let(:store) { stub 'store', :allow => nil, :allowed? => false }
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::FileSystem::File.overlay(Puppet::FileSystem::MemoryFile.a_regular_file_containing(autosign, "one\ntwo\n")) do
+ Puppet::FileSystem.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")
@ca.autosign(csr)
end
end
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::FileSystem.overlay(Puppet::FileSystem::MemoryFile.a_regular_file_containing(autosign, "one")) do
Puppet::Network::AuthStore.stubs(:new).times(2).returns store
@ca.autosign(csr)
@ca.autosign(csr)
end
end
it "should ignore comments" do
- Puppet::FileSystem::File.overlay(Puppet::FileSystem::MemoryFile.a_regular_file_containing(autosign, "one\n#two\n")) do
+ Puppet::FileSystem.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(csr)
end
end
it "should ignore blank lines" do
- Puppet::FileSystem::File.overlay(Puppet::FileSystem::MemoryFile.a_regular_file_containing(autosign, "one\n\n")) do
+ Puppet::FileSystem.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 "autosigns the CSR if the autosign command returned true" do
- Puppet::FileSystem::File.overlay(autosign_executable) do
+ Puppet::FileSystem.overlay(autosign_executable) do
autosign_cmd.expects(:allowed?).with(csr).returns true
@ca.expects(:sign).with('host')
@ca.autosign(csr)
end
end
it "doesn't autosign the CSR if the autosign_command returned false" do
- Puppet::FileSystem::File.overlay(autosign_executable) do
+ Puppet::FileSystem.overlay(autosign_executable) do
autosign_cmd.expects(:allowed?).with(csr).returns false
@ca.expects(:sign).never
@ca.autosign(csr)
end
end
end
end
end
describe "when managing certificate clients" do
before do
Puppet.settings.stubs(:use)
Puppet::SSL::CertificateAuthority.any_instance.stubs(:password?).returns true
stub_ca_host
Puppet::SSL::Host.expects(:new).returns @host
Puppet::SSL::CertificateAuthority.any_instance.stubs(:host).returns @host
@cacert = mock 'certificate'
@cacert.stubs(:content).returns "cacertificate"
@ca = Puppet::SSL::CertificateAuthority.new
end
it "should be able to list waiting certificate requests" do
req1 = stub 'req1', :name => "one"
req2 = stub 'req2', :name => "two"
Puppet::SSL::CertificateRequest.indirection.expects(:search).with("*").returns [req1, req2]
@ca.waiting?.should == %w{one two}
end
it "should delegate removing hosts to the Host class" do
Puppet::SSL::Host.expects(:destroy).with("myhost")
@ca.destroy("myhost")
end
it "should be able to verify certificates" do
@ca.should respond_to(:verify)
end
it "should list certificates as the sorted list of all existing signed certificates" do
cert1 = stub 'cert1', :name => "cert1"
cert2 = stub 'cert2', :name => "cert2"
Puppet::SSL::Certificate.indirection.expects(:search).with("*").returns [cert1, cert2]
@ca.list.should == %w{cert1 cert2}
end
it "should list the full certificates" do
cert1 = stub 'cert1', :name => "cert1"
cert2 = stub 'cert2', :name => "cert2"
Puppet::SSL::Certificate.indirection.expects(:search).with("*").returns [cert1, cert2]
@ca.list_certificates.should == [cert1, cert2]
end
describe "and printing certificates" do
it "should return nil if the certificate cannot be found" do
Puppet::SSL::Certificate.indirection.expects(:find).with("myhost").returns nil
@ca.print("myhost").should be_nil
end
it "should print certificates by calling :to_text on the host's certificate" do
cert1 = stub 'cert1', :name => "cert1", :to_text => "mytext"
Puppet::SSL::Certificate.indirection.expects(:find).with("myhost").returns cert1
@ca.print("myhost").should == "mytext"
end
end
describe "and fingerprinting certificates" do
before :each do
@cert = stub 'cert', :name => "cert", :fingerprint => "DIGEST"
Puppet::SSL::Certificate.indirection.stubs(:find).with("myhost").returns @cert
Puppet::SSL::CertificateRequest.indirection.stubs(:find).with("myhost")
end
it "should raise an error if the certificate or CSR cannot be found" do
Puppet::SSL::Certificate.indirection.expects(:find).with("myhost").returns nil
Puppet::SSL::CertificateRequest.indirection.expects(:find).with("myhost").returns nil
expect { @ca.fingerprint("myhost") }.to raise_error
end
it "should try to find a CSR if no certificate can be found" do
Puppet::SSL::Certificate.indirection.expects(:find).with("myhost").returns nil
Puppet::SSL::CertificateRequest.indirection.expects(:find).with("myhost").returns @cert
@cert.expects(:fingerprint)
@ca.fingerprint("myhost")
end
it "should delegate to the certificate fingerprinting" do
@cert.expects(:fingerprint)
@ca.fingerprint("myhost")
end
it "should propagate the digest algorithm to the certificate fingerprinting system" do
@cert.expects(:fingerprint).with(:digest)
@ca.fingerprint("myhost", :digest)
end
end
describe "and verifying certificates" do
let(:cacert) { File.expand_path("/ca/cert") }
before do
@store = stub 'store', :verify => true, :add_file => nil, :purpose= => nil, :add_crl => true, :flags= => nil
OpenSSL::X509::Store.stubs(:new).returns @store
@cert = stub 'cert', :content => "mycert"
Puppet::SSL::Certificate.indirection.stubs(:find).returns @cert
@crl = stub('crl', :content => "mycrl")
@ca.stubs(:crl).returns @crl
end
it "should fail if the host's certificate cannot be found" do
Puppet::SSL::Certificate.indirection.expects(:find).with("me").returns(nil)
expect { @ca.verify("me") }.to raise_error(ArgumentError)
end
it "should create an SSL Store to verify" do
OpenSSL::X509::Store.expects(:new).returns @store
@ca.verify("me")
end
it "should add the CA Certificate to the store" do
Puppet[:cacert] = cacert
@store.expects(:add_file).with cacert
@ca.verify("me")
end
it "should add the CRL to the store if the crl is enabled" do
@store.expects(:add_crl).with "mycrl"
@ca.verify("me")
end
it "should set the store purpose to OpenSSL::X509::PURPOSE_SSL_CLIENT" do
Puppet[:cacert] = cacert
@store.expects(:add_file).with cacert
@ca.verify("me")
end
it "should set the store flags to check the crl" do
@store.expects(:flags=).with OpenSSL::X509::V_FLAG_CRL_CHECK_ALL|OpenSSL::X509::V_FLAG_CRL_CHECK
@ca.verify("me")
end
it "should use the store to verify the certificate" do
@cert.expects(:content).returns "mycert"
@store.expects(:verify).with("mycert").returns true
@ca.verify("me")
end
it "should fail if the verification returns false" do
@cert.expects(:content).returns "mycert"
@store.expects(:verify).with("mycert").returns false
expect { @ca.verify("me") }.to raise_error
end
describe "certificate_is_alive?" do
it "should return false if verification fails" do
@cert.expects(:content).returns "mycert"
@store.expects(:verify).with("mycert").returns false
@ca.certificate_is_alive?(@cert).should be_false
end
it "should return true if verification passes" do
@cert.expects(:content).returns "mycert"
@store.expects(:verify).with("mycert").returns true
@ca.certificate_is_alive?(@cert).should be_true
end
it "should used a cached instance of the x509 store" do
OpenSSL::X509::Store.stubs(:new).returns(@store).once
@cert.expects(:content).returns "mycert"
@store.expects(:verify).with("mycert").returns true
@ca.certificate_is_alive?(@cert)
@ca.certificate_is_alive?(@cert)
end
end
end
describe "and revoking certificates" do
before do
@crl = mock 'crl'
@ca.stubs(:crl).returns @crl
@ca.stubs(:next_serial).returns 10
@real_cert = stub 'real_cert', :serial => 15
@cert = stub 'cert', :content => @real_cert
Puppet::SSL::Certificate.indirection.stubs(:find).returns @cert
end
it "should fail if the certificate revocation list is disabled" do
@ca.stubs(:crl).returns false
expect { @ca.revoke('ca_testing') }.to raise_error(ArgumentError)
end
it "should delegate the revocation to its CRL" do
@ca.crl.expects(:revoke)
@ca.revoke('host')
end
it "should get the serial number from the local certificate if it exists" do
@ca.crl.expects(:revoke).with { |serial, key| serial == 15 }
Puppet::SSL::Certificate.indirection.expects(:find).with("host").returns @cert
@ca.revoke('host')
end
it "should get the serial number from inventory if no local certificate exists" do
real_cert = stub 'real_cert', :serial => 15
cert = stub 'cert', :content => real_cert
Puppet::SSL::Certificate.indirection.expects(:find).with("host").returns nil
@ca.inventory.expects(:serial).with("host").returns 16
@ca.crl.expects(:revoke).with { |serial, key| serial == 16 }
@ca.revoke('host')
end
context "revocation by serial number (#16798)" do
it "revokes when given a lower case hexadecimal formatted string" do
@ca.crl.expects(:revoke).with { |serial, key| serial == 15 }
Puppet::SSL::Certificate.indirection.expects(:find).with("0xf").returns nil
@ca.revoke('0xf')
end
it "revokes when given an upper case hexadecimal formatted string" do
@ca.crl.expects(:revoke).with { |serial, key| serial == 15 }
Puppet::SSL::Certificate.indirection.expects(:find).with("0xF").returns nil
@ca.revoke('0xF')
end
it "handles very large serial numbers" do
bighex = '0x4000000000000000000000000000000000000000'
- @ca.crl.expects(:revoke).with { |serial, key| serial == 2**(159-1) }
+ bighex_int = 365375409332725729550921208179070754913983135744
+
+ @ca.crl.expects(:revoke).with(bighex_int, anything)
Puppet::SSL::Certificate.indirection.expects(:find).with(bighex).returns nil
@ca.revoke(bighex)
end
end
end
it "should be able to generate a complete new SSL host" do
@ca.should respond_to(:generate)
end
end
end
require 'puppet/indirector/memory'
describe "CertificateAuthority.generate" do
def expect_to_increment_serial_file
Puppet.settings.setting(:serial).expects(:exclusive_open)
end
def expect_to_sign_a_cert
expect_to_increment_serial_file
end
def expect_to_write_the_ca_password
Puppet.settings.setting(:capass).expects(:open).with('w')
end
def expect_ca_initialization
expect_to_write_the_ca_password
expect_to_sign_a_cert
end
INDIRECTED_CLASSES = [
Puppet::SSL::Certificate,
Puppet::SSL::CertificateRequest,
Puppet::SSL::CertificateRevocationList,
Puppet::SSL::Key,
]
INDIRECTED_CLASSES.each do |const|
class const::Memory < Puppet::Indirector::Memory
# @return Array of all the indirector's values
#
# This mirrors Puppet::Indirector::SslFile#search which returns all files
# in the directory.
def search(request)
return @instances.values
end
end
end
before do
Puppet::SSL::Inventory.stubs(:new).returns(stub("Inventory", :add => nil))
INDIRECTED_CLASSES.each { |const| const.indirection.terminus_class = :memory }
end
after do
INDIRECTED_CLASSES.each do |const|
const.indirection.terminus_class = :file
const.indirection.termini.clear
end
end
describe "when generating certificates" do
let(:ca) { Puppet::SSL::CertificateAuthority.new }
before do
expect_ca_initialization
end
it "should fail if a certificate already exists for the host" do
cert = Puppet::SSL::Certificate.new('pre.existing')
Puppet::SSL::Certificate.indirection.save(cert)
expect { ca.generate(cert.name) }.to raise_error(ArgumentError, /a certificate already exists/i)
end
describe "that do not yet exist" do
let(:cn) { "new.host" }
def expect_cert_does_not_exist(cn)
expect( Puppet::SSL::Certificate.indirection.find(cn) ).to be_nil
end
before do
expect_to_sign_a_cert
expect_cert_does_not_exist(cn)
end
it "should return the created certificate" do
cert = ca.generate(cn)
expect( cert ).to be_kind_of(Puppet::SSL::Certificate)
expect( cert.name ).to eq(cn)
end
it "should not have any subject_alt_names by default" do
cert = ca.generate(cn)
expect( cert.subject_alt_names ).to be_empty
end
it "should have subject_alt_names if passed dns_alt_names" do
cert = ca.generate(cn, :dns_alt_names => 'foo,bar')
expect( cert.subject_alt_names ).to match_array(["DNS:#{cn}",'DNS:foo','DNS:bar'])
end
context "if autosign is false" do
before do
Puppet[:autosign] = false
end
it "should still generate and explicitly sign the request" do
cert = nil
cert = ca.generate(cn)
expect(cert.name).to eq(cn)
end
end
context "if autosign is true (Redmine #6112)" do
def run_mode_must_be_master_for_autosign_to_be_attempted
Puppet.stubs(:run_mode).returns(Puppet::Util::RunMode[:master])
end
before do
Puppet[:autosign] = true
run_mode_must_be_master_for_autosign_to_be_attempted
Puppet::Util::Log.level = :info
end
it "should generate a cert without attempting to sign again" do
cert = ca.generate(cn)
expect(cert.name).to eq(cn)
expect(@logs.map(&:message)).to include("Autosigning #{cn}")
end
end
end
end
end
diff --git a/spec/unit/ssl/certificate_factory_spec.rb b/spec/unit/ssl/certificate_factory_spec.rb
index fa436edcf..59d0e0170 100755
--- a/spec/unit/ssl/certificate_factory_spec.rb
+++ b/spec/unit/ssl/certificate_factory_spec.rb
@@ -1,153 +1,168 @@
#! /usr/bin/env ruby
require 'spec_helper'
require 'puppet/ssl/certificate_factory'
describe Puppet::SSL::CertificateFactory do
let :serial do OpenSSL::BN.new('12') end
let :name do "example.local" end
let :x509_name do OpenSSL::X509::Name.new([['CN', name]]) end
let :key do Puppet::SSL::Key.new(name).generate end
let :csr do
csr = Puppet::SSL::CertificateRequest.new(name)
csr.generate(key)
csr
end
let :issuer do
- cert = OpenSSL::X509::Certificate.new
- cert.subject = OpenSSL::X509::Name.new([["CN", 'issuer.local']])
- cert
+ cert = Puppet::SSL::CertificateAuthority.new
+ cert.generate_ca_certificate
+ cert.host.certificate.content
end
describe "when generating the certificate" do
it "should return a new X509 certificate" do
subject.build(:server, csr, issuer, serial).should_not ==
subject.build(:server, csr, issuer, serial)
end
it "should set the certificate's version to 2" do
subject.build(:server, csr, issuer, serial).version.should == 2
end
it "should set the certificate's subject to the CSR's subject" do
cert = subject.build(:server, csr, issuer, serial)
cert.subject.should eql x509_name
end
it "should set the certificate's issuer to the Issuer's subject" do
cert = subject.build(:server, csr, issuer, serial)
cert.issuer.should eql issuer.subject
end
it "should set the certificate's public key to the CSR's public key" do
cert = subject.build(:server, csr, issuer, serial)
cert.public_key.should be_public
cert.public_key.to_s.should == csr.content.public_key.to_s
end
it "should set the certificate's serial number to the provided serial number" do
cert = subject.build(:server, csr, issuer, serial)
cert.serial.should == serial
end
it "should have 24 hours grace on the start of the cert" do
cert = subject.build(:server, csr, issuer, serial)
cert.not_before.should be_within(30).of(Time.now - 24*60*60)
end
it "should set the default TTL of the certificate to the `ca_ttl` setting" do
Puppet[:ca_ttl] = 12
now = Time.now.utc
Time.expects(:now).at_least_once.returns(now)
cert = subject.build(:server, csr, issuer, serial)
cert.not_after.to_i.should == now.to_i + 12
end
it "should not allow a non-integer TTL" do
[ 'foo', 1.2, Time.now, true ].each do |ttl|
expect { subject.build(:server, csr, issuer, serial, ttl) }.to raise_error(ArgumentError)
end
end
it "should respect a custom TTL for the CA" do
now = Time.now.utc
Time.expects(:now).at_least_once.returns(now)
cert = subject.build(:server, csr, issuer, serial, 12)
cert.not_after.to_i.should == now.to_i + 12
end
- it "should build extensions for the certificate" do
+ it "should adds an extension for the nsComment" do
cert = subject.build(:server, csr, issuer, serial)
cert.extensions.map {|x| x.to_h }.find {|x| x["oid"] == "nsComment" }.should ==
{ "oid" => "nsComment",
"value" => "Puppet Ruby/OpenSSL Internal Certificate",
"critical" => false }
end
+ it "should add an extension for the subjectKeyIdentifer" do
+ cert = subject.build(:server, csr, issuer, serial)
+ ef = OpenSSL::X509::ExtensionFactory.new(issuer, cert)
+ cert.extensions.map { |x| x.to_h }.find {|x| x["oid"] == "subjectKeyIdentifier" }.should ==
+ ef.create_extension("subjectKeyIdentifier", "hash", false).to_h
+ end
+
+
+ it "should add an extension for the authorityKeyIdentifer" do
+ cert = subject.build(:server, csr, issuer, serial)
+ ef = OpenSSL::X509::ExtensionFactory.new(issuer, cert)
+ cert.extensions.map { |x| x.to_h }.find {|x| x["oid"] == "authorityKeyIdentifier" }.should ==
+ ef.create_extension("authorityKeyIdentifier", "keyid:always", false).to_h
+ end
+
# See #2848 for why we are doing this: we need to make sure that
# subjectAltName is set if the CSR has it, but *not* if it is set when the
# certificate is built!
it "should not add subjectAltNames from dns_alt_names" do
Puppet[:dns_alt_names] = 'one, two'
# Verify the CSR still has no extReq, just in case...
csr.request_extensions.should == []
cert = subject.build(:server, csr, issuer, serial)
cert.extensions.find {|x| x.oid == 'subjectAltName' }.should be_nil
end
it "should add subjectAltName when the CSR requests them" do
Puppet[:dns_alt_names] = ''
expect = %w{one two} + [name]
csr = Puppet::SSL::CertificateRequest.new(name)
csr.generate(key, :dns_alt_names => expect.join(', '))
csr.request_extensions.should_not be_nil
csr.subject_alt_names.should =~ expect.map{|x| "DNS:#{x}"}
cert = subject.build(:server, csr, issuer, serial)
san = cert.extensions.find {|x| x.oid == 'subjectAltName' }
san.should_not be_nil
expect.each do |name|
san.value.should =~ /DNS:#{name}\b/i
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
{ :ca => 'CA:TRUE',
:terminalsubca => ['CA:TRUE', 'pathlen:0'],
:server => 'CA:FALSE',
:ocsp => 'CA:FALSE',
:client => 'CA:FALSE',
}.each do |name, value|
it "should set basicConstraints for #{name} #{value.inspect}" do
cert = subject.build(name, csr, issuer, serial)
bc = cert.extensions.find {|x| x.oid == 'basicConstraints' }
bc.should be
bc.value.split(/\s*,\s*/).should =~ Array(value)
end
end
end
end
diff --git a/spec/unit/ssl/certificate_request_attributes_spec.rb b/spec/unit/ssl/certificate_request_attributes_spec.rb
index 6165330aa..5c8f93b1b 100644
--- a/spec/unit/ssl/certificate_request_attributes_spec.rb
+++ b/spec/unit/ssl/certificate_request_attributes_spec.rb
@@ -1,61 +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::FileSystem.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_revocation_list_spec.rb b/spec/unit/ssl/certificate_revocation_list_spec.rb
index 21b7e6d99..cf25022b0 100755
--- a/spec/unit/ssl/certificate_revocation_list_spec.rb
+++ b/spec/unit/ssl/certificate_revocation_list_spec.rb
@@ -1,167 +1,196 @@
#! /usr/bin/env ruby
require 'spec_helper'
require 'puppet/ssl/certificate_revocation_list'
describe Puppet::SSL::CertificateRevocationList do
before do
- @cert = stub 'cert', :subject => "mysubject"
- @key = stub 'key', :private? => true
-
+ ca = Puppet::SSL::CertificateAuthority.new
+ ca.generate_ca_certificate
+ @cert = ca.host.certificate.content
+ @key = ca.host.key.content
@class = Puppet::SSL::CertificateRevocationList
end
+ def expects_time_close_to_now(time)
+ expect(time.to_i).to be_within(5*60).of(Time.now.to_i)
+ end
+
+ def expects_time_close_to_five_years(time)
+ future = Time.now + Puppet::SSL::CertificateRevocationList::FIVE_YEARS
+ expect(time.to_i).to be_within(5*60).of(future.to_i)
+ end
+
+ def expects_crlnumber_extension(crl, value)
+ crlNumber = crl.content.extensions.find { |ext| ext.oid == "crlNumber" }
+
+ expect(crlNumber.value).to eq(value.to_s)
+ expect(crlNumber).to_not be_critical
+ end
+
+ def expects_authkeyid_extension(crl, cert)
+ subjectKeyId = cert.extensions.find { |ext| ext.oid == 'subjectKeyIdentifier' }.value
+
+ authKeyId = crl.content.extensions.find { |ext| ext.oid == "authorityKeyIdentifier" }
+ expect(authKeyId.value.chomp).to eq("keyid:#{subjectKeyId}")
+ expect(authKeyId).to_not be_critical
+ end
+
+ def expects_crlreason_extension(crl, reason)
+ revoke = crl.content.revoked.first
+
+ crlNumber = crl.content.extensions.find { |ext| ext.oid == "crlNumber" }
+ expect(revoke.serial.to_s).to eq(crlNumber.value)
+
+ crlReason = revoke.extensions.find { |ext| ext.oid = 'CRLReason' }
+ expect(crlReason.value).to eq(reason)
+ expect(crlReason).to_not be_critical
+ end
+
it "should only support the text format" do
@class.supported_formats.should == [:s]
end
describe "when converting from a string" do
- it "should create a CRL instance with its name set to 'foo' and its content set to the extracted CRL" do
- crl = stub 'crl', :is_a? => true
- OpenSSL::X509::CRL.expects(:new).returns(crl)
+ it "deserializes a CRL" do
+ crl = @class.new('foo')
+ crl.generate(@cert, @key)
- mycrl = stub 'sslcrl'
- mycrl.expects(:content=).with(crl)
-
- @class.expects(:new).with("foo").returns mycrl
-
- @class.from_s("my crl").should == mycrl
+ new_crl = @class.from_s(crl.to_s)
+ expect(new_crl.content.to_text).to eq(crl.content.to_text)
end
end
describe "when an instance" do
before do
- @class.any_instance.stubs(:read_or_generate)
-
@crl = @class.new("whatever")
end
it "should always use 'crl' for its name" do
@crl.name.should == "crl"
end
it "should have a content attribute" do
@crl.should respond_to(:content)
end
end
describe "when generating the crl" do
before do
- @real_crl = mock 'crl'
- @real_crl.stub_everything
-
- OpenSSL::X509::CRL.stubs(:new).returns(@real_crl)
-
- @class.any_instance.stubs(:read_or_generate)
-
@crl = @class.new("crl")
end
it "should set its issuer to the subject of the passed certificate" do
- @real_crl.expects(:issuer=).with(@cert.subject)
-
- @crl.generate(@cert, @key)
+ @crl.generate(@cert, @key).issuer.to_s.should == @cert.subject.to_s
end
it "should set its version to 1" do
- @real_crl.expects(:version=).with(1)
-
- @crl.generate(@cert, @key)
+ @crl.generate(@cert, @key).version.should == 1
end
it "should create an instance of OpenSSL::X509::CRL" do
- OpenSSL::X509::CRL.expects(:new).returns(@real_crl)
-
- @crl.generate(@cert, @key)
+ @crl.generate(@cert, @key).should be_an_instance_of(OpenSSL::X509::CRL)
end
- # The next three tests aren't good, but at least they
- # specify the behaviour.
it "should add an extension for the CRL number" do
- @real_crl.expects(:extensions=)
@crl.generate(@cert, @key)
+
+ expects_crlnumber_extension(@crl, 0)
end
- it "should set the last update time" do
- @real_crl.expects(:last_update=)
+ it "should add an extension for the authority key identifier" do
@crl.generate(@cert, @key)
+
+ expects_authkeyid_extension(@crl, @cert)
end
- it "should set the next update time" do
- @real_crl.expects(:next_update=)
- @crl.generate(@cert, @key)
+ it "returns the last update time in UTC" do
+ # http://tools.ietf.org/html/rfc5280#section-5.1.2.4
+ thisUpdate = @crl.generate(@cert, @key).last_update
+ thisUpdate.should be_utc
+ expects_time_close_to_now(thisUpdate)
end
- it "should sign the CRL" do
- @real_crl.expects(:sign).with { |key, digest| key == @key }
- @crl.generate(@cert, @key)
+ it "returns the next update time in UTC 5 years from now" do
+ # http://tools.ietf.org/html/rfc5280#section-5.1.2.5
+ nextUpdate = @crl.generate(@cert, @key).next_update
+ nextUpdate.should be_utc
+ expects_time_close_to_five_years(nextUpdate)
end
- it "should set the content to the generated crl" do
- @crl.generate(@cert, @key)
- @crl.content.should equal(@real_crl)
+ it "should verify using the CA public_key" do
+ @crl.generate(@cert, @key).verify(@key.public_key).should be_true
end
- it "should return the generated crl" do
- @crl.generate(@cert, @key).should equal(@real_crl)
+ it "should set the content to the generated crl" do
+ # this test shouldn't be needed since we test the return of generate() which should be the content field
+ @crl.generate(@cert, @key)
+ @crl.content.should be_an_instance_of(OpenSSL::X509::CRL)
end
end
# This test suite isn't exactly complete, because the
# SSL stuff is very complicated. It just hits the high points.
describe "when revoking a certificate" do
before do
- @class.wrapped_class.any_instance.stubs(:issuer=)
- @class.wrapped_class.any_instance.stubs(:sign)
-
@crl = @class.new("crl")
@crl.generate(@cert, @key)
- @crl.content.stubs(:sign)
Puppet::SSL::CertificateRevocationList.indirection.stubs :save
-
- @key = mock 'key'
end
it "should require a serial number and the CA's private key" do
- lambda { @crl.revoke }.should raise_error(ArgumentError)
- end
-
- it "should default to OpenSSL::OCSP::REVOKED_STATUS_KEYCOMPROMISE as the revocation reason" do
- # This makes it a bit more of an integration test than we'd normally like, but that's life
- # with openssl.
- reason = OpenSSL::ASN1::Enumerated(OpenSSL::OCSP::REVOKED_STATUS_KEYCOMPROMISE)
- OpenSSL::ASN1.expects(:Enumerated).with(OpenSSL::OCSP::REVOKED_STATUS_KEYCOMPROMISE).returns reason
-
- @crl.revoke(1, @key)
+ expect { @crl.revoke }.to raise_error(ArgumentError)
end
it "should mark the CRL as updated at a time that makes it valid now" do
- time = Time.now
- Time.stubs(:now).returns time
-
- @crl.content.expects(:last_update=).with(time - 1)
-
@crl.revoke(1, @key)
+
+ expects_time_close_to_now(@crl.content.last_update)
end
it "should mark the CRL valid for five years" do
- time = Time.now
- Time.stubs(:now).returns time
-
- @crl.content.expects(:next_update=).with(time + (5 * 365*24*60*60))
-
@crl.revoke(1, @key)
+
+ expects_time_close_to_five_years(@crl.content.next_update)
end
it "should sign the CRL with the CA's private key and a digest instance" do
@crl.content.expects(:sign).with { |key, digest| key == @key and digest.is_a?(OpenSSL::Digest::SHA1) }
@crl.revoke(1, @key)
end
it "should save the CRL" do
Puppet::SSL::CertificateRevocationList.indirection.expects(:save).with(@crl, nil)
@crl.revoke(1, @key)
end
+
+ it "adds the crlNumber extension containing the serial number" do
+ serial = 1
+ @crl.revoke(serial, @key)
+
+ expects_crlnumber_extension(@crl, serial)
+ end
+
+ it "adds the CA cert's subjectKeyId as the authorityKeyIdentifier to the CRL" do
+ @crl.revoke(1, @key)
+
+ expects_authkeyid_extension(@crl, @cert)
+ end
+
+ it "adds a non-critical CRL reason specifying key compromise by default" do
+ # http://tools.ietf.org/html/rfc5280#section-5.3.1
+ serial = 1
+ @crl.revoke(serial, @key)
+
+ expects_crlreason_extension(@crl, 'Key Compromise')
+ end
+
+ it "allows alternate reasons to be specified" do
+ serial = 1
+ @crl.revoke(serial, @key, OpenSSL::OCSP::REVOKED_STATUS_CACOMPROMISE)
+
+ expects_crlreason_extension(@crl, 'CA Compromise')
+ end
end
end
diff --git a/spec/unit/ssl/host_spec.rb b/spec/unit/ssl/host_spec.rb
index 3b341cc4e..a80fe9205 100755
--- a/spec/unit/ssl/host_spec.rb
+++ b/spec/unit/ssl/host_spec.rb
@@ -1,952 +1,940 @@
#! /usr/bin/env ruby
require 'spec_helper'
require 'puppet/ssl/host'
+require 'matchers/json'
def base_pson_comparison(result, pson_hash)
result["fingerprint"].should == pson_hash["fingerprint"]
result["name"].should == pson_hash["name"]
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 JSONMatchers
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
# Get a safe temporary file
dir = tmpdir("ssl_host_testing")
Puppet.settings[:confdir] = dir
Puppet.settings[:vardir] = dir
Puppet.settings.use :main, :ssl
@host = Puppet::SSL::Host.new("myname")
end
after do
# Cleaned out any cached localhost instance.
Puppet::SSL::Host.reset
Puppet::SSL::Host.ca_location = :none
end
it "should use any provided name as its name" do
@host.name.should == "myname"
end
it "should retrieve its public key from its private key" do
realkey = mock 'realkey'
key = stub 'key', :content => realkey
Puppet::SSL::Key.indirection.stubs(:find).returns(key)
pubkey = mock 'public_key'
realkey.expects(:public_key).returns pubkey
@host.public_key.should equal(pubkey)
end
it "should default to being a non-ca host" do
@host.ca?.should be_false
end
it "should be a ca host if its name matches the CA_NAME" do
Puppet::SSL::Host.stubs(:ca_name).returns "yayca"
Puppet::SSL::Host.new("yayca").should be_ca
end
it "should have a method for determining the CA location" do
Puppet::SSL::Host.should respond_to(:ca_location)
end
it "should have a method for specifying the CA location" do
Puppet::SSL::Host.should respond_to(:ca_location=)
end
it "should have a method for retrieving the default ssl host" do
Puppet::SSL::Host.should respond_to(:ca_location=)
end
it "should have a method for producing an instance to manage the local host's keys" do
Puppet::SSL::Host.should respond_to(:localhost)
end
it "should allow to reset localhost" do
previous_host = Puppet::SSL::Host.localhost
Puppet::SSL::Host.reset
Puppet::SSL::Host.localhost.should_not == previous_host
end
it "should generate the certificate for the localhost instance if no certificate is available" do
host = stub 'host', :key => nil
Puppet::SSL::Host.expects(:new).returns host
host.expects(:certificate).returns nil
host.expects(:generate)
Puppet::SSL::Host.localhost.should equal(host)
end
it "should create a localhost cert if no cert is available and it is a CA with autosign and it is using DNS alt names", :unless => Puppet.features.microsoft_windows? do
Puppet[:autosign] = true
Puppet[:confdir] = tmpdir('conf')
Puppet[:dns_alt_names] = "foo,bar,baz"
ca = Puppet::SSL::CertificateAuthority.new
Puppet::SSL::CertificateAuthority.stubs(:instance).returns ca
localhost = Puppet::SSL::Host.localhost
cert = localhost.certificate
cert.should be_a(Puppet::SSL::Certificate)
cert.subject_alt_names.should =~ %W[DNS:#{Puppet[:certname]} DNS:foo DNS:bar DNS:baz]
end
context "with dns_alt_names" do
before :each do
@key = stub('key content')
key = stub('key', :generate => true, :content => @key)
Puppet::SSL::Key.stubs(:new).returns key
Puppet::SSL::Key.indirection.stubs(:save).with(key)
@cr = stub('certificate request')
Puppet::SSL::CertificateRequest.stubs(:new).returns @cr
Puppet::SSL::CertificateRequest.indirection.stubs(:save).with(@cr)
end
describe "explicitly specified" do
before :each do
Puppet[:dns_alt_names] = 'one, two'
end
it "should not include subjectAltName if not the local node" do
@cr.expects(:generate).with(@key, {})
Puppet::SSL::Host.new('not-the-' + Puppet[:certname]).generate
end
it "should include subjectAltName if I am a CA" do
@cr.expects(:generate).
with(@key, { :dns_alt_names => Puppet[:dns_alt_names] })
Puppet::SSL::Host.localhost
end
end
describe "implicitly defaulted" do
let(:ca) { stub('ca', :sign => nil) }
before :each do
Puppet[:dns_alt_names] = ''
Puppet::SSL::CertificateAuthority.stubs(:instance).returns ca
end
it "should not include defaults if we're not the CA" do
Puppet::SSL::CertificateAuthority.stubs(:ca?).returns false
@cr.expects(:generate).with(@key, {})
Puppet::SSL::Host.localhost
end
it "should not include defaults if not the local node" do
Puppet::SSL::CertificateAuthority.stubs(:ca?).returns true
@cr.expects(:generate).with(@key, {})
Puppet::SSL::Host.new('not-the-' + Puppet[:certname]).generate
end
it "should not include defaults if we can't resolve our fqdn" do
Puppet::SSL::CertificateAuthority.stubs(:ca?).returns true
Facter.stubs(:value).with(:fqdn).returns nil
@cr.expects(:generate).with(@key, {})
Puppet::SSL::Host.localhost
end
it "should provide defaults if we're bootstrapping the local master" do
Puppet::SSL::CertificateAuthority.stubs(:ca?).returns true
Facter.stubs(:value).with(:fqdn).returns 'web.foo.com'
Facter.stubs(:value).with(:domain).returns 'foo.com'
@cr.expects(:generate).with(@key, {:dns_alt_names => "puppet, web.foo.com, puppet.foo.com"})
Puppet::SSL::Host.localhost
end
end
end
it "should always read the key for the localhost instance in from disk" do
host = stub 'host', :certificate => "eh"
Puppet::SSL::Host.expects(:new).returns host
host.expects(:key)
Puppet::SSL::Host.localhost
end
it "should cache the localhost instance" do
host = stub 'host', :certificate => "eh", :key => 'foo'
Puppet::SSL::Host.expects(:new).once.returns host
Puppet::SSL::Host.localhost.should == Puppet::SSL::Host.localhost
end
it "should be able to verify its certificate matches its key" do
Puppet::SSL::Host.new("foo").should respond_to(:validate_certificate_with_key)
end
it "should consider the certificate invalid if it cannot find a key" do
host = Puppet::SSL::Host.new("foo")
certificate = mock('cert', :fingerprint => 'DEADBEEF')
host.expects(:certificate).twice.returns certificate
host.expects(:key).returns nil
lambda { host.validate_certificate_with_key }.should raise_error(Puppet::Error, "No private key with which to validate certificate with fingerprint: DEADBEEF")
end
it "should consider the certificate invalid if it cannot find a certificate" do
host = Puppet::SSL::Host.new("foo")
host.expects(:key).never
host.expects(:certificate).returns nil
lambda { host.validate_certificate_with_key }.should raise_error(Puppet::Error, "No certificate to validate.")
end
it "should consider the certificate invalid if the SSL certificate's key verification fails" do
host = Puppet::SSL::Host.new("foo")
key = mock 'key', :content => "private_key"
sslcert = mock 'sslcert'
certificate = mock 'cert', {:content => sslcert, :fingerprint => 'DEADBEEF'}
host.stubs(:key).returns key
host.stubs(:certificate).returns certificate
sslcert.expects(:check_private_key).with("private_key").returns false
lambda { host.validate_certificate_with_key }.should raise_error(Puppet::Error, /DEADBEEF/)
end
it "should consider the certificate valid if the SSL certificate's key verification succeeds" do
host = Puppet::SSL::Host.new("foo")
key = mock 'key', :content => "private_key"
sslcert = mock 'sslcert'
certificate = mock 'cert', :content => sslcert
host.stubs(:key).returns key
host.stubs(:certificate).returns certificate
sslcert.expects(:check_private_key).with("private_key").returns true
lambda{ host.validate_certificate_with_key }.should_not raise_error
end
describe "when specifying the CA location" do
it "should support the location ':local'" do
lambda { Puppet::SSL::Host.ca_location = :local }.should_not raise_error
end
it "should support the location ':remote'" do
lambda { Puppet::SSL::Host.ca_location = :remote }.should_not raise_error
end
it "should support the location ':none'" do
lambda { Puppet::SSL::Host.ca_location = :none }.should_not raise_error
end
it "should support the location ':only'" do
lambda { Puppet::SSL::Host.ca_location = :only }.should_not raise_error
end
it "should not support other modes" do
lambda { Puppet::SSL::Host.ca_location = :whatever }.should raise_error(ArgumentError)
end
describe "as 'local'" do
before do
Puppet::SSL::Host.ca_location = :local
end
it "should set the cache class for Certificate, CertificateRevocationList, and CertificateRequest as :file" do
Puppet::SSL::Certificate.indirection.cache_class.should == :file
Puppet::SSL::CertificateRequest.indirection.cache_class.should == :file
Puppet::SSL::CertificateRevocationList.indirection.cache_class.should == :file
end
it "should set the terminus class for Key and Host as :file" do
Puppet::SSL::Key.indirection.terminus_class.should == :file
Puppet::SSL::Host.indirection.terminus_class.should == :file
end
it "should set the terminus class for Certificate, CertificateRevocationList, and CertificateRequest as :ca" do
Puppet::SSL::Certificate.indirection.terminus_class.should == :ca
Puppet::SSL::CertificateRequest.indirection.terminus_class.should == :ca
Puppet::SSL::CertificateRevocationList.indirection.terminus_class.should == :ca
end
end
describe "as 'remote'" do
before do
Puppet::SSL::Host.ca_location = :remote
end
it "should set the cache class for Certificate, CertificateRevocationList, and CertificateRequest as :file" do
Puppet::SSL::Certificate.indirection.cache_class.should == :file
Puppet::SSL::CertificateRequest.indirection.cache_class.should == :file
Puppet::SSL::CertificateRevocationList.indirection.cache_class.should == :file
end
it "should set the terminus class for Key as :file" do
Puppet::SSL::Key.indirection.terminus_class.should == :file
end
it "should set the terminus class for Host, Certificate, CertificateRevocationList, and CertificateRequest as :rest" do
Puppet::SSL::Host.indirection.terminus_class.should == :rest
Puppet::SSL::Certificate.indirection.terminus_class.should == :rest
Puppet::SSL::CertificateRequest.indirection.terminus_class.should == :rest
Puppet::SSL::CertificateRevocationList.indirection.terminus_class.should == :rest
end
end
describe "as 'only'" do
before do
Puppet::SSL::Host.ca_location = :only
end
it "should set the terminus class for Key, Certificate, CertificateRevocationList, and CertificateRequest as :ca" do
Puppet::SSL::Key.indirection.terminus_class.should == :ca
Puppet::SSL::Certificate.indirection.terminus_class.should == :ca
Puppet::SSL::CertificateRequest.indirection.terminus_class.should == :ca
Puppet::SSL::CertificateRevocationList.indirection.terminus_class.should == :ca
end
it "should set the cache class for Certificate, CertificateRevocationList, and CertificateRequest to nil" do
Puppet::SSL::Certificate.indirection.cache_class.should be_nil
Puppet::SSL::CertificateRequest.indirection.cache_class.should be_nil
Puppet::SSL::CertificateRevocationList.indirection.cache_class.should be_nil
end
it "should set the terminus class for Host to :file" do
Puppet::SSL::Host.indirection.terminus_class.should == :file
end
end
describe "as 'none'" do
before do
Puppet::SSL::Host.ca_location = :none
end
it "should set the terminus class for Key, Certificate, CertificateRevocationList, and CertificateRequest as :file" do
Puppet::SSL::Key.indirection.terminus_class.should == :disabled_ca
Puppet::SSL::Certificate.indirection.terminus_class.should == :disabled_ca
Puppet::SSL::CertificateRequest.indirection.terminus_class.should == :disabled_ca
Puppet::SSL::CertificateRevocationList.indirection.terminus_class.should == :disabled_ca
end
it "should set the terminus class for Host to 'none'" do
lambda { Puppet::SSL::Host.indirection.terminus_class }.should raise_error(Puppet::DevError)
end
end
end
it "should have a class method for destroying all files related to a given host" do
Puppet::SSL::Host.should respond_to(:destroy)
end
describe "when destroying a host's SSL files" do
before do
Puppet::SSL::Key.indirection.stubs(:destroy).returns false
Puppet::SSL::Certificate.indirection.stubs(:destroy).returns false
Puppet::SSL::CertificateRequest.indirection.stubs(:destroy).returns false
end
it "should destroy its certificate, certificate request, and key" do
Puppet::SSL::Key.indirection.expects(:destroy).with("myhost")
Puppet::SSL::Certificate.indirection.expects(:destroy).with("myhost")
Puppet::SSL::CertificateRequest.indirection.expects(:destroy).with("myhost")
Puppet::SSL::Host.destroy("myhost")
end
it "should return true if any of the classes returned true" do
Puppet::SSL::Certificate.indirection.expects(:destroy).with("myhost").returns true
Puppet::SSL::Host.destroy("myhost").should be_true
end
it "should report that nothing was deleted if none of the classes returned true" do
Puppet::SSL::Host.destroy("myhost").should == "Nothing was deleted"
end
end
describe "when initializing" do
it "should default its name to the :certname setting" do
Puppet[:certname] = "myname"
Puppet::SSL::Host.new.name.should == "myname"
end
it "should downcase a passed in name" do
Puppet::SSL::Host.new("Host.Domain.Com").name.should == "host.domain.com"
end
it "should indicate that it is a CA host if its name matches the ca_name constant" do
Puppet::SSL::Host.stubs(:ca_name).returns "myca"
Puppet::SSL::Host.new("myca").should be_ca
end
end
describe "when managing its private key" do
before do
@realkey = "mykey"
@key = Puppet::SSL::Key.new("mykey")
@key.content = @realkey
end
it "should return nil if the key is not set and cannot be found" do
Puppet::SSL::Key.indirection.expects(:find).with("myname").returns(nil)
@host.key.should be_nil
end
it "should find the key in the Key class and return the Puppet instance" do
Puppet::SSL::Key.indirection.expects(:find).with("myname").returns(@key)
@host.key.should equal(@key)
end
it "should be able to generate and save a new key" do
Puppet::SSL::Key.expects(:new).with("myname").returns(@key)
@key.expects(:generate)
Puppet::SSL::Key.indirection.expects(:save)
@host.generate_key.should be_true
@host.key.should equal(@key)
end
it "should not retain keys that could not be saved" do
Puppet::SSL::Key.expects(:new).with("myname").returns(@key)
@key.stubs(:generate)
Puppet::SSL::Key.indirection.expects(:save).raises "eh"
lambda { @host.generate_key }.should raise_error
@host.key.should be_nil
end
it "should return any previously found key without requerying" do
Puppet::SSL::Key.indirection.expects(:find).with("myname").returns(@key).once
@host.key.should equal(@key)
@host.key.should equal(@key)
end
end
describe "when managing its certificate request" do
before do
@realrequest = "real request"
@request = Puppet::SSL::CertificateRequest.new("myname")
@request.content = @realrequest
end
it "should return nil if the key is not set and cannot be found" do
Puppet::SSL::CertificateRequest.indirection.expects(:find).with("myname").returns(nil)
@host.certificate_request.should be_nil
end
it "should find the request in the Key class and return it and return the Puppet SSL request" do
Puppet::SSL::CertificateRequest.indirection.expects(:find).with("myname").returns @request
@host.certificate_request.should equal(@request)
end
it "should generate a new key when generating the cert request if no key exists" do
Puppet::SSL::CertificateRequest.expects(:new).with("myname").returns @request
key = stub 'key', :public_key => mock("public_key"), :content => "mycontent"
@host.expects(:key).times(2).returns(nil).then.returns(key)
@host.expects(:generate_key).returns(key)
@request.stubs(:generate)
Puppet::SSL::CertificateRequest.indirection.stubs(:save)
@host.generate_certificate_request
end
it "should be able to generate and save a new request using the private key" do
Puppet::SSL::CertificateRequest.expects(:new).with("myname").returns @request
key = stub 'key', :public_key => mock("public_key"), :content => "mycontent"
@host.stubs(:key).returns(key)
@request.expects(:generate).with("mycontent", {})
Puppet::SSL::CertificateRequest.indirection.expects(:save).with(@request)
@host.generate_certificate_request.should be_true
@host.certificate_request.should equal(@request)
end
it "should return any previously found request without requerying" do
Puppet::SSL::CertificateRequest.indirection.expects(:find).with("myname").returns(@request).once
@host.certificate_request.should equal(@request)
@host.certificate_request.should equal(@request)
end
it "should not keep its certificate request in memory if the request cannot be saved" do
Puppet::SSL::CertificateRequest.expects(:new).with("myname").returns @request
key = stub 'key', :public_key => mock("public_key"), :content => "mycontent"
@host.stubs(:key).returns(key)
@request.stubs(:generate)
@request.stubs(:name).returns("myname")
terminus = stub 'terminus'
terminus.stubs(:validate)
Puppet::SSL::CertificateRequest.indirection.expects(:prepare).returns(terminus)
terminus.expects(:save).with { |req| req.instance == @request && req.key == "myname" }.raises "eh"
lambda { @host.generate_certificate_request }.should raise_error
@host.instance_eval { @certificate_request }.should be_nil
end
end
describe "when managing its certificate" do
before do
@realcert = mock 'certificate'
@cert = stub 'cert', :content => @realcert
@host.stubs(:key).returns mock("key")
@host.stubs(:validate_certificate_with_key)
end
it "should find the CA certificate if it does not have a certificate" do
Puppet::SSL::Certificate.indirection.expects(:find).with(Puppet::SSL::CA_NAME).returns mock("cacert")
Puppet::SSL::Certificate.indirection.stubs(:find).with("myname").returns @cert
@host.certificate
end
it "should not find the CA certificate if it is the CA host" do
@host.expects(:ca?).returns true
Puppet::SSL::Certificate.indirection.stubs(:find)
Puppet::SSL::Certificate.indirection.expects(:find).with(Puppet::SSL::CA_NAME).never
@host.certificate
end
it "should return nil if it cannot find a CA certificate" do
Puppet::SSL::Certificate.indirection.expects(:find).with(Puppet::SSL::CA_NAME).returns nil
Puppet::SSL::Certificate.indirection.expects(:find).with("myname").never
@host.certificate.should be_nil
end
it "should find the key if it does not have one" do
Puppet::SSL::Certificate.indirection.stubs(:find)
@host.expects(:key).returns mock("key")
@host.certificate
end
it "should generate the key if one cannot be found" do
Puppet::SSL::Certificate.indirection.stubs(:find)
@host.expects(:key).returns nil
@host.expects(:generate_key)
@host.certificate
end
it "should find the certificate in the Certificate class and return the Puppet certificate instance" do
Puppet::SSL::Certificate.indirection.expects(:find).with(Puppet::SSL::CA_NAME).returns mock("cacert")
Puppet::SSL::Certificate.indirection.expects(:find).with("myname").returns @cert
@host.certificate.should equal(@cert)
end
it "should return any previously found certificate" do
Puppet::SSL::Certificate.indirection.expects(:find).with(Puppet::SSL::CA_NAME).returns mock("cacert")
Puppet::SSL::Certificate.indirection.expects(:find).with("myname").returns(@cert).once
@host.certificate.should equal(@cert)
@host.certificate.should equal(@cert)
end
end
it "should have a method for listing certificate hosts" do
Puppet::SSL::Host.should respond_to(:search)
end
describe "when listing certificate hosts" do
it "should default to listing all clients with any file types" do
Puppet::SSL::Key.indirection.expects(:search).returns []
Puppet::SSL::Certificate.indirection.expects(:search).returns []
Puppet::SSL::CertificateRequest.indirection.expects(:search).returns []
Puppet::SSL::Host.search
end
it "should be able to list only clients with a key" do
Puppet::SSL::Key.indirection.expects(:search).returns []
Puppet::SSL::Certificate.indirection.expects(:search).never
Puppet::SSL::CertificateRequest.indirection.expects(:search).never
Puppet::SSL::Host.search :for => Puppet::SSL::Key
end
it "should be able to list only clients with a certificate" do
Puppet::SSL::Key.indirection.expects(:search).never
Puppet::SSL::Certificate.indirection.expects(:search).returns []
Puppet::SSL::CertificateRequest.indirection.expects(:search).never
Puppet::SSL::Host.search :for => Puppet::SSL::Certificate
end
it "should be able to list only clients with a certificate request" do
Puppet::SSL::Key.indirection.expects(:search).never
Puppet::SSL::Certificate.indirection.expects(:search).never
Puppet::SSL::CertificateRequest.indirection.expects(:search).returns []
Puppet::SSL::Host.search :for => Puppet::SSL::CertificateRequest
end
it "should return a Host instance created with the name of each found instance" do
key = stub 'key', :name => "key", :to_ary => nil
cert = stub 'cert', :name => "cert", :to_ary => nil
csr = stub 'csr', :name => "csr", :to_ary => nil
Puppet::SSL::Key.indirection.expects(:search).returns [key]
Puppet::SSL::Certificate.indirection.expects(:search).returns [cert]
Puppet::SSL::CertificateRequest.indirection.expects(:search).returns [csr]
returned = []
%w{key cert csr}.each do |name|
result = mock(name)
returned << result
Puppet::SSL::Host.expects(:new).with(name).returns result
end
result = Puppet::SSL::Host.search
returned.each do |r|
result.should be_include(r)
end
end
end
it "should have a method for generating all necessary files" do
Puppet::SSL::Host.new("me").should respond_to(:generate)
end
describe "when generating files" do
before do
@host = Puppet::SSL::Host.new("me")
@host.stubs(:generate_key)
@host.stubs(:generate_certificate_request)
end
it "should generate a key if one is not present" do
@host.stubs(:key).returns nil
@host.expects(:generate_key)
@host.generate
end
it "should generate a certificate request if one is not present" do
@host.expects(:certificate_request).returns nil
@host.expects(:generate_certificate_request)
@host.generate
end
describe "and it can create a certificate authority" do
before do
@ca = mock 'ca'
Puppet::SSL::CertificateAuthority.stubs(:instance).returns @ca
end
it "should use the CA to sign its certificate request if it does not have a certificate" do
@host.expects(:certificate).returns nil
@ca.expects(:sign).with(@host.name, true)
@host.generate
end
end
describe "and it cannot create a certificate authority" do
before do
Puppet::SSL::CertificateAuthority.stubs(:instance).returns nil
end
it "should seek its certificate" do
@host.expects(:certificate)
@host.generate
end
end
end
it "should have a method for creating an SSL store" do
Puppet::SSL::Host.new("me").should respond_to(:ssl_store)
end
it "should always return the same store" do
host = Puppet::SSL::Host.new("foo")
store = mock 'store'
store.stub_everything
OpenSSL::X509::Store.expects(:new).returns store
host.ssl_store.should equal(host.ssl_store)
end
describe "when creating an SSL store" do
before do
@host = Puppet::SSL::Host.new("me")
@store = mock 'store'
@store.stub_everything
OpenSSL::X509::Store.stubs(:new).returns @store
Puppet[:localcacert] = "ssl_host_testing"
Puppet::SSL::CertificateRevocationList.indirection.stubs(:find).returns(nil)
end
it "should accept a purpose" do
@store.expects(:purpose=).with "my special purpose"
@host.ssl_store("my special purpose")
end
it "should default to OpenSSL::X509::PURPOSE_ANY as the purpose" do
@store.expects(:purpose=).with OpenSSL::X509::PURPOSE_ANY
@host.ssl_store
end
it "should add the local CA cert file" do
Puppet[:localcacert] = "/ca/cert/file"
@store.expects(:add_file).with Puppet[:localcacert]
@host.ssl_store
end
describe "and a CRL is available" do
before do
@crl = stub 'crl', :content => "real_crl"
Puppet::SSL::CertificateRevocationList.indirection.stubs(:find).returns @crl
end
describe "and 'certificate_revocation' is true" do
before do
Puppet[:certificate_revocation] = true
end
it "should add the CRL" do
@store.expects(:add_crl).with "real_crl"
@host.ssl_store
end
it "should set the flags to OpenSSL::X509::V_FLAG_CRL_CHECK_ALL|OpenSSL::X509::V_FLAG_CRL_CHECK" do
@store.expects(:flags=).with OpenSSL::X509::V_FLAG_CRL_CHECK_ALL|OpenSSL::X509::V_FLAG_CRL_CHECK
@host.ssl_store
end
end
describe "and 'certificate_revocation' is false" do
before do
Puppet[:certificate_revocation] = false
end
it "should not add the CRL" do
@store.expects(:add_crl).never
@host.ssl_store
end
it "should not set the flags" do
@store.expects(:flags=).never
@host.ssl_store
end
end
end
end
describe "when waiting for a cert" do
before do
@host = Puppet::SSL::Host.new("me")
end
it "should generate its certificate request and attempt to read the certificate again if no certificate is found" do
@host.expects(:certificate).times(2).returns(nil).then.returns "foo"
@host.expects(:generate)
@host.wait_for_cert(1)
end
it "should catch and log errors during CSR saving" do
@host.expects(:certificate).times(2).returns(nil).then.returns "foo"
@host.expects(:generate).raises(RuntimeError).then.returns nil
@host.stubs(:sleep)
@host.wait_for_cert(1)
end
it "should sleep and retry after failures saving the CSR if waitforcert is enabled" do
@host.expects(:certificate).times(2).returns(nil).then.returns "foo"
@host.expects(:generate).raises(RuntimeError).then.returns nil
@host.expects(:sleep).with(1)
@host.wait_for_cert(1)
end
it "should exit after failures saving the CSR of waitforcert is disabled" do
@host.expects(:certificate).returns(nil)
@host.expects(:generate).raises(RuntimeError)
@host.expects(:puts)
expect { @host.wait_for_cert(0) }.to exit_with 1
end
it "should exit if the wait time is 0 and it can neither find nor retrieve a certificate" do
@host.stubs(:certificate).returns nil
@host.expects(:generate)
@host.expects(:puts)
expect { @host.wait_for_cert(0) }.to exit_with 1
end
it "should sleep for the specified amount of time if no certificate is found after generating its certificate request" do
@host.expects(:certificate).times(3).returns(nil).then.returns(nil).then.returns "foo"
@host.expects(:generate)
@host.expects(:sleep).with(1)
@host.wait_for_cert(1)
end
it "should catch and log exceptions during certificate retrieval" do
@host.expects(:certificate).times(3).returns(nil).then.raises(RuntimeError).then.returns("foo")
@host.stubs(:generate)
@host.stubs(:sleep)
Puppet.expects(:err)
@host.wait_for_cert(1)
end
end
describe "when handling PSON", :unless => Puppet.features.microsoft_windows? do
include PuppetSpec::Files
before do
Puppet[:vardir] = tmpdir("ssl_test_vardir")
Puppet[:ssldir] = tmpdir("ssl_test_ssldir")
# localcacert is where each client stores the CA certificate
# cacert is where the master stores the CA certificate
# Since we need to play the role of both for testing we need them to be the same and exist
Puppet[:cacert] = Puppet[:localcacert]
@ca=Puppet::SSL::CertificateAuthority.new
end
describe "when converting to PSON" do
let(:host) do
Puppet::SSL::Host.new("bazinga")
end
let(:pson_hash) do
{
"fingerprint" => host.certificate_request.fingerprint,
"desired_state" => 'requested',
"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
+ it "should validate against the schema" do
host.generate_certificate_request
- validate_json_for_host(host)
+
+ expect(host.to_pson).to validate_against('api/schemas/host.json')
end
describe "explicit fingerprints" do
[:SHA1, :SHA256, :SHA512].each do |md|
it "should include #{md}" do
mds = md.to_s
host.generate_certificate_request
pson_hash["fingerprints"] = {}
pson_hash["fingerprints"][mds] = host.certificate_request.fingerprint(md)
result = PSON.parse(Puppet::SSL::Host.new(host.name).to_pson)
base_pson_comparison result, pson_hash
result["fingerprints"][mds].should == pson_hash["fingerprints"][mds]
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
host.generate_certificate_request
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
end
[ "",
"test, alt, names"
].each do |alt_names|
describe "when #{alt_names}" 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)
+ it "should validate against the schema" do
+ expect(host.to_pson).to validate_against('api/schemas/host.json')
end
end
end
end
it "should be able to identify a host with a signed certificate" do
host.generate_certificate_request
@ca.sign(host.name)
pson_hash = {
"fingerprint" => Puppet::SSL::Certificate.indirection.find(host.name).fingerprint,
"desired_state" => 'signed',
"name" => host.name,
}
result = PSON.parse(Puppet::SSL::Host.new(host.name).to_pson)
base_pson_comparison result, pson_hash
end
it "should be able to identify a host with a revoked certificate" do
host.generate_certificate_request
@ca.sign(host.name)
@ca.revoke(host.name)
pson_hash["fingerprint"] = Puppet::SSL::Certificate.indirection.find(host.name).fingerprint
pson_hash["desired_state"] = 'revoked'
result = PSON.parse(Puppet::SSL::Host.new(host.name).to_pson)
base_pson_comparison result, pson_hash
end
end
describe "when converting from PSON" do
it "should return a Puppet::SSL::Host object with the specified desired state" do
host = Puppet::SSL::Host.new("bazinga")
host.desired_state="signed"
pson_hash = {
"name" => host.name,
"desired_state" => host.desired_state,
}
- generated_host = Puppet::SSL::Host.from_pson(pson_hash)
+ generated_host = Puppet::SSL::Host.from_data_hash(pson_hash)
generated_host.desired_state.should == host.desired_state
generated_host.name.should == host.name
end
end
end
end
diff --git a/spec/unit/ssl/inventory_spec.rb b/spec/unit/ssl/inventory_spec.rb
index 9bcbbcea5..6e4fbd340 100755
--- a/spec/unit/ssl/inventory_spec.rb
+++ b/spec/unit/ssl/inventory_spec.rb
@@ -1,137 +1,137 @@
#! /usr/bin/env ruby
require 'spec_helper'
require 'puppet/ssl/inventory'
describe Puppet::SSL::Inventory, :unless => Puppet.features.microsoft_windows? do
let(:cert_inventory) { File.expand_path("/inven/tory") }
before do
@class = Puppet::SSL::Inventory
end
describe "when initializing" do
it "should set its path to the inventory file" do
Puppet[:cert_inventory] = cert_inventory
@class.new.path.should == cert_inventory
end
end
describe "when managing an inventory" do
before do
Puppet[:cert_inventory] = cert_inventory
- Puppet::FileSystem::File.stubs(:exist?).with(cert_inventory).returns true
+ Puppet::FileSystem.stubs(:exist?).with(cert_inventory).returns true
@inventory = @class.new
@cert = mock 'cert'
end
describe "and creating the inventory file" do
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]
@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 use the Settings to write to the file" do
Puppet.settings.setting(:cert_inventory).expects(:open).with("a")
@inventory.add(@cert)
end
it "should add formatted certificate information to the end of the file" do
cert = Puppet::SSL::Certificate.new("mycert")
cert.content = @cert
fh = StringIO.new
Puppet.settings.setting(:cert_inventory).expects(:open).with("a").yields(fh)
@inventory.expects(:format).with(@cert).returns "myformat"
@inventory.add(@cert)
expect(fh.string).to eq("myformat")
end
end
describe "and formatting a certificate" do
before do
@cert = stub 'cert', :not_before => Time.now, :not_after => Time.now, :subject => "mycert", :serial => 15
end
it "should print the serial number as a 4 digit hex number in the first field" do
@inventory.format(@cert).split[0].should == "0x000f" # 15 in hex
end
it "should print the not_before date in '%Y-%m-%dT%H:%M:%S%Z' format in the second field" do
@cert.not_before.expects(:strftime).with('%Y-%m-%dT%H:%M:%S%Z').returns "before_time"
@inventory.format(@cert).split[1].should == "before_time"
end
it "should print the not_after date in '%Y-%m-%dT%H:%M:%S%Z' format in the third field" do
@cert.not_after.expects(:strftime).with('%Y-%m-%dT%H:%M:%S%Z').returns "after_time"
@inventory.format(@cert).split[2].should == "after_time"
end
it "should print the subject in the fourth field" do
@inventory.format(@cert).split[3].should == "mycert"
end
it "should add a carriage return" do
@inventory.format(@cert).should =~ /\n$/
end
it "should produce a line consisting of the serial number, start date, expiration date, and subject" do
# Just make sure our serial and subject bracket the lines.
@inventory.format(@cert).should =~ /^0x.+mycert$/
end
end
it "should be able to find a given host's serial number" do
@inventory.should respond_to(:serial)
end
describe "and finding a serial number" do
it "should return nil if the inventory file is missing" do
- Puppet::FileSystem::File.expects(:exist?).with(cert_inventory).returns false
+ Puppet::FileSystem.expects(:exist?).with(cert_inventory).returns false
@inventory.serial(:whatever).should be_nil
end
it "should return the serial number from the line matching the provided name" do
File.expects(:readlines).with(cert_inventory).returns ["0x00f blah blah /CN=me\n", "0x001 blah blah /CN=you\n"]
@inventory.serial("me").should == 15
end
it "should return the number as an integer" do
File.expects(:readlines).with(cert_inventory).returns ["0x00f blah blah /CN=me\n", "0x001 blah blah /CN=you\n"]
@inventory.serial("me").should == 15
end
end
end
end
diff --git a/spec/unit/ssl/key_spec.rb b/spec/unit/ssl/key_spec.rb
index 4cea5491c..c7f54ff5d 100755
--- a/spec/unit/ssl/key_spec.rb
+++ b/spec/unit/ssl/key_spec.rb
@@ -1,191 +1,191 @@
#! /usr/bin/env ruby
require 'spec_helper'
require 'puppet/ssl/key'
describe Puppet::SSL::Key do
before do
@class = Puppet::SSL::Key
end
it "should be extended with the Indirector module" do
@class.singleton_class.should be_include(Puppet::Indirector)
end
it "should indirect key" do
@class.indirection.name.should == :key
end
it "should only support the text format" do
@class.supported_formats.should == [:s]
end
it "should have a method for determining whether it's a CA key" do
@class.new("test").should respond_to(:ca?)
end
it "should consider itself a ca key if its name matches the CA_NAME" do
@class.new(Puppet::SSL::Host.ca_name).should be_ca
end
describe "when initializing" do
it "should set its password file to the :capass if it's a CA key" do
Puppet[:capass] = File.expand_path("/ca/pass")
key = Puppet::SSL::Key.new(Puppet::SSL::Host.ca_name)
key.password_file.should == Puppet[:capass]
end
it "should downcase its name" do
@class.new("MyName").name.should == "myname"
end
it "should set its password file to the default password file if it is not the CA key" do
Puppet[:passfile] = File.expand_path("/normal/pass")
key = Puppet::SSL::Key.new("notca")
key.password_file.should == Puppet[:passfile]
end
end
describe "when managing instances" do
before do
@key = @class.new("myname")
end
it "should have a name attribute" do
@key.name.should == "myname"
end
it "should have a content attribute" do
@key.should respond_to(:content)
end
it "should be able to read keys from disk" do
path = "/my/path"
File.expects(:read).with(path).returns("my key")
key = mock 'key'
OpenSSL::PKey::RSA.expects(:new).returns(key)
@key.read(path).should equal(key)
@key.content.should equal(key)
end
it "should not try to use the provided password file if the file does not exist" do
- Puppet::FileSystem::File.stubs(:exist?).returns false
+ Puppet::FileSystem.stubs(:exist?).returns false
@key.password_file = "/path/to/password"
path = "/my/path"
File.stubs(:read).with(path).returns("my key")
OpenSSL::PKey::RSA.expects(:new).with("my key", nil).returns(mock('key'))
File.expects(:read).with("/path/to/password").never
@key.read(path)
end
it "should read the key with the password retrieved from the password file if one is provided" do
- Puppet::FileSystem::File.stubs(:exist?).returns true
+ Puppet::FileSystem.stubs(:exist?).returns true
@key.password_file = "/path/to/password"
path = "/my/path"
File.expects(:read).with(path).returns("my key")
File.expects(:read).with("/path/to/password").returns("my password")
key = mock 'key'
OpenSSL::PKey::RSA.expects(:new).with("my key", "my password").returns(key)
@key.read(path).should equal(key)
@key.content.should equal(key)
end
it "should return an empty string when converted to a string with no key" do
@key.to_s.should == ""
end
it "should convert the key to pem format when converted to a string" do
key = mock 'key', :to_pem => "pem"
@key.content = key
@key.to_s.should == "pem"
end
it "should have a :to_text method that it delegates to the actual key" do
real_key = mock 'key'
real_key.expects(:to_text).returns "keytext"
@key.content = real_key
@key.to_text.should == "keytext"
end
end
describe "when generating the private key" do
before do
@instance = @class.new("test")
@key = mock 'key'
end
it "should create an instance of OpenSSL::PKey::RSA" do
OpenSSL::PKey::RSA.expects(:new).returns(@key)
@instance.generate
end
it "should create the private key with the keylength specified in the settings" do
Puppet[:keylength] = "50"
OpenSSL::PKey::RSA.expects(:new).with(50).returns(@key)
@instance.generate
end
it "should set the content to the generated key" do
OpenSSL::PKey::RSA.stubs(:new).returns(@key)
@instance.generate
@instance.content.should equal(@key)
end
it "should return the generated key" do
OpenSSL::PKey::RSA.stubs(:new).returns(@key)
@instance.generate.should equal(@key)
end
it "should return the key in pem format" do
@instance.generate
@instance.content.expects(:to_pem).returns "my normal key"
@instance.to_s.should == "my normal key"
end
describe "with a password file set" do
it "should return a nil password if the password file does not exist" do
- Puppet::FileSystem::File.expects(:exist?).with("/path/to/pass").returns false
+ Puppet::FileSystem.expects(:exist?).with("/path/to/pass").returns false
File.expects(:read).with("/path/to/pass").never
@instance.password_file = "/path/to/pass"
@instance.password.should be_nil
end
it "should return the contents of the password file as its password" do
- Puppet::FileSystem::File.expects(:exist?).with("/path/to/pass").returns true
+ Puppet::FileSystem.expects(:exist?).with("/path/to/pass").returns true
File.expects(:read).with("/path/to/pass").returns "my password"
@instance.password_file = "/path/to/pass"
@instance.password.should == "my password"
end
it "should export the private key to text using the password" do
Puppet[:keylength] = "50"
@instance.password_file = "/path/to/pass"
@instance.stubs(:password).returns "my password"
OpenSSL::PKey::RSA.expects(:new).returns(@key)
@instance.generate
cipher = mock 'cipher'
OpenSSL::Cipher::DES.expects(:new).with(:EDE3, :CBC).returns cipher
@key.expects(:export).with(cipher, "my password").returns "my encrypted key"
@instance.to_s.should == "my encrypted key"
end
end
end
end
diff --git a/spec/unit/status_spec.rb b/spec/unit/status_spec.rb
index 8f598f579..e71faa666 100755
--- a/spec/unit/status_spec.rb
+++ b/spec/unit/status_spec.rb
@@ -1,49 +1,51 @@
#! /usr/bin/env ruby
require 'spec_helper'
+require 'matchers/json'
+
describe Puppet::Status do
+ include JSONMatchers
+
it "should implement find" do
Puppet::Status.indirection.find( :default ).should be_is_a(Puppet::Status)
Puppet::Status.indirection.find( :default ).status["is_alive"].should == true
end
it "should default to is_alive is true" do
Puppet::Status.new.status["is_alive"].should == true
end
it "should return a pson hash" do
Puppet::Status.new.status.to_pson.should == '{"is_alive":true}'
end
it "should render to a pson hash" do
PSON::pretty_generate(Puppet::Status.new).should =~ /"is_alive":\s*true/
end
it "should accept a hash from pson" do
status = Puppet::Status.new( { "is_alive" => false } )
status.status.should == { "is_alive" => false }
end
it "should have a name" do
Puppet::Status.new.name
end
it "should allow a name to be set" do
Puppet::Status.new.name = "status"
end
it "can do a round-trip serialization via YAML" do
status = Puppet::Status.new
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'))
+ it "serializes to PSON that conforms to the status schema" do
status = Puppet::Status.new
status.version = Puppet.version
- JSON::Validator.validate!(JSON_META_SCHEMA, schema)
- JSON::Validator.validate!(schema, status.render('pson'))
+ expect(status.render('pson')).to validate_against('api/schemas/status.json')
end
end
diff --git a/spec/unit/transaction/additional_resource_generator_spec.rb b/spec/unit/transaction/additional_resource_generator_spec.rb
index d6f99ede3..27dc3cbeb 100644
--- a/spec/unit/transaction/additional_resource_generator_spec.rb
+++ b/spec/unit/transaction/additional_resource_generator_spec.rb
@@ -1,419 +1,411 @@
require 'spec_helper'
require 'puppet/transaction'
require 'puppet_spec/compiler'
require 'matchers/relationship_graph_matchers'
require 'matchers/include_in_order'
+require 'matchers/resource'
describe Puppet::Transaction::AdditionalResourceGenerator do
include PuppetSpec::Compiler
include PuppetSpec::Files
include RelationshipGraphMatchers
+ include Matchers::Resource
let(:prioritizer) { Puppet::Graph::SequentialPrioritizer.new }
def find_vertex(graph, type, title)
graph.vertices.find {|v| v.type == type and v.title == title}
end
Puppet::Type.newtype(:generator) do
include PuppetSpec::Compiler
newparam(:name) do
isnamevar
end
newparam(:kind) do
defaultto :eval_generate
newvalues(:eval_generate, :generate)
end
newparam(:code)
def respond_to?(method_name)
method_name == self[:kind] || super
end
def eval_generate
eval_code
end
def generate
eval_code
end
def eval_code
if self[:code]
compile_to_ral(self[:code]).resources.select { |r| r.ref =~ /Notify/ }
else
[]
end
end
end
context "when applying eval_generate" do
it "should add the generated resources to the catalog" do
catalog = compile_to_ral(<<-MANIFEST)
generator { thing:
code => 'notify { hello: }'
}
MANIFEST
eval_generate_resources_in(catalog, relationship_graph_for(catalog), 'Generator[thing]')
expect(catalog).to have_resource('Notify[hello]')
end
it "should add a sentinel whit for the resource" do
graph = relationships_after_eval_generating(<<-MANIFEST, 'Generator[thing]')
generator { thing:
code => 'notify { hello: }'
}
MANIFEST
find_vertex(graph, :whit, "completed_thing").must be_a(Puppet::Type.type(:whit))
end
it "should replace dependencies on the resource with dependencies on the sentinel" do
graph = relationships_after_eval_generating(<<-MANIFEST, 'Generator[thing]')
generator { thing:
code => 'notify { hello: }'
}
notify { last: require => Generator['thing'] }
MANIFEST
expect(graph).to enforce_order_with_edge(
'Whit[completed_thing]', 'Notify[last]')
end
it "should add an edge from the nearest ancestor to the generated resource" do
graph = relationships_after_eval_generating(<<-MANIFEST, 'Generator[thing]')
generator { thing:
code => 'notify { hello: } notify { goodbye: }'
}
MANIFEST
expect(graph).to enforce_order_with_edge(
'Generator[thing]', 'Notify[hello]')
expect(graph).to enforce_order_with_edge(
'Generator[thing]', 'Notify[goodbye]')
end
it "should add an edge from each generated resource to the sentinel" do
graph = relationships_after_eval_generating(<<-MANIFEST, 'Generator[thing]')
generator { thing:
code => 'notify { hello: } notify { goodbye: }'
}
MANIFEST
expect(graph).to enforce_order_with_edge(
'Notify[hello]', 'Whit[completed_thing]')
expect(graph).to enforce_order_with_edge(
'Notify[goodbye]', 'Whit[completed_thing]')
end
it "should add an edge from the resource to the sentinel" do
graph = relationships_after_eval_generating(<<-MANIFEST, 'Generator[thing]')
generator { thing:
code => 'notify { hello: }'
}
MANIFEST
expect(graph).to enforce_order_with_edge(
'Generator[thing]', 'Whit[completed_thing]')
end
it "should contain the generated resources in the same container as the generator" do
catalog = compile_to_ral(<<-MANIFEST)
class container {
generator { thing:
code => 'notify { hello: }'
}
}
include container
MANIFEST
eval_generate_resources_in(catalog, relationship_graph_for(catalog), 'Generator[thing]')
expect(catalog).to contain_resources_equally('Generator[thing]', 'Notify[hello]')
end
- it "should return false if an error occured when generating resources" do
+ it "should return false if an error occurred when generating resources" do
catalog = compile_to_ral(<<-MANIFEST)
generator { thing:
code => 'fail("not a good generation")'
}
MANIFEST
generator = Puppet::Transaction::AdditionalResourceGenerator.new(catalog, relationship_graph_for(catalog), prioritizer)
expect(generator.eval_generate(catalog.resource('Generator[thing]'))).
to eq(false)
end
it "should return true if resources were generated" do
catalog = compile_to_ral(<<-MANIFEST)
generator { thing:
code => 'notify { hello: }'
}
MANIFEST
generator = Puppet::Transaction::AdditionalResourceGenerator.new(catalog, relationship_graph_for(catalog), prioritizer)
expect(generator.eval_generate(catalog.resource('Generator[thing]'))).
to eq(true)
end
it "should not add a sentinel if no resources are generated" do
catalog = compile_to_ral(<<-MANIFEST)
generator { thing: }
MANIFEST
relationship_graph = relationship_graph_for(catalog)
generator = Puppet::Transaction::AdditionalResourceGenerator.new(catalog, relationship_graph, prioritizer)
expect(generator.eval_generate(catalog.resource('Generator[thing]'))).
to eq(false)
expect(find_vertex(relationship_graph, :whit, "completed_thing")).to be_nil
end
it "orders generated resources with the generator" do
graph = relationships_after_eval_generating(<<-MANIFEST, 'Generator[thing]')
notify { before: }
generator { thing:
code => 'notify { hello: }'
}
notify { after: }
MANIFEST
expect(order_resources_traversed_in(graph)).to(
include_in_order("Notify[before]", "Generator[thing]", "Notify[hello]", "Notify[after]"))
end
it "orders the generator in manifest order with dependencies" do
graph = relationships_after_eval_generating(<<-MANIFEST, 'Generator[thing]')
notify { before: }
generator { thing:
code => 'notify { hello: } notify { goodbye: }'
}
notify { third: require => Generator['thing'] }
notify { after: }
MANIFEST
expect(order_resources_traversed_in(graph)).to(
include_in_order("Notify[before]",
"Generator[thing]",
"Notify[hello]",
"Notify[goodbye]",
"Notify[third]",
"Notify[after]"))
end
it "duplicate generated resources are made dependent on the generator" do
graph = relationships_after_eval_generating(<<-MANIFEST, 'Generator[thing]')
notify { before: }
notify { hello: }
generator { thing:
code => 'notify { before: }'
}
notify { third: require => Generator['thing'] }
notify { after: }
MANIFEST
expect(order_resources_traversed_in(graph)).to(
include_in_order("Notify[hello]", "Generator[thing]", "Notify[before]", "Notify[third]", "Notify[after]"))
end
it "preserves dependencies on duplicate generated resources" do
graph = relationships_after_eval_generating(<<-MANIFEST, 'Generator[thing]')
notify { before: }
generator { thing:
code => 'notify { hello: } notify { before: }',
require => 'Notify[before]'
}
notify { third: require => Generator['thing'] }
notify { after: }
MANIFEST
expect(order_resources_traversed_in(graph)).to(
include_in_order("Notify[before]", "Generator[thing]", "Notify[hello]", "Notify[third]", "Notify[after]"))
end
def relationships_after_eval_generating(manifest, resource_to_generate)
catalog = compile_to_ral(manifest)
relationship_graph = relationship_graph_for(catalog)
eval_generate_resources_in(catalog, relationship_graph, resource_to_generate)
relationship_graph
end
def eval_generate_resources_in(catalog, relationship_graph, resource_to_generate)
generator = Puppet::Transaction::AdditionalResourceGenerator.new(catalog, relationship_graph, prioritizer)
generator.eval_generate(catalog.resource(resource_to_generate))
end
end
context "when applying generate" do
it "should add the generated resources to the catalog" do
catalog = compile_to_ral(<<-MANIFEST)
generator { thing:
kind => generate,
code => 'notify { hello: }'
}
MANIFEST
generate_resources_in(catalog, relationship_graph_for(catalog), 'Generator[thing]')
expect(catalog).to have_resource('Notify[hello]')
end
it "should contain the generated resources in the same container as the generator" do
catalog = compile_to_ral(<<-MANIFEST)
class container {
generator { thing:
kind => generate,
code => 'notify { hello: }'
}
}
include container
MANIFEST
generate_resources_in(catalog, relationship_graph_for(catalog), 'Generator[thing]')
expect(catalog).to contain_resources_equally('Generator[thing]', 'Notify[hello]')
end
it "should add an edge from the nearest ancestor to the generated resource" do
graph = relationships_after_generating(<<-MANIFEST, 'Generator[thing]')
generator { thing:
kind => generate,
code => 'notify { hello: } notify { goodbye: }'
}
MANIFEST
expect(graph).to enforce_order_with_edge(
'Generator[thing]', 'Notify[hello]')
expect(graph).to enforce_order_with_edge(
'Generator[thing]', 'Notify[goodbye]')
end
it "orders generated resources with the generator" do
graph = relationships_after_generating(<<-MANIFEST, 'Generator[thing]')
notify { before: }
generator { thing:
kind => generate,
code => 'notify { hello: }'
}
notify { after: }
MANIFEST
expect(order_resources_traversed_in(graph)).to(
include_in_order("Notify[before]", "Generator[thing]", "Notify[hello]", "Notify[after]"))
end
it "duplicate generated resources are made dependent on the generator" do
graph = relationships_after_generating(<<-MANIFEST, 'Generator[thing]')
notify { before: }
notify { hello: }
generator { thing:
kind => generate,
code => 'notify { before: }'
}
notify { third: require => Generator['thing'] }
notify { after: }
MANIFEST
expect(order_resources_traversed_in(graph)).to(
include_in_order("Notify[hello]", "Generator[thing]", "Notify[before]", "Notify[third]", "Notify[after]"))
end
it "preserves dependencies on duplicate generated resources" do
graph = relationships_after_generating(<<-MANIFEST, 'Generator[thing]')
notify { before: }
generator { thing:
kind => generate,
code => 'notify { hello: } notify { before: }',
require => 'Notify[before]'
}
notify { third: require => Generator['thing'] }
notify { after: }
MANIFEST
expect(order_resources_traversed_in(graph)).to(
include_in_order("Notify[before]", "Generator[thing]", "Notify[hello]", "Notify[third]", "Notify[after]"))
end
it "orders the generator in manifest order with dependencies" do
graph = relationships_after_generating(<<-MANIFEST, 'Generator[thing]')
notify { before: }
generator { thing:
kind => generate,
code => 'notify { hello: } notify { goodbye: }'
}
notify { third: require => Generator['thing'] }
notify { after: }
MANIFEST
expect(order_resources_traversed_in(graph)).to(
include_in_order("Notify[before]",
"Generator[thing]",
"Notify[hello]",
"Notify[goodbye]",
"Notify[third]",
"Notify[after]"))
end
def relationships_after_generating(manifest, resource_to_generate)
catalog = compile_to_ral(manifest)
relationship_graph = relationship_graph_for(catalog)
generate_resources_in(catalog, relationship_graph, resource_to_generate)
relationship_graph
end
def generate_resources_in(catalog, relationship_graph, resource_to_generate)
generator = Puppet::Transaction::AdditionalResourceGenerator.new(catalog, relationship_graph, prioritizer)
generator.generate_additional_resources(catalog.resource(resource_to_generate))
end
end
def relationship_graph_for(catalog)
relationship_graph = Puppet::Graph::RelationshipGraph.new(prioritizer)
relationship_graph.populate_from(catalog)
relationship_graph
end
def order_resources_traversed_in(relationships)
order_seen = []
relationships.traverse { |resource| order_seen << resource.ref }
order_seen
end
RSpec::Matchers.define :contain_resources_equally do |*resource_refs|
match do |catalog|
@containers = resource_refs.collect do |resource_ref|
catalog.container_of(catalog.resource(resource_ref)).ref
end
@containers.all? { |resource_ref| resource_ref == @containers[0] }
end
def failure_message_for_should
"expected #{@expected.join(', ')} to all be contained in the same resource but the containment was #{@expected.zip(@containers).collect { |(res, container)| res + ' => ' + container }.join(', ')}"
end
end
end
-
-RSpec::Matchers.define :have_resource do |expected_resource|
- match do |actual_catalog|
- actual_catalog.resource(expected_resource)
- end
-
- def failure_message_for_should
- "expected #{@actual.to_dot} to include #{@expected[0]}"
- end
-end
diff --git a/spec/unit/transaction/event_spec.rb b/spec/unit/transaction/event_spec.rb
index 8e62e02f6..4781cbca1 100755
--- a/spec/unit/transaction/event_spec.rb
+++ b/spec/unit/transaction/event_spec.rb
@@ -1,192 +1,192 @@
#! /usr/bin/env ruby
require 'spec_helper'
require 'puppet/transaction/event'
class TestResource
def to_s
"Foo[bar]"
end
def [](v)
nil
end
end
describe Puppet::Transaction::Event do
include PuppetSpec::Files
it "should support resource" do
event = Puppet::Transaction::Event.new
event.resource = TestResource.new
event.resource.should == "Foo[bar]"
end
it "should always convert the property to a string" do
Puppet::Transaction::Event.new(:property => :foo).property.should == "foo"
end
it "should always convert the resource to a string" do
Puppet::Transaction::Event.new(:resource => TestResource.new).resource.should == "Foo[bar]"
end
it "should produce the message when converted to a string" do
event = Puppet::Transaction::Event.new
event.expects(:message).returns "my message"
event.to_s.should == "my message"
end
it "should support 'status'" do
event = Puppet::Transaction::Event.new
event.status = "success"
event.status.should == "success"
end
it "should fail if the status is not to 'audit', 'noop', 'success', or 'failure" do
event = Puppet::Transaction::Event.new
lambda { event.status = "foo" }.should raise_error(ArgumentError)
end
it "should support tags" do
Puppet::Transaction::Event.ancestors.should include(Puppet::Util::Tagging)
end
it "should create a timestamp at its creation time" do
Puppet::Transaction::Event.new.time.should be_instance_of(Time)
end
describe "audit property" do
it "should default to false" do
Puppet::Transaction::Event.new.audited.should == false
end
end
describe "when sending logs" do
before do
Puppet::Util::Log.stubs(:new)
end
it "should set the level to the resources's log level if the event status is 'success' and a resource is available" do
resource = stub 'resource'
resource.expects(:[]).with(:loglevel).returns :myloglevel
Puppet::Util::Log.expects(:create).with { |args| args[:level] == :myloglevel }
Puppet::Transaction::Event.new(:status => "success", :resource => resource).send_log
end
it "should set the level to 'notice' if the event status is 'success' and no resource is available" do
Puppet::Util::Log.expects(:new).with { |args| args[:level] == :notice }
Puppet::Transaction::Event.new(:status => "success").send_log
end
it "should set the level to 'notice' if the event status is 'noop'" do
Puppet::Util::Log.expects(:new).with { |args| args[:level] == :notice }
Puppet::Transaction::Event.new(:status => "noop").send_log
end
it "should set the level to 'err' if the event status is 'failure'" do
Puppet::Util::Log.expects(:new).with { |args| args[:level] == :err }
Puppet::Transaction::Event.new(:status => "failure").send_log
end
it "should set the 'message' to the event log" do
Puppet::Util::Log.expects(:new).with { |args| args[:message] == "my message" }
Puppet::Transaction::Event.new(:message => "my message").send_log
end
it "should set the tags to the event tags" do
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
[:file, :line].each do |attr|
it "should pass the #{attr}" do
Puppet::Util::Log.expects(:new).with { |args| args[attr] == "my val" }
Puppet::Transaction::Event.new(attr => "my val").send_log
end
end
it "should use the source description as the source if one is set" do
Puppet::Util::Log.expects(:new).with { |args| args[:source] == "/my/param" }
Puppet::Transaction::Event.new(:source_description => "/my/param", :resource => TestResource.new, :property => "foo").send_log
end
it "should use the property as the source if one is available and no source description is set" do
Puppet::Util::Log.expects(:new).with { |args| args[:source] == "foo" }
Puppet::Transaction::Event.new(:resource => TestResource.new, :property => "foo").send_log
end
it "should use the property as the source if one is available and no property or source description is set" do
Puppet::Util::Log.expects(:new).with { |args| args[:source] == "Foo[bar]" }
Puppet::Transaction::Event.new(:resource => TestResource.new).send_log
end
end
describe "When converting to YAML" do
it "should include only documented attributes" do
resource = Puppet::Type.type(:file).new(:title => make_absolute("/tmp/foo"))
event = Puppet::Transaction::Event.new(:source_description => "/my/param", :resource => resource,
:file => "/foo.rb", :line => 27, :tags => %w{one two},
:desired_value => 7, :historical_value => 'Brazil',
:message => "Help I'm trapped in a spec test",
:name => :mode_changed, :previous_value => 6, :property => :mode,
:status => 'success')
event.to_yaml_properties.should =~ Puppet::Transaction::Event::YAML_ATTRIBUTES
end
end
it "should round trip through pson" do
resource = Puppet::Type.type(:file).new(:title => make_absolute("/tmp/foo"))
event = Puppet::Transaction::Event.new(
:source_description => "/my/param",
:resource => resource,
:file => "/foo.rb",
:line => 27,
:tags => %w{one two},
:desired_value => 7,
:historical_value => 'Brazil',
:message => "Help I'm trapped in a spec test",
:name => :mode_changed,
:previous_value => 6,
:property => :mode,
:status => 'success')
- tripped = Puppet::Transaction::Event.from_pson(PSON.parse(event.to_pson))
+ tripped = Puppet::Transaction::Event.from_data_hash(PSON.parse(event.to_pson))
tripped.audited.should == event.audited
tripped.property.should == event.property
tripped.previous_value.should == event.previous_value
tripped.desired_value.should == event.desired_value
tripped.historical_value.should == event.historical_value
tripped.message.should == event.message
tripped.name.should == event.name
tripped.status.should == event.status
tripped.time.should == event.time
end
it "should round trip an event for an inspect report through pson" do
resource = Puppet::Type.type(:file).new(:title => make_absolute("/tmp/foo"))
event = Puppet::Transaction::Event.new(
:audited => true,
:source_description => "/my/param",
:resource => resource,
:file => "/foo.rb",
:line => 27,
:tags => %w{one two},
:message => "Help I'm trapped in a spec test",
:previous_value => 6,
:property => :mode,
:status => 'success')
- tripped = Puppet::Transaction::Event.from_pson(PSON.parse(event.to_pson))
+ tripped = Puppet::Transaction::Event.from_data_hash(PSON.parse(event.to_pson))
tripped.desired_value.should be_nil
tripped.historical_value.should be_nil
tripped.name.should be_nil
tripped.audited.should == event.audited
tripped.property.should == event.property
tripped.previous_value.should == event.previous_value
tripped.message.should == event.message
tripped.status.should == event.status
tripped.time.should == event.time
end
end
diff --git a/spec/unit/transaction/report_spec.rb b/spec/unit/transaction/report_spec.rb
index 661349dab..1eafe5ae3 100755
--- a/spec/unit/transaction/report_spec.rb
+++ b/spec/unit/transaction/report_spec.rb
@@ -1,488 +1,497 @@
#! /usr/bin/env ruby
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
+require 'matchers/json'
describe Puppet::Transaction::Report do
+ include JSONMatchers
include PuppetSpec::Files
+
before do
Puppet::Util::Storage.stubs(:store)
end
it "should set its host name to the node_name_value" do
Puppet[:node_name_value] = 'mynode'
Puppet::Transaction::Report.new("apply").host.should == "mynode"
end
it "should return its host name as its name" do
r = Puppet::Transaction::Report.new("apply")
r.name.should == r.host
end
it "should create an initialization timestamp" do
Time.expects(:now).returns "mytime"
Puppet::Transaction::Report.new("apply").time.should == "mytime"
end
it "should take a 'kind' as an argument" do
Puppet::Transaction::Report.new("inspect").kind.should == "inspect"
end
it "should take a 'configuration_version' as an argument" do
Puppet::Transaction::Report.new("inspect", "some configuration version", "some environment").configuration_version.should == "some configuration version"
end
it "should take a 'transaction_uuid' as an argument" do
Puppet::Transaction::Report.new("inspect", "some configuration version", "some environment", "some transaction uuid").transaction_uuid.should == "some transaction uuid"
end
it "should be able to set configuration_version" do
report = Puppet::Transaction::Report.new("inspect")
report.configuration_version = "some version"
report.configuration_version.should == "some version"
end
it "should be able to set transaction_uuid" do
report = Puppet::Transaction::Report.new("inspect")
report.transaction_uuid = "some transaction uuid"
report.transaction_uuid.should == "some transaction uuid"
end
it "should take 'environment' as an argument" do
Puppet::Transaction::Report.new("inspect", "some configuration version", "some environment").environment.should == "some environment"
end
it "should be able to set environment" do
report = Puppet::Transaction::Report.new("inspect")
report.environment = "some environment"
report.environment.should == "some environment"
end
it "should not include whits" do
Puppet::FileBucket::File.indirection.stubs(:save)
filename = tmpfile('whit_test')
file = Puppet::Type.type(:file).new(:path => filename)
catalog = Puppet::Resource::Catalog.new
catalog.add_resource(file)
report = Puppet::Transaction::Report.new("apply")
catalog.apply(:report => report)
report.finalize_report
report.resource_statuses.values.any? {|res| res.resource_type =~ /whit/i}.should be_false
report.metrics['time'].values.any? {|metric| metric.first =~ /whit/i}.should be_false
end
describe "when accepting logs" do
before do
@report = Puppet::Transaction::Report.new("apply")
end
it "should add new logs to the log list" do
@report << "log"
@report.logs[-1].should == "log"
end
it "should return self" do
r = @report << "log"
r.should equal(@report)
end
end
describe "#as_logging_destination" do
it "makes the report collect logs during the block " do
log_string = 'Hello test report!'
report = Puppet::Transaction::Report.new('test')
report.as_logging_destination do
Puppet.err(log_string)
end
expect(report.logs.collect(&:message)).to include(log_string)
end
end
describe "when accepting resource statuses" do
before do
@report = Puppet::Transaction::Report.new("apply")
end
it "should add each status to its status list" do
status = stub 'status', :resource => "foo"
@report.add_resource_status status
@report.resource_statuses["foo"].should equal(status)
end
end
describe "when using the indirector" do
it "should redirect :save to the indirection" do
Facter.stubs(:value).returns("eh")
@indirection = stub 'indirection', :name => :report
Puppet::Transaction::Report.stubs(:indirection).returns(@indirection)
report = Puppet::Transaction::Report.new("apply")
@indirection.expects(:save)
Puppet::Transaction::Report.indirection.save(report)
end
it "should default to the 'processor' terminus" do
Puppet::Transaction::Report.indirection.terminus_class.should == :processor
end
it "should delegate its name attribute to its host method" do
report = Puppet::Transaction::Report.new("apply")
report.expects(:host).returns "me"
report.name.should == "me"
end
end
describe "when computing exit status" do
it "should produce 2 if changes are present" do
report = Puppet::Transaction::Report.new("apply")
report.add_metric("changes", {"total" => 1})
report.add_metric("resources", {"failed" => 0})
report.exit_status.should == 2
end
it "should produce 4 if failures are present" do
report = Puppet::Transaction::Report.new("apply")
report.add_metric("changes", {"total" => 0})
report.add_metric("resources", {"failed" => 1})
report.exit_status.should == 4
end
it "should produce 4 if failures to restart are present" do
report = Puppet::Transaction::Report.new("apply")
report.add_metric("changes", {"total" => 0})
report.add_metric("resources", {"failed" => 0})
report.add_metric("resources", {"failed_to_restart" => 1})
report.exit_status.should == 4
end
it "should produce 6 if both changes and failures are present" do
report = Puppet::Transaction::Report.new("apply")
report.add_metric("changes", {"total" => 1})
report.add_metric("resources", {"failed" => 1})
report.exit_status.should == 6
end
end
describe "before finalizing the report" do
it "should have a status of 'failed'" do
report = Puppet::Transaction::Report.new("apply")
report.status.should == 'failed'
end
end
describe "when finalizing the report" do
before do
@report = Puppet::Transaction::Report.new("apply")
end
def metric(name, value)
if metric = @report.metrics[name.to_s]
metric[value]
else
nil
end
end
def add_statuses(count, type = :file)
count.times do |i|
status = Puppet::Resource::Status.new(Puppet::Type.type(type).new(:title => make_absolute("/my/path#{i}")))
yield status if block_given?
@report.add_resource_status status
end
end
[:time, :resources, :changes, :events].each do |type|
it "should add #{type} metrics" do
@report.finalize_report
@report.metrics[type.to_s].should be_instance_of(Puppet::Transaction::Metric)
end
end
describe "for resources" do
it "should provide the total number of resources" do
add_statuses(3)
@report.finalize_report
metric(:resources, "total").should == 3
end
Puppet::Resource::Status::STATES.each do |state|
it "should provide the number of #{state} resources as determined by the status objects" do
add_statuses(3) { |status| status.send(state.to_s + "=", true) }
@report.finalize_report
metric(:resources, state.to_s).should == 3
end
it "should provide 0 for states not in status" do
@report.finalize_report
metric(:resources, state.to_s).should == 0
end
end
it "should mark the report as 'failed' if there are failing resources" do
add_statuses(1) { |status| status.failed = true }
@report.finalize_report
@report.status.should == 'failed'
end
end
describe "for changes" do
it "should provide the number of changes from the resource statuses and mark the report as 'changed'" do
add_statuses(3) { |status| 3.times { status << Puppet::Transaction::Event.new(:status => 'success') } }
@report.finalize_report
metric(:changes, "total").should == 9
@report.status.should == 'changed'
end
it "should provide a total even if there are no changes, and mark the report as 'unchanged'" do
@report.finalize_report
metric(:changes, "total").should == 0
@report.status.should == 'unchanged'
end
end
describe "for times" do
it "should provide the total amount of time for each resource type" do
add_statuses(3, :file) do |status|
status.evaluation_time = 1
end
add_statuses(3, :exec) do |status|
status.evaluation_time = 2
end
add_statuses(3, :tidy) do |status|
status.evaluation_time = 3
end
@report.finalize_report
metric(:time, "file").should == 3
metric(:time, "exec").should == 6
metric(:time, "tidy").should == 9
end
it "should add any provided times from external sources" do
@report.add_times :foobar, 50
@report.finalize_report
metric(:time, "foobar").should == 50
end
it "should have a total time" do
add_statuses(3, :file) do |status|
status.evaluation_time = 1.25
end
@report.add_times :config_retrieval, 0.5
@report.finalize_report
metric(:time, "total").should == 4.25
end
end
describe "for events" do
it "should provide the total number of events" do
add_statuses(3) do |status|
3.times { |i| status.add_event(Puppet::Transaction::Event.new :status => 'success') }
end
@report.finalize_report
metric(:events, "total").should == 9
end
it "should provide the total even if there are no events" do
@report.finalize_report
metric(:events, "total").should == 0
end
Puppet::Transaction::Event::EVENT_STATUSES.each do |status_name|
it "should provide the number of #{status_name} events" do
add_statuses(3) do |status|
3.times do |i|
event = Puppet::Transaction::Event.new
event.status = status_name
status.add_event(event)
end
end
@report.finalize_report
metric(:events, status_name).should == 9
end
end
end
end
describe "when producing a summary" do
before do
resource = Puppet::Type.type(:notify).new(:name => "testing")
catalog = Puppet::Resource::Catalog.new
catalog.add_resource resource
catalog.version = 1234567
trans = catalog.apply
@report = trans.report
@report.finalize_report
end
%w{changes time resources events version}.each do |main|
it "should include the key #{main} in the raw summary hash" do
@report.raw_summary.should be_key main
end
end
it "should include the last run time in the raw summary hash" do
Time.stubs(:now).returns(Time.utc(2010,11,10,12,0,24))
@report.raw_summary["time"]["last_run"].should == 1289390424
end
it "should include all resource statuses" do
resources_report = @report.raw_summary["resources"]
Puppet::Resource::Status::STATES.each do |state|
resources_report.should be_include(state.to_s)
end
end
%w{total failure success}.each do |r|
it "should include event #{r}" do
events_report = @report.raw_summary["events"]
events_report.should be_include(r)
end
end
it "should include config version" do
@report.raw_summary["version"]["config"].should == 1234567
end
it "should include puppet version" do
@report.raw_summary["version"]["puppet"].should == Puppet.version
end
%w{Changes Total Resources Time Events}.each do |main|
it "should include information on #{main} in the textual summary" do
@report.summary.should be_include(main)
end
end
end
describe "when outputting yaml" do
it "should not include @external_times" do
report = Puppet::Transaction::Report.new('apply')
report.add_times('config_retrieval', 1.0)
report.to_yaml_properties.should_not include('@external_times')
end
end
it "defaults to serializing to pson" do
expect(Puppet::Transaction::Report.default_format).to eq(:pson)
end
it "supports both yaml and pson" do
expect(Puppet::Transaction::Report.supported_formats).to eq([:pson, :yaml])
end
it "can make a round trip through pson" do
Puppet[:report_serialization_format] = "pson"
report = generate_report
tripped = Puppet::Transaction::Report.convert_from(:pson, report.render)
expect_equivalent_reports(tripped, report)
end
- it "generates pson which validates against the report schema", :unless => Puppet.features.microsoft_windows? do
+ it "generates pson which validates against the report schema" do
Puppet[:report_serialization_format] = "pson"
report = generate_report
- JSON::Validator.validate!(REPORT_SCHEMA, report.render)
+ expect(report.render).to validate_against('api/schemas/report.json')
+ end
+
+ it "generates pson for error report which validates against the report schema" do
+ Puppet[:report_serialization_format] = "pson"
+ error_report = generate_report_with_error
+ expect(error_report.render).to validate_against('api/schemas/report.json')
end
it "can make a round trip through yaml" do
Puppet[:report_serialization_format] = "yaml"
report = generate_report
yaml_output = report.render
tripped = Puppet::Transaction::Report.convert_from(:yaml, yaml_output)
yaml_output.should =~ /^--- /
expect_equivalent_reports(tripped, report)
end
def expect_equivalent_reports(tripped, report)
tripped.host.should == report.host
tripped.time.to_i.should == report.time.to_i
tripped.configuration_version.should == report.configuration_version
tripped.transaction_uuid.should == report.transaction_uuid
tripped.report_format.should == report.report_format
tripped.puppet_version.should == report.puppet_version
tripped.kind.should == report.kind
tripped.status.should == report.status
tripped.environment.should == report.environment
logs_as_strings(tripped).should == logs_as_strings(report)
metrics_as_hashes(tripped).should == metrics_as_hashes(report)
expect_equivalent_resource_statuses(tripped.resource_statuses, report.resource_statuses)
end
def logs_as_strings(report)
report.logs.map(&:to_report)
end
def metrics_as_hashes(report)
Hash[*report.metrics.collect do |name, m|
[name, { :name => m.name, :label => m.label, :value => m.value }]
end.flatten]
end
def expect_equivalent_resource_statuses(tripped, report)
tripped.keys.sort.should == report.keys.sort
tripped.each_pair do |name, status|
expected = report[name]
status.title.should == expected.title
status.file.should == expected.file
status.line.should == expected.line
status.resource.should == expected.resource
status.resource_type.should == expected.resource_type
status.containment_path.should == expected.containment_path
status.evaluation_time.should == expected.evaluation_time
status.tags.should == expected.tags
status.time.to_i.should == expected.time.to_i
status.failed.should == expected.failed
status.changed.should == expected.changed
status.out_of_sync.should == expected.out_of_sync
status.skipped.should == expected.skipped
status.change_count.should == expected.change_count
status.out_of_sync_count.should == expected.out_of_sync_count
status.events.should == expected.events
end
end
def generate_report
status = Puppet::Resource::Status.new(Puppet::Type.type(:notify).new(:title => "a resource"))
status.changed = true
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)
report.finalize_report
report
end
+ def generate_report_with_error
+ status = Puppet::Resource::Status.new(Puppet::Type.type(:notify).new(:title => "a resource"))
+ status.changed = true
+ status.failed_because("bad stuff happened")
+
+ 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)
+ report.finalize_report
+ report
+ end
+
end
diff --git a/spec/unit/transaction/resource_harness_spec.rb b/spec/unit/transaction/resource_harness_spec.rb
index a487c8199..5eeaf0ba4 100755
--- a/spec/unit/transaction/resource_harness_spec.rb
+++ b/spec/unit/transaction/resource_harness_spec.rb
@@ -1,478 +1,478 @@
#! /usr/bin/env ruby
require 'spec_helper'
require 'puppet/transaction/resource_harness'
describe Puppet::Transaction::ResourceHarness do
include PuppetSpec::Files
before do
@mode_750 = Puppet.features.microsoft_windows? ? '644' : '750'
@mode_755 = Puppet.features.microsoft_windows? ? '644' : '755'
path = make_absolute("/my/file")
@transaction = Puppet::Transaction.new(Puppet::Resource::Catalog.new, nil, nil)
@resource = Puppet::Type.type(:file).new :path => path
@harness = Puppet::Transaction::ResourceHarness.new(@transaction)
@current_state = Puppet::Resource.new(:file, path)
@resource.stubs(:retrieve).returns @current_state
end
it "should accept a transaction at initialization" do
harness = Puppet::Transaction::ResourceHarness.new(@transaction)
harness.transaction.should equal(@transaction)
end
it "should delegate to the transaction for its relationship graph" do
@transaction.expects(:relationship_graph).returns "relgraph"
Puppet::Transaction::ResourceHarness.new(@transaction).relationship_graph.should == "relgraph"
end
describe "when evaluating a resource" do
it "produces a resource state that describes what happened with the resource" do
status = @harness.evaluate(@resource)
status.resource.should == @resource.ref
status.should_not be_failed
status.events.should be_empty
end
it "retrieves the current state of the resource" do
@resource.expects(:retrieve).returns @current_state
@harness.evaluate(@resource)
end
it "produces a failure status for the resource when an error occurs" do
the_message = "retrieve failed in testing"
@resource.expects(:retrieve).raises(ArgumentError.new(the_message))
status = @harness.evaluate(@resource)
status.should be_failed
events_to_hash(status.events).collect do |event|
{ :@status => event[:@status], :@message => event[:@message] }
end.should == [{ :@status => "failure", :@message => the_message }]
end
it "records the time it took to evaluate the resource" do
before = Time.now
status = @harness.evaluate(@resource)
after = Time.now
status.evaluation_time.should be <= after - before
end
end
def events_to_hash(events)
events.map do |event|
hash = {}
event.instance_variables.each do |varname|
hash[varname.to_sym] = event.instance_variable_get(varname)
end
hash
end
end
def make_stub_provider
stubProvider = Class.new(Puppet::Type)
stubProvider.instance_eval do
initvars
newparam(:name) do
desc "The name var"
isnamevar
end
newproperty(:foo) do
desc "A property that can be changed successfully"
def sync
end
def retrieve
:absent
end
def insync?(reference_value)
false
end
end
newproperty(:bar) do
desc "A property that raises an exception when you try to change it"
def sync
raise ZeroDivisionError.new('bar')
end
def retrieve
:absent
end
def insync?(reference_value)
false
end
end
newproperty(:baz) do
desc "A property that raises an Exception (not StandardError) when you try to change it"
def sync
raise Exception.new('baz')
end
def retrieve
:absent
end
def insync?(reference_value)
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
context "interaction of ensure with other properties" do
def an_ensurable_resource_reacting_as(behaviors)
stub_type = Class.new(Puppet::Type)
stub_type.class_eval do
initvars
ensurable do
def sync
(@resource.behaviors[:on_ensure] || proc {}).call
end
def insync?(value)
@resource.behaviors[:ensure_insync?]
end
end
newparam(:name) do
desc "The name var"
isnamevar
end
newproperty(:prop) do
newvalue("new") do
#noop
end
def retrieve
"old"
end
end
attr_reader :behaviors
def initialize(options)
@behaviors = options.delete(:behaviors)
super
end
def exists?
@behaviors[:present?]
end
def present?(resource)
@behaviors[:present?]
end
def self.name
"Testing"
end
end
stub_type.new(:behaviors => behaviors,
:ensure => :present,
:name => "testing",
:prop => "new")
end
it "ensure errors means that the rest doesn't happen" do
resource = an_ensurable_resource_reacting_as(:ensure_insync? => false, :on_ensure => proc { raise StandardError }, :present? => true)
status = @harness.evaluate(resource)
expect(status.events.length).to eq(1)
expect(status.events[0].property).to eq('ensure')
expect(status.events[0].name.to_s).to eq('Testing_created')
expect(status.events[0].status).to eq('failure')
end
it "ensure fails completely means that the rest doesn't happen" do
resource = an_ensurable_resource_reacting_as(:ensure_insync? => false, :on_ensure => proc { raise Exception }, :present? => false)
expect do
@harness.evaluate(resource)
end.to raise_error(Exception)
@logs.first.message.should == "change from absent to present failed: Exception"
@logs.first.level.should == :err
end
it "ensure succeeds means that the rest doesn't happen" do
resource = an_ensurable_resource_reacting_as(:ensure_insync? => false, :on_ensure => proc { }, :present? => true)
status = @harness.evaluate(resource)
expect(status.events.length).to eq(1)
expect(status.events[0].property).to eq('ensure')
expect(status.events[0].name.to_s).to eq('Testing_created')
expect(status.events[0].status).to eq('success')
end
it "ensure is in sync means that the rest *does* happen" do
resource = an_ensurable_resource_reacting_as(:ensure_insync? => true, :present? => true)
status = @harness.evaluate(resource)
expect(status.events.length).to eq(1)
expect(status.events[0].property).to eq('prop')
expect(status.events[0].name.to_s).to eq('prop_changed')
expect(status.events[0].status).to eq('success')
end
it "ensure is in sync but resource not present, means that the rest doesn't happen" do
resource = an_ensurable_resource_reacting_as(:ensure_insync? => true, :present? => false)
status = @harness.evaluate(resource)
expect(status.events).to be_empty
end
end
describe "when a caught error occurs" do
before :each do
stub_provider = make_stub_provider
resource = stub_provider.new :name => 'name', :foo => 1, :bar => 2
resource.expects(:err).never
@status = @harness.evaluate(resource)
end
it "should record previous successful events" do
@status.events[0].property.should == 'foo'
@status.events[0].status.should == 'success'
end
it "should record a failure event" do
@status.events[1].property.should == 'bar'
@status.events[1].status.should == 'failure'
end
end
describe "when an Exception occurs during sync" do
before :each do
stub_provider = make_stub_provider
@resource = stub_provider.new :name => 'name', :baz => 1
@resource.expects(:err).never
end
it "should log and pass the exception through" do
lambda { @harness.evaluate(@resource) }.should raise_error(Exception, /baz/)
@logs.first.message.should == "change from absent to 1 failed: baz"
@logs.first.level.should == :err
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
resource = stub_provider.new :name => 'name', :audit => ['foo']
resource.property(:foo).expects(:insync?).never
status = @harness.evaluate(resource)
expect(status.events).to be_empty
end
it "should be able to audit a file's group" do # see bug #5710
test_file = tmpfile('foo')
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
end
end
describe "when applying changes" do
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)
- Puppet::FileSystem::File.exist?(test_file).should == false
+ Puppet::FileSystem.exist?(test_file).should == false
end
end
describe "when determining whether the resource can be changed" do
before do
@resource.stubs(:purging?).returns true
@resource.stubs(:deleting?).returns true
end
it "should be true if the resource is not being purged" do
@resource.expects(:purging?).returns false
@harness.should be_allow_changes(@resource)
end
it "should be true if the resource is not being deleted" do
@resource.expects(:deleting?).returns false
@harness.should be_allow_changes(@resource)
end
it "should be true if the resource has no dependents" do
@harness.relationship_graph.expects(:dependents).with(@resource).returns []
@harness.should be_allow_changes(@resource)
end
it "should be true if all dependents are being deleted" do
dep = stub 'dependent', :deleting? => true
@harness.relationship_graph.expects(:dependents).with(@resource).returns [dep]
@resource.expects(:purging?).returns true
@harness.should be_allow_changes(@resource)
end
it "should be false if the resource's dependents are not being deleted" do
dep = stub 'dependent', :deleting? => false, :ref => "myres"
@resource.expects(:warning)
@harness.relationship_graph.expects(:dependents).with(@resource).returns [dep]
@harness.should_not be_allow_changes(@resource)
end
end
describe "when finding the schedule" do
before do
@catalog = Puppet::Resource::Catalog.new
@resource.catalog = @catalog
end
it "should warn and return nil if the resource has no catalog" do
@resource.catalog = nil
@resource.expects(:warning)
@harness.schedule(@resource).should be_nil
end
it "should return nil if the resource specifies no schedule" do
@harness.schedule(@resource).should be_nil
end
it "should fail if the named schedule cannot be found" do
@resource[:schedule] = "whatever"
@resource.expects(:fail)
@harness.schedule(@resource)
end
it "should return the named schedule if it exists" do
sched = Puppet::Type.type(:schedule).new(:name => "sched")
@catalog.add_resource(sched)
@resource[:schedule] = "sched"
@harness.schedule(@resource).to_s.should == sched.to_s
end
end
describe "when determining if a resource is scheduled" do
before do
@catalog = Puppet::Resource::Catalog.new
@resource.catalog = @catalog
end
it "should return true if 'ignoreschedules' is set" do
Puppet[:ignoreschedules] = true
@resource[:schedule] = "meh"
@harness.should be_scheduled(@resource)
end
it "should return true if the resource has no schedule set" do
@harness.should be_scheduled(@resource)
end
it "should return the result of matching the schedule with the cached 'checked' time if a schedule is set" do
t = Time.now
@harness.expects(:cached).with(@resource, :checked).returns(t)
sched = Puppet::Type.type(:schedule).new(:name => "sched")
@catalog.add_resource(sched)
@resource[:schedule] = "sched"
sched.expects(:match?).with(t.to_i).returns "feh"
@harness.scheduled?(@resource).should == "feh"
end
end
it "should be able to cache data in the Storage module" do
data = {}
Puppet::Util::Storage.expects(:cache).with(@resource).returns data
@harness.cache(@resource, :foo, "something")
data[:foo].should == "something"
end
it "should be able to retrieve data from the cache" do
data = {:foo => "other"}
Puppet::Util::Storage.expects(:cache).with(@resource).returns data
@harness.cached(@resource, :foo).should == "other"
end
end
diff --git a/spec/unit/type/cron_spec.rb b/spec/unit/type/cron_spec.rb
index 3d08ba203..e7c0db625 100755
--- a/spec/unit/type/cron_spec.rb
+++ b/spec/unit/type/cron_spec.rb
@@ -1,506 +1,544 @@
#! /usr/bin/env ruby
require 'spec_helper'
describe Puppet::Type.type(:cron), :unless => Puppet.features.microsoft_windows? do
- before do
+ before :all do
@provider_class = described_class.provide(:simple) { mk_resource_methods }
@provider_class.stubs(:suitable?).returns true
described_class.stubs(:defaultprovider).returns @provider_class
end
+ after :all do
+ described_class.unprovide(:simple)
+ end
+
it "should have :name be its namevar" do
described_class.key_attributes.should == [:name]
end
describe "when validating attributes" do
[:name, :provider].each do |param|
it "should have a #{param} parameter" do
described_class.attrtype(param).should == :param
end
end
[:command, :special, :minute, :hour, :weekday, :month, :monthday, :environment, :user, :target].each do |property|
it "should have a #{property} property" do
described_class.attrtype(property).should == :property
end
end
[:command, :minute, :hour, :weekday, :month, :monthday].each do |cronparam|
it "should have #{cronparam} of type CronParam" do
described_class.attrclass(cronparam).ancestors.should include CronParam
end
end
end
describe "when validating values" do
describe "ensure" do
it "should support present as a value for ensure" do
expect { described_class.new(:name => 'foo', :ensure => :present) }.to_not raise_error
end
it "should support absent as a value for ensure" do
expect { described_class.new(:name => 'foo', :ensure => :present) }.to_not raise_error
end
it "should not support other values" do
expect { described_class.new(:name => 'foo', :ensure => :foo) }.to raise_error(Puppet::Error, /Invalid value/)
end
end
describe "command" do
it "should discard leading spaces" do
described_class.new(:name => 'foo', :command => " /bin/true")[:command].should_not match Regexp.new(" ")
end
it "should discard trailing spaces" do
described_class.new(:name => 'foo', :command => "/bin/true ")[:command].should_not match Regexp.new(" ")
end
end
describe "minute" do
it "should support absent" do
expect { described_class.new(:name => 'foo', :minute => 'absent') }.to_not raise_error
end
it "should support *" do
expect { described_class.new(:name => 'foo', :minute => '*') }.to_not raise_error
end
it "should translate absent to :absent" do
described_class.new(:name => 'foo', :minute => 'absent')[:minute].should == :absent
end
it "should translate * to :absent" do
described_class.new(:name => 'foo', :minute => '*')[:minute].should == :absent
end
it "should support valid single values" do
expect { described_class.new(:name => 'foo', :minute => '0') }.to_not raise_error
expect { described_class.new(:name => 'foo', :minute => '1') }.to_not raise_error
expect { described_class.new(:name => 'foo', :minute => '59') }.to_not raise_error
end
it "should not support non numeric characters" do
expect { described_class.new(:name => 'foo', :minute => 'z59') }.to raise_error(Puppet::Error, /z59 is not a valid minute/)
expect { described_class.new(:name => 'foo', :minute => '5z9') }.to raise_error(Puppet::Error, /5z9 is not a valid minute/)
expect { described_class.new(:name => 'foo', :minute => '59z') }.to raise_error(Puppet::Error, /59z is not a valid minute/)
end
it "should not support single values out of range" do
expect { described_class.new(:name => 'foo', :minute => '-1') }.to raise_error(Puppet::Error, /-1 is not a valid minute/)
expect { described_class.new(:name => 'foo', :minute => '60') }.to raise_error(Puppet::Error, /60 is not a valid minute/)
expect { described_class.new(:name => 'foo', :minute => '61') }.to raise_error(Puppet::Error, /61 is not a valid minute/)
expect { described_class.new(:name => 'foo', :minute => '120') }.to raise_error(Puppet::Error, /120 is not a valid minute/)
end
it "should support valid multiple values" do
expect { described_class.new(:name => 'foo', :minute => ['0','1','59'] ) }.to_not raise_error
expect { described_class.new(:name => 'foo', :minute => ['40','30','20'] ) }.to_not raise_error
expect { described_class.new(:name => 'foo', :minute => ['10','30','20'] ) }.to_not raise_error
end
it "should not support multiple values if at least one is invalid" do
# one invalid
expect { described_class.new(:name => 'foo', :minute => ['0','1','60'] ) }.to raise_error(Puppet::Error, /60 is not a valid minute/)
expect { described_class.new(:name => 'foo', :minute => ['0','120','59'] ) }.to raise_error(Puppet::Error, /120 is not a valid minute/)
expect { described_class.new(:name => 'foo', :minute => ['-1','1','59'] ) }.to raise_error(Puppet::Error, /-1 is not a valid minute/)
# two invalid
expect { described_class.new(:name => 'foo', :minute => ['0','61','62'] ) }.to raise_error(Puppet::Error, /(61|62) is not a valid minute/)
# all invalid
expect { described_class.new(:name => 'foo', :minute => ['-1','61','62'] ) }.to raise_error(Puppet::Error, /(-1|61|62) is not a valid minute/)
end
it "should support valid step syntax" do
expect { described_class.new(:name => 'foo', :minute => '*/2' ) }.to_not raise_error
expect { described_class.new(:name => 'foo', :minute => '10-16/2' ) }.to_not raise_error
end
it "should not support invalid steps" do
expect { described_class.new(:name => 'foo', :minute => '*/A' ) }.to raise_error(Puppet::Error, /\*\/A is not a valid minute/)
expect { described_class.new(:name => 'foo', :minute => '*/2A' ) }.to raise_error(Puppet::Error, /\*\/2A is not a valid minute/)
# As it turns out cron does not complaining about steps that exceed the valid range
# expect { described_class.new(:name => 'foo', :minute => '*/120' ) }.to raise_error(Puppet::Error, /is not a valid minute/)
end
end
describe "hour" do
it "should support absent" do
expect { described_class.new(:name => 'foo', :hour => 'absent') }.to_not raise_error
end
it "should support *" do
expect { described_class.new(:name => 'foo', :hour => '*') }.to_not raise_error
end
it "should translate absent to :absent" do
described_class.new(:name => 'foo', :hour => 'absent')[:hour].should == :absent
end
it "should translate * to :absent" do
described_class.new(:name => 'foo', :hour => '*')[:hour].should == :absent
end
it "should support valid single values" do
expect { described_class.new(:name => 'foo', :hour => '0') }.to_not raise_error
expect { described_class.new(:name => 'foo', :hour => '11') }.to_not raise_error
expect { described_class.new(:name => 'foo', :hour => '12') }.to_not raise_error
expect { described_class.new(:name => 'foo', :hour => '13') }.to_not raise_error
expect { described_class.new(:name => 'foo', :hour => '23') }.to_not raise_error
end
it "should not support non numeric characters" do
expect { described_class.new(:name => 'foo', :hour => 'z15') }.to raise_error(Puppet::Error, /z15 is not a valid hour/)
expect { described_class.new(:name => 'foo', :hour => '1z5') }.to raise_error(Puppet::Error, /1z5 is not a valid hour/)
expect { described_class.new(:name => 'foo', :hour => '15z') }.to raise_error(Puppet::Error, /15z is not a valid hour/)
end
it "should not support single values out of range" do
expect { described_class.new(:name => 'foo', :hour => '-1') }.to raise_error(Puppet::Error, /-1 is not a valid hour/)
expect { described_class.new(:name => 'foo', :hour => '24') }.to raise_error(Puppet::Error, /24 is not a valid hour/)
expect { described_class.new(:name => 'foo', :hour => '120') }.to raise_error(Puppet::Error, /120 is not a valid hour/)
end
it "should support valid multiple values" do
expect { described_class.new(:name => 'foo', :hour => ['0','1','23'] ) }.to_not raise_error
expect { described_class.new(:name => 'foo', :hour => ['5','16','14'] ) }.to_not raise_error
expect { described_class.new(:name => 'foo', :hour => ['16','13','9'] ) }.to_not raise_error
end
it "should not support multiple values if at least one is invalid" do
# one invalid
expect { described_class.new(:name => 'foo', :hour => ['0','1','24'] ) }.to raise_error(Puppet::Error, /24 is not a valid hour/)
expect { described_class.new(:name => 'foo', :hour => ['0','-1','5'] ) }.to raise_error(Puppet::Error, /-1 is not a valid hour/)
expect { described_class.new(:name => 'foo', :hour => ['-1','1','23'] ) }.to raise_error(Puppet::Error, /-1 is not a valid hour/)
# two invalid
expect { described_class.new(:name => 'foo', :hour => ['0','25','26'] ) }.to raise_error(Puppet::Error, /(25|26) is not a valid hour/)
# all invalid
expect { described_class.new(:name => 'foo', :hour => ['-1','24','120'] ) }.to raise_error(Puppet::Error, /(-1|24|120) is not a valid hour/)
end
it "should support valid step syntax" do
expect { described_class.new(:name => 'foo', :hour => '*/2' ) }.to_not raise_error
expect { described_class.new(:name => 'foo', :hour => '10-18/4' ) }.to_not raise_error
end
it "should not support invalid steps" do
expect { described_class.new(:name => 'foo', :hour => '*/A' ) }.to raise_error(Puppet::Error, /\*\/A is not a valid hour/)
expect { described_class.new(:name => 'foo', :hour => '*/2A' ) }.to raise_error(Puppet::Error, /\*\/2A is not a valid hour/)
# As it turns out cron does not complaining about steps that exceed the valid range
# expect { described_class.new(:name => 'foo', :hour => '*/26' ) }.to raise_error(Puppet::Error, /is not a valid hour/)
end
end
describe "weekday" do
it "should support absent" do
expect { described_class.new(:name => 'foo', :weekday => 'absent') }.to_not raise_error
end
it "should support *" do
expect { described_class.new(:name => 'foo', :weekday => '*') }.to_not raise_error
end
it "should translate absent to :absent" do
described_class.new(:name => 'foo', :weekday => 'absent')[:weekday].should == :absent
end
it "should translate * to :absent" do
described_class.new(:name => 'foo', :weekday => '*')[:weekday].should == :absent
end
it "should support valid numeric weekdays" do
expect { described_class.new(:name => 'foo', :weekday => '0') }.to_not raise_error
expect { described_class.new(:name => 'foo', :weekday => '1') }.to_not raise_error
expect { described_class.new(:name => 'foo', :weekday => '6') }.to_not raise_error
# According to http://www.manpagez.com/man/5/crontab 7 is also valid (Sunday)
expect { described_class.new(:name => 'foo', :weekday => '7') }.to_not raise_error
end
it "should support valid weekdays as words (long version)" do
expect { described_class.new(:name => 'foo', :weekday => 'Monday') }.to_not raise_error
expect { described_class.new(:name => 'foo', :weekday => 'Tuesday') }.to_not raise_error
expect { described_class.new(:name => 'foo', :weekday => 'Wednesday') }.to_not raise_error
expect { described_class.new(:name => 'foo', :weekday => 'Thursday') }.to_not raise_error
expect { described_class.new(:name => 'foo', :weekday => 'Friday') }.to_not raise_error
expect { described_class.new(:name => 'foo', :weekday => 'Saturday') }.to_not raise_error
expect { described_class.new(:name => 'foo', :weekday => 'Sunday') }.to_not raise_error
end
it "should support valid weekdays as words (3 character version)" do
expect { described_class.new(:name => 'foo', :weekday => 'Mon') }.to_not raise_error
expect { described_class.new(:name => 'foo', :weekday => 'Tue') }.to_not raise_error
expect { described_class.new(:name => 'foo', :weekday => 'Wed') }.to_not raise_error
expect { described_class.new(:name => 'foo', :weekday => 'Thu') }.to_not raise_error
expect { described_class.new(:name => 'foo', :weekday => 'Fri') }.to_not raise_error
expect { described_class.new(:name => 'foo', :weekday => 'Sat') }.to_not raise_error
expect { described_class.new(:name => 'foo', :weekday => 'Sun') }.to_not raise_error
end
it "should not support numeric values out of range" do
expect { described_class.new(:name => 'foo', :weekday => '-1') }.to raise_error(Puppet::Error, /-1 is not a valid weekday/)
expect { described_class.new(:name => 'foo', :weekday => '8') }.to raise_error(Puppet::Error, /8 is not a valid weekday/)
end
it "should not support invalid weekday names" do
expect { described_class.new(:name => 'foo', :weekday => 'Sar') }.to raise_error(Puppet::Error, /Sar is not a valid weekday/)
end
it "should support valid multiple values" do
expect { described_class.new(:name => 'foo', :weekday => ['0','1','6'] ) }.to_not raise_error
expect { described_class.new(:name => 'foo', :weekday => ['Mon','Wed','Friday'] ) }.to_not raise_error
end
it "should not support multiple values if at least one is invalid" do
# one invalid
expect { described_class.new(:name => 'foo', :weekday => ['0','1','8'] ) }.to raise_error(Puppet::Error, /8 is not a valid weekday/)
expect { described_class.new(:name => 'foo', :weekday => ['Mon','Fii','Sat'] ) }.to raise_error(Puppet::Error, /Fii is not a valid weekday/)
# two invalid
expect { described_class.new(:name => 'foo', :weekday => ['Mos','Fii','Sat'] ) }.to raise_error(Puppet::Error, /(Mos|Fii) is not a valid weekday/)
# all invalid
expect { described_class.new(:name => 'foo', :weekday => ['Mos','Fii','Saa'] ) }.to raise_error(Puppet::Error, /(Mos|Fii|Saa) is not a valid weekday/)
expect { described_class.new(:name => 'foo', :weekday => ['-1','8','11'] ) }.to raise_error(Puppet::Error, /(-1|8|11) is not a valid weekday/)
end
it "should support valid step syntax" do
expect { described_class.new(:name => 'foo', :weekday => '*/2' ) }.to_not raise_error
expect { described_class.new(:name => 'foo', :weekday => '0-4/2' ) }.to_not raise_error
end
it "should not support invalid steps" do
expect { described_class.new(:name => 'foo', :weekday => '*/A' ) }.to raise_error(Puppet::Error, /\*\/A is not a valid weekday/)
expect { described_class.new(:name => 'foo', :weekday => '*/2A' ) }.to raise_error(Puppet::Error, /\*\/2A is not a valid weekday/)
# As it turns out cron does not complaining about steps that exceed the valid range
# expect { described_class.new(:name => 'foo', :weekday => '*/9' ) }.to raise_error(Puppet::Error, /is not a valid weekday/)
end
end
describe "month" do
it "should support absent" do
expect { described_class.new(:name => 'foo', :month => 'absent') }.to_not raise_error
end
it "should support *" do
expect { described_class.new(:name => 'foo', :month => '*') }.to_not raise_error
end
it "should translate absent to :absent" do
described_class.new(:name => 'foo', :month => 'absent')[:month].should == :absent
end
it "should translate * to :absent" do
described_class.new(:name => 'foo', :month => '*')[:month].should == :absent
end
it "should support valid numeric values" do
expect { described_class.new(:name => 'foo', :month => '1') }.to_not raise_error
expect { described_class.new(:name => 'foo', :month => '12') }.to_not raise_error
end
it "should support valid months as words" do
expect { described_class.new(:name => 'foo', :month => 'January') }.to_not raise_error
expect { described_class.new(:name => 'foo', :month => 'February') }.to_not raise_error
expect { described_class.new(:name => 'foo', :month => 'March') }.to_not raise_error
expect { described_class.new(:name => 'foo', :month => 'April') }.to_not raise_error
expect { described_class.new(:name => 'foo', :month => 'May') }.to_not raise_error
expect { described_class.new(:name => 'foo', :month => 'June') }.to_not raise_error
expect { described_class.new(:name => 'foo', :month => 'July') }.to_not raise_error
expect { described_class.new(:name => 'foo', :month => 'August') }.to_not raise_error
expect { described_class.new(:name => 'foo', :month => 'September') }.to_not raise_error
expect { described_class.new(:name => 'foo', :month => 'October') }.to_not raise_error
expect { described_class.new(:name => 'foo', :month => 'November') }.to_not raise_error
expect { described_class.new(:name => 'foo', :month => 'December') }.to_not raise_error
end
it "should support valid months as words (3 character short version)" do
expect { described_class.new(:name => 'foo', :month => 'Jan') }.to_not raise_error
expect { described_class.new(:name => 'foo', :month => 'Feb') }.to_not raise_error
expect { described_class.new(:name => 'foo', :month => 'Mar') }.to_not raise_error
expect { described_class.new(:name => 'foo', :month => 'Apr') }.to_not raise_error
expect { described_class.new(:name => 'foo', :month => 'May') }.to_not raise_error
expect { described_class.new(:name => 'foo', :month => 'Jun') }.to_not raise_error
expect { described_class.new(:name => 'foo', :month => 'Jul') }.to_not raise_error
expect { described_class.new(:name => 'foo', :month => 'Aug') }.to_not raise_error
expect { described_class.new(:name => 'foo', :month => 'Sep') }.to_not raise_error
expect { described_class.new(:name => 'foo', :month => 'Oct') }.to_not raise_error
expect { described_class.new(:name => 'foo', :month => 'Nov') }.to_not raise_error
expect { described_class.new(:name => 'foo', :month => 'Dec') }.to_not raise_error
end
it "should not support numeric values out of range" do
expect { described_class.new(:name => 'foo', :month => '-1') }.to raise_error(Puppet::Error, /-1 is not a valid month/)
expect { described_class.new(:name => 'foo', :month => '0') }.to raise_error(Puppet::Error, /0 is not a valid month/)
expect { described_class.new(:name => 'foo', :month => '13') }.to raise_error(Puppet::Error, /13 is not a valid month/)
end
it "should not support words that are not valid months" do
expect { described_class.new(:name => 'foo', :month => 'Jal') }.to raise_error(Puppet::Error, /Jal is not a valid month/)
end
it "should not support single values out of range" do
expect { described_class.new(:name => 'foo', :month => '-1') }.to raise_error(Puppet::Error, /-1 is not a valid month/)
expect { described_class.new(:name => 'foo', :month => '60') }.to raise_error(Puppet::Error, /60 is not a valid month/)
expect { described_class.new(:name => 'foo', :month => '61') }.to raise_error(Puppet::Error, /61 is not a valid month/)
expect { described_class.new(:name => 'foo', :month => '120') }.to raise_error(Puppet::Error, /120 is not a valid month/)
end
it "should support valid multiple values" do
expect { described_class.new(:name => 'foo', :month => ['1','9','12'] ) }.to_not raise_error
expect { described_class.new(:name => 'foo', :month => ['Jan','March','Jul'] ) }.to_not raise_error
end
it "should not support multiple values if at least one is invalid" do
# one invalid
expect { described_class.new(:name => 'foo', :month => ['0','1','12'] ) }.to raise_error(Puppet::Error, /0 is not a valid month/)
expect { described_class.new(:name => 'foo', :month => ['1','13','10'] ) }.to raise_error(Puppet::Error, /13 is not a valid month/)
expect { described_class.new(:name => 'foo', :month => ['Jan','Feb','Jxx'] ) }.to raise_error(Puppet::Error, /Jxx is not a valid month/)
# two invalid
expect { described_class.new(:name => 'foo', :month => ['Jan','Fex','Jux'] ) }.to raise_error(Puppet::Error, /(Fex|Jux) is not a valid month/)
# all invalid
expect { described_class.new(:name => 'foo', :month => ['-1','0','13'] ) }.to raise_error(Puppet::Error, /(-1|0|13) is not a valid month/)
expect { described_class.new(:name => 'foo', :month => ['Jax','Fex','Aux'] ) }.to raise_error(Puppet::Error, /(Jax|Fex|Aux) is not a valid month/)
end
it "should support valid step syntax" do
expect { described_class.new(:name => 'foo', :month => '*/2' ) }.to_not raise_error
expect { described_class.new(:name => 'foo', :month => '1-12/3' ) }.to_not raise_error
end
it "should not support invalid steps" do
expect { described_class.new(:name => 'foo', :month => '*/A' ) }.to raise_error(Puppet::Error, /\*\/A is not a valid month/)
expect { described_class.new(:name => 'foo', :month => '*/2A' ) }.to raise_error(Puppet::Error, /\*\/2A is not a valid month/)
# As it turns out cron does not complaining about steps that exceed the valid range
# expect { described_class.new(:name => 'foo', :month => '*/13' ) }.to raise_error(Puppet::Error, /is not a valid month/)
end
end
describe "monthday" do
it "should support absent" do
expect { described_class.new(:name => 'foo', :monthday => 'absent') }.to_not raise_error
end
it "should support *" do
expect { described_class.new(:name => 'foo', :monthday => '*') }.to_not raise_error
end
it "should translate absent to :absent" do
described_class.new(:name => 'foo', :monthday => 'absent')[:monthday].should == :absent
end
it "should translate * to :absent" do
described_class.new(:name => 'foo', :monthday => '*')[:monthday].should == :absent
end
it "should support valid single values" do
expect { described_class.new(:name => 'foo', :monthday => '1') }.to_not raise_error
expect { described_class.new(:name => 'foo', :monthday => '30') }.to_not raise_error
expect { described_class.new(:name => 'foo', :monthday => '31') }.to_not raise_error
end
it "should not support non numeric characters" do
expect { described_class.new(:name => 'foo', :monthday => 'z23') }.to raise_error(Puppet::Error, /z23 is not a valid monthday/)
expect { described_class.new(:name => 'foo', :monthday => '2z3') }.to raise_error(Puppet::Error, /2z3 is not a valid monthday/)
expect { described_class.new(:name => 'foo', :monthday => '23z') }.to raise_error(Puppet::Error, /23z is not a valid monthday/)
end
it "should not support single values out of range" do
expect { described_class.new(:name => 'foo', :monthday => '-1') }.to raise_error(Puppet::Error, /-1 is not a valid monthday/)
expect { described_class.new(:name => 'foo', :monthday => '0') }.to raise_error(Puppet::Error, /0 is not a valid monthday/)
expect { described_class.new(:name => 'foo', :monthday => '32') }.to raise_error(Puppet::Error, /32 is not a valid monthday/)
end
it "should support valid multiple values" do
expect { described_class.new(:name => 'foo', :monthday => ['1','23','31'] ) }.to_not raise_error
expect { described_class.new(:name => 'foo', :monthday => ['31','23','1'] ) }.to_not raise_error
expect { described_class.new(:name => 'foo', :monthday => ['1','31','23'] ) }.to_not raise_error
end
it "should not support multiple values if at least one is invalid" do
# one invalid
expect { described_class.new(:name => 'foo', :monthday => ['1','23','32'] ) }.to raise_error(Puppet::Error, /32 is not a valid monthday/)
expect { described_class.new(:name => 'foo', :monthday => ['-1','12','23'] ) }.to raise_error(Puppet::Error, /-1 is not a valid monthday/)
expect { described_class.new(:name => 'foo', :monthday => ['13','32','30'] ) }.to raise_error(Puppet::Error, /32 is not a valid monthday/)
# two invalid
expect { described_class.new(:name => 'foo', :monthday => ['-1','0','23'] ) }.to raise_error(Puppet::Error, /(-1|0) is not a valid monthday/)
# all invalid
expect { described_class.new(:name => 'foo', :monthday => ['-1','0','32'] ) }.to raise_error(Puppet::Error, /(-1|0|32) is not a valid monthday/)
end
it "should support valid step syntax" do
expect { described_class.new(:name => 'foo', :monthday => '*/2' ) }.to_not raise_error
expect { described_class.new(:name => 'foo', :monthday => '10-16/2' ) }.to_not raise_error
end
it "should not support invalid steps" do
expect { described_class.new(:name => 'foo', :monthday => '*/A' ) }.to raise_error(Puppet::Error, /\*\/A is not a valid monthday/)
expect { described_class.new(:name => 'foo', :monthday => '*/2A' ) }.to raise_error(Puppet::Error, /\*\/2A is not a valid monthday/)
# As it turns out cron does not complaining about steps that exceed the valid range
# expect { described_class.new(:name => 'foo', :monthday => '*/32' ) }.to raise_error(Puppet::Error, /is not a valid monthday/)
end
end
+ describe "special" do
+ %w(reboot yearly annually monthly weekly daily midnight hourly).each do |value|
+ it "should support the value '#{value}'" do
+ expect { described_class.new(:name => 'foo', :special => value ) }.to_not raise_error(Puppet::Error, /cannot specify both a special schedule and a value/)
+ end
+ end
+
+ context "when combined with numeric schedule fields" do
+ context "which are 'absent'" do
+ [ %w(reboot yearly annually monthly weekly daily midnight hourly), :absent ].flatten.each { |value|
+ it "should accept the value '#{value}' for special" do
+ expect {
+ described_class.new(:name => 'foo', :minute => :absent, :special => value )
+ }.to_not raise_error(Puppet::Error, /cannot specify both a special schedule and a value/)
+ end
+ }
+ end
+ context "which are not absent" do
+ %w(reboot yearly annually monthly weekly daily midnight hourly).each { |value|
+ it "should not accept the value '#{value}' for special" do
+ expect {
+ described_class.new(:name => 'foo', :minute => "1", :special => value )
+ }.to raise_error(Puppet::Error, /cannot specify both a special schedule and a value/)
+ end
+ }
+ it "should accept the 'absent' value for special" do
+ expect {
+ described_class.new(:name => 'foo', :minute => "1", :special => :absent )
+ }.to_not raise_error(Puppet::Error, /cannot specify both a special schedule and a value/)
+ end
+ end
+ end
+ end
+
describe "environment" do
it "it should accept an :environment that looks like a path" do
expect do
described_class.new(:name => 'foo',:environment => 'PATH=/bin:/usr/bin:/usr/sbin')
end.to_not raise_error
end
it "should not accept environment variables that do not contain '='" do
expect do
described_class.new(:name => 'foo',:environment => 'INVALID')
end.to raise_error(Puppet::Error, /Invalid environment setting "INVALID"/)
end
it "should accept empty environment variables that do not contain '='" do
expect do
described_class.new(:name => 'foo',:environment => 'MAILTO=')
end.to_not raise_error
end
it "should accept 'absent'" do
expect do
described_class.new(:name => 'foo',:environment => 'absent')
end.to_not raise_error
end
end
end
describe "when autorequiring resources" do
before :each do
@user_bob = Puppet::Type.type(:user).new(:name => 'bob', :ensure => :present)
@user_alice = Puppet::Type.type(:user).new(:name => 'alice', :ensure => :present)
@catalog = Puppet::Resource::Catalog.new
@catalog.add_resource @user_bob, @user_alice
end
it "should autorequire the user" do
@resource = described_class.new(:name => 'dummy', :command => '/usr/bin/uptime', :user => 'alice')
@catalog.add_resource @resource
req = @resource.autorequire
req.size.should == 1
req[0].target.must == @resource
req[0].source.must == @user_alice
end
end
it "should require a command when adding an entry" do
entry = described_class.new(:name => "test_entry", :ensure => :present)
expect { entry.value(:command) }.to raise_error(Puppet::Error, /No command/)
end
it "should not require a command when removing an entry" do
entry = described_class.new(:name => "test_entry", :ensure => :absent)
entry.value(:command).should == nil
end
it "should default to user => root if Etc.getpwuid(Process.uid) returns nil (#12357)" do
Etc.expects(:getpwuid).returns(nil)
entry = described_class.new(:name => "test_entry", :ensure => :present)
entry.value(:user).should eql "root"
end
end
diff --git a/spec/unit/type/file/content_spec.rb b/spec/unit/type/file/content_spec.rb
index 5a73dceb1..91ce83100 100755
--- a/spec/unit/type/file/content_spec.rb
+++ b/spec/unit/type/file/content_spec.rb
@@ -1,464 +1,463 @@
#! /usr/bin/env ruby
require 'spec_helper'
require 'puppet/network/http_pool'
require 'puppet/network/resolver'
content = Puppet::Type.type(:file).attrclass(:content)
describe content do
include PuppetSpec::Files
before do
@filename = tmpfile('testfile')
@catalog = Puppet::Resource::Catalog.new
@resource = Puppet::Type.type(:file).new :path => @filename, :catalog => @catalog
File.open(@filename, 'w') {|f| f.write "initial file content"}
content.stubs(:standalone?).returns(false)
end
describe "when determining the checksum type" do
it "should use the type specified in the source checksum if a source is set" do
@resource[:source] = File.expand_path("/foo")
@resource.parameter(:source).expects(:checksum).returns "{md5lite}eh"
@content = content.new(:resource => @resource)
@content.checksum_type.should == :md5lite
end
it "should use the type specified by the checksum parameter if no source is set" do
@resource[:checksum] = :md5lite
@content = content.new(:resource => @resource)
@content.checksum_type.should == :md5lite
end
end
describe "when determining the actual content to write" do
it "should use the set content if available" do
@content = content.new(:resource => @resource)
@content.should = "ehness"
@content.actual_content.should == "ehness"
end
it "should not use the content from the source if the source is set" do
source = mock 'source'
@resource.expects(:parameter).never.with(:source).returns source
@content = content.new(:resource => @resource)
@content.actual_content.should be_nil
end
end
describe "when setting the desired content" do
it "should make the actual content available via an attribute" do
@content = content.new(:resource => @resource)
@content.stubs(:checksum_type).returns "md5"
@content.should = "this is some content"
@content.actual_content.should == "this is some content"
end
it "should store the checksum as the desired content" do
@content = content.new(:resource => @resource)
digest = Digest::MD5.hexdigest("this is some content")
@content.stubs(:checksum_type).returns "md5"
@content.should = "this is some content"
@content.should.must == "{md5}#{digest}"
end
it "should not checksum 'absent'" do
@content = content.new(:resource => @resource)
@content.should = :absent
@content.should.must == :absent
end
it "should accept a checksum as the desired content" do
@content = content.new(:resource => @resource)
digest = Digest::MD5.hexdigest("this is some content")
string = "{md5}#{digest}"
@content.should = string
@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
it "should return :absent if the file does not exist" do
@content = content.new(:resource => @resource)
@resource.expects(:stat).returns nil
@content.retrieve.should == :absent
end
it "should not manage content on directories" do
@content = content.new(:resource => @resource)
stat = mock 'stat', :ftype => "directory"
@resource.expects(:stat).returns stat
@content.retrieve.should be_nil
end
it "should not manage content on links" do
@content = content.new(:resource => @resource)
stat = mock 'stat', :ftype => "link"
@resource.expects(:stat).returns stat
@content.retrieve.should be_nil
end
it "should always return the checksum as a string" do
@content = content.new(:resource => @resource)
@resource[:checksum] = :mtime
stat = mock 'stat', :ftype => "file"
@resource.expects(:stat).returns stat
time = Time.now
@resource.parameter(:checksum).expects(:mtime_file).with(@resource[:path]).returns time
@content.retrieve.should == "{mtime}#{time}"
end
it "should return the checksum of the file if it exists and is a normal file" do
@content = content.new(:resource => @resource)
stat = mock 'stat', :ftype => "file"
@resource.expects(:stat).returns stat
@resource.parameter(:checksum).expects(:md5_file).with(@resource[:path]).returns "mysum"
@content.retrieve.should == "{md5}mysum"
end
end
describe "when testing whether the content is in sync" do
before do
@resource[:ensure] = :file
@content = content.new(:resource => @resource)
end
it "should return true if the resource shouldn't be a regular file" do
@resource.expects(:should_be_file?).returns false
@content.should = "foo"
@content.must be_safe_insync("whatever")
end
it "should warn that no content will be synced to links when ensure is :present" do
@resource[:ensure] = :present
@resource[:content] = 'foo'
@resource.stubs(:should_be_file?).returns false
@resource.stubs(:stat).returns mock("stat", :ftype => "link")
@resource.expects(:warning).with {|msg| msg =~ /Ensure set to :present but file type is/}
@content.insync? :present
end
it "should return false if the current content is :absent" do
@content.should = "foo"
@content.should_not be_safe_insync(:absent)
end
it "should return false if the file should be a file but is not present" do
@resource.expects(:should_be_file?).returns true
@content.should = "foo"
@content.should_not be_safe_insync(:absent)
end
describe "and the file exists" do
before do
@resource.stubs(:stat).returns mock("stat")
@content.should = "some content"
end
it "should return false if the current contents are different from the desired content" do
@content.should_not be_safe_insync("other content")
end
it "should return true if the sum for the current contents is the same as the sum for the desired content" do
@content.must be_safe_insync("{md5}" + Digest::MD5.hexdigest("some content"))
end
[true, false].product([true, false]).each do |cfg, param|
describe "and Puppet[:show_diff] is #{cfg} and show_diff => #{param}" do
before do
Puppet[:show_diff] = cfg
@resource.stubs(:show_diff?).returns param
+ @resource[:loglevel] = "debug"
end
if cfg and param
it "should display a diff" do
@content.expects(:diff).returns("my diff").once
- @content.expects(:notice).with("\nmy diff").once
+ @content.expects(:debug).with("\nmy diff").once
@content.should_not be_safe_insync("other content")
end
else
it "should not display a diff" do
@content.expects(:diff).never
@content.should_not be_safe_insync("other content")
end
end
end
end
end
describe "and :replace is false" do
before do
@resource.stubs(:replace?).returns false
end
it "should be insync if the file exists and the content is different" do
@resource.stubs(:stat).returns mock('stat')
@content.must be_safe_insync("whatever")
end
it "should be insync if the file exists and the content is right" do
@resource.stubs(:stat).returns mock('stat')
@content.must be_safe_insync("something")
end
it "should not be insync if the file does not exist" do
@content.should = "foo"
@content.should_not be_safe_insync(:absent)
end
end
end
describe "when changing the content" do
before do
@content = content.new(:resource => @resource)
@content.should = "some content"
@resource.stubs(:[]).with(:path).returns "/boo"
@resource.stubs(:stat).returns "eh"
end
it "should use the file's :write method to write the content" do
@resource.expects(:write).with(:content)
@content.sync
end
it "should return :file_changed if the file already existed" do
@resource.expects(:stat).returns "something"
@resource.stubs(:write)
@content.sync.should == :file_changed
end
it "should return :file_created if the file did not exist" do
@resource.expects(:stat).returns nil
@resource.stubs(:write)
@content.sync.should == :file_created
end
end
describe "when writing" do
before do
@content = content.new(:resource => @resource)
end
it "should attempt to read from the filebucket if no actual content nor source exists" do
@fh = File.open(@filename, 'wb')
@content.should = "{md5}foo"
@content.resource.bucket.class.any_instance.stubs(:getfile).returns "foo"
@content.write(@fh)
@fh.close
end
describe "from actual content" do
before(:each) do
@content.stubs(:actual_content).returns("this is content")
end
it "should write to the given file handle" do
fh = mock 'filehandle'
fh.expects(:print).with("this is content")
@content.write(fh)
end
it "should return the current checksum value" do
@resource.parameter(:checksum).expects(:sum_stream).returns "checksum"
@content.write(@fh).should == "checksum"
end
end
describe "from a file bucket" do
it "should fail if a file bucket cannot be retrieved" do
@content.should = "{md5}foo"
@content.resource.expects(:bucket).returns nil
lambda { @content.write(@fh) }.should raise_error(Puppet::Error)
end
it "should fail if the file bucket cannot find any content" do
@content.should = "{md5}foo"
bucket = stub 'bucket'
@content.resource.expects(:bucket).returns bucket
bucket.expects(:getfile).with("foo").raises "foobar"
lambda { @content.write(@fh) }.should raise_error(Puppet::Error)
end
it "should write the returned content to the file" do
@content.should = "{md5}foo"
bucket = stub 'bucket'
@content.resource.expects(:bucket).returns bucket
bucket.expects(:getfile).with("foo").returns "mycontent"
fh = mock 'filehandle'
fh.expects(:print).with("mycontent")
@content.write(fh)
end
end
describe "from local source" do
before(:each) do
@sourcename = tmpfile('source')
@resource = Puppet::Type.type(:file).new :path => @filename, :backup => false, :source => @sourcename, :catalog => @catalog
@source_content = "source file content\r\n"*10000
@sourcefile = File.open(@sourcename, 'wb') {|f| f.write @source_content}
@content = @resource.newattr(:content)
@source = @resource.parameter :source #newattr(:source)
end
it "should copy content from the source to the file" do
- dest_file = Puppet::FileSystem::File.new(@filename)
@resource.write(@source)
- dest_file.binread.should == @source_content
+ Puppet::FileSystem.binread(@filename).should == @source_content
end
it "should return the checksum computed" do
File.open(@filename, 'wb') do |file|
@content.write(file).should == "{md5}#{Digest::MD5.hexdigest(@source_content)}"
end
end
end
describe "from remote source" do
before(:each) do
@resource = Puppet::Type.type(:file).new :path => @filename, :backup => false, :catalog => @catalog
@response = stub_everything 'response', :code => "200"
@source_content = "source file content\n"*10000
@response.stubs(:read_body).multiple_yields(*(["source file content\n"]*10000))
@conn = stub_everything 'connection'
@conn.stubs(:request_get).yields @response
Puppet::Network::HttpPool.stubs(:http_instance).returns @conn
@content = @resource.newattr(:content)
@sourcename = "puppet:///test/foo"
@source = @resource.newattr(:source)
@source.stubs(:metadata).returns stub_everything('metadata', :source => @sourcename, :ftype => 'file')
end
it "should write the contents to the file" do
- dest_file = Puppet::FileSystem::File.new(@filename)
@resource.write(@source)
- dest_file.binread.should == @source_content
+ Puppet::FileSystem.binread(@filename).should == @source_content
end
it "should not write anything if source is not found" do
@response.stubs(:code).returns("404")
lambda {@resource.write(@source)}.should raise_error(Net::HTTPError) { |e| e.message =~ /404/ }
File.read(@filename).should == "initial file content"
end
it "should raise an HTTP error in case of server error" do
@response.stubs(:code).returns("500")
lambda { @content.write(@fh) }.should raise_error { |e| e.message.include? @source_content }
end
it "should return the checksum computed" do
File.open(@filename, 'w') do |file|
@content.write(file).should == "{md5}#{Digest::MD5.hexdigest(@source_content)}"
end
end
end
# These are testing the implementation rather than the desired behaviour; while that bites, there are a whole
# pile of other methods in the File type that depend on intimate details of this implementation and vice-versa.
# If these blow up, you are gonna have to review the callers to make sure they don't explode! --daniel 2011-02-01
describe "each_chunk_from should work" do
before do
@content = content.new(:resource => @resource)
end
it "when content is a string" do
@content.each_chunk_from('i_am_a_string') { |chunk| chunk.should == 'i_am_a_string' }
end
# The following manifest is a case where source and content.should are both set
# file { "/tmp/mydir" :
# source => '/tmp/sourcedir',
# recurse => true,
# }
it "when content checksum comes from source" do
source_param = Puppet::Type.type(:file).attrclass(:source)
source = source_param.new(:resource => @resource)
@content.should = "{md5}123abcd"
@content.expects(:chunk_file_from_source).returns('from_source')
@content.each_chunk_from(source) { |chunk| chunk.should == 'from_source' }
end
it "when no content, source, but ensure present" do
@resource[:ensure] = :present
@content.each_chunk_from(nil) { |chunk| chunk.should == '' }
end
# you might do this if you were just auditing
it "when no content, source, but ensure file" do
@resource[:ensure] = :file
@content.each_chunk_from(nil) { |chunk| chunk.should == '' }
end
it "when source_or_content is nil and content not a checksum" do
@content.each_chunk_from(nil) { |chunk| chunk.should == '' }
end
# the content is munged so that if it's a checksum nil gets passed in
it "when content is a checksum it should try to read from filebucket" do
@content.should = "{md5}123abcd"
@content.expects(:read_file_from_filebucket).once.returns('im_a_filebucket')
@content.each_chunk_from(nil) { |chunk| chunk.should == 'im_a_filebucket' }
end
it "when running as puppet apply" do
Puppet[:default_file_terminus] = "file_server"
source_or_content = stubs('source_or_content')
source_or_content.expects(:content).once.returns :whoo
@content.each_chunk_from(source_or_content) { |chunk| chunk.should == :whoo }
end
it "when running from source with a local file" do
source_or_content = stubs('source_or_content')
source_or_content.expects(:local?).returns true
@content.expects(:chunk_file_from_disk).with(source_or_content).once.yields 'woot'
@content.each_chunk_from(source_or_content) { |chunk| chunk.should == 'woot' }
end
it "when running from source with a remote file" do
source_or_content = stubs('source_or_content')
source_or_content.expects(:local?).returns false
@content.expects(:chunk_file_from_source).with(source_or_content).once.yields 'woot'
@content.each_chunk_from(source_or_content) { |chunk| chunk.should == 'woot' }
end
end
end
end
diff --git a/spec/unit/type/file/ctime_spec.rb b/spec/unit/type/file/ctime_spec.rb
index eea0f1f92..ecb7458bc 100755
--- a/spec/unit/type/file/ctime_spec.rb
+++ b/spec/unit/type/file/ctime_spec.rb
@@ -1,34 +1,34 @@
#! /usr/bin/env ruby
require 'spec_helper'
describe Puppet::Type.type(:file).attrclass(:ctime) do
require 'puppet_spec/files'
include PuppetSpec::Files
before do
@filename = tmpfile('ctime')
@resource = Puppet::Type.type(:file).new({:name => @filename})
end
it "should be able to audit the file's ctime" do
File.open(@filename, "w"){ }
@resource[:audit] = [:ctime]
# this .to_resource audit behavior is magical :-(
- @resource.to_resource[:ctime].should == Puppet::FileSystem::File.new(@filename).stat.ctime
+ @resource.to_resource[:ctime].should == Puppet::FileSystem.stat(@filename).ctime
end
it "should return absent if auditing an absent file" do
@resource[:audit] = [:ctime]
@resource.to_resource[:ctime].should == :absent
end
it "should prevent the user from trying to set the ctime" do
lambda {
@resource[:ctime] = Time.now.to_s
}.should raise_error(Puppet::Error, /ctime is read-only/)
end
end
diff --git a/spec/unit/type/file/mode_spec.rb b/spec/unit/type/file/mode_spec.rb
index 8663fe57d..9936ebdbc 100755
--- a/spec/unit/type/file/mode_spec.rb
+++ b/spec/unit/type/file/mode_spec.rb
@@ -1,194 +1,195 @@
#! /usr/bin/env ruby
require 'spec_helper'
describe Puppet::Type.type(:file).attrclass(:mode) do
include PuppetSpec::Files
let(:path) { tmpfile('mode_spec') }
let(:resource) { Puppet::Type.type(:file).new :path => path, :mode => 0644 }
let(:mode) { resource.property(:mode) }
describe "#validate" do
it "should accept values specified as integers" do
expect { mode.value = 0755 }.not_to raise_error
end
it "should accept values specified as octal numbers in strings" 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!'
end.to raise_error(Puppet::Error, /The file mode specification is invalid/)
end
end
describe "#munge" do
# This is sort of a redundant test, but its spec is important.
it "should return the value as a string" do
mode.munge('0644').should be_a(String)
end
it "should accept strings as arguments" 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
end
describe "#dirmask" do
before :each do
Dir.mkdir(path)
end
it "should add execute bits corresponding to read bits for directories" do
mode.dirmask('0644').should == '755'
end
it "should not add an execute bit when there is no read bit" do
mode.dirmask('0600').should == '700'
end
it "should not add execute bits for files that aren't directories" do
resource[:path] = tmpfile('other_file')
mode.dirmask('0644').should == '0644'
end
end
describe "#insync?" do
it "should return true if the mode is correct" do
FileUtils.touch(path)
mode.must be_insync('644')
end
it "should return false if the mode is incorrect" do
FileUtils.touch(path)
mode.must_not be_insync('755')
end
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)
+ Puppet::FileSystem.symlink('anything', 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
it "should return absent if the resource doesn't exist" do
resource[:path] = File.expand_path("/does/not/exist")
mode.retrieve.should == :absent
end
it "should retrieve the directory mode from the provider" do
Dir.mkdir(path)
mode.expects(:dirmask).with('644').returns '755'
resource.provider.expects(:mode).returns '755'
mode.retrieve.should == '755'
end
it "should retrieve the file mode from the provider" do
FileUtils.touch(path)
mode.expects(:dirmask).with('644').returns '644'
resource.provider.expects(:mode).returns '644'
mode.retrieve.should == '644'
end
end
describe '#should_to_s' do
describe 'with a 3-digit mode' do
it 'returns a 4-digit mode with a leading zero' do
mode.should_to_s('755').should == '0755'
end
end
describe 'with a 4-digit mode' do
it 'returns the 4-digit mode when the first digit is a zero' do
mode.should_to_s('0755').should == '0755'
end
it 'returns the 4-digit mode when the first digit is not a zero' do
mode.should_to_s('1755').should == '1755'
end
end
end
describe '#is_to_s' do
describe 'with a 3-digit mode' do
it 'returns a 4-digit mode with a leading zero' do
mode.is_to_s('755').should == '0755'
end
end
describe 'with a 4-digit mode' do
it 'returns the 4-digit mode when the first digit is a zero' do
mode.is_to_s('0755').should == '0755'
end
it 'returns the 4-digit mode when the first digit is not a zero' do
mode.is_to_s('1755').should == '1755'
end
end
describe 'when passed :absent' do
it 'returns :absent' do
mode.is_to_s(:absent).should == :absent
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
+ Puppet::FileSystem.chmod(0464, path)
+
mode_sym.sync
- file = Puppet::FileSystem::File.new(path)
- (file.stat.mode & 0777).to_s(8).should == "644"
+ stat = Puppet::FileSystem.stat(path)
+ (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 a20bdf196..5456ec38b 100755
--- a/spec/unit/type/file/mtime_spec.rb
+++ b/spec/unit/type/file/mtime_spec.rb
@@ -1,34 +1,34 @@
#! /usr/bin/env ruby
require 'spec_helper'
describe Puppet::Type.type(:file).attrclass(:mtime) do
require 'puppet_spec/files'
include PuppetSpec::Files
before do
@filename = tmpfile('mtime')
@resource = Puppet::Type.type(:file).new({:name => @filename})
end
it "should be able to audit the file's mtime" do
File.open(@filename, "w"){ }
@resource[:audit] = [:mtime]
# this .to_resource audit behavior is magical :-(
- @resource.to_resource[:mtime].should == Puppet::FileSystem::File.new(@filename).stat.mtime
+ @resource.to_resource[:mtime].should == Puppet::FileSystem.stat(@filename).mtime
end
it "should return absent if auditing an absent file" do
@resource[:audit] = [:mtime]
@resource.to_resource[:mtime].should == :absent
end
it "should prevent the user from trying to set the mtime" do
lambda {
@resource[:mtime] = Time.now.to_s
}.should raise_error(Puppet::Error, /mtime is read-only/)
end
end
diff --git a/spec/unit/type/file/source_spec.rb b/spec/unit/type/file/source_spec.rb
index e645842c0..b6e97cd7a 100755
--- a/spec/unit/type/file/source_spec.rb
+++ b/spec/unit/type/file/source_spec.rb
@@ -1,532 +1,555 @@
#! /usr/bin/env ruby
require 'spec_helper'
require 'uri'
source = Puppet::Type.type(:file).attrclass(:source)
describe Puppet::Type.type(:file).attrclass(:source) do
include PuppetSpec::Files
before do
# Wow that's a messy interface to the resource.
@environment = "myenv"
@resource = stub 'resource', :[]= => nil, :property => nil, :catalog => stub("catalog", :dependent_data_expired? => false, :environment => @environment), :line => 0, :file => ''
@foobar = make_absolute("/foo/bar baz")
@feebooz = make_absolute("/fee/booz baz")
@foobar_uri = URI.unescape(Puppet::Util.path_to_uri(@foobar).to_s)
@feebooz_uri = URI.unescape(Puppet::Util.path_to_uri(@feebooz).to_s)
end
it "should be a subclass of Parameter" do
source.superclass.must == Puppet::Parameter
end
describe "#validate" do
let(:path) { tmpfile('file_source_validate') }
let(:resource) { Puppet::Type.type(:file).new(:path => path) }
it "should fail if the set values are not URLs" do
URI.expects(:parse).with('foo').raises RuntimeError
lambda { resource[:source] = %w{foo} }.must raise_error(Puppet::Error)
end
it "should fail if the URI is not a local file, file URI, or puppet URI" do
lambda { resource[:source] = %w{http://foo/bar} }.must raise_error(Puppet::Error, /Cannot use URLs of type 'http' as source for fileserving/)
end
it "should strip trailing forward slashes", :unless => Puppet.features.microsoft_windows? do
resource[:source] = "/foo/bar\\//"
resource[:source].should == %w{file:/foo/bar\\}
end
it "should strip trailing forward and backslashes", :if => Puppet.features.microsoft_windows? do
resource[:source] = "X:/foo/bar\\//"
resource[:source].should == %w{file:/X:/foo/bar}
end
it "should accept an array of sources" do
resource[:source] = %w{file:///foo/bar puppet://host:8140/foo/bar}
resource[:source].should == %w{file:///foo/bar puppet://host:8140/foo/bar}
end
it "should accept file path characters that are not valid in URI" do
resource[:source] = 'file:///foo bar'
end
it "should reject relative URI sources" do
lambda { resource[:source] = 'foo/bar' }.must raise_error(Puppet::Error)
end
it "should reject opaque sources" do
lambda { resource[:source] = 'mailto:foo@com' }.must raise_error(Puppet::Error)
end
it "should accept URI authority component" do
resource[:source] = 'file://host/foo'
resource[:source].should == %w{file://host/foo}
end
it "should accept when URI authority is absent" do
resource[:source] = 'file:///foo/bar'
resource[:source].should == %w{file:///foo/bar}
end
end
describe "#munge" do
let(:path) { tmpfile('file_source_munge') }
let(:resource) { Puppet::Type.type(:file).new(:path => path) }
it "should prefix file scheme to absolute paths" do
resource[:source] = path
resource[:source].should == [URI.unescape(Puppet::Util.path_to_uri(path).to_s)]
end
%w[file puppet].each do |scheme|
it "should not prefix valid #{scheme} URIs" do
resource[:source] = "#{scheme}:///foo bar"
resource[:source].should == ["#{scheme}:///foo bar"]
end
end
end
describe "when returning the metadata" do
before do
@metadata = stub 'metadata', :source= => nil
@resource.stubs(:[]).with(:links).returns :manage
+ @resource.stubs(:[]).with(:source_permissions)
end
it "should return already-available metadata" do
@source = source.new(:resource => @resource)
@source.metadata = "foo"
@source.metadata.should == "foo"
end
it "should return nil if no @should value is set and no metadata is available" do
@source = source.new(:resource => @resource)
@source.metadata.should be_nil
end
it "should collect its metadata using the Metadata class if it is not already set" do
@source = source.new(:resource => @resource, :value => @foobar)
- Puppet::FileServing::Metadata.indirection.expects(:find).with(@foobar_uri, :environment => @environment, :links => :manage).returns @metadata
+ Puppet::FileServing::Metadata.indirection.expects(:find).with do |uri, options|
+ expect(uri).to eq @foobar_uri
+ expect(options[:environment]).to eq @environment
+ expect(options[:links]).to eq :manage
+ end.returns @metadata
+
@source.metadata
end
it "should use the metadata from the first found source" do
metadata = stub 'metadata', :source= => nil
@source = source.new(:resource => @resource, :value => [@foobar, @feebooz])
- Puppet::FileServing::Metadata.indirection.expects(:find).with(@foobar_uri, :environment => @environment, :links => :manage).returns nil
- Puppet::FileServing::Metadata.indirection.expects(:find).with(@feebooz_uri, :environment => @environment, :links => :manage).returns metadata
+ options = {
+ :environment => @environment,
+ :links => :manage,
+ :source_permissions => nil
+ }
+ Puppet::FileServing::Metadata.indirection.expects(:find).with(@foobar_uri, options).returns nil
+ Puppet::FileServing::Metadata.indirection.expects(:find).with(@feebooz_uri, options).returns metadata
@source.metadata.should equal(metadata)
end
it "should store the found source as the metadata's source" do
metadata = mock 'metadata'
@source = source.new(:resource => @resource, :value => @foobar)
- Puppet::FileServing::Metadata.indirection.expects(:find).with(@foobar_uri, :environment => @environment, :links => :manage).returns metadata
+ Puppet::FileServing::Metadata.indirection.expects(:find).with do |uri, options|
+ expect(uri).to eq @foobar_uri
+ expect(options[:environment]).to eq @environment
+ expect(options[:links]).to eq :manage
+ end.returns metadata
metadata.expects(:source=).with(@foobar_uri)
@source.metadata
end
it "should fail intelligently if an exception is encountered while querying for metadata" do
@source = source.new(:resource => @resource, :value => @foobar)
- Puppet::FileServing::Metadata.indirection.expects(:find).with(@foobar_uri, :environment => @environment, :links => :manage).raises RuntimeError
+ Puppet::FileServing::Metadata.indirection.expects(:find).with do |uri, options|
+ expect(uri).to eq @foobar_uri
+ expect(options[:environment]).to eq @environment
+ expect(options[:links]).to eq :manage
+ end.raises RuntimeError
@source.expects(:fail).raises ArgumentError
lambda { @source.metadata }.should raise_error(ArgumentError)
end
it "should fail if no specified sources can be found" do
@source = source.new(:resource => @resource, :value => @foobar)
- Puppet::FileServing::Metadata.indirection.expects(:find).with(@foobar_uri, :environment => @environment, :links => :manage).returns nil
+ Puppet::FileServing::Metadata.indirection.expects(:find).with do |uri, options|
+ expect(uri).to eq @foobar_uri
+ expect(options[:environment]).to eq @environment
+ expect(options[:links]).to eq :manage
+ end.returns nil
@source.expects(:fail).raises RuntimeError
lambda { @source.metadata }.should raise_error(RuntimeError)
end
end
it "should have a method for setting the desired values on the resource" do
source.new(:resource => @resource).must respond_to(:copy_source_values)
end
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 => "173", :checksum => "{md5}asdfasdf", :ftype => "file", :source => @foobar
@source.stubs(:metadata).returns @metadata
Puppet.features.stubs(:root?).returns true
end
it "should fail if there is no metadata" do
@source.stubs(:metadata).returns nil
@source.expects(:devfail).raises ArgumentError
lambda { @source.copy_source_values }.should raise_error(ArgumentError)
end
it "should set :ensure to the file type" do
@metadata.stubs(:ftype).returns "file"
@source.copy_source_values
@resource[:ensure].must == :file
end
it "should not set 'ensure' if it is already set to 'absent'" do
@metadata.stubs(:ftype).returns "file"
@resource[:ensure] = :absent
@source.copy_source_values
@resource[:ensure].must == :absent
end
describe "and the source is a file" do
before do
@metadata.stubs(:ftype).returns "file"
Puppet.features.stubs(:microsoft_windows?).returns false
end
it "should copy the metadata's owner, group, checksum, and mode to the resource if they are not set on the resource" do
@source.copy_source_values
@resource[:owner].must == 100
@resource[:group].must == 200
@resource[:mode].must == "173"
# Metadata calls it checksum, we call it content.
@resource[:content].must == @metadata.checksum
end
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
@resource[:content] = "foobar"
@source.copy_source_values
@resource[:owner].must == 1
@resource[:group].must == 2
@resource[:mode].must == "3"
@resource[:content].should_not == @metadata.checksum
end
describe "and puppet is not running as root" do
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)
+ Puppet::FileSystem.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 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
@source.stubs(:local?).returns true
@source.copy_source_values
@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
describe "and the source is a link" do
it "should set the target to the link destination" do
@metadata.stubs(:ftype).returns "link"
@metadata.stubs(:links).returns "manage"
@resource.stubs(:[])
@resource.stubs(:[]=)
@metadata.expects(:destination).returns "/path/to/symlink"
@resource.expects(:[]=).with(:target, "/path/to/symlink")
@source.copy_source_values
end
end
end
it "should have a local? method" do
source.new(:resource => @resource).must be_respond_to(:local?)
end
context "when accessing source properties" do
let(:catalog) { Puppet::Resource::Catalog.new }
let(:path) { tmpfile('file_resource') }
let(:resource) { Puppet::Type.type(:file).new(:path => path, :catalog => catalog) }
let(:sourcepath) { tmpfile('file_source') }
describe "for local sources" do
before :each do
FileUtils.touch(sourcepath)
end
describe "on POSIX systems", :if => Puppet.features.posix? do
['', "file:", "file://"].each do |prefix|
it "with prefix '#{prefix}' should be local" do
resource[:source] = "#{prefix}#{sourcepath}"
resource.parameter(:source).must be_local
end
it "should be able to return the metadata source full path" do
resource[:source] = "#{prefix}#{sourcepath}"
resource.parameter(:source).full_path.should == sourcepath
end
end
end
describe "on Windows systems", :if => Puppet.features.microsoft_windows? do
['', "file:/", "file:///"].each do |prefix|
it "should be local with prefix '#{prefix}'" do
resource[:source] = "#{prefix}#{sourcepath}"
resource.parameter(:source).must be_local
end
it "should be able to return the metadata source full path" do
resource[:source] = "#{prefix}#{sourcepath}"
resource.parameter(:source).full_path.should == sourcepath
end
it "should convert backslashes to forward slashes" do
resource[:source] = "#{prefix}#{sourcepath.gsub(/\\/, '/')}"
end
end
it "should be UNC with two slashes"
end
end
describe "for remote sources" do
let(:sourcepath) { "/path/to/source" }
let(:uri) { URI::Generic.build(:scheme => 'puppet', :host => 'server', :port => 8192, :path => sourcepath).to_s }
before(:each) do
metadata = Puppet::FileServing::Metadata.new(path, :source => uri, 'type' => 'file')
#metadata = stub('remote', :ftype => "file", :source => uri)
Puppet::FileServing::Metadata.indirection.stubs(:find).
with(uri,all_of(has_key(:environment), has_key(:links))).returns metadata
resource[:source] = uri
end
it "should not be local" do
resource.parameter(:source).should_not be_local
end
it "should be able to return the metadata source full path" do
resource.parameter(:source).full_path.should == "/path/to/source"
end
it "should be able to return the source server" do
resource.parameter(:source).server.should == "server"
end
it "should be able to return the source port" do
resource.parameter(:source).port.should == 8192
end
describe "which don't specify server or port" do
let(:uri) { "puppet:///path/to/source" }
it "should return the default source server" do
Puppet[:server] = "myserver"
resource.parameter(:source).server.should == "myserver"
end
it "should return the default source port" do
Puppet[:masterport] = 1234
resource.parameter(:source).port.should == 1234
end
end
end
end
end
diff --git a/spec/unit/type/file_spec.rb b/spec/unit/type/file_spec.rb
index c7b529e2d..12aed3e42 100755
--- a/spec/unit/type/file_spec.rb
+++ b/spec/unit/type/file_spec.rb
@@ -1,1502 +1,1502 @@
#! /usr/bin/env ruby
require 'spec_helper'
describe Puppet::Type.type(:file) do
include PuppetSpec::Files
let(:path) { tmpfile('file_testing') }
let(:file) { described_class.new(:path => path, :catalog => catalog) }
let(:provider) { file.provider }
let(:catalog) { Puppet::Resource::Catalog.new }
before do
Puppet.features.stubs("posix?").returns(true)
end
describe "the path parameter" do
describe "on POSIX systems", :if => Puppet.features.posix? do
it "should remove trailing slashes" do
file[:path] = "/foo/bar/baz/"
file[:path].should == "/foo/bar/baz"
end
it "should remove double slashes" do
file[:path] = "/foo/bar//baz"
file[:path].should == "/foo/bar/baz"
end
it "should remove triple slashes" do
file[:path] = "/foo/bar///baz"
file[:path].should == "/foo/bar/baz"
end
it "should remove trailing double slashes" do
file[:path] = "/foo/bar/baz//"
file[:path].should == "/foo/bar/baz"
end
it "should leave a single slash alone" do
file[:path] = "/"
file[:path].should == "/"
end
it "should accept and collapse a double-slash at the start of the path" do
file[:path] = "//tmp/xxx"
file[:path].should == '/tmp/xxx'
end
it "should accept and collapse a triple-slash at the start of the path" do
file[:path] = "///tmp/xxx"
file[:path].should == '/tmp/xxx'
end
end
describe "on Windows systems", :if => Puppet.features.microsoft_windows? do
it "should remove trailing slashes" do
file[:path] = "X:/foo/bar/baz/"
file[:path].should == "X:/foo/bar/baz"
end
it "should remove double slashes" do
file[:path] = "X:/foo/bar//baz"
file[:path].should == "X:/foo/bar/baz"
end
it "should remove trailing double slashes" do
file[:path] = "X:/foo/bar/baz//"
file[:path].should == "X:/foo/bar/baz"
end
it "should leave a drive letter with a slash alone" do
file[:path] = "X:/"
file[:path].should == "X:/"
end
it "should not accept a drive letter without a slash" do
expect { file[:path] = "X:" }.to raise_error(/File paths must be fully qualified/)
end
describe "when using UNC filenames", :if => Puppet.features.microsoft_windows? do
it "should remove trailing slashes" do
file[:path] = "//localhost/foo/bar/baz/"
file[:path].should == "//localhost/foo/bar/baz"
end
it "should remove double slashes" do
file[:path] = "//localhost/foo/bar//baz"
file[:path].should == "//localhost/foo/bar/baz"
end
it "should remove trailing double slashes" do
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] = "//localhost/foo/"
file[:path].should == "//localhost/foo"
end
it "should not modify a sharename" do
file[:path] = "//localhost/foo"
file[:path].should == "//localhost/foo"
end
end
end
end
describe "the backup parameter" do
[false, 'false', :false].each do |value|
it "should disable backup if the value is #{value.inspect}" do
file[:backup] = value
file[:backup].should == false
end
end
[true, 'true', '.puppet-bak'].each do |value|
it "should use .puppet-bak if the value is #{value.inspect}" do
file[:backup] = value
file[:backup].should == '.puppet-bak'
end
end
it "should use the provided value if it's any other string" do
file[:backup] = "over there"
file[:backup].should == "over there"
end
it "should fail if backup is set to anything else" do
expect do
file[:backup] = 97
end.to raise_error(Puppet::Error, /Invalid backup type 97/)
end
end
describe "the recurse parameter" do
it "should default to recursion being disabled" do
file[:recurse].should be_false
end
[true, "true", "inf", "remote"].each do |value|
it "should consider #{value} to enable recursion" do
file[:recurse] = value
file[:recurse].should be_true
end
end
it "should not allow numbers" do
expect { file[:recurse] = 10 }.to raise_error(
Puppet::Error, /Parameter recurse failed on File\[[^\]]+\]: Invalid recurse value 10/)
end
[false, "false"].each do |value|
it "should consider #{value} to disable recursion" do
file[:recurse] = value
file[:recurse].should be_false
end
end
end
describe "the recurselimit parameter" do
it "should accept integers" do
file[:recurselimit] = 12
file[:recurselimit].should == 12
end
it "should munge string numbers to number numbers" do
file[:recurselimit] = '12'
file[:recurselimit].should == 12
end
it "should fail if given a non-number" do
expect do
file[:recurselimit] = 'twelve'
end.to raise_error(Puppet::Error, /Invalid value "twelve"/)
end
end
describe "the replace parameter" do
[true, :true, :yes].each do |value|
it "should consider #{value} to be true" do
file[:replace] = value
file[:replace].should be_true
end
end
[false, :false, :no].each do |value|
it "should consider #{value} to be false" do
file[:replace] = value
file[:replace].should be_false
end
end
end
describe ".instances" do
it "should return an empty array" do
described_class.instances.should == []
end
end
describe "#bucket" do
it "should return nil if backup is off" do
file[:backup] = false
file.bucket.should == nil
end
it "should not return a bucket if using a file extension for backup" do
file[:backup] = '.backup'
file.bucket.should == nil
end
it "should return the default filebucket if using the 'puppet' filebucket" do
file[:backup] = 'puppet'
bucket = stub('bucket')
file.stubs(:default_bucket).returns bucket
file.bucket.should == bucket
end
it "should fail if using a remote filebucket and no catalog exists" do
file.catalog = nil
file[:backup] = 'my_bucket'
expect { file.bucket }.to raise_error(Puppet::Error, "Can not find filebucket for backups without a catalog")
end
it "should fail if the specified filebucket isn't in the catalog" do
file[:backup] = 'my_bucket'
expect { file.bucket }.to raise_error(Puppet::Error, "Could not find filebucket my_bucket specified in backup")
end
it "should use the specified filebucket if it is in the catalog" do
file[:backup] = 'my_bucket'
filebucket = Puppet::Type.type(:filebucket).new(:name => 'my_bucket')
catalog.add_resource(filebucket)
file.bucket.should == filebucket.bucket
end
end
describe "#asuser" do
before :each do
# Mocha won't let me just stub SUIDManager.asuser to yield and return,
# but it will do exactly that if we're not root.
Puppet.features.stubs(:root?).returns false
end
it "should return the desired owner if they can write to the parent directory" do
file[:owner] = 1001
FileTest.stubs(:writable?).with(File.dirname file[:path]).returns true
file.asuser.should == 1001
end
it "should return nil if the desired owner can't write to the parent directory" do
file[:owner] = 1001
FileTest.stubs(:writable?).with(File.dirname file[:path]).returns false
file.asuser.should == nil
end
it "should return nil if not managing owner" do
file.asuser.should == nil
end
end
describe "#exist?" do
it "should be considered existent if it can be stat'ed" do
file.expects(:stat).returns mock('stat')
file.must be_exist
end
it "should be considered nonexistent if it can not be stat'ed" do
file.expects(:stat).returns nil
file.must_not be_exist
end
end
describe "#eval_generate" do
before do
@graph = stub 'graph', :add_edge => nil
catalog.stubs(:relationship_graph).returns @graph
end
it "should recurse if recursion is enabled" do
resource = stub('resource', :[] => 'resource')
file.expects(:recurse).returns [resource]
file[:recurse] = true
file.eval_generate.should == [resource]
end
it "should not recurse if recursion is disabled" do
file.expects(:recurse).never
file[:recurse] = false
file.eval_generate.should == []
end
end
describe "#ancestors" do
it "should return the ancestors of the file, in ascending order" do
file = described_class.new(:path => make_absolute("/tmp/foo/bar/baz/qux"))
pieces = %W[#{make_absolute('/')} tmp foo bar baz]
ancestors = file.ancestors
ancestors.should_not be_empty
ancestors.reverse.each_with_index do |path,i|
path.should == File.join(*pieces[0..i])
end
end
end
describe "#flush" do
it "should flush all properties that respond to :flush" do
file[:source] = File.expand_path(__FILE__)
file.parameter(:source).expects(:flush)
file.flush
end
it "should reset its stat reference" do
FileUtils.touch(path)
stat1 = file.stat
file.stat.should equal(stat1)
file.flush
file.stat.should_not equal(stat1)
end
end
describe "#initialize" do
it "should remove a trailing slash from the title to create the path" do
title = File.expand_path("/abc/\n\tdef/")
file = described_class.new(:title => title)
file[:path].should == title
end
it "should set a desired 'ensure' value if none is set and 'content' is set" do
file = described_class.new(:path => path, :content => "/foo/bar")
file[:ensure].should == :file
end
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
end
describe "#mark_children_for_purging" do
it "should set each child's ensure to absent" do
paths = %w[foo bar baz]
children = paths.inject({}) do |children,child|
children.merge child => described_class.new(:path => File.join(path, child), :ensure => :present)
end
file.mark_children_for_purging(children)
children.length.should == 3
children.values.each do |child|
child[:ensure].should == :absent
end
end
it "should skip children which have a source" do
child = described_class.new(:path => path, :ensure => :present, :source => File.expand_path(__FILE__))
file.mark_children_for_purging('foo' => child)
child[:ensure].should == :present
end
end
describe "#newchild" do
it "should create a new resource relative to the parent" do
child = file.newchild('bar')
child.must be_a(described_class)
child[:path].should == File.join(file[:path], 'bar')
end
{
:ensure => :present,
:recurse => true,
:recurselimit => 5,
:target => "some_target",
:source => File.expand_path("some_source"),
}.each do |param, value|
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)
child = file.newchild('bar')
child[param].should_not == value
end
end
it "should copy all of the parent resource's 'should' values that were set at initialization" do
parent = described_class.new(:path => path, :owner => 'root', :group => 'wheel')
child = parent.newchild("my/path")
child[:owner].should == 'root'
child[:group].should == 'wheel'
end
it "should not copy default values to the new child" do
child = file.newchild("my/path")
child.original_parameters.should_not include(:backup)
end
it "should not copy values to the child which were set by the source" do
source = File.expand_path(__FILE__)
file[:source] = source
metadata = stub 'metadata', :owner => "root", :group => "root", :mode => 0755, :ftype => "file", :checksum => "{md5}whatever", :source => source
file.parameter(:source).stubs(:metadata).returns metadata
file.parameter(:source).copy_source_values
file.class.expects(:new).with { |params| params[:group].nil? }
file.newchild("my/path")
end
end
describe "#purge?" do
it "should return false if purge is not set" do
file.must_not be_purge
end
it "should return true if purge is set to true" do
file[:purge] = true
file.must be_purge
end
it "should return false if purge is set to false" do
file[:purge] = false
file.must_not be_purge
end
end
describe "#recurse" do
before do
file[:recurse] = true
@metadata = Puppet::FileServing::Metadata
end
describe "and a source is set" do
it "should pass the already-discovered resources to recurse_remote" do
file[:source] = File.expand_path(__FILE__)
file.stubs(:recurse_local).returns(:foo => "bar")
file.expects(:recurse_remote).with(:foo => "bar").returns []
file.recurse
end
end
describe "and a target is set" do
it "should use recurse_link" do
file[:target] = File.expand_path(__FILE__)
file.stubs(:recurse_local).returns(:foo => "bar")
file.expects(:recurse_link).with(:foo => "bar").returns []
file.recurse
end
end
it "should use recurse_local if recurse is not remote" do
file.expects(:recurse_local).returns({})
file.recurse
end
it "should not use recurse_local if recurse is remote" do
file[:recurse] = :remote
file.expects(:recurse_local).never
file.recurse
end
it "should return the generated resources as an array sorted by file path" do
one = stub 'one', :[] => "/one"
two = stub 'two', :[] => "/one/two"
three = stub 'three', :[] => "/three"
file.expects(:recurse_local).returns(:one => one, :two => two, :three => three)
file.recurse.should == [one, two, three]
end
describe "and purging is enabled" do
before do
file[:purge] = true
end
it "should mark each file for removal" do
local = described_class.new(:path => path, :ensure => :present)
file.expects(:recurse_local).returns("local" => local)
file.recurse
local[:ensure].should == :absent
end
it "should not remove files that exist in the remote repository" do
file[:source] = File.expand_path(__FILE__)
file.expects(:recurse_local).returns({})
remote = described_class.new(:path => path, :source => File.expand_path(__FILE__), :ensure => :present)
file.expects(:recurse_remote).with { |hash| hash["remote"] = remote }
file.recurse
remote[:ensure].should_not == :absent
end
end
end
describe "#remove_less_specific_files" do
it "should remove any nested files that are already in the catalog" do
foo = described_class.new :path => File.join(file[:path], 'foo')
bar = described_class.new :path => File.join(file[:path], 'bar')
baz = described_class.new :path => File.join(file[:path], 'baz')
catalog.add_resource(foo)
catalog.add_resource(bar)
file.remove_less_specific_files([foo, bar, baz]).should == [baz]
end
end
describe "#remove_less_specific_files" do
it "should remove any nested files that are already in the catalog" do
foo = described_class.new :path => File.join(file[:path], 'foo')
bar = described_class.new :path => File.join(file[:path], 'bar')
baz = described_class.new :path => File.join(file[:path], 'baz')
catalog.add_resource(foo)
catalog.add_resource(bar)
file.remove_less_specific_files([foo, bar, baz]).should == [baz]
end
end
describe "#recurse?" do
it "should be true if recurse is true" do
file[:recurse] = true
file.must be_recurse
end
it "should be true if recurse is remote" do
file[:recurse] = :remote
file.must be_recurse
end
it "should be false if recurse is false" do
file[:recurse] = false
file.must_not be_recurse
end
end
describe "#recurse_link" do
before do
@first = stub 'first', :relative_path => "first", :full_path => "/my/first", :ftype => "directory"
@second = stub 'second', :relative_path => "second", :full_path => "/my/second", :ftype => "file"
@resource = stub 'file', :[]= => nil
end
it "should pass its target to the :perform_recursion method" do
file[:target] = "mylinks"
file.expects(:perform_recursion).with("mylinks").returns [@first]
file.stubs(:newchild).returns @resource
file.recurse_link({})
end
it "should ignore the recursively-found '.' file and configure the top-level file to create a directory" do
@first.stubs(:relative_path).returns "."
file[:target] = "mylinks"
file.expects(:perform_recursion).with("mylinks").returns [@first]
file.stubs(:newchild).never
file.expects(:[]=).with(:ensure, :directory)
file.recurse_link({})
end
it "should create a new child resource for each generated metadata instance's relative path that doesn't already exist in the children hash" do
file.expects(:perform_recursion).returns [@first, @second]
file.expects(:newchild).with(@first.relative_path).returns @resource
file.recurse_link("second" => @resource)
end
it "should not create a new child resource for paths that already exist in the children hash" do
file.expects(:perform_recursion).returns [@first]
file.expects(:newchild).never
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", :if => described_class.defaultprovider.feature?(:manages_symlinks) do
file.stubs(:perform_recursion).returns [@first, @second]
file.recurse_link("first" => @resource, "second" => file)
file[:ensure].should == :link
file[:target].should == "/my/second"
end
it "should :ensure to :directory if the file is a directory" do
file.stubs(:perform_recursion).returns [@first, @second]
file.recurse_link("first" => file, "second" => @resource)
file[:ensure].should == :directory
end
it "should return a hash with both created and existing resources with the relative paths as the hash keys" do
file.expects(:perform_recursion).returns [@first, @second]
file.stubs(:newchild).returns file
file.recurse_link("second" => @resource).should == {"second" => @resource, "first" => file}
end
end
describe "#recurse_local" do
before do
@metadata = stub 'metadata', :relative_path => "my/file"
end
it "should pass its path to the :perform_recursion method" do
file.expects(:perform_recursion).with(file[:path]).returns [@metadata]
file.stubs(:newchild)
file.recurse_local
end
it "should return an empty hash if the recursion returns nothing" do
file.expects(:perform_recursion).returns nil
file.recurse_local.should == {}
end
it "should create a new child resource with each generated metadata instance's relative path" do
file.expects(:perform_recursion).returns [@metadata]
file.expects(:newchild).with(@metadata.relative_path).returns "fiebar"
file.recurse_local
end
it "should not create a new child resource for the '.' directory" do
@metadata.stubs(:relative_path).returns "."
file.expects(:perform_recursion).returns [@metadata]
file.expects(:newchild).never
file.recurse_local
end
it "should return a hash of the created resources with the relative paths as the hash keys" do
file.expects(:perform_recursion).returns [@metadata]
file.expects(:newchild).with("my/file").returns "fiebar"
file.recurse_local.should == {"my/file" => "fiebar"}
end
it "should set checksum_type to none if this file checksum is none" do
file[:checksum] = :none
Puppet::FileServing::Metadata.indirection.expects(:search).with { |path,params| params[:checksum_type] == :none }.returns [@metadata]
file.expects(:newchild).with("my/file").returns "fiebar"
file.recurse_local
end
end
describe "#recurse_remote" do
let(:my) { File.expand_path('/my') }
before do
file[:source] = "puppet://foo/bar"
@first = Puppet::FileServing::Metadata.new(my, :relative_path => "first")
@second = Puppet::FileServing::Metadata.new(my, :relative_path => "second")
@first.stubs(:ftype).returns "directory"
@second.stubs(:ftype).returns "directory"
@parameter = stub 'property', :metadata= => nil
@resource = stub 'file', :[]= => nil, :parameter => @parameter
end
it "should pass its source to the :perform_recursion method" do
data = Puppet::FileServing::Metadata.new(File.expand_path("/whatever"), :relative_path => "foobar")
file.expects(:perform_recursion).with("puppet://foo/bar").returns [data]
file.stubs(:newchild).returns @resource
file.recurse_remote({})
end
it "should not recurse when the remote file is not a directory" do
data = Puppet::FileServing::Metadata.new(File.expand_path("/whatever"), :relative_path => ".")
data.stubs(:ftype).returns "file"
file.expects(:perform_recursion).with("puppet://foo/bar").returns [data]
file.expects(:newchild).never
file.recurse_remote({})
end
it "should set the source of each returned file to the searched-for URI plus the found relative path" do
@first.expects(:source=).with File.join("puppet://foo/bar", @first.relative_path)
file.expects(:perform_recursion).returns [@first]
file.stubs(:newchild).returns @resource
file.recurse_remote({})
end
it "should create a new resource for any relative file paths that do not already have a resource" do
file.stubs(:perform_recursion).returns [@first]
file.expects(:newchild).with("first").returns @resource
file.recurse_remote({}).should == {"first" => @resource}
end
it "should not create a new resource for any relative file paths that do already have a resource" do
file.stubs(:perform_recursion).returns [@first]
file.expects(:newchild).never
file.recurse_remote("first" => @resource)
end
it "should set the source of each resource to the source of the metadata" do
file.stubs(:perform_recursion).returns [@first]
@resource.stubs(:[]=)
@resource.expects(:[]=).with(:source, File.join("puppet://foo/bar", @first.relative_path))
file.recurse_remote("first" => @resource)
end
# LAK:FIXME This is a bug, but I can't think of a fix for it. Fortunately it's already
# filed, and when it's fixed, we'll just fix the whole flow.
it "should set the checksum type to :md5 if the remote file is a file" do
@first.stubs(:ftype).returns "file"
file.stubs(:perform_recursion).returns [@first]
@resource.stubs(:[]=)
@resource.expects(:[]=).with(:checksum, :md5)
file.recurse_remote("first" => @resource)
end
it "should store the metadata in the source property for each resource so the source does not have to requery the metadata" do
file.stubs(:perform_recursion).returns [@first]
@resource.expects(:parameter).with(:source).returns @parameter
@parameter.expects(:metadata=).with(@first)
file.recurse_remote("first" => @resource)
end
it "should not create a new resource for the '.' file" do
@first.stubs(:relative_path).returns "."
file.stubs(:perform_recursion).returns [@first]
file.expects(:newchild).never
file.recurse_remote({})
end
it "should store the metadata in the main file's source property if the relative path is '.'" do
@first.stubs(:relative_path).returns "."
file.stubs(:perform_recursion).returns [@first]
file.parameter(:source).expects(:metadata=).with @first
file.recurse_remote("first" => @resource)
end
describe "and multiple sources are provided" do
let(:sources) do
h = {}
%w{/a /b /c /d}.each do |key|
h[key] = URI.unescape(Puppet::Util.path_to_uri(File.expand_path(key)).to_s)
end
h
end
describe "and :sourceselect is set to :first" do
it "should create file instances for the results for the first source to return any values" do
data = Puppet::FileServing::Metadata.new(File.expand_path("/whatever"), :relative_path => "foobar")
file[:source] = sources.keys.sort.map { |key| File.expand_path(key) }
file.expects(:perform_recursion).with(sources['/a']).returns nil
file.expects(:perform_recursion).with(sources['/b']).returns []
file.expects(:perform_recursion).with(sources['/c']).returns [data]
file.expects(:perform_recursion).with(sources['/d']).never
file.expects(:newchild).with("foobar").returns @resource
file.recurse_remote({})
end
end
describe "and :sourceselect is set to :all" do
before do
file[:sourceselect] = :all
end
it "should return every found file that is not in a previous source" do
klass = Puppet::FileServing::Metadata
file[:source] = abs_path = %w{/a /b /c /d}.map {|f| File.expand_path(f) }
file.stubs(:newchild).returns @resource
one = [klass.new(abs_path[0], :relative_path => "a")]
file.expects(:perform_recursion).with(sources['/a']).returns one
file.expects(:newchild).with("a").returns @resource
two = [klass.new(abs_path[1], :relative_path => "a"), klass.new(abs_path[1], :relative_path => "b")]
file.expects(:perform_recursion).with(sources['/b']).returns two
file.expects(:newchild).with("b").returns @resource
three = [klass.new(abs_path[2], :relative_path => "a"), klass.new(abs_path[2], :relative_path => "c")]
file.expects(:perform_recursion).with(sources['/c']).returns three
file.expects(:newchild).with("c").returns @resource
file.expects(:perform_recursion).with(sources['/d']).returns []
file.recurse_remote({})
end
end
end
end
describe "#perform_recursion" do
it "should use Metadata to do its recursion" do
Puppet::FileServing::Metadata.indirection.expects(:search)
file.perform_recursion(file[:path])
end
it "should use the provided path as the key to the search" do
Puppet::FileServing::Metadata.indirection.expects(:search).with { |key, options| key == "/foo" }
file.perform_recursion("/foo")
end
it "should return the results of the metadata search" do
Puppet::FileServing::Metadata.indirection.expects(:search).returns "foobar"
file.perform_recursion(file[:path]).should == "foobar"
end
it "should pass its recursion value to the search" do
file[:recurse] = true
Puppet::FileServing::Metadata.indirection.expects(:search).with { |key, options| options[:recurse] == true }
file.perform_recursion(file[:path])
end
it "should pass true if recursion is remote" do
file[:recurse] = :remote
Puppet::FileServing::Metadata.indirection.expects(:search).with { |key, options| options[:recurse] == true }
file.perform_recursion(file[:path])
end
it "should pass its recursion limit value to the search" do
file[:recurselimit] = 10
Puppet::FileServing::Metadata.indirection.expects(:search).with { |key, options| options[:recurselimit] == 10 }
file.perform_recursion(file[:path])
end
it "should configure the search to ignore or manage links" do
file[:links] = :manage
Puppet::FileServing::Metadata.indirection.expects(:search).with { |key, options| options[:links] == :manage }
file.perform_recursion(file[:path])
end
it "should pass its 'ignore' setting to the search if it has one" do
file[:ignore] = %w{.svn CVS}
Puppet::FileServing::Metadata.indirection.expects(:search).with { |key, options| options[:ignore] == %w{.svn CVS} }
file.perform_recursion(file[:path])
end
end
describe "#remove_existing" do
it "should do nothing if the file doesn't exist" do
file.remove_existing(:file).should == false
end
it "should fail if it can't backup the file" do
file.stubs(:stat).returns stub('stat', :ftype => 'file')
file.stubs(:perform_backup).returns false
expect { file.remove_existing(:file) }.to raise_error(Puppet::Error, /Could not back up; will not replace/)
end
describe "backing up directories" do
it "should not backup directories if force is false" do
file[:force] = false
file.stubs(:stat).returns stub('stat', :ftype => 'directory')
file.expects(:perform_backup).never
file.remove_existing(:file).should == false
end
it "should backup directories if force is true" do
file[:force] = true
FileUtils.expects(:rmtree).with(file[:path])
file.stubs(:stat).returns stub('stat', :ftype => 'directory')
file.expects(:perform_backup).once.returns(true)
file.remove_existing(:file).should == true
end
end
it "should not do anything if the file is already the right type and not a link" do
file.stubs(:stat).returns stub('stat', :ftype => 'file')
file.remove_existing(:file).should == false
end
it "should not remove directories and should not invalidate the stat unless force is set" do
# Actually call stat to set @needs_stat to nil
file.stat
file.stubs(:stat).returns stub('stat', :ftype => 'directory')
file.remove_existing(:file)
file.instance_variable_get(:@stat).should == nil
@logs.should be_any {|log| log.level == :notice and log.message =~ /Not removing directory; use 'force' to override/}
end
it "should remove a directory if force is set" do
file[:force] = true
file.stubs(:stat).returns stub('stat', :ftype => 'directory')
FileUtils.expects(:rmtree).with(file[:path])
file.remove_existing(:file).should == true
end
it "should remove an existing file" do
file.stubs(:perform_backup).returns true
FileUtils.touch(path)
file.remove_existing(:directory).should == true
- Puppet::FileSystem::File.exist?(file[:path]).should == false
+ Puppet::FileSystem.exist?(file[:path]).should == false
end
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)
- Puppet::FileSystem::File.new(target).symlink(path)
+ Puppet::FileSystem.symlink(target, path)
file[:target] = target
file.remove_existing(:directory).should == true
- Puppet::FileSystem::File.exist?(file[:path]).should == false
+ Puppet::FileSystem.exist?(file[:path]).should == false
end
it "should fail if the file is not a file, link, or directory" do
file.stubs(:stat).returns stub('stat', :ftype => 'socket')
expect { file.remove_existing(:file) }.to raise_error(Puppet::Error, /Could not back up files of type socket/)
end
it "should invalidate the existing stat of the file" do
# Actually call stat to set @needs_stat to nil
file.stat
file.stubs(:stat).returns stub('stat', :ftype => 'file')
- Puppet::FileSystem::File.stubs(:unlink)
+ Puppet::FileSystem.stubs(:unlink)
file.remove_existing(:directory).should == true
file.instance_variable_get(:@stat).should == :needs_stat
end
end
describe "#retrieve" do
it "should copy the source values if the 'source' parameter is set" do
file[:source] = File.expand_path('/foo/bar')
file.parameter(:source).expects(:copy_source_values)
file.retrieve
end
end
describe "#should_be_file?" do
it "should have a method for determining if the file should be a normal file" do
file.must respond_to(:should_be_file?)
end
it "should be a file if :ensure is set to :file" do
file[:ensure] = :file
file.must be_should_be_file
end
it "should be a file if :ensure is set to :present and the file exists as a normal file" do
file.stubs(:stat).returns(mock('stat', :ftype => "file"))
file[:ensure] = :present
file.must be_should_be_file
end
it "should not be a file if :ensure is set to something other than :file" do
file[:ensure] = :directory
file.must_not be_should_be_file
end
it "should not be a file if :ensure is set to :present and the file exists but is not a normal file" do
file.stubs(:stat).returns(mock('stat', :ftype => "directory"))
file[:ensure] = :present
file.must_not be_should_be_file
end
it "should be a file if :ensure is not set and :content is" do
file[:content] = "foo"
file.must be_should_be_file
end
it "should be a file if neither :ensure nor :content is set but the file exists as a normal file" do
file.stubs(:stat).returns(mock("stat", :ftype => "file"))
file.must be_should_be_file
end
it "should not be a file if neither :ensure nor :content is set but the file exists but not as a normal file" do
file.stubs(:stat).returns(mock("stat", :ftype => "directory"))
file.must_not be_should_be_file
end
end
describe "#stat", :if => described_class.defaultprovider.feature?(:manages_symlinks) do
before do
target = tmpfile('link_target')
FileUtils.touch(target)
- Puppet::FileSystem::File.new(target).symlink(path)
+ Puppet::FileSystem.symlink(target, path)
file[:target] = target
file[:links] = :manage # so we always use :lstat
end
it "should stat the target if it is following links" do
file[:links] = :follow
file.stat.ftype.should == 'file'
end
it "should stat the link if is it not following links" do
file[:links] = :manage
file.stat.ftype.should == 'link'
end
it "should return nil if the file does not exist" do
file[:path] = make_absolute('/foo/bar/baz/non-existent')
file.stat.should be_nil
end
it "should return nil if the file cannot be stat'ed" do
dir = tmpfile('link_test_dir')
child = File.join(dir, 'some_file')
Dir.mkdir(dir)
File.chmod(0, dir)
file[:path] = child
file.stat.should be_nil
# chmod it back so we can clean it up
File.chmod(0777, dir)
end
it "should return nil if parts of path are no directories" do
regular_file = tmpfile('ENOTDIR_test')
FileUtils.touch(regular_file)
impossible_child = File.join(regular_file, 'some_file')
file[:path] = impossible_child
file.stat.should be_nil
end
it "should return the stat instance" do
file.stat.should be_a(File::Stat)
end
it "should cache the stat instance" do
file.stat.should equal(file.stat)
end
end
describe "#write" do
describe "when validating the checksum" do
before { file.stubs(:validate_checksum?).returns(true) }
it "should fail if the checksum parameter and content checksums do not match" do
checksum = stub('checksum_parameter', :sum => 'checksum_b', :sum_file => 'checksum_b')
file.stubs(:parameter).with(:checksum).returns(checksum)
property = stub('content_property', :actual_content => "something", :length => "something".length, :write => 'checksum_a')
file.stubs(:property).with(:content).returns(property)
expect { file.write :NOTUSED }.to raise_error(Puppet::Error)
end
end
describe "when not validating the checksum" do
before { file.stubs(:validate_checksum?).returns(false) }
it "should not fail if the checksum property and content checksums do not match" do
checksum = stub('checksum_parameter', :sum => 'checksum_b')
file.stubs(:parameter).with(:checksum).returns(checksum)
property = stub('content_property', :actual_content => "something", :length => "something".length, :write => 'checksum_a')
file.stubs(:property).with(:content).returns(property)
expect { file.write :NOTUSED }.to_not raise_error
end
end
describe "when resource mode is supplied" do
before { file.stubs(:property_fix) }
context "and writing temporary files" do
before { file.stubs(:write_temporary_file?).returns(true) }
it "should convert symbolic mode to int" do
file[:mode] = 'oga=r'
Puppet::Util.expects(:replace_file).with(file[:path], 0444)
file.write :NOTUSED
end
it "should support int modes" do
file[:mode] = '0444'
Puppet::Util.expects(:replace_file).with(file[:path], 0444)
file.write :NOTUSED
end
end
context "and not writing temporary files" do
before { file.stubs(:write_temporary_file?).returns(false) }
it "should set a umask of 0" do
file[:mode] = 'oga=r'
Puppet::Util.expects(:withumask).with(0)
file.write :NOTUSED
end
it "should convert symbolic mode to int" do
file[:mode] = 'oga=r'
File.expects(:open).with(file[:path], anything, 0444)
file.write :NOTUSED
end
it "should support int modes" do
file[:mode] = '0444'
File.expects(:open).with(file[:path], anything, 0444)
file.write :NOTUSED
end
end
end
describe "when resource mode is not supplied" do
context "and content is supplied" do
it "should default to 0644 mode" do
file = described_class.new(:path => path, :content => "file content")
file.write :NOTUSED
expect(File.stat(file[:path]).mode & 0777).to eq(0644)
end
end
context "and no content is supplied" do
it "should use puppet's default umask of 022" do
file = described_class.new(:path => path)
umask_from_the_user = 0777
Puppet::Util.withumask(umask_from_the_user) do
file.write :NOTUSED
end
expect(File.stat(file[:path]).mode & 0777).to eq(0644)
end
end
end
end
describe "#fail_if_checksum_is_wrong" do
it "should fail if the checksum of the file doesn't match the expected one" do
expect do
file.instance_eval do
parameter(:checksum).stubs(:sum_file).returns('wrong!!')
fail_if_checksum_is_wrong(self[:path], 'anything!')
end
end.to raise_error(Puppet::Error, /File written to disk did not match checksum/)
end
it "should not fail if the checksum is correct" do
file.instance_eval do
parameter(:checksum).stubs(:sum_file).returns('anything!')
fail_if_checksum_is_wrong(self[:path], 'anything!').should == nil
end
end
it "should not fail if the checksum is absent" do
file.instance_eval do
parameter(:checksum).stubs(:sum_file).returns(nil)
fail_if_checksum_is_wrong(self[:path], 'anything!').should == nil
end
end
end
describe "#write_content" do
it "should delegate writing the file to the content property" do
io = stub('io')
file[:content] = "some content here"
file.property(:content).expects(:write).with(io)
file.send(:write_content, io)
end
end
describe "#write_temporary_file?" do
it "should be true if the file has specified content" do
file[:content] = 'some content'
file.send(:write_temporary_file?).should be_true
end
it "should be true if the file has specified source" do
file[:source] = File.expand_path('/tmp/foo')
file.send(:write_temporary_file?).should be_true
end
it "should be false if the file has neither content nor source" do
file.send(:write_temporary_file?).should be_false
end
end
describe "#property_fix" do
{
:mode => 0777,
:owner => 'joeuser',
:group => 'joeusers',
:seluser => 'seluser',
:selrole => 'selrole',
:seltype => 'seltype',
:selrange => 'selrange'
}.each do |name,value|
it "should sync the #{name} property if it's not in sync" do
file[name] = value
prop = file.property(name)
prop.expects(:retrieve)
prop.expects(:safe_insync?).returns false
prop.expects(:sync)
file.send(:property_fix)
end
end
end
describe "when autorequiring" do
describe "target" 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
catalog.add_resource link
reqs = link.autorequire
reqs.size.must == 1
reqs[0].source.must == file
reqs[0].target.must == link
end
it "should require file resource when specified with the ensure property" do
file = described_class.new(:path => File.expand_path("/foo"), :ensure => :directory)
link = described_class.new(:path => File.expand_path("/bar"), :ensure => File.expand_path("/foo"))
catalog.add_resource file
catalog.add_resource link
reqs = link.autorequire
reqs.size.must == 1
reqs[0].source.must == file
reqs[0].target.must == link
end
it "should not require target if target is not managed", :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
end
end
describe "directories" do
it "should autorequire its parent directory" do
dir = described_class.new(:path => File.dirname(path))
catalog.add_resource file
catalog.add_resource dir
reqs = file.autorequire
reqs[0].source.must == dir
reqs[0].target.must == file
end
it "should autorequire its nearest ancestor directory" do
dir = described_class.new(:path => File.dirname(path))
grandparent = described_class.new(:path => File.dirname(File.dirname(path)))
catalog.add_resource file
catalog.add_resource dir
catalog.add_resource grandparent
reqs = file.autorequire
reqs.length.must == 1
reqs[0].source.must == dir
reqs[0].target.must == file
end
it "should not autorequire anything when there is no nearest ancestor directory" do
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[:path] = File.expand_path('/')
catalog.add_resource file
file.autorequire.should be_empty
end
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] = '//localhost/foo/bar/baz'
dir = described_class.new(:path => "//localhost/foo/bar")
catalog.add_resource file
catalog.add_resource dir
reqs = file.autorequire
reqs[0].source.must == dir
reqs[0].target.must == file
end
it "should autorequire its nearest ancestor directory" do
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
reqs = file.autorequire
reqs.length.must == 1
reqs[0].source.must == dir
reqs[0].target.must == file
end
it "should not autorequire anything when there is no nearest ancestor directory" do
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 => "//localhost/foo")
catalog.add_resource file
puts file.autorequire
file.autorequire.should be_empty
end
end
end
end
end
describe "when managing links", :if => Puppet.features.manages_symlinks? do
require 'tempfile'
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
# to prevent the catalog from trying to write state.yaml
Puppet::Util::Storage.stubs(:store)
end
it "should preserve the original file mode and ignore the one set by the link" do
@link_resource[:links] = :manage # default
catalog.apply
# 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'
+ (Puppet::FileSystem.stat(@target).mode & 007777).to_s(8).should == '644'
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
- (Puppet::FileSystem::File.new(@target).stat.mode & 007777).to_s(8).should == '755'
+ (Puppet::FileSystem.stat(@target).mode & 007777).to_s(8).should == '755'
end
end
end
describe "when using source" do
before do
file[:source] = File.expand_path('/one')
end
Puppet::Type::File::ParameterChecksum.value_collection.values.reject {|v| v == :none}.each do |checksum_type|
describe "with checksum '#{checksum_type}'" do
before do
file[:checksum] = checksum_type
end
it 'should validate' do
expect { file.validate }.to_not raise_error
end
end
end
describe "with checksum 'none'" do
before do
file[:checksum] = :none
end
it 'should raise an exception when validating' do
expect { file.validate }.to raise_error(/You cannot specify source when using checksum 'none'/)
end
end
end
describe "when using content" do
before do
file[:content] = 'file contents'
end
(Puppet::Type::File::ParameterChecksum.value_collection.values - SOURCE_ONLY_CHECKSUMS).each do |checksum_type|
describe "with checksum '#{checksum_type}'" do
before do
file[:checksum] = checksum_type
end
it 'should validate' do
expect { file.validate }.to_not raise_error
end
end
end
SOURCE_ONLY_CHECKSUMS.each do |checksum_type|
describe "with checksum '#{checksum_type}'" do
it 'should raise an exception when validating' do
file[:checksum] = checksum_type
expect { file.validate }.to raise_error(/You cannot specify content when using checksum '#{checksum_type}'/)
end
end
end
end
describe "when auditing" do
before :each do
# to prevent the catalog from trying to write state.yaml
Puppet::Util::Storage.stubs(:store)
end
it "should not fail if creating a new file if group is not set" do
file = described_class.new(:path => path, :audit => 'all', :content => 'content')
catalog.add_resource(file)
report = catalog.apply.report
report.resource_statuses["File[#{path}]"].should_not be_failed
File.read(path).should == 'content'
end
it "should not log errors if creating a new file with ensure present and no content" do
file[:audit] = 'content'
file[:ensure] = 'present'
catalog.add_resource(file)
catalog.apply
- Puppet::FileSystem::File.exist?(path).should be_true
+ Puppet::FileSystem.exist?(path).should be_true
@logs.should_not be_any {|l| l.level != :notice }
end
end
describe "when specifying both source and checksum" do
it 'should use the specified checksum when source is first' do
file[:source] = File.expand_path('/foo')
file[:checksum] = :md5lite
file[:checksum].should == :md5lite
end
it 'should use the specified checksum when source is last' do
file[:checksum] = :md5lite
file[:source] = File.expand_path('/foo')
file[:checksum].should == :md5lite
end
end
describe "when validating" do
[[:source, :target], [:source, :content], [:target, :content]].each do |prop1,prop2|
it "should fail if both #{prop1} and #{prop2} are specified" do
file[prop1] = prop1 == :source ? File.expand_path("prop1 value") : "prop1 value"
file[prop2] = "prop2 value"
expect do
file.validate
end.to raise_error(Puppet::Error, /You cannot specify more than one of/)
end
end
end
end
diff --git a/spec/unit/type/k5login_spec.rb b/spec/unit/type/k5login_spec.rb
index 484ddf8e7..6c0dbb16d 100755
--- a/spec/unit/type/k5login_spec.rb
+++ b/spec/unit/type/k5login_spec.rb
@@ -1,115 +1,115 @@
#!/usr/bin/env ruby
require 'spec_helper'
require 'fileutils'
require 'puppet/type'
describe Puppet::Type.type(:k5login), :unless => Puppet.features.microsoft_windows? do
include PuppetSpec::Files
context "the type class" do
subject { described_class }
it { should be_validattr :ensure }
it { should be_validattr :path }
it { should be_validattr :principals }
it { should be_validattr :mode }
# We have one, inline provider implemented.
it { should be_validattr :provider }
end
let(:path) { tmpfile('k5login') }
def resource(attrs = {})
attrs = {
:ensure => 'present',
:path => path,
:principals => 'fred@EXAMPLE.COM'
}.merge(attrs)
if content = attrs.delete(:content)
File.open(path, 'w') { |f| f.print(content) }
end
resource = described_class.new(attrs)
resource
end
before :each do
FileUtils.touch(path)
end
context "the provider" do
context "when the file is missing" do
it "should initially be absent" do
File.delete(path)
resource.retrieve[:ensure].must == :absent
end
it "should create the file when synced" do
resource(:ensure => 'present').parameter(:ensure).sync
- Puppet::FileSystem::File.exist?(path).should be_true
+ Puppet::FileSystem.exist?(path).should be_true
end
end
context "when the file is present" do
context "retrieved initial state" do
subject { resource.retrieve }
it "should retrieve its properties correctly with zero principals" do
subject[:ensure].should == :present
subject[:principals].should == []
# We don't really care what the mode is, just that it got it
subject[:mode].should_not be_nil
end
context "with one principal" do
subject { resource(:content => "daniel@EXAMPLE.COM\n").retrieve }
it "should retrieve its principals correctly" do
subject[:principals].should == ["daniel@EXAMPLE.COM"]
end
end
context "with two principals" do
subject do
content = ["daniel@EXAMPLE.COM", "george@EXAMPLE.COM"].join("\n")
resource(:content => content).retrieve
end
it "should retrieve its principals correctly" do
subject[:principals].should == ["daniel@EXAMPLE.COM", "george@EXAMPLE.COM"]
end
end
end
it "should remove the file ensure is absent" do
resource(:ensure => 'absent').property(:ensure).sync
- Puppet::FileSystem::File.exist?(path).should be_false
+ Puppet::FileSystem.exist?(path).should be_false
end
it "should write one principal to the file" do
File.read(path).should == ""
resource(:principals => ["daniel@EXAMPLE.COM"]).property(:principals).sync
File.read(path).should == "daniel@EXAMPLE.COM\n"
end
it "should write multiple principals to the file" do
content = ["daniel@EXAMPLE.COM", "george@EXAMPLE.COM"]
File.read(path).should == ""
resource(:principals => content).property(:principals).sync
File.read(path).should == content.join("\n") + "\n"
end
describe "when setting the mode" do
# The defined input type is "mode, as an octal string"
["400", "600", "700", "644", "664"].each do |mode|
it "should update the mode to #{mode}" do
resource(:mode => mode).property(:mode).sync
- (Puppet::FileSystem::File.new(path).stat.mode & 07777).to_s(8).should == mode
+ (Puppet::FileSystem.stat(path).mode & 07777).to_s(8).should == mode
end
end
end
end
end
end
diff --git a/spec/unit/type/nagios_spec.rb b/spec/unit/type/nagios_spec.rb
index 84d4338de..bc96c26d4 100755
--- a/spec/unit/type/nagios_spec.rb
+++ b/spec/unit/type/nagios_spec.rb
@@ -1,278 +1,284 @@
#! /usr/bin/env ruby
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}")
it "should have a valid type for #{name}" do
puppet_type.should_not be_nil
end
next unless puppet_type
describe puppet_type do
it "should be defined as a Puppet resource type" do
puppet_type.should_not be_nil
end
it "should have documentation" do
puppet_type.instance_variable_get("@doc").should_not == ""
end
it "should have #{nagios_type.namevar} as its key attribute" do
puppet_type.key_attributes.should == [nagios_type.namevar]
end
it "should have documentation for its #{nagios_type.namevar} parameter" do
puppet_type.attrclass(nagios_type.namevar).instance_variable_get("@doc").should_not be_nil
end
it "should have an ensure property" do
puppet_type.should be_validproperty(:ensure)
end
it "should have a target property" do
puppet_type.should be_validproperty(:target)
end
it "should have documentation for its target property" do
puppet_type.attrclass(:target).instance_variable_get("@doc").should_not be_nil
end
+ [ :owner, :group, :mode ].each do |fileprop|
+ it "should have a #{fileprop} parameter" do
+ puppet_type.parameters.should be_include(fileprop)
+ end
+ end
+
nagios_type.parameters.reject { |param| param == nagios_type.namevar or param.to_s =~ /^[0-9]/ }.each do |param|
it "should have a #{param} property" do
puppet_type.should be_validproperty(param)
end
it "should have documentation for its #{param} property" do
puppet_type.attrclass(param).instance_variable_get("@doc").should_not be_nil
end
end
nagios_type.parameters.find_all { |param| param.to_s =~ /^[0-9]/ }.each do |param|
it "should have not have a #{param} property" do
puppet_type.should_not be_validproperty(:param)
end
end
end
end
end
diff --git a/spec/unit/type/package/package_settings_spec.rb b/spec/unit/type/package/package_settings_spec.rb
new file mode 100755
index 000000000..18e7a4b7e
--- /dev/null
+++ b/spec/unit/type/package/package_settings_spec.rb
@@ -0,0 +1,135 @@
+#! /usr/bin/env ruby
+require 'spec_helper'
+
+describe Puppet::Type.type(:package) do
+
+ before do
+ Puppet::Util::Storage.stubs(:store)
+ end
+
+ it "should have a :package_settings feature that requires :package_settings_insync?, :package_settings and :package_settings=" do
+ described_class.provider_feature(:package_settings).methods.should == [:package_settings_insync?, :package_settings, :package_settings=]
+ end
+
+ context "when validating attributes" do
+ it "should have a package_settings property" do
+ described_class.attrtype(:package_settings).should == :property
+ end
+ end
+
+ context "when validating attribute values" do
+ let(:provider) do
+ stub( 'provider',
+ :class => described_class.defaultprovider,
+ :clear => nil,
+ :validate_source => false )
+ end
+ before do
+ provider.class.stubs(:supports_parameter?).returns(true)
+ described_class.defaultprovider.stubs(:new).returns(provider)
+ end
+ describe 'package_settings' do
+ context "with a minimalistic provider supporting package_settings" do
+ context "and {:package_settings => :settings}" do
+ let(:resource) do
+ described_class.new :name => 'foo', :package_settings => :settings
+ end
+ it { expect { resource }.to_not raise_error }
+ it "should set package_settings to :settings" do
+ resource.value(:package_settings).should be :settings
+ end
+ end
+ end
+ context "with a provider that supports validation of the package_settings" do
+ context "and {:package_settings => :valid_value}" do
+ before do
+ provider.expects(:package_settings_validate).once.with(:valid_value).returns(true)
+ end
+ let(:resource) do
+ described_class.new :name => 'foo', :package_settings => :valid_value
+ end
+ it { expect { resource }.to_not raise_error }
+ it "should set package_settings to :valid_value" do
+ resource.value(:package_settings).should == :valid_value
+ end
+ end
+ context "and {:package_settings => :invalid_value}" do
+ before do
+ msg = "package_settings must be a Hash, not Symbol"
+ provider.expects(:package_settings_validate).once.
+ with(:invalid_value).raises(ArgumentError, msg)
+ end
+ let(:resource) do
+ described_class.new :name => 'foo', :package_settings => :invalid_value
+ end
+ it do
+ expect { resource }.to raise_error Puppet::Error,
+ /package_settings must be a Hash, not Symbol/
+ end
+ end
+ end
+ context "with a provider that supports munging of the package_settings" do
+ context "and {:package_settings => 'A'}" do
+ before do
+ provider.expects(:package_settings_munge).once.with('A').returns(:a)
+ end
+ let(:resource) do
+ described_class.new :name => 'foo', :package_settings => 'A'
+ end
+ it do
+ expect { resource }.to_not raise_error
+ end
+ it "should set package_settings to :a" do
+ resource.value(:package_settings).should be :a
+ end
+ end
+ end
+ end
+ end
+ describe "package_settings property" do
+ let(:provider) do
+ stub( 'provider',
+ :class => described_class.defaultprovider,
+ :clear => nil,
+ :validate_source => false )
+ end
+ before do
+ provider.class.stubs(:supports_parameter?).returns(true)
+ described_class.defaultprovider.stubs(:new).returns(provider)
+ end
+ context "with {package_settings => :should}" do
+ let(:resource) do
+ described_class.new :name => 'foo', :package_settings => :should
+ end
+ describe "#insync?(:is)" do
+ it "returns the result of provider.package_settings_insync?(:should,:is)" do
+ resource.provider.expects(:package_settings_insync?).once.with(:should,:is).returns :ok1
+ resource.property(:package_settings).insync?(:is).should be :ok1
+ end
+ end
+ describe "#should_to_s(:newvalue)" do
+ it "returns the result of provider.package_settings_should_to_s(:should,:newvalue)" do
+ resource.provider.expects(:package_settings_should_to_s).once.with(:should,:newvalue).returns :ok2
+ resource.property(:package_settings).should_to_s(:newvalue).should be :ok2
+ end
+ end
+ describe "#is_to_s(:currentvalue)" do
+ it "returns the result of provider.package_settings_is_to_s(:should,:currentvalue)" do
+ resource.provider.expects(:package_settings_is_to_s).once.with(:should,:currentvalue).returns :ok3
+ resource.property(:package_settings).is_to_s(:currentvalue).should be :ok3
+ end
+ end
+ end
+ context "with any non-nil package_settings" do
+ describe "#change_to_s(:currentvalue,:newvalue)" do
+ let(:resource) do
+ described_class.new :name => 'foo', :package_settings => {}
+ end
+ it "returns the result of provider.package_settings_change_to_s(:currentvalue,:newvalue)" do
+ resource.provider.expects(:package_settings_change_to_s).once.with(:currentvalue,:newvalue).returns :ok4
+ resource.property(:package_settings).change_to_s(:currentvalue,:newvalue).should be :ok4
+ end
+ end
+ end
+ end
+end
diff --git a/spec/unit/type/package_spec.rb b/spec/unit/type/package_spec.rb
index fefd15805..3e9ceb8c6 100755
--- a/spec/unit/type/package_spec.rb
+++ b/spec/unit/type/package_spec.rb
@@ -1,292 +1,304 @@
#! /usr/bin/env ruby
require 'spec_helper'
describe Puppet::Type.type(:package) do
before do
Puppet::Util::Storage.stubs(:store)
end
it "should have an :installable feature that requires the :install method" do
Puppet::Type.type(:package).provider_feature(:installable).methods.should == [:install]
end
it "should have an :uninstallable feature that requires the :uninstall method" do
Puppet::Type.type(:package).provider_feature(:uninstallable).methods.should == [:uninstall]
end
it "should have an :upgradeable feature that requires :update and :latest methods" do
Puppet::Type.type(:package).provider_feature(:upgradeable).methods.should == [:update, :latest]
end
it "should have a :purgeable feature that requires the :purge latest method" do
Puppet::Type.type(:package).provider_feature(:purgeable).methods.should == [:purge]
end
it "should have a :versionable feature" do
Puppet::Type.type(:package).provider_feature(:versionable).should_not be_nil
end
+ it "should have a :package_settings feature that requires :package_settings_insync?, :package_settings and :package_settings=" do
+ Puppet::Type.type(:package).provider_feature(:package_settings).methods.should == [:package_settings_insync?, :package_settings, :package_settings=]
+ end
+
it "should default to being installed" do
pkg = Puppet::Type.type(:package).new(:name => "yay", :provider => :apt)
pkg.should(:ensure).should == :present
end
describe "when validating attributes" do
[:name, :source, :instance, :status, :adminfile, :responsefile, :configfiles, :category, :platform, :root, :vendor, :description, :allowcdrom].each do |param|
it "should have a #{param} parameter" do
Puppet::Type.type(:package).attrtype(param).should == :param
end
end
it "should have an ensure property" do
Puppet::Type.type(:package).attrtype(:ensure).should == :property
end
+
+ it "should have a package_settings property" do
+ Puppet::Type.type(:package).attrtype(:package_settings).should == :property
+ end
end
describe "when validating attribute values" do
- before do
+ before :each do
@provider = stub(
'provider',
:class => Puppet::Type.type(:package).defaultprovider,
:clear => nil,
:validate_source => nil
)
Puppet::Type.type(:package).defaultprovider.stubs(:new).returns(@provider)
end
+ after :each do
+ Puppet::Type.type(:package).defaultprovider = nil
+ end
+
it "should support :present as a value to :ensure" do
Puppet::Type.type(:package).new(:name => "yay", :ensure => :present)
end
it "should alias :installed to :present as a value to :ensure" do
pkg = Puppet::Type.type(:package).new(:name => "yay", :ensure => :installed)
pkg.should(:ensure).should == :present
end
it "should support :absent as a value to :ensure" do
Puppet::Type.type(:package).new(:name => "yay", :ensure => :absent)
end
it "should support :purged as a value to :ensure if the provider has the :purgeable feature" do
@provider.expects(:satisfies?).with([:purgeable]).returns(true)
Puppet::Type.type(:package).new(:name => "yay", :ensure => :purged)
end
it "should not support :purged as a value to :ensure if the provider does not have the :purgeable feature" do
@provider.expects(:satisfies?).with([:purgeable]).returns(false)
expect { Puppet::Type.type(:package).new(:name => "yay", :ensure => :purged) }.to raise_error(Puppet::Error)
end
it "should support :latest as a value to :ensure if the provider has the :upgradeable feature" do
@provider.expects(:satisfies?).with([:upgradeable]).returns(true)
Puppet::Type.type(:package).new(:name => "yay", :ensure => :latest)
end
it "should not support :latest as a value to :ensure if the provider does not have the :upgradeable feature" do
@provider.expects(:satisfies?).with([:upgradeable]).returns(false)
expect { Puppet::Type.type(:package).new(:name => "yay", :ensure => :latest) }.to raise_error(Puppet::Error)
end
it "should support version numbers as a value to :ensure if the provider has the :versionable feature" do
@provider.expects(:satisfies?).with([:versionable]).returns(true)
Puppet::Type.type(:package).new(:name => "yay", :ensure => "1.0")
end
it "should not support version numbers as a value to :ensure if the provider does not have the :versionable feature" do
@provider.expects(:satisfies?).with([:versionable]).returns(false)
expect { Puppet::Type.type(:package).new(:name => "yay", :ensure => "1.0") }.to raise_error(Puppet::Error)
end
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
def setprops(properties)
@provider.stubs(:properties).returns(properties)
end
end
describe Puppet::Type.type(:package) do
before :each do
@provider = stub(
'provider',
:class => Puppet::Type.type(:package).defaultprovider,
:clear => nil,
:satisfies? => true,
:name => :mock,
:validate_source => nil
)
Puppet::Type.type(:package).defaultprovider.stubs(:new).returns(@provider)
Puppet::Type.type(:package).defaultprovider.stubs(:instances).returns([])
@package = Puppet::Type.type(:package).new(:name => "yay")
@catalog = Puppet::Resource::Catalog.new
@catalog.add_resource(@package)
end
describe Puppet::Type.type(:package), "when it should be purged" do
include PackageEvaluationTesting
before { @package[:ensure] = :purged }
it "should do nothing if it is :purged" do
@provider.expects(:properties).returns(:ensure => :purged).at_least_once
@catalog.apply
end
[:absent, :installed, :present, :latest].each do |state|
it "should purge if it is #{state.to_s}" do
@provider.stubs(:properties).returns(:ensure => state)
@provider.expects(:purge)
@catalog.apply
end
end
end
describe Puppet::Type.type(:package), "when it should be absent" do
include PackageEvaluationTesting
before { @package[:ensure] = :absent }
[:purged, :absent].each do |state|
it "should do nothing if it is #{state.to_s}" do
@provider.expects(:properties).returns(:ensure => state).at_least_once
@catalog.apply
end
end
[:installed, :present, :latest].each do |state|
it "should uninstall if it is #{state.to_s}" do
@provider.stubs(:properties).returns(:ensure => state)
@provider.expects(:uninstall)
@catalog.apply
end
end
end
describe Puppet::Type.type(:package), "when it should be present" do
include PackageEvaluationTesting
before { @package[:ensure] = :present }
[:present, :latest, "1.0"].each do |state|
it "should do nothing if it is #{state.to_s}" do
@provider.expects(:properties).returns(:ensure => state).at_least_once
@catalog.apply
end
end
[:purged, :absent].each do |state|
it "should install if it is #{state.to_s}" do
@provider.stubs(:properties).returns(:ensure => state)
@provider.expects(:install)
@catalog.apply
end
end
end
describe Puppet::Type.type(:package), "when it should be latest" do
include PackageEvaluationTesting
before { @package[:ensure] = :latest }
[:purged, :absent].each do |state|
it "should upgrade if it is #{state.to_s}" do
@provider.stubs(:properties).returns(:ensure => state)
@provider.expects(:update)
@catalog.apply
end
end
it "should upgrade if the current version is not equal to the latest version" do
@provider.stubs(:properties).returns(:ensure => "1.0")
@provider.stubs(:latest).returns("2.0")
@provider.expects(:update)
@catalog.apply
end
it "should do nothing if it is equal to the latest version" do
@provider.stubs(:properties).returns(:ensure => "1.0")
@provider.stubs(:latest).returns("1.0")
@provider.expects(:update).never
@catalog.apply
end
it "should do nothing if the provider returns :present as the latest version" do
@provider.stubs(:properties).returns(:ensure => :present)
@provider.stubs(:latest).returns("1.0")
@provider.expects(:update).never
@catalog.apply
end
end
describe Puppet::Type.type(:package), "when it should be a specific version" do
include PackageEvaluationTesting
before { @package[:ensure] = "1.0" }
[:purged, :absent].each do |state|
it "should install if it is #{state.to_s}" do
@provider.stubs(:properties).returns(:ensure => state)
@package.property(:ensure).insync?(state).should be_false
@provider.expects(:install)
@catalog.apply
end
end
it "should do nothing if the current version is equal to the desired version" do
@provider.stubs(:properties).returns(:ensure => "1.0")
@package.property(:ensure).insync?('1.0').should be_true
@provider.expects(:install).never
@catalog.apply
end
it "should install if the current version is not equal to the specified version" do
@provider.stubs(:properties).returns(:ensure => "2.0")
@package.property(:ensure).insync?('2.0').should be_false
@provider.expects(:install)
@catalog.apply
end
describe "when current value is an array" do
let(:installed_versions) { ["1.0", "2.0", "3.0"] }
before (:each) do
@provider.stubs(:properties).returns(:ensure => installed_versions)
end
it "should install if value not in the array" do
@package[:ensure] = "1.5"
@package.property(:ensure).insync?(installed_versions).should be_false
@provider.expects(:install)
@catalog.apply
end
it "should not install if value is in the array" do
@package[:ensure] = "2.0"
@package.property(:ensure).insync?(installed_versions).should be_true
@provider.expects(:install).never
@catalog.apply
end
describe "when ensure is set to 'latest'" do
it "should not install if the value is in the array" do
@provider.expects(:latest).returns("3.0")
@package[:ensure] = "latest"
@package.property(:ensure).insync?(installed_versions).should be_true
@provider.expects(:install).never
@catalog.apply
end
end
end
end
end
end
diff --git a/spec/unit/type/resources_spec.rb b/spec/unit/type/resources_spec.rb
index 30b60edf4..f08afd7ae 100755
--- a/spec/unit/type/resources_spec.rb
+++ b/spec/unit/type/resources_spec.rb
@@ -1,129 +1,284 @@
#! /usr/bin/env ruby
require 'spec_helper'
resources = Puppet::Type.type(:resources)
# There are still plenty of tests to port over from test/.
describe resources do
describe "when initializing" do
it "should fail if the specified resource type does not exist" do
Puppet::Type.stubs(:type).with { |x| x.to_s.downcase == "resources"}.returns resources
Puppet::Type.expects(:type).with("nosuchtype").returns nil
lambda { resources.new :name => "nosuchtype" }.should raise_error(Puppet::Error)
end
it "should not fail when the specified resource type exists" do
lambda { resources.new :name => "file" }.should_not raise_error
end
it "should set its :resource_type attribute" do
resources.new(:name => "file").resource_type.should == Puppet::Type.type(:file)
end
end
describe :purge do
let (:instance) { described_class.new(:name => 'file') }
it "defaults to false" do
instance[:purge].should be_false
end
it "can be set to false" do
instance[:purge] = 'false'
end
it "cannot be set to true for a resource type that does not accept ensure" do
instance.resource_type.stubs(:respond_to?).returns true
instance.resource_type.stubs(:validproperty?).returns false
expect { instance[:purge] = 'yes' }.to raise_error Puppet::Error
end
it "cannot be set to true for a resource type that does not have instances" do
instance.resource_type.stubs(:respond_to?).returns false
instance.resource_type.stubs(:validproperty?).returns true
expect { instance[:purge] = 'yes' }.to raise_error Puppet::Error
end
it "can be set to true for a resource type that has instances and can accept ensure" do
instance.resource_type.stubs(:respond_to?).returns true
instance.resource_type.stubs(:validproperty?).returns true
expect { instance[:purge] = 'yes' }.not_to raise_error Puppet::Error
end
end
+ describe "#check_user purge behaviour" do
+ describe "with unless_system_user => true" do
+ before do
+ @res = Puppet::Type.type(:resources).new :name => :user, :purge => true, :unless_system_user => true
+ @res.catalog = Puppet::Resource::Catalog.new
+ end
+
+ it "should never purge hardcoded system users" do
+ %w{root nobody bin noaccess daemon sys}.each do |sys_user|
+ @res.user_check(Puppet::Type.type(:user).new(:name => sys_user)).should be_false
+ end
+ end
+
+ it "should not purge system users if unless_system_user => true" do
+ user_hash = {:name => 'system_user', :uid => 125, :system => true}
+ user = Puppet::Type.type(:user).new(user_hash)
+ user.stubs(:retrieve_resource).returns Puppet::Resource.new("user", user_hash[:name], :parameters => user_hash)
+ @res.user_check(user).should be_false
+ end
+
+ it "should purge manual users if unless_system_user => true" do
+ user_hash = {:name => 'system_user', :uid => 525, :system => true}
+ user = Puppet::Type.type(:user).new(user_hash)
+ user.stubs(:retrieve_resource).returns Puppet::Resource.new("user", user_hash[:name], :parameters => user_hash)
+ @res.user_check(user).should be_true
+ end
+
+ it "should purge system users over 500 if unless_system_user => 600" do
+ res = Puppet::Type.type(:resources).new :name => :user, :purge => true, :unless_system_user => 600
+ res.catalog = Puppet::Resource::Catalog.new
+ user_hash = {:name => 'system_user', :uid => 525, :system => true}
+ user = Puppet::Type.type(:user).new(user_hash)
+ user.stubs(:retrieve_resource).returns Puppet::Resource.new("user", user_hash[:name], :parameters => user_hash)
+ res.user_check(user).should be_false
+ end
+ end
+
+ describe "with unless_uid" do
+ describe "with a uid range" do
+ before do
+ @res = Puppet::Type.type(:resources).new :name => :user, :purge => true, :unless_uid => 10_000..20_000
+ @res.catalog = Puppet::Resource::Catalog.new
+ end
+
+ it "should purge uids that are not in a specified range" do
+ user_hash = {:name => 'special_user', :uid => 25_000}
+ user = Puppet::Type.type(:user).new(user_hash)
+ user.stubs(:retrieve_resource).returns Puppet::Resource.new("user", user_hash[:name], :parameters => user_hash)
+ @res.user_check(user).should be_true
+ end
+
+ it "should not purge uids that are in a specified range" do
+ user_hash = {:name => 'special_user', :uid => 15_000}
+ user = Puppet::Type.type(:user).new(user_hash)
+ user.stubs(:retrieve_resource).returns Puppet::Resource.new("user", user_hash[:name], :parameters => user_hash)
+ @res.user_check(user).should be_false
+ end
+ end
+
+ describe "with a uid range array" do
+ before do
+ @res = Puppet::Type.type(:resources).new :name => :user, :purge => true, :unless_uid => [10_000..15_000, 15_000..20_000]
+ @res.catalog = Puppet::Resource::Catalog.new
+ end
+
+ it "should purge uids that are not in a specified range array" do
+ user_hash = {:name => 'special_user', :uid => 25_000}
+ user = Puppet::Type.type(:user).new(user_hash)
+ user.stubs(:retrieve_resource).returns Puppet::Resource.new("user", user_hash[:name], :parameters => user_hash)
+ @res.user_check(user).should be_true
+ end
+
+ it "should not purge uids that are in a specified range array" do
+ user_hash = {:name => 'special_user', :uid => 15_000}
+ user = Puppet::Type.type(:user).new(user_hash)
+ user.stubs(:retrieve_resource).returns Puppet::Resource.new("user", user_hash[:name], :parameters => user_hash)
+ @res.user_check(user).should be_false
+ end
+
+ end
+
+ describe "with a uid array" do
+ before do
+ @res = Puppet::Type.type(:resources).new :name => :user, :purge => true, :unless_uid => [15_000, 15_001, 15_002]
+ @res.catalog = Puppet::Resource::Catalog.new
+ end
+
+ it "should purge uids that are not in a specified array" do
+ user_hash = {:name => 'special_user', :uid => 25_000}
+ user = Puppet::Type.type(:user).new(user_hash)
+ user.stubs(:retrieve_resource).returns Puppet::Resource.new("user", user_hash[:name], :parameters => user_hash)
+ @res.user_check(user).should be_true
+ end
+
+ it "should not purge uids that are in a specified array" do
+ user_hash = {:name => 'special_user', :uid => 15000}
+ user = Puppet::Type.type(:user).new(user_hash)
+ user.stubs(:retrieve_resource).returns Puppet::Resource.new("user", user_hash[:name], :parameters => user_hash)
+ @res.user_check(user).should be_false
+ end
+
+ end
+
+ describe "with a single uid" do
+ before do
+ @res = Puppet::Type.type(:resources).new :name => :user, :purge => true, :unless_uid => 15_000
+ @res.catalog = Puppet::Resource::Catalog.new
+ end
+
+ it "should purge uids that are not specified" do
+ user_hash = {:name => 'special_user', :uid => 25_000}
+ user = Puppet::Type.type(:user).new(user_hash)
+ user.stubs(:retrieve_resource).returns Puppet::Resource.new("user", user_hash[:name], :parameters => user_hash)
+ @res.user_check(user).should be_true
+ end
+
+ it "should not purge uids that are specified" do
+ user_hash = {:name => 'special_user', :uid => 15_000}
+ user = Puppet::Type.type(:user).new(user_hash)
+ user.stubs(:retrieve_resource).returns Puppet::Resource.new("user", user_hash[:name], :parameters => user_hash)
+ @res.user_check(user).should be_false
+ end
+ end
+
+ describe "with a mixed uid array" do
+ before do
+ @res = Puppet::Type.type(:resources).new :name => :user, :purge => true, :unless_uid => [10_000..15_000, 16_666]
+ @res.catalog = Puppet::Resource::Catalog.new
+ end
+
+ it "should not purge ids in the range" do
+ user_hash = {:name => 'special_user', :uid => 15_000}
+ user = Puppet::Type.type(:user).new(user_hash)
+ user.stubs(:retrieve_resource).returns Puppet::Resource.new("user", user_hash[:name], :parameters => user_hash)
+ @res.user_check(user).should be_false
+ end
+
+ it "should not purge specified ids" do
+ user_hash = {:name => 'special_user', :uid => 16_666}
+ user = Puppet::Type.type(:user).new(user_hash)
+ user.stubs(:retrieve_resource).returns Puppet::Resource.new("user", user_hash[:name], :parameters => user_hash)
+ @res.user_check(user).should be_false
+ end
+
+ it "should purge unspecified ids" do
+ user_hash = {:name => 'special_user', :uid => 17_000}
+ user = Puppet::Type.type(:user).new(user_hash)
+ user.stubs(:retrieve_resource).returns Puppet::Resource.new("user", user_hash[:name], :parameters => user_hash)
+ @res.user_check(user).should be_true
+ end
+ end
+
+ end
+ end
+
describe "#generate" do
before do
@host1 = Puppet::Type.type(:host).new(:name => 'localhost', :ip => '127.0.0.1')
@catalog = Puppet::Resource::Catalog.new
end
describe "when dealing with non-purging resources" do
before do
@resources = Puppet::Type.type(:resources).new(:name => 'host')
end
it "should not generate any resource" do
@resources.generate.should be_empty
end
end
describe "when the catalog contains a purging resource" do
before do
@resources = Puppet::Type.type(:resources).new(:name => 'host', :purge => true)
@purgeable_resource = Puppet::Type.type(:host).new(:name => 'localhost', :ip => '127.0.0.1')
@catalog.add_resource @resources
end
it "should not generate a duplicate of that resource" do
Puppet::Type.type(:host).stubs(:instances).returns [@host1]
@catalog.add_resource @host1
@resources.generate.collect { |r| r.ref }.should_not include(@host1.ref)
end
it "should not include the skipped system users" do
res = Puppet::Type.type(:resources).new :name => :user, :purge => true
res.catalog = Puppet::Resource::Catalog.new
root = Puppet::Type.type(:user).new(:name => "root")
Puppet::Type.type(:user).expects(:instances).returns [ root ]
list = res.generate
names = list.collect { |r| r[:name] }
names.should_not be_include("root")
end
describe "when generating a purgeable resource" do
it "should be included in the generated resources" do
Puppet::Type.type(:host).stubs(:instances).returns [@purgeable_resource]
@resources.generate.collect { |r| r.ref }.should include(@purgeable_resource.ref)
end
end
describe "when the instance's do not have an ensure property" do
it "should not be included in the generated resources" do
@no_ensure_resource = Puppet::Type.type(:exec).new(:name => "#{File.expand_path('/usr/bin/env')} echo")
Puppet::Type.type(:host).stubs(:instances).returns [@no_ensure_resource]
@resources.generate.collect { |r| r.ref }.should_not include(@no_ensure_resource.ref)
end
end
describe "when the instance's ensure property does not accept absent" do
it "should not be included in the generated resources" do
@no_absent_resource = Puppet::Type.type(:service).new(:name => 'foobar')
Puppet::Type.type(:host).stubs(:instances).returns [@no_absent_resource]
@resources.generate.collect { |r| r.ref }.should_not include(@no_absent_resource.ref)
end
end
describe "when checking the instance fails" do
it "should not be included in the generated resources" do
@purgeable_resource = Puppet::Type.type(:host).new(:name => 'foobar')
Puppet::Type.type(:host).stubs(:instances).returns [@purgeable_resource]
@resources.expects(:check).with(@purgeable_resource).returns(false)
@resources.generate.collect { |r| r.ref }.should_not include(@purgeable_resource.ref)
end
end
end
end
end
diff --git a/spec/unit/type/service_spec.rb b/spec/unit/type/service_spec.rb
index e36d11a56..cfb701d0c 100755
--- a/spec/unit/type/service_spec.rb
+++ b/spec/unit/type/service_spec.rb
@@ -1,261 +1,261 @@
#! /usr/bin/env ruby
require 'spec_helper'
describe Puppet::Type.type(:service) do
it "should have an :enableable feature that requires the :enable, :disable, and :enabled? methods" do
Puppet::Type.type(:service).provider_feature(:enableable).methods.should == [:disable, :enable, :enabled?]
end
it "should have a :refreshable feature that requires the :restart method" do
Puppet::Type.type(:service).provider_feature(:refreshable).methods.should == [:restart]
end
end
describe Puppet::Type.type(:service), "when validating attributes" do
[:name, :binary, :hasstatus, :path, :pattern, :start, :restart, :stop, :status, :hasrestart, :control].each do |param|
it "should have a #{param} parameter" do
Puppet::Type.type(:service).attrtype(param).should == :param
end
end
[:ensure, :enable].each do |param|
it "should have an #{param} property" do
Puppet::Type.type(:service).attrtype(param).should == :property
end
end
end
describe Puppet::Type.type(:service), "when validating attribute values" do
before do
@provider = stub 'provider', :class => Puppet::Type.type(:service).defaultprovider, :clear => nil, :controllable? => false
Puppet::Type.type(:service).defaultprovider.stubs(:new).returns(@provider)
end
it "should support :running as a value to :ensure" do
Puppet::Type.type(:service).new(:name => "yay", :ensure => :running)
end
it "should support :stopped as a value to :ensure" do
Puppet::Type.type(:service).new(:name => "yay", :ensure => :stopped)
end
it "should alias the value :true to :running in :ensure" do
svc = Puppet::Type.type(:service).new(:name => "yay", :ensure => true)
svc.should(:ensure).should == :running
end
it "should alias the value :false to :stopped in :ensure" do
svc = Puppet::Type.type(:service).new(:name => "yay", :ensure => false)
svc.should(:ensure).should == :stopped
end
describe "the enable property" do
before :each do
@provider.class.stubs(:supports_parameter?).returns true
end
it "should support :true as a value" do
srv = Puppet::Type.type(:service).new(:name => "yay", :enable => :true)
srv.should(:enable).should == :true
end
it "should support :false as a value" do
srv = Puppet::Type.type(:service).new(:name => "yay", :enable => :false)
srv.should(:enable).should == :false
end
it "should support :manual as a value on Windows" do
Puppet.features.stubs(:microsoft_windows?).returns true
srv = Puppet::Type.type(:service).new(:name => "yay", :enable => :manual)
srv.should(:enable).should == :manual
end
it "should not support :manual as a value when not on Windows" do
Puppet.features.stubs(:microsoft_windows?).returns false
expect { Puppet::Type.type(:service).new(:name => "yay", :enable => :manual) }.to raise_error(
Puppet::Error,
/Setting enable to manual is only supported on Microsoft Windows\./
)
end
end
it "should support :true as a value to :hasstatus" do
srv = Puppet::Type.type(:service).new(:name => "yay", :hasstatus => :true)
srv[:hasstatus].should == :true
end
it "should support :false as a value to :hasstatus" do
srv = Puppet::Type.type(:service).new(:name => "yay", :hasstatus => :false)
srv[:hasstatus].should == :false
end
it "should specify :true as the default value of hasstatus" do
srv = Puppet::Type.type(:service).new(:name => "yay")
srv[:hasstatus].should == :true
end
it "should support :true as a value to :hasrestart" do
srv = Puppet::Type.type(:service).new(:name => "yay", :hasrestart => :true)
srv[:hasrestart].should == :true
end
it "should support :false as a value to :hasrestart" do
srv = Puppet::Type.type(:service).new(:name => "yay", :hasrestart => :false)
srv[:hasrestart].should == :false
end
it "should allow setting the :enable parameter if the provider has the :enableable feature" do
Puppet::Type.type(:service).defaultprovider.stubs(:supports_parameter?).returns(true)
Puppet::Type.type(:service).defaultprovider.expects(:supports_parameter?).with(Puppet::Type.type(:service).attrclass(:enable)).returns(true)
svc = Puppet::Type.type(:service).new(:name => "yay", :enable => true)
svc.should(:enable).should == :true
end
it "should not allow setting the :enable parameter if the provider is missing the :enableable feature" do
Puppet::Type.type(:service).defaultprovider.stubs(:supports_parameter?).returns(true)
Puppet::Type.type(:service).defaultprovider.expects(:supports_parameter?).with(Puppet::Type.type(:service).attrclass(:enable)).returns(false)
svc = Puppet::Type.type(:service).new(:name => "yay", :enable => true)
svc.should(:enable).should be_nil
end
it "should split paths on '#{File::PATH_SEPARATOR}'" do
- Puppet::FileSystem::File.stubs(:exist?).returns(true)
+ Puppet::FileSystem.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
- Puppet::FileSystem::File.stubs(:exist?).returns(true)
+ Puppet::FileSystem.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}
end
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)
- Puppet::FileSystem::File.stubs(:exist?).returns(true)
+ Puppet::FileSystem.stubs(:exist?).returns(true)
Puppet::Type.type(:service).defaultprovider.stubs(:respond_to?).returns(true)
Puppet::Type.type(:service).defaultprovider.stubs(:defpath).returns("testing")
svc = Puppet::Type.type(:service).new(:name => "other")
svc[:path].should == ["testing"]
end
it "should default 'pattern' to the binary if one is provided" do
svc = Puppet::Type.type(:service).new(:name => "other", :binary => "/some/binary")
svc[:pattern].should == "/some/binary"
end
it "should default 'pattern' to the name if no pattern is provided" do
svc = Puppet::Type.type(:service).new(:name => "other")
svc[:pattern].should == "other"
end
it "should default 'control' to the upcased service name with periods replaced by underscores if the provider supports the 'controllable' feature" do
provider = stub 'provider', :controllable? => true, :class => Puppet::Type.type(:service).defaultprovider, :clear => nil
Puppet::Type.type(:service).defaultprovider.stubs(:new).returns(provider)
svc = Puppet::Type.type(:service).new(:name => "nfs.client")
svc[:control].should == "NFS_CLIENT_START"
end
end
describe Puppet::Type.type(:service), "when retrieving the host's current state" do
before do
@service = Puppet::Type.type(:service).new(:name => "yay")
end
it "should use the provider's status to determine whether the service is running" do
@service.provider.expects(:status).returns(:yepper)
@service[:ensure] = :running
@service.property(:ensure).retrieve.should == :yepper
end
it "should ask the provider whether it is enabled" do
@service.provider.class.stubs(:supports_parameter?).returns(true)
@service.provider.expects(:enabled?).returns(:yepper)
@service[:enable] = true
@service.property(:enable).retrieve.should == :yepper
end
end
describe Puppet::Type.type(:service), "when changing the host" do
before do
@service = Puppet::Type.type(:service).new(:name => "yay")
end
it "should start the service if it is supposed to be running" do
@service[:ensure] = :running
@service.provider.expects(:start)
@service.property(:ensure).sync
end
it "should stop the service if it is supposed to be stopped" do
@service[:ensure] = :stopped
@service.provider.expects(:stop)
@service.property(:ensure).sync
end
it "should enable the service if it is supposed to be enabled" do
@service.provider.class.stubs(:supports_parameter?).returns(true)
@service[:enable] = true
@service.provider.expects(:enable)
@service.property(:enable).sync
end
it "should disable the service if it is supposed to be disabled" do
@service.provider.class.stubs(:supports_parameter?).returns(true)
@service[:enable] = false
@service.provider.expects(:disable)
@service.property(:enable).sync
end
it "should sync the service's enable state when changing the state of :ensure if :enable is being managed" do
@service.provider.class.stubs(:supports_parameter?).returns(true)
@service[:enable] = false
@service[:ensure] = :stopped
@service.property(:enable).expects(:retrieve).returns("whatever")
@service.property(:enable).expects(:insync?).returns(false)
@service.property(:enable).expects(:sync)
@service.provider.stubs(:stop)
@service.property(:ensure).sync
end
end
describe Puppet::Type.type(:service), "when refreshing the service" do
before do
@service = Puppet::Type.type(:service).new(:name => "yay")
end
it "should restart the service if it is running" do
@service[:ensure] = :running
@service.provider.expects(:status).returns(:running)
@service.provider.expects(:restart)
@service.refresh
end
it "should restart the service if it is running, even if it is supposed to stopped" do
@service[:ensure] = :stopped
@service.provider.expects(:status).returns(:running)
@service.provider.expects(:restart)
@service.refresh
end
it "should not restart the service if it is not running" do
@service[:ensure] = :running
@service.provider.expects(:status).returns(:stopped)
@service.refresh
end
it "should add :ensure as a property if it is not being managed" do
@service.provider.expects(:status).returns(:running)
@service.provider.expects(:restart)
@service.refresh
end
end
diff --git a/spec/unit/type/ssh_authorized_key_spec.rb b/spec/unit/type/ssh_authorized_key_spec.rb
index 70861e61d..fa82941c7 100755
--- a/spec/unit/type/ssh_authorized_key_spec.rb
+++ b/spec/unit/type/ssh_authorized_key_spec.rb
@@ -1,255 +1,258 @@
#! /usr/bin/env ruby
require 'spec_helper'
-ssh_authorized_key = Puppet::Type.type(:ssh_authorized_key)
-describe ssh_authorized_key, :unless => Puppet.features.microsoft_windows? do
+describe Puppet::Type.type(:ssh_authorized_key), :unless => Puppet.features.microsoft_windows? do
include PuppetSpec::Files
before do
- @class = Puppet::Type.type(:ssh_authorized_key)
+ provider_class = stub 'provider_class', :name => "fake", :suitable? => true, :supports_parameter? => true
+ described_class.stubs(:defaultprovider).returns(provider_class)
+ described_class.stubs(:provider).returns(provider_class)
- @provider_class = stub 'provider_class', :name => "fake", :suitable? => true, :supports_parameter? => true
- @class.stubs(:defaultprovider).returns(@provider_class)
- @class.stubs(:provider).returns(@provider_class)
-
- @provider = stub 'provider', :class => @provider_class, :file_path => make_absolute("/tmp/whatever"), :clear => nil
- @provider_class.stubs(:new).returns(@provider)
- @catalog = Puppet::Resource::Catalog.new
+ provider = stub 'provider', :class => provider_class, :file_path => make_absolute("/tmp/whatever"), :clear => nil
+ provider_class.stubs(:new).returns(provider)
end
- it "should have :name be its namevar" do
- @class.key_attributes.should == [:name]
+ it "has :name as its namevar" do
+ expect(described_class.key_attributes).to eq [:name]
end
describe "when validating attributes" do
[:name, :provider].each do |param|
- it "should have a #{param} parameter" do
- @class.attrtype(param).should == :param
+ it "has a #{param} parameter" do
+ expect(described_class.attrtype(param)).to eq :param
end
end
[:type, :key, :user, :target, :options, :ensure].each do |property|
- it "should have a #{property} property" do
- @class.attrtype(property).should == :property
+ it "has a #{property} property" do
+ expect(described_class.attrtype(property)).to eq :property
end
end
end
describe "when validating values" do
describe "for name" do
- it "should support valid names" do
- proc { @class.new(:name => "username", :ensure => :present, :user => "nobody") }.should_not raise_error
- proc { @class.new(:name => "username@hostname", :ensure => :present, :user => "nobody") }.should_not raise_error
+ it "supports valid names" do
+ described_class.new(:name => "username", :ensure => :present, :user => "nobody")
+ described_class.new(:name => "username@hostname", :ensure => :present, :user => "nobody")
end
- it "should support whitespace" do
- proc { @class.new(:name => "my test", :ensure => :present, :user => "nobody") }.should_not raise_error
+ it "supports whitespace" do
+ described_class.new(:name => "my test", :ensure => :present, :user => "nobody")
end
end
describe "for ensure" do
- it "should support :present" do
- proc { @class.new(:name => "whev", :ensure => :present, :user => "nobody") }.should_not raise_error
+ it "supports :present" do
+ described_class.new(:name => "whev", :ensure => :present, :user => "nobody")
end
- it "should support :absent" do
- proc { @class.new(:name => "whev", :ensure => :absent, :user => "nobody") }.should_not raise_error
+ it "supports :absent" do
+ described_class.new(:name => "whev", :ensure => :absent, :user => "nobody")
end
- it "should not support other values" do
- proc { @class.new(:name => "whev", :ensure => :foo, :user => "nobody") }.should raise_error(Puppet::Error, /Invalid value/)
+ it "nots support other values" do
+ expect { described_class.new(:name => "whev", :ensure => :foo, :user => "nobody") }.to raise_error(Puppet::Error, /Invalid value/)
end
end
describe "for type" do
- [:'ssh-dss', :'ssh-rsa', :rsa, :dsa, :'ecdsa-sha2-nistp256', :'ecdsa-sha2-nistp384', :'ecdsa-sha2-nistp521'].each do |keytype|
- it "should support #{keytype}" do
- proc { @class.new(:name => "whev", :type => keytype, :user => "nobody") }.should_not raise_error
+ [
+ :'ssh-dss', :dsa,
+ :'ssh-rsa', :rsa,
+ :'ecdsa-sha2-nistp256',
+ :'ecdsa-sha2-nistp384',
+ :'ecdsa-sha2-nistp521',
+ :ed25519, :'ssh-ed25519',
+ ].each do |keytype|
+ it "supports #{keytype}" do
+ described_class.new(:name => "whev", :type => keytype, :user => "nobody")
end
end
- it "should alias :rsa to :ssh-rsa" do
- key = @class.new(:name => "whev", :type => :rsa, :user => "nobody")
- key.should(:type).should == :'ssh-rsa'
+ it "aliases :rsa to :ssh-rsa" do
+ key = described_class.new(:name => "whev", :type => :rsa, :user => "nobody")
+ expect(key.should(:type)).to eq :'ssh-rsa'
end
- it "should alias :dsa to :ssh-dss" do
- key = @class.new(:name => "whev", :type => :dsa, :user => "nobody")
- key.should(:type).should == :'ssh-dss'
+ it "aliases :dsa to :ssh-dss" do
+ key = described_class.new(:name => "whev", :type => :dsa, :user => "nobody")
+ expect(key.should(:type)).to eq :'ssh-dss'
end
- it "should not support values other than ssh-dss, ssh-rsa, dsa, rsa" do
- proc { @class.new(:name => "whev", :type => :something) }.should raise_error(Puppet::Error,/Invalid value/)
+ it "doesn't support values other than ssh-dss, ssh-rsa, dsa, rsa" do
+ expect { described_class.new(:name => "whev", :type => :something) }.to raise_error(Puppet::Error,/Invalid value/)
end
end
describe "for key" do
- it "should support a valid key like a 1024 bit rsa key" do
- proc { @class.new(:name => "whev", :type => :rsa, :user => "nobody", :key => 'AAAAB3NzaC1yc2EAAAADAQABAAAAgQDCPfzW2ry7XvMc6E5Kj2e5fF/YofhKEvsNMUogR3PGL/HCIcBlsEjKisrY0aYgD8Ikp7ZidpXLbz5dBsmPy8hJiBWs5px9ZQrB/EOQAwXljvj69EyhEoGawmxQMtYw+OAIKHLJYRuk1QiHAMHLp5piqem8ZCV2mLb9AsJ6f7zUVw==')}.should_not raise_error
+ it "supports a valid key like a 1024 bit rsa key" do
+ expect { described_class.new(:name => "whev", :type => :rsa, :user => "nobody", :key => 'AAAAB3NzaC1yc2EAAAADAQABAAAAgQDCPfzW2ry7XvMc6E5Kj2e5fF/YofhKEvsNMUogR3PGL/HCIcBlsEjKisrY0aYgD8Ikp7ZidpXLbz5dBsmPy8hJiBWs5px9ZQrB/EOQAwXljvj69EyhEoGawmxQMtYw+OAIKHLJYRuk1QiHAMHLp5piqem8ZCV2mLb9AsJ6f7zUVw==')}.to_not raise_error
end
- it "should support a valid key like a 4096 bit rsa key" do
- proc { @class.new(:name => "whev", :type => :rsa, :user => "nobody", :key => 'AAAAB3NzaC1yc2EAAAADAQABAAACAQDEY4pZFyzSfRc9wVWI3DfkgT/EL033UZm/7x1M+d+lBD00qcpkZ6CPT7lD3Z+vylQlJ5S8Wcw6C5Smt6okZWY2WXA9RCjNJMIHQbJAzwuQwgnwU/1VMy9YPp0tNVslg0sUUgpXb13WW4mYhwxyGmIVLJnUrjrQmIFhtfHsJAH8ZVqCWaxKgzUoC/YIu1u1ScH93lEdoBPLlwm6J0aiM7KWXRb7Oq1nEDZtug1zpX5lhgkQWrs0BwceqpUbY+n9sqeHU5e7DCyX/yEIzoPRW2fe2Gx1Iq6JKM/5NNlFfaW8rGxh3Z3S1NpzPHTRjw8js3IeGiV+OPFoaTtM1LsWgPDSBlzIdyTbSQR7gKh0qWYCNV/7qILEfa0yIFB5wIo4667iSPZw2pNgESVtenm8uXyoJdk8iWQ4mecdoposV/znknNb2GPgH+n/2vme4btZ0Sl1A6rev22GQjVgbWOn8zaDglJ2vgCN1UAwmq41RXprPxENGeLnWQppTnibhsngu0VFllZR5kvSIMlekLRSOFLFt92vfd+tk9hZIiKm9exxcbVCGGQPsf6dZ27rTOmg0xM2Sm4J6RRKuz79HQgA4Eg18+bqRP7j/itb89DmtXEtoZFAsEJw8IgIfeGGDtHTkfAlAC92mtK8byeaxGq57XCTKbO/r5gcOMElZHy1AcB8kw==')}.should_not raise_error
+ it "supports a valid key like a 4096 bit rsa key" do
+ expect { described_class.new(:name => "whev", :type => :rsa, :user => "nobody", :key => 'AAAAB3NzaC1yc2EAAAADAQABAAACAQDEY4pZFyzSfRc9wVWI3DfkgT/EL033UZm/7x1M+d+lBD00qcpkZ6CPT7lD3Z+vylQlJ5S8Wcw6C5Smt6okZWY2WXA9RCjNJMIHQbJAzwuQwgnwU/1VMy9YPp0tNVslg0sUUgpXb13WW4mYhwxyGmIVLJnUrjrQmIFhtfHsJAH8ZVqCWaxKgzUoC/YIu1u1ScH93lEdoBPLlwm6J0aiM7KWXRb7Oq1nEDZtug1zpX5lhgkQWrs0BwceqpUbY+n9sqeHU5e7DCyX/yEIzoPRW2fe2Gx1Iq6JKM/5NNlFfaW8rGxh3Z3S1NpzPHTRjw8js3IeGiV+OPFoaTtM1LsWgPDSBlzIdyTbSQR7gKh0qWYCNV/7qILEfa0yIFB5wIo4667iSPZw2pNgESVtenm8uXyoJdk8iWQ4mecdoposV/znknNb2GPgH+n/2vme4btZ0Sl1A6rev22GQjVgbWOn8zaDglJ2vgCN1UAwmq41RXprPxENGeLnWQppTnibhsngu0VFllZR5kvSIMlekLRSOFLFt92vfd+tk9hZIiKm9exxcbVCGGQPsf6dZ27rTOmg0xM2Sm4J6RRKuz79HQgA4Eg18+bqRP7j/itb89DmtXEtoZFAsEJw8IgIfeGGDtHTkfAlAC92mtK8byeaxGq57XCTKbO/r5gcOMElZHy1AcB8kw==')}.to_not raise_error
end
- it "should support a valid key like a 1024 bit dsa key" do
- proc { @class.new(:name => "whev", :type => :dsa, :user => "nobody", :key => 'AAAAB3NzaC1kc3MAAACBAI80iR78QCgpO4WabVqHHdEDigOjUEHwIjYHIubR/7u7DYrXY+e+TUmZ0CVGkiwB/0yLHK5dix3Y/bpj8ZiWCIhFeunnXccOdE4rq5sT2V3l1p6WP33RpyVYbLmeuHHl5VQ1CecMlca24nHhKpfh6TO/FIwkMjghHBfJIhXK+0w/AAAAFQDYzLupuMY5uz+GVrcP+Kgd8YqMmwAAAIB3SVN71whLWjFPNTqGyyIlMy50624UfNOaH4REwO+Of3wm/cE6eP8n75vzTwQGBpJX3BPaBGW1S1Zp/DpTOxhCSAwZzAwyf4WgW7YyAOdxN3EwTDJZeyiyjWMAOjW9/AOWt9gtKg0kqaylbMHD4kfiIhBzo31ZY81twUzAfN7angAAAIBfva8sTSDUGKsWWIXkdbVdvM4X14K4gFdy0ZJVzaVOtZ6alysW6UQypnsl6jfnbKvsZ0tFgvcX/CPyqNY/gMR9lyh/TCZ4XQcbqeqYPuceGehz+jL5vArfqsW2fJYFzgCcklmr/VxtP5h6J/T0c9YcDgc/xIfWdZAlznOnphI/FA==')}.should_not raise_error
+ it "supports a valid key like a 1024 bit dsa key" do
+ expect { described_class.new(:name => "whev", :type => :dsa, :user => "nobody", :key => 'AAAAB3NzaC1kc3MAAACBAI80iR78QCgpO4WabVqHHdEDigOjUEHwIjYHIubR/7u7DYrXY+e+TUmZ0CVGkiwB/0yLHK5dix3Y/bpj8ZiWCIhFeunnXccOdE4rq5sT2V3l1p6WP33RpyVYbLmeuHHl5VQ1CecMlca24nHhKpfh6TO/FIwkMjghHBfJIhXK+0w/AAAAFQDYzLupuMY5uz+GVrcP+Kgd8YqMmwAAAIB3SVN71whLWjFPNTqGyyIlMy50624UfNOaH4REwO+Of3wm/cE6eP8n75vzTwQGBpJX3BPaBGW1S1Zp/DpTOxhCSAwZzAwyf4WgW7YyAOdxN3EwTDJZeyiyjWMAOjW9/AOWt9gtKg0kqaylbMHD4kfiIhBzo31ZY81twUzAfN7angAAAIBfva8sTSDUGKsWWIXkdbVdvM4X14K4gFdy0ZJVzaVOtZ6alysW6UQypnsl6jfnbKvsZ0tFgvcX/CPyqNY/gMR9lyh/TCZ4XQcbqeqYPuceGehz+jL5vArfqsW2fJYFzgCcklmr/VxtP5h6J/T0c9YcDgc/xIfWdZAlznOnphI/FA==')}.to_not raise_error
end
- it "should not support whitespaces" do
- proc { @class.new(:name => "whev", :type => :rsa, :user => "nobody", :key => 'AAA FA==')}.should raise_error(Puppet::Error,/Key must not contain whitespace/)
+ it "doesn't support whitespaces" do
+ expect { described_class.new(:name => "whev", :type => :rsa, :user => "nobody", :key => 'AAA FA==')}.to raise_error(Puppet::Error,/Key must not contain whitespace/)
end
end
describe "for options" do
- it "should support flags as options" do
- proc { @class.new(:name => "whev", :type => :rsa, :user => "nobody", :options => 'cert-authority')}.should_not raise_error
- proc { @class.new(:name => "whev", :type => :rsa, :user => "nobody", :options => 'no-port-forwarding')}.should_not raise_error
+ it "supports flags as options" do
+ expect { described_class.new(:name => "whev", :type => :rsa, :user => "nobody", :options => 'cert-authority')}.to_not raise_error
+ expect { described_class.new(:name => "whev", :type => :rsa, :user => "nobody", :options => 'no-port-forwarding')}.to_not raise_error
end
- it "should support key-value pairs as options" do
- proc { @class.new(:name => "whev", :type => :rsa, :user => "nobody", :options => 'command="command"')}.should_not raise_error
+ it "supports key-value pairs as options" do
+ expect { described_class.new(:name => "whev", :type => :rsa, :user => "nobody", :options => 'command="command"')}.to_not raise_error
end
- it "should support key-value pairs where value consist of multiple items" do
- proc { @class.new(:name => "whev", :type => :rsa, :user => "nobody", :options => 'from="*.domain1,host1.domain2"')}.should_not raise_error
+ it "supports key-value pairs where value consist of multiple items" do
+ expect { described_class.new(:name => "whev", :type => :rsa, :user => "nobody", :options => 'from="*.domain1,host1.domain2"')}.to_not raise_error
end
- it "should support environments as options" do
- proc { @class.new(:name => "whev", :type => :rsa, :user => "nobody", :options => 'environment="NAME=value"')}.should_not raise_error
+ it "supports environments as options" do
+ expect { described_class.new(:name => "whev", :type => :rsa, :user => "nobody", :options => 'environment="NAME=value"')}.to_not raise_error
end
- it "should support multiple options as an array" do
- proc { @class.new(:name => "whev", :type => :rsa, :user => "nobody", :options => ['cert-authority','environment="NAME=value"'])}.should_not raise_error
+ it "supports multiple options as an array" do
+ expect { described_class.new(:name => "whev", :type => :rsa, :user => "nobody", :options => ['cert-authority','environment="NAME=value"'])}.to_not raise_error
end
- it "should not support a comma separated list" do
- proc { @class.new(:name => "whev", :type => :rsa, :user => "nobody", :options => 'cert-authority,no-port-forwarding')}.should raise_error(Puppet::Error, /must be provided as an array/)
+ it "doesn't support a comma separated list" do
+ expect { described_class.new(:name => "whev", :type => :rsa, :user => "nobody", :options => 'cert-authority,no-port-forwarding')}.to raise_error(Puppet::Error, /must be provided as an array/)
end
- it "should use :absent as a default value" do
- @class.new(:name => "whev", :type => :rsa, :user => "nobody").should(:options).should == [:absent]
+ it "uses :absent as a default value" do
+ expect(described_class.new(:name => "whev", :type => :rsa, :user => "nobody").should(:options)).to eq [:absent]
end
it "property should return well formed string of arrays from is_to_s" do
- resource = @class.new(:name => "whev", :type => :rsa, :user => "nobody", :options => ["a","b","c"])
- resource.property(:options).is_to_s(["a","b","c"]).should == "a,b,c"
+ resource = described_class.new(:name => "whev", :type => :rsa, :user => "nobody", :options => ["a","b","c"])
+ expect(resource.property(:options).is_to_s(["a","b","c"])).to eq "a,b,c"
end
it "property should return well formed string of arrays from should_to_s" do
- resource = @class.new(:name => "whev", :type => :rsa, :user => "nobody", :options => ["a","b","c"])
- resource.property(:options).should_to_s(["a","b","c"]).should == "a,b,c"
+ resource = described_class.new(:name => "whev", :type => :rsa, :user => "nobody", :options => ["a","b","c"])
+ expect(resource.property(:options).should_to_s(["a","b","c"])).to eq "a,b,c"
end
end
describe "for user" do
- it "should support present users" do
- proc { @class.new(:name => "whev", :type => :rsa, :user => "root") }.should_not raise_error
+ it "supports present users" do
+ described_class.new(:name => "whev", :type => :rsa, :user => "root")
end
- it "should support absent users" do
- proc { @class.new(:name => "whev", :type => :rsa, :user => "ihopeimabsent") }.should_not raise_error
+ it "supports absent users" do
+ described_class.new(:name => "whev", :type => :rsa, :user => "ihopeimabsent")
end
end
describe "for target" do
- it "should support absolute paths" do
- proc { @class.new(:name => "whev", :type => :rsa, :target => "/tmp/here") }.should_not raise_error
+ it "supports absolute paths" do
+ described_class.new(:name => "whev", :type => :rsa, :target => "/tmp/here")
end
- it "should use the user's path if not explicitly specified" do
- @class.new(:name => "whev", :user => 'root').should(:target).should == File.expand_path("~root/.ssh/authorized_keys")
+ it "uses the user's path if not explicitly specified" do
+ expect(described_class.new(:name => "whev", :user => 'root').should(:target)).to eq File.expand_path("~root/.ssh/authorized_keys")
end
- it "should not consider the user's path if explicitly specified" do
- @class.new(:name => "whev", :user => 'root', :target => '/tmp/here').should(:target).should == '/tmp/here'
+ it "doesn't consider the user's path if explicitly specified" do
+ expect(described_class.new(:name => "whev", :user => 'root', :target => '/tmp/here').should(:target)).to eq '/tmp/here'
end
- it "should inform about an absent user" do
+ it "informs about an absent user" do
Puppet::Log.level = :debug
- @class.new(:name => "whev", :user => 'idontexist').should(:target)
+ described_class.new(:name => "whev", :user => 'idontexist').should(:target)
@logs.map(&:message).should include("The required user is not yet present on the system")
end
end
end
describe "when neither user nor target is specified" do
- it "should raise an error" do
- proc do
- @class.new(
+ it "raises an error" do
+ expect do
+ described_class.new(
:name => "Test",
:key => "AAA",
:type => "ssh-rsa",
:ensure => :present)
- end.should raise_error(Puppet::Error,/user.*or.*target.*mandatory/)
+ end.to raise_error(Puppet::Error,/user.*or.*target.*mandatory/)
end
end
describe "when both target and user are specified" do
- it "should use target" do
- resource = @class.new(
+ it "uses target" do
+ resource = described_class.new(
:name => "Test",
:user => "root",
:target => "/tmp/blah"
)
- resource.should(:target).should == "/tmp/blah"
+ expect(resource.should(:target)).to eq "/tmp/blah"
end
end
describe "when user is specified" do
- it "should determine target" do
- resource = @class.new(
+ it "determines target" do
+ resource = described_class.new(
:name => "Test",
:user => "root"
)
target = File.expand_path("~root/.ssh/authorized_keys")
- resource.should(:target).should == target
+ expect(resource.should(:target)).to eq target
end
# Bug #2124 - ssh_authorized_key always changes target if target is not defined
- it "should not raise spurious change events" do
- resource = @class.new(:name => "Test", :user => "root")
+ it "doesn't raise spurious change events" do
+ resource = described_class.new(:name => "Test", :user => "root")
target = File.expand_path("~root/.ssh/authorized_keys")
- resource.property(:target).safe_insync?(target).should == true
+ expect(resource.property(:target).safe_insync?(target)).to eq true
end
end
describe "when calling validate" do
- it "should not crash on a non-existant user" do
- resource = @class.new(
+ it "doesn't crash on a non-existant user" do
+ resource = described_class.new(
:name => "Test",
:user => "ihopesuchuserdoesnotexist"
)
- proc { resource.validate }.should_not raise_error
+ resource.validate
end
end
end
diff --git a/spec/unit/type/sshkey_spec.rb b/spec/unit/type/sshkey_spec.rb
index 37a4865b7..d16e59556 100755
--- a/spec/unit/type/sshkey_spec.rb
+++ b/spec/unit/type/sshkey_spec.rb
@@ -1,68 +1,77 @@
#! /usr/bin/env ruby
require 'spec_helper'
-sshkey = Puppet::Type.type(:sshkey)
-describe sshkey do
- before do
- @class = sshkey
- end
+describe Puppet::Type.type(:sshkey) do
- it "should have :name its namevar" do
- @class.key_attributes.should == [:name]
+ it "uses :name as its namevar" do
+ expect(described_class.key_attributes).to eq [:name]
end
describe "when validating attributes" do
[:name, :provider].each do |param|
- it "should have a #{param} parameter" do
- @class.attrtype(param).should == :param
+ it "has a #{param} parameter" do
+ expect(described_class.attrtype(param)).to eq :param
end
end
[:host_aliases, :ensure, :key, :type].each do |property|
- it "should have a #{property} property" do
- @class.attrtype(property).should == :property
+ it "has a #{property} property" do
+ expect(described_class.attrtype(property)).to eq :property
end
end
end
describe "when validating values" do
- [:'ssh-dss', :'ssh-rsa', :rsa, :dsa, :'ecdsa-sha2-nistp256', :'ecdsa-sha2-nistp384', :'ecdsa-sha2-nistp521'].each do |keytype|
- it "should support #{keytype} as a type value" do
- proc { @class.new(:name => "foo", :type => keytype) }.should_not raise_error
+ [
+ :'ssh-dss', :dsa,
+ :'ssh-rsa', :rsa,
+ :'ecdsa-sha2-nistp256',
+ :'ecdsa-sha2-nistp384',
+ :'ecdsa-sha2-nistp521',
+ :'ssh-ed25519', :ed25519,
+ ].each do |keytype|
+ it "supports #{keytype} as a type value" do
+ described_class.new(:name => "foo", :type => keytype)
end
end
- it "should alias :rsa to :ssh-rsa" do
- key = @class.new(:name => "foo", :type => :rsa)
- key.should(:type).should == :'ssh-rsa'
+ it "aliases :rsa to :ssh-rsa" do
+ key = described_class.new(:name => "foo", :type => :rsa)
+ expect(key.should(:type)).to eq :'ssh-rsa'
end
- it "should alias :dsa to :ssh-dss" do
- key = @class.new(:name => "foo", :type => :dsa)
- key.should(:type).should == :'ssh-dss'
+ it "aliases :dsa to :ssh-dss" do
+ key = described_class.new(:name => "foo", :type => :dsa)
+ expect(key.should(:type)).to eq :'ssh-dss'
end
- it "should not support values other than ssh-dss, ssh-rsa, dsa, rsa for type" do
- proc { @class.new(:name => "whev", :type => :'ssh-dsa') }.should raise_error(Puppet::Error)
+ it "doesn't support values other than ssh-dss, ssh-rsa, dsa, rsa for type" do
+ expect {
+ described_class.new(:name => "whev", :type => :'ssh-dsa')
+ }.to raise_error(Puppet::Error, /Invalid value.*ssh-dsa/)
end
- it "should accept one host_alias" do
- proc { @class.new(:name => "foo", :host_aliases => 'foo.bar.tld') }.should_not raise_error
+ it "accepts one host_alias" do
+ described_class.new(:name => "foo", :host_aliases => 'foo.bar.tld')
end
- it "should accept multiple host_aliases as an array" do
- proc { @class.new(:name => "foo", :host_aliases => ['foo.bar.tld','10.0.9.9']) }.should_not raise_error
+ it "accepts multiple host_aliases as an array" do
+ described_class.new(:name => "foo", :host_aliases => ['foo.bar.tld','10.0.9.9'])
end
- it "should not accept spaces in any host_alias" do
- proc { @class.new(:name => "foo", :host_aliases => ['foo.bar.tld','foo bar']) }.should raise_error(Puppet::Error)
+ it "doesn't accept spaces in any host_alias" do
+ expect {
+ described_class.new(:name => "foo", :host_aliases => ['foo.bar.tld','foo bar'])
+ }.to raise_error(Puppet::Error, /cannot include whitespace/)
end
- it "should not accept aliases in the resourcename" do
- proc { @class.new(:name => 'host,host.domain,ip') }.should raise_error(Puppet::Error)
+ it "doesn't accept aliases in the resourcename" do
+ expect {
+ described_class.new(:name => 'host,host.domain,ip')
+ }.to raise_error(Puppet::Error, /No comma in resourcename/)
end
end
end
diff --git a/spec/unit/type/tidy_spec.rb b/spec/unit/type/tidy_spec.rb
index abe5d26ff..48a930b66 100755
--- a/spec/unit/type/tidy_spec.rb
+++ b/spec/unit/type/tidy_spec.rb
@@ -1,426 +1,433 @@
#! /usr/bin/env ruby
require 'spec_helper'
require 'puppet/file_bucket/dipper'
tidy = Puppet::Type.type(:tidy)
describe tidy do
include PuppetSpec::Files
before do
@basepath = make_absolute("/what/ever")
Puppet.settings.stubs(:use)
end
it "should use :lstat when stating a file" do
path = '/foo/bar'
- resource = tidy.new :path => path, :age => "1d"
stat = mock 'stat'
- stub_file = stub(path, :lstat => stat)
- Puppet::FileSystem::File.expects(:new).with(path).returns stub_file
+ Puppet::FileSystem.expects(:lstat).with(path).returns stat
+
+ resource = tidy.new :path => path, :age => "1d"
+
resource.stat(path).should == stat
end
[:age, :size, :path, :matches, :type, :recurse, :rmdirs].each do |param|
it "should have a #{param} parameter" do
Puppet::Type.type(:tidy).attrclass(param).ancestors.should be_include(Puppet::Parameter)
end
it "should have documentation for its #{param} param" do
Puppet::Type.type(:tidy).attrclass(param).doc.should be_instance_of(String)
end
end
describe "when validating parameter values" do
describe "for 'recurse'" do
before do
@tidy = Puppet::Type.type(:tidy).new :path => "/tmp", :age => "100d"
end
it "should allow 'true'" do
lambda { @tidy[:recurse] = true }.should_not raise_error
end
it "should allow 'false'" do
lambda { @tidy[:recurse] = false }.should_not raise_error
end
it "should allow integers" do
lambda { @tidy[:recurse] = 10 }.should_not raise_error
end
it "should allow string representations of integers" do
lambda { @tidy[:recurse] = "10" }.should_not raise_error
end
it "should allow 'inf'" do
lambda { @tidy[:recurse] = "inf" }.should_not raise_error
end
it "should not allow arbitrary values" do
lambda { @tidy[:recurse] = "whatever" }.should raise_error
end
end
describe "for 'matches'" do
before do
@tidy = Puppet::Type.type(:tidy).new :path => "/tmp", :age => "100d"
end
it "should object if matches is given with recurse is not specified" do
lambda { @tidy[:matches] = '*.doh' }.should raise_error
end
it "should object if matches is given and recurse is 0" do
lambda { @tidy[:recurse] = 0; @tidy[:matches] = '*.doh' }.should raise_error
end
it "should object if matches is given and recurse is false" do
lambda { @tidy[:recurse] = false; @tidy[:matches] = '*.doh' }.should raise_error
end
it "should not object if matches is given and recurse is > 0" do
lambda { @tidy[:recurse] = 1; @tidy[:matches] = '*.doh' }.should_not raise_error
end
it "should not object if matches is given and recurse is true" do
lambda { @tidy[:recurse] = true; @tidy[:matches] = '*.doh' }.should_not raise_error
end
end
end
describe "when matching files by age" do
convertors = {
:second => 1,
:minute => 60
}
convertors[:hour] = convertors[:minute] * 60
convertors[:day] = convertors[:hour] * 24
convertors[:week] = convertors[:day] * 7
convertors.each do |unit, multiple|
it "should consider a #{unit} to be #{multiple} seconds" do
@tidy = Puppet::Type.type(:tidy).new :path => @basepath, :age => "5#{unit.to_s[0..0]}"
@tidy[:age].should == 5 * multiple
end
end
end
describe "when matching files by size" do
convertors = {
:b => 0,
:kb => 1,
:mb => 2,
:gb => 3,
:tb => 4
}
convertors.each do |unit, multiple|
it "should consider a #{unit} to be 1024^#{multiple} bytes" do
@tidy = Puppet::Type.type(:tidy).new :path => @basepath, :size => "5#{unit}"
total = 5
multiple.times { total *= 1024 }
@tidy[:size].should == total
end
end
end
describe "when tidying" do
before do
@tidy = Puppet::Type.type(:tidy).new :path => @basepath
@stat = stub 'stat', :ftype => "directory"
- @stub_file = stub(@basepath, :lstat => @stat)
- Puppet::FileSystem::File.stubs(:new).with(@basepath).returns @stub_file
+ lstat_is(@basepath, @stat)
end
describe "and generating files" do
it "should set the backup on the file if backup is set on the tidy instance" do
@tidy[:backup] = "whatever"
Puppet::Type.type(:file).expects(:new).with { |args| args[:backup] == "whatever" }
@tidy.mkfile(@basepath)
end
it "should set the file's path to the tidy's path" do
Puppet::Type.type(:file).expects(:new).with { |args| args[:path] == @basepath }
@tidy.mkfile(@basepath)
end
it "should configure the file for deletion" do
Puppet::Type.type(:file).expects(:new).with { |args| args[:ensure] == :absent }
@tidy.mkfile(@basepath)
end
it "should force deletion on the file" do
Puppet::Type.type(:file).expects(:new).with { |args| args[:force] == true }
@tidy.mkfile(@basepath)
end
it "should do nothing if the targeted file does not exist" do
- @stub_file.expects(:lstat).raises Errno::ENOENT
+ lstat_raises(@basepath, Errno::ENOENT)
@tidy.generate.should == []
end
end
describe "and recursion is not used" do
it "should generate a file resource if the file should be tidied" do
@tidy.expects(:tidy?).with(@basepath).returns true
file = Puppet::Type.type(:file).new(:path => @basepath+"/eh")
@tidy.expects(:mkfile).with(@basepath).returns file
@tidy.generate.should == [file]
end
it "should do nothing if the file should not be tidied" do
@tidy.expects(:tidy?).with(@basepath).returns false
@tidy.expects(:mkfile).never
@tidy.generate.should == []
end
end
describe "and recursion is used" do
before do
@tidy[:recurse] = true
Puppet::FileServing::Fileset.any_instance.stubs(:stat).returns mock("stat")
@fileset = Puppet::FileServing::Fileset.new(@basepath)
Puppet::FileServing::Fileset.stubs(:new).returns @fileset
end
it "should use a Fileset for infinite recursion" do
Puppet::FileServing::Fileset.expects(:new).with(@basepath, :recurse => true).returns @fileset
@fileset.expects(:files).returns %w{. one two}
@tidy.stubs(:tidy?).returns false
@tidy.generate
end
it "should use a Fileset for limited recursion" do
@tidy[:recurse] = 42
Puppet::FileServing::Fileset.expects(:new).with(@basepath, :recurse => true, :recurselimit => 42).returns @fileset
@fileset.expects(:files).returns %w{. one two}
@tidy.stubs(:tidy?).returns false
@tidy.generate
end
it "should generate a file resource for every file that should be tidied but not for files that should not be tidied" do
@fileset.expects(:files).returns %w{. one two}
@tidy.expects(:tidy?).with(@basepath).returns true
@tidy.expects(:tidy?).with(@basepath+"/one").returns true
@tidy.expects(:tidy?).with(@basepath+"/two").returns false
file = Puppet::Type.type(:file).new(:path => @basepath+"/eh")
@tidy.expects(:mkfile).with(@basepath).returns file
@tidy.expects(:mkfile).with(@basepath+"/one").returns file
@tidy.generate
end
end
describe "and determining whether a file matches provided glob patterns" do
before do
@tidy = Puppet::Type.type(:tidy).new :path => @basepath, :recurse => 1
@tidy[:matches] = %w{*foo* *bar*}
@stat = mock 'stat'
@matcher = @tidy.parameter(:matches)
end
it "should always convert the globs to an array" do
@matcher.value = "*foo*"
@matcher.value.should == %w{*foo*}
end
it "should return true if any pattern matches the last part of the file" do
@matcher.value = %w{*foo* *bar*}
@matcher.must be_tidy("/file/yaybarness", @stat)
end
it "should return false if no pattern matches the last part of the file" do
@matcher.value = %w{*foo* *bar*}
@matcher.should_not be_tidy("/file/yayness", @stat)
end
end
describe "and determining whether a file is too old" do
before do
@tidy = Puppet::Type.type(:tidy).new :path => @basepath
@stat = stub 'stat'
@tidy[:age] = "1s"
@tidy[:type] = "mtime"
@ager = @tidy.parameter(:age)
end
it "should use the age type specified" do
@tidy[:type] = :ctime
@stat.expects(:ctime).returns(Time.now)
@ager.tidy?(@basepath, @stat)
end
it "should return false if the file is more recent than the specified age" do
@stat.expects(:mtime).returns(Time.now)
@ager.should_not be_tidy(@basepath, @stat)
end
it "should return true if the file is older than the specified age" do
@stat.expects(:mtime).returns(Time.now - 10)
@ager.must be_tidy(@basepath, @stat)
end
end
describe "and determining whether a file is too large" do
before do
@tidy = Puppet::Type.type(:tidy).new :path => @basepath
@stat = stub 'stat', :ftype => "file"
@tidy[:size] = "1kb"
@sizer = @tidy.parameter(:size)
end
it "should return false if the file is smaller than the specified size" do
@stat.expects(:size).returns(4) # smaller than a kilobyte
@sizer.should_not be_tidy(@basepath, @stat)
end
it "should return true if the file is larger than the specified size" do
@stat.expects(:size).returns(1500) # larger than a kilobyte
@sizer.must be_tidy(@basepath, @stat)
end
it "should return true if the file is equal to the specified size" do
@stat.expects(:size).returns(1024)
@sizer.must be_tidy(@basepath, @stat)
end
end
describe "and determining whether a file should be tidied" do
before do
@tidy = Puppet::Type.type(:tidy).new :path => @basepath
@stat = stub 'stat', :ftype => "file"
- @stub_file = stub(@basepath, :lstat => @stat)
- Puppet::FileSystem::File.expects(:new).with(@basepath).returns @stub_file
+ lstat_is(@basepath, @stat)
end
it "should not try to recurse if the file does not exist" do
@tidy[:recurse] = true
- @stub_file.stubs(:lstat).returns nil
+ lstat_is(@basepath, nil)
@tidy.generate.should == []
end
it "should not be tidied if the file does not exist" do
- @stub_file.expects(:lstat).raises Errno::ENOENT
+ lstat_raises(@basepath, Errno::ENOENT)
@tidy.should_not be_tidy(@basepath)
end
it "should not be tidied if the user has no access to the file" do
- @stub_file.expects(:lstat).raises Errno::EACCES
+ lstat_raises(@basepath, 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"
- @stub_file.expects(:lstat).returns stat
+ lstat_is(@basepath, stat)
@tidy.should_not be_tidy(@basepath)
end
it "should return false if it does not match any provided globs" do
@tidy[:recurse] = 1
@tidy[:matches] = "globs"
matches = @tidy.parameter(:matches)
matches.expects(:tidy?).with(@basepath, @stat).returns false
@tidy.should_not be_tidy(@basepath)
end
it "should return false if it does not match aging requirements" do
@tidy[:age] = "1d"
ager = @tidy.parameter(:age)
ager.expects(:tidy?).with(@basepath, @stat).returns false
@tidy.should_not be_tidy(@basepath)
end
it "should return false if it does not match size requirements" do
@tidy[:size] = "1b"
sizer = @tidy.parameter(:size)
sizer.expects(:tidy?).with(@basepath, @stat).returns false
@tidy.should_not be_tidy(@basepath)
end
it "should tidy a file if age and size are set but only size matches" do
@tidy[:size] = "1b"
@tidy[:age] = "1d"
@tidy.parameter(:size).stubs(:tidy?).returns true
@tidy.parameter(:age).stubs(:tidy?).returns false
@tidy.must be_tidy(@basepath)
end
it "should tidy a file if age and size are set but only age matches" do
@tidy[:size] = "1b"
@tidy[:age] = "1d"
@tidy.parameter(:size).stubs(:tidy?).returns false
@tidy.parameter(:age).stubs(:tidy?).returns true
@tidy.must be_tidy(@basepath)
end
it "should tidy all files if neither age nor size is set" do
@tidy.must be_tidy(@basepath)
end
it "should sort the results inversely by path length, so files are added to the catalog before their directories" do
@tidy[:recurse] = true
@tidy[:rmdirs] = true
fileset = Puppet::FileServing::Fileset.new(@basepath)
Puppet::FileServing::Fileset.expects(:new).returns fileset
fileset.expects(:files).returns %w{. one one/two}
@tidy.stubs(:tidy?).returns true
@tidy.generate.collect { |r| r[:path] }.should == [@basepath+"/one/two", @basepath+"/one", @basepath]
end
end
it "should configure directories to require their contained files if rmdirs is enabled, so the files will be deleted first" do
@tidy[:recurse] = true
@tidy[:rmdirs] = true
fileset = mock 'fileset'
Puppet::FileServing::Fileset.expects(:new).with(@basepath, :recurse => true).returns fileset
fileset.expects(:files).returns %w{. one two one/subone two/subtwo one/subone/ssone}
@tidy.stubs(:tidy?).returns true
result = @tidy.generate.inject({}) { |hash, res| hash[res[:path]] = res; hash }
{
@basepath => [ @basepath+"/one", @basepath+"/two" ],
@basepath+"/one" => [@basepath+"/one/subone"],
@basepath+"/two" => [@basepath+"/two/subtwo"],
@basepath+"/one/subone" => [@basepath+"/one/subone/ssone"]
}.each do |parent, children|
children.each do |child|
ref = Puppet::Resource.new(:file, child)
result[parent][:require].find { |req| req.to_s == ref.to_s }.should_not be_nil
end
end
end
end
+
+ def lstat_is(path, stat)
+ Puppet::FileSystem.stubs(:lstat).with(path).returns(stat)
+ end
+
+ def lstat_raises(path, error_class)
+ Puppet::FileSystem.expects(:lstat).with(path).raises Errno::ENOENT
+ end
end
diff --git a/spec/unit/type/user_spec.rb b/spec/unit/type/user_spec.rb
index b8f1e9dbd..d9d48d6fc 100755
--- a/spec/unit/type/user_spec.rb
+++ b/spec/unit/type/user_spec.rb
@@ -1,374 +1,419 @@
#! /usr/bin/env ruby
# encoding: UTF-8
require 'spec_helper'
describe Puppet::Type.type(:user) do
before :each do
@provider_class = described_class.provide(:simple) do
- has_features :manages_expiry, :manages_password_age, :manages_passwords, :manages_solaris_rbac
+ has_features :manages_expiry, :manages_password_age, :manages_passwords, :manages_solaris_rbac, :manages_shell
mk_resource_methods
def create; end
def delete; end
def exists?; get(:ensure) != :absent; end
def flush; end
def self.instances; []; end
end
described_class.stubs(:defaultprovider).returns @provider_class
end
it "should be able to create an instance" do
described_class.new(:name => "foo").should_not be_nil
end
it "should have an allows_duplicates feature" do
described_class.provider_feature(:allows_duplicates).should_not be_nil
end
it "should have a manages_homedir feature" do
described_class.provider_feature(:manages_homedir).should_not be_nil
end
it "should have a manages_passwords feature" do
described_class.provider_feature(:manages_passwords).should_not be_nil
end
it "should have a manages_solaris_rbac feature" do
described_class.provider_feature(:manages_solaris_rbac).should_not be_nil
end
it "should have a manages_expiry feature" do
described_class.provider_feature(:manages_expiry).should_not be_nil
end
it "should have a manages_password_age feature" do
described_class.provider_feature(:manages_password_age).should_not be_nil
end
it "should have a system_users feature" do
described_class.provider_feature(:system_users).should_not be_nil
end
+ it "should have a manages_shell feature" do
+ described_class.provider_feature(:manages_shell).should_not be_nil
+ end
+
describe :managehome do
let (:provider) { @provider_class.new(:name => 'foo', :ensure => :absent) }
let (:instance) { described_class.new(:name => 'foo', :provider => provider) }
it "defaults to false" do
instance[:managehome].should be_false
end
it "can be set to false" do
instance[:managehome] = 'false'
end
it "cannot be set to true for a provider that does not manage homedirs" do
provider.class.stubs(:manages_homedir?).returns false
- expect { instance[:managehome] = 'yes' }.to raise_error Puppet::Error
+ expect { instance[:managehome] = 'yes' }.to raise_error(Puppet::Error, /can not manage home directories/)
end
it "can be set to true for a provider that does manage homedirs" do
provider.class.stubs(:manages_homedir?).returns true
instance[:managehome] = 'yes'
end
end
describe "instances" do
it "should delegate existence questions to its provider" do
@provider = @provider_class.new(:name => 'foo', :ensure => :absent)
instance = described_class.new(:name => "foo", :provider => @provider)
instance.exists?.should == false
@provider.set(:ensure => :present)
instance.exists?.should == true
end
end
properties = [:ensure, :uid, :gid, :home, :comment, :shell, :password, :password_min_age, :password_max_age, :groups, :roles, :auths, :profiles, :project, :keys, :expiry]
properties.each do |property|
it "should have a #{property} property" do
described_class.attrclass(property).ancestors.should be_include(Puppet::Property)
end
it "should have documentation for its #{property} property" do
described_class.attrclass(property).doc.should be_instance_of(String)
end
end
list_properties = [:groups, :roles, :auths]
list_properties.each do |property|
it "should have a list '#{property}'" do
described_class.attrclass(property).ancestors.should be_include(Puppet::Property::List)
end
end
it "should have an ordered list 'profiles'" do
described_class.attrclass(:profiles).ancestors.should be_include(Puppet::Property::OrderedList)
end
it "should have key values 'keys'" do
described_class.attrclass(:keys).ancestors.should be_include(Puppet::Property::KeyValue)
end
describe "when retrieving all current values" do
before do
@provider = @provider_class.new(:name => 'foo', :ensure => :present, :uid => 15, :gid => 15)
@user = described_class.new(:name => "foo", :uid => 10, :provider => @provider)
end
it "should return a hash containing values for all set properties" do
@user[:gid] = 10
values = @user.retrieve
[@user.property(:uid), @user.property(:gid)].each { |property| values.should be_include(property) }
end
it "should set all values to :absent if the user is absent" do
@user.property(:ensure).expects(:retrieve).returns :absent
@user.property(:uid).expects(:retrieve).never
@user.retrieve[@user.property(:uid)].should == :absent
end
it "should include the result of retrieving each property's current value if the user is present" do
@user.retrieve[@user.property(:uid)].should == 15
end
end
describe "when managing the ensure property" do
it "should support a :present value" do
expect { described_class.new(:name => 'foo', :ensure => :present) }.to_not raise_error
end
it "should support an :absent value" do
expect { described_class.new(:name => 'foo', :ensure => :absent) }.to_not raise_error
end
it "should call :create on the provider when asked to sync to the :present state" do
@provider = @provider_class.new(:name => 'foo', :ensure => :absent)
@provider.expects(:create)
described_class.new(:name => 'foo', :ensure => :present, :provider => @provider).parameter(:ensure).sync
end
it "should call :delete on the provider when asked to sync to the :absent state" do
@provider = @provider_class.new(:name => 'foo', :ensure => :present)
@provider.expects(:delete)
described_class.new(:name => 'foo', :ensure => :absent, :provider => @provider).parameter(:ensure).sync
end
describe "and determining the current state" do
it "should return :present when the provider indicates the user exists" do
@provider = @provider_class.new(:name => 'foo', :ensure => :present)
described_class.new(:name => 'foo', :ensure => :absent, :provider => @provider).parameter(:ensure).retrieve.should == :present
end
it "should return :absent when the provider indicates the user does not exist" do
@provider = @provider_class.new(:name => 'foo', :ensure => :absent)
described_class.new(:name => 'foo', :ensure => :present, :provider => @provider).parameter(:ensure).retrieve.should == :absent
end
end
end
describe "when managing the uid property" do
it "should convert number-looking strings into actual numbers" do
described_class.new(:name => 'foo', :uid => '50')[:uid].should == 50
end
it "should support UIDs as numbers" do
described_class.new(:name => 'foo', :uid => 50)[:uid].should == 50
end
it "should support :absent as a value" do
described_class.new(:name => 'foo', :uid => :absent)[:uid].should == :absent
end
end
describe "when managing the gid" do
it "should support :absent as a value" do
described_class.new(:name => 'foo', :gid => :absent)[:gid].should == :absent
end
it "should convert number-looking strings into actual numbers" do
described_class.new(:name => 'foo', :gid => '50')[:gid].should == 50
end
it "should support GIDs specified as integers" do
described_class.new(:name => 'foo', :gid => 50)[:gid].should == 50
end
it "should support groups specified by name" do
described_class.new(:name => 'foo', :gid => 'foo')[:gid].should == 'foo'
end
describe "when testing whether in sync" do
it "should return true if no 'should' values are set" do
# this is currently not the case because gid has no default value, so we would never even
# call insync? on that property
if param = described_class.new(:name => 'foo').parameter(:gid)
param.must be_safe_insync(500)
end
end
it "should return true if any of the specified groups are equal to the current integer" do
Puppet::Util.expects(:gid).with("foo").returns 300
Puppet::Util.expects(:gid).with("bar").returns 500
described_class.new(:name => 'baz', :gid => [ 'foo', 'bar' ]).parameter(:gid).must be_safe_insync(500)
end
it "should return false if none of the specified groups are equal to the current integer" do
Puppet::Util.expects(:gid).with("foo").returns 300
Puppet::Util.expects(:gid).with("bar").returns 500
described_class.new(:name => 'baz', :gid => [ 'foo', 'bar' ]).parameter(:gid).must_not be_safe_insync(700)
end
end
describe "when syncing" do
it "should use the first found, specified group as the desired value and send it to the provider" do
Puppet::Util.expects(:gid).with("foo").returns nil
Puppet::Util.expects(:gid).with("bar").returns 500
@provider = @provider_class.new(:name => 'foo')
resource = described_class.new(:name => 'foo', :provider => @provider, :gid => [ 'foo', 'bar' ])
@provider.expects(:gid=).with 500
resource.parameter(:gid).sync
end
end
end
describe "when managing groups" do
it "should support a singe group" do
expect { described_class.new(:name => 'foo', :groups => 'bar') }.to_not raise_error
end
it "should support multiple groups as an array" do
expect { described_class.new(:name => 'foo', :groups => [ 'bar' ]) }.to_not raise_error
expect { described_class.new(:name => 'foo', :groups => [ 'bar', 'baz' ]) }.to_not raise_error
end
it "should not support a comma separated list" do
expect { described_class.new(:name => 'foo', :groups => 'bar,baz') }.to raise_error(Puppet::Error, /Group names must be provided as an array/)
end
it "should not support an empty string" do
expect { described_class.new(:name => 'foo', :groups => '') }.to raise_error(Puppet::Error, /Group names must not be empty/)
end
describe "when testing is in sync" do
before :each do
# the useradd provider uses a single string to represent groups and so does Puppet::Property::List when converting to should values
@provider = @provider_class.new(:name => 'foo', :groups => 'a,b,e,f')
end
it "should not care about order" do
@property = described_class.new(:name => 'foo', :groups => [ 'a', 'c', 'b' ]).property(:groups)
@property.must be_safe_insync([ 'a', 'b', 'c' ])
@property.must be_safe_insync([ 'a', 'c', 'b' ])
@property.must be_safe_insync([ 'b', 'a', 'c' ])
@property.must be_safe_insync([ 'b', 'c', 'a' ])
@property.must be_safe_insync([ 'c', 'a', 'b' ])
@property.must be_safe_insync([ 'c', 'b', 'a' ])
end
it "should merge current value and desired value if membership minimal" do
@instance = described_class.new(:name => 'foo', :groups => [ 'a', 'c', 'b' ], :provider => @provider)
@instance[:membership] = :minimum
@instance[:groups].should == 'a,b,c,e,f'
end
it "should not treat a subset of groups insync if membership inclusive" do
@instance = described_class.new(:name => 'foo', :groups => [ 'a', 'c', 'b' ], :provider => @provider)
@instance[:membership] = :inclusive
@instance[:groups].should == 'a,b,c'
end
end
end
describe "when managing expiry" do
it "should fail if given an invalid date" do
expect { described_class.new(:name => 'foo', :expiry => "200-20-20") }.to raise_error(Puppet::Error, /Expiry dates must be YYYY-MM-DD/)
end
end
describe "when managing minimum password age" do
it "should accept a negative minimum age" do
expect { described_class.new(:name => 'foo', :password_min_age => '-1') }.to_not raise_error
end
it "should fail with an empty minimum age" do
expect { described_class.new(:name => 'foo', :password_min_age => '') }.to raise_error(Puppet::Error, /minimum age must be provided as a number/)
end
end
describe "when managing maximum password age" do
it "should accept a negative maximum age" do
expect { described_class.new(:name => 'foo', :password_max_age => '-1') }.to_not raise_error
end
it "should fail with an empty maximum age" do
expect { described_class.new(:name => 'foo', :password_max_age => '') }.to raise_error(Puppet::Error, /maximum age must be provided as a number/)
end
end
describe "when managing passwords" do
before do
@password = described_class.new(:name => 'foo', :password => 'mypass').parameter(:password)
end
it "should not include the password in the change log when adding the password" do
@password.change_to_s(:absent, "mypass").should_not be_include("mypass")
end
it "should not include the password in the change log when changing the password" do
@password.change_to_s("other", "mypass").should_not be_include("mypass")
end
it "should redact the password when displaying the old value" do
@password.is_to_s("currentpassword").should =~ /^\[old password hash redacted\]$/
end
it "should redact the password when displaying the new value" do
@password.should_to_s("newpassword").should =~ /^\[new password hash redacted\]$/
end
it "should fail if a ':' is included in the password" do
expect { described_class.new(:name => 'foo', :password => "some:thing") }.to raise_error(Puppet::Error, /Passwords cannot include ':'/)
end
it "should allow the value to be set to :absent" do
expect { described_class.new(:name => 'foo', :password => :absent) }.to_not raise_error
end
end
describe "when managing comment on Ruby 1.9", :if => String.method_defined?(:encode) do
it "should force value encoding to ASCII-8BIT" do
value = 'abcd™'
value.encoding.should == Encoding::UTF_8
user = described_class.new(:name => 'foo', :comment => value)
user[:comment].encoding.should == Encoding::ASCII_8BIT
user[:comment].should == value.force_encoding(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
end
end
describe "when user has roles" do
it "should autorequire roles" do
testuser = described_class.new(:name => "testuser", :roles => ['testrole'] )
testrole = described_class.new(:name => "testrole")
config = Puppet::Resource::Catalog.new :testing do |conf|
[testuser, testrole].each { |resource| conf.add_resource resource }
end
Puppet::Type::User::ProviderDirectoryservice.stubs(:get_macosx_version_major).returns "10.5"
rel = testuser.autorequire[0]
rel.source.ref.should == testrole.ref
rel.target.ref.should == testuser.ref
end
end
+
+ describe "when setting shell" do
+ before :each do
+ @shell_provider_class = described_class.provide(:shell_manager) do
+ has_features :manages_shell
+ mk_resource_methods
+ def create; check_valid_shell;end
+ def shell=(value); check_valid_shell; end
+ def delete; end
+ def exists?; get(:ensure) != :absent; end
+ def flush; end
+ def self.instances; []; end
+ def check_valid_shell; end
+ end
+
+ described_class.stubs(:defaultprovider).returns @shell_provider_class
+ end
+
+ it "should call :check_valid_shell on the provider when changing shell value" do
+ @provider = @shell_provider_class.new(:name => 'foo', :shell => '/bin/bash', :ensure => :present)
+ @provider.expects(:check_valid_shell)
+ resource = described_class.new(:name => 'foo', :shell => '/bin/zsh', :provider => @provider)
+ Puppet::Util::Storage.stubs(:load)
+ Puppet::Util::Storage.stubs(:store)
+ catalog = Puppet::Resource::Catalog.new
+ catalog.add_resource resource
+ catalog.apply
+ end
+
+ it "should call :check_valid_shell on the provider when changing ensure from present to absent" do
+ @provider = @shell_provider_class.new(:name => 'foo', :shell => '/bin/bash', :ensure => :absent)
+ @provider.expects(:check_valid_shell)
+ resource = described_class.new(:name => 'foo', :shell => '/bin/zsh', :provider => @provider)
+ Puppet::Util::Storage.stubs(:load)
+ Puppet::Util::Storage.stubs(:store)
+ catalog = Puppet::Resource::Catalog.new
+ catalog.add_resource resource
+ catalog.apply
+ end
+
+ end
end
diff --git a/spec/unit/type/whit_spec.rb b/spec/unit/type/whit_spec.rb
index f18e845ad..73498e55d 100755
--- a/spec/unit/type/whit_spec.rb
+++ b/spec/unit/type/whit_spec.rb
@@ -1,10 +1,10 @@
#! /usr/bin/env ruby
require 'spec_helper'
-whit = Puppet::Type.type(:whit).new(:name => "Foo::Bar")
+whit = Puppet::Type.type(:whit)
describe whit do
it "should stringify in a way that users will regognise" do
- whit.to_s.should == "Foo::Bar"
+ whit.new(:name => "Foo::Bar").to_s.should == "Foo::Bar"
end
end
diff --git a/spec/unit/type/yumrepo_spec.rb b/spec/unit/type/yumrepo_spec.rb
index 66ddfd3e9..25dd8d833 100644
--- a/spec/unit/type/yumrepo_spec.rb
+++ b/spec/unit/type/yumrepo_spec.rb
@@ -1,223 +1,78 @@
-#! /usr/bin/env ruby
-
require 'spec_helper'
+require 'puppet'
describe Puppet::Type.type(:yumrepo) do
- include PuppetSpec::Files
+ let(:yumrepo) {
+ Puppet::Type.type(:yumrepo).new(
+ :name => "puppetlabs"
+ )
+ }
describe "When validating attributes" do
it "should have a 'name' parameter'" do
- Puppet::Type.type(:yumrepo).new(:name => "puppetlabs")[:name].should == "puppetlabs"
+ yumrepo[:name].should == "puppetlabs"
end
- [:baseurl, :cost, :descr, :enabled, :enablegroups, :exclude, :failovermethod, :gpgcheck, :gpgkey, :http_caching,
- :include, :includepkgs, :keepalive, :metadata_expire, :mirrorlist, :priority, :protect, :proxy, :proxy_username, :proxy_password, :timeout,
- :sslcacert, :sslverify, :sslclientcert, :sslclientkey, :s3_enabled].each do |param|
+ [:baseurl, :cost, :descr, :enabled, :enablegroups, :exclude, :failovermethod,
+ :gpgcheck, :repo_gpgcheck, :gpgkey, :http_caching, :include, :includepkgs, :keepalive,
+ :metadata_expire, :mirrorlist, :priority, :protect, :proxy, :proxy_username,
+ :proxy_password, :timeout, :sslcacert, :sslverify, :sslclientcert,
+ :sslclientkey, :s3_enabled, :metalink].each do |param|
it "should have a '#{param}' parameter" do
Puppet::Type.type(:yumrepo).attrtype(param).should == :property
end
end
end
describe "When validating attribute values" do
- [:cost, :enabled, :enablegroups, :failovermethod, :gpgcheck, :http_caching, :keepalive, :metadata_expire, :priority, :protect, :timeout].each do |param|
+ [:cost, :enabled, :enablegroups, :failovermethod, :gpgcheck, :repo_gpgcheck, :http_caching,
+ :keepalive, :metadata_expire, :priority, :protect, :timeout].each do |param|
it "should support :absent as a value to '#{param}' parameter" do
- Puppet::Type.type(:yumrepo).new(:name => "puppetlabs.repo", param => :absent)
+ Puppet::Type.type(:yumrepo).new(:name => 'puppetlabs', param => :absent)
end
end
- [:cost, :enabled, :enablegroups, :gpgcheck, :keepalive, :metadata_expire, :priority, :protect, :timeout].each do |param|
- it "should fail if '#{param}' is not a number" do
- lambda { Puppet::Type.type(:yumrepo).new(:name => "puppetlabs", param => "notanumber") }.should raise_error
+ [:cost, :enabled, :enablegroups, :gpgcheck, :repo_gpgcheck, :keepalive, :metadata_expire,
+ :priority, :protect, :timeout].each do |param|
+ it "should fail if '#{param}' is not true/false, 0/1, or yes/no" do
+ expect { Puppet::Type.type(:yumrepo).new(:name => "puppetlabs", param => "notanumber") }.to raise_error
end
end
- [:enabled, :enabledgroups, :gpgcheck, :keepalive, :protect, :s3_enabled].each do |param|
+ [:enabled, :enabledgroups, :gpgcheck, :repo_gpgcheck, :keepalive, :protect, :s3_enabled].each do |param|
it "should fail if '#{param}' does not have one of the following values (0|1)" do
- lambda { Puppet::Type.type(:yumrepo).new(:name => "puppetlabs", param => "2") }.should raise_error
+ expect { Puppet::Type.type(:yumrepo).new(:name => "puppetlabs", param => "2") }.to raise_error
end
end
it "should fail if 'failovermethod' does not have one of the following values (roundrobin|priority)" do
- lambda { Puppet::Type.type(:yumrepo).new(:name => "puppetlabs", :failovermethod => "notavalidvalue") }.should raise_error
+ expect { Puppet::Type.type(:yumrepo).new(:name => "puppetlabs", :failovermethod => "notavalidvalue") }.to raise_error
end
it "should fail if 'http_caching' does not have one of the following values (packages|all|none)" do
- lambda { Puppet::Type.type(:yumrepo).new(:name => "puppetlabs", :http_caching => "notavalidvalue") }.should raise_error
+ expect { Puppet::Type.type(:yumrepo).new(:name => "puppetlabs", :http_caching => "notavalidvalue") }.to raise_error
end
it "should fail if 'sslverify' does not have one of the following values (True|False)" do
- lambda { Puppet::Type.type(:yumrepo).new(:name => "puppetlabs", :sslverify => "notavalidvalue") }.should raise_error
+ expect { Puppet::Type.type(:yumrepo).new(:name => "puppetlabs", :sslverify => "notavalidvalue") }.to raise_error
end
it "should succeed if 'sslverify' has one of the following values (True|False)" do
Puppet::Type.type(:yumrepo).new(:name => "puppetlabs", :sslverify => "True")[:sslverify].should == "True"
Puppet::Type.type(:yumrepo).new(:name => "puppetlabs", :sslverify => "False")[:sslverify].should == "False"
end
- end
-
- # these tests were ported from the old spec/unit/type/yumrepo_spec.rb, pretty much verbatim
- describe "When manipulating config file" do
- def make_repo(name, hash={})
- hash[:name] = name
- Puppet::Type.type(:yumrepo).new(hash)
- end
- def all_sections(inifile)
- sections = []
- inifile.each_section { |section| sections << section.name }
- sections.sort
- end
-
- def create_data_files()
- File.open(File.join(@yumdir, "fedora.repo"), "w") do |f|
- f.print(FEDORA_REPO_FILE)
+ [:mirrorlist, :baseurl, :gpgkey, :include, :proxy, :metalink].each do |param|
+ it "should succeed if '#{param}' uses one of the following protocols (file|http|https|ftp)" do
+ Puppet::Type.type(:yumrepo).new(:name => "puppetlabs", param => "file:///srv/example/")[param].should =~ %r{\Afile://}
+ Puppet::Type.type(:yumrepo).new(:name => "puppetlabs", param => "http://example.com/")[param].should =~ %r{\Ahttp://}
+ Puppet::Type.type(:yumrepo).new(:name => "puppetlabs", param => "https://example.com/")[param].should =~ %r{\Ahttps://}
+ Puppet::Type.type(:yumrepo).new(:name => "puppetlabs", param => "ftp://example.com/")[param].should =~ %r{\Aftp://}
end
- File.open(File.join(@yumdir, "fedora-devel.repo"), "w") do |f|
- f.print(FEDORA_DEVEL_REPO_FILE)
+ it "should fail if '#{param}' does not use one of the following protocols (file|http|https|ftp)" do
+ expect { Puppet::Type.type(:yumrepo).new(:name => "puppetlabs", param => "gopher://example.com/") }.to raise_error
end
end
-
- before(:each) do
- @yumdir = tmpdir("yumrepo_spec_tmpdir")
- @yumconf = File.join(@yumdir, "yum.conf")
- File.open(@yumconf, "w") do |f|
- f.print "[main]\nreposdir=#{@yumdir} /no/such/dir\n"
- end
- Puppet::Type.type(:yumrepo).yumconf = @yumconf
-
- # It needs to be reset each time, otherwise the cache is used.
- Puppet::Type.type(:yumrepo).inifile = nil
- end
-
- it "should be able to create a valid config file" do
- values = {
- :descr => "Fedora Core $releasever - $basearch - Base",
- :baseurl => "http://example.com/yum/$releasever/$basearch/os/",
- :enabled => "1",
- :gpgcheck => "1",
- :includepkgs => "absent",
- :gpgkey => "file:///etc/pki/rpm-gpg/RPM-GPG-KEY-fedora",
- :proxy => "http://proxy.example.com:80/",
- :proxy_username => "username",
- :proxy_password => "password"
- }
- repo = make_repo("base", values)
-
- catalog = Puppet::Resource::Catalog.new
- # Stop Puppet from doing a bunch of magic; might want to think about a util for specs that handles this
- catalog.host_config = false
- catalog.add_resource(repo)
- catalog.apply
-
- inifile = Puppet::Type.type(:yumrepo).read
- sections = all_sections(inifile)
- sections.should == ['base', 'main']
- text = inifile["base"].format
- text.should == EXPECTED_CONTENTS_FOR_CREATED_FILE
- end
-
- # Modify one existing section
- it "should be able to modify an existing config file" do
- create_data_files
-
- devel = make_repo("development", { :descr => "New description" })
- current_values = devel.retrieve
-
- devel[:name].should == "development"
- current_values[devel.property(:descr)].should == 'Fedora Core $releasever - Development Tree'
- devel[:descr].should == 'New description'
-
- catalog = Puppet::Resource::Catalog.new
- # Stop Puppet from doing a bunch of magic; might want to think about a util for specs that handles this
- catalog.host_config = false
- catalog.add_resource(devel)
- catalog.apply
-
- inifile = Puppet::Type.type(:yumrepo).read
- inifile['development']['name'].should == 'New description'
- inifile['base']['name'].should == 'Fedora Core $releasever - $basearch - Base'
- inifile['base']['exclude'].should == "foo\n bar\n baz"
- all_sections(inifile).should == ['base', 'development', 'main']
- end
-
- # Delete mirrorlist by setting it to :absent and enable baseurl
- it "should support 'absent' value" do
- create_data_files
-
- baseurl = 'http://example.com/'
-
- devel = make_repo(
- "development",
- { :mirrorlist => 'absent',
-
- :baseurl => baseurl })
- devel.retrieve
-
- catalog = Puppet::Resource::Catalog.new
- # Stop Puppet from doing a bunch of magic; might want to think about a util for specs that handles this
- catalog.host_config = false
- catalog.add_resource(devel)
- catalog.apply
-
- inifile = Puppet::Type.type(:yumrepo).read
- sec = inifile["development"]
- sec["mirrorlist"].should == nil
- sec["baseurl"].should == baseurl
- end
end
end
-
-EXPECTED_CONTENTS_FOR_CREATED_FILE = <<'EOF'
-[base]
-name=Fedora Core $releasever - $basearch - Base
-baseurl=http://example.com/yum/$releasever/$basearch/os/
-enabled=1
-gpgcheck=1
-gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-fedora
-proxy=http://proxy.example.com:80/
-proxy_username=username
-proxy_password=password
-EOF
-
-FEDORA_REPO_FILE = <<END
-[base]
-name=Fedora Core $releasever - $basearch - Base
-mirrorlist=http://fedora.redhat.com/download/mirrors/fedora-core-$releasever
-enabled=1
-gpgcheck=1
-gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-fedora
-exclude=foo
- bar
- baz
-END
-
-FEDORA_DEVEL_REPO_FILE = <<END
-[development]
-# These packages are untested and still under development. This
-# repository is used for updates to test releases, and for
-# development of new releases.
-#
-# This repository can see significant daily turn over and can see major
-# functionality changes which cause unexpected problems with other
-# development packages. Please use these packages if you want to work
-# with the Fedora developers by testing these new development packages.
-#
-# fedora-test-list@redhat.com is available as a discussion forum for
-# testing and troubleshooting for development packages in conjunction
-# with new test releases.
-#
-# fedora-devel-list@redhat.com is available as a discussion forum for
-# testing and troubleshooting for development packages in conjunction
-# with developing new releases.
-#
-# Reportable issues should be filed at bugzilla.redhat.com
-# Product: Fedora Core
-# Version: devel
-name=Fedora Core $releasever - Development Tree
-#baseurl=http://download.fedora.redhat.com/pub/fedora/linux/core/development/$basearch/
-mirrorlist=http://fedora.redhat.com/download/mirrors/fedora-core-rawhide
-enabled=0
-gpgcheck=0
-END
diff --git a/spec/unit/type/zone_spec.rb b/spec/unit/type/zone_spec.rb
index b34cf5841..e54902627 100755
--- a/spec/unit/type/zone_spec.rb
+++ b/spec/unit/type/zone_spec.rb
@@ -1,129 +1,129 @@
#! /usr/bin/env ruby
require 'spec_helper'
describe Puppet::Type.type(:zone) do
let(:zone) { described_class.new(:name => 'dummy', :path => '/dummy', :provider => :solaris) }
let(:provider) { zone.provider }
parameters = [:create_args, :install_args, :sysidcfg, :realhostname]
parameters.each do |parameter|
it "should have a #{parameter} parameter" do
described_class.attrclass(parameter).ancestors.should be_include(Puppet::Parameter)
end
end
properties = [:ip, :iptype, :autoboot, :pool, :shares, :inherit, :path]
properties.each do |property|
it "should have a #{property} property" do
described_class.attrclass(property).ancestors.should be_include(Puppet::Property)
end
end
it "should be valid when only :path is given" do
described_class.new(:name => "dummy", :path => '/dummy', :provider => :solaris)
end
it "should be invalid when :ip is missing a \":\" and iptype is :shared" do
expect {
described_class.new(:name => "dummy", :ip => "if", :path => "/dummy", :provider => :solaris)
}.to raise_error(Puppet::Error, /ip must contain interface name and ip address separated by a ":"/)
end
it "should be invalid when :ip has a \":\" and iptype is :exclusive" do
expect {
described_class.new(:name => "dummy", :ip => "if:1.2.3.4", :iptype => :exclusive, :provider => :solaris)
}.to raise_error(Puppet::Error, /only interface may be specified when using exclusive IP stack/)
end
it "should be invalid when :ip has two \":\" and iptype is :exclusive" do
expect {
described_class.new(:name => "dummy", :ip => "if:1.2.3.4:2.3.4.5", :iptype => :exclusive, :provider => :solaris)
}.to raise_error(Puppet::Error, /only interface may be specified when using exclusive IP stack/)
end
it "should be valid when :iptype is :shared and using interface and ip" do
described_class.new(:name => "dummy", :path => "/dummy", :ip => "if:1.2.3.4", :provider => :solaris)
end
it "should be valid when :iptype is :shared and using interface, ip and default route" do
described_class.new(:name => "dummy", :path => "/dummy", :ip => "if:1.2.3.4:2.3.4.5", :provider => :solaris)
end
it "should be valid when :iptype is :exclusive and using interface" do
described_class.new(:name => "dummy", :path => "/dummy", :ip => "if", :iptype => :exclusive, :provider => :solaris)
end
it "should auto-require :dataset entries" do
fs = 'random-pool/some-zfs'
catalog = Puppet::Resource::Catalog.new
relationship_graph = Puppet::Graph::RelationshipGraph.new(Puppet::Graph::RandomPrioritizer.new)
zfs = Puppet::Type.type(:zfs).new(:name => fs)
catalog.add_resource zfs
zone = described_class.new(:name => "dummy",
:path => "/foo",
:ip => 'en1:1.0.0.0',
:dataset => fs,
:provider => :solaris)
catalog.add_resource zone
relationship_graph.populate_from(catalog)
relationship_graph.dependencies(zone).should == [zfs]
end
- describe StateMachine do
- let (:sm) { StateMachine.new }
+ describe Puppet::Zone::StateMachine do
+ let (:sm) { Puppet::Zone::StateMachine.new }
before :each do
sm.insert_state :absent, :down => :destroy
sm.insert_state :configured, :up => :configure, :down => :uninstall
sm.insert_state :installed, :up => :install, :down => :stop
sm.insert_state :running, :up => :start
end
context ":insert_state" do
it "should insert state in correct order" do
sm.insert_state :dummy, :left => :right
sm.index(:dummy).should == 4
end
end
context ":alias_state" do
it "should alias state" do
sm.alias_state :dummy, :running
sm.name(:dummy).should == :running
end
end
context ":name" do
it "should get an aliased state correctly" do
sm.alias_state :dummy, :running
sm.name(:dummy).should == :running
end
it "should get an un aliased state correctly" do
sm.name(:dummy).should == :dummy
end
end
context ":index" do
it "should return the state index correctly" do
sm.insert_state :dummy, :left => :right
sm.index(:dummy).should == 4
end
end
context ":sequence" do
it "should correctly return the actions to reach state specified" do
sm.sequence(:absent, :running).map{|p|p[:up]}.should == [:configure,:install,:start]
end
it "should correctly return the actions to reach state specified(2)" do
sm.sequence(:running, :absent).map{|p|p[:down]}.should == [:stop, :uninstall, :destroy]
end
end
context ":cmp" do
it "should correctly compare state sequence values" do
sm.cmp?(:absent, :running).should == true
sm.cmp?(:running, :running).should == false
sm.cmp?(:running, :absent).should == false
end
end
end
end
diff --git a/spec/unit/type_spec.rb b/spec/unit/type_spec.rb
index ca29d680e..75a5a0768 100755
--- a/spec/unit/type_spec.rb
+++ b/spec/unit/type_spec.rb
@@ -1,1083 +1,1094 @@
#! /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")
b = Puppet::Type.type(:notify).new(:name => "b")
c = Puppet::Type.type(:notify).new(:name => "c")
[[a, b, c], [a, c, b], [b, a, c], [b, c, a], [c, a, b], [c, b, a]].each do |this|
this.sort.should == [a, b, c]
end
a.must be < b
a.must be < c
b.must be > a
b.must be < c
c.must be > a
c.must be > b
[a, b, c].each {|x| a.must be <= x }
[a, b, c].each {|x| c.must be >= x }
b.must be_between(a, c)
end
it "should consider a parameter to be valid if it is a valid parameter" do
Puppet::Type.type(:mount).should be_valid_parameter(:name)
end
it "should consider a parameter to be valid if it is a valid property" do
Puppet::Type.type(:mount).should be_valid_parameter(:fstype)
end
it "should consider a parameter to be valid if it is a valid metaparam" do
Puppet::Type.type(:mount).should be_valid_parameter(:noop)
end
it "should be able to retrieve a property by name" do
resource = Puppet::Type.type(:mount).new(:name => "foo", :fstype => "bar", :pass => 1, :ensure => :present)
resource.property(:fstype).must be_instance_of(Puppet::Type.type(:mount).attrclass(:fstype))
end
it "should be able to retrieve a parameter by name" do
resource = Puppet::Type.type(:mount).new(:name => "foo", :fstype => "bar", :pass => 1, :ensure => :present)
resource.parameter(:name).must be_instance_of(Puppet::Type.type(:mount).attrclass(:name))
end
it "should be able to retrieve a property by name using the :parameter method" do
resource = Puppet::Type.type(:mount).new(:name => "foo", :fstype => "bar", :pass => 1, :ensure => :present)
resource.parameter(:fstype).must be_instance_of(Puppet::Type.type(:mount).attrclass(:fstype))
end
it "should be able to retrieve all set properties" do
resource = Puppet::Type.type(:mount).new(:name => "foo", :fstype => "bar", :pass => 1, :ensure => :present)
props = resource.properties
props.should_not be_include(nil)
[:fstype, :ensure, :pass].each do |name|
props.should be_include(resource.parameter(name))
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
it "should do nothing for attributes that have no defaults and no specified value" do
Puppet::Type.type(:mount).new(:name => "foo").parameter(:noop).should be_nil
end
it "should have a method for adding tags" do
Puppet::Type.type(:mount).new(:name => "foo").must respond_to(:tags)
end
it "should use the tagging module" do
Puppet::Type.type(:mount).ancestors.should be_include(Puppet::Util::Tagging)
end
it "should delegate to the tagging module when tags are added" do
resource = Puppet::Type.type(:mount).new(:name => "foo")
resource.stubs(:tag).with(:mount)
resource.expects(:tag).with(:tag1, :tag2)
resource.tags = [:tag1,:tag2]
end
it "should add the current type as tag" do
resource = Puppet::Type.type(:mount).new(:name => "foo")
resource.stubs(:tag)
resource.expects(:tag).with(:mount)
resource.tags = [:tag1,:tag2]
end
it "should have a method to know if the resource is exported" do
Puppet::Type.type(:mount).new(:name => "foo").must respond_to(:exported?)
end
it "should have a method to know if the resource is virtual" do
Puppet::Type.type(:mount).new(:name => "foo").must respond_to(:virtual?)
end
it "should consider its version to be zero if it has no catalog" 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")
catalog = Puppet::Resource::Catalog.new
catalog.version = 50
catalog.add_resource resource
resource
}
it "should consider its version to be its catalog version" do
resource.version.should == 50
end
it "should have tags" do
expect(resource).to be_tagged("mount")
expect(resource).to be_tagged("foo")
end
it "should have a path" do
resource.path.should == "/Mount[foo]"
end
end
it "should consider its type to be the name of its class" do
Puppet::Type.type(:mount).new(:name => "foo").type.should == :mount
end
it "should use any provided noop value" do
Puppet::Type.type(:mount).new(:name => "foo", :noop => true).must be_noop
end
it "should use the global noop value if none is provided" do
Puppet[:noop] = true
Puppet::Type.type(:mount).new(:name => "foo").must be_noop
end
it "should not be noop if in a non-host_config catalog" do
resource = Puppet::Type.type(:mount).new(:name => "foo")
catalog = Puppet::Resource::Catalog.new
catalog.add_resource resource
resource.should_not be_noop
end
describe "when creating an event" do
before do
@resource = Puppet::Type.type(:mount).new :name => "foo"
end
it "should have the resource's reference as the resource" do
@resource.event.resource.should == "Mount[foo]"
end
it "should have the resource's log level as the default log level" do
@resource[:loglevel] = :warning
@resource.event.default_log_level.should == :warning
end
{: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
end
describe "when creating a provider" do
before :each do
@type = Puppet::Type.newtype(:provider_test_type) do
newparam(:name) { isnamevar }
newparam(:foo)
newproperty(:bar)
end
end
after :each do
@type.provider_hash.clear
end
describe "when determining if instances of the type are managed" do
it "should not consider audit only resources to be managed" do
@type.new(:name => "foo", :audit => 'all').managed?.should be_false
end
it "should not consider resources with only parameters to be managed" do
@type.new(:name => "foo", :foo => 'did someone say food?').managed?.should be_false
end
it "should consider resources with any properties set to be managed" do
@type.new(:name => "foo", :bar => 'Let us all go there').managed?.should be_true
end
end
it "should have documentation for the 'provider' parameter if there are providers" do
@type.provide(:test_provider)
@type.paramdoc(:provider).should =~ /`provider_test_type`[\s\r]+resource/
end
it "should not have documentation for the 'provider' parameter if there are no providers" do
expect { @type.paramdoc(:provider) }.to raise_error(NoMethodError)
end
it "should create a subclass of Puppet::Provider for the provider" do
provider = @type.provide(:test_provider)
provider.ancestors.should include(Puppet::Provider)
end
it "should use a parent class if specified" do
parent_provider = @type.provide(:parent_provider)
child_provider = @type.provide(:child_provider, :parent => parent_provider)
child_provider.ancestors.should include(parent_provider)
end
it "should use a parent class if specified by name" do
parent_provider = @type.provide(:parent_provider)
child_provider = @type.provide(:child_provider, :parent => :parent_provider)
child_provider.ancestors.should include(parent_provider)
end
it "should raise an error when the parent class can't be found" do
expect {
@type.provide(:child_provider, :parent => :parent_provider)
}.to raise_error(Puppet::DevError, /Could not find parent provider.+parent_provider/)
end
it "should ensure its type has a 'provider' parameter" do
@type.provide(:test_provider)
@type.parameters.should include(:provider)
end
it "should remove a previously registered provider with the same name" do
old_provider = @type.provide(:test_provider)
new_provider = @type.provide(:test_provider)
old_provider.should_not equal(new_provider)
end
it "should register itself as a provider for the type" do
provider = @type.provide(:test_provider)
provider.should == @type.provider(:test_provider)
end
it "should create a provider when a provider with the same name previously failed" do
@type.provide(:test_provider) do
raise "failed to create this provider"
end rescue nil
provider = @type.provide(:test_provider)
provider.ancestors.should include(Puppet::Provider)
provider.should == @type.provider(:test_provider)
end
end
describe "when choosing a default provider" do
it "should choose the provider with the highest specificity" do
# Make a fake type
type = Puppet::Type.newtype(:defaultprovidertest) do
newparam(:name) do end
end
basic = type.provide(:basic) {}
greater = type.provide(:greater) {}
basic.stubs(:specificity).returns 1
greater.stubs(:specificity).returns 2
type.defaultprovider.should equal(greater)
end
end
+
+ describe "when defining a parent on a newtype" do
+ it "prints a deprecation message" do
+ Puppet.expects(:deprecation_warning)
+ type = Puppet::Type.newtype(:test_with_parent, :parent => Puppet::Type) do
+ newparam(:name) do end
+ end
+ end
+ end
+
+
describe "when initializing" do
describe "and passed a Puppet::Resource instance" do
it "should set its title to the title of the resource if the resource type is equal to the current type" do
resource = Puppet::Resource.new(:mount, "/foo", :parameters => {:name => "/other"})
Puppet::Type.type(:mount).new(resource).title.should == "/foo"
end
it "should set its title to the resource reference if the resource type is not equal to the current type" do
resource = Puppet::Resource.new(:user, "foo")
Puppet::Type.type(:mount).new(resource).title.should == "User[foo]"
end
[:line, :file, :catalog, :exported, :virtual].each do |param|
it "should copy '#{param}' from the resource if present" do
resource = Puppet::Resource.new(:mount, "/foo")
resource.send(param.to_s + "=", "foo")
resource.send(param.to_s + "=", "foo")
Puppet::Type.type(:mount).new(resource).send(param).should == "foo"
end
end
it "should copy any tags from the resource" do
resource = Puppet::Resource.new(:mount, "/foo")
resource.tag "one", "two"
tags = Puppet::Type.type(:mount).new(resource).tags
tags.should be_include("one")
tags.should be_include("two")
end
it "should copy the resource's parameters as its own" do
resource = Puppet::Resource.new(:mount, "/foo", :parameters => {:atboot => :yes, :fstype => "boo"})
params = Puppet::Type.type(:mount).new(resource).to_hash
params[:fstype].should == "boo"
params[:atboot].should == :yes
end
end
describe "and passed a Hash" do
it "should extract the title from the hash" do
Puppet::Type.type(:mount).new(:title => "/yay").title.should == "/yay"
end
it "should work when hash keys are provided as strings" do
Puppet::Type.type(:mount).new("title" => "/yay").title.should == "/yay"
end
it "should work when hash keys are provided as symbols" do
Puppet::Type.type(:mount).new(:title => "/yay").title.should == "/yay"
end
it "should use the name from the hash as the title if no explicit title is provided" do
Puppet::Type.type(:mount).new(:name => "/yay").title.should == "/yay"
end
it "should use the Resource Type's namevar to determine how to find the name in the hash" do
yay = make_absolute('/yay')
Puppet::Type.type(:file).new(:path => yay).title.should == yay
end
[:catalog].each do |param|
it "should extract '#{param}' from the hash if present" do
Puppet::Type.type(:mount).new(:name => "/yay", param => "foo").send(param).should == "foo"
end
end
it "should use any remaining hash keys as its parameters" do
resource = Puppet::Type.type(:mount).new(:title => "/foo", :catalog => "foo", :atboot => :yes, :fstype => "boo")
resource[:fstype].must == "boo"
resource[:atboot].must == :yes
end
end
it "should fail if any invalid attributes have been provided" do
expect { Puppet::Type.type(:mount).new(:title => "/foo", :nosuchattr => "whatever") }.to raise_error(Puppet::Error, /Invalid parameter/)
end
context "when an attribute fails validation" do
it "should fail with Puppet::ResourceError when PuppetError raised" do
expect { Puppet::Type.type(:file).new(:title => "/foo", :source => "unknown:///") }.to raise_error(Puppet::ResourceError, /Parameter source failed on File\[.*foo\]/)
end
it "should fail with Puppet::ResourceError when ArgumentError raised" do
expect { Puppet::Type.type(:file).new(:title => "/foo", :mode => "abcdef") }.to raise_error(Puppet::ResourceError, /Parameter mode failed on File\[.*foo\]/)
end
it "should include the file/line in the error" do
Puppet::Type.type(:file).any_instance.stubs(:file).returns("example.pp")
Puppet::Type.type(:file).any_instance.stubs(:line).returns(42)
expect { Puppet::Type.type(:file).new(:title => "/foo", :source => "unknown:///") }.to raise_error(Puppet::ResourceError, /example.pp:42/)
end
end
it "should set its name to the resource's title if the resource does not have a :name or namevar parameter set" do
resource = Puppet::Resource.new(:mount, "/foo")
Puppet::Type.type(:mount).new(resource).name.should == "/foo"
end
it "should fail if no title, name, or namevar are provided" do
expect { Puppet::Type.type(:mount).new(:atboot => :yes) }.to raise_error(Puppet::Error)
end
it "should set the attributes in the order returned by the class's :allattrs method" do
Puppet::Type.type(:mount).stubs(:allattrs).returns([:name, :atboot, :noop])
resource = Puppet::Resource.new(:mount, "/foo", :parameters => {:name => "myname", :atboot => :yes, :noop => "whatever"})
set = []
Puppet::Type.type(:mount).any_instance.stubs(:newattr).with do |param, hash|
set << param
true
end.returns(stub_everything("a property"))
Puppet::Type.type(:mount).new(resource)
set[-1].should == :noop
set[-2].should == :atboot
end
it "should always set the name and then default provider before anything else" do
Puppet::Type.type(:mount).stubs(:allattrs).returns([:provider, :name, :atboot])
resource = Puppet::Resource.new(:mount, "/foo", :parameters => {:name => "myname", :atboot => :yes})
set = []
Puppet::Type.type(:mount).any_instance.stubs(:newattr).with do |param, hash|
set << param
true
end.returns(stub_everything("a property"))
Puppet::Type.type(:mount).new(resource)
set[0].should == :name
set[1].should == :provider
end
# This one is really hard to test :/
it "should set each default immediately if no value is provided" do
defaults = []
Puppet::Type.type(:service).any_instance.stubs(:set_default).with { |value| defaults << value; true }
Puppet::Type.type(:service).new :name => "whatever"
defaults[0].should == :provider
end
it "should retain a copy of the originally provided parameters" do
Puppet::Type.type(:mount).new(:name => "foo", :atboot => :yes, :noop => false).original_parameters.should == {:atboot => :yes, :noop => false}
end
it "should delete the name via the namevar from the originally provided parameters" do
Puppet::Type.type(:file).new(:name => make_absolute('/foo')).original_parameters[:path].should be_nil
end
context "when validating the resource" do
it "should call the type's validate method if present" do
Puppet::Type.type(:file).any_instance.expects(:validate)
Puppet::Type.type(:file).new(:name => make_absolute('/foo'))
end
it "should raise Puppet::ResourceError with resource name when Puppet::Error raised" do
expect do
Puppet::Type.type(:file).new(
:name => make_absolute('/foo'),
:source => "puppet:///",
:content => "foo"
)
end.to raise_error(Puppet::ResourceError, /Validation of File\[.*foo.*\]/)
end
it "should raise Puppet::ResourceError with manifest file and line on failure" do
Puppet::Type.type(:file).any_instance.stubs(:file).returns("example.pp")
Puppet::Type.type(:file).any_instance.stubs(:line).returns(42)
expect do
Puppet::Type.type(:file).new(
:name => make_absolute('/foo'),
:source => "puppet:///",
:content => "foo"
)
end.to raise_error(Puppet::ResourceError, /Validation.*example.pp:42/)
end
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
describe "when converting a hash to a Puppet::Resource instance" do
before do
@type = Puppet::Type.type(:mount)
end
it "should treat a :title key as the title of the resource" do
@type.hash2resource(:name => "/foo", :title => "foo").title.should == "foo"
end
it "should use the name from the hash as the title if no explicit title is provided" do
@type.hash2resource(:name => "foo").title.should == "foo"
end
it "should use the Resource Type's namevar to determine how to find the name in the hash" do
@type.stubs(:key_attributes).returns([ :myname ])
@type.hash2resource(:myname => "foo").title.should == "foo"
end
[:catalog].each do |attr|
it "should use any provided #{attr}" do
@type.hash2resource(:name => "foo", attr => "eh").send(attr).should == "eh"
end
end
it "should set all provided parameters on the resource" do
@type.hash2resource(:name => "foo", :fstype => "boo", :boot => "fee").to_hash.should == {:name => "foo", :fstype => "boo", :boot => "fee"}
end
it "should not set the title as a parameter on the resource" do
@type.hash2resource(:name => "foo", :title => "eh")[:title].should be_nil
end
it "should not set the catalog as a parameter on the resource" do
@type.hash2resource(:name => "foo", :catalog => "eh")[:catalog].should be_nil
end
it "should treat hash keys equivalently whether provided as strings or symbols" do
resource = @type.hash2resource("name" => "foo", "title" => "eh", "fstype" => "boo")
resource.title.should == "eh"
resource[:name].should == "foo"
resource[:fstype].should == "boo"
end
end
describe "when retrieving current property values" do
before do
@resource = Puppet::Type.type(:mount).new(:name => "foo", :fstype => "bar", :pass => 1, :ensure => :present)
@resource.property(:ensure).stubs(:retrieve).returns :absent
end
it "should fail if its provider is unsuitable" do
@resource = Puppet::Type.type(:mount).new(:name => "foo", :fstype => "bar", :pass => 1, :ensure => :present)
@resource.provider.class.expects(:suitable?).returns false
expect { @resource.retrieve_resource }.to raise_error(Puppet::Error)
end
it "should return a Puppet::Resource instance with its type and title set appropriately" do
result = @resource.retrieve_resource
result.should be_instance_of(Puppet::Resource)
result.type.should == "Mount"
result.title.should == "foo"
end
it "should set the name of the returned resource if its own name and title differ" do
@resource[:name] = "myname"
@resource.title = "other name"
@resource.retrieve_resource[:name].should == "myname"
end
it "should provide a value for all set properties" do
values = @resource.retrieve_resource
[:ensure, :fstype, :pass].each { |property| values[property].should_not be_nil }
end
it "should provide a value for 'ensure' even if no desired value is provided" do
@resource = Puppet::Type.type(:file).new(:path => make_absolute("/my/file/that/can't/exist"))
end
it "should not call retrieve on non-ensure properties if the resource is absent and should consider the property absent" do
@resource.property(:ensure).expects(:retrieve).returns :absent
@resource.property(:fstype).expects(:retrieve).never
@resource.retrieve_resource[:fstype].should == :absent
end
it "should include the result of retrieving each property's current value if the resource is present" do
@resource.property(:ensure).expects(:retrieve).returns :present
@resource.property(:fstype).expects(:retrieve).returns 15
@resource.retrieve_resource[:fstype] == 15
end
end
describe "#to_resource" do
it "should return a Puppet::Resource that includes properties, parameters and tags" do
type_resource = Puppet::Type.type(:mount).new(
:ensure => :present,
:name => "foo",
:fstype => "bar",
:remounts => true
)
type_resource.tags = %w{bar baz}
# If it's not a property it's a parameter
type_resource.parameters[:remounts].should_not be_a(Puppet::Property)
type_resource.parameters[:fstype].is_a?(Puppet::Property).should be_true
type_resource.property(:ensure).expects(:retrieve).returns :present
type_resource.property(:fstype).expects(:retrieve).returns 15
resource = type_resource.to_resource
resource.should be_a Puppet::Resource
resource[:fstype].should == 15
resource[:remounts].should == :true
resource.tags.should == Puppet::Util::TagSet.new(%w{foo bar baz mount})
end
end
describe ".title_patterns" do
describe "when there's one namevar" do
before do
@type_class = Puppet::Type.type(:notify)
@type_class.stubs(:key_attributes).returns([:one])
end
it "should have a default pattern for when there's one namevar" do
patterns = @type_class.title_patterns
patterns.length.should == 1
patterns[0].length.should == 2
end
it "should have a regexp that captures the entire string" do
patterns = @type_class.title_patterns
string = "abc\n\tdef"
patterns[0][0] =~ string
$1.should == "abc\n\tdef"
end
end
end
describe "when in a catalog" do
before do
@catalog = Puppet::Resource::Catalog.new
@container = Puppet::Type.type(:component).new(:name => "container")
@one = Puppet::Type.type(:file).new(:path => make_absolute("/file/one"))
@two = Puppet::Type.type(:file).new(:path => make_absolute("/file/two"))
@catalog.add_resource @container
@catalog.add_resource @one
@catalog.add_resource @two
@catalog.add_edge @container, @one
@catalog.add_edge @container, @two
end
it "should have no parent if there is no in edge" do
@container.parent.should be_nil
end
it "should set its parent to its in edge" do
@one.parent.ref.should == @container.ref
end
after do
@catalog.clear(true)
end
end
it "should have a 'stage' metaparam" do
Puppet::Type.metaparamclass(:stage).should be_instance_of(Class)
end
describe "#suitable?" do
let(:type) { Puppet::Type.type(:file) }
let(:resource) { type.new :path => tmpfile('suitable') }
let(:provider) { resource.provider }
it "should be suitable if its type doesn't use providers" do
type.stubs(:paramclass).with(:provider).returns nil
resource.must be_suitable
end
it "should be suitable if it has a provider which is suitable" do
resource.must be_suitable
end
it "should not be suitable if it has a provider which is not suitable" do
provider.class.stubs(:suitable?).returns false
resource.should_not be_suitable
end
it "should be suitable if it does not have a provider and there is a default provider" do
resource.stubs(:provider).returns nil
resource.must be_suitable
end
it "should not be suitable if it doesn't have a provider and there is not default provider" do
resource.stubs(:provider).returns nil
type.stubs(:defaultprovider).returns nil
resource.should_not be_suitable
end
end
describe "::instances" do
after :each do Puppet::Type.rmtype(:type_spec_fake_type) end
let :type do
Puppet::Type.newtype(:type_spec_fake_type) do
newparam(:name) do
isnamevar
end
newproperty(:prop1) {}
end
Puppet::Type.type(:type_spec_fake_type)
end
it "should not fail if no suitable providers are found" do
type.provide(:fake1) do
confine :exists => '/no/such/file'
mk_resource_methods
end
expect { type.instances.should == [] }.to_not raise_error
end
context "with a default provider" do
before :each do
type.provide(:default) do
defaultfor :operatingsystem => Facter.value(:operatingsystem)
mk_resource_methods
class << self
attr_accessor :names
end
def self.instance(name)
new(:name => name, :ensure => :present)
end
def self.instances
@instances ||= names.collect { |name| instance(name.to_s) }
end
@names = [:one, :two]
end
end
it "should return only instances of the type" do
type.instances.should be_all {|x| x.is_a? type }
end
it "should return instances from the default provider" do
type.instances.map(&:name).should == ["one", "two"]
end
it "should return instances from all providers" do
type.provide(:fake1, :parent => :default) { @names = [:three, :four] }
type.instances.map(&:name).should == ["one", "two", "three", "four"]
end
it "should not return instances from unsuitable providers" do
type.provide(:fake1, :parent => :default) do
@names = [:three, :four]
confine :exists => "/no/such/file"
end
type.instances.map(&:name).should == ["one", "two"]
end
end
end
describe "::ensurable?" do
before :each do
class TestEnsurableType < Puppet::Type
def exists?; end
def create; end
def destroy; end
end
end
it "is true if the class has exists?, create, and destroy methods defined" do
TestEnsurableType.should be_ensurable
end
it "is false if exists? is not defined" do
TestEnsurableType.class_eval { remove_method(:exists?) }
TestEnsurableType.should_not be_ensurable
end
it "is false if create is not defined" do
TestEnsurableType.class_eval { remove_method(:create) }
TestEnsurableType.should_not be_ensurable
end
it "is false if destroy is not defined" do
TestEnsurableType.class_eval { remove_method(:destroy) }
TestEnsurableType.should_not be_ensurable
end
end
end
describe Puppet::Type::RelationshipMetaparam do
include PuppetSpec::Files
it "should be a subclass of Puppet::Parameter" do
Puppet::Type::RelationshipMetaparam.superclass.should equal(Puppet::Parameter)
end
it "should be able to produce a list of subclasses" do
Puppet::Type::RelationshipMetaparam.should respond_to(:subclasses)
end
describe "when munging relationships" do
before do
@path = File.expand_path('/foo')
@resource = Puppet::Type.type(:file).new :name => @path
@metaparam = Puppet::Type.metaparamclass(:require).new :resource => @resource
end
it "should accept Puppet::Resource instances" do
ref = Puppet::Resource.new(:file, @path)
@metaparam.munge(ref)[0].should equal(ref)
end
it "should turn any string into a Puppet::Resource" do
@metaparam.munge("File[/ref]")[0].should be_instance_of(Puppet::Resource)
end
end
it "should be able to validate relationships" do
Puppet::Type.metaparamclass(:require).new(:resource => mock("resource")).should respond_to(:validate_relationship)
end
describe 'if any specified resource is not in the catalog' do
let(:catalog) { mock 'catalog' }
let(:resource) do
stub 'resource',
:catalog => catalog,
:ref => 'resource',
:line= => nil,
:file= => nil
end
let(:param) { Puppet::Type.metaparamclass(:require).new(:resource => resource, :value => %w{Foo[bar] Class[test]}) }
before do
catalog.expects(:resource).with("Foo[bar]").returns "something"
catalog.expects(:resource).with("Class[Test]").returns nil
end
describe "and the resource doesn't have a file or line number" do
it "raises an error" do
expect { param.validate_relationship }.to raise_error do |error|
error.should be_a Puppet::ResourceError
error.message.should match %r[Class\[Test\]]
end
end
end
describe "and the resource has a file or line number" do
before do
resource.stubs(:line).returns '42'
resource.stubs(:file).returns '/hitchhikers/guide/to/the/galaxy'
end
it "raises an error with context" do
expect { param.validate_relationship }.to raise_error do |error|
error.should be_a Puppet::ResourceError
error.message.should match %r[Class\[Test\]]
error.message.should match %r["in /hitchhikers/guide/to/the/galaxy:42"]
end
end
end
end
end
describe Puppet::Type.metaparamclass(:audit) do
include PuppetSpec::Files
before do
@resource = Puppet::Type.type(:file).new :path => make_absolute('/foo')
end
it "should default to being nil" do
@resource[:audit].should be_nil
end
it "should specify all possible properties when asked to audit all properties" do
@resource[:audit] = :all
list = @resource.class.properties.collect { |p| p.name }
@resource[:audit].should == list
end
it "should accept the string 'all' to specify auditing all possible properties" do
@resource[:audit] = 'all'
list = @resource.class.properties.collect { |p| p.name }
@resource[:audit].should == list
end
it "should fail if asked to audit an invalid property" do
expect { @resource[:audit] = :foobar }.to raise_error(Puppet::Error)
end
it "should create an attribute instance for each auditable property" do
@resource[:audit] = :mode
@resource.parameter(:mode).should_not be_nil
end
it "should accept properties specified as a string" do
@resource[:audit] = "mode"
@resource.parameter(:mode).should_not be_nil
end
it "should not create attribute instances for parameters, only properties" do
@resource[:audit] = :noop
@resource.parameter(:noop).should be_nil
end
describe "when generating the uniqueness key" do
it "should include all of the key_attributes in alphabetical order by attribute name" do
Puppet::Type.type(:file).stubs(:key_attributes).returns [:path, :mode, :owner]
Puppet::Type.type(:file).stubs(:title_patterns).returns(
[ [ /(.*)/, [ [:path, lambda{|x| x} ] ] ] ]
)
myfile = make_absolute('/my/file')
res = Puppet::Type.type(:file).new( :title => myfile, :path => myfile, :owner => 'root', :content => 'hello' )
res.uniqueness_key.should == [ nil, 'root', myfile]
end
end
context "type attribute bracket methods" do
after :each do Puppet::Type.rmtype(:attributes) end
let :type do
Puppet::Type.newtype(:attributes) do
newparam(:name) {}
end
end
it "should work with parameters" do
type.newparam(:param) {}
instance = type.new(:name => 'test')
expect { instance[:param] = true }.to_not raise_error
expect { instance["param"] = true }.to_not raise_error
instance[:param].should == true
instance["param"].should == true
end
it "should work with meta-parameters" do
instance = type.new(:name => 'test')
expect { instance[:noop] = true }.to_not raise_error
expect { instance["noop"] = true }.to_not raise_error
instance[:noop].should == true
instance["noop"].should == true
end
it "should work with properties" do
type.newproperty(:property) {}
instance = type.new(:name => 'test')
expect { instance[:property] = true }.to_not raise_error
expect { instance["property"] = true }.to_not raise_error
instance.property(:property).must be
instance.should(:property).must be_true
end
it "should handle proprieties correctly" do
# Order of assignment is significant in this test.
props = {}
[:one, :two, :three].each {|prop| type.newproperty(prop) {} }
instance = type.new(:name => "test")
instance[:one] = "boo"
one = instance.property(:one)
instance.properties.must == [one]
instance[:three] = "rah"
three = instance.property(:three)
instance.properties.must == [one, three]
instance[:two] = "whee"
two = instance.property(:two)
instance.properties.must == [one, two, three]
end
it "newattr should handle required features correctly" do
Puppet::Util::Log.level = :debug
type.feature :feature1, "one"
type.feature :feature2, "two"
none = type.newproperty(:none) {}
one = type.newproperty(:one, :required_features => :feature1) {}
two = type.newproperty(:two, :required_features => [:feature1, :feature2]) {}
nope = type.provide(:nope) {}
maybe = type.provide(:maybe) { has_features :feature1 }
yep = type.provide(:yep) { has_features :feature1, :feature2 }
[nope, maybe, yep].each_with_index do |provider, i|
rsrc = type.new(:provider => provider.name, :name => "test#{i}",
:none => "a", :one => "b", :two => "c")
rsrc.should(:none).must be
if provider.declared_feature? :feature1
rsrc.should(:one).must be
else
rsrc.should(:one).must_not be
@logs.find {|l| l.message =~ /not managing attribute one/ }.should be
end
if provider.declared_feature? :feature2
rsrc.should(:two).must be
else
rsrc.should(:two).must_not be
@logs.find {|l| l.message =~ /not managing attribute two/ }.should be
end
end
end
end
end
diff --git a/spec/unit/util/adsi_spec.rb b/spec/unit/util/adsi_spec.rb
index 974aba79c..491c4374b 100755
--- a/spec/unit/util/adsi_spec.rb
+++ b/spec/unit/util/adsi_spec.rb
@@ -1,390 +1,437 @@
#!/usr/bin/env ruby
require 'spec_helper'
require 'puppet/util/adsi'
describe Puppet::Util::ADSI do
let(:connection) { stub 'connection' }
before(:each) do
Puppet::Util::ADSI.instance_variable_set(:@computer_name, 'testcomputername')
Puppet::Util::ADSI.stubs(:connect).returns connection
end
after(:each) do
Puppet::Util::ADSI.instance_variable_set(:@computer_name, nil)
end
it "should generate the correct URI for a resource" do
Puppet::Util::ADSI.uri('test', 'user').should == "WinNT://./test,user"
end
it "should be able to get the name of the computer" do
Puppet::Util::ADSI.computer_name.should == 'testcomputername'
end
it "should be able to provide the correct WinNT base URI for the computer" do
Puppet::Util::ADSI.computer_uri.should == "WinNT://."
end
it "should generate a fully qualified WinNT URI" do
Puppet::Util::ADSI.computer_uri('testcomputername').should == "WinNT://testcomputername"
end
describe ".sid_for_account", :if => Puppet.features.microsoft_windows? do
it "should return nil if the account does not exist" do
Puppet::Util::Windows::Security.expects(:name_to_sid).with('foobar').returns nil
Puppet::Util::ADSI.sid_for_account('foobar').should be_nil
end
it "should return a SID for a passed user or group name" do
Puppet::Util::Windows::Security.expects(:name_to_sid).with('testers').returns 'S-1-5-32-547'
Puppet::Util::ADSI.sid_for_account('testers').should == 'S-1-5-32-547'
end
it "should return a SID for a passed fully-qualified user or group name" do
Puppet::Util::Windows::Security.expects(:name_to_sid).with('MACHINE\testers').returns 'S-1-5-32-547'
Puppet::Util::ADSI.sid_for_account('MACHINE\testers').should == 'S-1-5-32-547'
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.stubs(:sid_uri_safe).returns(nil)
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.stubs(:sid_uri_safe).returns(nil)
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')
connection.expects(:Create).with('user', username).returns(adsi_user)
Puppet::Util::ADSI::Group.expects(:exists?).with(username).returns(false)
user = Puppet::Util::ADSI::User.create(username)
user.should be_a(Puppet::Util::ADSI::User)
user.native_user.should == adsi_user
end
it "should be able to check the existence of a user" do
+ Puppet::Util::ADSI.stubs(:sid_uri_safe).returns(nil)
Puppet::Util::ADSI.expects(:connect).with("WinNT://./#{username},user").returns connection
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.stubs(:sid_uri_safe).returns(nil)
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 confirm the existence of a user with a well-known SID",
+ :if => Puppet.features.microsoft_windows? do
+
+ system_user = Win32::Security::SID::LocalSystem
+ # ensure that the underlying OS is queried here
+ Puppet::Util::ADSI.unstub(:connect)
+ Puppet::Util::ADSI::User.exists?(system_user).should be_true
+ end
+
+ it "should return nil with an unknown SID",
+ :if => Puppet.features.microsoft_windows? do
+
+ bogus_sid = 'S-1-2-3-4'
+ # ensure that the underlying OS is queried here
+ Puppet::Util::ADSI.unstub(:connect)
+ Puppet::Util::ADSI::User.exists?(bogus_sid).should be_false
+ end
+
it "should be able to delete a user" do
connection.expects(:Delete).with('user', username)
Puppet::Util::ADSI::User.delete(username)
end
it "should return an enumeration of IADsUser wrapped objects" do
+ Puppet::Util::ADSI.stubs(:sid_uri_safe).returns(nil)
+
name = 'Administrator'
wmi_users = [stub('WMI', :name => name)]
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}"
native_user.expects(:Get).with('HomeDirectory').returns(homedir)
Puppet::Util::ADSI.expects(:connect).with("WinNT://./#{name},user").returns(native_user)
users = Puppet::Util::ADSI::User.to_a
users.length.should == 1
users[0].name.should == name
users[0]['HomeDirectory'].should == homedir
end
describe "an instance" do
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
names = ["group1", "group2"]
groups = names.map { |name| mock('group', :Name => name) }
adsi_user.expects(:Groups).returns(groups)
user.groups.should =~ names
end
it "should be able to test whether a given password is correct" do
Puppet::Util::ADSI::User.expects(:logon).with(username, 'pwdwrong').returns(false)
Puppet::Util::ADSI::User.expects(:logon).with(username, 'pwdright').returns(true)
user.password_is?('pwdwrong').should be_false
user.password_is?('pwdright').should be_true
end
it "should be able to set a password" do
adsi_user.expects(:SetPassword).with('pwd')
adsi_user.expects(:SetInfo).at_least_once
flagname = "UserFlags"
fADS_UF_DONT_EXPIRE_PASSWD = 0x10000
adsi_user.expects(:Get).with(flagname).returns(0)
adsi_user.expects(:Put).with(flagname, fADS_UF_DONT_EXPIRE_PASSWD)
user.password = 'pwd'
end
- it "should generate the correct URI",:if => Puppet.features.microsoft_windows? do
+ 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", :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
describe "if membership is specified as inclusive" do
it "should add the user to those groups, and remove it from groups not in the list" do
group1 = stub 'group1'
group1.expects(:Add).with("WinNT://testcomputername/#{username},user")
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
user.set_groups(groups_to_set, false)
end
end
describe "if membership is specified as minimum" do
it "should add the user to the specified groups without affecting its other memberships" 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)
end
end
end
end
end
describe Puppet::Util::ADSI::Group do
let(:groupname) { 'testgroup' }
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")
adsi_group.expects(:Add).with("WinNT://testcomputername/someone,user")
group.add_member('someone')
end
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']
users = names.map { |name| mock('user', :Name => name) }
adsi_group.expects(:Members).returns(users)
group.members.should =~ names
end
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])
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
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
+ Puppet::Util::ADSI.stubs(:sid_uri_safe).returns(nil)
group.uri.should == "WinNT://./#{groupname},group"
end
end
it "should generate the correct URI" do
+ Puppet::Util::ADSI.stubs(:sid_uri_safe).returns(nil)
Puppet::Util::ADSI::Group.uri("people").should == "WinNT://./people,group"
end
it "should be able to create a group" do
adsi_group = stub("adsi")
connection.expects(:Create).with('group', groupname).returns(adsi_group)
Puppet::Util::ADSI::User.expects(:exists?).with(groupname).returns(false)
group = Puppet::Util::ADSI::Group.create(groupname)
group.should be_a(Puppet::Util::ADSI::Group)
group.native_group.should == adsi_group
end
it "should be able to confirm the existence of a group" do
+ Puppet::Util::ADSI.stubs(:sid_uri_safe).returns(nil)
Puppet::Util::ADSI.expects(:connect).with("WinNT://./#{groupname},group").returns connection
Puppet::Util::ADSI::Group.exists?(groupname).should be_true
end
+ it "should be able to confirm the existence of a group with a well-known SID",
+ :if => Puppet.features.microsoft_windows? do
+
+ service_group = Win32::Security::SID::Service
+ # ensure that the underlying OS is queried here
+ Puppet::Util::ADSI.unstub(:connect)
+ Puppet::Util::ADSI::Group.exists?(service_group).should be_true
+ end
+
+ it "should return nil with an unknown SID",
+ :if => Puppet.features.microsoft_windows? do
+
+ bogus_sid = 'S-1-2-3-4'
+ # ensure that the underlying OS is queried here
+ Puppet::Util::ADSI.unstub(:connect)
+ Puppet::Util::ADSI::Group.exists?(bogus_sid).should be_false
+ end
+
it "should be able to delete a group" do
connection.expects(:Delete).with('group', groupname)
Puppet::Util::ADSI::Group.delete(groupname)
end
it "should return an enumeration of IADsGroup wrapped objects" do
+ Puppet::Util::ADSI.stubs(:sid_uri_safe).returns(nil)
+
name = 'Administrators'
wmi_groups = [stub('WMI', :name => name)]
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')])
Puppet::Util::ADSI.expects(:connect).with("WinNT://./#{name},group").returns(native_group)
groups = Puppet::Util::ADSI::Group.to_a
groups.length.should == 1
groups[0].name.should == name
groups[0].members.should == ['Administrator']
end
end
describe Puppet::Util::ADSI::UserProfile do
it "should be able to delete a user profile" do
connection.expects(:Delete).with("Win32_UserProfile.SID='S-A-B-C'")
Puppet::Util::ADSI::UserProfile.delete('S-A-B-C')
end
it "should warn on 2003" do
connection.expects(:Delete).raises(RuntimeError,
"Delete (WIN32OLERuntimeError)
OLE error code:80041010 in SWbemServicesEx
Invalid class
HRESULT error code:0x80020009
Exception occurred.")
Puppet.expects(:warning).with("Cannot delete user profile for 'S-A-B-C' prior to Vista SP1")
Puppet::Util::ADSI::UserProfile.delete('S-A-B-C')
end
end
end
diff --git a/spec/unit/util/autoload_spec.rb b/spec/unit/util/autoload_spec.rb
index 4a0782ac7..1800dd2dc 100755
--- a/spec/unit/util/autoload_spec.rb
+++ b/spec/unit/util/autoload_spec.rb
@@ -1,271 +1,256 @@
#! /usr/bin/env ruby
require 'spec_helper'
require 'puppet/util/autoload'
describe Puppet::Util::Autoload do
include PuppetSpec::Files
before do
@autoload = Puppet::Util::Autoload.new("foo", "tmp")
@autoload.stubs(:eachdir).yields make_absolute("/my/dir")
@loaded = {}
@autoload.class.stubs(:loaded).returns(@loaded)
end
describe "when building the search path" do
before :each do
## modulepath/libdir can't be used until after app settings are initialized, so we need to simulate that:
Puppet.settings.expects(:app_defaults_initialized?).returns(true).at_least_once
@dira = File.expand_path('/a')
@dirb = File.expand_path('/b')
@dirc = File.expand_path('/c')
end
it "should collect all of the lib directories that exist in the current environment's module path" do
- Puppet[:environment] = "foo"
- Puppet.settings.set_value(:modulepath, "#{@dira}#{File::PATH_SEPARATOR}#{@dirb}#{File::PATH_SEPARATOR}#{@dirc}", :foo)
- Dir.expects(:entries).with(@dira).returns %w{one two}
- Dir.expects(:entries).with(@dirb).returns %w{one two}
-
- FileTest.stubs(:directory?).returns false
- FileTest.expects(:directory?).with(@dira).returns true
- FileTest.expects(:directory?).with(@dirb).returns true
- ["#{@dira}/two/lib", "#{@dirb}/two/lib"].each do |d|
- FileTest.expects(:directory?).with(d).returns true
- end
-
- @autoload.class.module_directories.should == ["#{@dira}/two/lib", "#{@dirb}/two/lib"]
- end
+ environment = Puppet::Node::Environment.create(:foo, [@dira, @dirb, @dirc], '')
+ Dir.expects(:entries).with(@dira).returns %w{. .. one two}
+ Dir.expects(:entries).with(@dirb).returns %w{. .. one two}
- it "should not look for lib directories in directories starting with '.'" do
- Puppet[:environment] = "foo"
- Puppet.settings.set_value(:modulepath, @dira, :foo)
- Dir.expects(:entries).with(@dira).returns %w{. ..}
+ Puppet::FileSystem.expects(:directory?).with(@dira).returns true
+ Puppet::FileSystem.expects(:directory?).with(@dirb).returns true
+ Puppet::FileSystem.expects(:directory?).with(@dirc).returns false
- FileTest.expects(:directory?).with(@dira).returns true
- FileTest.expects(:directory?).with("#{@dira}/./lib").never
- FileTest.expects(:directory?).with("#{@dira}/./plugins").never
- FileTest.expects(:directory?).with("#{@dira}/../lib").never
- FileTest.expects(:directory?).with("#{@dira}/../plugins").never
+ FileTest.expects(:directory?).with(regexp_matches(%r{two/lib})).times(2).returns true
+ FileTest.expects(:directory?).with(regexp_matches(%r{one/lib})).times(2).returns false
- @autoload.class.module_directories
+ @autoload.class.module_directories(environment).should == ["#{@dira}/two/lib", "#{@dirb}/two/lib"]
end
it "should include the module directories, the Puppet libdir, and all of the Ruby load directories" do
Puppet[:libdir] = %w{/libdir1 /lib/dir/two /third/lib/dir}.join(File::PATH_SEPARATOR)
@autoload.class.expects(:gem_directories).returns %w{/one /two}
@autoload.class.expects(:module_directories).returns %w{/three /four}
- @autoload.class.search_directories.should == %w{/one /two /three /four} + Puppet[:libdir].split(File::PATH_SEPARATOR) + $LOAD_PATH
+ @autoload.class.search_directories(nil).should == %w{/one /two /three /four} + Puppet[:libdir].split(File::PATH_SEPARATOR) + $LOAD_PATH
end
end
describe "when loading a file" do
before do
@autoload.class.stubs(:search_directories).returns [make_absolute("/a")]
FileTest.stubs(:directory?).returns true
@time_a = Time.utc(2010, 'jan', 1, 6, 30)
File.stubs(:mtime).returns @time_a
end
[RuntimeError, LoadError, SyntaxError].each do |error|
it "should die with Puppet::Error if a #{error.to_s} exception is thrown" do
- Puppet::FileSystem::File.stubs(:exist?).returns true
+ Puppet::FileSystem.stubs(:exist?).returns true
Kernel.expects(:load).raises error
lambda { @autoload.load("foo") }.should raise_error(Puppet::Error)
end
end
it "should not raise an error if the file is missing" do
@autoload.load("foo").should == false
end
it "should register loaded files with the autoloader" do
- Puppet::FileSystem::File.stubs(:exist?).returns true
+ Puppet::FileSystem.stubs(:exist?).returns true
Kernel.stubs(:load)
@autoload.load("myfile")
@autoload.class.loaded?("tmp/myfile.rb").should be
$LOADED_FEATURES.delete("tmp/myfile.rb")
end
it "should be seen by loaded? on the instance using the short name" do
- Puppet::FileSystem::File.stubs(:exist?).returns true
+ Puppet::FileSystem.stubs(:exist?).returns true
Kernel.stubs(:load)
@autoload.load("myfile")
@autoload.loaded?("myfile.rb").should be
$LOADED_FEATURES.delete("tmp/myfile.rb")
end
it "should register loaded files with the main loaded file list so they are not reloaded by ruby" do
- Puppet::FileSystem::File.stubs(:exist?).returns true
+ Puppet::FileSystem.stubs(:exist?).returns true
Kernel.stubs(:load)
@autoload.load("myfile")
$LOADED_FEATURES.should be_include("tmp/myfile.rb")
$LOADED_FEATURES.delete("tmp/myfile.rb")
end
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
- Puppet::FileSystem::File.stubs(:exist?).returns true
+ Puppet::FileSystem.stubs(:exist?).returns true
Kernel.expects(:load).with(make_absolute("/a/tmp/myfile.rb"), optionally(anything))
@autoload.load("myfile")
$LOADED_FEATURES.delete("tmp/myfile.rb")
end
it "should treat equivalent paths to a loaded file as loaded" do
- Puppet::FileSystem::File.stubs(:exist?).returns true
+ Puppet::FileSystem.stubs(:exist?).returns true
Kernel.stubs(:load)
@autoload.load("myfile")
@autoload.class.loaded?("tmp/myfile").should be
@autoload.class.loaded?("tmp/./myfile.rb").should be
@autoload.class.loaded?("./tmp/myfile.rb").should be
@autoload.class.loaded?("tmp/../tmp/myfile.rb").should be
$LOADED_FEATURES.delete("tmp/myfile.rb")
end
end
describe "when loading all files" do
before 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")]
- Puppet::FileSystem::File.stubs(:exist?).returns true
+ Puppet::FileSystem.stubs(:exist?).returns true
@time_a = Time.utc(2010, 'jan', 1, 6, 30)
File.stubs(:mtime).returns @time_a
@autoload.class.stubs(:loaded?).returns(false)
end
[RuntimeError, LoadError, SyntaxError].each do |error|
it "should die an if a #{error.to_s} exception is thrown" do
Kernel.expects(:load).raises error
lambda { @autoload.loadall }.should raise_error(Puppet::Error)
end
end
it "should require the full path to the file" do
Kernel.expects(:load).with(make_absolute("/a/foo/file.rb"), optionally(anything))
@autoload.loadall
end
end
describe "when reloading files" do
before :each do
@file_a = make_absolute("/a/file.rb")
@file_b = make_absolute("/b/file.rb")
@first_time = Time.utc(2010, 'jan', 1, 6, 30)
@second_time = @first_time + 60
end
after :each do
$LOADED_FEATURES.delete("a/file.rb")
$LOADED_FEATURES.delete("b/file.rb")
end
it "#changed? should return true for a file that was not loaded" do
@autoload.class.changed?(@file_a).should be
end
it "changes should be seen by changed? on the instance using the short name" do
File.stubs(:mtime).returns(@first_time)
- Puppet::FileSystem::File.stubs(:exist?).returns true
+ Puppet::FileSystem.stubs(:exist?).returns true
Kernel.stubs(:load)
@autoload.load("myfile")
@autoload.loaded?("myfile").should be
@autoload.changed?("myfile").should_not be
File.stubs(:mtime).returns(@second_time)
@autoload.changed?("myfile").should be
$LOADED_FEATURES.delete("tmp/myfile.rb")
end
describe "in one directory" do
before :each do
@autoload.class.stubs(:search_directories).returns [make_absolute("/a")]
File.expects(:mtime).with(@file_a).returns(@first_time)
@autoload.class.mark_loaded("file", @file_a)
end
it "should reload if mtime changes" do
File.stubs(:mtime).with(@file_a).returns(@first_time + 60)
- Puppet::FileSystem::File.stubs(:exist?).with(@file_a).returns true
+ Puppet::FileSystem.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)
- Puppet::FileSystem::File.stubs(:exist?).with(@file_a).returns false
+ Puppet::FileSystem.stubs(:exist?).with(@file_a).returns false
Kernel.expects(:load).never
@autoload.class.reload_changed
end
end
describe "in two directories" do
before :each do
@autoload.class.stubs(:search_directories).returns [make_absolute("/a"), make_absolute("/b")]
end
it "should load b/file when a/file is deleted" 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)
- Puppet::FileSystem::File.stubs(:exist?).with(@file_a).returns false
- Puppet::FileSystem::File.stubs(:exist?).with(@file_b).returns true
+ Puppet::FileSystem.stubs(:exist?).with(@file_a).returns false
+ Puppet::FileSystem.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
@autoload.class.send(:loaded)["file"].should == [@file_b, @first_time]
end
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
- Puppet::FileSystem::File.stubs(:exist?).with(@file_b).returns true
+ Puppet::FileSystem.stubs(:exist?).with(@file_b).returns true
@autoload.class.mark_loaded("file", @file_b)
File.stubs(:mtime).with(@file_a).returns @first_time
- Puppet::FileSystem::File.stubs(:exist?).with(@file_a).returns true
+ Puppet::FileSystem.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]
end
end
end
describe "#cleanpath" do
it "should leave relative paths relative" do
path = "hello/there"
Puppet::Util::Autoload.cleanpath(path).should == path
end
describe "on Windows", :if => Puppet.features.microsoft_windows? do
it "should convert c:\ to c:/" do
Puppet::Util::Autoload.cleanpath('c:\\').should == 'c:/'
end
end
end
describe "#expand" do
it "should expand relative to the autoloader's prefix" do
@autoload.expand('bar').should == 'tmp/bar'
end
end
end
diff --git a/spec/unit/util/backups_spec.rb b/spec/unit/util/backups_spec.rb
index 654ddb788..ce7b9b756 100755
--- a/spec/unit/util/backups_spec.rb
+++ b/spec/unit/util/backups_spec.rb
@@ -1,137 +1,134 @@
#! /usr/bin/env ruby
require 'spec_helper'
require 'puppet/util/backups'
describe Puppet::Util::Backups do
include PuppetSpec::Files
let(:bucket) { stub('bucket', :name => "foo") }
let!(:file) do
f = Puppet::Type.type(:file).new(:name => path, :backup => 'foo')
f.stubs(:bucket).returns(bucket)
f
end
describe "when backing up a file" do
let(:path) { make_absolute('/no/such/file') }
it "should noop if the file does not exist" do
file = Puppet::Type.type(:file).new(:name => path)
file.expects(:bucket).never
- Puppet::FileSystem::File.expects(:exist?).with(path).returns false
+ Puppet::FileSystem.expects(:exist?).with(path).returns false
file.perform_backup
end
it "should succeed silently if self[:backup] is false" do
file = Puppet::Type.type(:file).new(:name => path, :backup => false)
file.expects(:bucket).never
- Puppet::FileSystem::File.expects(:exist?).never
+ Puppet::FileSystem.expects(:exist?).never
file.perform_backup
end
it "a bucket should be used when provided" do
- stub_file = stub(path, :lstat => mock('lstat', :ftype => 'file'))
- Puppet::FileSystem::File.expects(:new).with(path).returns stub_file
+ lstat_path_as(path, 'file')
bucket.expects(:backup).with(path).returns("mysum")
- Puppet::FileSystem::File.expects(:exist?).with(path).returns(true)
+ Puppet::FileSystem.expects(:exist?).with(path).returns(true)
file.perform_backup
end
it "should propagate any exceptions encountered when backing up to a filebucket" do
- stub_file = stub(path, :lstat => mock('lstat', :ftype => 'file'))
- Puppet::FileSystem::File.expects(:new).with(path).returns stub_file
+ lstat_path_as(path, 'file')
bucket.expects(:backup).raises ArgumentError
- Puppet::FileSystem::File.expects(:exist?).with(path).returns(true)
+ Puppet::FileSystem.expects(:exist?).with(path).returns(true)
lambda { file.perform_backup }.should raise_error(ArgumentError)
end
describe "and local backup is configured" do
let(:ext) { 'foobkp' }
let(:backup) { path + '.' + ext }
let(:file) { Puppet::Type.type(:file).new(:name => path, :backup => '.'+ext) }
it "should remove any local backup if one exists" do
- 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)
+ lstat_path_as(backup, 'file')
+ Puppet::FileSystem.expects(:unlink).with(backup)
FileUtils.stubs(:cp_r)
- Puppet::FileSystem::File.expects(:exist?).with(path).returns(true)
+ Puppet::FileSystem.expects(:exist?).with(path).returns(true)
file.perform_backup
end
it "should fail when the old backup can't be removed" do
- 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
+ lstat_path_as(backup, 'file')
+ Puppet::FileSystem.expects(:unlink).with(backup).raises ArgumentError
FileUtils.expects(:cp_r).never
- Puppet::FileSystem::File.expects(:exist?).with(path).returns(true)
+ Puppet::FileSystem.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
- 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
+ Puppet::FileSystem.expects(:lstat).with(backup).raises(Errno::ENOENT)
+ Puppet::FileSystem.expects(:unlink).with(backup).never
FileUtils.stubs(:cp_r)
- Puppet::FileSystem::File.expects(:exist?).with(path).returns(true)
+ Puppet::FileSystem.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)
- Puppet::FileSystem::File.stubs(:exist?).with(path).returns(true)
+ Puppet::FileSystem.stubs(:exist?).with(path).returns(true)
file.perform_backup.should be_true
end
it "should propagate exceptions if no backup can be created" do
FileUtils.expects(:cp_r).raises ArgumentError
- Puppet::FileSystem::File.stubs(:exist?).with(path).returns(true)
+ Puppet::FileSystem.stubs(:exist?).with(path).returns(true)
lambda { file.perform_backup }.should raise_error(Puppet::Error)
end
end
end
describe "when backing up a directory" do
let(:path) { make_absolute('/my/dir') }
let(:filename) { File.join(path, 'file') }
it "a bucket should work when provided" do
File.stubs(:file?).with(filename).returns true
Find.expects(:find).with(path).yields(filename)
bucket.expects(:backup).with(filename).returns true
- stub_file = stub(path, :lstat => stub('stat', :ftype => 'directory'))
- Puppet::FileSystem::File.expects(:new).with(path).returns stub_file
+ lstat_path_as(path, 'directory')
- Puppet::FileSystem::File.stubs(:exist?).with(path).returns(true)
- Puppet::FileSystem::File.stubs(:exist?).with(filename).returns(true)
+ Puppet::FileSystem.stubs(:exist?).with(path).returns(true)
+ Puppet::FileSystem.stubs(:exist?).with(filename).returns(true)
file.perform_backup
end
it "should do nothing when recursing" do
file = Puppet::Type.type(:file).new(:name => path, :backup => 'foo', :recurse => true)
bucket.expects(:backup).never
stub_file = stub('file', :stat => stub('stat', :ftype => 'directory'))
- Puppet::FileSystem::File.stubs(:new).with(path).returns stub_file
+ Puppet::FileSystem.stubs(:new).with(path).returns stub_file
Find.expects(:find).never
file.perform_backup
end
end
+
+ def lstat_path_as(path, ftype)
+ Puppet::FileSystem.expects(:lstat).with(path).returns(stub('File::Stat', :ftype => ftype))
+ end
end
diff --git a/spec/unit/util/checksums_spec.rb b/spec/unit/util/checksums_spec.rb
index 0adff7a9a..f4b85e402 100755
--- a/spec/unit/util/checksums_spec.rb
+++ b/spec/unit/util/checksums_spec.rb
@@ -1,175 +1,173 @@
#! /usr/bin/env ruby
require 'spec_helper'
require 'puppet/util/checksums'
describe Puppet::Util::Checksums do
include PuppetSpec::Files
before do
@summer = Object.new
@summer.extend(Puppet::Util::Checksums)
end
content_sums = [:md5, :md5lite, :sha1, :sha1lite]
file_only = [:ctime, :mtime, :none]
content_sums.each do |sumtype|
it "should be able to calculate #{sumtype} sums from strings" do
@summer.should be_respond_to(sumtype)
end
end
[content_sums, file_only].flatten.each do |sumtype|
it "should be able to calculate #{sumtype} sums from files" do
@summer.should be_respond_to(sumtype.to_s + "_file")
end
end
[content_sums, file_only].flatten.each do |sumtype|
it "should be able to calculate #{sumtype} sums from stream" do
@summer.should be_respond_to(sumtype.to_s + "_stream")
end
end
it "should have a method for determining whether a given string is a checksum" do
@summer.should respond_to(:checksum?)
end
%w{{md5}asdfasdf {sha1}asdfasdf {ctime}asdasdf {mtime}asdfasdf}.each do |sum|
it "should consider #{sum} to be a checksum" do
@summer.should be_checksum(sum)
end
end
%w{{nosuchsum}asdfasdf {a}asdfasdf {ctime}}.each do |sum|
it "should not consider #{sum} to be a checksum" do
@summer.should_not be_checksum(sum)
end
end
it "should have a method for stripping a sum type from an existing checksum" do
@summer.sumtype("{md5}asdfasdfa").should == "md5"
end
it "should have a method for stripping the data from a checksum" do
@summer.sumdata("{md5}asdfasdfa").should == "asdfasdfa"
end
it "should return a nil sumtype if the checksum does not mention a checksum type" do
@summer.sumtype("asdfasdfa").should be_nil
end
{:md5 => Digest::MD5, :sha1 => Digest::SHA1}.each do |sum, klass|
describe("when using #{sum}") do
it "should use #{klass} to calculate string checksums" do
klass.expects(:hexdigest).with("mycontent").returns "whatever"
@summer.send(sum, "mycontent").should == "whatever"
end
it "should use incremental #{klass} sums to calculate file checksums" do
digest = mock 'digest'
klass.expects(:new).returns digest
file = "/path/to/my/file"
fh = mock 'filehandle'
fh.expects(:read).with(4096).times(3).returns("firstline").then.returns("secondline").then.returns(nil)
#fh.expects(:read).with(512).returns("secondline")
#fh.expects(:read).with(512).returns(nil)
File.expects(:open).with(file, "rb").yields(fh)
digest.expects(:<<).with "firstline"
digest.expects(:<<).with "secondline"
digest.expects(:hexdigest).returns :mydigest
@summer.send(sum.to_s + "_file", file).should == :mydigest
end
it "should yield #{klass} to the given block to calculate stream checksums" do
digest = mock 'digest'
klass.expects(:new).returns digest
digest.expects(:hexdigest).returns :mydigest
@summer.send(sum.to_s + "_stream") do |checksum|
checksum.should == digest
end.should == :mydigest
end
end
end
{:md5lite => Digest::MD5, :sha1lite => Digest::SHA1}.each do |sum, klass|
describe("when using #{sum}") do
it "should use #{klass} to calculate string checksums from the first 512 characters of the string" do
content = "this is a test" * 100
klass.expects(:hexdigest).with(content[0..511]).returns "whatever"
@summer.send(sum, content).should == "whatever"
end
it "should use #{klass} to calculate a sum from the first 512 characters in the file" do
digest = mock 'digest'
klass.expects(:new).returns digest
file = "/path/to/my/file"
fh = mock 'filehandle'
fh.expects(:read).with(512).returns('my content')
File.expects(:open).with(file, "rb").yields(fh)
digest.expects(:<<).with "my content"
digest.expects(:hexdigest).returns :mydigest
@summer.send(sum.to_s + "_file", file).should == :mydigest
end
end
end
[:ctime, :mtime].each do |sum|
describe("when using #{sum}") do
it "should use the '#{sum}' on the file to determine the ctime" do
file = "/my/file"
stat = mock 'stat', sum => "mysum"
-
- stub_file = stub(file, :stat => stat)
- Puppet::FileSystem::File.expects(:new).with(file).returns stub_file
+ Puppet::FileSystem.expects(:stat).with(file).returns(stat)
@summer.send(sum.to_s + "_file", file).should == "mysum"
end
it "should return nil for streams" do
expectation = stub "expectation"
expectation.expects(:do_something!).at_least_once
@summer.send(sum.to_s + "_stream"){ |checksum| checksum << "anything" ; expectation.do_something! }.should be_nil
end
end
end
describe "when using the none checksum" do
it "should return an empty string" do
@summer.none_file("/my/file").should == ""
end
it "should return an empty string for streams" do
expectation = stub "expectation"
expectation.expects(:do_something!).at_least_once
@summer.none_stream{ |checksum| checksum << "anything" ; expectation.do_something! }.should == ""
end
end
{:md5 => Digest::MD5, :sha1 => Digest::SHA1}.each do |sum, klass|
describe "when using #{sum}" do
let(:content) { "hello\r\nworld" }
let(:path) do
path = tmpfile("checksum_#{sum}")
File.open(path, 'wb') {|f| f.write(content)}
path
end
it "should preserve nl/cr sequences" do
@summer.send(sum.to_s + "_file", path).should == klass.hexdigest(content)
end
end
end
end
diff --git a/spec/unit/util/colors_spec.rb b/spec/unit/util/colors_spec.rb
index f114894da..7407b628b 100755
--- a/spec/unit/util/colors_spec.rb
+++ b/spec/unit/util/colors_spec.rb
@@ -1,69 +1,83 @@
#!/usr/bin/env ruby
require 'spec_helper'
describe Puppet::Util::Colors do
include Puppet::Util::Colors
let (:message) { 'a message' }
let (:color) { :black }
let (:subject) { self }
describe ".console_color" do
it { should respond_to :console_color }
it "should generate ANSI escape sequences" do
subject.console_color(color, message).should == "\e[0;30m#{message}\e[0m"
end
end
describe ".html_color" do
it { should respond_to :html_color }
it "should generate an HTML span element and style attribute" do
subject.html_color(color, message).should =~ /<span style=\"color: #FFA0A0\">#{message}<\/span>/
end
end
describe ".colorize" do
it { should respond_to :colorize }
context "ansicolor supported" do
before :each do
subject.stubs(:console_has_color?).returns(true)
end
it "should colorize console output" do
Puppet[:color] = true
subject.expects(:console_color).with(color, message)
subject.colorize(:black, message)
end
it "should not colorize unknown color schemes" do
Puppet[:color] = :thisisanunknownscheme
subject.colorize(:black, message).should == message
end
end
context "ansicolor not supported" do
before :each do
subject.stubs(:console_has_color?).returns(false)
end
it "should not colorize console output" do
Puppet[:color] = true
subject.expects(:console_color).never
subject.colorize(:black, message).should == message
end
it "should colorize html output" do
Puppet[:color] = :html
subject.expects(:html_color).with(color, message)
subject.colorize(color, message)
end
end
end
+
+ describe "on Windows", :if => Puppet.features.microsoft_windows? do
+ it "expects a trailing embedded NULL character in the wide string" do
+ message = "hello"
+
+ console = Puppet::Util::Colors::WideConsole.new
+ wstr, nchars = console.string_encode(message)
+
+ expect(nchars).to eq(message.length)
+
+ expect(wstr.length).to eq(nchars + 1)
+ expect(wstr[-1].ord).to be_zero
+ end
+ end
end
diff --git a/spec/unit/util/docs_spec.rb b/spec/unit/util/docs_spec.rb
index eb736ff72..5823e303f 100644
--- a/spec/unit/util/docs_spec.rb
+++ b/spec/unit/util/docs_spec.rb
@@ -1,91 +1,100 @@
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
+ it "trims leading space from one-liners (even when they're buffered with extra newlines)" do
+ input = "
+ Updates values in the `puppet.conf` configuration file.
+ "
+ expected_output = "Updates values in the `puppet.conf` configuration file."
+ output = Puppet::Util::Docs.scrub(input)
+ expect(output).to eq expected_output
+ end
+
end
end
diff --git a/spec/unit/util/execution_spec.rb b/spec/unit/util/execution_spec.rb
index 7bb15cd75..7c6238f9f 100755
--- a/spec/unit/util/execution_spec.rb
+++ b/spec/unit/util/execution_spec.rb
@@ -1,637 +1,637 @@
#! /usr/bin/env ruby
require 'spec_helper'
describe Puppet::Util::Execution do
include Puppet::Util::Execution
# utility method to help deal with some windows vs. unix differences
def process_status(exitstatus)
return exitstatus if Puppet.features.microsoft_windows?
stub('child_status', :exitstatus => exitstatus)
end
# utility methods to help us test some private methods without being quite so verbose
def call_exec_posix(command, arguments, stdin, stdout, stderr)
Puppet::Util::Execution.send(:execute_posix, command, arguments, stdin, stdout, stderr)
end
def call_exec_windows(command, arguments, stdin, stdout, stderr)
Puppet::Util::Execution.send(:execute_windows, command, arguments, stdin, stdout, stderr)
end
describe "execution methods" do
let(:pid) { 5501 }
let(:process_handle) { 0xDEADBEEF }
let(:thread_handle) { 0xCAFEBEEF }
let(:proc_info_stub) { stub 'processinfo', :process_handle => process_handle, :thread_handle => thread_handle, :process_id => pid}
let(:null_file) { Puppet.features.microsoft_windows? ? 'NUL' : '/dev/null' }
def stub_process_wait(exitstatus)
if Puppet.features.microsoft_windows?
Puppet::Util::Windows::Process.stubs(:wait_process).with(process_handle).returns(exitstatus)
Process.stubs(:CloseHandle).with(process_handle)
Process.stubs(:CloseHandle).with(thread_handle)
else
Process.stubs(:waitpid2).with(pid).returns([pid, stub('child_status', :exitstatus => exitstatus)])
end
end
describe "#execute_posix (stubs)", :unless => Puppet.features.microsoft_windows? do
before :each do
# Most of the things this method does are bad to do during specs. :/
Kernel.stubs(:fork).returns(pid).yields
Process.stubs(:setsid)
Kernel.stubs(:exec)
Puppet::Util::SUIDManager.stubs(:change_user)
Puppet::Util::SUIDManager.stubs(:change_group)
# ensure that we don't really close anything!
(0..256).each {|n| IO.stubs(:new) }
$stdin.stubs(:reopen)
$stdout.stubs(:reopen)
$stderr.stubs(:reopen)
@stdin = File.open(null_file, 'r')
@stdout = Tempfile.new('stdout')
@stderr = File.open(null_file, 'w')
# there is a danger here that ENV will be modified by exec_posix. Normally it would only affect the ENV
# of a forked process, but here, we're stubbing Kernel.fork, so the method has the ability to override the
# "real" ENV. To guard against this, we'll capture a snapshot of ENV before each test.
@saved_env = ENV.to_hash
# Now, we're going to effectively "mock" the magic ruby 'ENV' variable by creating a local definition of it
# inside of the module we're testing.
Puppet::Util::Execution::ENV = {}
end
after :each do
# And here we remove our "mock" version of 'ENV', which will allow us to validate that the real ENV has been
# left unharmed.
Puppet::Util::Execution.send(:remove_const, :ENV)
# capture the current environment and make sure it's the same as it was before the test
cur_env = ENV.to_hash
# we will get some fairly useless output if we just use the raw == operator on the hashes here, so we'll
# be a bit more explicit and laborious in the name of making the error more useful...
@saved_env.each_pair { |key,val| cur_env[key].should == val }
(cur_env.keys - @saved_env.keys).should == []
end
it "should fork a child process to execute the command" do
Kernel.expects(:fork).returns(pid).yields
Kernel.expects(:exec).with('test command')
call_exec_posix('test command', {}, @stdin, @stdout, @stderr)
end
it "should start a new session group" do
Process.expects(:setsid)
call_exec_posix('test command', {}, @stdin, @stdout, @stderr)
end
it "should permanently change to the correct user and group if specified" do
Puppet::Util::SUIDManager.expects(:change_group).with(55, true)
Puppet::Util::SUIDManager.expects(:change_user).with(50, true)
call_exec_posix('test command', {:uid => 50, :gid => 55}, @stdin, @stdout, @stderr)
end
it "should exit failure if there is a problem execing the command" do
Kernel.expects(:exec).with('test command').raises("failed to execute!")
Puppet::Util::Execution.stubs(:puts)
Puppet::Util::Execution.expects(:exit!).with(1)
call_exec_posix('test command', {}, @stdin, @stdout, @stderr)
end
it "should properly execute commands specified as arrays" do
Kernel.expects(:exec).with('test command', 'with', 'arguments')
call_exec_posix(['test command', 'with', 'arguments'], {:uid => 50, :gid => 55}, @stdin, @stdout, @stderr)
end
it "should properly execute string commands with embedded newlines" do
Kernel.expects(:exec).with("/bin/echo 'foo' ; \n /bin/echo 'bar' ;")
call_exec_posix("/bin/echo 'foo' ; \n /bin/echo 'bar' ;", {:uid => 50, :gid => 55}, @stdin, @stdout, @stderr)
end
it "should return the pid of the child process" do
call_exec_posix('test command', {}, @stdin, @stdout, @stderr).should == pid
end
end
describe "#execute_windows (stubs)", :if => Puppet.features.microsoft_windows? do
before :each do
Process.stubs(:create).returns(proc_info_stub)
stub_process_wait(0)
@stdin = File.open(null_file, 'r')
@stdout = Tempfile.new('stdout')
@stderr = File.open(null_file, 'w')
end
it "should create a new process for the command" do
Process.expects(:create).with(
:command_line => "test command",
:startup_info => {:stdin => @stdin, :stdout => @stdout, :stderr => @stderr},
:close_handles => false
).returns(proc_info_stub)
call_exec_windows('test command', {}, @stdin, @stdout, @stderr)
end
it "should return the process info of the child process" do
call_exec_windows('test command', {}, @stdin, @stdout, @stderr).should == proc_info_stub
end
it "should quote arguments containing spaces if command is specified as an array" do
Process.expects(:create).with do |args|
args[:command_line] == '"test command" with some "arguments \"with spaces"'
end.returns(proc_info_stub)
call_exec_windows(['test command', 'with', 'some', 'arguments "with spaces'], {}, @stdin, @stdout, @stderr)
end
end
describe "#execute (stubs)" do
before :each do
stub_process_wait(0)
end
describe "when an execution stub is specified" do
before :each do
Puppet::Util::ExecutionStub.set do |command,args,stdin,stdout,stderr|
"execution stub output"
end
end
it "should call the block on the stub" do
Puppet::Util::Execution.execute("/usr/bin/run_my_execute_stub").should == "execution stub output"
end
it "should not actually execute anything" do
Puppet::Util::Execution.expects(:execute_posix).never
Puppet::Util::Execution.expects(:execute_windows).never
Puppet::Util::Execution.execute("/usr/bin/run_my_execute_stub")
end
end
describe "when setting up input and output files" do
include PuppetSpec::Files
let(:executor) { Puppet.features.microsoft_windows? ? 'execute_windows' : 'execute_posix' }
let(:rval) { Puppet.features.microsoft_windows? ? proc_info_stub : pid }
before :each do
Puppet::Util::Execution.stubs(:wait_for_output)
end
it "should set stdin to the stdinfile if specified" do
input = tmpfile('stdin')
FileUtils.touch(input)
Puppet::Util::Execution.expects(executor).with do |_,_,stdin,_,_|
stdin.path == input
end.returns(rval)
Puppet::Util::Execution.execute('test command', :stdinfile => input)
end
it "should set stdin to the null file if not specified" do
Puppet::Util::Execution.expects(executor).with do |_,_,stdin,_,_|
stdin.path == null_file
end.returns(rval)
Puppet::Util::Execution.execute('test command')
end
describe "when squelch is set" do
it "should set stdout and stderr to the null file" do
Puppet::Util::Execution.expects(executor).with do |_,_,_,stdout,stderr|
stdout.path == null_file and stderr.path == null_file
end.returns(rval)
Puppet::Util::Execution.execute('test command', :squelch => true)
end
end
describe "when squelch is not set" do
it "should set stdout to a temporary output file" do
outfile = Tempfile.new('stdout')
Tempfile.stubs(:new).returns(outfile)
Puppet::Util::Execution.expects(executor).with do |_,_,_,stdout,_|
stdout.path == outfile.path
end.returns(rval)
Puppet::Util::Execution.execute('test command', :squelch => false)
end
it "should set stderr to the same file as stdout if combine is true" do
outfile = Tempfile.new('stdout')
Tempfile.stubs(:new).returns(outfile)
Puppet::Util::Execution.expects(executor).with do |_,_,_,stdout,stderr|
stdout.path == outfile.path and stderr.path == outfile.path
end.returns(rval)
Puppet::Util::Execution.execute('test command', :squelch => false, :combine => true)
end
it "should set stderr to the null device if combine is false" do
outfile = Tempfile.new('stdout')
Tempfile.stubs(:new).returns(outfile)
Puppet::Util::Execution.expects(executor).with do |_,_,_,stdout,stderr|
stdout.path == outfile.path and stderr.path == null_file
end.returns(rval)
Puppet::Util::Execution.execute('test command', :squelch => false, :combine => false)
end
it "should combine stdout and stderr if combine is true" do
outfile = Tempfile.new('stdout')
Tempfile.stubs(:new).returns(outfile)
Puppet::Util::Execution.expects(executor).with do |_,_,_,stdout,stderr|
stdout.path == outfile.path and stderr.path == outfile.path
end.returns(rval)
Puppet::Util::Execution.execute('test command', :combine => true)
end
it "should default combine to true when no options are specified" do
outfile = Tempfile.new('stdout')
Tempfile.stubs(:new).returns(outfile)
Puppet::Util::Execution.expects(executor).with do |_,_,_,stdout,stderr|
stdout.path == outfile.path and stderr.path == outfile.path
end.returns(rval)
Puppet::Util::Execution.execute('test command')
end
it "should default combine to false when options are specified, but combine is not" do
outfile = Tempfile.new('stdout')
Tempfile.stubs(:new).returns(outfile)
Puppet::Util::Execution.expects(executor).with do |_,_,_,stdout,stderr|
stdout.path == outfile.path and stderr.path == null_file
end.returns(rval)
Puppet::Util::Execution.execute('test command', :failonfail => false)
end
it "should default combine to false when an empty hash of options is specified" do
outfile = Tempfile.new('stdout')
Tempfile.stubs(:new).returns(outfile)
Puppet::Util::Execution.expects(executor).with do |_,_,_,stdout,stderr|
stdout.path == outfile.path and stderr.path == null_file
end.returns(rval)
Puppet::Util::Execution.execute('test command', {})
end
end
end
describe "on Windows", :if => Puppet.features.microsoft_windows? do
it "should always close the process and thread handles" do
Puppet::Util::Execution.stubs(:execute_windows).returns(proc_info_stub)
Puppet::Util::Windows::Process.expects(:wait_process).with(process_handle).raises('whatever')
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
describe "#execute (posix locale)", :unless => Puppet.features.microsoft_windows? do
before :each do
# there is a danger here that ENV will be modified by exec_posix. Normally it would only affect the ENV
# of a forked process, but, in some of the previous tests in this file we're stubbing Kernel.fork., which could
# allow the method to override the "real" ENV. This shouldn't be a problem for these tests because they are
# not stubbing Kernel.fork, but, better safe than sorry... so, to guard against this, we'll capture a snapshot
# of ENV before each test.
@saved_env = ENV.to_hash
end
after :each do
# capture the current environment and make sure it's the same as it was before the test
cur_env = ENV.to_hash
# we will get some fairly useless output if we just use the raw == operator on the hashes here, so we'll
# be a bit more explicit and laborious in the name of making the error more useful...
@saved_env.each_pair { |key,val| cur_env[key].should == val }
(cur_env.keys - @saved_env.keys).should == []
end
# build up a printf-style string that contains a command to get the value of an environment variable
# from the operating system. We can substitute into this with the names of the desired environment variables later.
get_env_var_cmd = 'echo $%s'
# a sentinel value that we can use to emulate what locale environment variables might be set to on an international
# system.
lang_sentinel_value = "en_US.UTF-8"
# a temporary hash that contains sentinel values for each of the locale environment variables that we override in
# "execute"
locale_sentinel_env = {}
Puppet::Util::POSIX::LOCALE_ENV_VARS.each { |var| locale_sentinel_env[var] = lang_sentinel_value }
it "should override the locale environment variables when :override_locale is not set (defaults to true)" do
# temporarily override the locale environment vars with a sentinel value, so that we can confirm that
# execute is actually setting them.
Puppet::Util.withenv(locale_sentinel_env) do
Puppet::Util::POSIX::LOCALE_ENV_VARS.each do |var|
# we expect that all of the POSIX vars will have been cleared except for LANG and LC_ALL
expected_value = (['LANG', 'LC_ALL'].include?(var)) ? "C" : ""
Puppet::Util::execute(get_env_var_cmd % var).strip.should == expected_value
end
end
end
it "should override the LANG environment variable when :override_locale is set to true" do
# temporarily override the locale environment vars with a sentinel value, so that we can confirm that
# execute is actually setting them.
Puppet::Util.withenv(locale_sentinel_env) do
Puppet::Util::POSIX::LOCALE_ENV_VARS.each do |var|
# we expect that all of the POSIX vars will have been cleared except for LANG and LC_ALL
expected_value = (['LANG', 'LC_ALL'].include?(var)) ? "C" : ""
Puppet::Util::execute(get_env_var_cmd % var, {:override_locale => true}).strip.should == expected_value
end
end
end
it "should *not* override the LANG environment variable when :override_locale is set to false" do
# temporarily override the locale environment vars with a sentinel value, so that we can confirm that
# execute is not setting them.
Puppet::Util.withenv(locale_sentinel_env) do
Puppet::Util::POSIX::LOCALE_ENV_VARS.each do |var|
Puppet::Util::execute(get_env_var_cmd % var, {:override_locale => false}).strip.should == lang_sentinel_value
end
end
end
it "should have restored the LANG and locale environment variables after execution" do
# we'll do this once without any sentinel values, to give us a little more test coverage
orig_env_vals = {}
Puppet::Util::POSIX::LOCALE_ENV_VARS.each do |var|
orig_env_vals[var] = ENV[var]
end
# now we can really execute any command--doesn't matter what it is...
Puppet::Util::execute(get_env_var_cmd % 'anything', {:override_locale => true})
# now we check and make sure the original environment was restored
Puppet::Util::POSIX::LOCALE_ENV_VARS.each do |var|
ENV[var].should == orig_env_vals[var]
end
# now, once more... but with our sentinel values
Puppet::Util.withenv(locale_sentinel_env) do
# now we can really execute any command--doesn't matter what it is...
Puppet::Util::execute(get_env_var_cmd % 'anything', {:override_locale => true})
# now we check and make sure the original environment was restored
Puppet::Util::POSIX::LOCALE_ENV_VARS.each do |var|
ENV[var].should == locale_sentinel_env[var]
end
end
end
end
describe "#execute (posix user env vars)", :unless => Puppet.features.microsoft_windows? do
# build up a printf-style string that contains a command to get the value of an environment variable
# from the operating system. We can substitute into this with the names of the desired environment variables later.
get_env_var_cmd = 'echo $%s'
# a sentinel value that we can use to emulate what locale environment variables might be set to on an international
# system.
user_sentinel_value = "Abracadabra"
# a temporary hash that contains sentinel values for each of the locale environment variables that we override in
# "execute"
user_sentinel_env = {}
Puppet::Util::POSIX::USER_ENV_VARS.each { |var| user_sentinel_env[var] = user_sentinel_value }
it "should unset user-related environment vars during execution" do
# first we set up a temporary execution environment with sentinel values for the user-related environment vars
# that we care about.
Puppet::Util.withenv(user_sentinel_env) do
# with this environment, we loop over the vars in question
Puppet::Util::POSIX::USER_ENV_VARS.each do |var|
# ensure that our temporary environment is set up as we expect
ENV[var].should == user_sentinel_env[var]
# run an "exec" via the provider and ensure that it unsets the vars
Puppet::Util::execute(get_env_var_cmd % var).strip.should == ""
# ensure that after the exec, our temporary env is still intact
ENV[var].should == user_sentinel_env[var]
end
end
end
it "should have restored the user-related environment variables after execution" do
# we'll do this once without any sentinel values, to give us a little more test coverage
orig_env_vals = {}
Puppet::Util::POSIX::USER_ENV_VARS.each do |var|
orig_env_vals[var] = ENV[var]
end
# now we can really execute any command--doesn't matter what it is...
Puppet::Util::execute(get_env_var_cmd % 'anything')
# now we check and make sure the original environment was restored
Puppet::Util::POSIX::USER_ENV_VARS.each do |var|
ENV[var].should == orig_env_vals[var]
end
# now, once more... but with our sentinel values
Puppet::Util.withenv(user_sentinel_env) do
# now we can really execute any command--doesn't matter what it is...
Puppet::Util::execute(get_env_var_cmd % 'anything')
# now we check and make sure the original environment was restored
Puppet::Util::POSIX::USER_ENV_VARS.each do |var|
ENV[var].should == user_sentinel_env[var]
end
end
end
end
describe "after execution" do
before :each do
stub_process_wait(0)
if Puppet.features.microsoft_windows?
Puppet::Util::Execution.stubs(:execute_windows).returns(proc_info_stub)
else
Puppet::Util::Execution.stubs(:execute_posix).returns(pid)
end
end
it "should wait for the child process to exit" do
Puppet::Util::Execution.stubs(:wait_for_output)
Puppet::Util::Execution.execute('test command')
end
it "should close the stdin/stdout/stderr files used by the child" do
stdin = mock 'file', :close
stdout = mock 'file', :close
stderr = mock 'file', :close
File.expects(:open).
times(3).
returns(stdin).
then.returns(stdout).
then.returns(stderr)
Puppet::Util::Execution.execute('test command', {:squelch => true, :combine => false})
end
it "should read and return the output if squelch is false" do
stdout = Tempfile.new('test')
Tempfile.stubs(:new).returns(stdout)
stdout.write("My expected command output")
Puppet::Util::Execution.execute('test command').should == "My expected command output"
end
it "should not read the output if squelch is true" do
stdout = Tempfile.new('test')
Tempfile.stubs(:new).returns(stdout)
stdout.write("My expected command output")
Puppet::Util::Execution.execute('test command', :squelch => true).should == ''
end
it "should delete the file used for output if squelch is false" do
stdout = Tempfile.new('test')
path = stdout.path
Tempfile.stubs(:new).returns(stdout)
Puppet::Util::Execution.execute('test command')
- Puppet::FileSystem::File.exist?(path).should be_false
+ Puppet::FileSystem.exist?(path).should be_false
end
it "should not raise an error if the file is open" do
stdout = Tempfile.new('test')
Tempfile.stubs(:new).returns(stdout)
file = File.new(stdout.path, 'r')
Puppet::Util.execute('test command')
end
it "should raise an error if failonfail is true and the child failed" do
stub_process_wait(1)
expect {
subject.execute('fail command', :failonfail => true)
}.to raise_error(Puppet::ExecutionFailure, /Execution of 'fail command' returned 1/)
end
it "should not raise an error if failonfail is false and the child failed" do
stub_process_wait(1)
subject.execute('fail command', :failonfail => false)
end
it "should not raise an error if failonfail is true and the child succeeded" do
stub_process_wait(0)
subject.execute('fail command', :failonfail => true)
end
it "should not raise an error if failonfail is false and the child succeeded" do
stub_process_wait(0)
subject.execute('fail command', :failonfail => false)
end
it "should default failonfail to true when no options are specified" do
stub_process_wait(1)
expect {
subject.execute('fail command')
}.to raise_error(Puppet::ExecutionFailure, /Execution of 'fail command' returned 1/)
end
it "should default failonfail to false when options are specified, but failonfail is not" do
stub_process_wait(1)
subject.execute('fail command', { :combine => true })
end
it "should default failonfail to false when an empty hash of options is specified" do
stub_process_wait(1)
subject.execute('fail command', {})
end
it "should raise an error if a nil option is specified" do
expect {
Puppet::Util::Execution.execute('fail command', nil)
}.to raise_error(TypeError, /(can\'t convert|no implicit conversion of) nil into Hash/)
end
end
end
describe "#execpipe" do
it "should execute a string as a string" do
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').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)
Puppet::Util::Execution.execpipe(['echo', 'hello']).should == 'hello'
end
it "should fail if asked to fail, and the child does" do
Puppet::Util::Execution.stubs(:open).returns('error message')
$CHILD_STATUS.expects(:==).with(0).returns(false)
expect { Puppet::Util::Execution.execpipe('echo hello') }.
to raise_error Puppet::ExecutionFailure, /error message/
end
it "should not fail if asked not to fail, and the child does" do
Puppet::Util::Execution.stubs(:open).returns('error message')
$CHILD_STATUS.stubs(:==).with(0).returns(false)
Puppet::Util::Execution.execpipe('echo hello', false).should == 'error message'
end
end
end
diff --git a/spec/unit/util/filetype_spec.rb b/spec/unit/util/filetype_spec.rb
index 5d8f0b36d..304b49352 100755
--- a/spec/unit/util/filetype_spec.rb
+++ b/spec/unit/util/filetype_spec.rb
@@ -1,211 +1,211 @@
#! /usr/bin/env ruby
require 'spec_helper'
require 'puppet/util/filetype'
# XXX Import all of the tests into this file.
describe Puppet::Util::FileType do
describe "the flat filetype" do
let(:path) { '/my/file' }
let(:type) { Puppet::Util::FileType.filetype(:flat) }
let(:file) { type.new(path) }
it "should exist" do
type.should_not be_nil
end
describe "when the file already exists" do
it "should return the file's contents when asked to read it" do
- Puppet::FileSystem::File.expects(:exist?).with(path).returns true
+ Puppet::FileSystem.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
- Puppet::FileSystem::File.expects(:exist?).with(path).returns true
- Puppet::FileSystem::File.expects(:unlink).with(path)
+ Puppet::FileSystem.expects(:exist?).with(path).returns true
+ Puppet::FileSystem.expects(:unlink).with(path)
file.remove
end
end
describe "when the file does not exist" do
it "should return an empty string when asked to read the file" do
- Puppet::FileSystem::File.expects(:exist?).with(path).returns false
+ Puppet::FileSystem.expects(:exist?).with(path).returns false
file.read.should == ""
end
end
describe "when writing the file" do
let(:tempfile) { stub 'tempfile', :print => nil, :close => nil, :flush => nil, :path => "/other/file" }
before do
FileUtils.stubs(:cp)
Tempfile.stubs(:new).returns tempfile
end
it "should first create a temp file and copy its contents over to the file location" do
Tempfile.expects(:new).with("puppet").returns tempfile
tempfile.expects(:print).with("my text")
tempfile.expects(:flush)
tempfile.expects(:close)
FileUtils.expects(:cp).with(tempfile.path, path)
file.write "my text"
end
it "should set the selinux default context on the file" do
file.expects(:set_selinux_default_context).with(path)
file.write "eh"
end
end
describe "when backing up a file" do
it "should do nothing if the file does not exist" do
- Puppet::FileSystem::File.expects(:exist?).with(path).returns false
+ Puppet::FileSystem.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
- Puppet::FileSystem::File.expects(:exist?).with(path).returns true
+ Puppet::FileSystem.expects(:exist?).with(path).returns true
bucket = mock 'bucket'
bucket.expects(:backup).with(path)
file.expects(:bucket).returns bucket
file.backup
end
it "should use the default filebucket" do
bucket = mock 'bucket'
bucket.expects(:bucket).returns "mybucket"
Puppet::Type.type(:filebucket).expects(:mkdefaultbucket).returns bucket
file.bucket.should == "mybucket"
end
end
end
shared_examples_for "crontab provider" do
let(:cron) { type.new('no_such_user') }
let(:crontab) { File.read(my_fixture(crontab_output)) }
let(:options) { { :failonfail => true, :combine => true } }
let(:uid) { 'no_such_user' }
let(:user_options) { options.merge({:uid => uid}) }
it "should exist" do
type.should_not be_nil
end
# make Puppet::Util::SUIDManager return something deterministic, not the
# uid of the user running the tests, except where overridden below.
before :each do
Puppet::Util::SUIDManager.stubs(:uid).returns 1234
end
describe "#read" do
it "should run crontab -l as the target user" do
Puppet::Util::Execution.expects(:execute).with(['crontab', '-l'], user_options).returns crontab
cron.read.should == crontab
end
it "should not switch user if current user is the target user" do
Puppet::Util.expects(:uid).with(uid).returns 9000
Puppet::Util::SUIDManager.expects(:uid).returns 9000
Puppet::Util::Execution.expects(:execute).with(['crontab', '-l'], options).returns crontab
cron.read.should == crontab
end
it "should treat an absent crontab as empty" do
Puppet::Util::Execution.expects(:execute).with(['crontab', '-l'], user_options).raises(Puppet::ExecutionFailure, absent_crontab)
cron.read.should == ''
end
it "should raise an error if the user is not authorized to use cron" do
Puppet::Util::Execution.expects(:execute).with(['crontab', '-l'], user_options).raises(Puppet::ExecutionFailure, unauthorized_crontab)
expect {
cron.read
}.to raise_error Puppet::Error, /User #{uid} not authorized to use cron/
end
end
describe "#remove" do
it "should run crontab -r as the target user" do
Puppet::Util::Execution.expects(:execute).with(['crontab', '-r'], user_options)
cron.remove
end
it "should not switch user if current user is the target user" do
Puppet::Util.expects(:uid).with(uid).returns 9000
Puppet::Util::SUIDManager.expects(:uid).returns 9000
Puppet::Util::Execution.expects(:execute).with(['crontab','-r'], options)
cron.remove
end
end
describe "#write" do
before :each do
@tmp_cron = Tempfile.new("puppet_crontab_spec")
@tmp_cron_path = @tmp_cron.path
Puppet::Util.stubs(:uid).with(uid).returns 9000
Tempfile.expects(:new).with("puppet_#{name}").returns @tmp_cron
end
after :each do
- Puppet::FileSystem::File.exist?(@tmp_cron_path).should be_false
+ Puppet::FileSystem.exist?(@tmp_cron_path).should be_false
end
it "should run crontab as the target user on a temporary file" do
File.expects(:chown).with(9000, nil, @tmp_cron_path)
Puppet::Util::Execution.expects(:execute).with(["crontab", @tmp_cron_path], user_options)
@tmp_cron.expects(:print).with("foo\n")
cron.write "foo\n"
end
it "should not switch user if current user is the target user" do
Puppet::Util::SUIDManager.expects(:uid).returns 9000
File.expects(:chown).with(9000, nil, @tmp_cron_path)
Puppet::Util::Execution.expects(:execute).with(["crontab", @tmp_cron_path], options)
@tmp_cron.expects(:print).with("foo\n")
cron.write "foo\n"
end
end
end
describe "the suntab filetype", :unless => Puppet.features.microsoft_windows? do
let(:type) { Puppet::Util::FileType.filetype(:suntab) }
let(:name) { type.name }
let(:crontab_output) { 'suntab_output' }
# possible crontab output was taken from here:
# http://docs.oracle.com/cd/E19082-01/819-2380/sysrescron-60/index.html
let(:absent_crontab) do
'crontab: can\'t open your crontab file'
end
let(:unauthorized_crontab) do
'crontab: you are not authorized to use cron. Sorry.'
end
it_should_behave_like "crontab provider"
end
describe "the aixtab filetype", :unless => Puppet.features.microsoft_windows? do
let(:type) { Puppet::Util::FileType.filetype(:aixtab) }
let(:name) { type.name }
let(:crontab_output) { 'aixtab_output' }
let(:absent_crontab) do
'0481-103 Cannot open a file in the /var/spool/cron/crontabs directory.'
end
let(:unauthorized_crontab) do
'0481-109 You are not authorized to use the cron command.'
end
it_should_behave_like "crontab provider"
end
end
diff --git a/spec/unit/util/instrumentation/data_spec.rb b/spec/unit/util/instrumentation/data_spec.rb
index 847ff9ad2..418d89724 100755
--- a/spec/unit/util/instrumentation/data_spec.rb
+++ b/spec/unit/util/instrumentation/data_spec.rb
@@ -1,44 +1,46 @@
#! /usr/bin/env ruby
require 'spec_helper'
require 'matchers/json'
require 'puppet/util/instrumentation'
require 'puppet/util/instrumentation/data'
describe Puppet::Util::Instrumentation::Data do
+ include JSONMatchers
+
Puppet::Util::Instrumentation::Data
before(:each) do
@listener = stub 'listener', :name => "name"
Puppet::Util::Instrumentation.stubs(:[]).with("name").returns(@listener)
end
it "should indirect instrumentation_data" do
Puppet::Util::Instrumentation::Data.indirection.name.should == :instrumentation_data
end
it "should lookup the corresponding listener" do
Puppet::Util::Instrumentation.expects(:[]).with("name").returns(@listener)
Puppet::Util::Instrumentation::Data.new("name")
end
it "should error if the listener can not be found" do
Puppet::Util::Instrumentation.expects(:[]).with("name").returns(nil)
expect { Puppet::Util::Instrumentation::Data.new("name") }.to raise_error
end
it "should return pson data" do
data = Puppet::Util::Instrumentation::Data.new("name")
@listener.stubs(:data).returns({ :this_is_data => "here also" })
data.should set_json_attribute('name').to("name")
data.should set_json_attribute('this_is_data').to("here also")
end
it "should not error if the underlying listener doesn't have data" do
lambda { Puppet::Util::Instrumentation::Data.new("name").to_pson }.should_not raise_error
end
it "should return a hash containing data when unserializing from pson" do
- Puppet::Util::Instrumentation::Data.from_pson({:name => "name"}).should == {:name => "name"}
+ Puppet::Util::Instrumentation::Data.from_data_hash({:name => "name"}).should == {:name => "name"}
end
end
diff --git a/spec/unit/util/instrumentation/indirection_probe_spec.rb b/spec/unit/util/instrumentation/indirection_probe_spec.rb
index 337777ca6..d06849e5e 100644
--- a/spec/unit/util/instrumentation/indirection_probe_spec.rb
+++ b/spec/unit/util/instrumentation/indirection_probe_spec.rb
@@ -1,19 +1,21 @@
#! /usr/bin/env ruby
require 'spec_helper'
require 'matchers/json'
require 'puppet/util/instrumentation'
require 'puppet/util/instrumentation/indirection_probe'
describe Puppet::Util::Instrumentation::IndirectionProbe do
+ include JSONMatchers
+
Puppet::Util::Instrumentation::IndirectionProbe
it "should indirect instrumentation_probe" do
Puppet::Util::Instrumentation::IndirectionProbe.indirection.name.should == :instrumentation_probe
end
it "should return pson data" do
probe = Puppet::Util::Instrumentation::IndirectionProbe.new("probe")
probe.should set_json_attribute('name').to("probe")
end
end
diff --git a/spec/unit/util/instrumentation/listener_spec.rb b/spec/unit/util/instrumentation/listener_spec.rb
index 5072c4fe5..344934908 100755
--- a/spec/unit/util/instrumentation/listener_spec.rb
+++ b/spec/unit/util/instrumentation/listener_spec.rb
@@ -1,100 +1,101 @@
#! /usr/bin/env ruby
require 'spec_helper'
require 'matchers/json'
require 'puppet/util/instrumentation'
require 'puppet/util/instrumentation/listener'
describe Puppet::Util::Instrumentation::Listener do
+ include JSONMatchers
Listener = Puppet::Util::Instrumentation::Listener
before(:each) do
@delegate = stub 'listener', :notify => nil, :name => 'listener'
@listener = Listener.new(@delegate)
@listener.enabled = true
end
it "should indirect instrumentation_listener" do
Listener.indirection.name.should == :instrumentation_listener
end
it "should raise an error if delegate doesn't support notify" do
lambda { Listener.new(Object.new) }.should raise_error
end
it "should not be enabled by default" do
Listener.new(@delegate).should_not be_enabled
end
it "should delegate notification" do
@delegate.expects(:notify).with(:event, :start, {})
listener = Listener.new(@delegate)
listener.notify(:event, :start, {})
end
it "should not listen is not enabled" do
@listener.enabled = false
@listener.should_not be_listen_to(:label)
end
it "should listen to all label if created without pattern" do
@listener.should be_listen_to(:improbable_label)
end
it "should listen to specific string pattern" do
listener = Listener.new(@delegate, "specific")
listener.enabled = true
listener.should be_listen_to(:specific)
end
it "should not listen to non-matching string pattern" do
listener = Listener.new(@delegate, "specific")
listener.enabled = true
listener.should_not be_listen_to(:unspecific)
end
it "should listen to specific regex pattern" do
listener = Listener.new(@delegate, /spe.*/)
listener.enabled = true
listener.should be_listen_to(:specific_pattern)
end
it "should not listen to non matching regex pattern" do
listener = Listener.new(@delegate, /^match.*/)
listener.enabled = true
listener.should_not be_listen_to(:not_matching)
end
it "should delegate its name to the underlying listener" do
@delegate.expects(:name).returns("myname")
@listener.name.should == "myname"
end
it "should delegate data fetching to the underlying listener" do
@delegate.expects(:data).returns(:data)
@listener.data.should == {:data => :data }
end
describe "when serializing to pson" do
it "should return a pson object containing pattern, name and status" do
@listener.should set_json_attribute('enabled').to(true)
@listener.should set_json_attribute('name').to("listener")
end
end
describe "when deserializing from pson" do
it "should lookup the archetype listener from the instrumentation layer" do
Puppet::Util::Instrumentation.expects(:[]).with("listener").returns(@listener)
- Puppet::Util::Instrumentation::Listener.from_pson({"name" => "listener"})
+ Puppet::Util::Instrumentation::Listener.from_data_hash({"name" => "listener"})
end
it "should create a new listener shell instance delegating to the archetypal listener" do
Puppet::Util::Instrumentation.expects(:[]).with("listener").returns(@listener)
@listener.stubs(:listener).returns(@delegate)
Puppet::Util::Instrumentation::Listener.expects(:new).with(@delegate, nil, true)
- Puppet::Util::Instrumentation::Listener.from_pson({"name" => "listener", "enabled" => true})
+ Puppet::Util::Instrumentation::Listener.from_data_hash({"name" => "listener", "enabled" => true})
end
end
end
diff --git a/spec/unit/util/json_lockfile_spec.rb b/spec/unit/util/json_lockfile_spec.rb
index 8f184646c..a547508a5 100644
--- a/spec/unit/util/json_lockfile_spec.rb
+++ b/spec/unit/util/json_lockfile_spec.rb
@@ -1,29 +1,50 @@
#! /usr/bin/env ruby
require 'spec_helper'
require 'puppet/util/json_lockfile'
describe Puppet::Util::JsonLockfile do
require 'puppet_spec/files'
include PuppetSpec::Files
before(:each) do
@lockfile = tmpfile("lock")
@lock = Puppet::Util::JsonLockfile.new(@lockfile)
end
describe "#lock" do
it "should create a lock file containing a json hash" do
data = { "foo" => "foofoo", "bar" => "barbar" }
@lock.lock(data)
PSON.parse(File.read(@lockfile)).should == data
end
end
- it "should return the lock data" do
- data = { "foo" => "foofoo", "bar" => "barbar" }
- @lock.lock(data)
- @lock.lock_data.should == data
+ describe "reading lock data" do
+ it "returns deserialized JSON from the lockfile" do
+ data = { "foo" => "foofoo", "bar" => "barbar" }
+ @lock.lock(data)
+ expect(@lock.lock_data).to eq data
+ end
+
+ it "returns nil if the file read returned nil" do
+ @lock.lock
+ File.stubs(:read).returns nil
+ expect(@lock.lock_data).to be_nil
+ end
+
+ it "returns nil if the file was empty" do
+ @lock.lock
+ File.stubs(:read).returns ''
+ expect(@lock.lock_data).to be_nil
+ end
+
+ it "returns nil if the file was not in PSON" do
+ @lock.lock
+ File.stubs(:read).returns ']['
+ expect(@lock.lock_data).to be_nil
+ end
+
end
end
diff --git a/spec/unit/util/lockfile_spec.rb b/spec/unit/util/lockfile_spec.rb
index 8d10d5106..33755dad2 100644
--- a/spec/unit/util/lockfile_spec.rb
+++ b/spec/unit/util/lockfile_spec.rb
@@ -1,76 +1,118 @@
#! /usr/bin/env ruby
require 'spec_helper'
require 'puppet/util/lockfile'
+module LockfileSpecHelper
+ def self.run_in_forks(count, &blk)
+ forks = {}
+ results = []
+ count.times do |i|
+ forks[i] = {}
+ forks[i][:read], forks[i][:write] = IO.pipe
+
+ forks[i][:pid] = fork do
+ forks[i][:read].close
+ res = yield
+ Marshal.dump(res, forks[i][:write])
+ exit!
+ end
+ end
+
+ count.times do |i|
+ forks[i][:write].close
+ result = forks[i][:read].read
+ forks[i][:read].close
+ Process.wait2(forks[i][:pid])
+ results << Marshal.load(result)
+ end
+ results
+ end
+end
+
describe Puppet::Util::Lockfile do
require 'puppet_spec/files'
include PuppetSpec::Files
before(:each) do
@lockfile = tmpfile("lock")
@lock = Puppet::Util::Lockfile.new(@lockfile)
end
describe "#lock" do
- it "should return false if already locked" do
- @lock.stubs(:locked?).returns(true)
- @lock.lock.should be_false
- end
-
it "should return true if it successfully locked" do
@lock.lock.should be_true
end
+ it "should return false if already locked" do
+ @lock.lock
+ @lock.lock.should be_false
+ end
+
it "should create a lock file" do
@lock.lock
- Puppet::FileSystem::File.exist?(@lockfile).should be_true
+ Puppet::FileSystem.exist?(@lockfile).should be_true
+ end
+
+ # We test simultaneous locks using fork which isn't supported on Windows.
+ it "should not be acquired by another process", :unless => Puppet.features.microsoft_windows? do
+ 30.times do
+ forks = 3
+ results = LockfileSpecHelper.run_in_forks(forks) do
+ @lock.lock(Process.pid)
+ end
+ @lock.unlock
+
+ # Confirm one fork returned true and everyone else false.
+ (results - [true]).size.should == forks - 1
+ (results - [false]).size.should == 1
+ end
end
it "should create a lock file containing a string" do
data = "foofoo barbar"
@lock.lock(data)
File.read(@lockfile).should == data
end
end
describe "#unlock" do
it "should return true when unlocking" do
@lock.lock
@lock.unlock.should be_true
end
it "should return false when not locked" do
@lock.unlock.should be_false
end
it "should clear the lock file" do
File.open(@lockfile, 'w') { |fd| fd.print("locked") }
@lock.unlock
- Puppet::FileSystem::File.exist?(@lockfile).should be_false
+ Puppet::FileSystem.exist?(@lockfile).should be_false
end
end
it "should be locked when locked" do
@lock.lock
@lock.should be_locked
end
it "should not be locked when not locked" do
@lock.should_not be_locked
end
it "should not be locked when unlocked" do
@lock.lock
@lock.unlock
@lock.should_not be_locked
end
it "should return the lock data" do
data = "foofoo barbar"
@lock.lock(data)
@lock.lock_data.should == data
end
end
diff --git a/spec/unit/util/log/destinations_spec.rb b/spec/unit/util/log/destinations_spec.rb
index b34b973cf..a91236dba 100755
--- a/spec/unit/util/log/destinations_spec.rb
+++ b/spec/unit/util/log/destinations_spec.rb
@@ -1,182 +1,183 @@
#! /usr/bin/env ruby
require 'spec_helper'
+require 'json'
require 'puppet/util/log'
describe Puppet::Util::Log.desttypes[:report] do
before do
@dest = Puppet::Util::Log.desttypes[:report]
end
it "should require a report at initialization" do
@dest.new("foo").report.should == "foo"
end
it "should send new messages to the report" do
report = mock 'report'
dest = @dest.new(report)
report.expects(:<<).with("my log")
dest.handle "my log"
end
end
describe Puppet::Util::Log.desttypes[:file] do
include PuppetSpec::Files
before do
File.stubs(:open) # prevent actually creating the file
File.stubs(:chown) # prevent chown on non existing file from failing
@class = Puppet::Util::Log.desttypes[:file]
end
it "should default to autoflush false" do
@class.new(tmpfile('log')).autoflush.should == true
end
describe "when matching" do
shared_examples_for "file destination" do
it "should match an absolute path" do
@class.match?(abspath).should be_true
end
it "should not match a relative path" do
@class.match?(relpath).should be_false
end
end
describe "on POSIX systems", :as_platform => :posix do
let (:abspath) { '/tmp/log' }
let (:relpath) { 'log' }
it_behaves_like "file destination"
end
describe "on Windows systems", :as_platform => :windows do
let (:abspath) { 'C:\\temp\\log.txt' }
let (:relpath) { 'log.txt' }
it_behaves_like "file destination"
end
end
end
describe Puppet::Util::Log.desttypes[:syslog] do
let (:klass) { Puppet::Util::Log.desttypes[:syslog] }
# these tests can only be run when syslog is present, because
# we can't stub the top-level Syslog module
describe "when syslog is available", :if => Puppet.features.syslog? do
before :each do
Syslog.stubs(:opened?).returns(false)
Syslog.stubs(:const_get).returns("LOG_KERN").returns(0)
Syslog.stubs(:open)
end
it "should open syslog" do
Syslog.expects(:open)
klass.new
end
it "should close syslog" do
Syslog.expects(:close)
dest = klass.new
dest.close
end
it "should send messages to syslog" do
syslog = mock 'syslog'
syslog.expects(:info).with("don't panic")
Syslog.stubs(:open).returns(syslog)
msg = Puppet::Util::Log.new(:level => :info, :message => "don't panic")
dest = klass.new
dest.handle(msg)
end
end
describe "when syslog is unavailable" do
it "should not be a suitable log destination" do
Puppet.features.stubs(:syslog?).returns(false)
klass.suitable?(:syslog).should be_false
end
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] }
describe "when color is available" do
before :each do
subject.stubs(:console_has_color?).returns(true)
end
it "should support color output" do
Puppet[:color] = true
subject.colorize(:red, 'version').should == "\e[0;31mversion\e[0m"
end
it "should withhold color output when not appropriate" do
Puppet[:color] = false
subject.colorize(:red, 'version').should == "version"
end
it "should handle multiple overlapping colors in a stack-like way" do
Puppet[:color] = true
vstring = subject.colorize(:red, 'version')
subject.colorize(:green, "(#{vstring})").should == "\e[0;32m(\e[0;31mversion\e[0;32m)\e[0m"
end
it "should handle resets in a stack-like way" do
Puppet[:color] = true
vstring = subject.colorize(:reset, 'version')
subject.colorize(:green, "(#{vstring})").should == "\e[0;32m(\e[mversion\e[0;32m)\e[0m"
end
it "should include the log message's source/context in the output when available" do
Puppet[:color] = false
$stdout.expects(:puts).with("Info: a hitchhiker: don't panic")
msg = Puppet::Util::Log.new(:level => :info, :message => "don't panic", :source => "a hitchhiker")
dest = klass.new
dest.handle(msg)
end
end
end
diff --git a/spec/unit/util/log_spec.rb b/spec/unit/util/log_spec.rb
index 99984246a..bc6ecfcf0 100755
--- a/spec/unit/util/log_spec.rb
+++ b/spec/unit/util/log_spec.rb
@@ -1,378 +1,378 @@
#! /usr/bin/env ruby
require 'spec_helper'
require 'puppet/util/log'
describe Puppet::Util::Log do
include PuppetSpec::Files
def log_notice(message)
Puppet::Util::Log.new(:level => :notice, :message => message)
end
it "should write a given message to the specified destination" do
arraydest = []
Puppet::Util::Log.newdestination(Puppet::Test::LogCollector.new(arraydest))
Puppet::Util::Log.new(:level => :notice, :message => "foo")
message = arraydest.last.message
message.should == "foo"
end
describe ".setup_default" do
it "should default to :syslog" do
Puppet.features.stubs(:syslog?).returns(true)
Puppet::Util::Log.expects(:newdestination).with(:syslog)
Puppet::Util::Log.setup_default
end
it "should fall back to :eventlog" do
Puppet.features.stubs(:syslog?).returns(false)
Puppet.features.stubs(:eventlog?).returns(true)
Puppet::Util::Log.expects(:newdestination).with(:eventlog)
Puppet::Util::Log.setup_default
end
it "should fall back to :file" do
Puppet.features.stubs(:syslog?).returns(false)
Puppet.features.stubs(:eventlog?).returns(false)
Puppet::Util::Log.expects(:newdestination).with(Puppet[:puppetdlog])
Puppet::Util::Log.setup_default
end
end
describe "#with_destination" do
it "does nothing when nested" do
logs = []
destination = Puppet::Test::LogCollector.new(logs)
Puppet::Util::Log.with_destination(destination) do
Puppet::Util::Log.with_destination(destination) do
log_notice("Inner block")
end
log_notice("Outer block")
end
log_notice("Outside")
expect(logs.collect(&:message)).to include("Inner block", "Outer block")
expect(logs.collect(&:message)).not_to include("Outside")
end
it "logs when called a second time" do
logs = []
destination = Puppet::Test::LogCollector.new(logs)
Puppet::Util::Log.with_destination(destination) do
log_notice("First block")
end
log_notice("Between blocks")
Puppet::Util::Log.with_destination(destination) do
log_notice("Second block")
end
expect(logs.collect(&:message)).to include("First block", "Second block")
expect(logs.collect(&:message)).not_to include("Between blocks")
end
it "doesn't close the destination if already set manually" do
logs = []
destination = Puppet::Test::LogCollector.new(logs)
Puppet::Util::Log.newdestination(destination)
Puppet::Util::Log.with_destination(destination) do
log_notice "Inner block"
end
log_notice "Outer block"
Puppet::Util::Log.close(destination)
expect(logs.collect(&:message)).to include("Inner block", "Outer block")
end
end
describe Puppet::Util::Log::DestConsole do
before do
@console = Puppet::Util::Log::DestConsole.new
@console.stubs(:console_has_color?).returns(true)
end
it "should colorize if Puppet[:color] is :ansi" do
Puppet[:color] = :ansi
@console.colorize(:alert, "abc").should == "\e[0;31mabc\e[0m"
end
it "should colorize if Puppet[:color] is 'yes'" do
Puppet[:color] = "yes"
@console.colorize(:alert, "abc").should == "\e[0;31mabc\e[0m"
end
it "should htmlize if Puppet[:color] is :html" do
Puppet[:color] = :html
@console.colorize(:alert, "abc").should == "<span style=\"color: #FFA0A0\">abc</span>"
end
it "should do nothing if Puppet[:color] is false" do
Puppet[:color] = false
@console.colorize(:alert, "abc").should == "abc"
end
it "should do nothing if Puppet[:color] is invalid" do
Puppet[:color] = "invalid option"
@console.colorize(:alert, "abc").should == "abc"
end
end
describe Puppet::Util::Log::DestSyslog do
before do
@syslog = Puppet::Util::Log::DestSyslog.new
end
end
describe Puppet::Util::Log::DestEventlog, :if => Puppet.features.eventlog? do
before :each do
Win32::EventLog.stubs(:open).returns(mock 'mylog')
Win32::EventLog.stubs(:report_event)
Win32::EventLog.stubs(:close)
Puppet.features.stubs(:eventlog?).returns(true)
end
it "should restrict its suitability" do
Puppet.features.expects(:eventlog?).returns(false)
Puppet::Util::Log::DestEventlog.suitable?('whatever').should == false
end
it "should open the 'Application' event log" do
Win32::EventLog.expects(:open).with('Application')
Puppet::Util::Log.newdestination(:eventlog)
end
it "should close the event log" do
log = mock('myeventlog')
log.expects(:close)
Win32::EventLog.expects(:open).returns(log)
Puppet::Util::Log.newdestination(:eventlog)
Puppet::Util::Log.close(:eventlog)
end
it "should handle each puppet log level" do
log = Puppet::Util::Log::DestEventlog.new
Puppet::Util::Log.eachlevel do |level|
log.to_native(level).should be_is_a(Array)
end
end
end
describe "instances" do
before do
Puppet::Util::Log.stubs(:newmessage)
end
[:level, :message, :time, :remote].each do |attr|
it "should have a #{attr} attribute" do
log = Puppet::Util::Log.new :level => :notice, :message => "A test message"
log.should respond_to(attr)
log.should respond_to(attr.to_s + "=")
end
end
it "should fail if created without a level" do
lambda { Puppet::Util::Log.new(:message => "A test message") }.should raise_error(ArgumentError)
end
it "should fail if created without a message" do
lambda { Puppet::Util::Log.new(:level => :notice) }.should raise_error(ArgumentError)
end
it "should make available the level passed in at initialization" do
Puppet::Util::Log.new(:level => :notice, :message => "A test message").level.should == :notice
end
it "should make available the message passed in at initialization" do
Puppet::Util::Log.new(:level => :notice, :message => "A test message").message.should == "A test message"
end
# LAK:NOTE I don't know why this behavior is here, I'm just testing what's in the code,
# at least at first.
it "should always convert messages to strings" do
Puppet::Util::Log.new(:level => :notice, :message => :foo).message.should == "foo"
end
it "should flush the log queue when the first destination is specified" do
Puppet::Util::Log.close_all
Puppet::Util::Log.expects(:flushqueue)
Puppet::Util::Log.newdestination(:console)
end
it "should convert the level to a symbol if it's passed in as a string" do
Puppet::Util::Log.new(:level => "notice", :message => :foo).level.should == :notice
end
it "should fail if the level is not a symbol or string" do
lambda { Puppet::Util::Log.new(:level => 50, :message => :foo) }.should raise_error(ArgumentError)
end
it "should fail if the provided level is not valid" do
Puppet::Util::Log.expects(:validlevel?).with(:notice).returns false
lambda { Puppet::Util::Log.new(:level => :notice, :message => :foo) }.should raise_error(ArgumentError)
end
it "should set its time to the initialization time" do
time = mock 'time'
Time.expects(:now).returns time
Puppet::Util::Log.new(:level => "notice", :message => :foo).time.should equal(time)
end
it "should make available any passed-in tags" do
log = Puppet::Util::Log.new(:level => "notice", :message => :foo, :tags => %w{foo bar})
log.tags.should be_include("foo")
log.tags.should be_include("bar")
end
it "should use a passed-in source" do
Puppet::Util::Log.any_instance.expects(:source=).with "foo"
Puppet::Util::Log.new(:level => "notice", :message => :foo, :source => "foo")
end
[:file, :line].each do |attr|
it "should use #{attr} if provided" do
Puppet::Util::Log.any_instance.expects(attr.to_s + "=").with "foo"
Puppet::Util::Log.new(:level => "notice", :message => :foo, attr => "foo")
end
end
it "should default to 'Puppet' as its source" do
Puppet::Util::Log.new(:level => "notice", :message => :foo).source.should == "Puppet"
end
it "should register itself with Log" do
Puppet::Util::Log.expects(:newmessage)
Puppet::Util::Log.new(:level => "notice", :message => :foo)
end
it "should update Log autoflush when Puppet[:autoflush] is set" do
Puppet::Util::Log.expects(:autoflush=).once.with(true)
Puppet[:autoflush] = true
end
it "should have a method for determining if a tag is present" do
Puppet::Util::Log.new(:level => "notice", :message => :foo).should respond_to(:tagged?)
end
it "should match a tag if any of the tags are equivalent to the passed tag as a string" do
Puppet::Util::Log.new(:level => "notice", :message => :foo, :tags => %w{one two}).should be_tagged(:one)
end
it "should tag itself with its log level" do
Puppet::Util::Log.new(:level => "notice", :message => :foo).should be_tagged(:notice)
end
it "should return its message when converted to a string" do
Puppet::Util::Log.new(:level => "notice", :message => :foo).to_s.should == "foo"
end
it "should include its time, source, level, and message when prepared for reporting" do
log = Puppet::Util::Log.new(:level => "notice", :message => :foo)
report = log.to_report
report.should be_include("notice")
report.should be_include("foo")
report.should be_include(log.source)
report.should be_include(log.time.to_s)
end
it "should not create unsuitable log destinations" do
Puppet.features.stubs(:syslog?).returns(false)
Puppet::Util::Log::DestSyslog.expects(:suitable?)
Puppet::Util::Log::DestSyslog.expects(:new).never
Puppet::Util::Log.newdestination(:syslog)
end
describe "when setting the source as a RAL object" do
let(:path) { File.expand_path('/foo/bar') }
it "should tag itself with any tags the source has" do
source = Puppet::Type.type(:file).new :path => path
log = Puppet::Util::Log.new(:level => "notice", :message => :foo, :source => source)
source.tags.each do |tag|
log.tags.should be_include(tag)
end
end
it "should set the source to 'path', when available" do
source = Puppet::Type.type(:file).new :path => path
source.tags = ["tag", "tag2"]
log = Puppet::Util::Log.new(:level => "notice", :message => :foo)
log.expects(:tag).with("file")
log.expects(:tag).with("tag")
log.expects(:tag).with("tag2")
log.source = source
log.source.should == "/File[#{path}]"
end
it "should copy over any file and line information" do
source = Puppet::Type.type(:file).new :path => path
source.file = "/my/file"
source.line = 50
log = Puppet::Util::Log.new(:level => "notice", :message => :foo, :source => source)
log.line.should == 50
log.file.should == "/my/file"
end
end
describe "when setting the source as a non-RAL object" do
it "should not try to copy over file, version, line, or tag information" do
source = mock
source.expects(:file).never
log = Puppet::Util::Log.new(:level => "notice", :message => :foo, :source => source)
end
end
end
describe "to_yaml" do
it "should not include the @version attribute" do
log = Puppet::Util::Log.new(:level => "notice", :message => :foo, :version => 100)
log.to_yaml_properties.should_not include('@version')
end
it "should include attributes @level, @message, @source, @tags, and @time" do
log = Puppet::Util::Log.new(:level => "notice", :message => :foo, :version => 100)
log.to_yaml_properties.should =~ [:@level, :@message, :@source, :@tags, :@time]
end
it "should include attributes @file and @line if specified" do
log = Puppet::Util::Log.new(:level => "notice", :message => :foo, :file => "foo", :line => 35)
log.to_yaml_properties.should include(:@file)
log.to_yaml_properties.should include(:@line)
end
end
it "should round trip through pson" do
log = Puppet::Util::Log.new(:level => 'notice', :message => 'hooray', :file => 'thefile', :line => 1729, :source => 'specs', :tags => ['a', 'b', 'c'])
- tripped = Puppet::Util::Log.from_pson(PSON.parse(log.to_pson))
+ tripped = Puppet::Util::Log.from_data_hash(PSON.parse(log.to_pson))
tripped.file.should == log.file
tripped.line.should == log.line
tripped.level.should == log.level
tripped.message.should == log.message
tripped.source.should == log.source
tripped.tags.should == log.tags
tripped.time.should == log.time
end
end
diff --git a/spec/unit/util/metric_spec.rb b/spec/unit/util/metric_spec.rb
index e8ffe00dd..2f0d99b6a 100755
--- a/spec/unit/util/metric_spec.rb
+++ b/spec/unit/util/metric_spec.rb
@@ -1,98 +1,98 @@
#! /usr/bin/env ruby
require 'spec_helper'
require 'puppet/util/metric'
describe Puppet::Util::Metric do
before do
@metric = Puppet::Util::Metric.new("foo")
end
it "should be aliased to Puppet::Metric" do
Puppet::Util::Metric.should equal(Puppet::Metric)
end
[:type, :name, :value, :label, :basedir].each do |name|
it "should have a #{name} attribute" do
@metric.should respond_to(name)
@metric.should respond_to(name.to_s + "=")
end
end
it "should default to the :rrdir as the basedir "do
rrddir = File.expand_path("myrrd")
Puppet[:rrddir] = rrddir
@metric.basedir.should == rrddir
end
it "should use any provided basedir" do
@metric.basedir = "foo"
@metric.basedir.should == "foo"
end
it "should require a name at initialization" do
lambda { Puppet::Util::Metric.new }.should raise_error(ArgumentError)
end
it "should always convert its name to a string" do
Puppet::Util::Metric.new(:foo).name.should == "foo"
end
it "should support a label" do
Puppet::Util::Metric.new("foo", "mylabel").label.should == "mylabel"
end
it "should autogenerate a label if none is provided" do
Puppet::Util::Metric.new("foo_bar").label.should == "Foo bar"
end
it "should have a method for adding values" do
@metric.should respond_to(:newvalue)
end
it "should have a method for returning values" do
@metric.should respond_to(:values)
end
it "should require a name and value for its values" do
lambda { @metric.newvalue }.should raise_error(ArgumentError)
end
it "should support a label for values" do
@metric.newvalue("foo", 10, "label")
@metric.values[0][1].should == "label"
end
it "should autogenerate value labels if none is provided" do
@metric.newvalue("foo_bar", 10)
@metric.values[0][1].should == "Foo bar"
end
it "should return its values sorted by label" do
@metric.newvalue("foo", 10, "b")
@metric.newvalue("bar", 10, "a")
@metric.values.should == [["bar", "a", 10], ["foo", "b", 10]]
end
it "should use an array indexer method to retrieve individual values" do
@metric.newvalue("foo", 10)
@metric["foo"].should == 10
end
it "should return nil if the named value cannot be found" do
@metric["foo"].should == 0
end
it "should round trip through pson" do
metric = Puppet::Util::Metric.new("foo", "mylabel")
metric.newvalue("v1", 10.1, "something")
metric.newvalue("v2", 20, "something else")
- tripped = Puppet::Util::Metric.from_pson(PSON.parse(metric.to_pson))
+ tripped = Puppet::Util::Metric.from_data_hash(PSON.parse(metric.to_pson))
tripped.name.should == metric.name
tripped.label.should == metric.label
tripped.values.should == metric.values
end
end
diff --git a/spec/unit/util/pidlock_spec.rb b/spec/unit/util/pidlock_spec.rb
index e9c28f2b0..2ebe7dec8 100644
--- a/spec/unit/util/pidlock_spec.rb
+++ b/spec/unit/util/pidlock_spec.rb
@@ -1,182 +1,182 @@
#! /usr/bin/env ruby
require 'spec_helper'
require 'puppet/util/pidlock'
describe Puppet::Util::Pidlock do
require 'puppet_spec/files'
include PuppetSpec::Files
before(:each) do
@lockfile = tmpfile("lock")
@lock = Puppet::Util::Pidlock.new(@lockfile)
end
describe "#lock" do
it "should not be locked at start" do
@lock.should_not be_locked
end
it "should not be mine at start" do
@lock.should_not be_mine
end
it "should become locked" do
@lock.lock
@lock.should be_locked
end
it "should become mine" do
@lock.lock
@lock.should be_mine
end
it "should be possible to lock multiple times" do
@lock.lock
lambda { @lock.lock }.should_not raise_error
end
it "should return true when locking" do
@lock.lock.should be_true
end
it "should return true if locked by me" do
@lock.lock
@lock.lock.should be_true
end
it "should create a lock file" do
@lock.lock
- Puppet::FileSystem::File.exist?(@lockfile).should be_true
+ Puppet::FileSystem.exist?(@lockfile).should be_true
end
it "should expose the lock file_path" do
@lock.file_path.should == @lockfile
end
end
describe "#unlock" do
it "should not be locked anymore" do
@lock.lock
@lock.unlock
@lock.should_not be_locked
end
it "should return false if not locked" do
@lock.unlock.should be_false
end
it "should return true if properly unlocked" do
@lock.lock
@lock.unlock.should be_true
end
it "should get rid of the lock file" do
@lock.lock
@lock.unlock
- Puppet::FileSystem::File.exist?(@lockfile).should be_false
+ Puppet::FileSystem.exist?(@lockfile).should be_false
end
end
describe "#locked?" do
it "should return true if locked" do
@lock.lock
@lock.should be_locked
end
end
describe "with a stale lock" do
before(:each) do
# fake our pid to be 1234
Process.stubs(:pid).returns(1234)
# lock the file
@lock.lock
# fake our pid to be a different pid, to simulate someone else
# holding the lock
Process.stubs(:pid).returns(6789)
Process.stubs(:kill).with(0, 6789)
Process.stubs(:kill).with(0, 1234).raises(Errno::ESRCH)
end
it "should not be locked" do
@lock.should_not be_locked
end
describe "#lock" do
it "should clear stale locks" do
@lock.locked?
- Puppet::FileSystem::File.exist?(@lockfile).should be_false
+ Puppet::FileSystem.exist?(@lockfile).should be_false
end
it "should replace with new locks" do
@lock.lock
- Puppet::FileSystem::File.exist?(@lockfile).should be_true
+ Puppet::FileSystem.exist?(@lockfile).should be_true
@lock.lock_pid.should == 6789
@lock.should be_mine
@lock.should be_locked
end
end
describe "#unlock" do
it "should not be allowed" do
@lock.unlock.should be_false
end
it "should not remove the lock file" do
@lock.unlock
- Puppet::FileSystem::File.exist?(@lockfile).should be_true
+ Puppet::FileSystem.exist?(@lockfile).should be_true
end
end
end
describe "with another process lock" do
before(:each) do
# fake our pid to be 1234
Process.stubs(:pid).returns(1234)
# lock the file
@lock.lock
# fake our pid to be a different pid, to simulate someone else
# holding the lock
Process.stubs(:pid).returns(6789)
Process.stubs(:kill).with(0, 6789)
Process.stubs(:kill).with(0, 1234)
end
it "should be locked" do
@lock.should be_locked
end
it "should not be mine" do
@lock.should_not be_mine
end
describe "#lock" do
it "should not be possible" do
@lock.lock.should be_false
end
it "should not overwrite the lock" do
@lock.lock
@lock.should_not be_mine
end
end
describe "#unlock" do
it "should not be possible" do
@lock.unlock.should be_false
end
it "should not remove the lock file" do
@lock.unlock
- Puppet::FileSystem::File.exist?(@lockfile).should be_true
+ Puppet::FileSystem.exist?(@lockfile).should be_true
end
it "should still not be our lock" do
@lock.unlock
@lock.should_not be_mine
end
end
end
end
diff --git a/spec/unit/util/pson_spec.rb b/spec/unit/util/pson_spec.rb
index e0d79cda6..c2307498f 100755
--- a/spec/unit/util/pson_spec.rb
+++ b/spec/unit/util/pson_spec.rb
@@ -1,71 +1,71 @@
#! /usr/bin/env ruby
# Encoding: UTF-8
require 'spec_helper'
require 'puppet/util/pson'
class PsonUtil
include Puppet::Util::Pson
end
describe Puppet::Util::Pson do
it "should fail if no data is provided" do
expect {
PsonUtil.new.pson_create("type" => "foo")
}.to raise_error(ArgumentError, /No data provided in pson data/)
end
- it "should call 'from_pson' with the provided data" do
+ it "should call 'from_data_hash' with the provided data" do
pson = PsonUtil.new
- pson.expects(:from_pson).with("mydata")
+ pson.expects(:from_data_hash).with("mydata")
pson.pson_create("type" => "foo", "data" => "mydata")
end
{
'foo' => '"foo"',
1 => '1',
"\x80" => "\"\x80\"",
[] => '[]'
}.each do |str, expect|
it "should be able to encode #{str.inspect}" do
got = str.to_pson
if got.respond_to? :force_encoding
got.force_encoding('binary').should == expect.force_encoding('binary')
else
got.should == expect
end
end
end
it "should be able to handle arbitrary binary data" do
bin_string = (1..20000).collect { |i| ((17*i+13*i*i) % 255).chr }.join
parsed = PSON.parse(%Q{{ "type": "foo", "data": #{bin_string.to_pson} }})["data"]
if parsed.respond_to? :force_encoding
parsed.force_encoding('binary')
bin_string.force_encoding('binary')
end
parsed.should == bin_string
end
it "should be able to handle UTF8 that isn't a real unicode character" do
s = ["\355\274\267"]
PSON.parse( [s].to_pson ).should == [s]
end
it "should be able to handle UTF8 for \\xFF" do
s = ["\xc3\xbf"]
PSON.parse( [s].to_pson ).should == [s]
end
it "should be able to handle invalid UTF8 bytes" do
s = ["\xc3\xc3"]
PSON.parse( [s].to_pson ).should == [s]
end
it "should be able to parse JSON containing UTF-8 characters in strings" do
s = '{ "foö": "bár" }'
lambda { PSON.parse s }.should_not raise_error
end
end
diff --git a/spec/unit/util/rdoc/parser_spec.rb b/spec/unit/util/rdoc/parser_spec.rb
index 1699020a7..fa536703a 100755
--- a/spec/unit/util/rdoc/parser_spec.rb
+++ b/spec/unit/util/rdoc/parser_spec.rb
@@ -1,596 +1,599 @@
#! /usr/bin/env ruby
require 'spec_helper'
describe "RDoc::Parser", :if => Puppet.features.rdoc1? do
before :all do
require 'puppet/resource/type_collection'
require 'puppet/util/rdoc/parser'
require 'puppet/util/rdoc/code_objects'
require 'rdoc/options'
require 'rdoc/rdoc'
end
include PuppetSpec::Files
before :each do
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
+ Puppet::FileSystem.stubs(:stat).with('init.pp').returns stub() # 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
describe "when scanning files" do
it "should parse puppet files with the puppet parser" do
@parser.stubs(:scan_top_level)
parser = stub 'parser'
Puppet::Parser::Parser.stubs(:new).returns(parser)
parser.expects(:parse).returns(Puppet::Parser::AST::Hostclass.new('')).at_least_once
parser.expects(:file=).with("module/manifests/init.pp")
- parser.expects(:file=).with(File.expand_path("/dev/null/manifests/site.pp"))
+ parser.expects(:file=).with do |args|
+ args =~ /.*\/etc\/manifests\/site.pp/
+ end
@parser.scan
end
it "should scan the ast for Puppet files" do
parser = stub_everything 'parser'
Puppet::Parser::Parser.stubs(:new).returns(parser)
parser.expects(:parse).returns(Puppet::Parser::AST::Hostclass.new('')).at_least_once
@parser.expects(:scan_top_level)
@parser.scan
end
it "should return a PuppetTopLevel to RDoc" do
parser = stub_everything 'parser'
Puppet::Parser::Parser.stubs(:new).returns(parser)
parser.expects(:parse).returns(Puppet::Parser::AST::Hostclass.new('')).at_least_once
@parser.expects(:scan_top_level)
@parser.scan.should be_a(RDoc::PuppetTopLevel)
end
it "should scan the top level even if the file has already parsed" do
known_type = stub 'known_types'
env = stub 'env'
Puppet::Node::Environment.stubs(:new).returns(env)
env.stubs(:known_resource_types).returns(known_type)
known_type.expects(:watching_file?).with("module/manifests/init.pp").returns(true)
@parser.expects(:scan_top_level)
@parser.scan
end
end
describe "when scanning top level entities" do
+ let(:environment) { Puppet::Node::Environment.create(:env, [], '') }
+
before :each do
@resource_type_collection = resource_type_collection = stub_everything('resource_type_collection')
- @parser.instance_eval { @known_resource_types = resource_type_collection }
+ environment.stubs(:known_resource_types).returns(@resource_type_collection)
@parser.stubs(:split_module).returns("module")
@topcontainer = stub_everything 'topcontainer'
@container = stub_everything 'container'
@module = stub_everything 'module'
@container.stubs(:add_module).returns(@module)
@parser.stubs(:get_class_or_module).returns([@container, "module"])
end
it "should read any present README as module documentation" do
FileTest.stubs(:readable?).with("module/README").returns(true)
FileTest.stubs(:readable?).with("module/README.rdoc").returns(false)
File.stubs(:open).returns("readme")
@parser.stubs(:parse_elements)
@module.expects(:add_comment).with("readme", "module/manifests/init.pp")
- @parser.scan_top_level(@topcontainer)
+ @parser.scan_top_level(@topcontainer, environment)
end
it "should read any present README.rdoc as module documentation" do
FileTest.stubs(:readable?).with("module/README.rdoc").returns(true)
FileTest.stubs(:readable?).with("module/README").returns(false)
File.stubs(:open).returns("readme")
@parser.stubs(:parse_elements)
@module.expects(:add_comment).with("readme", "module/manifests/init.pp")
- @parser.scan_top_level(@topcontainer)
+ @parser.scan_top_level(@topcontainer, environment)
end
it "should prefer README.rdoc over README as module documentation" do
FileTest.stubs(:readable?).with("module/README.rdoc").returns(true)
FileTest.stubs(:readable?).with("module/README").returns(true)
File.stubs(:open).with("module/README", "r").returns("readme")
File.stubs(:open).with("module/README.rdoc", "r").returns("readme.rdoc")
@parser.stubs(:parse_elements)
@module.expects(:add_comment).with("readme.rdoc", "module/manifests/init.pp")
- @parser.scan_top_level(@topcontainer)
+ @parser.scan_top_level(@topcontainer, environment)
end
it "should tell the container its module name" do
@parser.stubs(:parse_elements)
@topcontainer.expects(:module_name=).with("module")
- @parser.scan_top_level(@topcontainer)
+ @parser.scan_top_level(@topcontainer, environment)
end
it "should not document our toplevel if it isn't a valid module" do
@parser.stubs(:split_module).returns(nil)
@topcontainer.expects(:document_self=).with(false)
@parser.expects(:parse_elements).never
- @parser.scan_top_level(@topcontainer)
+ @parser.scan_top_level(@topcontainer, environment)
end
it "should set the module as global if we parse the global manifests (ie __site__ module)" do
@parser.stubs(:split_module).returns(RDoc::Parser::SITE)
@parser.stubs(:parse_elements)
@topcontainer.expects(:global=).with(true)
- @parser.scan_top_level(@topcontainer)
+ @parser.scan_top_level(@topcontainer, environment)
end
it "should attach this module container to the toplevel container" do
@parser.stubs(:parse_elements)
@container.expects(:add_module).with(RDoc::PuppetModule, "module").returns(@module)
- @parser.scan_top_level(@topcontainer)
+ @parser.scan_top_level(@topcontainer, environment)
end
it "should defer ast parsing to parse_elements for this module" do
- @parser.expects(:parse_elements).with(@module)
+ @parser.expects(:parse_elements).with(@module, @resource_type_collection)
- @parser.scan_top_level(@topcontainer)
+ @parser.scan_top_level(@topcontainer, environment)
end
it "should defer plugins parsing to parse_plugins for this module" do
@parser.input_file_name = "module/lib/puppet/parser/function.rb"
@parser.expects(:parse_plugins).with(@module)
- @parser.scan_top_level(@topcontainer)
+ @parser.scan_top_level(@topcontainer, environment)
end
end
describe "when finding modules from filepath" do
- before :each do
- Puppet::Node::Environment.any_instance.stubs(:modulepath).returns("/path/to/modules")
- end
+ let(:environment) {
+ Puppet::FileSystem.expects(:directory?).with("/path/to/modules").at_least_once.returns(true)
+ Puppet::Node::Environment.create(:env, ["/path/to/modules"], '')
+ }
it "should return the module name for modulized puppet manifests" do
- File.stubs(:expand_path).returns("/path/to/module/manifests/init.pp")
- File.stubs(:identical?).with("/path/to", "/path/to/modules").returns(true)
- @parser.split_module("/path/to/modules/mymodule/manifests/init.pp").should == "module"
+ File.stubs(:identical?).with("/path/to/modules", "/path/to/modules").returns(true)
+ @parser.split_module("/path/to/modules/mymodule/manifests/init.pp", environment).should == "mymodule"
end
it "should return <site> for manifests not under module path" do
- File.stubs(:expand_path).returns("/path/to/manifests/init.pp")
File.stubs(:identical?).returns(false)
- @parser.split_module("/path/to/manifests/init.pp").should == RDoc::Parser::SITE
+ @parser.split_module("/path/to/manifests/init.pp", environment).should == RDoc::Parser::SITE
end
it "should handle windows paths with drive letters", :if => Puppet.features.microsoft_windows? && Puppet.features.rdoc1? do
- @parser.split_module("C:/temp/init.pp").should == RDoc::Parser::SITE
+ @parser.split_module("C:/temp/init.pp", environment).should == RDoc::Parser::SITE
end
end
describe "when parsing AST elements" do
before :each do
@klass = stub_everything 'klass', :file => "module/manifests/init.pp", :name => "myclass", :type => :hostclass
@definition = stub_everything 'definition', :file => "module/manifests/init.pp", :type => :definition, :name => "mydef"
@node = stub_everything 'node', :file => "module/manifests/init.pp", :type => :node, :name => "mynode"
@resource_type_collection = resource_type_collection = Puppet::Resource::TypeCollection.new("env")
@parser.instance_eval { @known_resource_types = resource_type_collection }
@container = stub_everything 'container'
end
it "should document classes in the parsed file" do
@resource_type_collection.add_hostclass(@klass)
@parser.expects(:document_class).with("myclass", @klass, @container)
- @parser.parse_elements(@container)
+ @parser.parse_elements(@container, @resource_type_collection)
end
it "should not document class parsed in an other file" do
@klass.stubs(:file).returns("/not/same/path/file.pp")
@resource_type_collection.add_hostclass(@klass)
@parser.expects(:document_class).with("myclass", @klass, @container).never
- @parser.parse_elements(@container)
+ @parser.parse_elements(@container, @resource_type_collection)
end
it "should document vardefs for the main class" do
@klass.stubs(:name).returns :main
@resource_type_collection.add_hostclass(@klass)
code = stub 'code', :is_a? => false
@klass.stubs(:name).returns("")
@klass.stubs(:code).returns(code)
@parser.expects(:scan_for_vardef).with(@container, code)
- @parser.parse_elements(@container)
+ @parser.parse_elements(@container, @resource_type_collection)
end
it "should document definitions in the parsed file" do
@resource_type_collection.add_definition(@definition)
@parser.expects(:document_define).with("mydef", @definition, @container)
- @parser.parse_elements(@container)
+ @parser.parse_elements(@container, @resource_type_collection)
end
it "should not document definitions parsed in an other file" do
@definition.stubs(:file).returns("/not/same/path/file.pp")
@resource_type_collection.add_definition(@definition)
@parser.expects(:document_define).with("mydef", @definition, @container).never
- @parser.parse_elements(@container)
+ @parser.parse_elements(@container, @resource_type_collection)
end
it "should document nodes in the parsed file" do
@resource_type_collection.add_node(@node)
@parser.expects(:document_node).with("mynode", @node, @container)
- @parser.parse_elements(@container)
+ @parser.parse_elements(@container, @resource_type_collection)
end
it "should not document node parsed in an other file" do
@node.stubs(:file).returns("/not/same/path/file.pp")
@resource_type_collection.add_node(@node)
@parser.expects(:document_node).with("mynode", @node, @container).never
- @parser.parse_elements(@container)
+ @parser.parse_elements(@container, @resource_type_collection)
end
end
describe "when documenting definition" do
before(:each) do
@define = stub_everything 'define', :arguments => [], :doc => "mydoc", :file => "file", :line => 42
@class = stub_everything 'class'
@parser.stubs(:get_class_or_module).returns([@class, "mydef"])
end
it "should register a RDoc method to the current container" do
@class.expects(:add_method).with { |m| m.name == "mydef"}
@parser.document_define("mydef", @define, @class)
end
it "should attach the documentation to this method" do
@class.expects(:add_method).with { |m| m.comment = "mydoc" }
@parser.document_define("mydef", @define, @class)
end
it "should produce a better error message on unhandled exception" do
@class.expects(:add_method).raises(ArgumentError)
lambda { @parser.document_define("mydef", @define, @class) }.should raise_error(Puppet::ParseError, /in file at line 42/)
end
it "should convert all definition parameter to string" do
arg = stub 'arg'
val = stub 'val'
@define.stubs(:arguments).returns({arg => val})
arg.expects(:to_s).returns("arg")
val.expects(:to_s).returns("val")
@parser.document_define("mydef", @define, @class)
end
end
describe "when documenting nodes" do
before :each do
@code = stub_everything 'code'
@node = stub_everything 'node', :doc => "mydoc", :parent => "parent", :code => @code, :file => "file", :line => 42
@rdoc_node = stub_everything 'rdocnode'
@class = stub_everything 'class'
@class.stubs(:add_node).returns(@rdoc_node)
end
it "should add a node to the current container" do
@class.expects(:add_node).with("mynode", "parent").returns(@rdoc_node)
@parser.document_node("mynode", @node, @class)
end
it "should associate the node documentation to the rdoc node" do
@rdoc_node.expects(:add_comment).with("mydoc", "file")
@parser.document_node("mynode", @node, @class)
end
it "should scan for include and require" do
@parser.expects(:scan_for_include_or_require).with(@rdoc_node, @code)
@parser.document_node("mynode", @node, @class)
end
it "should scan for variable definition" do
@parser.expects(:scan_for_vardef).with(@rdoc_node, @code)
@parser.document_node("mynode", @node, @class)
end
it "should scan for resources if needed" do
Puppet[:document_all] = true
@parser.expects(:scan_for_resource).with(@rdoc_node, @code)
@parser.document_node("mynode", @node, @class)
end
it "should produce a better error message on unhandled exception" do
@class.stubs(:add_node).raises(ArgumentError)
lambda { @parser.document_node("mynode", @node, @class) }.should raise_error(Puppet::ParseError, /in file at line 42/)
end
end
describe "when documenting classes" do
before :each do
@code = stub_everything 'code'
@class = stub_everything 'class', :doc => "mydoc", :parent => "parent", :code => @code, :file => "file", :line => 42
@rdoc_class = stub_everything 'rdoc-class'
@module = stub_everything 'class'
@module.stubs(:add_class).returns(@rdoc_class)
@parser.stubs(:get_class_or_module).returns([@module, "myclass"])
end
it "should add a class to the current container" do
@module.expects(:add_class).with(RDoc::PuppetClass, "myclass", "parent").returns(@rdoc_class)
@parser.document_class("mynode", @class, @module)
end
it "should set the superclass" do
@rdoc_class.expects(:superclass=).with("parent")
@parser.document_class("mynode", @class, @module)
end
it "should associate the node documentation to the rdoc class" do
@rdoc_class.expects(:add_comment).with("mydoc", "file")
@parser.document_class("mynode", @class, @module)
end
it "should scan for include and require" do
@parser.expects(:scan_for_include_or_require).with(@rdoc_class, @code)
@parser.document_class("mynode", @class, @module)
end
it "should scan for resources if needed" do
Puppet[:document_all] = true
@parser.expects(:scan_for_resource).with(@rdoc_class, @code)
@parser.document_class("mynode", @class, @module)
end
it "should produce a better error message on unhandled exception" do
@module.stubs(:add_class).raises(ArgumentError)
lambda { @parser.document_class("mynode", @class, @module) }.should raise_error(Puppet::ParseError, /in file at line 42/)
end
end
describe "when scanning for includes and requires" do
def create_stmt(name)
stmt_value = stub "#{name}_value", :to_s => "myclass"
Puppet::Parser::AST::Function.new(
:name => name,
:arguments => [stmt_value],
:doc => 'mydoc'
)
end
before(:each) do
@class = stub_everything 'class'
@code = stub_everything 'code'
@code.stubs(:is_a?).with(Puppet::Parser::AST::BlockExpression).returns(true)
end
it "should also scan mono-instruction code" do
@class.expects(:add_include).with { |i| i.is_a?(RDoc::Include) and i.name == "myclass" and i.comment == "mydoc" }
@parser.scan_for_include_or_require(@class, create_stmt("include"))
end
it "should register recursively includes to the current container" do
@code.stubs(:children).returns([ create_stmt("include") ])
@class.expects(:add_include)#.with { |i| i.is_a?(RDoc::Include) and i.name == "myclass" and i.comment == "mydoc" }
@parser.scan_for_include_or_require(@class, [@code])
end
it "should register requires to the current container" do
@code.stubs(:children).returns([ create_stmt("require") ])
@class.expects(:add_require).with { |i| i.is_a?(RDoc::Include) and i.name == "myclass" and i.comment == "mydoc" }
@parser.scan_for_include_or_require(@class, [@code])
end
end
describe "when scanning for realized virtual resources" do
def create_stmt
stmt_value = stub "resource_ref", :to_s => "File[\"/tmp/a\"]"
Puppet::Parser::AST::Function.new(
:name => 'realize',
:arguments => [stmt_value],
:doc => 'mydoc'
)
end
before(:each) do
@class = stub_everything 'class'
@code = stub_everything 'code'
@code.stubs(:is_a?).with(Puppet::Parser::AST::BlockExpression).returns(true)
end
it "should also scan mono-instruction code" do
@class.expects(:add_realize).with { |i| i.is_a?(RDoc::Include) and i.name == "File[\"/tmp/a\"]" and i.comment == "mydoc" }
@parser.scan_for_realize(@class,create_stmt)
end
it "should register recursively includes to the current container" do
@code.stubs(:children).returns([ create_stmt ])
@class.expects(:add_realize).with { |i| i.is_a?(RDoc::Include) and i.name == "File[\"/tmp/a\"]" and i.comment == "mydoc" }
@parser.scan_for_realize(@class, [@code])
end
end
describe "when scanning for variable definition" do
before :each do
@class = stub_everything 'class'
@stmt = stub_everything 'stmt', :name => "myvar", :value => "myvalue", :doc => "mydoc"
@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::BlockExpression).returns(true)
end
it "should recursively register variables to the current container" do
@code.stubs(:children).returns([ @stmt ])
@class.expects(:add_constant).with { |i| i.is_a?(RDoc::Constant) and i.name == "myvar" and i.comment == "mydoc" }
@parser.scan_for_vardef(@class, [ @code ])
end
it "should also scan mono-instruction code" do
@class.expects(:add_constant).with { |i| i.is_a?(RDoc::Constant) and i.name == "myvar" and i.comment == "mydoc" }
@parser.scan_for_vardef(@class, @stmt)
end
end
describe "when scanning for resources" do
before :each do
@class = stub_everything 'class'
@stmt = Puppet::Parser::AST::Resource.new(
:type => "File",
:instances => Puppet::Parser::AST::BlockExpression.new(:children => [
Puppet::Parser::AST::ResourceInstance.new(
:title => Puppet::Parser::AST::Name.new(:value => "myfile"),
:parameters => Puppet::Parser::AST::BlockExpression.new(:children => [])
)
]),
:doc => 'mydoc'
)
@code = stub_everything 'code'
@code.stubs(:is_a?).with(Puppet::Parser::AST::BlockExpression).returns(true)
end
it "should register a PuppetResource to the current container" do
@code.stubs(:children).returns([ @stmt ])
@class.expects(:add_resource).with { |i| i.is_a?(RDoc::PuppetResource) and i.title == "myfile" and i.comment == "mydoc" }
@parser.scan_for_resource(@class, [ @code ])
end
it "should also scan mono-instruction code" do
@class.expects(:add_resource).with { |i| i.is_a?(RDoc::PuppetResource) and i.title == "myfile" and i.comment == "mydoc" }
@parser.scan_for_resource(@class, @stmt)
end
end
describe "when parsing plugins" do
before :each do
@container = stub 'container'
end
it "should delegate parsing custom facts to parse_facts" do
@parser = RDoc::Parser.new(@top_level, "module/manifests/lib/puppet/facter/test.rb", nil, Options.instance, RDoc::Stats.new)
@parser.expects(:parse_fact).with(@container)
@parser.parse_plugins(@container)
end
it "should delegate parsing plugins to parse_plugins" do
@parser = RDoc::Parser.new(@top_level, "module/manifests/lib/puppet/functions/test.rb", nil, Options.instance, RDoc::Stats.new)
@parser.expects(:parse_puppet_plugin).with(@container)
@parser.parse_plugins(@container)
end
end
describe "when parsing plugins" do
before :each do
@container = stub_everything 'container'
end
it "should add custom functions to the container" do
File.stubs(:open).yields("# documentation
module Puppet::Parser::Functions
newfunction(:myfunc, :type => :rvalue) do |args|
File.dirname(args[0])
end
end".split("\n"))
@container.expects(:add_plugin).with do |plugin|
plugin.comment == "documentation\n" #and
plugin.name == "myfunc"
end
@parser.parse_puppet_plugin(@container)
end
it "should add custom types to the container" do
File.stubs(:open).yields("# documentation
Puppet::Type.newtype(:mytype) do
end".split("\n"))
@container.expects(:add_plugin).with do |plugin|
plugin.comment == "documentation\n" #and
plugin.name == "mytype"
end
@parser.parse_puppet_plugin(@container)
end
end
describe "when parsing facts" do
before :each do
@container = stub_everything 'container'
File.stubs(:open).yields(["# documentation", "Facter.add('myfact') do", "confine :kernel => :linux", "end"])
end
it "should add facts to the container" do
@container.expects(:add_fact).with do |fact|
fact.comment == "documentation\n" and
fact.name == "myfact"
end
@parser.parse_fact(@container)
end
it "should add confine to the parsed facts" do
ourfact = nil
@container.expects(:add_fact).with do |fact|
ourfact = fact
true
end
@parser.parse_fact(@container)
ourfact.confine.should == { :type => "kernel", :value => ":linux" }
end
end
end
diff --git a/spec/unit/util/resource_template_spec.rb b/spec/unit/util/resource_template_spec.rb
index 0e6037119..182a78ab0 100755
--- a/spec/unit/util/resource_template_spec.rb
+++ b/spec/unit/util/resource_template_spec.rb
@@ -1,57 +1,57 @@
#! /usr/bin/env ruby
require 'spec_helper'
require 'puppet/util/resource_template'
describe Puppet::Util::ResourceTemplate do
describe "when initializing" do
it "should fail if the template does not exist" do
- Puppet::FileSystem::File.expects(:exist?).with("/my/template").returns false
+ Puppet::FileSystem.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
- Puppet::FileSystem::File.expects(:exist?).with("/my/template").returns true
+ Puppet::FileSystem.expects(:exist?).with("/my/template").returns true
Puppet::Util::ResourceTemplate.new("/my/template", mock('resource'))
end
end
describe "when evaluating" do
before do
- Puppet::FileSystem::File.stubs(:exist?).returns true
+ Puppet::FileSystem.stubs(:exist?).returns true
File.stubs(:read).returns "eh"
@template = stub 'template', :result => nil
ERB.stubs(:new).returns @template
@resource = mock 'resource'
@wrapper = Puppet::Util::ResourceTemplate.new("/my/template", @resource)
end
it "should set all of the resource's parameters as instance variables" do
@resource.expects(:to_hash).returns(:one => "uno", :two => "dos")
@template.expects(:result).with do |bind|
eval("@one", bind) == "uno" and eval("@two", bind) == "dos"
end
@wrapper.evaluate
end
it "should create a template instance with the contents of the file" do
File.expects(:read).with("/my/template").returns "yay"
ERB.expects(:new).with("yay", 0, "-").returns(@template)
@wrapper.stubs :set_resource_variables
@wrapper.evaluate
end
it "should return the result of the template" do
@wrapper.stubs :set_resource_variables
@wrapper.expects(:binding).returns "mybinding"
@template.expects(:result).with("mybinding").returns "myresult"
@wrapper.evaluate.should == "myresult"
end
end
end
diff --git a/spec/unit/util/selinux_spec.rb b/spec/unit/util/selinux_spec.rb
index 1357b981a..b15a1971c 100755
--- a/spec/unit/util/selinux_spec.rb
+++ b/spec/unit/util/selinux_spec.rb
@@ -1,289 +1,289 @@
#! /usr/bin/env ruby
require 'spec_helper'
require 'pathname'
require 'puppet/util/selinux'
include Puppet::Util::SELinux
unless defined?(Selinux)
module Selinux
def self.is_selinux_enabled
false
end
end
end
describe Puppet::Util::SELinux do
describe "selinux_support?" do
before do
end
it "should return :true if this system has SELinux enabled" do
Selinux.expects(:is_selinux_enabled).returns 1
selinux_support?.should be_true
end
it "should return :false if this system lacks SELinux" do
Selinux.expects(:is_selinux_enabled).returns 0
selinux_support?.should be_false
end
it "should return nil if /proc/mounts does not exist" do
File.stubs(:open).with("/proc/mounts").raises("No such file or directory - /proc/mounts")
read_mounts.should == nil
end
end
describe "read_mounts" do
before :each do
fh = stub 'fh', :close => nil
File.stubs(:open).with("/proc/mounts").returns fh
fh.expects(:read_nonblock).times(2).returns("rootfs / rootfs rw 0 0\n/dev/root / ext3 rw,relatime,errors=continue,user_xattr,acl,data=ordered 0 0\n/dev /dev tmpfs rw,relatime,mode=755 0 0\n/proc /proc proc rw,relatime 0 0\n/sys /sys sysfs rw,relatime 0 0\n192.168.1.1:/var/export /mnt/nfs nfs rw,relatime,vers=3,rsize=32768,wsize=32768,namlen=255,hard,nointr,proto=tcp,timeo=600,retrans=2,sec=sys,mountaddr=192.168.1.1,mountvers=3,mountproto=udp,addr=192.168.1.1 0 0\n").then.raises EOFError
end
it "should parse the contents of /proc/mounts" do
read_mounts.should == {
'/' => 'ext3',
'/sys' => 'sysfs',
'/mnt/nfs' => 'nfs',
'/proc' => 'proc',
'/dev' => 'tmpfs' }
end
end
describe "filesystem detection" do
before :each do
self.stubs(:read_mounts).returns({
'/' => 'ext3',
'/sys' => 'sysfs',
'/mnt/nfs' => 'nfs',
'/proc' => 'proc',
'/dev' => 'tmpfs' })
end
it "should match a path on / to ext3" do
find_fs('/etc/puppet/testfile').should == "ext3"
end
it "should match a path on /mnt/nfs to nfs" do
find_fs('/mnt/nfs/testfile/foobar').should == "nfs"
end
- it "should reture true for a capable filesystem" do
+ it "should return true for a capable filesystem" do
selinux_label_support?('/etc/puppet/testfile').should be_true
end
it "should return false for a noncapable filesystem" do
selinux_label_support?('/mnt/nfs/testfile').should be_false
end
it "(#8714) don't follow symlinks when determining file systems", :unless => Puppet.features.microsoft_windows? do
scratch = Pathname(PuppetSpec::Files.tmpdir('selinux'))
self.stubs(:read_mounts).returns({
'/' => 'ext3',
scratch + 'nfs' => 'nfs',
})
(scratch + 'foo').make_symlink('nfs/bar')
selinux_label_support?(scratch + 'foo').should be_true
end
it "should handle files that don't exist" do
scratch = Pathname(PuppetSpec::Files.tmpdir('selinux'))
selinux_label_support?(scratch + 'nonesuch').should be_true
end
end
describe "get_selinux_current_context" do
it "should return nil if no SELinux support" do
self.expects(:selinux_support?).returns false
get_selinux_current_context("/foo").should be_nil
end
it "should return a context" do
self.expects(:selinux_support?).returns true
Selinux.expects(:lgetfilecon).with("/foo").returns [0, "user_u:role_r:type_t:s0"]
get_selinux_current_context("/foo").should == "user_u:role_r:type_t:s0"
end
it "should return nil if lgetfilecon fails" do
self.expects(:selinux_support?).returns true
Selinux.expects(:lgetfilecon).with("/foo").returns -1
get_selinux_current_context("/foo").should be_nil
end
end
describe "get_selinux_default_context" do
it "should return nil if no SELinux support" do
self.expects(:selinux_support?).returns false
get_selinux_default_context("/foo").should be_nil
end
it "should return a context if a default context exists" do
self.expects(:selinux_support?).returns true
fstat = stub 'File::Stat', :mode => 0
- stub_file = stub('/foo', :lstat => fstat)
- Puppet::FileSystem::File.expects(:new).with('/foo').returns stub_file
+ Puppet::FileSystem.expects(:lstat).with('/foo').returns(fstat)
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"
end
it "handles permission denied errors by issuing a warning" do
self.stubs(:selinux_support?).returns true
self.stubs(:selinux_label_support?).returns true
Selinux.stubs(:matchpathcon).with("/root/chuj", 0).returns(-1)
self.stubs(:file_lstat).with("/root/chuj").raises(Errno::EACCES, "/root/chuj")
get_selinux_default_context("/root/chuj").should be_nil
end
it "handles no such file or directory errors by issuing a warning" do
self.stubs(:selinux_support?).returns true
self.stubs(:selinux_label_support?).returns true
Selinux.stubs(:matchpathcon).with("/root/chuj", 0).returns(-1)
self.stubs(:file_lstat).with("/root/chuj").raises(Errno::ENOENT, "/root/chuj")
get_selinux_default_context("/root/chuj").should be_nil
end
it "should return nil if matchpathcon returns failure" do
self.expects(:selinux_support?).returns true
fstat = stub 'File::Stat', :mode => 0
- stub_file = stub('/foo', :lstat => fstat)
- Puppet::FileSystem::File.expects(:new).with('/foo').returns stub_file
+ Puppet::FileSystem.expects(:lstat).with('/foo').returns(fstat)
self.expects(:find_fs).with("/foo").returns "ext3"
Selinux.expects(:matchpathcon).with("/foo", 0).returns -1
+
get_selinux_default_context("/foo").should be_nil
end
it "should return nil if selinux_label_support returns false" do
self.expects(:selinux_support?).returns true
self.expects(:find_fs).with("/foo").returns "nfs"
get_selinux_default_context("/foo").should be_nil
end
end
describe "parse_selinux_context" do
it "should return nil if no context is passed" do
parse_selinux_context(:seluser, nil).should be_nil
end
it "should return nil if the context is 'unlabeled'" do
parse_selinux_context(:seluser, "unlabeled").should be_nil
end
it "should return the user type when called with :seluser" do
parse_selinux_context(:seluser, "user_u:role_r:type_t:s0").should == "user_u"
end
it "should return the role type when called with :selrole" do
parse_selinux_context(:selrole, "user_u:role_r:type_t:s0").should == "role_r"
end
it "should return the type type when called with :seltype" do
parse_selinux_context(:seltype, "user_u:role_r:type_t:s0").should == "type_t"
end
it "should return nil for :selrange when no range is returned" do
parse_selinux_context(:selrange, "user_u:role_r:type_t").should be_nil
end
it "should return the range type when called with :selrange" do
parse_selinux_context(:selrange, "user_u:role_r:type_t:s0").should == "s0"
end
describe "with a variety of SELinux range formats" do
['s0', 's0:c3', 's0:c3.c123', 's0:c3,c5,c8', 'TopSecret', 'TopSecret,Classified', 'Patient_Record'].each do |range|
it "should parse range '#{range}'" do
parse_selinux_context(:selrange, "user_u:role_r:type_t:#{range}").should == range
end
end
end
end
describe "set_selinux_context" do
before :each do
fh = stub 'fh', :close => nil
File.stubs(:open).with("/proc/mounts").returns fh
fh.stubs(:read_nonblock).returns(
"rootfs / rootfs rw 0 0\n/dev/root / ext3 rw,relatime,errors=continue,user_xattr,acl,data=ordered 0 0\n"+
"/dev /dev tmpfs rw,relatime,mode=755 0 0\n/proc /proc proc rw,relatime 0 0\n"+
"/sys /sys sysfs rw,relatime 0 0\n"
).then.raises EOFError
end
it "should return nil if there is no SELinux support" do
self.expects(:selinux_support?).returns false
set_selinux_context("/foo", "user_u:role_r:type_t:s0").should be_nil
end
it "should return nil if selinux_label_support returns false" do
self.expects(:selinux_support?).returns true
self.expects(:selinux_label_support?).with("/foo").returns false
set_selinux_context("/foo", "user_u:role_r:type_t:s0").should be_nil
end
it "should use lsetfilecon to set a context" do
self.expects(:selinux_support?).returns true
Selinux.expects(:lsetfilecon).with("/foo", "user_u:role_r:type_t:s0").returns 0
set_selinux_context("/foo", "user_u:role_r:type_t:s0").should be_true
end
it "should use lsetfilecon to set user_u user context" do
self.expects(:selinux_support?).returns true
Selinux.expects(:lgetfilecon).with("/foo").returns [0, "foo:role_r:type_t:s0"]
Selinux.expects(:lsetfilecon).with("/foo", "user_u:role_r:type_t:s0").returns 0
set_selinux_context("/foo", "user_u", :seluser).should be_true
end
it "should use lsetfilecon to set role_r role context" do
self.expects(:selinux_support?).returns true
Selinux.expects(:lgetfilecon).with("/foo").returns [0, "user_u:foo:type_t:s0"]
Selinux.expects(:lsetfilecon).with("/foo", "user_u:role_r:type_t:s0").returns 0
set_selinux_context("/foo", "role_r", :selrole).should be_true
end
it "should use lsetfilecon to set type_t type context" do
self.expects(:selinux_support?).returns true
Selinux.expects(:lgetfilecon).with("/foo").returns [0, "user_u:role_r:foo:s0"]
Selinux.expects(:lsetfilecon).with("/foo", "user_u:role_r:type_t:s0").returns 0
set_selinux_context("/foo", "type_t", :seltype).should be_true
end
it "should use lsetfilecon to set s0:c3,c5 range context" do
self.expects(:selinux_support?).returns true
Selinux.expects(:lgetfilecon).with("/foo").returns [0, "user_u:role_r:type_t:s0"]
Selinux.expects(:lsetfilecon).with("/foo", "user_u:role_r:type_t:s0:c3,c5").returns 0
set_selinux_context("/foo", "s0:c3,c5", :selrange).should be_true
end
end
describe "set_selinux_default_context" do
it "should return nil if there is no SELinux support" do
self.expects(:selinux_support?).returns false
set_selinux_default_context("/foo").should be_nil
end
it "should return nil if no default context exists" do
self.expects(:get_selinux_default_context).with("/foo").returns nil
set_selinux_default_context("/foo").should be_nil
end
it "should do nothing and return nil if the current context matches the default context" do
self.expects(:get_selinux_default_context).with("/foo").returns "user_u:role_r:type_t"
self.expects(:get_selinux_current_context).with("/foo").returns "user_u:role_r:type_t"
set_selinux_default_context("/foo").should be_nil
end
it "should set and return the default context if current and default do not match" do
self.expects(:get_selinux_default_context).with("/foo").returns "user_u:role_r:type_t"
self.expects(:get_selinux_current_context).with("/foo").returns "olduser_u:role_r:type_t"
self.expects(:set_selinux_context).with("/foo", "user_u:role_r:type_t").returns true
set_selinux_default_context("/foo").should == "user_u:role_r:type_t"
end
end
end
diff --git a/spec/unit/util/storage_spec.rb b/spec/unit/util/storage_spec.rb
index 582bd2422..fe6b422c9 100755
--- a/spec/unit/util/storage_spec.rb
+++ b/spec/unit/util/storage_spec.rb
@@ -1,210 +1,210 @@
#! /usr/bin/env ruby
require 'spec_helper'
require 'yaml'
require 'fileutils'
require 'puppet/util/storage'
describe Puppet::Util::Storage do
include PuppetSpec::Files
before(:each) do
@basepath = File.expand_path("/somepath")
end
describe "when caching a symbol" do
it "should return an empty hash" do
Puppet::Util::Storage.cache(:yayness).should == {}
Puppet::Util::Storage.cache(:more_yayness).should == {}
end
it "should add the symbol to its internal state" do
Puppet::Util::Storage.cache(:yayness)
Puppet::Util::Storage.state.should == {:yayness=>{}}
end
it "should not clobber existing state when caching additional objects" do
Puppet::Util::Storage.cache(:yayness)
Puppet::Util::Storage.state.should == {:yayness=>{}}
Puppet::Util::Storage.cache(:bubblyness)
Puppet::Util::Storage.state.should == {:yayness=>{},:bubblyness=>{}}
end
end
describe "when caching a Puppet::Type" do
before(:each) do
@file_test = Puppet::Type.type(:file).new(:name => @basepath+"/yayness", :audit => %w{checksum type})
@exec_test = Puppet::Type.type(:exec).new(:name => @basepath+"/bin/ls /yayness")
end
it "should return an empty hash" do
Puppet::Util::Storage.cache(@file_test).should == {}
Puppet::Util::Storage.cache(@exec_test).should == {}
end
it "should add the resource ref to its internal state" do
Puppet::Util::Storage.state.should == {}
Puppet::Util::Storage.cache(@file_test)
Puppet::Util::Storage.state.should == {"File[#{@basepath}/yayness]"=>{}}
Puppet::Util::Storage.cache(@exec_test)
Puppet::Util::Storage.state.should == {"File[#{@basepath}/yayness]"=>{}, "Exec[#{@basepath}/bin/ls /yayness]"=>{}}
end
end
describe "when caching something other than a resource or symbol" do
it "should cache by converting to a string" do
data = Puppet::Util::Storage.cache(42)
data[:yay] = true
Puppet::Util::Storage.cache("42")[:yay].should be_true
end
end
it "should clear its internal state when clear() is called" do
Puppet::Util::Storage.cache(:yayness)
Puppet::Util::Storage.state.should == {:yayness=>{}}
Puppet::Util::Storage.clear
Puppet::Util::Storage.state.should == {}
end
describe "when loading from the state file" do
before do
Puppet.settings.stubs(:use).returns(true)
end
describe "when the state file/directory does not exist" do
before(:each) do
@path = tmpfile('storage_test')
end
it "should not fail to load" do
- Puppet::FileSystem::File.exist?(@path).should be_false
+ Puppet::FileSystem.exist?(@path).should be_false
Puppet[:statedir] = @path
Puppet::Util::Storage.load
Puppet[:statefile] = @path
Puppet::Util::Storage.load
end
it "should not lose its internal state when load() is called" do
- Puppet::FileSystem::File.exist?(@path).should be_false
+ Puppet::FileSystem.exist?(@path).should be_false
Puppet::Util::Storage.cache(:yayness)
Puppet::Util::Storage.state.should == {:yayness=>{}}
Puppet[:statefile] = @path
Puppet::Util::Storage.load
Puppet::Util::Storage.state.should == {:yayness=>{}}
end
end
describe "when the state file/directory exists" do
before(:each) do
@state_file = tmpfile('storage_test')
FileUtils.touch(@state_file)
Puppet[:statefile] = @state_file
end
def write_state_file(contents)
File.open(@state_file, 'w') { |f| f.write(contents) }
end
it "should overwrite its internal state if load() is called" do
# Should the state be overwritten even if Puppet[:statefile] is not valid YAML?
Puppet::Util::Storage.cache(:yayness)
Puppet::Util::Storage.state.should == {:yayness=>{}}
Puppet::Util::Storage.load
Puppet::Util::Storage.state.should == {}
end
it "should restore its internal state if the state file contains valid YAML" do
test_yaml = {'File["/yayness"]'=>{"name"=>{:a=>:b,:c=>:d}}}
write_state_file(test_yaml.to_yaml)
Puppet::Util::Storage.load
Puppet::Util::Storage.state.should == test_yaml
end
it "should initialize with a clear internal state if the state file does not contain valid YAML" do
write_state_file('{ invalid')
Puppet::Util::Storage.load
Puppet::Util::Storage.state.should == {}
end
it "should initialize with a clear internal state if the state file does not contain a hash of data" do
write_state_file("not_a_hash")
Puppet::Util::Storage.load
Puppet::Util::Storage.state.should == {}
end
it "should raise an error if the state file does not contain valid YAML and cannot be renamed" do
write_state_file('{ invalid')
File.expects(:rename).raises(SystemCallError)
expect { Puppet::Util::Storage.load }.to raise_error(Puppet::Error, /Could not rename/)
end
it "should attempt to rename the state file if the file is corrupted" do
write_state_file('{ invalid')
File.expects(:rename).at_least_once
Puppet::Util::Storage.load
end
it "should fail gracefully on load() if the state file is not a regular file" do
FileUtils.rm_f(@state_file)
Dir.mkdir(@state_file)
Puppet::Util::Storage.load
end
end
end
describe "when storing to the state file" do
before(:each) do
@state_file = tmpfile('storage_test')
@saved_statefile = Puppet[:statefile]
Puppet[:statefile] = @state_file
end
it "should create the state file if it does not exist" do
- Puppet::FileSystem::File.exist?(Puppet[:statefile]).should be_false
+ Puppet::FileSystem.exist?(Puppet[:statefile]).should be_false
Puppet::Util::Storage.cache(:yayness)
Puppet::Util::Storage.store
- Puppet::FileSystem::File.exist?(Puppet[:statefile]).should be_true
+ Puppet::FileSystem.exist?(Puppet[:statefile]).should be_true
end
it "should raise an exception if the state file is not a regular file" do
Dir.mkdir(Puppet[:statefile])
Puppet::Util::Storage.cache(:yayness)
expect { Puppet::Util::Storage.store }.to raise_error
Dir.rmdir(Puppet[:statefile])
end
it "should load() the same information that it store()s" do
Puppet::Util::Storage.cache(:yayness)
Puppet::Util::Storage.state.should == {:yayness=>{}}
Puppet::Util::Storage.store
Puppet::Util::Storage.clear
Puppet::Util::Storage.state.should == {}
Puppet::Util::Storage.load
Puppet::Util::Storage.state.should == {:yayness=>{}}
end
end
end
diff --git a/spec/unit/util/tag_set_spec.rb b/spec/unit/util/tag_set_spec.rb
index c59674fcb..5d6d36dfe 100644
--- a/spec/unit/util/tag_set_spec.rb
+++ b/spec/unit/util/tag_set_spec.rb
@@ -1,46 +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 = Puppet::Util::TagSet.from_data_hash(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/watcher_spec.rb b/spec/unit/util/watcher_spec.rb
index 33f75ab12..83439c246 100644
--- a/spec/unit/util/watcher_spec.rb
+++ b/spec/unit/util/watcher_spec.rb
@@ -1,59 +1,56 @@
#! /usr/bin/env ruby
require 'spec_helper'
require 'puppet/util/watcher'
describe Puppet::Util::Watcher do
describe "the common file ctime watcher" do
FakeStat = Struct.new(:ctime)
def ctime(time)
FakeStat.new(time)
end
let(:filename) { "fake" }
def after_reading_the_sequence(initial, *results)
- mock_file = mock(filename)
- Puppet::FileSystem::File.expects(:new).with(filename).at_least(1).returns mock_file
-
- expectation = mock_file.stubs(:stat)
+ expectation = Puppet::FileSystem.expects(:stat).with(filename).at_least(1)
([initial] + results).each do |result|
expectation = if result.is_a? Class
expectation.raises(result)
else
expectation.returns(result)
end.then
end
watcher = Puppet::Util::Watcher::Common.file_ctime_change_watcher(filename)
results.size.times { watcher = watcher.next_reading }
watcher
end
it "is intially unchanged" do
expect(after_reading_the_sequence(ctime(20))).to_not be_changed
end
it "has not changed if a section of the file path continues to not exist" do
expect(after_reading_the_sequence(Errno::ENOTDIR, Errno::ENOTDIR)).to_not be_changed
end
it "has not changed if the file continues to not exist" do
expect(after_reading_the_sequence(Errno::ENOENT, Errno::ENOENT)).to_not be_changed
end
it "has changed if the file is created" do
expect(after_reading_the_sequence(Errno::ENOENT, ctime(20))).to be_changed
end
it "is marked as changed if the file is deleted" do
expect(after_reading_the_sequence(ctime(20), Errno::ENOENT)).to be_changed
end
it "is marked as changed if the file modified" do
expect(after_reading_the_sequence(ctime(20), ctime(21))).to be_changed
end
end
end
diff --git a/spec/unit/util/yaml_spec.rb b/spec/unit/util/yaml_spec.rb
index 978afb0a2..a535d79c7 100644
--- a/spec/unit/util/yaml_spec.rb
+++ b/spec/unit/util/yaml_spec.rb
@@ -1,55 +1,55 @@
require 'spec_helper'
require 'puppet/util/yaml'
describe Puppet::Util::Yaml do
include PuppetSpec::Files
let(:filename) { tmpfile("yaml") }
it "reads a YAML file from disk" do
write_file(filename, YAML.dump({ "my" => "data" }))
expect(Puppet::Util::Yaml.load_file(filename)).to eq({ "my" => "data" })
end
it "writes data formatted as YAML to disk" do
Puppet::Util::Yaml.dump({ "my" => "data" }, filename)
expect(Puppet::Util::Yaml.load_file(filename)).to eq({ "my" => "data" })
end
it "raises an error when the file is invalid YAML" do
write_file(filename, "{ invalid")
expect { Puppet::Util::Yaml.load_file(filename) }.to raise_error(Puppet::Util::Yaml::YamlLoadError)
end
it "raises an error when the file does not exist" do
expect { Puppet::Util::Yaml.load_file("no") }.to raise_error(Puppet::Util::Yaml::YamlLoadError, /No such file or directory/)
end
it "raises an error when the filename is illegal" 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
+ Puppet::FileSystem.touch(filename)
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
+ Puppet::FileSystem.touch(filename)
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)
end
end
end
diff --git a/spec/unit/util_spec.rb b/spec/unit/util_spec.rb
index 58cc4bbd8..1c6f30367 100755
--- a/spec/unit/util_spec.rb
+++ b/spec/unit/util_spec.rb
@@ -1,577 +1,577 @@
#!/usr/bin/env ruby
require 'spec_helper'
describe Puppet::Util do
include PuppetSpec::Files
if Puppet.features.microsoft_windows?
def set_mode(mode, file)
Puppet::Util::Windows::Security.set_mode(mode, file)
end
def get_mode(file)
Puppet::Util::Windows::Security.get_mode(file) & 07777
end
else
def set_mode(mode, file)
File.chmod(mode, file)
end
def get_mode(file)
- Puppet::FileSystem::File.new(file).lstat.mode & 07777
+ Puppet::FileSystem.lstat(file).mode & 07777
end
end
describe "#withenv" do
before :each do
@original_path = ENV["PATH"]
@new_env = {:PATH => "/some/bogus/path"}
end
it "should change environment variables within the block then reset environment variables to their original values" do
Puppet::Util.withenv @new_env do
ENV["PATH"].should == "/some/bogus/path"
end
ENV["PATH"].should == @original_path
end
it "should reset environment variables to their original values even if the block fails" do
begin
Puppet::Util.withenv @new_env do
ENV["PATH"].should == "/some/bogus/path"
raise "This is a failure"
end
rescue
end
ENV["PATH"].should == @original_path
end
it "should reset environment variables even when they are set twice" do
# Setting Path & Environment parameters in Exec type can cause weirdness
@new_env["PATH"] = "/someother/bogus/path"
Puppet::Util.withenv @new_env do
# When assigning duplicate keys, can't guarantee order of evaluation
ENV["PATH"].should =~ /\/some.*\/bogus\/path/
end
ENV["PATH"].should == @original_path
end
it "should remove any new environment variables after the block ends" do
@new_env[:FOO] = "bar"
ENV["FOO"] = nil
Puppet::Util.withenv @new_env do
ENV["FOO"].should == "bar"
end
ENV["FOO"].should == nil
end
end
describe "#absolute_path?" do
describe "on posix systems", :as_platform => :posix do
it "should default to the platform of the local system" do
Puppet::Util.should be_absolute_path('/foo')
Puppet::Util.should_not be_absolute_path('C:/foo')
end
end
describe "on windows", :as_platform => :windows do
it "should default to the platform of the local system" do
Puppet::Util.should be_absolute_path('C:/foo')
Puppet::Util.should_not be_absolute_path('/foo')
end
end
describe "when using platform :posix" do
%w[/ /foo /foo/../bar //foo //Server/Foo/Bar //?/C:/foo/bar /\Server/Foo /foo//bar/baz].each do |path|
it "should return true for #{path}" do
Puppet::Util.should be_absolute_path(path, :posix)
end
end
%w[. ./foo \foo C:/foo \\Server\Foo\Bar \\?\C:\foo\bar \/?/foo\bar \/Server/foo foo//bar/baz].each do |path|
it "should return false for #{path}" do
Puppet::Util.should_not be_absolute_path(path, :posix)
end
end
end
describe "when using platform :windows" do
%w[C:/foo C:\foo \\\\Server\Foo\Bar \\\\?\C:\foo\bar //Server/Foo/Bar //?/C:/foo/bar /\?\C:/foo\bar \/Server\Foo/Bar c:/foo//bar//baz].each do |path|
it "should return true for #{path}" do
Puppet::Util.should be_absolute_path(path, :windows)
end
end
%w[/ . ./foo \foo /foo /foo/../bar //foo C:foo/bar foo//bar/baz].each do |path|
it "should return false for #{path}" do
Puppet::Util.should_not be_absolute_path(path, :windows)
end
end
end
end
describe "#path_to_uri" do
%w[. .. foo foo/bar foo/../bar].each do |path|
it "should reject relative path: #{path}" do
lambda { Puppet::Util.path_to_uri(path) }.should raise_error(Puppet::Error)
end
end
it "should perform URI escaping" do
Puppet::Util.path_to_uri("/foo bar").path.should == "/foo%20bar"
end
describe "when using platform :posix" do
before :each do
Puppet.features.stubs(:posix).returns true
Puppet.features.stubs(:microsoft_windows?).returns false
end
%w[/ /foo /foo/../bar].each do |path|
it "should convert #{path} to URI" do
Puppet::Util.path_to_uri(path).path.should == path
end
end
end
describe "when using platform :windows" do
before :each do
Puppet.features.stubs(:posix).returns false
Puppet.features.stubs(:microsoft_windows?).returns true
end
it "should normalize backslashes" do
Puppet::Util.path_to_uri('c:\\foo\\bar\\baz').path.should == '/' + 'c:/foo/bar/baz'
end
%w[C:/ C:/foo/bar].each do |path|
it "should convert #{path} to absolute URI" do
Puppet::Util.path_to_uri(path).path.should == '/' + path
end
end
%w[share C$].each do |path|
it "should convert UNC #{path} to absolute URI" do
uri = Puppet::Util.path_to_uri("\\\\server\\#{path}")
uri.host.should == 'server'
uri.path.should == '/' + path
end
end
end
end
describe ".uri_to_path" do
require 'uri'
it "should strip host component" do
Puppet::Util.uri_to_path(URI.parse('http://foo/bar')).should == '/bar'
end
it "should accept puppet URLs" do
Puppet::Util.uri_to_path(URI.parse('puppet:///modules/foo')).should == '/modules/foo'
end
it "should return unencoded path" do
Puppet::Util.uri_to_path(URI.parse('http://foo/bar%20baz')).should == '/bar baz'
end
it "should be nil-safe" do
Puppet::Util.uri_to_path(nil).should be_nil
end
describe "when using platform :posix",:if => Puppet.features.posix? do
it "should accept root" do
Puppet::Util.uri_to_path(URI.parse('file:/')).should == '/'
end
it "should accept single slash" do
Puppet::Util.uri_to_path(URI.parse('file:/foo/bar')).should == '/foo/bar'
end
it "should accept triple slashes" do
Puppet::Util.uri_to_path(URI.parse('file:///foo/bar')).should == '/foo/bar'
end
end
describe "when using platform :windows", :if => Puppet.features.microsoft_windows? do
it "should accept root" do
Puppet::Util.uri_to_path(URI.parse('file:/C:/')).should == 'C:/'
end
it "should accept single slash" do
Puppet::Util.uri_to_path(URI.parse('file:/C:/foo/bar')).should == 'C:/foo/bar'
end
it "should accept triple slashes" do
Puppet::Util.uri_to_path(URI.parse('file:///C:/foo/bar')).should == 'C:/foo/bar'
end
it "should accept file scheme with double slashes as a UNC path" do
Puppet::Util.uri_to_path(URI.parse('file://host/share/file')).should == '//host/share/file'
end
end
end
describe "safe_posix_fork" do
let(:pid) { 5501 }
before :each do
# Most of the things this method does are bad to do during specs. :/
Kernel.stubs(:fork).returns(pid).yields
$stdin.stubs(:reopen)
$stdout.stubs(:reopen)
$stderr.stubs(:reopen)
# ensure that we don't really close anything!
(0..256).each {|n| IO.stubs(:new) }
end
it "should close all open file descriptors except stdin/stdout/stderr" do
# This is ugly, but I can't really think of a better way to do it without
# letting it actually close fds, which seems risky
(0..2).each {|n| IO.expects(:new).with(n).never}
(3..256).each {|n| IO.expects(:new).with(n).returns mock('io', :close) }
Puppet::Util.safe_posix_fork
end
it "should fork a child process to execute the block" do
Kernel.expects(:fork).returns(pid).yields
Puppet::Util.safe_posix_fork do
message = "Fork this!"
end
end
it "should return the pid of the child process" do
Puppet::Util.safe_posix_fork.should == pid
end
end
describe "#which" do
let(:base) { File.expand_path('/bin') }
let(:path) { File.join(base, 'foo') }
before :each do
FileTest.stubs(:file?).returns false
FileTest.stubs(:file?).with(path).returns true
FileTest.stubs(:executable?).returns false
FileTest.stubs(:executable?).with(path).returns true
end
it "should accept absolute paths" do
Puppet::Util.which(path).should == path
end
it "should return nil if no executable found" do
Puppet::Util.which('doesnotexist').should be_nil
end
it "should warn if the user's HOME is not set but their PATH contains a ~" do
env_path = %w[~/bin /usr/bin /bin].join(File::PATH_SEPARATOR)
env = {:HOME => nil, :PATH => env_path}
env.merge!({:HOMEDRIVE => nil, :USERPROFILE => nil}) if Puppet.features.microsoft_windows?
Puppet::Util.withenv(env) do
Puppet::Util::Warnings.expects(:warnonce).once
Puppet::Util.which('foo')
end
end
it "should reject directories" do
Puppet::Util.which(base).should be_nil
end
it "should ignore ~user directories if the user doesn't exist" do
# Windows treats *any* user as a "user that doesn't exist", which means
# that this will work correctly across all our platforms, and should
# behave consistently. If they ever implement it correctly (eg: to do
# the lookup for real) it should just work transparently.
baduser = 'if_this_user_exists_I_will_eat_my_hat'
Puppet::Util.withenv("PATH" => "~#{baduser}#{File::PATH_SEPARATOR}#{base}") do
Puppet::Util.which('foo').should == path
end
end
describe "on POSIX systems" do
before :each do
Puppet.features.stubs(:posix?).returns true
Puppet.features.stubs(:microsoft_windows?).returns false
end
it "should walk the search PATH returning the first executable" do
ENV.stubs(:[]).with('PATH').returns(File.expand_path('/bin'))
Puppet::Util.which('foo').should == path
end
end
describe "on Windows systems" do
let(:path) { File.expand_path(File.join(base, 'foo.CMD')) }
before :each do
Puppet.features.stubs(:posix?).returns false
Puppet.features.stubs(:microsoft_windows?).returns true
end
describe "when a file extension is specified" do
it "should walk each directory in PATH ignoring PATHEXT" do
ENV.stubs(:[]).with('PATH').returns(%w[/bar /bin].map{|dir| File.expand_path(dir)}.join(File::PATH_SEPARATOR))
FileTest.expects(:file?).with(File.join(File.expand_path('/bar'), 'foo.CMD')).returns false
ENV.expects(:[]).with('PATHEXT').never
Puppet::Util.which('foo.CMD').should == path
end
end
describe "when a file extension is not specified" do
it "should walk each extension in PATHEXT until an executable is found" do
bar = File.expand_path('/bar')
ENV.stubs(:[]).with('PATH').returns("#{bar}#{File::PATH_SEPARATOR}#{base}")
ENV.stubs(:[]).with('PATHEXT').returns(".EXE#{File::PATH_SEPARATOR}.CMD")
exts = sequence('extensions')
FileTest.expects(:file?).in_sequence(exts).with(File.join(bar, 'foo.EXE')).returns false
FileTest.expects(:file?).in_sequence(exts).with(File.join(bar, 'foo.CMD')).returns false
FileTest.expects(:file?).in_sequence(exts).with(File.join(base, 'foo.EXE')).returns false
FileTest.expects(:file?).in_sequence(exts).with(path).returns true
Puppet::Util.which('foo').should == path
end
it "should walk the default extension path if the environment variable is not defined" do
ENV.stubs(:[]).with('PATH').returns(base)
ENV.stubs(:[]).with('PATHEXT').returns(nil)
exts = sequence('extensions')
%w[.COM .EXE .BAT].each do |ext|
FileTest.expects(:file?).in_sequence(exts).with(File.join(base, "foo#{ext}")).returns false
end
FileTest.expects(:file?).in_sequence(exts).with(path).returns true
Puppet::Util.which('foo').should == path
end
it "should fall back if no extension matches" do
ENV.stubs(:[]).with('PATH').returns(base)
ENV.stubs(:[]).with('PATHEXT').returns(".EXE")
FileTest.stubs(:file?).with(File.join(base, 'foo.EXE')).returns false
FileTest.stubs(:file?).with(File.join(base, 'foo')).returns true
FileTest.stubs(:executable?).with(File.join(base, 'foo')).returns true
Puppet::Util.which('foo').should == File.join(base, 'foo')
end
end
end
end
describe "#binread" do
let(:contents) { "foo\r\nbar" }
it "should preserve line endings" do
path = tmpfile('util_binread')
File.open(path, 'wb') { |f| f.print contents }
Puppet::Util.binread(path).should == contents
end
it "should raise an error if the file doesn't exist" do
expect { Puppet::Util.binread('/path/does/not/exist') }.to raise_error(Errno::ENOENT)
end
end
describe "hash symbolizing functions" do
let (:myhash) { { "foo" => "bar", :baz => "bam" } }
let (:resulthash) { { :foo => "bar", :baz => "bam" } }
describe "#symbolizehash" do
it "should return a symbolized hash" do
newhash = Puppet::Util.symbolizehash(myhash)
newhash.should == resulthash
end
end
end
context "#replace_file" do
subject { Puppet::Util }
it { should respond_to :replace_file }
let :target do
target = Tempfile.new("puppet-util-replace-file")
target.puts("hello, world")
target.flush # make sure content is on disk.
target.fsync rescue nil
target.close
target
end
it "should fail if no block is given" do
expect { subject.replace_file(target.path, 0600) }.to raise_error /block/
end
it "should replace a file when invoked" do
# Check that our file has the expected content.
File.read(target.path).should == "hello, world\n"
# Replace the file.
subject.replace_file(target.path, 0600) do |fh|
fh.puts "I am the passenger..."
end
# ...and check the replacement was complete.
File.read(target.path).should == "I am the passenger...\n"
end
# When running with the same user and group sid, which is the default,
# Windows collapses the owner and group modes into a single ACE, resulting
# in set(0600) => get(0660) and so forth. --daniel 2012-03-30
modes = [0555, 0660, 0770]
modes += [0600, 0700] unless Puppet.features.microsoft_windows?
modes.each do |mode|
it "should copy 0#{mode.to_s(8)} permissions from the target file by default" do
set_mode(mode, target.path)
get_mode(target.path).should == mode
subject.replace_file(target.path, 0000) {|fh| fh.puts "bazam" }
get_mode(target.path).should == mode
File.read(target.path).should == "bazam\n"
end
end
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 = Puppet::FileSystem::File.new(target.path).stat.ino
+ inode = Puppet::FileSystem.stat(target.path).ino
yielded = false
subject.replace_file(target.path, 0600) do |fh|
get_mode(fh.path).should == 0555
yielded = true
end
yielded.should be_true
- Puppet::FileSystem::File.new(target.path).stat.ino.should_not == inode
+ Puppet::FileSystem.stat(target.path).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'
- Puppet::FileSystem::File.exist?(new_target).should be_false
+ Puppet::FileSystem.exist?(new_target).should be_false
begin
subject.replace_file(new_target, 0555) {|fh| fh.puts "foo" }
get_mode(new_target).should == 0555
ensure
- Puppet::FileSystem::File.unlink(new_target) if Puppet::FileSystem::File.exist?(new_target)
+ Puppet::FileSystem.unlink(new_target) if Puppet::FileSystem.exist?(new_target)
end
end
it "should not replace the file if an exception is thrown in the block" do
yielded = false
threw = false
begin
subject.replace_file(target.path, 0600) do |fh|
yielded = true
fh.puts "different content written, then..."
raise "...throw some random failure"
end
rescue Exception => e
if e.to_s =~ /some random failure/
threw = true
else
raise
end
end
yielded.should be_true
threw.should be_true
# ...and check the replacement was complete.
File.read(target.path).should == "hello, world\n"
end
{: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"
- Puppet::FileSystem::File.exist?(new_target).should be_false
+ Puppet::FileSystem.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
- Puppet::FileSystem::File.unlink(new_target) if Puppet::FileSystem::File.exist?(new_target)
+ Puppet::FileSystem.unlink(new_target) if Puppet::FileSystem.exist?(new_target)
end
end
end
end
describe "#pretty_backtrace" do
it "should include lines that don't match the standard backtrace pattern" do
line = "non-standard line\n"
trace = caller[0..2] + [line] + caller[3..-1]
Puppet::Util.pretty_backtrace(trace).should =~ /#{line}/
end
it "should include function names" do
Puppet::Util.pretty_backtrace.should =~ /:in `\w+'/
end
it "should work with Windows paths" do
Puppet::Util.pretty_backtrace(["C:/work/puppet/c.rb:12:in `foo'\n"]).
should == "C:/work/puppet/c.rb:12:in `foo'"
end
end
describe "#execute" do
let(:command) { 'mycommand' }
it "should pass arguments through" do
arguments = 'myarg'
Puppet::Util::Execution.expects(:execute).with(command, arguments)
subject.execute(command, arguments)
end
it "should not supply default arguments" do
Puppet::Util::Execution.expects(:execute).with(command)
subject.execute(command)
end
end
describe "#deterministic_rand" do
it "should not fiddle with future rand calls" do
Puppet::Util.deterministic_rand(123,20)
rand_one = rand()
Puppet::Util.deterministic_rand(123,20)
rand().should_not eql(rand_one)
end
if defined?(Random) == 'constant' && Random.class == Class
it "should not fiddle with the global seed" do
srand(1234)
Puppet::Util.deterministic_rand(123,20)
srand().should eql(1234)
end
# ruby below 1.9.2 variant
else
it "should set a new global seed" do
srand(1234)
Puppet::Util.deterministic_rand(123,20)
srand().should_not eql(1234)
end
end
end
end
diff --git a/tasks/benchmark.rake b/tasks/benchmark.rake
index bd1dcb9d7..7456c5c0a 100644
--- a/tasks/benchmark.rake
+++ b/tasks/benchmark.rake
@@ -1,110 +1,109 @@
require 'benchmark'
require 'tmpdir'
require 'csv'
namespace :benchmark do
def generate_scenario_tasks(location, name)
desc File.read(File.join(location, 'description'))
task name => "#{name}:run"
namespace name do
task :setup do
ENV['ITERATIONS'] ||= '10'
ENV['SIZE'] ||= '100'
ENV['TARGET'] ||= Dir.mktmpdir(name)
ENV['TARGET'] = File.expand_path(ENV['TARGET'])
mkdir_p(ENV['TARGET'])
require File.expand_path(File.join(location, 'benchmarker.rb'))
@benchmark = Benchmarker.new(ENV['TARGET'], ENV['SIZE'].to_i)
end
- desc "Generate the #{name} scenario."
task :generate => :setup do
@benchmark.generate
@benchmark.setup
end
desc "Run the #{name} scenario."
task :run => :generate do
format = if RUBY_VERSION =~ /^1\.8/
Benchmark::FMTSTR
else
Benchmark::FORMAT
end
report = []
Benchmark.benchmark(Benchmark::CAPTION, 10, format, "> total:", "> avg:") do |b|
times = []
ENV['ITERATIONS'].to_i.times do |i|
start_time = Time.now.to_i
times << b.report("Run #{i + 1}") do
@benchmark.run
end
report << [to_millis(start_time), to_millis(times.last.real), 200, true, name]
end
sum = times.inject(Benchmark::Tms.new, &:+)
[sum, sum / times.length]
end
write_csv("#{name}.samples",
%w{timestamp elapsed responsecode success name},
report)
end
desc "Profile a single run of the #{name} scenario."
task :profile => :generate do
require 'ruby-prof'
result = RubyProf.profile do
@benchmark.run
end
printer = RubyProf::CallTreePrinter.new(result)
File.open(File.join("callgrind.#{name}.#{Time.now.to_i}.trace"), "w") do |f|
printer.print(f)
end
end
def to_millis(seconds)
(seconds * 1000).round
end
def write_csv(file, header, data)
CSV.open(file, 'w') do |csv|
csv << header
data.each do |line|
csv << line
end
end
end
end
end
scenarios = []
Dir.glob('benchmarks/*') do |location|
name = File.basename(location)
scenarios << name
generate_scenario_tasks(location, File.basename(location))
end
namespace :all do
desc "Profile all of the scenarios. (#{scenarios.join(', ')})"
task :profile do
scenarios.each do |name|
sh "rake benchmark:#{name}:profile"
end
end
desc "Run all of the scenarios. (#{scenarios.join(', ')})"
task :run do
scenarios.each do |name|
sh "rake benchmark:#{name}:run"
end
end
end
end
diff --git a/tasks/parallel.rake b/tasks/parallel.rake
new file mode 100644
index 000000000..87b25cf95
--- /dev/null
+++ b/tasks/parallel.rake
@@ -0,0 +1,408 @@
+# encoding: utf-8
+
+require 'rubygems'
+require 'thread'
+begin
+ require 'rspec'
+ require 'rspec/core/formatters/helpers'
+ require 'facter'
+rescue LoadError
+ # Don't define the task if we don't have rspec or facter present
+else
+ module Parallel
+ module RSpec
+ #
+ # Responsible for buffering the output of RSpec's progress formatter.
+ #
+ class ProgressFormatBuffer
+ attr_reader :pending_lines
+ attr_reader :failure_lines
+ attr_reader :examples
+ attr_reader :failures
+ attr_reader :pending
+ attr_reader :failed_example_lines
+ attr_reader :state
+
+ module OutputState
+ HEADER = 1
+ PROGRESS = 2
+ SUMMARY = 3
+ PENDING = 4
+ FAILURES = 5
+ DURATION = 6
+ COUNTS = 7
+ FAILED_EXAMPLES = 8
+ end
+
+ def initialize(io, color)
+ @io = io
+ @color = color
+ @state = OutputState::HEADER
+ @pending_lines = []
+ @failure_lines = []
+ @examples = 0
+ @failures = 0
+ @pending = 0
+ @failed_example_lines = []
+ end
+
+ def color?
+ @color
+ end
+
+ def read
+ # Parse and ignore the one line header
+ if @state == OutputState::HEADER
+ begin
+ @io.readline
+ rescue EOFError
+ return nil
+ end
+ @state = OutputState::PROGRESS
+ return ''
+ end
+
+ # If the progress has been read, parse the summary
+ if @state == OutputState::SUMMARY
+ parse_summary
+ return nil
+ end
+
+ # Read the progress output up to 128 bytes at a time
+ # 128 is a small enough number to show some progress, but not too small that
+ # we're constantly writing synchronized output
+ data = @io.read(128)
+ return nil unless data
+
+ data = @remainder + data if @remainder
+
+ # Check for the end of the progress line
+ if (index = data.index "\n")
+ @state = OutputState::SUMMARY
+ @remainder = data[(index+1)..-1]
+ data = data[0...index]
+ # Check for partial ANSI escape codes in colorized output
+ elsif @color && !data.end_with?("\e[0m") && (index = data.rindex("\e[", -6))
+ @remainder = data[index..-1]
+ data = data[0...index]
+ else
+ @remainder = nil
+ end
+
+ data
+ end
+
+ private
+
+ def parse_summary
+ # If there is a remainder, concat it with the next line and handle each line
+ unless @remainder.empty?
+ lines = @remainder
+ eof = false
+ begin
+ lines += @io.readline
+ rescue EOFError
+ eof = true
+ end
+ lines.each_line do |line|
+ parse_summary_line line
+ end
+ return if eof
+ end
+
+ # Process the rest of the lines
+ begin
+ @io.each_line do |line|
+ parse_summary_line line
+ end
+ rescue EOFError
+ end
+ end
+
+ def parse_summary_line(line)
+ line.chomp!
+ return if line.empty?
+
+ if line == 'Pending:'
+ @status = OutputState::PENDING
+ return
+ elsif line == 'Failures:'
+ @status = OutputState::FAILURES
+ return
+ elsif line == 'Failed examples:'
+ @status = OutputState::FAILED_EXAMPLES
+ return
+ elsif (line.match /^Finished in ((\d+\.?\d*) minutes?)? ?(\d+\.?\d*) seconds?$/)
+ @status = OutputState::DURATION
+ return
+ elsif (match = line.gsub(/\e\[\d+m/, '').match /^(\d+) examples?, (\d+) failures?(, (\d+) pending)?$/)
+ @status = OutputState::COUNTS
+ @examples = match[1].to_i
+ @failures = match[2].to_i
+ @pending = (match[4] || 0).to_i
+ return
+ end
+
+ case @status
+ when OutputState::PENDING
+ @pending_lines << line
+ when OutputState::FAILURES
+ @failure_lines << line
+ when OutputState::FAILED_EXAMPLES
+ @failed_example_lines << line
+ end
+ end
+ end
+
+ #
+ # Responsible for parallelizing spec testing.
+ #
+ class Parallelizer
+ include ::RSpec::Core::Formatters::Helpers
+
+ # Number of processes to use
+ attr_reader :process_count
+ # Approximate size of each group of tests
+ attr_reader :group_size
+
+ def initialize(process_count, group_size, color)
+ @process_count = process_count
+ @group_size = group_size
+ @color = color
+ end
+
+ def color?
+ @color
+ end
+
+ def run
+ @start_time = Time.now
+
+ groups = group_specs
+ fail red('error: no specs were found') if groups.length == 0
+
+ begin
+ run_specs groups
+ ensure
+ groups.each do |file|
+ File.unlink(file)
+ end
+ end
+ end
+
+ private
+
+ def group_specs
+ # Spawn the rspec_grouper utility to perform the test grouping
+ # We do this in a separate process to limit this processes' long-running footprint
+ io = IO.popen("ruby util/rspec_grouper #{@group_size}")
+
+ header = true
+ spec_group_files = []
+ io.each_line do |line|
+ line.chomp!
+ header = false if line.empty?
+ next if header || line.empty?
+ spec_group_files << line
+ end
+
+ _, status = Process.waitpid2(io.pid)
+ io.close
+
+ fail red('error: no specs were found.') unless status.success?
+ spec_group_files
+ end
+
+ def run_specs(groups)
+ puts "Processing #{groups.length} spec group(s) with #{@process_count} worker(s)"
+
+ interrupted = false
+ success = true
+ worker_threads = []
+ group_index = -1
+ pids = Array.new(@process_count)
+ mutex = Mutex.new
+
+ # Handle SIGINT by killing child processes
+ original_handler = Signal.trap :SIGINT do
+ break if interrupted
+ interrupted = true
+
+ # Can't synchronize in a trap context, so read dirty
+ pids.each do |pid|
+ begin
+ Process.kill(:SIGKILL, pid) if pid
+ rescue Errno::ESRCH
+ end
+ end
+ puts yellow("\nshutting down...")
+ end
+
+ buffers = []
+
+ process_count.times do |thread_id|
+ worker_threads << Thread.new do
+ while !interrupted do
+ # Get the spec file for this rspec run
+ group = mutex.synchronize { if group_index < groups.length then groups[group_index += 1] else nil end }
+ break unless group && !interrupted
+
+ # Spawn the worker process with redirected output
+ io = IO.popen("ruby util/rspec_runner #{group}")
+ pids[thread_id] = io.pid
+
+ # TODO: make the buffer pluggable to handle other output formats like documentation
+ buffer = ProgressFormatBuffer.new(io, @color)
+
+ # Process the output
+ while !interrupted
+ output = buffer.read
+ break unless output && !interrupted
+ next if output.empty?
+ mutex.synchronize { print output }
+ end
+
+ # Kill the process if we were interrupted, just to be sure
+ if interrupted
+ begin
+ Process.kill(:SIGKILL, pids[thread_id])
+ rescue Errno::ESRCH
+ end
+ end
+
+ # Reap the process
+ result = Process.waitpid2(pids[thread_id])[1].success?
+ io.close
+ pids[thread_id] = nil
+ mutex.synchronize do
+ buffers << buffer
+ success &= result
+ end
+ end
+ end
+ end
+
+ # Join all worker threads
+ worker_threads.each do |thread|
+ thread.join
+ end
+
+ Signal.trap :SIGINT, original_handler
+ fail yellow('execution was interrupted') if interrupted
+
+ dump_summary buffers
+ success
+ end
+
+ def colorize(text, color_code)
+ if @color
+ "#{color_code}#{text}\e[0m"
+ else
+ text
+ end
+ end
+
+ def red(text)
+ colorize(text, "\e[31m")
+ end
+
+ def green(text)
+ colorize(text, "\e[32m")
+ end
+
+ def yellow(text)
+ colorize(text, "\e[33m")
+ end
+
+ def dump_summary(buffers)
+ puts
+
+ # Print out the pending tests
+ print_header = true
+ buffers.each do |buffer|
+ next if buffer.pending_lines.empty?
+ if print_header
+ puts "\nPending:"
+ print_header = false
+ end
+ puts buffer.pending_lines
+ end
+
+ # Print out the failures
+ print_header = true
+ buffers.each do |buffer|
+ next if buffer.failure_lines.empty?
+ if print_header
+ puts "\nFailures:"
+ print_header = false
+ end
+ puts
+ puts buffer.failure_lines
+ end
+
+ # Print out the run time
+ puts "\nFinished in #{format_duration(Time.now - @start_time)}"
+
+ # Count all of the examples
+ examples = 0
+ failures = 0
+ pending = 0
+ buffers.each do |buffer|
+ examples += buffer.examples
+ failures += buffer.failures
+ pending += buffer.pending
+ end
+ if failures > 0
+ puts red(summary_count_line(examples, failures, pending))
+ elsif pending > 0
+ puts yellow(summary_count_line(examples, failures, pending))
+ else
+ puts green(summary_count_line(examples, failures, pending))
+ end
+
+ # Print out the failed examples
+ print_header = true
+ buffers.each do |buffer|
+ next if buffer.failed_example_lines.empty?
+ if print_header
+ puts "\nFailed examples:"
+ print_header = false
+ end
+ puts buffer.failed_example_lines
+ end
+ end
+
+ def summary_count_line(examples, failures, pending)
+ summary = pluralize(examples, "example")
+ summary << ", " << pluralize(failures, "failure")
+ summary << ", #{pending} pending" if pending > 0
+ summary
+ end
+ end
+ end
+ end
+
+ namespace 'parallel' do
+ def color_output?
+ # Check with RSpec to see if color is enabled
+ config = ::RSpec::Core::Configuration.new
+ config.error_stream = $stderr
+ config.output_stream = $stdout
+ options = ::RSpec::Core::ConfigurationOptions.new []
+ options.parse_options
+ options.configure config
+ config.color
+ end
+
+ desc 'Runs specs in parallel.'
+ task 'spec', :process_count, :group_size do |_, args|
+ # Default group size in rspec examples
+ DEFAULT_GROUP_SIZE = 1000
+
+ process_count = [(args[:process_count] || Facter.processorcount).to_i, 1].max
+ group_size = [(args[:group_size] || DEFAULT_GROUP_SIZE).to_i, 1].max
+
+ abort unless Parallel::RSpec::Parallelizer.new(process_count, group_size, color_output?).run
+ end
+ end
+end
\ No newline at end of file
diff --git a/tasks/yard.rake b/tasks/yard.rake
new file mode 100644
index 000000000..d513be771
--- /dev/null
+++ b/tasks/yard.rake
@@ -0,0 +1,59 @@
+begin
+ require 'yard'
+
+ namespace :doc do
+ desc "Clean up generated documentation"
+ task :clean do
+ rm_rf "doc"
+ end
+
+ desc "Generate public documentation pages for the API"
+ YARD::Rake::YardocTask.new(:api) do |t|
+ t.files = ['lib/**/*.rb']
+ t.options = %w{
+ --protected
+ --private
+ --verbose
+ --markup markdown
+ --readme README.md
+ --tag status
+ --transitive-tag status
+ --tag comment
+ --hide-tag comment
+ --tag dsl:"DSL"
+ --no-transitive-tag api
+ --template-path yardoc/templates
+ --files README_DEVELOPER.md,CO*.md,api/**/*.md
+ --api public
+ --api private
+ --hide-void-return
+ }
+ end
+
+ desc "Generate documentation pages for all of the code"
+ YARD::Rake::YardocTask.new(:all) do |t|
+ t.files = ['lib/**/*.rb']
+ t.options = %w{
+ --verbose
+ --markup markdown
+ --readme README.md
+ --tag status
+ --transitive-tag status
+ --tag comment
+ --hide-tag comment
+ --tag dsl:"DSL"
+ --no-transitive-tag api
+ --template-path yardoc/templates
+ --files README_DEVELOPER.md,CO*.md,api/**/*.md
+ --api public
+ --api private
+ --no-api
+ --hide-void-return
+ }
+ end
+ end
+rescue LoadError => e
+ if verbose
+ puts "Document generation not available without yard. #{e.message}"
+ end
+end
diff --git a/util/rspec_grouper b/util/rspec_grouper
new file mode 100755
index 000000000..22260d461
--- /dev/null
+++ b/util/rspec_grouper
@@ -0,0 +1,117 @@
+#!/usr/bin/env ruby
+require 'rubygems'
+require 'rspec'
+
+# Disable ruby verbosity
+# We need control over output so that the parallel task can parse it correctly
+$VERBOSE = nil
+
+# Monkey patch Proc.source_location for 1.8.7
+unless Proc.method_defined? :source_location
+ class Proc
+ def source_location
+ match = to_s.match(/^#<Proc.*@(.*):([0-9]+)>$/)
+ return unless match
+ [match[1], match[2]]
+ end
+ end
+end
+
+module Parallel
+ module RSpec
+ #
+ # Responsible for grouping rspec examples into groups of a given size.
+ #
+ class Grouper
+ attr_reader :groups
+ attr_reader :files
+ attr_reader :total_examples
+
+ def initialize(group_size)
+ config = ::RSpec::Core::Configuration.new
+ options = ::RSpec::Core::ConfigurationOptions.new((ENV['TEST'] || ENV['TESTS'] || 'spec').split(';'))
+ options.parse_options
+ options.configure config
+
+ # This will scan and load all spec examples
+ config.load_spec_files
+
+ @total_examples = 0
+
+ # Populate a map of spec file => example count, sorted ascending by count
+ # NOTE: this uses a private API of RSpec and is may break if the gem is updated
+ @files = ::RSpec::Core::ExampleGroup.children.inject({}) do |files, group|
+ file = group.metadata[:example_group_block].source_location[0]
+ count = count_examples(group)
+ files[file] = (files[file] || 0) + count
+ @total_examples += count
+ files
+ end.sort_by { |_, v| v}
+
+ # Group the spec files
+ @groups = []
+ group = nil
+ example_count = 0
+ @files.each do |file, count|
+ group = [] unless group
+ group << file
+ if (example_count += count) > group_size
+ example_count = 0
+ @groups << group
+ group = nil
+ end
+ end
+ @groups << group if group
+ end
+
+ private
+ def count_examples(group)
+ return 0 unless group
+ # Each group can have examples as well as child groups, so recursively traverse
+ group.children.inject(group.examples.count) { |count, g| count + count_examples(g) }
+ end
+ end
+ end
+end
+
+def print_usage
+ puts 'usage: rspec_grouper <group_size>'
+end
+
+if __FILE__ == $0
+ if ARGV.length != 1
+ print_usage
+ else
+ group_size = ARGV[0].to_i
+ abort 'error: group count must be greater than zero.' if group_size < 1
+ grouper = Parallel::RSpec::Grouper.new(group_size)
+ abort 'error: no rspec examples were found.' if grouper.total_examples == 0
+ groups = grouper.groups
+ puts "Grouped #{grouper.total_examples} rspec example(s) into #{groups.length} group(s) from #{grouper.files.count} file(s)."
+ puts
+
+ paths = []
+
+ begin
+ # Create a temp directory and write out group files
+ tmpdir = Dir.mktmpdir
+ groups.each_with_index do |group, index|
+ path = File.join(tmpdir, "group#{index+1}")
+ file = File.new(path, 'w')
+ paths << path
+ file.puts group
+ file.close
+ puts path
+ end
+ rescue Exception
+ # Delete all files on an exception
+ paths.each do |path|
+ begin
+ File.delete path
+ rescue Exception
+ end
+ end
+ raise
+ end
+ end
+end
\ No newline at end of file
diff --git a/util/rspec_runner b/util/rspec_runner
new file mode 100755
index 000000000..e4452a01c
--- /dev/null
+++ b/util/rspec_runner
@@ -0,0 +1,53 @@
+#!/usr/bin/env ruby
+require 'rubygems'
+require 'rspec'
+require 'rspec/core/formatters/progress_formatter'
+require 'rspec/core/command_line'
+
+# Disable ruby verbosity
+# We need control over output so that the parallel task can parse it correctly
+$VERBOSE = nil
+
+module Parallel
+ module RSpec
+ #
+ # Responsible for formatting output.
+ # This differs from the built-in progress formatter by not appending an index to failures.
+ #
+ class Formatter < ::RSpec::Core::Formatters::ProgressFormatter
+ def dump_failure(example, _)
+ # Unlike the super class implementation, do not print the failure number
+ output.puts "#{short_padding}#{example.full_description}"
+ dump_failure_info(example)
+ end
+ end
+ #
+ # Responsible for running spec files given a spec file.
+ # We do it this way so that we can run very long spec file lists on Windows, since
+ # Windows has a limited argument length depending on method of invocation.
+ #
+ class Runner
+ def initialize(specs_file)
+ abort "error: spec list file '#{specs_file}' does not exist." unless File.exists? specs_file
+ @options = ['-fParallel::RSpec::Formatter']
+ File.readlines(specs_file).each { |line| @options << line.chomp }
+ end
+
+ def run
+ ::RSpec::Core::CommandLine.new(@options).run($stderr, $stdout)
+ end
+ end
+ end
+end
+
+def print_usage
+ puts 'usage: rspec_runner <spec_list_file>'
+end
+
+if __FILE__ == $0
+ if ARGV.length != 1
+ print_usage
+ else
+ exit Parallel::RSpec::Runner.new(ARGV[0]).run
+ end
+end
\ No newline at end of file

File Metadata

Mime Type
application/octet-stream
Expires
Tue, Oct 8, 4:24 PM (1 d, 22 h)
Storage Engine
chunks
Storage Format
Chunks
Storage Handle
vFS3Jb7S0Uhb
Default Alt Text
(6 MB)

Event Timeline