diff --git a/acceptance/tests/environment/enc_nonexistent_directory_environment.rb b/acceptance/tests/environment/enc_nonexistent_directory_environment.rb index 7ad330c55..fdfec201b 100644 --- a/acceptance/tests/environment/enc_nonexistent_directory_environment.rb +++ b/acceptance/tests/environment/enc_nonexistent_directory_environment.rb @@ -1,64 +1,64 @@ test_name "Master should produce error if enc specifies a nonexistent environment" require 'puppet/acceptance/classifier_utils.rb' extend Puppet::Acceptance::ClassifierUtils testdir = create_tmpdir_for_user master, 'nonexistent_env' apply_manifest_on(master, <<-MANIFEST, :catch_failures => true) File { ensure => directory, owner => #{master.puppet['user']}, group => #{master.puppet['group']}, mode => '0755', } file { "#{testdir}":; "#{testdir}/environments":; "#{testdir}/environments/production":; "#{testdir}/environments/production/manifests":; "#{testdir}/environments/production/manifests/site.pp": ensure => file, mode => '0644', content => 'notify { "In the production environment": }'; } MANIFEST if master.is_pe? group = { 'name' => 'Environment Does Not Exist', 'description' => 'Classify our test agent nodes in an environment that does not exist.', 'environment' => 'doesnotexist', 'environment_trumps' => true, } create_group_for_nodes(agents, group) else apply_manifest_on(master, <<-MANIFEST, :catch_failures => true) file { "#{testdir}/enc.rb": ensure => file, mode => '0775', content => '#!#{master['puppetbindir']}/ruby puts "environment: doesnotexist" '; } MANIFEST end master_opts = { 'main' => { 'environmentpath' => "#{testdir}/environments", } } master_opts['master'] = { 'node_terminus' => 'exec', 'external_nodes' => "#{testdir}/enc.rb", } if !master.is_pe? with_puppet_running_on master, master_opts, testdir do agents.each do |agent| on(agent, puppet("agent -t --server #{master} --verbose"), :acceptable_exit_codes => [1]) do - assert_match(/Could not find a directory environment named 'doesnotexist'/, stderr, "Errors when nonexistant environment is specified") + assert_match(/Could not find a directory environment named 'doesnotexist'/, stderr, "Errors when nonexistent environment is specified") assert_not_match(/In the production environment/, stdout, "Executed manifest from production environment") end end end diff --git a/acceptance/tests/ticket_6907_use_provider_in_same_run_it_becomes_suitable.rb b/acceptance/tests/ticket_6907_use_provider_in_same_run_it_becomes_suitable.rb index c8402e3a5..29e15c3be 100644 --- a/acceptance/tests/ticket_6907_use_provider_in_same_run_it_becomes_suitable.rb +++ b/acceptance/tests/ticket_6907_use_provider_in_same_run_it_becomes_suitable.rb @@ -1,46 +1,46 @@ test_name "providers should be useable in the same run they become suitable" agents.each do |agent| dir = agent.tmpdir('provider-6907') on agent, "mkdir -p #{dir}/lib/puppet/{type,provider/test6907}" on agent, "cat > #{dir}/lib/puppet/type/test6907.rb", :stdin => < true) newproperty(:file) end TYPE on agent, "cat > #{dir}/lib/puppet/provider/test6907/only.rb", :stdin => < "#{dir}/must_exist.exe" require 'fileutils' def file 'not correct' end def file=(value) FileUtils.touch(value) end end PROVIDER on agent, puppet_apply("--libdir #{dir}/lib --trace"), :stdin => < "#{dir}/test_file", } # The name of the file is chosen to be *.exe so it works on windows and *nix - # becasue windows inspects the PATHEXT environment variable in 1.9.3 and later. + # because windows inspects the PATHEXT environment variable in 1.9.3 and later. file { "#{dir}/must_exist.exe": ensure => file, mode => "0755", } MANIFEST on agent, "ls #{dir}/test_file" end diff --git a/docs/acceptance_tests.md b/docs/acceptance_tests.md index 809ac9c43..45fd7b29d 100644 --- a/docs/acceptance_tests.md +++ b/docs/acceptance_tests.md @@ -1,239 +1,239 @@ 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 ``` ### Using Git Mirrors By default if you are installing from source, packages will be installed from Github, from their puppetlabs forks. This can be selectively overridden for all installed projects, or per project, by setting environment variables. GIT_SERVER => this will be the address the git server used for all installed projects. Defaults to 'github.com'. FORK => this will be the fork of the project for all installed projects. Defaults to 'puppetlabs'. To customize the server or fork for a specific project use PROJECT_NAME_GIT_SERVER and PROJECT_NAME_FORK. For example, run with these options: ```sh bundle exec rake ci:test:git CONFIG=config/nodes/win2008r2.yaml SHA=abcd PUPPET_GIT_SERVER=percival.corp.puppetlabs.net GIT_SERVER=github.delivery.puppetlabs.net ``` Beaker will install the following: ``` :install=> ["git://github.delivery.puppetlabs.net/puppetlabs-facter.git#stable", "git://github.delivery.puppetlabs.net/puppetlabs-hiera.git#stable", "git://percival.corp.puppetlabs.net/puppetlabs-puppet.git#abcd"], ``` This corresponds to installing facter and hiera stable from our internal mirror, while installing puppet SHA abcd from a git daemon on my local machine percival. See below for details on setting up a local git daemon. 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-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/production/secure/jenkins/id_rsa-acceptance https://github.com/puppetlabs/puppetlabs-modules/blob/production/secure/jenkins/id_rsa-acceptance.pub Please note in acceptance/Rakefile where the ssh key is defaulted to. It may be looking in ~/.ssh/id_rsa-acceptance, but it may want to look in the working directory (e.g. puppet/acceptance). You will also need QA credentials to vsphere in a ~/.fog file. These credentials can be found on any of the Jenkins coordinator hosts. You may want to check periodically to ensure that the credentials you have are still valid as they may change periodically. ### 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 # EXAMPLE - DO NOT RUN THIS 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. NOTE: If you want configuration information to be preserved for all runs (potentially allowing you to run ci:test_against_preserved_hosts for any previous run that failed, and who's hosts were preserved, regardless of whether you initiated with a ci:test_and_preserve_hosts call) then you should add a ':__preserve_config__ => true' to your local_options.rb. ### 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 relase. E.g. you may want to relase a subset of the hosts you've created. Or, if a test run terminates early, ci:release_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. +There also may be scenarios where you want to specify the host(s) to release. E.g. you may want to release a subset of the hosts you've created. Or, if a test run terminates early, ci:release_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:release_hosts HOST_NAMES=lvwwr9tdplg351u,ylrqjh5l6xvym4t bundle exec rake ci:release_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`. +Our hash will only contain the `tests` key, and its value will be a comma-separated 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 . puppetlabs-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 'puppetlabs-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. +near the end of the output, indicating that all tests completed successfully, or you'll see the end of a stack trace, indicating failed tests further up. diff --git a/lib/puppet/external/pson/common.rb b/lib/puppet/external/pson/common.rb index 980df6ece..7bdfa673e 100644 --- a/lib/puppet/external/pson/common.rb +++ b/lib/puppet/external/pson/common.rb @@ -1,370 +1,370 @@ 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 # 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 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. 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. 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", $!.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 + # Outputs _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/file_system.rb b/lib/puppet/file_system.rb index 8a37e7e30..6ed247da5 100644 --- a/lib/puppet/file_system.rb +++ b/lib/puppet/file_system.rb @@ -1,366 +1,366 @@ module Puppet::FileSystem require 'puppet/file_system/path_pattern' require 'puppet/file_system/file_impl' require 'puppet/file_system/memory_file' require 'puppet/file_system/memory_impl' require 'puppet/file_system/uniquefile' # 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 + # While polling to acquire 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] 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/functions.rb b/lib/puppet/functions.rb index 965d7b8f1..0ee1e5c89 100644 --- a/lib/puppet/functions.rb +++ b/lib/puppet/functions.rb @@ -1,555 +1,555 @@ # @note WARNING: This new function API is still under development and may change at any time # # Functions in the puppet language can be written in Ruby and distributed in # puppet modules. The function is written by creating a file in the module's # `lib/puppet/functions/` directory, where `` is # replaced with the module's name. The file should have the name of the function. # For example, to create a function named `min` in a module named `math` create # a file named `lib/puppet/functions/math/min.rb` in the module. # # A function is implemented by calling {Puppet::Functions.create_function}, and # passing it a block that defines the implementation of the function. # # Functions are namespaced inside the module that contains them. The name of # the function is prefixed with the name of the module. For example, # `math::min`. # # @example A simple function # Puppet::Functions.create_function('math::min') do # def min(a, b) # a <= b ? a : b # end # end # # Anatomy of a function # --- # # Functions are composed of four parts: the name, the implementation methods, # the signatures, and the dispatches. # # The name is the string given to the {Puppet::Functions.create_function} # method. It specifies the name to use when calling the function in the puppet # language, or from other functions. # # The implementation methods are ruby methods (there can be one or more) that # provide that actual implementation of the function's behavior. In the # simplest case the name of the function (excluding any namespace) and the name # of the method are the same. When that is done no other parts (signatures and # dispatches) need to be used. # # Signatures are a way of specifying the types of the function's parameters. # The types of any arguments will be checked against the types declared in the # signature and an error will be produced if they don't match. The types are # defined by using the same syntax for types as in the puppet language. # # Dispatches are how signatures and implementation methods are tied together. # When the function is called, puppet searches the signatures for one that # matches the supplied arguments. Each signature is part of a dispatch, which # specifies the method that should be called for that signature. When a # matching signature is found, the corrosponding method is called. # # Documentation for the function should be placed as comments to the # implementation method(s). # # @todo Documentation for individual instances of these new functions is not # yet tied into the puppet doc system. # # @example Dispatching to different methods by type # Puppet::Functions.create_function('math::min') do # dispatch :numeric_min do # param 'Numeric', 'a' # param 'Numeric', 'b' # end # # dispatch :string_min do # param 'String', 'a' # param 'String', 'b' # end # # def numeric_min(a, b) # a <= b ? a : b # end # # def string_min(a, b) # a.downcase <= b.downcase ? a : b # end # end # # Specifying Signatures # --- # # If nothing is specified, the number of arguments given to the function must # be the same as the number of parameters, and all of the parameters are of # type 'Any'. # # To express that the last parameter captures the rest, the method # `last_captures_rest` can be called. This indicates that the last parameter is # a varargs parameter and will be passed to the implementing method as an array # of the given type. # # When defining a dispatch for a function, the resulting dispatch matches # against the specified argument types and min/max occurrence of optional # entries. When the dispatch makes the call to the implementation method the # arguments are simply passed and it is the responsibility of the method's # implementor to ensure it can handle those arguments (i.e. there is no check # that what was declared as optional actually has a default value, and that # a "captures rest" is declared using a `*`). # # @example Varargs # Puppet::Functions.create_function('foo') do # dispatch :foo do # param 'Numeric', 'first' # param 'Numeric', 'values' # last_captures_rest # end # # def foo(first, *values) # # do something # end # end # # Access to Scope # --- # In general, functions should not need access to scope; they should be # written to act on their given input only. If they absolutely must look up # variable values, they should do so via the closure scope (the scope where # they are defined) - this is done by calling `closure_scope()`. # # Calling other Functions # --- # Calling other functions by name is directly supported via # {Puppet::Pops::Functions::Function#call_function}. This allows a function to # call other functions visible from its loader. # # @api public module Puppet::Functions # @param func_name [String, Symbol] a simple or qualified function name # @param block [Proc] the block that defines the methods and dispatch of the # Function to create # @return [Class] the newly created Function class # # @api public def self.create_function(func_name, function_base = Function, &block) if function_base.ancestors.none? { |s| s == Puppet::Pops::Functions::Function } raise ArgumentError, "Functions must be based on Puppet::Pops::Functions::Function. Got #{function_base}" end func_name = func_name.to_s # Creates an anonymous class to represent the function # The idea being that it is garbage collected when there are no more # references to it. # the_class = Class.new(function_base, &block) # Make the anonymous class appear to have the class-name # Even if this class is not bound to such a symbol in a global ruby scope and # must be resolved via the loader. # This also overrides any attempt to define a name method in the given block # (Since it redefines it) # # TODO, enforce name in lower case (to further make it stand out since Ruby # class names are upper case) # the_class.instance_eval do @func_name = func_name def name @func_name end end # Automatically create an object dispatcher based on introspection if the # loaded user code did not define any dispatchers. Fail if function name # does not match a given method name in user code. # if the_class.dispatcher.empty? simple_name = func_name.split(/::/)[-1] type, names = default_dispatcher(the_class, simple_name) last_captures_rest = (type.size_range[1] == Puppet::Pops::Types::INFINITY) the_class.dispatcher.add_dispatch(type, simple_name, names, nil, nil, nil, last_captures_rest) end # The function class is returned as the result of the create function method the_class end # Creates a default dispatcher configured from a method with the same name as the function # # @api private def self.default_dispatcher(the_class, func_name) unless the_class.method_defined?(func_name) raise ArgumentError, "Function Creation Error, cannot create a default dispatcher for function '#{func_name}', no method with this name found" end any_signature(*min_max_param(the_class.instance_method(func_name))) end # @api private def self.min_max_param(method) # Ruby 1.8.7 does not have support for details about parameters if method.respond_to?(:parameters) result = {:req => 0, :opt => 0, :rest => 0 } # TODO: Optimize into one map iteration that produces names map, and sets # count as side effect method.parameters.each { |p| result[p[0]] += 1 } from = result[:req] to = result[:rest] > 0 ? :default : from + result[:opt] names = method.parameters.map {|p| p[1].to_s } else # Cannot correctly compute the signature in Ruby 1.8.7 because arity for # optional values is screwed up (there is no way to get the upper limit), # an optional looks the same as a varargs In this case - the failure will # simply come later when the call fails # arity = method.arity from = arity >= 0 ? arity : -arity -1 to = arity >= 0 ? arity : :default # i.e. infinite (which is wrong when there are optional - flaw in 1.8.7) names = [] # no names available end [from, to, names] end # Construct a signature consisting of Object type, with min, and max, and given names. # (there is only one type entry). # # @api private def self.any_signature(from, to, names) # Construct the type for the signature # Tuple[Object, from, to] factory = Puppet::Pops::Types::TypeFactory [factory.callable(factory.any, from, to), names] end # Function # === # This class is the base class for all Puppet 4x Function API functions. A # specialized class is created for each puppet function. # # @api public class Function < Puppet::Pops::Functions::Function # @api private def self.builder @type_parser ||= Puppet::Pops::Types::TypeParser.new @all_callables ||= Puppet::Pops::Types::TypeFactory.all_callables DispatcherBuilder.new(dispatcher, @type_parser, @all_callables) end # Dispatch any calls that match the signature to the provided method name. # # @param meth_name [Symbol] The name of the implementation method to call # when the signature defined in the block matches the arguments to a call # to the function. # @return [Void] # # @api public def self.dispatch(meth_name, &block) builder().instance_eval do dispatch(meth_name, &block) end end end # Public api methods of the DispatcherBuilder are available within dispatch() # blocks declared in a Puppet::Function.create_function() call. # # @api public class DispatcherBuilder # @api private def initialize(dispatcher, type_parser, all_callables) @type_parser = type_parser @all_callables = all_callables @dispatcher = dispatcher end # Defines a positional parameter with type and name # # @param type [String] The type specification for the parameter. # @param name [String] The name of the parameter. This is primarily used # for error message output and does not have to match the name of the # parameter on the implementation method. # @return [Void] # # @api public def param(type, name) if type.is_a?(String) @types << type @names << name # mark what should be picked for this position when dispatching @weaving << @names.size()-1 else raise ArgumentError, "Type signature argument must be a String reference to a Puppet Data Type. Got #{type.class}" end end # Defines one required block parameter that may appear last. If type and name is missing the # default type is "Callable", and the name is "block". If only one # parameter is given, then that is the name and the type is "Callable". # # @api public def required_block_param(*type_and_name) case type_and_name.size when 0 # the type must be an independent instance since it will be contained in another type type = @all_callables.copy name = 'block' when 1 # the type must be an independent instance since it will be contained in another type type = @all_callables.copy name = type_and_name[0] when 2 type_string, name = type_and_name type = @type_parser.parse(type_string) else raise ArgumentError, "block_param accepts max 2 arguments (type, name), got #{type_and_name.size}." end unless Puppet::Pops::Types::TypeCalculator.is_kind_of_callable?(type, false) raise ArgumentError, "Expected PCallableType or PVariantType thereof, got #{type.class}" end unless name.is_a?(String) || name.is_a?(Symbol) raise ArgumentError, "Expected block_param name to be a String or Symbol, got #{name.class}" end if @block_type.nil? @block_type = type @block_name = name else raise ArgumentError, "Attempt to redefine block" end end # Defines one optional block parameter that may appear last. If type or name is missing the # defaults are "any callable", and the name is "block". The implementor of the dispatch target # must use block = nil when it is optional (or an error is raised when the call is made). # # @api public def optional_block_param(*type_and_name) # same as required, only wrap the result in an optional type required_block_param(*type_and_name) @block_type = Puppet::Pops::Types::TypeFactory.optional(@block_type) end - # Specifies the min and max occurance of arguments (of the specified types) + # Specifies the min and max occurrence of arguments (of the specified types) # if something other than the exact count from the number of specified # types). The max value may be specified as :default if an infinite number of # arguments are supported. When max is > than the number of specified # types, the last specified type repeats. # # @api public def arg_count(min_occurs, max_occurs) @min = min_occurs @max = max_occurs unless min_occurs.is_a?(Integer) && min_occurs >= 0 raise ArgumentError, "min arg_count of function parameter must be an Integer >=0, got #{min_occurs.class} '#{min_occurs}'" end unless max_occurs == :default || (max_occurs.is_a?(Integer) && max_occurs >= 0) raise ArgumentError, "max arg_count of function parameter must be an Integer >= 0, or :default, got #{max_occurs.class} '#{max_occurs}'" end unless max_occurs == :default || (max_occurs.is_a?(Integer) && max_occurs >= min_occurs) raise ArgumentError, "max arg_count must be :default (infinite) or >= min arg_count, got min: '#{min_occurs}, max: '#{max_occurs}'" end end # Specifies that the last argument captures the rest. # # @api public def last_captures_rest @last_captures = true end private # @api private def dispatch(meth_name, &block) # an array of either an index into names/types, or an array with # injection information [type, name, injection_name] used when the call # is being made to weave injections into the given arguments. # @types = [] @names = [] @weaving = [] @injections = [] @min = nil @max = nil @last_captures = false @block_type = nil @block_name = nil self.instance_eval &block callable_t = create_callable(@types, @block_type, @min, @max) @dispatcher.add_dispatch(callable_t, meth_name, @names, @block_name, @injections, @weaving, @last_captures) end # Handles creation of a callable type from strings specifications of puppet # types and allows the min/max occurs of the given types to be given as one # or two integer values at the end. The given block_type should be # Optional[Callable], Callable, or nil. # # @api private def create_callable(types, block_type, from, to) mapped_types = types.map do |t| @type_parser.parse(t) end if !(from.nil? && to.nil?) mapped_types << from mapped_types << to end if block_type mapped_types << block_type end Puppet::Pops::Types::TypeFactory.callable(*mapped_types) end end private # @note WARNING: This style of creating functions is not public. It is a system # under development that will be used for creating "system" functions. # # This is a private, internal, system for creating functions. It supports # everything that the public function definition system supports as well as a # few extra features. # # Injection Support # === # The Function API supports injection of data and services. It is possible to # make injection that takes effect when the function is loaded (for services # and runtime configuration that does not change depending on how/from where # in what context the function is called. It is also possible to inject and # weave argument values into a call. # # Injection of attributes # --- # Injection of attributes is performed by one of the methods `attr_injected`, # and `attr_injected_producer`. The injected attributes are available via # accessor method calls. # # @example using injected attributes # Puppet::Functions.create_function('test') do # attr_injected String, :larger, 'message_larger' # attr_injected String, :smaller, 'message_smaller' # def test(a, b) # a > b ? larger() : smaller() # end # end # # @api private class InternalFunction < Function # @api private def self.builder @type_parser ||= Puppet::Pops::Types::TypeParser.new @all_callables ||= Puppet::Pops::Types::TypeFactory.all_callables InternalDispatchBuilder.new(dispatcher, @type_parser, @all_callables) end # Defines class level injected attribute with reader method # # @api private def self.attr_injected(type, attribute_name, injection_name = nil) define_method(attribute_name) do ivar = :"@#{attribute_name.to_s}" unless instance_variable_defined?(ivar) injector = Puppet.lookup(:injector) instance_variable_set(ivar, injector.lookup(closure_scope, type, injection_name)) end instance_variable_get(ivar) end end # Defines class level injected producer attribute with reader method # # @api private def self.attr_injected_producer(type, attribute_name, injection_name = nil) define_method(attribute_name) do ivar = :"@#{attribute_name.to_s}" unless instance_variable_defined?(ivar) injector = Puppet.lookup(:injector) instance_variable_set(ivar, injector.lookup_producer(closure_scope, type, injection_name)) end instance_variable_get(ivar) end end end # @note WARNING: This style of creating functions is not public. It is a system # under development that will be used for creating "system" functions. # # Injection and Weaving of parameters # --- # It is possible to inject and weave parameters into a call. These extra # parameters are not part of the parameters passed from the Puppet logic, and # they can not be overridden by parameters given as arguments in the call. # They are invisible to the Puppet Language. # # @example using injected parameters # Puppet::Functions.create_function('test') do # dispatch :test do # param 'Scalar', 'a' # param 'Scalar', 'b' # injected_param 'String', 'larger', 'message_larger' # injected_param 'String', 'smaller', 'message_smaller' # end # def test(a, b, larger, smaller) # a > b ? larger : smaller # end # end # # The function in the example above is called like this: # # test(10, 20) # # Using injected value as default # --- # Default value assignment is handled by using the regular Ruby mechanism (a # value is assigned to the variable). The dispatch simply indicates that the # value is optional. If the default value should be injected, it can be # handled different ways depending on what is desired: # # * by calling the accessor method for an injected Function class attribute. # This is suitable if the value is constant across all instantiations of the # function, and across all calls. # * by injecting a parameter into the call # to the left of the parameter, and then assigning that as the default value. # * One of the above forms, but using an injected producer instead of a # directly injected value. # # @example method with injected default values # Puppet::Functions.create_function('test') do # dispatch :test do # injected_param String, 'b_default', 'b_default_value_key' # param 'Scalar', 'a' # param 'Scalar', 'b' # end # def test(b_default, a, b = b_default) # # ... # end # end # # @api private class InternalDispatchBuilder < DispatcherBuilder def scope_param() @injections << [:scope, 'scope', '', :dispatcher_internal] # mark what should be picked for this position when dispatching @weaving << [@injections.size()-1] end # TODO: is param name really needed? Perhaps for error messages? (it is unused now) # # @api private def injected_param(type, name, injection_name = '') @injections << [type, name, injection_name] # mark what should be picked for this position when dispatching @weaving << [@injections.size() -1] end # TODO: is param name really needed? Perhaps for error messages? (it is unused now) # # @api private def injected_producer_param(type, name, injection_name = '') @injections << [type, name, injection_name, :producer] # mark what should be picked for this position when dispatching @weaving << [@injections.size()-1] end end end diff --git a/lib/puppet/interface/action_builder.rb b/lib/puppet/interface/action_builder.rb index da1bdd0fe..abae7ff9a 100644 --- a/lib/puppet/interface/action_builder.rb +++ b/lib/puppet/interface/action_builder.rb @@ -1,149 +1,149 @@ # This class is used to build {Puppet::Interface::Action actions}. # When an action is defined with # {Puppet::Interface::ActionManager#action} the block is evaluated # within the context of a new instance of this class. # @api public class Puppet::Interface::ActionBuilder # The action under construction # @return [Puppet::Interface::Action] # @api private attr_reader :action # Builds a new action. # @return [Puppet::Interface::Action] # @api private def self.build(face, name, &block) raise "Action #{name.inspect} must specify a block" unless block new(face, name, &block).action end # Ideally the method we're defining here would be added to the action, and a # method on the face would defer to it, but we can't get scope correct, so # we stick with this. --daniel 2011-03-24 # Sets what the action does when it is invoked. This takes a block # which will be called when the action is invoked. The action will # accept arguments based on the arity of the block. It should always # take at least one argument for options. Options will be the last # argument. # # @overload when_invoked({|options| ... }) # An action with no arguments # @overload when_invoked({|arg1, arg2, options| ... }) # An action with two arguments # @return [void] # @api public # @dsl Faces def when_invoked(&block) @action.when_invoked = block end # Sets a block to be run at the rendering stage, for a specific # rendering type (eg JSON, YAML, console), after the block for # when_invoked gets run. This manipulates the value returned by the # action. It makes it possible to work around limitations in the # underlying object returned, and should be avoided in favor of # returning a more capable object. # @api private # @todo this needs more # @dsl Faces def when_rendering(type = nil, &block) if type.nil? then # the default error message sucks --daniel 2011-04-18 raise ArgumentError, 'You must give a rendering format to when_rendering' end if block.nil? then raise ArgumentError, 'You must give a block to when_rendering' end @action.set_rendering_method_for(type, block) end # Declare that this action can take a specific option, and provide the # code to do so. One or more strings are given, in the style of # OptionParser (see example). These strings are parsed to derive a # name for the option. Any `-` characters within the option name (ie - # excluding the intial `-` or `--` for an option) will be translated + # excluding the initial `-` or `--` for an option) will be translated # to `_`.The first long option will be used as the name, and the rest # are retained as aliases. The original form of the option is used # when invoking the face, the translated form is used internally. # # When the action is invoked the value of the option is available in # a hash passed to the {Puppet::Interface::ActionBuilder#when_invoked # when_invoked} block, using the option name in symbol form as the # hash key. # # The block to this method is used to set attributes for the option # (see {Puppet::Interface::OptionBuilder}). # # @param declaration [String] Option declarations, as described above # and in the example. # # @example Say hi # action :say_hi do # option "-u USER", "--user-name USER" do # summary "Who to say hi to" # end # # when_invoked do |options| # "Hi, #{options[:user_name]}" # end # end # @api public # @dsl Faces def option(*declaration, &block) option = Puppet::Interface::OptionBuilder.build(@action, *declaration, &block) @action.add_option(option) end # Set this as the default action for the face. # @api public # @dsl Faces # @return [void] def default(value = true) @action.default = !!value end # @api private def display_global_options(*args) @action.add_display_global_options args end alias :display_global_option :display_global_options # Sets the default rendering format # @api private def render_as(value = nil) value.nil? and raise ArgumentError, "You must give a rendering format to render_as" formats = Puppet::Network::FormatHandler.formats unless formats.include? value raise ArgumentError, "#{value.inspect} is not a valid rendering format: #{formats.sort.join(", ")}" end @action.render_as = value end # Metaprogram the simple DSL from the target class. Puppet::Interface::Action.instance_methods.grep(/=$/).each do |setter| next if setter =~ /^=/ property = setter.to_s.chomp('=') unless method_defined? property # Using eval because the argument handling semantics are less awful than # when we use the define_method/block version. The later warns on older # Ruby versions if you pass the wrong number of arguments, but carries # on, which is totally not what we want. --daniel 2011-04-18 eval <<-METHOD def #{property}(value) @action.#{property} = value end METHOD end end private def initialize(face, name, &block) @face = face @action = Puppet::Interface::Action.new(face, name) instance_eval(&block) @action.when_invoked or raise ArgumentError, "actions need to know what to do when_invoked; please add the block" end end diff --git a/lib/puppet/module_tool.rb b/lib/puppet/module_tool.rb index 984947d33..790a66e04 100644 --- a/lib/puppet/module_tool.rb +++ b/lib/puppet/module_tool.rb @@ -1,194 +1,194 @@ # 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', 'checksums.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 'metadata.json' or '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 + 'metadata.json') || 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].to_s.sub(/^(?!v)/, 'v') if mod[:action] == :upgrade previous_version = mod[:previous_version].to_s.sub(/^(?!v)/, 'v') version_string = "#{previous_version} -> #{version_string}" end mod[:text] = "#{mod[:name]} (#{colorize(:cyan, version_string)})" mod[:text] += " [#{mod[:path]}]" unless mod[:path].to_s == dir.to_s deps = (mod[:dependencies] || []) deps.sort! { |a, b| a[:name] <=> b[:name] } build_tree(deps, dir) end end # @param options [Hash] 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. # @todo Validate the above note... # # An :environment_instance and a :target_dir are added/updated in the # options parameter. # # @api private def self.set_option_defaults(options) current_environment = environment_from_options(options) modulepath = [options[:target_dir]] + current_environment.full_modulepath face_environment = current_environment.override_with(:modulepath => modulepath.compact) options[:environment_instance] = face_environment # Note: environment will have expanded the path options[:target_dir] = face_environment.full_modulepath.first end # Given a hash of options, we should discover or create a # {Puppet::Node::Environment} instance that reflects the provided options. # - # Generally speaking, the `:modulepath` parameter should supercede all + # Generally speaking, the `:modulepath` parameter should supersede all # others, the `:environment` parameter should follow after that, and we # should default to Puppet's current environment. # # @param options [{Symbol => Object}] the options to derive environment from # @return [Puppet::Node::Environment] the environment described by the options def self.environment_from_options(options) if options[:modulepath] path = options[:modulepath].split(File::PATH_SEPARATOR) Puppet::Node::Environment.create(:anonymous, path, '') elsif options[:environment].is_a?(Puppet::Node::Environment) options[:environment] elsif options[:environment] # This use of looking up an environment is correct since it honours # a reguest to get a particular environment via environment name. Puppet.lookup(:environments).get!(options[:environment]) else Puppet.lookup(:current_environment) end end # Handles parsing of module dependency expressions into proper # {Semantic::VersionRange}s, including reasonable error handling. # # @param where [String] a description of the thing we're parsing the # dependency expression for # @param dep [Hash] the dependency description to parse # @return [Array(String, Semantic::VersionRange, String)] an tuple of the # dependent module's name, the version range dependency, and the # unparsed range expression. def self.parse_module_dependency(where, dep) dep_name = dep['name'].tr('/', '-') range = dep['version_requirement'] || dep['versionRequirement'] || '>= 0.0.0' begin parsed_range = Semantic::VersionRange.parse(range) rescue ArgumentError => e Puppet.debug "Error in #{where} parsing dependency #{dep_name} (#{e.message}); using empty range." parsed_range = Semantic::VersionRange::EMPTY_RANGE end [ dep_name, parsed_range, range ] 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/forge/cache' require 'puppet/forge' diff --git a/lib/puppet/module_tool/skeleton/templates/generator/manifests/init.pp.erb b/lib/puppet/module_tool/skeleton/templates/generator/manifests/init.pp.erb index d7c5f4aa7..e93be8f59 100644 --- a/lib/puppet/module_tool/skeleton/templates/generator/manifests/init.pp.erb +++ b/lib/puppet/module_tool/skeleton/templates/generator/manifests/init.pp.erb @@ -1,41 +1,41 @@ # == Class: <%= metadata.name %> # # Full description of class <%= metadata.name %> here. # # === Parameters # # Document parameters here. # # [*sample_parameter*] # Explanation of what this parameter affects and what it defaults to. # e.g. "Specify one or more upstream ntp servers as an array." # # === Variables # # Here you should define a list of variables that this module would require. # # [*sample_variable*] -# Explanation of how this variable affects the funtion of this class and if +# Explanation of how this variable affects the function of this class and if # it has a default. e.g. "The parameter enc_ntp_servers must be set by the # External Node Classifier as a comma separated list of hostnames." (Note, # global variables should be avoided in favor of class parameters as # of Puppet 2.6.) # # === Examples # # class { '<%= metadata.name %>': # servers => [ 'pool.ntp.org', 'ntp.local.company.com' ], # } # # === Authors # # Author Name # # === Copyright # # Copyright <%= Time.now.year %> Your name here, unless otherwise noted. # class <%= metadata.name %> { } diff --git a/lib/puppet/network/authconfig.rb b/lib/puppet/network/authconfig.rb index f99c12952..d4302d44c 100644 --- a/lib/puppet/network/authconfig.rb +++ b/lib/puppet/network/authconfig.rb @@ -1,76 +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 + # denies not explicitly 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(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/parameter/value_collection.rb b/lib/puppet/parameter/value_collection.rb index 4cbd95d6a..992591d3f 100644 --- a/lib/puppet/parameter/value_collection.rb +++ b/lib/puppet/parameter/value_collection.rb @@ -1,211 +1,211 @@ require 'puppet/parameter/value' # A collection of values and regular expressions, used for specifying allowed values # in a given parameter. # @note This class is considered part of the internal implementation of {Puppet::Parameter}, and # {Puppet::Property} and the functionality provided by this class should be used via their interfaces. # @comment This class probably have several problems when trying to use it with a combination of # regular expressions and aliases as it finds an acceptable value holder vi "name" which may be # a regular expression... # # @api private # class Puppet::Parameter::ValueCollection # Aliases the given existing _other_ value with the additional given _name_. # @return [void] # @api private # def aliasvalue(name, other) other = other.to_sym unless value = match?(other) raise Puppet::DevError, "Cannot alias nonexistent value #{other}" end value.alias(name) end # Returns a doc string (enumerating the acceptable values) for all of the values in this parameter/property. # @return [String] a documentation string. # @api private # def doc unless defined?(@doc) @doc = "" unless values.empty? @doc << "Valid values are " @doc << @strings.collect do |value| if aliases = value.aliases and ! aliases.empty? "`#{value.name}` (also called `#{aliases.join(", ")}`)" else "`#{value.name}`" end end.join(", ") << ". " end unless regexes.empty? @doc << "Values can match `#{regexes.join("`, `")}`." end end @doc end # @return [Boolean] Returns whether the set of allowed values is empty or not. # @api private # def empty? @values.empty? end # @api private # def initialize # We often look values up by name, so a hash makes more sense. @values = {} # However, we want to retain the ability to match values in order, # but we always prefer directly equality (i.e., strings) over regex matches. @regexes = [] @strings = [] end # Checks if the given value is acceptable (matches one of the literal values or patterns) and returns # the "matcher" that matched. # Literal string matchers are tested first, if both a literal and a regexp match would match, the literal # match wins. # # @param test_value [Object] the value to test if it complies with the configured rules # @return [Puppet::Parameter::Value, nil] The instance of Puppet::Parameter::Value that matched the given value, or nil if there was no match. # @api private # def match?(test_value) # First look for normal values if value = @strings.find { |v| v.match?(test_value) } return value end # Then look for a regex match @regexes.find { |v| v.match?(test_value) } end # Munges the value if it is valid, else produces the same value. # @param value [Object] the value to munge # @return [Object] the munged value, or the given value # @todo This method does not seem to do any munging. It just returns the value if it matches the # regexp, or the (most likely Symbolic) allowed value if it matches (which is more of a replacement # of one instance with an equal one. Is the intent that this method should be specialized? # @api private # def munge(value) return value if empty? if instance = match?(value) if instance.regex? return value else return instance.name end else return value end end # Defines a new valid value for a {Puppet::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 Puppet::Property#set). # @option options [Symbol] :invalidate_refreshes True if 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). # @api private # def newvalue(name, options = {}, &block) value = Puppet::Parameter::Value.new(name) @values[value.name] = value if value.regex? @regexes << value else @strings << value end options.each { |opt, arg| value.send(opt.to_s + "=", arg) } if block_given? value.block = block else value.call = options[:call] || :none end value.method ||= "set_#{value.name}" if block_given? and ! value.regex? value end # Defines one or more valid values (literal or regexp) for a parameter or property. # @return [void] # @dsl type # @api private # def newvalues(*names) names.each { |name| newvalue(name) } end # @return [Array] An array of the regular expressions in string form, configured as matching valid values. # @api private # def regexes @regexes.collect { |r| r.name.inspect } end # Validates the given value against the set of valid literal values and regular expressions. # @raise [ArgumentError] if the value is not accepted # @return [void] # @api private # def validate(value) return if empty? unless @values.detect { |name, v| v.match?(value) } str = "Invalid value #{value.inspect}. " str += "Valid values are #{values.join(", ")}. " unless values.empty? str += "Valid values match #{regexes.join(", ")}." unless regexes.empty? raise ArgumentError, str end end # Returns a valid value matcher (a literal or regular expression) # @todo This looks odd, asking for an instance that matches a symbol, or an instance that has # a regexp. What is the intention here? Marking as api private... # - # @return [Puppet::Parameter::Value] a valid valud matcher + # @return [Puppet::Parameter::Value] a valid value matcher # @api private # def value(name) @values[name] end # @return [Array] Returns a list of valid literal values. # @see regexes # @api private # def values @strings.collect { |s| s.name } end end diff --git a/lib/puppet/parser/compiler.rb b/lib/puppet/parser/compiler.rb index dc7ee26cc..34914e4bf 100644 --- a/lib/puppet/parser/compiler.rb +++ b/lib/puppet/parser/compiler.rb @@ -1,624 +1,624 @@ 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 errors = node.environment.validation_errors if !errors.empty? errors.each { |e| Puppet.err(e) } if errors.size > 1 errmsg = [ "Compilation has been halted because: #{errors.first}", "For more information, see http://docs.puppetlabs.com/puppet/latest/reference/environments.html", ] raise(Puppet::Error, errmsg.join(' ')) end 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 # Access to the configured loaders for 4x # @return [Puppet::Pops::Loader::Loaders] the configured loaders # @api private attr_reader :loaders # 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 Puppet.override( @context_overrides , "For compiling #{node.name}") do @catalog.environment_instance = environment # Set the client's parameters into the top scope. Puppet::Util::Profiler.profile("Compile: Set node parameters", [:compiler, :set_node_params]) { set_node_parameters } Puppet::Util::Profiler.profile("Compile: Created settings scope", [:compiler, :create_settings_scope]) { create_settings_scope } if is_binder_active? # create injector, if not already created - this is for 3x that does not trigger # lazy loading of injector via context Puppet::Util::Profiler.profile("Compile: Created injector", [:compiler, :create_injector]) { injector } end Puppet::Util::Profiler.profile("Compile: Evaluated main", [:compiler, :evaluate_main]) { evaluate_main } Puppet::Util::Profiler.profile("Compile: Evaluated AST node", [:compiler, :evaluate_ast_node]) { evaluate_ast_node } Puppet::Util::Profiler.profile("Compile: Evaluated node classes", [:compiler, :evaluate_node_classes]) { evaluate_node_classes } Puppet::Util::Profiler.profile("Compile: Evaluated generators", [:compiler, :evaluate_generators]) { evaluate_generators } Puppet::Util::Profiler.profile("Compile: Finished catalog", [:compiler, :finish_catalog]) { finish } fail_on_unevaluated @catalog end end # Constructs the overrides for the context def context_overrides() if Puppet[:parser] == 'future' require 'puppet/loaders' { :current_environment => environment, :global_scope => @topscope, # 4x placeholder for new global scope :loaders => lambda {|| loaders() }, # 4x loaders :injector => lambda {|| injector() } # 4x API - via context instead of via compiler } else { :current_environment => environment, } end end def_delegator :@collections, :delete, :delete_collection # Return the node's environment. def 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_with_params, @node_scope || topscope) evaluate_classes(classes_without_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 hostclasses = classes.collect do |name| scope.find_hostclass(name, :assume_fqname => fqname) or raise Puppet::Error, "Could not find class #{name} for #{node.name}" end if class_parameters resources = ensure_classes_with_parameters(scope, hostclasses, class_parameters) if !lazy_evaluate resources.each(&:evaluate) end resources else already_included, newly_included = ensure_classes_without_parameters(scope, hostclasses) if !lazy_evaluate newly_included.each(&:evaluate) end already_included + newly_included 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 loaders @loaders ||= Puppet::Pops::Loaders.new(environment) 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() 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 def ensure_classes_with_parameters(scope, hostclasses, parameters) hostclasses.collect do |klass| klass.ensure_in_catalog(scope, parameters[klass.name] || {}) end end def ensure_classes_without_parameters(scope, hostclasses) already_included = [] newly_included = [] hostclasses.each do |klass| class_scope = scope.class_scope(klass) if class_scope already_included << class_scope.resource else newly_included << klass.ensure_in_catalog(scope) end end [already_included, newly_included] end # 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", [:compiler, :evaluate_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", [:compiler, :evaluate_definitions]) do !unevaluated_resources.each do |resource| Puppet::Util::Profiler.profile("Evaluated resource #{resource}", [:compiler, :evaluate_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", [:compiler, :iterate_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, @node.environment) # MOVED HERE - SCOPE IS NEEDED (MOVE-SCOPE) # Create the initial scope, it is needed early @topscope = Puppet::Parser::Scope.new(self) # Need to compute overrides here, and remember them, because we are about to - # enter the magic zone of known_resource_types and intial import. + # enter the magic zone of known_resource_types and initial import. # Expensive entries in the context are bound lazily. @context_overrides = context_overrides() # This construct ensures that initial import (triggered by instantiating # the structure 'known_resource_types') has a configured context # It cannot survive the initvars method, and is later reinstated # as part of compiling... # Puppet.override( @context_overrides , "For initializing compiler") do # THE MAGIC STARTS HERE ! This triggers parsing, loading etc. @catalog.version = known_resource_types.version end @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 settings_type = Puppet::Resource::Type.new :hostclass, "settings" environment.known_resource_types.add(settings_type) 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) env = environment Puppet.settings.each do |name, setting| next if name == :name scope[name.to_s] = env[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) @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/pops/binder/binder.rb b/lib/puppet/pops/binder/binder.rb index 54620db46..f5807fb5c 100644 --- a/lib/puppet/pops/binder/binder.rb +++ b/lib/puppet/pops/binder/binder.rb @@ -1,393 +1,393 @@ # The Binder is responsible for processing layered bindings that can be used to setup an Injector. # # 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 # @api private attr_reader :injector_entries # @api private attr :id_index # @api private attr_reader :key_factory # A parent Binder or nil # @api private attr_reader :parent # The next anonymous key to use # @api private attr_reader :anonymous_key # This binder's precedence # @api private attr_reader :binder_precedence # @api public 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 = {} 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 define_layers(layered_bindings) end # Binds layers from highest to lowest as defined by the given LayeredBindings. # @note # 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 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) LayerProcessor.new(self, key_factory).bind(layered_bindings) 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 # 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 = @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 ['', ''] 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 '' 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 :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 @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(b, binder_precedence) end # Add a multibind contribution # @api private # def add_contribution(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 precedence # @api private def highest(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 = 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) 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, [ 'Conflicting binding for', "'#{b1.binding.name}'", 'being resolved across layers', "'#{b1_layer_name}' and", "'#{b2_layer_name}'" ].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(' ')+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(' ')+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 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 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 + # All except abstract (==error) are transferred 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 (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/issues.rb b/lib/puppet/pops/issues.rb index ba53ddc0e..60af9ddce 100644 --- a/lib/puppet/pops/issues.rb +++ b/lib/puppet/pops/issues.rb @@ -1,548 +1,548 @@ # 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(it = nil) raise "Label provider key :label must be set to produce the text of the message!" unless @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 # parameters cannot have numeric names, clashes with match result variables ILLEGAL_NUMERIC_PARAMETER = issue :ILLEGAL_NUMERIC_PARAMETER, :name do "The numeric parameter name '$#{name}' 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 APPENDS_DELETES_NO_LONGER_SUPPORTED = hard_issue :APPENDS_DELETES_NO_LONGER_SUPPORTED, :operator do "The operator '#{operator}' is no longer supported. See http://links.puppetlabs.com/remove-plus-equals" end # For unsupported operators (e.g. += and -= in puppet 4). # UNSUPPORTED_OPERATOR = hard_issue :UNSUPPORTED_OPERATOR, :operator do "The operator '#{operator}' is not supported." end # For operators that are not supported in specific contexts (e.g. '* =>' in # resource defaults) # UNSUPPORTED_OPERATOR_IN_CONTEXT = hard_issue :UNSUPPORTED_OPERATOR_IN_CONTEXT, :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-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 a variable is not a NAME # 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 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 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 && max != min "#{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 EMPTY_RESOURCE_SPECIALIZATION = issue :EMPTY_RESOURCE_SPECIALIZATION do "Arguments to Resource[] are all empty/undefined" 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 - ILLEGAL_DEFINITION_NAME = hard_issue :ILLEGAL_DEFINTION_NAME, :name do + ILLEGAL_DEFINITION_NAME = hard_issue :ILLEGAL_DEFINITION_NAME, :name do "Unacceptable name. The name '#{name}' is unacceptable as the name of #{label.a_an(semantic)}" end CAPTURES_REST_NOT_LAST = hard_issue :CAPTURES_REST_NOT_LAST, :param_name do "Parameter $#{param_name} is not last, and has 'captures rest'" end CAPTURES_REST_NOT_SUPPORTED = hard_issue :CAPTURES_REST_NOT_SUPPORTED, :container, :param_name do "Parameter $#{param_name} has 'captures rest' - not supported in #{label.a_an(container)}" end REQUIRED_PARAMETER_AFTER_OPTIONAL = hard_issue :REQUIRED_PARAMETER_AFTER_OPTIONAL, :param_name do "Parameter $#{param_name} is required but appears after optional parameters" end MISSING_REQUIRED_PARAMETER = hard_issue :MISSING_REQUIRED_PARAMETER, :param_name do "Parameter $#{param_name} is required but no value was given" 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 ILLEGAL_RESOURCE_TYPE = hard_issue :ILLEGAL_RESOURCE_TYPE, :actual do "Illegal Resource Type expression, expected result to be a type name, or untitled Resource, got #{actual}" end DUPLICATE_TITLE = issue :DUPLICATE_TITLE, :title do "The title '#{title}' has already been used in this resource expression" end DUPLICATE_ATTRIBUTE = issue :DUPLICATE_ATTRIBUE, :attribute do "The attribute '#{attribute}' has already been set in this resource body" end MISSING_TITLE = hard_issue :MISSING_TITLE do "Missing title. The title expression resulted in undef" end MISSING_TITLE_AT = hard_issue :MISSING_TITLE_AT, :index do "Missing title at index #{index}. The title expression resulted in an undef title" end ILLEGAL_TITLE_TYPE_AT = hard_issue :ILLEGAL_TITLE_TYPE_AT, :index, :actual do "Illegal title type at index #{index}. Expected String, got #{actual}" end EMPTY_STRING_TITLE_AT = hard_issue :EMPTY_STRING_TITLE_AT, :index do "Empty string title at #{index}. Title strings must have a length greater than zero." 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 DISCONTINUED_IMPORT = hard_issue :DISCONTINUED_IMPORT do "Use of 'import' has been discontinued in favor of a manifest directory. See http://links.puppetlabs.com/puppet-import-deprecation" end IDEM_EXPRESSION_NOT_LAST = issue :IDEM_EXPRESSION_NOT_LAST do "This #{label.label(semantic)} is not productive. A non productive construct may only be placed last in a block/sequence" end IDEM_NOT_ALLOWED_LAST = hard_issue :IDEM_NOT_ALLOWED_LAST, :container do "This #{label.label(semantic)} is not productive. #{label.a_an_uc(container)} can not end with a non productive construct" end RESERVED_WORD = hard_issue :RESERVED_WORD, :word do "Use of reserved word: #{word}, must be quoted if intended to be a String value" end RESERVED_TYPE_NAME = hard_issue :RESERVED_TYPE_NAME, :name do "The name: '#{name}' is already defined by Puppet and can not be used as the name of #{label.a_an(semantic)}." end UNMATCHED_SELECTOR = hard_issue :UNMATCHED_SELECTOR, :param_value do "No matching entry for selector parameter with value '#{param_value}'" end ILLEGAL_NODE_INHERITANCE = issue :ILLEGAL_NODE_INHERITANCE do "Node inheritance is not supported in Puppet >= 4.0.0. See http://links.puppetlabs.com/puppet-node-inheritance-deprecation" end ILLEGAL_OVERRIDEN_TYPE = issue :ILLEGAL_OVERRIDEN_TYPE, :actual do "Resource Override can only operate on resources, got: #{label.label(actual)}" end RESERVED_PARAMETER = hard_issue :RESERVED_PARAMETER, :container, :param_name do "The parameter $#{param_name} redefines a built in parameter in #{label.the(container)}" end TYPE_MISMATCH = hard_issue :TYPE_MISMATCH, :expected, :actual do "Expected value of type #{expected}, got #{actual}" end MULTIPLE_ATTRIBUTES_UNFOLD = hard_issue :MULTIPLE_ATTRIBUTES_UNFOLD do "Unfolding of attributes from Hash can only be used once per resource body" end end diff --git a/lib/puppet/pops/model/model_meta.rb b/lib/puppet/pops/model/model_meta.rb index 788b6949d..6f9cc4877 100644 --- a/lib/puppet/pops/model/model_meta.rb +++ b/lib/puppet/pops/model/model_meta.rb @@ -1,582 +1,582 @@ # # 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. # require 'rgen/metamodel_builder' module Puppet::Pops::Model extend RGen::MetamodelBuilder::ModuleExtension # A base class for modeled objects that makes them Visitable, and Adaptable. # class PopsObject < RGen::MetamodelBuilder::MMBase 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 < 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 # 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 # Unfolds an array (a.k.a 'splat') class UnfoldExpression < UnaryExpression; end OpAssignment = RGen::MetamodelBuilder::DataTypes::Enum.new( :literals => [:'=', :'+=', :'-='], :name => 'OpAssignment') # An assignment expression assigns a value to the lval() of the left_expr. # class AssignmentExpression < BinaryExpression has_attr 'operator', OpAssignment, :lowerBound => 1 end OpArithmetic = RGen::MetamodelBuilder::DataTypes::Enum.new( :literals => [:'+', :'-', :'*', :'%', :'/', :'<<', :'>>' ], :name => 'OpArithmetic') # An arithmetic expression applies an arithmetic operator on left and right expressions. # class ArithmeticExpression < BinaryExpression has_attr 'operator', OpArithmetic, :lowerBound => 1 end OpRelationship = RGen::MetamodelBuilder::DataTypes::Enum.new( :literals => [:'->', :'<-', :'~>', :'<~'], :name => 'OpRelationship') # A relationship expression associates the left and right expressions # class RelationshipExpression < BinaryExpression has_attr 'operator', OpRelationship, :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 OpComparison = RGen::MetamodelBuilder::DataTypes::Enum.new( :literals => [:'==', :'!=', :'<', :'>', :'<=', :'>=' ], :name => 'OpComparison') # A comparison expression compares left and right using a comparison operator. # class ComparisonExpression < BinaryExpression has_attr 'operator', OpComparison, :lowerBound => 1 end OpMatch = RGen::MetamodelBuilder::DataTypes::Enum.new( :literals => [:'!~', :'=~'], :name => 'OpMatch') # A match expression matches left and right using a matching operator. # class MatchExpression < BinaryExpression has_attr 'operator', OpMatch, :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 is typically used as an entry in a Hash. # 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 OpAttribute = RGen::MetamodelBuilder::DataTypes::Enum.new( :literals => [:'=>', :'+>', ], :name => 'OpAttribute') class AbstractAttributeOperation < Positioned end # An attribute operation sets or appends a value to a named attribute. # class AttributeOperation < AbstractAttributeOperation has_attr 'attribute_name', String, :lowerBound => 1 has_attr 'operator', OpAttribute, :lowerBound => 1 contains_one_uni 'value_expr', Expression, :lowerBound => 1 end # An attribute operation containing an expression that must evaluate to a Hash # class AttributesOperation < AbstractAttributeOperation contains_one_uni 'expr', Expression, :lowerBound => 1 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 < Positioned has_attr 'name', String, :lowerBound => 1 contains_one_uni 'value', Expression contains_one_uni 'type_expr', Expression, :lowerBound => 0 has_attr 'captures_rest', Boolean end # Abstract base class for definitions. # class Definition < Expression abstract end # 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 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 < 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 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 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 < 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 end # A Regular Expression Literal. # class LiteralRegularExpression < LiteralValue has_attr 'value', Object, :lowerBound => 1, :transient => true has_attr 'pattern', String, :lowerBound => 1 end # A Literal String # class LiteralString < LiteralValue has_attr 'value', String, :lowerBound => 1 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 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 has_attr 'value', Boolean, :lowerBound => 1 end # A text expression is an interpolation of an expression. If the embedded expression is # 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 has_attr 'value', String, :lowerBound => 1 end # Represents a parsed reserved word class ReservedWord < LiteralValue has_attr 'word', 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 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 # Epp start class EppExpression < Expression # EPP can be specified without giving any parameter specification. # However, the parameters of the lambda in that case are the empty - # array, which is the same as when the parameters are explicity + # array, which is the same as when the parameters are explicitly # specified as empty. This attribute tracks that difference. has_attr 'parameters_specified', Boolean contains_one_uni 'body', Expression end # 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 < Positioned contains_one_uni 'title', Expression contains_many_uni 'operations', AbstractAttributeOperation end ResourceFormEnum = RGen::MetamodelBuilder::DataTypes::Enum.new( :literals => [:regular, :virtual, :exported ], :name => 'ResourceFormEnum') # 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', ResourceFormEnum, :lowerBound => 1, :defaultValueLiteral => "regular" has_attr 'virtual', Boolean, :derived => true has_attr 'exported', Boolean, :derived => true 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', Expression contains_many_uni 'operations', AbstractAttributeOperation end # A resource override overrides already set values. # class ResourceOverrideExpression < AbstractResource contains_one_uni 'resources', Expression, :lowerBound => 1 contains_many_uni 'operations', AbstractAttributeOperation end # A selector entry describes a map from matching_expr to value_expr. # 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 # A named access expression looks up a named part. (e.g. $a.b) # class NamedAccessExpression < BinaryExpression; end # 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 measured # in bytes or characters (as given by the attribute char_offsets). The `char_offsets` setting # applies to all offsets recorded in the mode (not just the line_offsets). # # A model that will be shared across different platforms should use char_offsets true as the byte # offsets are platform and encoding dependent. # 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 'char_offsets', Boolean, :defaultValueLiteral => 'false' has_attr 'locator', Object, :lowerBound => 1, :transient => true end end diff --git a/lib/puppet/provider/package/appdmg.rb b/lib/puppet/provider/package/appdmg.rb index 83915375f..89ba7b5ee 100644 --- a/lib/puppet/provider/package/appdmg.rb +++ b/lib/puppet/provider/package/appdmg.rb @@ -1,109 +1,109 @@ # Jeff McCune # Changed to app.dmg by: Udo Waechter # 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_ 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) 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", "-", "-L", "-s", "--url", source - Puppet.debug "Success: curl transfered [#{name}]" + Puppet.debug "Success: curl transferred [#{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 found_app = false mounts.each do |fspath| Dir.entries(fspath).select { |f| f =~ /\.app$/i }.each do |pkg| found_app = true installapp("#{fspath}/#{pkg}", name, source) end end Puppet.debug "Unable to find .app in .appdmg. #{name} will not be installed." if !found_app ensure hdiutil "eject", mounts[0] end end ensure FileUtils.remove_entry_secure(tmpdir, true) end end def query 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/apt.rb b/lib/puppet/provider/package/apt.rb index f43f51446..e5b356fc3 100644 --- a/lib/puppet/provider/package/apt.rb +++ b/lib/puppet/provider/package/apt.rb @@ -1,116 +1,116 @@ Puppet::Type.type(:package).provide :apt, :parent => :dpkg, :source => :dpkg do # Provide sorting functionality include Puppet::Util::Package desc "Package management via `apt-get`. This provider supports the `install_options` attribute, which allows command-line flags to be passed to apt-get. These options should be specified as a string (e.g. '--flag'), a hash (e.g. {'--flag' => 'value'}), or an array where each element is either a string or a hash." has_feature :versionable, :install_options 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_options if @resource[:install_options] 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.exist?(response) self.info("Preseeding #{response} to debconf-set-selections") preseed response else - self.info "No responsefile specified or non existant, not preseeding anything" + self.info "No responsefile specified or non existent, 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 def install_options join_options(@resource[:install_options]) end end diff --git a/lib/puppet/provider/package/fink.rb b/lib/puppet/provider/package/fink.rb index 6b495171b..655946004 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.exist?(response) self.info("Preseeding #{response} to debconf-set-selections") preseed response else - self.info "No responsefile specified or non existant, not preseeding anything" + self.info "No responsefile specified or non existent, 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/pkgdmg.rb b/lib/puppet/provider/package/pkgdmg.rb index 8932a7ce1..9cb530a0e 100644 --- a/lib/puppet/provider/package/pkgdmg.rb +++ b/lib/puppet/provider/package/pkgdmg.rb @@ -1,149 +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_ 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 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' # 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", "-", "-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(" ")})" + Puppet.debug "Success: curl transferred [#{name}] (via: curl #{args.join(" ")})" rescue Puppet::ExecutionFailure 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.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/ssh_authorized_key/parsed.rb b/lib/puppet/provider/ssh_authorized_key/parsed.rb index a1c9ad4c7..76653b32b 100644 --- a/lib/puppet/provider/ssh_authorized_key/parsed.rb +++ b/lib/puppet/provider/ssh_authorized_key/parsed.rb @@ -1,105 +1,105 @@ 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 => Puppet::Type.type(:ssh_authorized_key).keyline_regex, :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| # if this name was generated, don't write it back to disk h[:name] = "" if h[:unnamed] 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.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. + # so calling it here suppresses 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.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 def self.prefetch_hook(records) name_index = 0 records.each do |record| if record[:record_type] == :parsed && record[:name].empty? record[:unnamed] = true # Generate a unique ID for unnamed keys, in case they need purging. # If you change this, you have to keep # Puppet::Type::User#unknown_keys_in_file in sync! (PUP-3357) record[:name] = "#{record[:target]}:unnamed-#{ name_index += 1 }" Puppet.debug("generating name for on-disk ssh_authorized_key #{record[:key]}: #{record[:name]}") end end end end diff --git a/lib/puppet/resource.rb b/lib/puppet/resource.rb index 6733a5fff..31add98c0 100644 --- a/lib/puppet/resource.rb +++ b/lib/puppet/resource.rb @@ -1,603 +1,603 @@ require 'puppet' require 'puppet/util/tagging' 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 include Puppet::Util::Tagging 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_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 = data['parameters'] params.each { |param, value| resource[param] = value } end if tags = data['tags'] tags.each { |tag| resource.tag(tag) } end ATTRIBUTES.each do |a| if value = data[a.to_s] resource.send(a.to_s + "=", value) end end resource 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 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] 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 # 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 %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 # Construct a resource from data. # # Constructs a resource instance with the given `type` and `title`. Multiple # type signatures are possible for these arguments and most will result in an # expensive call to {Puppet::Node::Environment#known_resource_types} in order # to resolve `String` and `Symbol` Types to actual Ruby classes. # # @param type [Symbol, String] The name of the Puppet Type, as a string or # symbol. The actual Type will be looked up using # {Puppet::Node::Environment#known_resource_types}. This lookup is expensive. # @param type [String] The full resource name in the form of # `"Type[Title]"`. This method of calling should only be used when # `title` is `nil`. # @param type [nil] If a `nil` is passed, the title argument must be a string # of the form `"Type[Title]"`. # @param type [Class] A class that inherits from `Puppet::Type`. This method # of construction is much more efficient as it skips calls to # {Puppet::Node::Environment#known_resource_types}. # # @param title [String, :main, nil] The title of the resource. If type is `nil`, may also # be the full resource name in the form of `"Type[Title]"`. # # @api public def initialize(type, title = nil, attributes = {}) @parameters = {} if type.is_a?(Class) && type < Puppet::Type # Set the resource type to avoid an expensive `known_resource_types` # lookup. self.resource_type = type # From this point on, the constructor behaves the same as if `type` had # been passed as a symbol. type = type.name end # 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 if resource_type && resource_type.respond_to?(:deprecate_params) resource_type.deprecate_params(title, attributes[:parameters]) end tag(self.type) tag(self.title) if valid_tag?(self.title) 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"; environment.known_resource_types.hostclass(title == :main ? "" : title) when "Node"; environment.known_resource_types.node(title) else 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 ||= if catalog catalog.environment_instance else Puppet.lookup(:current_environment) { Puppet::Node::Environment::NONE } end 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 + # Temporary kludge to deal with inconsistent 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 yaml for Hiera purposes. def to_hierayaml # 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:\n%s" % [self.title, attributes] 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 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| the_param = parameters[param.to_sym] the_param.nil? || the_param.value.nil? || the_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_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) 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) 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 Puppet[:parser] == 'current' # 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. # # This behavior is not done in the future parser, but we can't issue a # deprecation warning either since there isn't anything that a user can # do about it. result[p] = if v.is_a?(Array) and v.length == 1 v[0] else v end else result[p] = v end end 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 # Perform optional type checking if Puppet[:parser] == 'future' # Perform type checking arg_types = resource_type.argument_types # Parameters is a map from name, to parameter, and the parameter again has name and value parameters.each do |name, value| next unless t = arg_types[name.to_s] # untyped, and parameters are symbols here (aargh, strings in the type) unless Puppet::Pops::Types::TypeCalculator.instance?(t, value.value) inferred_type = Puppet::Pops::Types::TypeCalculator.infer(value.value) actual = Puppet::Pops::Types::TypeCalculator.generalize!(inferred_type) fail Puppet::ParseError, "Expected parameter '#{name}' of '#{self}' to have type #{t.to_s}, got #{actual.to_s}" end end 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 (argtype.nil? || argtype == :component || argtype == :whit) && argtitle =~ /^([^\[\]]+)\[(.+)\]$/m then [ $1, $2 ] elsif argtitle.nil? && 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/type/exec.rb b/lib/puppet/type/exec.rb index 325219bf4..cfe00bb66 100644 --- a/lib/puppet/type/exec.rb +++ b/lib/puppet/type/exec.rb @@ -1,592 +1,592 @@ 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 exit code(s). An error will be returned if the executed command has some other exit code. Defaults to 0. Can be specified as an array of acceptable exit codes or a single value. On POSIX systems, exit codes are always integers between 0 and 255. On Windows, **most** exit codes should be integers between 0 and 2147483647. Larger exit codes on Windows can behave inconsistently across different tools. The Win32 APIs define exit codes as 32-bit unsigned integers, but both the cmd.exe shell and the .NET runtime cast them to signed integers. This means some tools will report negative numbers for exit codes above 2147483647. (For example, cmd.exe reports 4294967295 as -1.) Since Puppet uses the plain Win32 APIs, it will report the very large number instead of the negative number, which might not be what you expect if you got the exit code from a cmd.exe session. Microsoft recommends against using negative/very large exit codes, and you should avoid them when possible. To convert a negative exit code to the positive one Puppet will use, add it to 4294967296." # 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 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." validate do |user| if Puppet.features.microsoft_windows? self.fail "Unable to execute commands as other users on Windows" elsif !Puppet.features.root? && resource.current_username() != user self.fail "Only root can execute commands as other users" end 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. 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.", $!.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 + Note that the timeout parameter 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.exist?(value) end end newcheck(:unless) do desc <<-'EOT' If this parameter is set, then this `exec` will run unless the command has an exit code of 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. It also uses the same provider as the main command, so any behavior that differs by provider will match. 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 has an exit code of 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. It also uses the same provider as the main command, so any behavior that differs by provider will match. 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 def current_username Etc.getpwuid(Process.uid).name end end end diff --git a/lib/puppet/type/file/selcontext.rb b/lib/puppet/type/file/selcontext.rb index 580dbbf28..f9d8ba152 100644 --- a/lib/puppet/type/file/selcontext.rb +++ b/lib/puppet/type/file/selcontext.rb @@ -1,124 +1,124 @@ # Manage SELinux context of files. # # This code actually manages three pieces of data in the context. # # [root@delenn files]# ls -dZ / # drwxr-xr-x root root system_u:object_r:root_t / # # The context of '/' here is 'system_u:object_r:root_t'. This is -# three seperate fields: +# three separate fields: # # system_u is the user context # object_r is the role context # root_t is the type context # # All three of these fields are returned in a single string by the # output of the stat command, but set individually with the chcon # command. This allows the user to specify a subset of the three # values while leaving the others alone. # # See http://www.nsa.gov/selinux/ for complete docs on SELinux. module Puppet require 'puppet/util/selinux' class SELFileContext < Puppet::Property include Puppet::Util::SELinux def retrieve return :absent unless @resource.stat context = self.get_selinux_current_context(@resource[:path]) parse_selinux_context(name, context) end def retrieve_default_context(property) if @resource[:selinux_ignore_defaults] == :true return nil end unless context = self.get_selinux_default_context(@resource[:path]) return nil end property_default = self.parse_selinux_context(property, context) self.debug "Found #{property} default '#{property_default}' for #{@resource[:path]}" if not property_default.nil? property_default end def insync?(value) if not selinux_support? debug("SELinux bindings not found. Ignoring parameter.") true elsif not selinux_label_support?(@resource[:path]) debug("SELinux not available for this filesystem. Ignoring parameter.") true else super end end def sync self.set_selinux_context(@resource[:path], @should, name) :file_changed end end Puppet::Type.type(:file).newparam(:selinux_ignore_defaults) do desc "If this is set then Puppet will not ask SELinux (via matchpathcon) to supply defaults for the SELinux attributes (seluser, selrole, seltype, and selrange). In general, you should leave this set at its default and only set it to true when you need Puppet to not try to fix SELinux labels automatically." newvalues(:true, :false) defaultto :false end Puppet::Type.type(:file).newproperty(:seluser, :parent => Puppet::SELFileContext) do desc "What the SELinux user component of the context of the file should be. Any valid SELinux user component is accepted. For example `user_u`. If not specified it defaults to the value returned by matchpathcon for the file, if any exists. Only valid on systems with SELinux support enabled." @event = :file_changed defaultto { self.retrieve_default_context(:seluser) } end Puppet::Type.type(:file).newproperty(:selrole, :parent => Puppet::SELFileContext) do desc "What the SELinux role component of the context of the file should be. Any valid SELinux role component is accepted. For example `role_r`. If not specified it defaults to the value returned by matchpathcon for the file, if any exists. Only valid on systems with SELinux support enabled." @event = :file_changed defaultto { self.retrieve_default_context(:selrole) } end Puppet::Type.type(:file).newproperty(:seltype, :parent => Puppet::SELFileContext) do desc "What the SELinux type component of the context of the file should be. Any valid SELinux type component is accepted. For example `tmp_t`. If not specified it defaults to the value returned by matchpathcon for the file, if any exists. Only valid on systems with SELinux support enabled." @event = :file_changed defaultto { self.retrieve_default_context(:seltype) } end Puppet::Type.type(:file).newproperty(:selrange, :parent => Puppet::SELFileContext) do desc "What the SELinux range component of the context of the file should be. Any valid SELinux range component is accepted. For example `s0` or `SystemHigh`. If not specified it defaults to the value returned by matchpathcon for the file, if any exists. Only valid on systems with SELinux support enabled and that have support for MCS (Multi-Category Security)." @event = :file_changed defaultto { self.retrieve_default_context(:selrange) } end end diff --git a/lib/puppet/type/selboolean.rb b/lib/puppet/type/selboolean.rb index eb30742a5..c02e7e1b9 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 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. + desc "If set true, SELinux booleans will be written to disk and persist across reboots. The default is `false`." defaultto :false newvalues(:true, :false) end end end diff --git a/lib/puppet/util/rdoc.rb b/lib/puppet/util/rdoc.rb index c2b9c15f4..497e9847a 100644 --- a/lib/puppet/util/rdoc.rb +++ b/lib/puppet/util/rdoc.rb @@ -1,96 +1,96 @@ 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 # Rdoc root default is Dir.pwd, but the win32-dir gem monkey patchs Dir.pwd # replacing Ruby's normal / with \. When RDoc generates relative paths it # uses relative_path_from that will generate errors when the slashes don't # properly match. This is a workaround for that issue. if Puppet.features.microsoft_windows? && RDoc::VERSION !~ /^[0-3]\./ options += [ "--root", Dir.pwd.gsub(/\\/, '/')] end 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.lookup(:current_environment)) parser.file = f ast = parser.parse output(f, ast) end end - # Ouputs to the console the documentation + # Outputs 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/selinux.rb b/lib/puppet/util/selinux.rb index feb5c3266..a28e1113f 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" + raise ArgumentError, "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', '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.lstat(path) end private :file_lstat end diff --git a/lib/puppet/util/windows/file.rb b/lib/puppet/util/windows/file.rb index b7c0cb4ff..51557e97c 100644 --- a/lib/puppet/util/windows/file.rb +++ b/lib/puppet/util/windows/file.rb @@ -1,395 +1,395 @@ require 'puppet/util/windows' module Puppet::Util::Windows::File require 'ffi' extend FFI::Library extend Puppet::Util::Windows::String FILE_ATTRIBUTE_READONLY = 0x00000001 SYNCHRONIZE = 0x100000 STANDARD_RIGHTS_REQUIRED = 0xf0000 STANDARD_RIGHTS_READ = 0x20000 STANDARD_RIGHTS_WRITE = 0x20000 STANDARD_RIGHTS_EXECUTE = 0x20000 STANDARD_RIGHTS_ALL = 0x1F0000 SPECIFIC_RIGHTS_ALL = 0xFFFF FILE_READ_DATA = 1 FILE_WRITE_DATA = 2 FILE_APPEND_DATA = 4 FILE_READ_EA = 8 FILE_WRITE_EA = 16 FILE_EXECUTE = 32 FILE_DELETE_CHILD = 64 FILE_READ_ATTRIBUTES = 128 FILE_WRITE_ATTRIBUTES = 256 FILE_ALL_ACCESS = STANDARD_RIGHTS_REQUIRED | SYNCHRONIZE | 0x1FF FILE_GENERIC_READ = STANDARD_RIGHTS_READ | FILE_READ_DATA | FILE_READ_ATTRIBUTES | FILE_READ_EA | SYNCHRONIZE FILE_GENERIC_WRITE = STANDARD_RIGHTS_WRITE | FILE_WRITE_DATA | FILE_WRITE_ATTRIBUTES | FILE_WRITE_EA | FILE_APPEND_DATA | SYNCHRONIZE FILE_GENERIC_EXECUTE = STANDARD_RIGHTS_EXECUTE | FILE_READ_ATTRIBUTES | FILE_EXECUTE | SYNCHRONIZE def replace_file(target, source) target_encoded = wide_string(target.to_s) source_encoded = wide_string(source.to_s) flags = 0x1 backup_file = nil result = ReplaceFileW( target_encoded, source_encoded, backup_file, flags, FFI::Pointer::NULL, FFI::Pointer::NULL ) return true if result != FFI::WIN32_FALSE raise Puppet::Util::Windows::Error.new("ReplaceFile(#{target}, #{source})") end module_function :replace_file def move_file_ex(source, target, flags = 0) result = MoveFileExW(wide_string(source.to_s), wide_string(target.to_s), flags) return true if result != FFI::WIN32_FALSE raise Puppet::Util::Windows::Error. new("MoveFileEx(#{source}, #{target}, #{flags.to_s(8)})") end module_function :move_file_ex def symlink(target, symlink) flags = File.directory?(target) ? 0x1 : 0x0 result = CreateSymbolicLinkW(wide_string(symlink.to_s), wide_string(target.to_s), flags) return true if result != FFI::WIN32_FALSE 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 get_attributes(file_name) result = GetFileAttributesW(wide_string(file_name.to_s)) return result unless result == INVALID_FILE_ATTRIBUTES raise Puppet::Util::Windows::Error.new("GetFileAttributes(#{file_name})") end module_function :get_attributes def add_attributes(path, flags) oldattrs = get_attributes(path) if (oldattrs | flags) != oldattrs set_attributes(path, oldattrs | flags) end end module_function :add_attributes def remove_attributes(path, flags) oldattrs = get_attributes(path) if (oldattrs & ~flags) != oldattrs set_attributes(path, oldattrs & ~flags) end end module_function :remove_attributes def set_attributes(path, flags) success = SetFileAttributesW(wide_string(path), flags) != FFI::WIN32_FALSE raise Puppet::Util::Windows::Error.new("Failed to set file attributes") if !success success end module_function :set_attributes #define INVALID_HANDLE_VALUE ((HANDLE)(LONG_PTR)-1) INVALID_HANDLE_VALUE = FFI::Pointer.new(-1).address def self.create_file(file_name, desired_access, share_mode, security_attributes, creation_disposition, flags_and_attributes, template_file_handle) result = CreateFileW(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.get_reparse_point_data(handle, &block) # must be multiple of 1024, min 10240 FFI::MemoryPointer.new(REPARSE_DATA_BUFFER.size) do |reparse_data_buffer_ptr| device_io_control(handle, FSCTL_GET_REPARSE_POINT, nil, reparse_data_buffer_ptr) yield REPARSE_DATA_BUFFER.new(reparse_data_buffer_ptr) end # underlying struct MemoryPointer has been cleaned up by this point, nothing to return nil 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 FFI::MemoryPointer.new(:dword, 1) do |bytes_returned_ptr| result = DeviceIoControl( handle, io_control_code, in_buffer, in_buffer.nil? ? 0 : in_buffer.size, out_buffer, out_buffer.size, bytes_returned_ptr, nil ) if result == FFI::WIN32_FALSE raise Puppet::Util::Windows::Error.new( "DeviceIoControl(#{handle}, #{io_control_code}, " + "#{in_buffer}, #{in_buffer ? in_buffer.size : ''}, " + "#{out_buffer}, #{out_buffer ? out_buffer.size : ''}") end end out_buffer end FILE_ATTRIBUTE_REPARSE_POINT = 0x400 def symlink?(file_name) begin attributes = get_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 GENERIC_WRITE = 0x40000000 GENERIC_EXECUTE = 0x20000000 GENERIC_ALL = 0x10000000 FILE_SHARE_READ = 1 FILE_SHARE_WRITE = 2 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( link_name, GENERIC_READ, FILE_SHARE_READ, nil, # security_attributes OPEN_EXISTING, FILE_FLAG_OPEN_REPARSE_POINT | FILE_FLAG_BACKUP_SEMANTICS, 0) # template_file ensure FFI::WIN32.CloseHandle(handle) if handle end # handle has had CloseHandle called against it, so nothing to return nil end def readlink(link_name) link = nil open_symlink(link_name) do |handle| link = resolve_symlink(handle) end link end module_function :readlink def stat(file_name) - file_name = file_name.to_s # accomodate PathName or String + file_name = file_name.to_s # accommodate PathName or String stat = File.stat(file_name) singleton_class = class << stat; self; end target_path = file_name if symlink?(file_name) 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.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 + file_name = file_name.to_s # accommodate 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) path = nil get_reparse_point_data(handle) do |reparse_data| offset = reparse_data[:PrintNameOffset] length = reparse_data[:PrintNameLength] ptr = reparse_data.pointer + reparse_data.offset_of(:PathBuffer) + offset path = ptr.read_wide_string(length / 2) # length is bytes, need UTF-16 wchars end path end 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 # ); ffi_lib :kernel32 attach_function_private :ReplaceFileW, [:lpcwstr, :lpcwstr, :lpcwstr, :dword, :lpvoid, :lpvoid], :win32_bool # http://msdn.microsoft.com/en-us/library/windows/desktop/aa365240(v=vs.85).aspx # BOOL WINAPI MoveFileEx( # _In_ LPCTSTR lpExistingFileName, # _In_opt_ LPCTSTR lpNewFileName, # _In_ DWORD dwFlags # ); ffi_lib :kernel32 attach_function_private :MoveFileExW, [:lpcwstr, :lpcwstr, :dword], :win32_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 ffi_lib :kernel32 attach_function_private :CreateSymbolicLinkW, [:lpwstr, :lpwstr, :dword], :win32_bool rescue LoadError end # http://msdn.microsoft.com/en-us/library/windows/desktop/aa364944(v=vs.85).aspx # DWORD WINAPI GetFileAttributes( # _In_ LPCTSTR lpFileName # ); ffi_lib :kernel32 attach_function_private :GetFileAttributesW, [:lpcwstr], :dword # http://msdn.microsoft.com/en-us/library/windows/desktop/aa365535(v=vs.85).aspx # BOOL WINAPI SetFileAttributes( # _In_ LPCTSTR lpFileName, # _In_ DWORD dwFileAttributes # ); ffi_lib :kernel32 attach_function_private :SetFileAttributesW, [:lpcwstr, :dword], :win32_bool # 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 # ); ffi_lib :kernel32 attach_function_private :CreateFileW, [:lpcwstr, :dword, :dword, :pointer, :dword, :dword, :handle], :handle # 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 # ); ffi_lib :kernel32 attach_function_private :DeviceIoControl, [:handle, :dword, :lpvoid, :dword, :lpvoid, :dword, :lpdword, :pointer], :win32_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 REPARSE_DATA_BUFFER < FFI::Struct layout :ReparseTag, :win32_ulong, :ReparseDataLength, :ushort, :Reserved, :ushort, :SubstituteNameOffset, :ushort, :SubstituteNameLength, :ushort, :PrintNameOffset, :ushort, :PrintNameLength, :ushort, :Flags, :win32_ulong, # max less above fields dword / uint 4 bytes, ushort 2 bytes # technically a WCHAR buffer, but we care about size in bytes here :PathBuffer, [:byte, MAXIMUM_REPARSE_DATA_BUFFER_SIZE - 20] end end diff --git a/lib/puppet/util/zaml.rb b/lib/puppet/util/zaml.rb index abd36a033..510910241 100644 --- a/lib/puppet/util/zaml.rb +++ b/lib/puppet/util/zaml.rb @@ -1,419 +1,419 @@ # encoding: UTF-8 # # The above encoding line is a magic comment to set the default source encoding # of this file for the Ruby interpreter. It must be on the first or second # line of the file if an interpreter is in use. In Ruby 1.9 and later, the # source encoding determines the encoding of String and Regexp objects created -# from this source file. This explicit encoding is important becuase otherwise +# from this source file. This explicit encoding is important because otherwise # Ruby will pick an encoding based on LANG or LC_CTYPE environment variables. # These may be different from site to site so it's important for us to # establish a consistent behavior. For more information on M17n please see: # http://links.puppetlabs.com/understanding_m17n # ZAML -- A partial replacement for YAML, writen with speed and code clarity # in mind. ZAML fixes one YAML bug (loading Exceptions) and provides # a replacement for YAML.dump unimaginatively called ZAML.dump, # which is faster on all known cases and an order of magnitude faster # with complex structures. # # http://github.com/hallettj/zaml # # ## License (from upstream) # # Copyright (c) 2008-2009 ZAML contributers # # This program is dual-licensed under the GNU General Public License # version 3 or later and under the Apache License, version 2.0. # # This program is free software: you can redistribute it and/or modify it # under the terms of the GNU General Public License as published by the # Free Software Foundation, either version 3 of the License, or (at your # option) any later version; or under the terms of the Apache License, # Version 2.0. # # This program is distributed in the hope that it will be useful, but # WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # General Public License and the Apache License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see # . # # You may obtain a copy of the Apache License at # . require 'yaml' class ZAML VERSION = "0.1.3" # # Class Methods # def self.dump(stuff, where='') z = new stuff.to_zaml(z) where << z.to_s end # # Instance Methods # def initialize @result = [] @indent = nil @structured_key_prefix = nil @previously_emitted_object = {} @next_free_label_number = 0 emit('--- ') end def nested(tail=' ') old_indent = @indent @indent = "#{@indent || "\n"}#{tail}" yield @indent = old_indent end class Label # # YAML only wants objects in the datastream once; if the same object # occurs more than once, we need to emit a label ("&idxxx") on the # first occurrence and then emit a back reference (*idxxx") on any # subsequent occurrence(s). # # To accomplish this we keeps a hash (by object id) of the labels of # the things we serialize as we begin to serialize them. The labels # initially serialize as an empty string (since most objects are only # going to be be encountered once), but can be changed to a valid # (by assigning it a number) the first time it is subsequently used, # if it ever is. Note that we need to do the label setup BEFORE we # start to serialize the object so that circular structures (in # which we will encounter a reference to the object as we serialize # it can be handled). # attr_accessor :this_label_number def initialize(obj,indent) @indent = indent @this_label_number = nil @obj = obj # prevent garbage collection so that object id isn't reused end def to_s @this_label_number ? ('&id%03d%s' % [@this_label_number, @indent]) : '' end def reference @reference ||= '*id%03d' % @this_label_number end end def label_for(obj) @previously_emitted_object[obj.object_id] end def new_label_for(obj) label = Label.new(obj,(Hash === obj || Array === obj) ? "#{@indent || "\n"} " : ' ') @previously_emitted_object[obj.object_id] = label label end def first_time_only(obj) if label = label_for(obj) label.this_label_number ||= (@next_free_label_number += 1) emit(label.reference) else with_structured_prefix(obj) do emit(new_label_for(obj)) yield end end end def with_structured_prefix(obj) if @structured_key_prefix unless obj.is_a?(String) and obj !~ /\n/ emit(@structured_key_prefix) @structured_key_prefix = nil end end yield end def emit(s) @result << s @recent_nl = false unless s.kind_of?(Label) end def nl(s = nil) emit(@indent || "\n") unless @recent_nl emit(s) if s @recent_nl = true end def to_s @result.join end def prefix_structured_keys(x) @structured_key_prefix = x yield nl unless @structured_key_prefix @structured_key_prefix = nil end end ################################################################ # # Behavior for custom classes # ################################################################ class Object # Users of this method need to do set math consistently with the # result. Since #instance_variables returns strings in 1.8 and symbols # on 1.9, standardize on symbols if RUBY_VERSION[0,3] == '1.8' def to_yaml_properties instance_variables.map(&:to_sym) end else def to_yaml_properties instance_variables end end def yaml_property_munge(x) x end def zamlized_class_name(root) cls = self.class "!ruby/#{root.name.downcase}#{cls == root ? '' : ":#{cls.respond_to?(:name) ? cls.name : cls}"}" end def to_zaml(z) z.first_time_only(self) { z.emit(zamlized_class_name(Object)) z.nested { instance_variables = to_yaml_properties if instance_variables.empty? z.emit(" {}") else instance_variables.each { |v| z.nl v.to_s[1..-1].to_zaml(z) # Remove leading '@' z.emit(': ') yaml_property_munge(instance_variable_get(v)).to_zaml(z) } end } } end end ################################################################ # # Behavior for built-in classes # ################################################################ class NilClass def to_zaml(z) z.emit('') # NOTE: blank turns into nil in YAML.load end end class Symbol def to_zaml(z) z.emit("!ruby/sym ") to_s.to_zaml(z) end end class TrueClass def to_zaml(z) z.emit('true') end end class FalseClass def to_zaml(z) z.emit('false') end end class Numeric def to_zaml(z) z.emit(self) end end class Regexp def to_zaml(z) z.first_time_only(self) { z.emit("#{zamlized_class_name(Regexp)} #{inspect}") } end end class Exception def to_zaml(z) z.emit(zamlized_class_name(Exception)) z.nested { z.nl("message: ") message.to_zaml(z) } end # # Monkey patch for buggy Exception restore in YAML # # This makes it work for now but is not very future-proof; if things # change we'll most likely want to remove this. To mitigate the risks # as much as possible, we test for the bug before appling the patch. # if respond_to? :yaml_new and yaml_new(self, :tag, "message" => "blurp").message != "blurp" def self.yaml_new( klass, tag, val ) o = YAML.object_maker( klass, {} ).exception(val.delete( 'message')) val.each_pair do |k,v| o.instance_variable_set("@#{k}", v) end o end end end class String ZAML_ESCAPES = { "\a" => "\\a", "\e" => "\\e", "\f" => "\\f", "\n" => "\\n", "\r" => "\\r", "\t" => "\\t", "\v" => "\\v" } def to_zaml(z) z.with_structured_prefix(self) do case when self == '' z.emit('""') when self.to_ascii8bit !~ /\A(?: # ?: non-capturing group (grouping with no back references) [\x09\x0A\x0D\x20-\x7E] # ASCII | [\xC2-\xDF][\x80-\xBF] # non-overlong 2-byte | \xE0[\xA0-\xBF][\x80-\xBF] # excluding overlongs | [\xE1-\xEC\xEE\xEF][\x80-\xBF]{2} # straight 3-byte | \xED[\x80-\x9F][\x80-\xBF] # excluding surrogates | \xF0[\x90-\xBF][\x80-\xBF]{2} # planes 1-3 | [\xF1-\xF3][\x80-\xBF]{3} # planes 4-15 | \xF4[\x80-\x8F][\x80-\xBF]{2} # plane 16 )*\z/mnx # Emit the binary tag, then recurse. Ruby splits BASE64 output at the 60 # character mark when packing strings, and we can wind up a multi-line # string here. We could reimplement the multi-line string logic, # but why would we - this does just as well for producing solid output. z.emit("!binary ") [self].pack("m*").to_zaml(z) # Only legal UTF-8 characters can make it this far, so we are safe # against emitting something dubious. That means we don't need to mess # about, just emit them directly. --daniel 2012-07-14 when ((self =~ /\A[a-zA-Z\/][-\[\]_\/.a-zA-Z0-9]*\z/) and (self !~ /^(?:true|false|yes|no|on|null|off)$/i)) # simple string literal, safe to emit unquoted. z.emit(self) when (self =~ /\n/ and self !~ /\A\s/ and self !~ /\s\z/) # embedded newline, split line-wise in quoted string block form. if self[-1..-1] == "\n" then z.emit('|+') else z.emit('|-') end z.nested { split("\n",-1).each { |line| z.nl; z.emit(line) } } else # ...though we still have to escape unsafe characters. escaped = gsub(/[\\"\x00-\x1F]/) do |c| ZAML_ESCAPES[c] || "\\x#{c[0].ord.to_s(16)}" end z.emit("\"#{escaped}\"") end end end # Return a guranteed ASCII-8BIT encoding for Ruby 1.9 This is a helper # method for other methods that perform regular expressions against byte # sequences deliberately rather than dealing with characters. # The method may or may not return a new instance. if String.method_defined?(:encoding) ASCII_ENCODING = Encoding.find("ASCII-8BIT") def to_ascii8bit if self.encoding == ASCII_ENCODING self else self.dup.force_encoding(ASCII_ENCODING) end end else def to_ascii8bit self end end end class Hash def to_zaml(z) z.first_time_only(self) { z.nested { if empty? z.emit('{}') else each_pair { |k, v| z.nl z.prefix_structured_keys('? ') { k.to_zaml(z) } z.emit(': ') v.to_zaml(z) } end } } end end class Array def to_zaml(z) z.first_time_only(self) { z.nested { if empty? z.emit('[]') else each { |v| z.nl('- '); v.to_zaml(z) } end } } end end class Time def to_zaml(z) # 2008-12-06 10:06:51.373758 -07:00 ms = ("%0.6f" % (usec * 1e-6))[2..-1] offset = "%+0.2i:%0.2i" % [utc_offset / 3600.0, (utc_offset / 60) % 60] z.emit(self.strftime("%Y-%m-%d %H:%M:%S.#{ms} #{offset}")) end end class Date def to_zaml(z) z.emit(strftime('%Y-%m-%d')) end end class Range def to_zaml(z) z.first_time_only(self) { z.emit(zamlized_class_name(Range)) z.nested { z.nl z.emit('begin: ') z.emit(first) z.nl z.emit('end: ') z.emit(last) z.nl z.emit('excl: ') z.emit(exclude_end?) } } end end diff --git a/spec/integration/environments/setting_hooks_spec.rb b/spec/integration/environments/setting_hooks_spec.rb index 1b91fb8f8..2becedec4 100644 --- a/spec/integration/environments/setting_hooks_spec.rb +++ b/spec/integration/environments/setting_hooks_spec.rb @@ -1,27 +1,27 @@ require 'spec_helper' describe "setting hooks" do let(:confdir) { Puppet[:confdir] } let(:environmentpath) { File.expand_path("envdir", confdir) } describe "reproducing PUP-3500" do let(:productiondir) { File.join(environmentpath, "production") } before(:each) do FileUtils.mkdir_p(productiondir) end - it "accesses correct directory environment settings after intializing a setting with an on_write hook" do + it "accesses correct directory environment settings after initializing a setting with an on_write hook" do expect(Puppet.settings.setting(:certname).call_hook).to eq(:on_write_only) File.open(File.join(confdir, "puppet.conf"), "w") do |f| f.puts("environmentpath=#{environmentpath}") f.puts("certname=something") end Puppet.initialize_settings production_env = Puppet.lookup(:environments).get(:production) expect(Puppet.settings.value(:manifest, production_env)).to eq("#{productiondir}/manifests") end end end diff --git a/spec/integration/environments/settings_interpolation_spec.rb b/spec/integration/environments/settings_interpolation_spec.rb index bcdc136c7..5d97fe76e 100644 --- a/spec/integration/environments/settings_interpolation_spec.rb +++ b/spec/integration/environments/settings_interpolation_spec.rb @@ -1,165 +1,165 @@ require 'pp' require 'spec_helper' module SettingsInterpolationSpec describe "interpolating $environment" do let(:confdir) { Puppet[:confdir] } let(:cmdline_args) { ['--confdir', confdir, '--vardir', Puppet[:vardir], '--hiera_config', Puppet[:hiera_config]] } before(:each) do FileUtils.mkdir_p(confdir) end shared_examples_for "a setting that does not interpolate $environment" do before(:each) do set_puppet_conf(confdir, <<-EOF) environmentpath=$confdir/environments #{setting}=#{value} EOF end it "does not interpolate $environment" do Puppet.initialize_settings(cmdline_args) expect(Puppet[:environmentpath]).to eq("#{confdir}/environments") expect(Puppet[setting.intern]).to eq(expected) end it "displays the interpolated value in the warning" do Puppet.initialize_settings(cmdline_args) Puppet[setting.intern] expect(@logs).to have_matching_log(/cannot interpolate \$environment within '#{setting}'.*Its value will remain #{Regexp.escape(expected)}/) end end context "when environmentpath is set" do describe "config_version" do it "interpolates $environment" do envname = 'testing' setting = 'config_version' value = '/some/script $environment' expected = "#{File.expand_path('/some/script')} testing" set_puppet_conf(confdir, <<-EOF) environmentpath=$confdir/environments environment=#{envname} EOF set_environment_conf("#{confdir}/environments", envname, <<-EOF) #{setting}=#{value} EOF Puppet.initialize_settings(cmdline_args) expect(Puppet[:environmentpath]).to eq("#{confdir}/environments") environment = Puppet.lookup(:environments).get(envname) expect(environment.config_version).to eq(expected) expect(@logs).to be_empty end end describe "basemodulepath" do let(:setting) { "basemodulepath" } let(:value) { "$confdir/environments/$environment/modules:$confdir/environments/$environment/other_modules" } let(:expected) { "#{confdir}/environments/$environment/modules:#{confdir}/environments/$environment/other_modules" } it_behaves_like "a setting that does not interpolate $environment" - it "logs a single warning for multiple instaces of $environment in the setting" do + it "logs a single warning for multiple instances of $environment in the setting" do set_puppet_conf(confdir, <<-EOF) environmentpath=$confdir/environments #{setting}=#{value} EOF Puppet.initialize_settings(cmdline_args) expect(@logs.map(&:to_s).grep(/cannot interpolate \$environment within '#{setting}'/).count).to eq(1) end end describe "environment" do let(:setting) { "environment" } let(:value) { "whatareyouthinking$environment" } let(:expected) { value } it_behaves_like "a setting that does not interpolate $environment" end describe "the default_manifest" do let(:setting) { "default_manifest" } let(:value) { "$confdir/manifests/$environment" } let(:expected) { "#{confdir}/manifests/$environment" } it_behaves_like "a setting that does not interpolate $environment" end it "does not interpolate $environment and logs a warning when interpolating environmentpath" do setting = 'environmentpath' value = "$confdir/environments/$environment" expected = "#{confdir}/environments/$environment" set_puppet_conf(confdir, <<-EOF) #{setting}=#{value} EOF Puppet.initialize_settings(cmdline_args) expect(Puppet[setting.intern]).to eq(expected) expect(@logs).to have_matching_log(/cannot interpolate \$environment within '#{setting}'/) end end def assert_does_interpolate_environment(setting, value, expected_interpolation) set_puppet_conf(confdir, <<-EOF) #{setting}=#{value} EOF Puppet.initialize_settings(cmdline_args) expect(Puppet[:environmentpath]).to be_empty expect(Puppet[setting.intern]).to eq(expected_interpolation) expect(@logs).to_not have_matching_log(/cannot interpolate \$environment within '#{setting}'/) end context "when environmentpath is not set" do it "does interpolate $environment in config_version" do value = "/some/script $environment" expect = "/some/script production" assert_does_interpolate_environment("config_version", value, expect) end it "does interpolate $environment in basemodulepath" do value = "$confdir/environments/$environment/modules:$confdir/environments/$environment/other_modules" expected = "#{confdir}/environments/production/modules:#{confdir}/environments/production/other_modules" assert_does_interpolate_environment("basemodulepath", value, expected) end it "does interpolate $environment in default_manifest, which is fine, because this setting isn't used" do value = "$confdir/manifests/$environment" expected = "#{confdir}/manifests/production" assert_does_interpolate_environment("default_manifest", value, expected) end it "raises something" do value = expected = "whatareyouthinking$environment" expect { assert_does_interpolate_environment("environment", value, expected) }.to raise_error(SystemStackError, /stack level too deep/) end end def set_puppet_conf(confdir, settings) write_file(File.join(confdir, "puppet.conf"), settings) end def set_environment_conf(environmentpath, environment, settings) envdir = File.join(environmentpath, environment) FileUtils.mkdir_p(envdir) write_file(File.join(envdir, 'environment.conf'), settings) end def write_file(file, contents) File.open(file, "w") do |f| f.puts(contents) end end end end diff --git a/spec/integration/network/authconfig_spec.rb b/spec/integration/network/authconfig_spec.rb index cdd9a2559..71e25a9ab 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) true rescue Puppet::Network::AuthorizationError false end end failure_message_for_should do |instance| "expected #{params[2][:node]}/#{params[2][:ip]} to be allowed" end failure_message_for_should_not do |instance| "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) [: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") + add_rule("allow inexistent") @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/unit/face/module/search_spec.rb b/spec/unit/face/module/search_spec.rb index d826a3ca9..4771bbdc9 100644 --- a/spec/unit/face/module/search_spec.rb +++ b/spec/unit/face/module/search_spec.rb @@ -1,201 +1,201 @@ require 'spec_helper' require 'puppet/face' require 'puppet/application/module' require 'puppet/module_tool' describe "puppet module search" do subject { Puppet::Face[:module, :current] } let(:options) do {} end describe Puppet::Application::Module do subject do app = Puppet::Application::Module.new app.stubs(:action).returns(Puppet::Face.find_action(:module, :search)) app end before { subject.render_as = :console } before { Puppet::Util::Terminal.stubs(:width).returns(100) } it 'should output nothing when receiving an empty dataset' do - subject.render({:answers => [], :result => :sucess}, ['apache', {}]).should == "No results found for 'apache'." + subject.render({:answers => [], :result => :success}, ['apache', {}]).should == "No results found for 'apache'." end it 'should return error and exit when error returned' do results = { :result => :failure, :error => { :oneline => 'Something failed', :multiline => 'Something failed', } } expect { subject.render(results, ['apache', {}]) }.to raise_error 'Something failed' end it 'should output a header when receiving a non-empty dataset' do results = { :result => :success, :answers => [ {'full_name' => '', 'author' => '', 'desc' => '', 'tag_list' => [] }, ], } subject.render(results, ['apache', {}]).should =~ /NAME/ subject.render(results, ['apache', {}]).should =~ /DESCRIPTION/ subject.render(results, ['apache', {}]).should =~ /AUTHOR/ subject.render(results, ['apache', {}]).should =~ /KEYWORDS/ end it 'should output the relevant fields when receiving a non-empty dataset' do results = { :result => :success, :answers => [ {'full_name' => 'Name', 'author' => 'Author', 'desc' => 'Summary', 'tag_list' => ['tag1', 'tag2'] }, ] } subject.render(results, ['apache', {}]).should =~ /Name/ subject.render(results, ['apache', {}]).should =~ /Author/ subject.render(results, ['apache', {}]).should =~ /Summary/ subject.render(results, ['apache', {}]).should =~ /tag1/ subject.render(results, ['apache', {}]).should =~ /tag2/ end it 'should elide really long descriptions' do results = { :result => :success, :answers => [ { 'full_name' => 'Name', 'author' => 'Author', 'desc' => 'This description is really too long to fit in a single data table, guys -- we should probably set about truncating it', 'tag_list' => ['tag1', 'tag2'], }, ] } subject.render(results, ['apache', {}]).should =~ /\.{3} @Author/ end it 'should never truncate the module name' do results = { :result => :success, :answers => [ { 'full_name' => 'This-module-has-a-really-really-long-name', 'author' => 'Author', 'desc' => 'Description', 'tag_list' => ['tag1', 'tag2'], }, ] } subject.render(results, ['apache', {}]).should =~ /This-module-has-a-really-really-long-name/ end it 'should never truncate the author name' do results = { :result => :success, :answers => [ { 'full_name' => 'Name', 'author' => 'This-author-has-a-really-really-long-name', 'desc' => 'Description', 'tag_list' => ['tag1', 'tag2'], }, ] } subject.render(results, ['apache', {}]).should =~ /@This-author-has-a-really-really-long-name/ end it 'should never remove tags that match the search term' do results = { :results => :success, :answers => [ { 'full_name' => 'Name', 'author' => 'Author', 'desc' => 'Description', 'tag_list' => ['Supercalifragilisticexpialidocious'] + (1..100).map { |i| "tag#{i}" }, }, ] } subject.render(results, ['Supercalifragilisticexpialidocious', {}]).should =~ /Supercalifragilisticexpialidocious/ subject.render(results, ['Supercalifragilisticexpialidocious', {}]).should_not =~ /tag/ end { 100 => "NAME DESCRIPTION AUTHOR KEYWORDS#{' '*15}\n"\ "Name This description is really too long to fit ... @JohnnyApples tag1 tag2 taggitty3#{' '*4}\n", 70 => "NAME DESCRIPTION AUTHOR KEYWORDS#{' '*5}\n"\ "Name This description is rea... @JohnnyApples tag1 tag2#{' '*4}\n", 80 => "NAME DESCRIPTION AUTHOR KEYWORDS#{' '*8}\n"\ "Name This description is really too... @JohnnyApples tag1 tag2#{' '*7}\n", 200 => "NAME DESCRIPTION AUTHOR KEYWORDS#{' '*48}\n"\ "Name This description is really too long to fit in a single data table, guys -- we should probably set about trunca... @JohnnyApples tag1 tag2 taggitty3#{' '*37}\n" }.each do |width, expectation| it "should resize the table to fit the screen, when #{width} columns" do results = { :result => :success, :answers => [ { 'full_name' => 'Name', 'author' => 'JohnnyApples', 'desc' => 'This description is really too long to fit in a single data table, guys -- we should probably set about truncating it', 'tag_list' => ['tag1', 'tag2', 'taggitty3'], }, ] } Puppet::Util::Terminal.expects(:width).returns(width) result = subject.render(results, ['apache', {}]) result.lines.sort_by(&:length).last.chomp.length.should <= width result.should == expectation end end end describe "option validation" do context "without any options" do it "should require a search term" do pattern = /wrong number of arguments/ expect { subject.search }.to raise_error ArgumentError, pattern end end it "should accept the --module-repository option" do forge = mock("Puppet::Forge") searcher = mock("Searcher") options[:module_repository] = "http://forge.example.com" Puppet::Forge.expects(:new).with().returns(forge) Puppet::ModuleTool::Applications::Searcher.expects(:new).with("puppetlabs-apache", forge, has_entries(options)).returns(searcher) searcher.expects(:run) subject.search("puppetlabs-apache", options) end end describe "inline documentation" do subject { Puppet::Face[:module, :current].get_action :search } its(:summary) { should =~ /search.*module/im } its(:description) { should =~ /search.*module/im } its(:returns) { should =~ /array/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/node_spec.rb b/spec/unit/face/node_spec.rb index d7e06b7b6..e310fbb06 100755 --- a/spec/unit/face/node_spec.rb +++ b/spec/unit/face/node_spec.rb @@ -1,272 +1,272 @@ #! /usr/bin/env ruby require 'spec_helper' require 'puppet/face' describe Puppet::Face[:node, '0.0.1'] do after :all do Puppet::SSL::Host.ca_location = :none end describe '#cleanup' do it "should clean everything" do { "cert" => ['hostname'], "cached_facts" => ['hostname'], "cached_node" => ['hostname'], "reports" => ['hostname'], "storeconfigs" => ['hostname', :unexport] }.each { |k, v| subject.expects("clean_#{k}".to_sym).with(*v) } subject.cleanup('hostname', :unexport) end end describe 'when running #clean' do before :each do Puppet::Node::Facts.indirection.stubs(:terminus_class=) Puppet::Node::Facts.indirection.stubs(:cache_class=) Puppet::Node.stubs(:terminus_class=) Puppet::Node.stubs(:cache_class=) end it 'should invoke #cleanup' do subject.expects(:cleanup).with('hostname', nil) subject.clean('hostname') end end describe "clean action" do before :each do Puppet::Node::Facts.indirection.stubs(:terminus_class=) Puppet::Node::Facts.indirection.stubs(:cache_class=) Puppet::Node.stubs(:terminus_class=) Puppet::Node.stubs(:cache_class=) subject.stubs(:cleanup) end it "should have a clean action" do subject.should be_action :clean end it "should not accept a call with no arguments" do expect { subject.clean() }.to raise_error end it "should accept a node name" do expect { subject.clean('hostname') }.to_not raise_error end it "should accept more than one node name" do expect do subject.clean('hostname', 'hostname2', {}) end.to_not raise_error expect do subject.clean('hostname', 'hostname2', 'hostname3', { :unexport => true }) end.to_not raise_error end it "should accept the option --unexport" do expect { subject.clean('hostname', :unexport => true) }.to_not raise_error end context "clean action" do subject { Puppet::Face[:node, :current] } before :each do Puppet::Util::Log.stubs(:newdestination) Puppet::Util::Log.stubs(:level=) end describe "during setup" do it "should set facts terminus and cache class to yaml" do Puppet::Node::Facts.indirection.expects(:terminus_class=).with(:yaml) Puppet::Node::Facts.indirection.expects(:cache_class=).with(:yaml) subject.clean('hostname') end it "should run in master mode" do subject.clean('hostname') Puppet.run_mode.should be_master end it "should set node cache as yaml" do Puppet::Node.indirection.expects(:terminus_class=).with(:yaml) Puppet::Node.indirection.expects(:cache_class=).with(:yaml) subject.clean('hostname') end it "should manage the certs if the host is a CA" do Puppet::SSL::CertificateAuthority.stubs(:ca?).returns(true) Puppet::SSL::Host.expects(:ca_location=).with(:local) subject.clean('hostname') end it "should not manage the certs if the host is not a CA" do Puppet::SSL::CertificateAuthority.stubs(:ca?).returns(false) Puppet::SSL::Host.expects(:ca_location=).with(:none) subject.clean('hostname') end end describe "when cleaning certificate" do before :each do Puppet::SSL::Host.stubs(:destroy) @ca = mock() Puppet::SSL::CertificateAuthority.stubs(:instance).returns(@ca) end it "should send the :destroy order to the ca if we are a CA" do Puppet::SSL::CertificateAuthority.stubs(:ca?).returns(true) @ca.expects(:revoke).with(@host) @ca.expects(:destroy).with(@host) subject.clean_cert(@host) end it "should not destroy the certs if we are not a CA" do Puppet::SSL::CertificateAuthority.stubs(:ca?).returns(false) @ca.expects(:revoke).never @ca.expects(:destroy).never subject.clean_cert(@host) end end describe "when cleaning cached facts" do it "should destroy facts" do @host = 'node' Puppet::Node::Facts.indirection.expects(:destroy).with(@host) subject.clean_cached_facts(@host) end end describe "when cleaning cached node" do it "should destroy the cached node" do Puppet::Node.indirection.expects(:destroy).with(@host) subject.clean_cached_node(@host) end end describe "when cleaning archived reports" do it "should tell the reports to remove themselves" do Puppet::Transaction::Report.indirection.stubs(:destroy).with(@host) subject.clean_reports(@host) end end describe "when cleaning storeconfigs entries for host", :if => Puppet.features.rails? do before :each do # Stub this so we don't need access to the DB require 'puppet/rails/host' Puppet[:storeconfigs] = true Puppet::Rails.stubs(:connect) @rails_node = stub_everything 'rails_node' Puppet::Rails::Host.stubs(:find_by_name).returns(@rails_node) end it "should connect to the database" do Puppet::Rails.expects(:connect) subject.clean_storeconfigs(@host, false) end it "should find the right host entry" do Puppet::Rails::Host.expects(:find_by_name).with(@host).returns(@rails_node) subject.clean_storeconfigs(@host, false) end describe "without unexport" do it "should remove the host and it's content" do @rails_node.expects(:destroy) subject.clean_storeconfigs(@host, false) end end describe "with unexport" do before :each do @rails_node.stubs(:id).returns(1234) @type = stub_everything 'type' @type.stubs(:validattr?).with(:ensure).returns(true) @ensure_name = stub_everything 'ensure_name', :id => 23453 Puppet::Rails::ParamName.stubs(:find_or_create_by_name).returns(@ensure_name) @param_values = stub_everything 'param_values' @resource = stub_everything 'resource', :param_values => @param_values, :restype => "File" Puppet::Rails::Resource.stubs(:find).returns([@resource]) end it "should find all resources" do Puppet::Rails::Resource.expects(:find).with(:all, {:include => {:param_values => :param_name}, :conditions => ["exported=? AND host_id=?", true, 1234]}).returns([]) subject.clean_storeconfigs(@host, true) end describe "with an exported native type" do before :each do Puppet::Type.stubs(:type).returns(@type) @type.expects(:validattr?).with(:ensure).returns(true) end it "should test a native type for ensure as an attribute" do subject.clean_storeconfigs(@host, true) end it "should delete the old ensure parameter" do ensure_param = stub 'ensure_param', :id => 12345, :line => 12 @param_values.stubs(:find).returns(ensure_param) Puppet::Rails::ParamValue.expects(:delete).with(12345); subject.clean_storeconfigs(@host, true) end it "should add an ensure => absent parameter" do @param_values.expects(:create).with(:value => "absent", :line => 0, :param_name => @ensure_name) subject.clean_storeconfigs(@host, true) end end describe "with an exported definition" do it "should try to lookup a definition and test it for the ensure argument" do Puppet::Type.stubs(:type).returns(nil) definition = stub_everything 'definition', :arguments => { 'ensure' => 'present' } Puppet::Resource::TypeCollection.any_instance.expects(:find_definition).with('', "File").returns(definition) subject.clean_storeconfigs(@host, true) end end - it "should not unexport the resource of an unkown type" do + it "should not unexport the resource of an unknown type" do Puppet::Type.stubs(:type).returns(nil) Puppet::Resource::TypeCollection.any_instance.expects(:find_definition).with('', "File").returns(nil) Puppet::Rails::ParamName.expects(:find_or_create_by_name).never subject.clean_storeconfigs(@host, true) end it "should not unexport the resource of a not ensurable native type" do Puppet::Type.stubs(:type).returns(@type) @type.expects(:validattr?).with(:ensure).returns(false) Puppet::Resource::TypeCollection.any_instance.expects(:find_definition).with('', "File").returns(nil) Puppet::Rails::ParamName.expects(:find_or_create_by_name).never subject.clean_storeconfigs(@host, true) end it "should not unexport the resource of a not ensurable definition" do Puppet::Type.stubs(:type).returns(nil) definition = stub_everything 'definition', :arguments => { 'foobar' => 'someValue' } Puppet::Resource::TypeCollection.any_instance.expects(:find_definition).with('', "File").returns(definition) Puppet::Rails::ParamName.expects(:find_or_create_by_name).never subject.clean_storeconfigs(@host, true) end end end end end end diff --git a/spec/unit/file_system_spec.rb b/spec/unit/file_system_spec.rb index f3b36a66d..90d943bbf 100644 --- a/spec/unit/file_system_spec.rb +++ b/spec/unit/file_system_spec.rb @@ -1,508 +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 + it "times out if the lock cannot be acquired 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 + it "should return false for exist? on a non-existent 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/module_spec.rb b/spec/unit/module_spec.rb index 7bdbcade6..9a8c8fd1f 100755 --- a/spec/unit/module_spec.rb +++ b/spec/unit/module_spec.rb @@ -1,722 +1,722 @@ #! /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.stubs(:exist?).returns false end it "should have a class method that returns a named module from a given environment" do env = Puppet::Node::Environment.create(:myenv, []) env.expects(:module).with(name).returns "yep" Puppet.override(:environments => Puppet::Environments::Static.new(env)) do Puppet::Module.find(name, "myenv").should == "yep" end end it "should return nil if asked for a named module that doesn't exist" do env = Puppet::Node::Environment.create(:myenv, []) env.expects(:module).with(name).returns nil Puppet.override(:environments => Puppet::Environments::Static.new(env)) do Puppet::Module.find(name, "myenv").should be_nil end 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 " mod.author.should == "Luke Kanies " 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.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.expects(:exist?).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.expects(:exist?).with(metadata_file).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 env = Puppet::Node::Environment.create(:testing, [@modpath]) ['test_gte_req', 'test_specific_req', 'foobar'].each do |mod_name| metadata_file = "#{@modpath}/#{mod_name}/metadata.json" Puppet::FileSystem.stubs(:exist?).with(metadata_file).returns true end mod = PuppetSpec::Modules.create( 'test_gte_req', @modpath, :metadata => { :dependencies => [{ "version_requirement" => ">= 2.2.0", "name" => "baz/foobar" }] }, :environment => env ) mod2 = PuppetSpec::Modules.create( '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' }, :environment => env ) mod.unmet_dependencies.should == [{ :reason => :version_mismatch, :name => "baz/foobar", :version_constraint => ">= 2.2.0", :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/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 env = Puppet::Node::Environment.create(:testing, [@modpath]) metadata_file = "#{@modpath}/foobar/metadata.json" Puppet::FileSystem.expects(:exist?).with(metadata_file).times(3).returns true 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 env = Puppet::Node::Environment.create(:testing, [@modpath]) [name, 'satisfied'].each do |mod_name| metadata_file = "#{@modpath}/#{mod_name}/metadata.json" Puppet::FileSystem.expects(:exist?).with(metadata_file).twice.returns true end 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.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.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.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.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.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.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 + it "should match the glob pattern plus '.{pp,rb}' if no extension 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.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.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.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.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.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 env = Puppet::Node::Environment.create(:testing, [@modpath]) dependable = PuppetSpec::Modules.create( 'dependable', @modpath, :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/parser/ast/collection_spec.rb b/spec/unit/parser/ast/collection_spec.rb index 5b2f7c14d..3e1dbfaa5 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.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 + it "should evaluate overriden parameters" 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 9cbbbf978..f2466b5da 100755 --- a/spec/unit/parser/ast/leaf_spec.rb +++ b/spec/unit/parser/ast/leaf_spec.rb @@ -1,511 +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 + it "should not downcase the parameter 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/lexer_spec.rb b/spec/unit/parser/lexer_spec.rb index f0f10e9f3..8d44a960e 100755 --- a/spec/unit/parser/lexer_spec.rb +++ b/spec/unit/parser/lexer_spec.rb @@ -1,877 +1,877 @@ #! /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 + it "should not think the terminator is escaped, when preceded 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) lexer = stub("lexer") @token.convert(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 handles reserved words' do ['function', 'private', 'attr', 'type'].each do |reserved_bare_word| it "by delivering '#{reserved_bare_word}' as a bare word" do expect(tokens_scanned_from(reserved_bare_word)).to eq([[:NAME, {:value=>reserved_bare_word, :line => 1}]]) end 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.exist?(nofile).should == false lexer.fullscan.should == [[false,false]] end end diff --git a/spec/unit/parser/scope_spec.rb b/spec/unit/parser/scope_spec.rb index f14faf831..c74d818dd 100755 --- a/spec/unit/parser/scope_spec.rb +++ b/spec/unit/parser/scope_spec.rb @@ -1,661 +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 "create_test_scope_for_node" do let(:node_name) { "node_name_foo" } 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) end it "should set the source to a node resource" do 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) end it "should set the parent to the compiler topscope" do scope.parent.should be(scope.compiler.topscope) end end it "should return a scope for use in a test harness" do 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.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.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.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.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 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.set_match_data({1 => :value}) @scope["1"].should == :value end 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(:all) @scope["1"].should be_nil end it "should not remove classic variables when unset_ephemeral_var(:all) is called" do @scope['myvar'] = :value1 @scope.set_match_data({1 => :value2}) @scope.stubs(:parent).returns(nil) @scope.unset_ephemeral_var(:all) @scope["myvar"].should == :value1 end it "should raise an error when setting numerical variable" do expect { @scope.setvar("1", :value3, :ephemeral => true) }.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.set_match_data({0 => :earliest}) @scope.new_ephemeral @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 not check presence of an ephemeral variable accross multiple levels" do + it "should not check presence of an ephemeral variable across 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.set_match_data({1 => :value1}) @scope.new_ephemeral @scope.set_match_data({0 => :value2}) @scope.new_ephemeral @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.set_match_data({1 => :value1}) @scope.new_ephemeral @scope.set_match_data({0 => :value2}) @scope.new_ephemeral @scope.include?("2").should be_false end it "should not get ephemeral values from earlier scope when not in later" do @scope.set_match_data({1 => :value1}) @scope.new_ephemeral @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.set_match_data({1 => :value1}) @scope.new_ephemeral @scope.set_match_data({1 => :value2}) level = @scope.ephemeral_level() @scope.new_ephemeral @scope.set_match_data({1 => :value3}) @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 # 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 # 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 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/pops/types/type_parser_spec.rb b/spec/unit/pops/types/type_parser_spec.rb index b67b7b6cd..b2d85056f 100644 --- a/spec/unit/pops/types/type_parser_spec.rb +++ b/spec/unit/pops/types/type_parser_spec.rb @@ -1,240 +1,240 @@ 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_error(Puppet::ParseError, /The expression is not a valid type specification/) end it "rejects an unknown parameterized type" do expect { parser.parse("notAType[Integer]") }.to raise_error(Puppet::ParseError, /The expression is not a valid type specification/) end it "rejects an unknown type parameter" do expect { parser.parse("Array[notAType]") }.to raise_error(Puppet::ParseError, /The expression is not a valid type specification/) end [ 'Any', 'Data', 'CatalogEntry', 'Boolean', 'Scalar', 'Undef', 'Numeric', 'Default' ].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.any)).to be_the_type(types.any) 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) expect(the_type_parsed_from(types.default)).to be_the_type(types.default) 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 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 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 "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 + it "parses tuple type with occurrence 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 describe "handles parsing of patterns and regexp" do { 'Pattern[/([a-z]+)([1-9]+)/]' => [:pattern, [/([a-z]+)([1-9]+)/]], 'Pattern["([a-z]+)([1-9]+)"]' => [:pattern, [/([a-z]+)([1-9]+)/]], 'Regexp[/([a-z]+)([1-9]+)/]' => [:regexp, [/([a-z]+)([1-9]+)/]], 'Pattern[/x9/, /([a-z]+)([1-9]+)/]' => [:pattern, [/x9/, /([a-z]+)([1-9]+)/]], }.each do |source, type| it "such that the source '#{source}' yields the type #{type.to_s}" do expect(parser.parse(source)).to be_the_type(Puppet::Pops::Types::TypeFactory.send(type[0], *type[1])) end end 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("Runtime[ruby, 'Integer']")).to be_the_type(types.ruby_type('Integer')) end it 'parses a callable type' do expect(parser.parse("Callable")).to be_the_type(types.all_callables()) end it 'parses a parameterized callable type' do expect(parser.parse("Callable[String, Integer]")).to be_the_type(types.callable(String, Integer)) end it 'parses a parameterized callable type with min/max' do expect(parser.parse("Callable[String, Integer, 1, default]")).to be_the_type(types.callable(String, Integer, 1, :default)) end it 'parses a parameterized callable type with block' do expect(parser.parse("Callable[String, Callable[Boolean]]")).to be_the_type(types.callable(String, types.callable(true))) end it 'parses a parameterized callable type with 0 min/max' do t = parser.parse("Callable[0,0]") expect(t).to be_the_type(types.callable()) expect(t.param_types.types).to be_empty end it 'parses a parameterized callable type with >0 min/max' do t = parser.parse("Callable[0,1]") expect(t).to be_the_type(types.callable(0,1)) # Contains a Unit type to indicate "called with what you accept" expect(t.param_types.types[0]).to be_the_type(Puppet::Pops::Types::PUnitType.new()) 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/provider/user/directoryservice_spec.rb b/spec/unit/provider/user/directoryservice_spec.rb index 55cb6eabd..5f4ba3552 100755 --- a/spec/unit/provider/user/directoryservice_spec.rb +++ b/spec/unit/provider/user/directoryservice_spec.rb @@ -1,1070 +1,1070 @@ #! /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(:username) { 'nonexistent_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/` which # will return a hash of keys whose values are all arrays. let(:user_plist_xml) do ' dsAttrTypeStandard:NFSHomeDirectory - /Users/nonexistant_user + /Users/nonexistent_user dsAttrTypeStandard:RealName - nonexistant_user + nonexistent_user dsAttrTypeStandard:PrimaryGroupID 22 dsAttrTypeStandard:UniqueID 1000 dsAttrTypeStandard:RecordName - nonexistant_user + nonexistent_user ' 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/ 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/ 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:RecordName" =>["nonexistent_user"], "dsAttrTypeStandard:UniqueID" =>["1000"], "dsAttrTypeStandard:AuthenticationAuthority"=> [";Kerberosv5;;testuser@LKDC:SHA1.4383E152D9D394AA32D13AE98F6F6E1FE8D00F81;LKDC:SHA1.4383E152D9D394AA32D13AE98F6F6E1FE8D00F81", ";ShadowHash;HASHLIST:"], "dsAttrTypeStandard:AppleMetaNodeLocation" =>["/Local/Default"], - "dsAttrTypeStandard:NFSHomeDirectory" =>["/Users/nonexistant_user"], + "dsAttrTypeStandard:NFSHomeDirectory" =>["/Users/nonexistent_user"], "dsAttrTypeStandard:RecordType" =>["dsRecTypeStandard:Users"], - "dsAttrTypeStandard:RealName" =>["nonexistant_user"], + "dsAttrTypeStandard:RealName" =>["nonexistent_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" => ["\n \n \n \n failedLoginCount\n 0\n failedLoginTimestamp\n 2001-01-01T00:00:00Z\n lastLoginTimestamp\n 2001-01-01T00:00:00Z\n passwordTimestamp\n 2012-08-10T23:53:50Z\n \n \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 ' ' 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_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.class.expects(:get_attribute_from_dscl).with('Users', username, 'GeneratedUID').returns({'dsAttrTypeStandard:GeneratedUID' => ['guidnonexistent_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.expects(:dscl).with('.', '-merge', '/Groups/one', 'GroupMembership', 'nonexistent_user') + provider.expects(:dscl).with('.', '-merge', '/Groups/one', 'GroupMembers', 'guidnonexistent_user') provider.class.prefetch({}) provider.groups= 'one,two,three' end it 'should call the get_salted_sha512 method on 10.7 and return the correct hash' do provider.class.expects(:get_attribute_from_dscl).with('Users', username, 'ShadowHashData').returns(sha512_shadowhashdata_hash) provider.class.expects(:convert_binary_to_xml).with(sha512_embedded_bplist).returns(sha512_embedded_bplist_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_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 it 'should call write_password_to_users_plist when setting the password' 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' do provider.class.stubs(:get_os_version).returns('10.8') resource[:salt] = pbkdf2_salt_value resource[:iterations] = pbkdf2_iterations_value resource[:password] = pbkdf2_password_hash provider.expects(:write_password_to_users_plist).with(pbkdf2_password_hash) provider.password = resource[:password] 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 end describe "passwords on 10.8" do before :each do provider.class.stubs(:get_os_version).returns('10.8') end it "should raise an error on 10.8 if a password hash that doesn't contain 256 characters is passed" do expect do provider.password = 'password' end.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 it "fails if a password is given but not salt and iterations" do resource[:password] = pbkdf2_password_hash expect do provider.password = resource[:password] end.to raise_error(Puppet::Error, /OS X versions > 10\.7 use PBKDF2 password hashes, which requires all three of salt, iterations, and password hash\. This resource is missing: salt, iterations\./) end it "fails if salt is given but not password and iterations" do resource[:salt] = pbkdf2_salt_value expect do provider.salt = resource[:salt] end.to raise_error(Puppet::Error, /OS X versions > 10\.7 use PBKDF2 password hashes, which requires all three of salt, iterations, and password hash\. This resource is missing: password, iterations\./) end it "fails if iterations is given but not password and salt" do resource[:iterations] = pbkdf2_iterations_value expect do provider.iterations = resource[:iterations] end.to raise_error(Puppet::Error, /OS X versions > 10\.7 use PBKDF2 password hashes, which requires all three of salt, iterations, and password hash\. This resource is missing: password, salt\./) 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 ' 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 # 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 before :each do # Ensure we don't have a value cached from another spec provider.class.instance_variable_set(:@groups, nil) if provider.class.instance_variable_defined? :@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/ dsAttrTypeStandard:GeneratedUID DCC660C6-F5A9-446D-B9FF-3C0258AB5BA0 ' 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) 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'/) 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.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.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.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.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}/) end end describe '#write_password_to_users_plist' do let(:sha512_plist_xml) do "\n\n\n\n\tKerberosKeys\n\t\n\t\t\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\n\t\n\tShadowHashData\n\t\n\t\t\n\t\tYnBsaXN0MDDRAQJdU0FMVEVELVNIQTUxMk8QRFNL0iuruijP6becUWe43GTX\n\t\t5WTgOTi2emx41DMnwnB4vbKieVOE4eNHiyocX5c0GX1LWJ6VlZqZ9EnDLsuA\n\t\tNC5Ga9qlCAsZAAAAAAAAAQEAAAAAAAAAAwAAAAAAAAAAAAAAAAAAAGA=\n\t\t\n\t\n\tauthentication_authority\n\t\n\t\t;Kerberosv5;;jeff@LKDC:SHA1.4383E152D9D394AA32D13AE98F6F6E1FE8D00F81;LKDC:SHA1.4383E152D9D394AA32D13AE98F6F6E1FE8D00F81\n\t\t;ShadowHash;HASHLIST:<SALTED-SHA512>\n\t\n\tdsAttrTypeStandard:ShadowHashData\n\t\n\t\t\n\t\tYnBsaXN0MDDRAQJdU0FMVEVELVNIQTUxMk8QRH6n1ZITH1eyyPi9vOyNnfEh\n\t\tKKOGOTpPAMdhm6wmIqRNRRQZ0R2lEtWRWrmOOXGKyUCD/i79a/cQpU1Hf4/3\n\t\tNbElhxktCAsZAAAAAAAAAQEAAAAAAAAAAwAAAAAAAAAAAAAAAAAAAGA=\n\t\t\n\t\n\tgenerateduid\n\t\n\t\t3AC74939-C14F-45DD-B6A9-D1A82373F0B0\n\t\n\tname\n\t\n\t\tjeff\n\t\n\tpasswd\n\t\n\t\t********\n\t\n\tpasswordpolicyoptions\n\t\n\t\t\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\n\t\n\tuid\n\t\n\t\t28\n\t\n\n" end let(:pbkdf2_plist_xml) do "\n\n\n\n\tKerberosKeys\n\t\n\t\t\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\n\t\n\tShadowHashData\n\t\n\t\t\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\n\t\n\tauthentication_authority\n\t\n\t\t;Kerberosv5;;jeff@LKDC:SHA1.413A30F590EEDC787D532981850A975245B0A723;LKDC:SHA1.413A30F590EEDC787D532981850A975245B0A723\n\t\t;ShadowHash;HASHLIST:<SALTED-SHA512-PBKDF2>\n\t\n\tgenerateduid\n\t\n\t\t1CB825D1-2DF7-43CC-B874-DB6BBB76C402\n\t\n\tgid\n\t\n\t\t21\n\t\n\tname\n\t\n\t\tjeff\n\t\n\tpasswd\n\t\n\t\t********\n\t\n\tpasswordpolicyoptions\n\t\n\t\t\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\n\t\n\tuid\n\t\n\t\t28\n\t\n\n" 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:", ";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") + Plist::Emit.expects(:save_plist).with(user_plist_xml, "#{users_plist_dir}/nonexistent_user.plist") + provider.expects(:plutil).with('-convert', 'binary1', "#{users_plist_dir}/nonexistent_user.plist") provider.write_users_plist_to_disk(user_plist_xml) 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 "\n\n\n\n\tshell\n\t/bin/bash\n\tuser\n\tpuppet\n\n\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 before :each do # Ensure we don't have a value cached from another spec provider.class.instance_variable_set(:@os_version, nil) if provider.class.instance_variable_defined? :@os_version end 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 resource[:salt] = pbkdf2_salt_value resource[:iterations] = pbkdf2_iterations_value resource[:password] = pbkdf2_password_hash 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 = resource[:password] end end end diff --git a/spec/unit/provider/user/pw_spec.rb b/spec/unit/provider/user/pw_spec.rb index e0074f474..7a815c328 100755 --- a/spec/unit/provider/user/pw_spec.rb +++ b/spec/unit/provider/user/pw_spec.rb @@ -1,214 +1,214 @@ #! /usr/bin/env ruby require 'spec_helper' provider_class = Puppet::Type.type(:user).provider(:pw) describe provider_class do let :resource do Puppet::Type.type(:user).new(:name => "testuser", :provider => :pw) end describe "when creating users" do let :provider do prov = resource.provider prov.expects(:exists?).returns nil prov end it "should run pw with no additional flags when no properties are given" do provider.addcmd.must == [provider_class.command(:pw), "useradd", "testuser"] provider.expects(:execute).with([provider_class.command(:pw), "useradd", "testuser"], kind_of(Hash)) provider.create end it "should use -o when allowdupe is enabled" do resource[:allowdupe] = true provider.expects(:execute).with(includes("-o"), kind_of(Hash)) provider.create end it "should use -c with the correct argument when the comment property is set" do resource[:comment] = "Testuser Name" provider.expects(:execute).with(all_of(includes("-c"), includes("Testuser Name")), kind_of(Hash)) provider.create end it "should use -e with the correct argument when the expiry property is set" do resource[:expiry] = "2010-02-19" provider.expects(:execute).with(all_of(includes("-e"), includes("19-02-2010")), kind_of(Hash)) provider.create end it "should use -e 00-00-0000 if the expiry property has to be removed" do resource[:expiry] = :absent provider.expects(:execute).with(all_of(includes("-e"), includes("00-00-0000")), kind_of(Hash)) provider.create end it "should use -g with the correct argument when the gid property is set" do resource[:gid] = 12345 provider.expects(:execute).with(all_of(includes("-g"), includes(12345)), kind_of(Hash)) provider.create end it "should use -G with the correct argument when the groups property is set" do resource[:groups] = "group1" provider.expects(:execute).with(all_of(includes("-G"), includes("group1")), kind_of(Hash)) provider.create end it "should use -G with all the given groups when the groups property is set to an array" do resource[:groups] = ["group1", "group2"] provider.expects(:execute).with(all_of(includes("-G"), includes("group1,group2")), kind_of(Hash)) provider.create end it "should use -d with the correct argument when the home property is set" do resource[:home] = "/home/testuser" provider.expects(:execute).with(all_of(includes("-d"), includes("/home/testuser")), kind_of(Hash)) provider.create end it "should use -m when the managehome property is enabled" do resource[:managehome] = true provider.expects(:execute).with(includes("-m"), kind_of(Hash)) provider.create end it "should call the password set function with the correct argument when the password property is set" do resource[:password] = "*" provider.expects(:execute) provider.expects(:password=).with("*") provider.create end it "should use -s with the correct argument when the shell property is set" do resource[:shell] = "/bin/sh" provider.expects(:execute).with(all_of(includes("-s"), includes("/bin/sh")), kind_of(Hash)) provider.create end it "should use -u with the correct argument when the uid property is set" do resource[:uid] = 12345 provider.expects(:execute).with(all_of(includes("-u"), includes(12345)), kind_of(Hash)) provider.create end # (#7500) -p should not be used to set a password (it means something else) it "should not use -p when a password is given" do resource[:password] = "*" provider.addcmd.should_not include("-p") provider.expects(:password=) provider.expects(:execute).with(Not(includes("-p")), kind_of(Hash)) provider.create end end describe "when deleting users" do it "should run pw with no additional flags" do provider = resource.provider provider.expects(:exists?).returns true provider.deletecmd.must == [provider_class.command(:pw), "userdel", "testuser"] provider.expects(:execute).with([provider_class.command(:pw), "userdel", "testuser"]) provider.delete end # The above test covers this, but given the consequences of - # accidently deleting a user's home directory it seems better to + # accidentally deleting a user's home directory it seems better to # have an explicit test. it "should not use -r when managehome is not set" do provider = resource.provider provider.expects(:exists?).returns true resource[:managehome] = false provider.expects(:execute).with(Not(includes("-r"))) provider.delete end it "should use -r when managehome is set" do provider = resource.provider provider.expects(:exists?).returns true resource[:managehome] = true provider.expects(:execute).with(includes("-r")) provider.delete end end describe "when modifying users" do let :provider do resource.provider end it "should run pw with the correct arguments" do provider.modifycmd("uid", 12345).must == [provider_class.command(:pw), "usermod", "testuser", "-u", 12345] provider.expects(:execute).with([provider_class.command(:pw), "usermod", "testuser", "-u", 12345]) provider.uid = 12345 end it "should use -c with the correct argument when the comment property is changed" do resource[:comment] = "Testuser Name" provider.expects(:execute).with(all_of(includes("-c"), includes("Testuser New Name"))) provider.comment = "Testuser New Name" end it "should use -e with the correct argument when the expiry property is changed" do resource[:expiry] = "2010-02-19" provider.expects(:execute).with(all_of(includes("-e"), includes("19-02-2011"))) provider.expiry = "2011-02-19" end it "should use -e with the correct argument when the expiry property is removed" do resource[:expiry] = :absent provider.expects(:execute).with(all_of(includes("-e"), includes("00-00-0000"))) provider.expiry = :absent end it "should use -g with the correct argument when the gid property is changed" do resource[:gid] = 12345 provider.expects(:execute).with(all_of(includes("-g"), includes(54321))) provider.gid = 54321 end it "should use -G with the correct argument when the groups property is changed" do resource[:groups] = "group1" provider.expects(:execute).with(all_of(includes("-G"), includes("group2"))) provider.groups = "group2" end it "should use -G with all the given groups when the groups property is changed with an array" do resource[:groups] = ["group1", "group2"] provider.expects(:execute).with(all_of(includes("-G"), includes("group3,group4"))) provider.groups = "group3,group4" end it "should use -d with the correct argument when the home property is changed" do resource[:home] = "/home/testuser" provider.expects(:execute).with(all_of(includes("-d"), includes("/newhome/testuser"))) provider.home = "/newhome/testuser" end it "should use -m and -d with the correct argument when the home property is changed and managehome is enabled" do resource[:home] = "/home/testuser" resource[:managehome] = true provider.expects(:execute).with(all_of(includes("-d"), includes("/newhome/testuser"), includes("-m"))) provider.home = "/newhome/testuser" end it "should call the password set function with the correct argument when the password property is changed" do resource[:password] = "*" provider.expects(:password=).with("!") provider.password = "!" end it "should use -s with the correct argument when the shell property is changed" do resource[:shell] = "/bin/sh" provider.expects(:execute).with(all_of(includes("-s"), includes("/bin/tcsh"))) provider.shell = "/bin/tcsh" end it "should use -u with the correct argument when the uid property is changed" do resource[:uid] = 12345 provider.expects(:execute).with(all_of(includes("-u"), includes(54321))) provider.uid = 54321 end end end diff --git a/spec/unit/provider/user/user_role_add_spec.rb b/spec/unit/provider/user/user_role_add_spec.rb index 42cc4995e..a7bdf10ff 100755 --- a/spec/unit/provider/user/user_role_add_spec.rb +++ b/spec/unit/provider/user/user_role_add_spec.rb @@ -1,357 +1,357 @@ require 'spec_helper' require 'puppet_spec/files' require 'tempfile' describe Puppet::Type.type(:user).provider(:user_role_add), :unless => Puppet.features.microsoft_windows? do include PuppetSpec::Files let(:resource) { Puppet::Type.type(:user).new(:name => 'myuser', :managehome => false, :allowdupe => false) } let(:provider) { described_class.new(resource) } before do resource.stubs(:should).returns "fakeval" resource.stubs(:should).with(:keys).returns Hash.new resource.stubs(:[]).returns "fakeval" end describe "#command" do before do klass = stub("provider") klass.stubs(:command).with(:foo).returns("userfoo") klass.stubs(:command).with(:role_foo).returns("rolefoo") provider.stubs(:class).returns(klass) end it "should use the command if not a role and ensure!=role" do provider.stubs(:is_role?).returns(false) provider.stubs(:exists?).returns(false) resource.stubs(:[]).with(:ensure).returns(:present) provider.class.stubs(:foo) provider.command(:foo).should == "userfoo" end it "should use the role command when a role" do provider.stubs(:is_role?).returns(true) provider.command(:foo).should == "rolefoo" end it "should use the role command when !exists and ensure=role" do provider.stubs(:is_role?).returns(false) provider.stubs(:exists?).returns(false) resource.stubs(:[]).with(:ensure).returns(:role) provider.command(:foo).should == "rolefoo" end end describe "#transition" do it "should return the type set to whatever is passed in" do provider.expects(:command).with(:modify).returns("foomod") provider.transition("bar").include?("type=bar") end end describe "#create" do before do provider.stubs(:password=) end it "should use the add command when the user is not a role" do provider.stubs(:is_role?).returns(false) provider.expects(:addcmd).returns("useradd") provider.expects(:run).at_least_once provider.create end it "should use transition(normal) when the user is a role" do provider.stubs(:is_role?).returns(true) provider.expects(:transition).with("normal") provider.expects(:run) provider.create end it "should set password age rules" do resource = Puppet::Type.type(:user).new :name => "myuser", :password_min_age => 5, :password_max_age => 10, :provider => :user_role_add provider = described_class.new(resource) provider.stubs(:user_attributes) provider.stubs(:execute) provider.expects(:execute).with { |cmd, *args| args == ["-n", 5, "-x", 10, "myuser"] } provider.create end end describe "#destroy" do it "should use the delete command if the user exists and is not a role" do provider.stubs(:exists?).returns(true) provider.stubs(:is_role?).returns(false) provider.expects(:deletecmd) provider.expects(:run) provider.destroy end it "should use the delete command if the user is a role" do provider.stubs(:exists?).returns(true) provider.stubs(:is_role?).returns(true) provider.expects(:deletecmd) provider.expects(:run) provider.destroy end end describe "#create_role" do it "should use the transition(role) if the user exists" do provider.stubs(:exists?).returns(true) provider.stubs(:is_role?).returns(false) provider.expects(:transition).with("role") provider.expects(:run) provider.create_role end it "should use the add command when role doesn't exists" do provider.stubs(:exists?).returns(false) provider.expects(:addcmd) provider.expects(:run) provider.create_role end end describe "with :allow_duplicates" do before do resource.stubs(:allowdupe?).returns true provider.stubs(:is_role?).returns(false) provider.stubs(:execute) resource.stubs(:system?).returns false provider.expects(:execute).with { |args| args.include?("-o") } end it "should add -o when the user is being created" do provider.stubs(:password=) provider.create end it "should add -o when the uid is being modified" do provider.uid = 150 end end [:roles, :auths, :profiles].each do |val| context "#send" do describe "when getting #{val}" do it "should get the user_attributes" do provider.expects(:user_attributes) provider.send(val) end it "should get the #{val} attribute" do attributes = mock("attributes") attributes.expects(:[]).with(val) provider.stubs(:user_attributes).returns(attributes) provider.send(val) end end end end describe "#keys" do it "should get the user_attributes" do provider.expects(:user_attributes) provider.keys end it "should call removed_managed_attributes" do provider.stubs(:user_attributes).returns({ :type => "normal", :foo => "something" }) provider.expects(:remove_managed_attributes) provider.keys end it "should removed managed attribute (type, auths, roles, etc)" do provider.stubs(:user_attributes).returns({ :type => "normal", :foo => "something" }) provider.keys.should == { :foo => "something" } end end describe "#add_properties" do it "should call build_keys_cmd" do resource.stubs(:should).returns "" resource.expects(:should).with(:keys).returns({ :foo => "bar" }) provider.expects(:build_keys_cmd).returns([]) provider.add_properties end it "should add the elements of the keys hash to an array" do resource.stubs(:should).returns "" resource.expects(:should).with(:keys).returns({ :foo => "bar"}) provider.add_properties.must == ["-K", "foo=bar"] end end describe "#build_keys_cmd" do - it "should build cmd array with keypairs seperated by -K ending with user" do + it "should build cmd array with keypairs separated by -K ending with user" do provider.build_keys_cmd({"foo" => "bar", "baz" => "boo"}).should.eql? ["-K", "foo=bar", "-K", "baz=boo"] end end describe "#keys=" do before do provider.stubs(:is_role?).returns(false) end it "should run a command" do provider.expects(:run) provider.keys=({}) end it "should build the command" do resource.stubs(:[]).with(:name).returns("someuser") provider.stubs(:command).returns("usermod") provider.expects(:build_keys_cmd).returns(["-K", "foo=bar"]) provider.expects(:run).with(["usermod", "-K", "foo=bar", "someuser"], "modify attribute key pairs") provider.keys=({}) end end describe "#password" do before do @array = mock "array" end it "should readlines of /etc/shadow" do File.expects(:readlines).with("/etc/shadow").returns([]) provider.password end it "should reject anything that doesn't start with alpha numerics" do @array.expects(:reject).returns([]) File.stubs(:readlines).with("/etc/shadow").returns(@array) provider.password end it "should collect splitting on ':'" do @array.stubs(:reject).returns(@array) @array.expects(:collect).returns([]) File.stubs(:readlines).with("/etc/shadow").returns(@array) provider.password end it "should find the matching user" do resource.stubs(:[]).with(:name).returns("username") @array.stubs(:reject).returns(@array) @array.stubs(:collect).returns([["username", "hashedpassword"], ["someoneelse", "theirpassword"]]) File.stubs(:readlines).with("/etc/shadow").returns(@array) provider.password.must == "hashedpassword" end it "should get the right password" do resource.stubs(:[]).with(:name).returns("username") File.stubs(:readlines).with("/etc/shadow").returns(["#comment", " nonsense", " ", "username:hashedpassword:stuff:foo:bar:::", "other:pword:yay:::"]) provider.password.must == "hashedpassword" end end describe "#password=" do let(:path) { tmpfile('etc-shadow') } before :each do provider.stubs(:target_file_path).returns(path) end def write_fixture(content) File.open(path, 'w') { |f| f.print(content) } end it "should update the target user" do write_fixture < 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.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 + it "should find all parameters 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/settings/ini_file_spec.rb b/spec/unit/settings/ini_file_spec.rb index 11e698a52..d12b01a36 100644 --- a/spec/unit/settings/ini_file_spec.rb +++ b/spec/unit/settings/ini_file_spec.rb @@ -1,184 +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 + # this is the preceding 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 + # this is the preceding 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/value_translator_spec.rb b/spec/unit/settings/value_translator_spec.rb index a318f6d48..a2464c045 100644 --- a/spec/unit/settings/value_translator_spec.rb +++ b/spec/unit/settings/value_translator_spec.rb @@ -1,77 +1,77 @@ #! /usr/bin/env ruby -S rspec require 'spec_helper' require 'puppet/settings/value_translator' describe Puppet::Settings::ValueTranslator do let(:translator) { Puppet::Settings::ValueTranslator.new } context "booleans" do it "translates strings representing booleans to booleans" do translator['true'].should == true translator['false'].should == false end it "translates boolean values into themselves" do translator[true].should == true translator[false].should == false end it "leaves a boolean string with whitespace as a string" do translator[' true'].should == " true" translator['true '].should == "true" translator[' false'].should == " false" translator['false '].should == "false" end end context "numbers" do it "translates integer strings to integers" do translator["1"].should == 1 translator["2"].should == 2 end it "translates numbers starting with a 0 as octal" do translator["011"].should == 9 end it "leaves hex numbers as strings" do translator["0x11"].should == "0x11" end end context "arbitrary strings" do it "translates an empty string as the empty string" do translator[""].should == "" end it "strips double quotes" do translator['"a string"'].should == 'a string' end it "strips single quotes" do translator["'a string'"].should == "a string" end - it "does not strip preceeding whitespace" do + it "does not strip preceding whitespace" do translator[" \ta string"].should == " \ta string" end it "strips trailing whitespace" do translator["a string\t "].should == "a string" end - it "leaves leading quote that is preceeded by whitespace" do + it "leaves leading quote that is preceded by whitespace" do translator[" 'a string'"].should == " 'a string" end it "leaves trailing quote that is succeeded by whitespace" do translator["'a string' "].should == "a string'" end it "leaves quotes that are not at the beginning or end of the string" do translator["a st'\"ring"].should == "a st'\"ring" end end end diff --git a/spec/unit/type/group_spec.rb b/spec/unit/type/group_spec.rb index 55f6e548b..ffadfac26 100755 --- a/spec/unit/type/group_spec.rb +++ b/spec/unit/type/group_spec.rb @@ -1,84 +1,84 @@ #! /usr/bin/env ruby require 'spec_helper' describe Puppet::Type.type(:group) do before do @class = Puppet::Type.type(:group) end it "should have a system_groups feature" do @class.provider_feature(:system_groups).should_not be_nil end describe "when validating attributes" do [:name, :allowdupe].each do |param| it "should have a #{param} parameter" do @class.attrtype(param).should == :param end end [:ensure, :gid].each do |param| it "should have a #{param} property" do @class.attrtype(param).should == :property end end it "should convert gids provided as strings into integers" do @class.new(:name => "foo", :gid => "15")[:gid].should == 15 end it "should accepts gids provided as integers" do @class.new(:name => "foo", :gid => 15)[:gid].should == 15 end end it "should have a boolean method for determining if duplicates are allowed" do @class.new(:name => "foo").must respond_to "allowdupe?" end it "should have a boolean method for determining if system groups are allowed" do @class.new(:name => "foo").must respond_to "system?" end it "should call 'create' to create the group" do group = @class.new(:name => "foo", :ensure => :present) group.provider.expects(:create) group.parameter(:ensure).sync end it "should call 'delete' to remove the group" do group = @class.new(:name => "foo", :ensure => :absent) group.provider.expects(:delete) group.parameter(:ensure).sync end - it "delegates the existance check to its provider" do + it "delegates the existence check to its provider" do provider = @class.provide(:testing) {} provider_instance = provider.new provider_instance.expects(:exists?).returns true type = @class.new(:name => "group", :provider => provider_instance) type.exists?.should == true end describe "should delegate :members implementation to the provider:" do let (:provider) { @class.provide(:testing) { has_features :manages_members } } let (:provider_instance) { provider.new } let (:type) { @class.new(:name => "group", :provider => provider_instance, :members => ['user1']) } it "insync? calls members_insync?" do provider_instance.expects(:members_insync?).with(['user1'], ['user1']).returns true type.property(:members).insync?(['user1']).should be_true end it "is_to_s and should_to_s call members_to_s" do provider_instance.expects(:members_to_s).with(['user2', 'user1']).returns "user2 (), user1 ()" provider_instance.expects(:members_to_s).with(['user1']).returns "user1 ()" type.property(:members).is_to_s('user1').should == 'user1 ()' type.property(:members).should_to_s('user2,user1').should == 'user2 (), user1 ()' end end end diff --git a/spec/unit/type/mount_spec.rb b/spec/unit/type/mount_spec.rb index 75b2f505d..e860303d6 100755 --- a/spec/unit/type/mount_spec.rb +++ b/spec/unit/type/mount_spec.rb @@ -1,592 +1,592 @@ #! /usr/bin/env ruby require 'spec_helper' describe Puppet::Type.type(:mount), :unless => Puppet.features.microsoft_windows? do before :each do Puppet::Type.type(:mount).stubs(:defaultprovider).returns providerclass end let :providerclass do described_class.provide(:fake_mount_provider) do attr_accessor :property_hash def create; end def destroy; end def exists? get(:ensure) != :absent end def mount; end def umount; end def mounted? [:mounted, :ghost].include?(get(:ensure)) end mk_resource_methods end end let :provider do providerclass.new(:name => 'yay') end let :resource do described_class.new(:name => "yay", :audit => :ensure, :provider => provider) end let :ensureprop do resource.property(:ensure) end it "should have a :refreshable feature that requires the :remount method" do described_class.provider_feature(:refreshable).methods.should == [:remount] end it "should have no default value for :ensure" do mount = described_class.new(:name => "yay") mount.should(:ensure).should be_nil end it "should have :name as the only keyattribut" do described_class.key_attributes.should == [:name] end describe "when validating attributes" do [:name, :remounts, :provider].each do |param| it "should have a #{param} parameter" do described_class.attrtype(param).should == :param end end [:ensure, :device, :blockdevice, :fstype, :options, :pass, :dump, :atboot, :target].each do |param| it "should have a #{param} property" do described_class.attrtype(param).should == :property end end end describe "when validating values" do describe "for name" do it "should allow full qualified paths" do described_class.new(:name => "/mnt/foo")[:name].should == '/mnt/foo' end it "should remove trailing slashes" do described_class.new(:name => '/')[:name].should == '/' described_class.new(:name => '//')[:name].should == '/' described_class.new(:name => '/foo/')[:name].should == '/foo' described_class.new(:name => '/foo/bar/')[:name].should == '/foo/bar' described_class.new(:name => '/foo/bar/baz//')[:name].should == '/foo/bar/baz' end it "should not allow spaces" do expect { described_class.new(:name => "/mnt/foo bar") }.to raise_error Puppet::Error, /name.*whitespace/ end it "should allow pseudo mountpoints (e.g. swap)" do described_class.new(:name => 'none')[:name].should == 'none' end end describe "for ensure" do it "should alias :present to :defined as a value to :ensure" do mount = described_class.new(:name => "yay", :ensure => :present) mount.should(:ensure).should == :defined end it "should support :present as a value to :ensure" do expect { described_class.new(:name => "yay", :ensure => :present) }.to_not raise_error end it "should support :defined as a value to :ensure" do expect { described_class.new(:name => "yay", :ensure => :defined) }.to_not raise_error end it "should support :unmounted as a value to :ensure" do expect { described_class.new(:name => "yay", :ensure => :unmounted) }.to_not raise_error end it "should support :absent as a value to :ensure" do expect { described_class.new(:name => "yay", :ensure => :absent) }.to_not raise_error end it "should support :mounted as a value to :ensure" do expect { described_class.new(:name => "yay", :ensure => :mounted) }.to_not raise_error end it "should not support other values for :ensure" do expect { described_class.new(:name => "yay", :ensure => :mount) }.to raise_error Puppet::Error, /Invalid value/ end end describe "for device" do it "should support normal /dev paths for device" do expect { described_class.new(:name => "/foo", :ensure => :present, :device => '/dev/hda1') }.to_not raise_error expect { described_class.new(:name => "/foo", :ensure => :present, :device => '/dev/dsk/c0d0s0') }.to_not raise_error end it "should support labels for device" do expect { described_class.new(:name => "/foo", :ensure => :present, :device => 'LABEL=/boot') }.to_not raise_error expect { described_class.new(:name => "/foo", :ensure => :present, :device => 'LABEL=SWAP-hda6') }.to_not raise_error end it "should support pseudo devices for device" do expect { described_class.new(:name => "/foo", :ensure => :present, :device => 'ctfs') }.to_not raise_error expect { described_class.new(:name => "/foo", :ensure => :present, :device => 'swap') }.to_not raise_error expect { described_class.new(:name => "/foo", :ensure => :present, :device => 'sysfs') }.to_not raise_error expect { described_class.new(:name => "/foo", :ensure => :present, :device => 'proc') }.to_not raise_error end it 'should not support whitespace in device' do expect { described_class.new(:name => "/foo", :ensure => :present, :device => '/dev/my dev/foo') }.to raise_error Puppet::Error, /device.*whitespace/ expect { described_class.new(:name => "/foo", :ensure => :present, :device => "/dev/my\tdev/foo") }.to raise_error Puppet::Error, /device.*whitespace/ end end describe "for blockdevice" do before :each do # blockdevice is only used on Solaris Facter.stubs(:value).with(:operatingsystem).returns 'Solaris' Facter.stubs(:value).with(:osfamily).returns 'Solaris' end it "should support normal /dev/rdsk paths for blockdevice" do expect { described_class.new(:name => "/foo", :ensure => :present, :blockdevice => '/dev/rdsk/c0d0s0') }.to_not raise_error end it "should support a dash for blockdevice" do expect { described_class.new(:name => "/foo", :ensure => :present, :blockdevice => '-') }.to_not raise_error end it "should not support whitespace in blockdevice" do expect { described_class.new(:name => "/foo", :ensure => :present, :blockdevice => '/dev/my dev/foo') }.to raise_error Puppet::Error, /blockdevice.*whitespace/ expect { described_class.new(:name => "/foo", :ensure => :present, :blockdevice => "/dev/my\tdev/foo") }.to raise_error Puppet::Error, /blockdevice.*whitespace/ end it "should default to /dev/rdsk/DEVICE if device is /dev/dsk/DEVICE" do obj = described_class.new(:name => "/foo", :device => '/dev/dsk/c0d0s0') obj[:blockdevice].should == '/dev/rdsk/c0d0s0' end it "should default to - if it is an nfs-share" do obj = described_class.new(:name => "/foo", :device => "server://share", :fstype => 'nfs') obj[:blockdevice].should == '-' end it "should have no default otherwise" do described_class.new(:name => "/foo")[:blockdevice].should == nil described_class.new(:name => "/foo", :device => "/foo")[:blockdevice].should == nil end it "should overwrite any default if blockdevice is explicitly set" do described_class.new(:name => "/foo", :device => '/dev/dsk/c0d0s0', :blockdevice => '/foo')[:blockdevice].should == '/foo' described_class.new(:name => "/foo", :device => "server://share", :fstype => 'nfs', :blockdevice => '/foo')[:blockdevice].should == '/foo' end end describe "for fstype" do it "should support valid fstypes" do expect { described_class.new(:name => "/foo", :ensure => :present, :fstype => 'ext3') }.to_not raise_error expect { described_class.new(:name => "/foo", :ensure => :present, :fstype => 'proc') }.to_not raise_error expect { described_class.new(:name => "/foo", :ensure => :present, :fstype => 'sysfs') }.to_not raise_error end it "should support auto as a special fstype" do expect { described_class.new(:name => "/foo", :ensure => :present, :fstype => 'auto') }.to_not raise_error end it "should not support whitespace in fstype" do expect { described_class.new(:name => "/foo", :ensure => :present, :fstype => 'ext 3') }.to raise_error Puppet::Error, /fstype.*whitespace/ end end describe "for options" do it "should support a single option" do expect { described_class.new(:name => "/foo", :ensure => :present, :options => 'ro') }.to_not raise_error end - it "should support muliple options as a comma separated list" do + it "should support multiple options as a comma separated list" do expect { described_class.new(:name => "/foo", :ensure => :present, :options => 'ro,rsize=4096') }.to_not raise_error end it "should not support whitespace in options" do expect { described_class.new(:name => "/foo", :ensure => :present, :options => ['ro','foo bar','intr']) }.to raise_error Puppet::Error, /option.*whitespace/ end end describe "for pass" do it "should support numeric values" do expect { described_class.new(:name => "/foo", :ensure => :present, :pass => '0') }.to_not raise_error expect { described_class.new(:name => "/foo", :ensure => :present, :pass => '1') }.to_not raise_error expect { described_class.new(:name => "/foo", :ensure => :present, :pass => '2') }.to_not raise_error end it "should support - on Solaris" do Facter.stubs(:value).with(:operatingsystem).returns 'Solaris' Facter.stubs(:value).with(:osfamily).returns 'Solaris' expect { described_class.new(:name => "/foo", :ensure => :present, :pass => '-') }.to_not raise_error end it "should default to 0 on non Solaris" do Facter.stubs(:value).with(:osfamily).returns nil Facter.stubs(:value).with(:operatingsystem).returns 'HP-UX' described_class.new(:name => "/foo", :ensure => :present)[:pass].should == 0 end it "should default to - on Solaris" do Facter.stubs(:value).with(:operatingsystem).returns 'Solaris' Facter.stubs(:value).with(:osfamily).returns 'Solaris' described_class.new(:name => "/foo", :ensure => :present)[:pass].should == '-' end end describe "for dump" do it "should support 0 as a value for dump" do expect { described_class.new(:name => "/foo", :ensure => :present, :dump => '0') }.to_not raise_error end it "should support 1 as a value for dump" do expect { described_class.new(:name => "/foo", :ensure => :present, :dump => '1') }.to_not raise_error end # Unfortunately the operatingsystem is evaluatet at load time so I am unable to stub operatingsystem it "should support 2 as a value for dump on FreeBSD", :if => Facter.value(:operatingsystem) == 'FreeBSD' do expect { described_class.new(:name => "/foo", :ensure => :present, :dump => '2') }.to_not raise_error end it "should not support 2 as a value for dump when not on FreeBSD", :if => Facter.value(:operatingsystem) != 'FreeBSD' do expect { described_class.new(:name => "/foo", :ensure => :present, :dump => '2') }.to raise_error Puppet::Error, /Invalid value/ end it "should default to 0" do described_class.new(:name => "/foo", :ensure => :present)[:dump].should == 0 end end describe "for atboot" do it "does not allow non-boolean values" do expect { described_class.new(:name => "/foo", :ensure => :present, :atboot => 'unknown') }.to raise_error Puppet::Error, /expected a boolean value/ end it "interprets yes as yes" do resource = described_class.new(:name => "/foo", :ensure => :present, :atboot => :yes) expect(resource[:atboot]).to eq(:yes) end it "interprets true as yes" do resource = described_class.new(:name => "/foo", :ensure => :present, :atboot => :true) expect(resource[:atboot]).to eq(:yes) end it "interprets no as no" do resource = described_class.new(:name => "/foo", :ensure => :present, :atboot => :no) expect(resource[:atboot]).to eq(:no) end it "interprets false as no" do resource = described_class.new(:name => "/foo", :ensure => :present, :atboot => false) expect(resource[:atboot]).to eq(:no) end end end describe "when changing the host" do def test_ensure_change(options) provider.set(:ensure => options[:from]) provider.expects(:create).times(options[:create] || 0) provider.expects(:destroy).times(options[:destroy] || 0) provider.expects(:mount).never provider.expects(:unmount).times(options[:unmount] || 0) ensureprop.stubs(:syncothers) ensureprop.should = options[:to] ensureprop.sync (!!provider.property_hash[:needs_mount]).should == (!!options[:mount]) end it "should create itself when changing from :ghost to :present" do test_ensure_change(:from => :ghost, :to => :present, :create => 1) end it "should create itself when changing from :absent to :present" do test_ensure_change(:from => :absent, :to => :present, :create => 1) end it "should create itself and unmount when changing from :ghost to :unmounted" do test_ensure_change(:from => :ghost, :to => :unmounted, :create => 1, :unmount => 1) end it "should unmount resource when changing from :mounted to :unmounted" do test_ensure_change(:from => :mounted, :to => :unmounted, :unmount => 1) end it "should create itself when changing from :absent to :unmounted" do test_ensure_change(:from => :absent, :to => :unmounted, :create => 1) end it "should unmount resource when changing from :ghost to :absent" do test_ensure_change(:from => :ghost, :to => :absent, :unmount => 1) end it "should unmount and destroy itself when changing from :mounted to :absent" do test_ensure_change(:from => :mounted, :to => :absent, :destroy => 1, :unmount => 1) end it "should destroy itself when changing from :unmounted to :absent" do test_ensure_change(:from => :unmounted, :to => :absent, :destroy => 1) end it "should create itself when changing from :ghost to :mounted" do test_ensure_change(:from => :ghost, :to => :mounted, :create => 1) end it "should create itself and mount when changing from :absent to :mounted" do test_ensure_change(:from => :absent, :to => :mounted, :create => 1, :mount => 1) end it "should mount resource when changing from :unmounted to :mounted" do test_ensure_change(:from => :unmounted, :to => :mounted, :mount => 1) end it "should be in sync if it is :absent and should be :absent" do ensureprop.should = :absent ensureprop.safe_insync?(:absent).should == true end it "should be out of sync if it is :absent and should be :defined" do ensureprop.should = :defined ensureprop.safe_insync?(:absent).should == false end it "should be out of sync if it is :absent and should be :mounted" do ensureprop.should = :mounted ensureprop.safe_insync?(:absent).should == false end it "should be out of sync if it is :absent and should be :unmounted" do ensureprop.should = :unmounted ensureprop.safe_insync?(:absent).should == false end it "should be out of sync if it is :mounted and should be :absent" do ensureprop.should = :absent ensureprop.safe_insync?(:mounted).should == false end it "should be in sync if it is :mounted and should be :defined" do ensureprop.should = :defined ensureprop.safe_insync?(:mounted).should == true end it "should be in sync if it is :mounted and should be :mounted" do ensureprop.should = :mounted ensureprop.safe_insync?(:mounted).should == true end it "should be out in sync if it is :mounted and should be :unmounted" do ensureprop.should = :unmounted ensureprop.safe_insync?(:mounted).should == false end it "should be out of sync if it is :unmounted and should be :absent" do ensureprop.should = :absent ensureprop.safe_insync?(:unmounted).should == false end it "should be in sync if it is :unmounted and should be :defined" do ensureprop.should = :defined ensureprop.safe_insync?(:unmounted).should == true end it "should be out of sync if it is :unmounted and should be :mounted" do ensureprop.should = :mounted ensureprop.safe_insync?(:unmounted).should == false end it "should be in sync if it is :unmounted and should be :unmounted" do ensureprop.should = :unmounted ensureprop.safe_insync?(:unmounted).should == true end it "should be out of sync if it is :ghost and should be :absent" do ensureprop.should = :absent ensureprop.safe_insync?(:ghost).should == false end it "should be out of sync if it is :ghost and should be :defined" do ensureprop.should = :defined ensureprop.safe_insync?(:ghost).should == false end it "should be out of sync if it is :ghost and should be :mounted" do ensureprop.should = :mounted ensureprop.safe_insync?(:ghost).should == false end it "should be out of sync if it is :ghost and should be :unmounted" do ensureprop.should = :unmounted ensureprop.safe_insync?(:ghost).should == false end end describe "when responding to refresh" do pending "2.6.x specifies slightly different behavior and the desired behavior needs to be clarified and revisited. See ticket #4904" do it "should remount if it is supposed to be mounted" do resource[:ensure] = "mounted" provider.expects(:remount) resource.refresh end it "should not remount if it is supposed to be present" do resource[:ensure] = "present" provider.expects(:remount).never resource.refresh end it "should not remount if it is supposed to be absent" do resource[:ensure] = "absent" provider.expects(:remount).never resource.refresh end it "should not remount if it is supposed to be defined" do resource[:ensure] = "defined" provider.expects(:remount).never resource.refresh end it "should not remount if it is supposed to be unmounted" do resource[:ensure] = "unmounted" provider.expects(:remount).never resource.refresh end it "should not remount swap filesystems" do resource[:ensure] = "mounted" resource[:fstype] = "swap" provider.expects(:remount).never resource.refresh end end end describe "when modifying an existing mount entry" do let :initial_values do { :ensure => :mounted, :name => '/mnt/foo', :device => "/foo/bar", :blockdevice => "/other/bar", :target => "/what/ever", :options => "soft", :pass => 0, :dump => 0, :atboot => :no, } end let :resource do described_class.new(initial_values.merge(:provider => provider)) end let :provider do providerclass.new(initial_values) end def run_in_catalog(*resources) Puppet::Util::Storage.stubs(:store) catalog = Puppet::Resource::Catalog.new catalog.add_resource *resources catalog.apply end it "should use the provider to change the dump value" do provider.expects(:dump=).with(1) resource[:dump] = 1 run_in_catalog(resource) end it "should umount before flushing changes to disk" do syncorder = sequence('syncorder') provider.expects(:unmount).in_sequence(syncorder) provider.expects(:options=).in_sequence(syncorder).with 'hard' resource.expects(:flush).in_sequence(syncorder) # Call inside syncothers resource.expects(:flush).in_sequence(syncorder) # I guess transaction or anything calls flush again resource[:ensure] = :unmounted resource[:options] = 'hard' run_in_catalog(resource) end end describe "establishing autorequires" do def create_resource(path) described_class.new( :name => path, :provider => providerclass.new(path) ) end def create_catalog(*resources) catalog = Puppet::Resource::Catalog.new resources.each do |resource| catalog.add_resource resource end catalog end let(:root_mount) { create_resource("/") } let(:var_mount) { create_resource("/var") } let(:log_mount) { create_resource("/var/log") } before do create_catalog(root_mount, var_mount, log_mount) end it "adds no autorequires for the root mount" do expect(root_mount.autorequire).to be_empty end it "adds the parent autorequire for a mount with one parent" do parent_relationship = var_mount.autorequire[0] expect(var_mount.autorequire).to have_exactly(1).item expect(parent_relationship.source).to eq root_mount expect(parent_relationship.target).to eq var_mount end it "adds both parent autorequires for a mount with two parents" do grandparent_relationship = log_mount.autorequire[0] parent_relationship = log_mount.autorequire[1] expect(log_mount.autorequire).to have_exactly(2).items expect(grandparent_relationship.source).to eq root_mount expect(grandparent_relationship.target).to eq log_mount expect(parent_relationship.source).to eq var_mount expect(parent_relationship.target).to eq log_mount end end end diff --git a/spec/unit/type/ssh_authorized_key_spec.rb b/spec/unit/type/ssh_authorized_key_spec.rb index fa82941c7..3e819540f 100755 --- a/spec/unit/type/ssh_authorized_key_spec.rb +++ b/spec/unit/type/ssh_authorized_key_spec.rb @@ -1,258 +1,258 @@ #! /usr/bin/env ruby require 'spec_helper' describe Puppet::Type.type(:ssh_authorized_key), :unless => Puppet.features.microsoft_windows? do include PuppetSpec::Files before do 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 = stub 'provider', :class => provider_class, :file_path => make_absolute("/tmp/whatever"), :clear => nil provider_class.stubs(:new).returns(provider) end 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 "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 "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 "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 "supports whitespace" do described_class.new(:name => "my test", :ensure => :present, :user => "nobody") end end describe "for ensure" do it "supports :present" do described_class.new(:name => "whev", :ensure => :present, :user => "nobody") end it "supports :absent" do described_class.new(:name => "whev", :ensure => :absent, :user => "nobody") end 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', :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 "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 "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 "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 "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 "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 "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 "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 "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 "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 "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 "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 "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 "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 "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 = 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 = 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 "supports present users" do described_class.new(:name => "whev", :type => :rsa, :user => "root") end it "supports absent users" do described_class.new(:name => "whev", :type => :rsa, :user => "ihopeimabsent") end end describe "for target" do it "supports absolute paths" do described_class.new(:name => "whev", :type => :rsa, :target => "/tmp/here") end 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 "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 "informs about an absent user" do Puppet::Log.level = :debug 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 "raises an error" do expect do described_class.new( :name => "Test", :key => "AAA", :type => "ssh-rsa", :ensure => :present) end.to raise_error(Puppet::Error,/user.*or.*target.*mandatory/) end end describe "when both target and user are specified" do it "uses target" do resource = described_class.new( :name => "Test", :user => "root", :target => "/tmp/blah" ) expect(resource.should(:target)).to eq "/tmp/blah" end end describe "when user is specified" do it "determines target" do resource = described_class.new( :name => "Test", :user => "root" ) target = File.expand_path("~root/.ssh/authorized_keys") expect(resource.should(:target)).to eq target end # Bug #2124 - ssh_authorized_key always changes target if target is not defined it "doesn't raise spurious change events" do resource = described_class.new(:name => "Test", :user => "root") target = File.expand_path("~root/.ssh/authorized_keys") expect(resource.property(:target).safe_insync?(target)).to eq true end end describe "when calling validate" do - it "doesn't crash on a non-existant user" do + it "doesn't crash on a non-existent user" do resource = described_class.new( :name => "Test", :user => "ihopesuchuserdoesnotexist" ) resource.validate end end end diff --git a/spec/unit/type/zpool_spec.rb b/spec/unit/type/zpool_spec.rb index a832c961b..e6d551cff 100755 --- a/spec/unit/type/zpool_spec.rb +++ b/spec/unit/type/zpool_spec.rb @@ -1,109 +1,109 @@ #! /usr/bin/env ruby require 'spec_helper' zpool = Puppet::Type.type(:zpool) describe zpool do before do @provider = stub 'provider' @resource = stub 'resource', :resource => nil, :provider => @provider, :line => nil, :file => nil end properties = [:ensure, :disk, :mirror, :raidz, :spare, :log] properties.each do |property| it "should have a #{property} property" do zpool.attrclass(property).ancestors.should be_include(Puppet::Property) end end parameters = [:pool, :raid_parity] parameters.each do |parameter| it "should have a #{parameter} parameter" do zpool.attrclass(parameter).ancestors.should be_include(Puppet::Parameter) end end end vdev_property = Puppet::Property::VDev describe vdev_property do before do vdev_property.initvars @resource = stub 'resource', :[]= => nil, :property => nil @property = vdev_property.new(:resource => @resource) end it "should be insync if the devices are the same" do @property.should = ["dev1 dev2"] @property.safe_insync?(["dev2 dev1"]).must be_true end it "should be out of sync if the devices are not the same" do @property.should = ["dev1 dev3"] @property.safe_insync?(["dev2 dev1"]).must be_false end - it "should be insync if the devices are the same and the should values are comma seperated" do + it "should be insync if the devices are the same and the should values are comma separated" do @property.should = ["dev1", "dev2"] @property.safe_insync?(["dev2 dev1"]).must be_true end it "should be out of sync if the device is absent and should has a value" do @property.should = ["dev1", "dev2"] @property.safe_insync?(:absent).must be_false end it "should be insync if the device is absent and should is absent" do @property.should = [:absent] @property.safe_insync?(:absent).must be_true end end multi_vdev_property = Puppet::Property::MultiVDev describe multi_vdev_property do before do multi_vdev_property.initvars @resource = stub 'resource', :[]= => nil, :property => nil @property = multi_vdev_property.new(:resource => @resource) end it "should be insync if the devices are the same" do @property.should = ["dev1 dev2"] @property.safe_insync?(["dev2 dev1"]).must be_true end it "should be out of sync if the devices are not the same" do @property.should = ["dev1 dev3"] @property.safe_insync?(["dev2 dev1"]).must be_false end it "should be out of sync if the device is absent and should has a value" do @property.should = ["dev1", "dev2"] @property.safe_insync?(:absent).must be_false end it "should be insync if the device is absent and should is absent" do @property.should = [:absent] @property.safe_insync?(:absent).must be_true end describe "when there are multiple lists of devices" do it "should be in sync if each group has the same devices" do @property.should = ["dev1 dev2", "dev3 dev4"] @property.safe_insync?(["dev2 dev1", "dev3 dev4"]).must be_true end it "should be out of sync if any group has the different devices" do @property.should = ["dev1 devX", "dev3 dev4"] @property.safe_insync?(["dev2 dev1", "dev3 dev4"]).must be_false end it "should be out of sync if devices are in the wrong group" do @property.should = ["dev1 dev2", "dev3 dev4"] @property.safe_insync?(["dev2 dev3", "dev1 dev4"]).must be_false end end end diff --git a/spec/unit/util/ssl_spec.rb b/spec/unit/util/ssl_spec.rb index 17c0362a7..a467a6b78 100644 --- a/spec/unit/util/ssl_spec.rb +++ b/spec/unit/util/ssl_spec.rb @@ -1,92 +1,92 @@ #! /usr/bin/env ruby require 'spec_helper' require 'openssl' require 'puppet/util/ssl' describe Puppet::Util::SSL do def parse(dn) Puppet::Util::SSL.subject_from_dn(dn) end describe "when getting a subject from a DN" do RSpec::Matchers.define :be_a_subject_with do |expected| match do |actual| parts = actual.to_a.map { |part| part[0..1] }.flatten Hash[*parts] == expected end end NO_PARTS = {} it "parses a DN with a single part" do parse('CN=client.example.org').should be_a_subject_with({ 'CN' => 'client.example.org' }) end it "parses a DN with parts separated by slashes" do parse('/CN=Root CA/OU=Server Operations/O=Example Org').should be_a_subject_with({ 'CN' => 'Root CA', 'OU' => 'Server Operations', 'O' => 'Example Org' }) end - it "parses a DN with a single part preceeded by a slash" do + it "parses a DN with a single part preceded by a slash" do parse('/CN=client.example.org').should be_a_subject_with({ 'CN' => 'client.example.org' }) end it "parses a DN with parts separated by commas" do parse('O=Foo\, Inc,CN=client2a.example.org').should be_a_subject_with({ 'O' => 'Foo, Inc', 'CN' => 'client2a.example.org' }) end it "finds no parts in something that is not a DN" do parse('(no)').should be_a_subject_with(NO_PARTS) end it "finds no parts in a DN with an invalid part" do parse('no=yes,CN=Root CA').should be_a_subject_with(NO_PARTS) end it "finds no parts in an empty DN" do parse('').should be_a_subject_with(NO_PARTS) end end describe "when getting a CN from a subject" do def cn_from(subject) Puppet::Util::SSL.cn_from_subject(subject) end it "should correctly parse a subject containing only a CN" do subj = parse('/CN=foo') cn_from(subj).should == 'foo' end it "should correctly parse a subject containing other components" do subj = parse('/CN=Root CA/OU=Server Operations/O=Example Org') cn_from(subj).should == 'Root CA' end it "should correctly parse a subject containing other components with CN not first" do subj = parse('/emailAddress=foo@bar.com/CN=foo.bar.com/O=Example Org') cn_from(subj).should == 'foo.bar.com' end it "should return nil for a subject with no CN" do subj = parse('/OU=Server Operations/O=Example Org') cn_from(subj).should == nil end it "should return nil for a bare string" do cn_from("/CN=foo").should == nil end end end diff --git a/spec/unit/util/watcher_spec.rb b/spec/unit/util/watcher_spec.rb index 83439c246..2b5cfe000 100644 --- a/spec/unit/util/watcher_spec.rb +++ b/spec/unit/util/watcher_spec.rb @@ -1,56 +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) 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 + it "is initially 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/zaml_spec.rb b/spec/unit/util/zaml_spec.rb index b239b4a84..9a3485049 100755 --- a/spec/unit/util/zaml_spec.rb +++ b/spec/unit/util/zaml_spec.rb @@ -1,308 +1,308 @@ #! /usr/bin/env ruby # encoding: UTF-8 # # The above encoding line is a magic comment to set the default source encoding # of this file for the Ruby interpreter. It must be on the first or second # line of the file if an interpreter is in use. In Ruby 1.9 and later, the # source encoding determines the encoding of String and Regexp objects created -# from this source file. This explicit encoding is important becuase otherwise +# from this source file. This explicit encoding is important because otherwise # Ruby will pick an encoding based on LANG or LC_CTYPE environment variables. # These may be different from site to site so it's important for us to # establish a consistent behavior. For more information on M17n please see: # http://links.puppetlabs.com/understanding_m17n require 'spec_helper' require 'puppet/util/monkey_patches' describe "Pure ruby yaml implementation" do RSpec::Matchers.define :round_trip_through_yaml do match do |object| YAML.load(object.to_yaml) == object end end RSpec::Matchers.define :be_equivalent_to do |expected_yaml| match do |object| object.to_yaml == expected_yaml and YAML.load(expected_yaml) == object end failure_message_for_should do |object| if object.to_yaml != expected_yaml "#{object} serialized to #{object.to_yaml}" else "#{expected_yaml} deserialized as #{YAML.load(expected_yaml)}" end end end { 7 => "--- 7", 3.14159 => "--- 3.14159", "3.14159" => '--- "3.14159"', "+3.14159" => '--- "+3.14159"', "0x123abc" => '--- "0x123abc"', "-0x123abc" => '--- "-0x123abc"', "-0x123" => '--- "-0x123"', "+0x123" => '--- "+0x123"', "0x123.456" => '--- "0x123.456"', 'test' => "--- test", [] => "--- []", :symbol => "--- !ruby/sym symbol", {:a => "A"} => "--- \n !ruby/sym a: A", {:a => "x\ny"} => "--- \n !ruby/sym a: |-\n x\n y", }.each do |data, serialized| it "should convert the #{data.class} #{data.inspect} to yaml" do data.should be_equivalent_to serialized end end context Time do def the_time_in(timezone) Puppet::Util.withenv("TZ" => timezone) do Time.local(2012, "dec", 11, 15, 59, 2) end end def the_time_in_yaml_offset_by(offset) "--- 2012-12-11 15:59:02.000000 #{offset}" end it "serializes a time in UTC" do bad_rubies = RUBY_VERSION[0,3] == '1.8' || RUBY_VERSION[0,3] == '2.0' && RUBY_PLATFORM == 'i386-mingw32' pending("not supported on Windows", :if => Puppet.features.microsoft_windows? && bad_rubies) do the_time_in("Europe/London").should be_equivalent_to(the_time_in_yaml_offset_by("+00:00")) end end it "serializes a time behind UTC" do pending("not supported on Windows", :if => Puppet.features.microsoft_windows?) do the_time_in("America/Chicago").should be_equivalent_to(the_time_in_yaml_offset_by("-06:00")) end end it "serializes a time behind UTC that is not a complete hour (Bug #15496)" do pending("not supported on Windows", :if => Puppet.features.microsoft_windows?) do the_time_in("America/Caracas").should be_equivalent_to(the_time_in_yaml_offset_by("-04:30")) end end it "serializes a time ahead of UTC" do pending("not supported on Windows", :if => Puppet.features.microsoft_windows?) do the_time_in("Europe/Berlin").should be_equivalent_to(the_time_in_yaml_offset_by("+01:00")) end end it "serializes a time ahead of UTC that is not a complete hour" do pending("not supported on Windows", :if => Puppet.features.microsoft_windows?) do the_time_in("Asia/Kathmandu").should be_equivalent_to(the_time_in_yaml_offset_by("+05:45")) end end it "serializes a time more than 12 hours ahead of UTC" do pending("not supported on Windows", :if => Puppet.features.microsoft_windows?) do the_time_in("Pacific/Kiritimati").should be_equivalent_to(the_time_in_yaml_offset_by("+14:00")) end end it "should roundtrip Time.now" do tm = Time.now # yaml only emits 6 digits of precision, but on some systems with ruby 1.9 # the original time object may contain nanoseconds, which will cause # the equality check to fail. So truncate the time object to only microsecs tm = Time.at(tm.to_i, tm.usec) tm.should round_trip_through_yaml end end [ { :a => "a:" }, { :a => "a:", :b => "b:" }, ["a:", "b:"], { :a => "/:", :b => "/:" }, { :a => "a/:", :b => "a/:" }, { :a => "\"" }, { :a => {}.to_yaml }, { :a => [].to_yaml }, { :a => "".to_yaml }, { :a => :a.to_yaml }, { "a:" => "b" }, { :a.to_yaml => "b" }, { [1, 2, 3] => "b" }, { "b:" => { "a" => [] } } ].each do |value| it "properly escapes #{value.inspect}, which contains YAML characters" do value.should round_trip_through_yaml end end # # Can't test for equality on raw objects { Object.new => "--- !ruby/object {}", [Object.new] => "--- \n - !ruby/object {}", {Object.new => Object.new} => "--- \n ? !ruby/object {}\n : !ruby/object {}" }.each do |o,y| it "should convert the #{o.class} #{o.inspect} to yaml" do o.to_yaml.should == y end it "should produce yaml for the #{o.class} #{o.inspect} that can be reconstituted" do lambda { YAML.load(o.to_yaml) }.should_not raise_error end end it "should emit proper labels and backreferences for common objects" do # Note: this test makes assumptions about the names ZAML chooses # for labels. x = [1, 2] y = [3, 4] z = [x, y, x, y] z.should be_equivalent_to("--- \n - &id001\n - 1\n - 2\n - &id002\n - 3\n - 4\n - *id001\n - *id002") end it "should emit proper labels and backreferences for recursive objects" do x = [1, 2] x << x x.to_yaml.should == "--- &id001\n \n - 1\n - 2\n - *id001" x2 = YAML.load(x.to_yaml) x2.should be_a(Array) x2.length.should == 3 x2[0].should == 1 x2[1].should == 2 x2[2].should equal(x2) end # Note, many of these tests will pass on Ruby 1.8 but fail on 1.9 if the patch # fix is not applied to Puppet or there's a regression. These version # dependant failures are intentional since the string encoding behavior changed # significantly in 1.9. context "UTF-8 encoded String#to_yaml (Bug #11246)" do # JJM All of these snowmen are different representations of the same # UTF-8 encoded string. let(:snowman) { 'Snowman: [☃]' } let(:snowman_escaped) { "Snowman: [\xE2\x98\x83]" } it "should serialize and deserialize to the same thing" do snowman.should round_trip_through_yaml end it "should serialize and deserialize to a String compatible with a UTF-8 encoded Regexp" do YAML.load(snowman.to_yaml).should =~ /☃/u end end context "binary data" do subject { "M\xC0\xDF\xE5tt\xF6" } if String.method_defined?(:encoding) def binary(str) str.force_encoding('binary') end else def binary(str) str end end it "should not explode encoding binary data" do expect { subject.to_yaml }.not_to raise_error end it "should mark the binary data as binary" do subject.to_yaml.should =~ /!binary/ end it "should round-trip the data" do yaml = subject.to_yaml read = YAML.load(yaml) binary(read).should == binary(subject) end [ "\xC0\xAE", # over-long UTF-8 '.' character "\xC0\x80", # over-long NULL byte "\xC0\xFF", "\xC1\xAE", "\xC1\x80", "\xC1\xFF", "\x80", # first continuation byte "\xbf", # last continuation byte # all possible continuation bytes in one shot "\x80\x81\x82\x83\x84\x85\x86\x87\x88\x89\x8A\x8B\x8C\x8D\x8E\x8F" + "\x90\x91\x92\x93\x94\x95\x96\x97\x98\x99\x9A\x9B\x9C\x9D\x9E\x9F" + "\xA0\xA1\xA2\xA3\xA4\xA5\xA6\xA7\xA8\xA9\xAA\xAB\xAC\xAD\xAE\xAF" + "\xB0\xB1\xB2\xB3\xB4\xB5\xB6\xB7\xB8\xB9\xBA\xBB\xBC\xBD\xBE\xBF", # lonely start characters - first, all possible two byte sequences "\xC0 \xC1 \xC2 \xC3 \xC4 \xC5 \xC6 \xC7 \xC8 \xC9 \xCA \xCB \xCC \xCD \xCE \xCF " + "\xD0 \xD1 \xD2 \xD3 \xD4 \xD5 \xD6 \xD7 \xD8 \xD9 \xDA \xDB \xDC \xDD \xDE \xDF ", # and so for three byte sequences, four, five, and six, as follow. "\xE0 \xE1 \xE2 \xE3 \xE4 \xE5 \xE6 \xE7 \xE8 \xE9 \xEA \xEB \xEC \xED \xEE \xEF ", "\xF0 \xF1 \xF2 \xF3 \xF4 \xF5 \xF6 \xF7 ", "\xF8 \xF9 \xFA \xFB ", "\xFC \xFD ", # sequences with the last byte missing "\xC0", "\xE0", "\xF0\x80\x80", "\xF8\x80\x80\x80", "\xFC\x80\x80\x80\x80", "\xDF", "\xEF\xBF", "\xF7\xBF\xBF", "\xFB\xBF\xBF\xBF", "\xFD\xBF\xBF\xBF\xBF", # impossible bytes "\xFE", "\xFF", "\xFE\xFE\xFF\xFF", # over-long '/' character "\xC0\xAF", "\xE0\x80\xAF", "\xF0\x80\x80\xAF", "\xF8\x80\x80\x80\xAF", "\xFC\x80\x80\x80\x80\xAF", # maximum overlong sequences "\xc1\xbf", "\xe0\x9f\xbf", "\xf0\x8f\xbf\xbf", "\xf8\x87\xbf\xbf\xbf", "\xfc\x83\xbf\xbf\xbf\xbf", # overlong NUL "\xc0\x80", "\xe0\x80\x80", "\xf0\x80\x80\x80", "\xf8\x80\x80\x80\x80", "\xfc\x80\x80\x80\x80\x80", ].each do |input| # It might seem like we should more correctly reject these sequences in # the encoder, and I would personally agree, but the sad reality is that # we do not distinguish binary and textual data in our language, and so we # wind up with the same thing - a string - containing both. # # That leads to the position where we must treat these invalid sequences, # which are both legitimate binary content, and illegitimate potential # attacks on the system, as something that passes through correctly in # a string. --daniel 2012-07-14 it "binary encode highly dubious non-compliant UTF-8 input #{input.inspect}" do encoded = ZAML.dump(binary(input)) encoded.should =~ /!binary/ YAML.load(encoded).should == input end end end context "multi-line values" do [ "none", "one\n", "two\n\n", ["one\n", "two"], ["two\n\n", "three"], { "\nkey" => "value" }, { "key\n" => "value" }, { "\nkey\n" => "value" }, { "key\nkey" => "value" }, { "\nkey\nkey" => "value" }, { "key\nkey\n" => "value" }, { "\nkey\nkey\n" => "value" }, ].each do |input| it "handles #{input.inspect} without corruption" do input.should round_trip_through_yaml end end end end