diff options
| author | Josh Cooper <josh@puppetlabs.com> | 2011-04-21 14:37:50 -0700 |
|---|---|---|
| committer | Josh Cooper <josh@puppetlabs.com> | 2011-04-21 14:37:50 -0700 |
| commit | 01f610bb223b435dc52f491260af3ea002930102 (patch) | |
| tree | 93565136d5ef2b4156fdd64476792e441bcfbb4e | |
| parent | ac428b9557e2da251e4b51e48de844833ca0aa2a (diff) | |
| parent | fc66e98b84b9a16728af054485883334a5887cca (diff) | |
| download | puppet-01f610bb223b435dc52f491260af3ea002930102.tar.gz puppet-01f610bb223b435dc52f491260af3ea002930102.tar.xz puppet-01f610bb223b435dc52f491260af3ea002930102.zip | |
Merge branch 'next'
103 files changed, 3597 insertions, 808 deletions
diff --git a/README.strings b/README.strings deleted file mode 100644 index 28289ee10..000000000 --- a/README.strings +++ /dev/null @@ -1,115 +0,0 @@ -Puppet Strings -================= -A set of executables that provide complete CLI access to Puppet's -core data types. They also provide String classes for -each of the core data types, which are extensible via plugins. - -For instance, you can create a new action for catalogs at -lib/puppet/string/catalog/$action.rb. - -This is a Puppet module and should work fine if you install it -in Puppet's module path. - -**Note that this only works with Puppet 2.6.next (and thus will work -with 2.6.5), because there is otherwise a bug in finding Puppet applications. -You also have to either install the lib files into your Puppet libdir, or -you need to add this lib directory to your RUBYLIB.** - -This is meant to be tested and iterated upon, with the plan that it will be -merged into Puppet core once we're satisfied with it. - -Usage ------ -The general usage is: - - $ puppet <string> <verb> <name> - -So, e.g.: - - $ puppet facts find myhost.domain.com - $ puppet node destroy myhost - -You can use it to list all known data types and the available terminus classes: - - $ puppet string list - catalog : active_record, compiler, queue, rest, yaml - certificate : ca, file, rest - certificate_request : ca, file, rest - certificate_revocation_list : ca, file, rest - file_bucket_file : file, rest - inventory : yaml - key : ca, file - node : active_record, exec, ldap, memory, plain, rest, yaml - report : processor, rest, yaml - resource : ral, rest - resource_type : parser, rest - status : local, rest - -But most interestingly, you can use it for two main purposes: - -* As a client for any Puppet REST server, such as catalogs, facts, reports, etc. -* As a local CLI for any local Puppet data - -A simple case is looking at the local facts: - - $ puppet facts find localhost - -If you're on the server, you can look in that server's fact collection: - - $ puppet facts --mode master --vardir /tmp/foo --terminus yaml find localhost - -Note that we're setting both the vardir and the 'mode', which switches from the default 'agent' mode to server mode (requires a patch in my branch). - -If you'd prefer the data be outputted in json instead of yaml, well, you can do that, too: - - $ puppet find --mode master facts --vardir /tmp/foo --terminus yaml --format pson localhost - -To test using it as an endpoint for compiling and retrieving catalogs from a remote server, (from my commit), try this: - - # Terminal 1 - $ sbin/puppetmasterd --trace --confdir /tmp/foo --vardir /tmp/foo --debug --manifest ~/bin/test.pp --certname localhost --no-daemonize - - # Terminal 2 - $ sbin/puppetd --trace --debug --confdir /tmp/foo --vardir /tmp/foo --certname localhost --server localhost --test --report - - # Terminal 3, actual testing - $ puppet catalog find localhost --certname localhost --server localhost --mode master --confdir /tmp/foo --vardir /tmp/foo --trace --terminus rest - -This compiles a test catalog (assuming that ~/bin/test.pp exists) and returns it. With the right auth setup, you can also get facts: - - $ puppet facts find localhost --certname localhost --server localhost --mode master --confdir /tmp/foo --vardir /tmp/foo --trace --terminus rest - -Or use IRB to do the same thing: - - $ irb - >> require 'puppet/string' - => true - >> string = Puppet::String[:facts, '1.0.0'] - => #<Puppet::String::Facts:0x1024a1390 @format=:yaml> - >> facts = string.find("myhost") - -Like I said, a prototype, but I'd love it if people would play it with some and make some recommendations. - -Extending ---------- -Like most parts of Puppet, these are easy to extend. Just drop a new action into a given string's directory. E.g.: - - $ cat lib/puppet/string/catalog/select.rb - # Select and show a list of resources of a given type. - Puppet::String.define(:catalog, '1.0.0') do - action :select do - invoke do |host,type| - catalog = Puppet::Resource::Catalog.indirection.find(host) - - catalog.resources.reject { |res| res.type != type }.each { |res| puts res } - end - end - end - $ puppet catalog select localhost Class - Class[main] - Class[Settings] - $ - -Notice that this gets loaded automatically when you try to use it. So, if you have a simple command you've written, such as for cleaning up nodes or diffing catalogs, you an port it to this framework and it should fit cleanly. - -Also note that strings are versioned. These version numbers are interpreted according to Semantic Versioning (http://semver.org). diff --git a/acceptance/pending/ticket_3360_allow_duplicate_csr_with_option_set.rb b/acceptance/pending/ticket_3360_allow_duplicate_csr_with_option_set.rb new file mode 100644 index 000000000..ba02227ea --- /dev/null +++ b/acceptance/pending/ticket_3360_allow_duplicate_csr_with_option_set.rb @@ -0,0 +1,50 @@ +test_name "#3360: Allow duplicate CSR when allow_duplicate_certs is on" + +agent_hostnames = agents.map {|a| a.to_s} + +# Kill running Puppet Master -- should not be running at this point +step "Master: kill running Puppet Master" +on master, "ps -U puppet | awk '/puppet/ { print \$1 }' | xargs kill || echo \"Puppet Master not running\"" + +step "Master: Start Puppet Master" +on master, puppet_master("--allow_duplicate_certs --certdnsnames=\"puppet:$(hostname -s):$(hostname -f)\" --verbose --noop") + +step "Generate a certificate request for the agent" +on agents, "puppet certificate generate `hostname -f` --ca-location remote --server #{master}" + +step "Collect the original certs" +on master, puppet_cert("--sign --all") +original_certs = on master, puppet_cert("--list --all") + +old_certs = {} +original_certs.stdout.each_line do |line| + if line =~ /^\+ (\S+) \((.+)\)$/ + old_certs[$1] = $2 + puts "old cert: #{$1} #{$2}" + end +end + +step "Make another request with the same certname" +on agents, "puppet certificate generate `hostname -f` --ca-location remote --server #{master}" + +step "Collect the new certs" + +on master, puppet_cert("--sign --all") +new_cert_list = on master, puppet_cert("--list --all") + +new_certs = {} +new_cert_list.stdout.each_line do |line| + if line =~ /^\+ (\S+) \((.+)\)$/ + new_certs[$1] = $2 + puts "new cert: #{$1} #{$2}" + end +end + +step "Verify the certs have changed" +# using the agent name as the key may cause errors; +# agent name from cfg file is likely to have short name +# where certs might be signed with long names. +old_certs.each_key { |key| + next if key.include? master # skip the masters cert, only care about agents + fail_test("#{key} does not have a new signed certificate") if old_certs[key] == new_certs[key] +} diff --git a/acceptance/pending/ticket_3360_reject_duplicate_csr_with_option_unset.rb b/acceptance/pending/ticket_3360_reject_duplicate_csr_with_option_unset.rb new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/acceptance/pending/ticket_3360_reject_duplicate_csr_with_option_unset.rb diff --git a/acceptance/pending/ticket_5027_warn_on_dynamic_scope.rb b/acceptance/pending/ticket_5027_warn_on_dynamic_scope.rb new file mode 100644 index 000000000..762116ce9 --- /dev/null +++ b/acceptance/pending/ticket_5027_warn_on_dynamic_scope.rb @@ -0,0 +1,28 @@ +test_name "#5027: Issue warnings when using dynamic scope" + +step "Apply dynamic scoping manifest on agents" +apply_manifest_on agents, %q{ + $foo = 'foo_value' + + class a { + $bar = 'bar_value' + + include b + } + + class b inherits c { + notify { $baz: } # should not generate a warning -- inherited from class c + notify { $bar: } # should generate a warning -- uses dynamic scoping + notify { $foo: } # should not generate a warning -- comes from top scope + } + + class c { + $baz = 'baz_value' + } + + include a +} + +step "Verify deprecation warning" +fail_test "Deprecation warning not issued" unless + stdout.include? 'warning: Dynamic lookup of $bar will not be supported in future versions. Use a fully-qualified variable name or parameterized classes.' diff --git a/acceptance/pending/ticket_6928_puppet_master_parse_fails.rb b/acceptance/pending/ticket_6928_puppet_master_parse_fails.rb new file mode 100644 index 000000000..aac53138a --- /dev/null +++ b/acceptance/pending/ticket_6928_puppet_master_parse_fails.rb @@ -0,0 +1,38 @@ +test_name "#6928: Puppet --parseonly should return deprication message" + +# Create good and bad formatted manifests +step "Master: create valid, invalid formatted manifests" +create_remote_file(master, '/tmp/good.pp', %w{notify{good:}} ) +create_remote_file(master, '/tmp/bad.pp', 'notify{bad:') + +step "Master: use --parseonly on an invalid manifest, should return 1 and issue deprecation warning" +on master, puppet_master( %w{--parseonly /tmp/bad.pp} ), :acceptable_exit_codes => [ 1 ] + fail_test "Deprecation warning not issued for --parseonly" unless + stdout.include? '--parseonly has been removed. Please use \'puppet parser validate <manifest>\'' + +step "Agents: create valid, invalid formatted manifests" +agents.each do |host| + create_remote_file(host, '/tmp/good.pp', %w{notify{good:}} ) + create_remote_file(host, '/tmp/bad.pp', 'notify{bad:') +end + +step "Agents: use --parseonly on an invalid manifest, should return 1 and issue deprecation warning" +agents.each do |host| + on(host, "puppet --parseonly /tmp/bad.pp}", :acceptable_exit_codes => [ 1 ]) do + fail_test "Deprecation warning not issued for --parseonly" unless + stdout.include? '--parseonly has been removed. Please use \'puppet parser validate <manifest>\'' + end +end + +step "Test Face for ‘parser validate’ with good manifest -- should pass" +agents.each do |host| + on(host, "puppet parser validate /tmp/good.pp", :acceptable_exit_codes => [ 0 ]) +end + +step "Test Face for ‘parser validate’ with bad manifest -- should fail" +agents.each do |host| + on(host, "puppet parser validate /tmp/bad.pp", :acceptable_exit_codes => [ 1 ]) do + fail_test "Bad manifest detection failed" unless + stderr.include? 'Could not run: Could not parse for environment production' + end +end diff --git a/acceptance/tests/stages/ticket_4655_default_stage_for_classes.rb b/acceptance/tests/stages/ticket_4655_default_stage_for_classes.rb new file mode 100644 index 000000000..1b0b537bf --- /dev/null +++ b/acceptance/tests/stages/ticket_4655_default_stage_for_classes.rb @@ -0,0 +1,39 @@ +test_name "#4655: Allow setting the default stage for parameterized classes" + +temp_file_name = "/tmp/4655-stage-in-parameterized-class.#{$$}" +test_manifest = <<HERE +stage { one: before => Stage[two] } +stage { two: before => Stage[three] } +stage { three: before => Stage[main] } + +class in_one { + exec { "echo 'in_one' > #{temp_file_name}": + path => '/usr/bin:/bin', + } +} +class { in_one: stage => "one" } + +class in_two( $stage=two ){ + exec { "echo 'in_two' >> #{temp_file_name}": + path => '/usr/bin:/bin', + } +} +class { in_two: } + +class in_three { + exec { "echo 'in_three' >> #{temp_file_name}": + path => '/usr/bin:/bin', + } +} +class { "in_three": stage => "three" } +HERE + +expected_results = "in_one +in_two +in_three +" +apply_manifest_on agents, test_manifest + +on(agents, "cat #{temp_file_name}").each do |result| + assert_equal(expected_results, "#{result.stdout}", "Unexpected result for host '#{result.host}'") +end diff --git a/lib/puppet/application/cert.rb b/lib/puppet/application/cert.rb index c08775380..162672b6a 100644 --- a/lib/puppet/application/cert.rb +++ b/lib/puppet/application/cert.rb @@ -61,9 +61,8 @@ but mostly used for signing certificate requests from puppet clients. USAGE ----- -puppet cert [-h|--help] [-V|--version] [-d|--debug] [-v|--verbose] - [-g|--generate] [-l|--list] [-s|--sign] [-r|--revoke] [-p|--print] - [-c|--clean] [--verify] [--digest <digest>] [--fingerprint] [host] +puppet cert <action> [-h|--help] [-V|--version] [-d|--debug] [-v|--verbose] + [--digest <digest>] [<host>] DESCRIPTION @@ -73,6 +72,51 @@ certificate requests, this script is available for signing outstanding requests. It can be used to list outstanding requests and then either sign them individually or sign all of them. +ACTIONS +------- + +Every action except 'list' and 'generate' requires a hostname to act on, +unless the '--all' option is set. + +* clean: + Revoke a host's certificate (if applicable) and remove all files + related to that host from puppet cert's storage. This is useful when + rebuilding hosts, since new certificate signing requests will only be + honored if puppet cert does not have a copy of a signed certificate + for that host. If '--all' is specified then all host certificates, + both signed and unsigned, will be removed. + +* fingerprint: + Print the DIGEST (defaults to md5) fingerprint of a host's + certificate. + +* generate: + Generate a certificate for a named client. A certificate/keypair will + be generated for each client named on the command line. + +* list: + List outstanding certificate requests. If '--all' is specified, signed + certificates are also listed, prefixed by '+', and revoked or invalid + certificates are prefixed by '-' (the verification outcome is printed + in parenthesis). + +* print: + Print the full-text version of a host's certificate. + +* revoke: + Revoke the certificate of a client. The certificate can be specified + either by its serial number (given as a decimal number or a + hexadecimal number prefixed by '0x') or by its hostname. The + certificate is revoked by adding it to the Certificate Revocation List + given by the 'cacrl' configuration option. Note that the puppet master + needs to be restarted after revoking certificates. + +* sign: + Sign an outstanding certificate request. + +* verify: + Verify the named certificate against the local CA certificate. + OPTIONS ------- @@ -88,72 +132,32 @@ configuration options can also be generated by running puppet cert with '--genconfig'. * --all: - Operate on all items. Currently only makes sense with '--sign', - '--clean', or '--list'. + Operate on all items. Currently only makes sense with the 'sign', + 'clean', 'list', and 'fingerprint' actions. * --digest: Set the digest for fingerprinting (defaults to md5). Valid values depends on your openssl and openssl ruby extension version, but should contain at least md5, sha1, md2, sha256. -* --clean: - Remove all files related to a host from puppet cert's storage. This is - useful when rebuilding hosts, since new certificate signing requests - will only be honored if puppet cert does not have a copy of a signed - certificate for that host. The certificate of the host is also - revoked. If '--all' is specified then all host certificates, both - signed and unsigned, will be removed. - * --debug: Enable full debugging. -* --generate: - Generate a certificate for a named client. A certificate/keypair will - be generated for each client named on the command line. - * --help: Print this help message -* --list: - List outstanding certificate requests. If '--all' is specified, signed - certificates are also listed, prefixed by '+', and revoked or invalid - certificates are prefixed by '-' (the verification outcome is printed - in parenthesis). - -* --print: - Print the full-text version of a host's certificate. - -* --fingerprint: - Print the DIGEST (defaults to md5) fingerprint of a host's - certificate. - -* --revoke: - Revoke the certificate of a client. The certificate can be specified - either by its serial number, given as a decimal number or a - hexadecimal number prefixed by '0x', or by its hostname. The - certificate is revoked by adding it to the Certificate Revocation List - given by the 'cacrl' config parameter. Note that the puppetmasterd - needs to be restarted after revoking certificates. - -* --sign: - Sign an outstanding certificate request. Unless '--all' is specified, - hosts must be listed after all flags. - * --verbose: Enable verbosity. * --version: Print the puppet version number and exit. -* --verify: - Verify the named certificate against the local CA certificate. - EXAMPLE ------- - $ puppet cert -l + $ puppet cert list culain.madstop.com - $ puppet cert -s culain.madstop.com + $ puppet cert sign culain.madstop.com AUTHOR diff --git a/lib/puppet/application/device.rb b/lib/puppet/application/device.rb new file mode 100644 index 000000000..df5bac26a --- /dev/null +++ b/lib/puppet/application/device.rb @@ -0,0 +1,255 @@ +require 'puppet/application' +require 'puppet/util/network_device' + + +class Puppet::Application::Device < Puppet::Application + + should_parse_config + run_mode :agent + + attr_accessor :args, :agent, :host + + def preinit + # Do an initial trap, so that cancels don't get a stack trace. + trap(:INT) do + $stderr.puts "Cancelling startup" + exit(0) + end + + { + :waitforcert => nil, + :detailed_exitcodes => false, + :verbose => false, + :debug => false, + :centrallogs => false, + :setdest => false, + }.each do |opt,val| + options[opt] = val + end + + @args = {} + end + + option("--centrallogging") + option("--debug","-d") + option("--verbose","-v") + + option("--detailed-exitcodes") do |arg| + options[:detailed_exitcodes] = true + end + + option("--logdest DEST", "-l DEST") do |arg| + begin + Puppet::Util::Log.newdestination(arg) + options[:setdest] = true + rescue => detail + puts detail.backtrace if Puppet[:debug] + $stderr.puts detail.to_s + end + end + + option("--waitforcert WAITFORCERT", "-w") do |arg| + options[:waitforcert] = arg.to_i + end + + option("--port PORT","-p") do |arg| + @args[:Port] = arg + end + + def help + <<-HELP + +puppet-device(8) -- Manage remote network devices +======== + +SYNOPSIS +-------- +Retrieves all configurations from the puppet master and apply +them to the remote devices configured in /etc/puppet/device.conf. + +Currently must be run out periodically, using cron or something similar. + +USAGE +----- + puppet device [-d|--debug] [--detailed-exitcodes] [-V|--version] + [-h|--help] [-l|--logdest syslog|<file>|console] + [-v|--verbose] [-w|--waitforcert <seconds>] + + +DESCRIPTION +----------- +Once the client has a signed certificate for a given remote device, it will +retrieve its configuration and apply it. + +USAGE NOTES +----------- +One need a /etc/puppet/device.conf file with the following content: + +[remote.device.fqdn] +type <type> +url <url> + +where: + * type: the current device type (the only value at this time is cisco) + * url: an url allowing to connect to the device + +Supported url must conforms to: + scheme://user:password@hostname/?query + + with: + * scheme: either ssh or telnet + * user: username, can be omitted depending on the switch/router configuration + * password: the connection password + * query: this is device specific. Cisco devices supports an enable parameter whose + value would be the enable password. + +OPTIONS +------- +Note that any configuration parameter that's valid in the configuration file +is also a valid long argument. For example, 'server' is a valid configuration +parameter, so you can specify '--server <servername>' as an argument. + +* --debug: + Enable full debugging. + +* --detailed-exitcodes: + Provide transaction information via exit codes. If this is enabled, an + exit code of '2' means there were changes, and an exit code of '4' means + that there were failures during the transaction. This option only makes + sense in conjunction with --onetime. + +* --help: + Print this help message + +* --logdest: + Where to send messages. Choose between syslog, the console, and a log file. + Defaults to sending messages to syslog, or the console if debugging or + verbosity is enabled. + +* --verbose: + Turn on verbose reporting. + +* --waitforcert: + This option only matters for daemons that do not yet have certificates + and it is enabled by default, with a value of 120 (seconds). This causes + +puppet agent+ to connect to the server every 2 minutes and ask it to sign a + certificate request. This is useful for the initial setup of a puppet + client. You can turn off waiting for certificates by specifying a time + of 0. + +EXAMPLE +------- + $ puppet device --server puppet.domain.com + +AUTHOR +------ +Brice Figureau + + +COPYRIGHT +--------- +Copyright (c) 2011 Puppet Labs, LLC +Licensed under the Apache 2.0 License + HELP + end + + + def main + vardir = Puppet[:vardir] + confdir = Puppet[:confdir] + certname = Puppet[:certname] + + # find device list + require 'puppet/util/network_device/config' + devices = Puppet::Util::NetworkDevice::Config.devices + if devices.empty? + Puppet.err "No device found in #{Puppet[:deviceconfig]}" + exit(1) + end + devices.each_value do |device| + begin + Puppet.info "starting applying configuration to #{device.name} at #{device.url}" + + # override local $vardir and $certname + Puppet.settings.set_value(:confdir, File.join(Puppet[:devicedir], device.name), :cli) + Puppet.settings.set_value(:vardir, File.join(Puppet[:devicedir], device.name), :cli) + Puppet.settings.set_value(:certname, device.name, :cli) + + # this will reload and recompute default settings and create the devices sub vardir, or we hope so :-) + Puppet.settings.use :main, :agent, :ssl + + # this init the device singleton, so that the facts terminus + # and the various network_device provider can use it + Puppet::Util::NetworkDevice.init(device) + + # ask for a ssl cert if needed, but at least + # setup the ssl system for this device. + setup_host + + require 'puppet/configurer' + configurer = Puppet::Configurer.new + report = configurer.run(:network_device => true) + rescue => detail + puts detail.backtrace if Puppet[:trace] + Puppet.err detail.to_s + ensure + Puppet.settings.set_value(:vardir, vardir, :cli) + Puppet.settings.set_value(:confdir, confdir, :cli) + Puppet.settings.set_value(:certname, certname, :cli) + end + end + end + + # Handle the logging settings. + def setup_logs + if options[:debug] or options[:verbose] + Puppet::Util::Log.newdestination(:console) + if options[:debug] + Puppet::Util::Log.level = :debug + else + Puppet::Util::Log.level = :info + end + end + + Puppet::Util::Log.newdestination(:syslog) unless options[:setdest] + end + + def setup_host + @host = Puppet::SSL::Host.new + waitforcert = options[:waitforcert] || (Puppet[:onetime] ? 0 : 120) + cert = @host.wait_for_cert(waitforcert) + end + + def setup + setup_logs + + args[:Server] = Puppet[:server] + if options[:centrallogs] + logdest = args[:Server] + + logdest += ":" + args[:Port] if args.include?(:Port) + Puppet::Util::Log.newdestination(logdest) + end + + Puppet.settings.use :main, :agent, :device, :ssl + + # Always ignoreimport for agent. It really shouldn't even try to import, + # but this is just a temporary band-aid. + Puppet[:ignoreimport] = true + + # We need to specify a ca location for all of the SSL-related i + # indirected classes to work; in fingerprint mode we just need + # access to the local files and we don't need a ca. + Puppet::SSL::Host.ca_location = :remote + + Puppet::Transaction::Report.indirection.terminus_class = :rest + + # Override the default; puppetd needs this, usually. + # You can still override this on the command-line with, e.g., :compiler. + Puppet[:catalog_terminus] = :rest + + Puppet[:facts_terminus] = :network_device + + Puppet::Resource::Catalog.indirection.cache_class = :yaml + end +end diff --git a/lib/puppet/application/face_base.rb b/lib/puppet/application/face_base.rb index 2a048a532..9da48af55 100644 --- a/lib/puppet/application/face_base.rb +++ b/lib/puppet/application/face_base.rb @@ -1,6 +1,7 @@ require 'puppet/application' require 'puppet/face' require 'optparse' +require 'pp' class Puppet::Application::FaceBase < Puppet::Application should_parse_config @@ -14,8 +15,8 @@ class Puppet::Application::FaceBase < Puppet::Application Puppet::Util::Log.level = :info end - option("--format FORMAT") do |arg| - @format = arg.to_sym + option("--render-as FORMAT") do |arg| + @render_as = arg.to_sym end option("--mode RUNMODE", "-r") do |arg| @@ -25,7 +26,7 @@ class Puppet::Application::FaceBase < Puppet::Application end - attr_accessor :face, :action, :type, :arguments, :format + attr_accessor :face, :action, :type, :arguments, :render_as attr_writer :exit_code # This allows you to set the exit code if you don't want to just exit @@ -34,17 +35,49 @@ class Puppet::Application::FaceBase < Puppet::Application @exit_code || 0 end - # Override this if you need custom rendering. def render(result) - render_method = Puppet::Network::FormatHandler.format(format).render_method - if render_method == "to_pson" - jj result - exit(0) + format = render_as || action.render_as || :for_humans + + # Invoke the rendering hook supplied by the user, if appropriate. + if hook = action.when_rendering(format) then + result = hook.call(result) + end + + if format == :for_humans then + render_for_humans(result) else - result.send(render_method) + render_method = Puppet::Network::FormatHandler.format(format).render_method + if render_method == "to_pson" + PSON::pretty_generate(result, :allow_nan => true, :max_nesting => false) + else + result.send(render_method) + end end end + def render_for_humans(result) + # String to String + return result if result.is_a? String + return result if result.is_a? Numeric + + # Simple hash to table + if result.is_a? Hash and result.keys.all? { |x| x.is_a? String or x.is_a? Numeric } + output = '' + column_a = result.map do |k,v| k.to_s.length end.max + 2 + column_b = 79 - column_a + result.sort_by { |k,v| k.to_s } .each do |key, value| + output << key.to_s.ljust(column_a) + output << PP.pp(value, '', column_b). + chomp.gsub(/\n */) { |x| x + (' ' * column_a) } + output << "\n" + end + return output + end + + # ...or pretty-print the inspect outcome. + return result.pretty_inspect + end + def preinit super Signal.trap(:INT) do @@ -59,9 +92,8 @@ class Puppet::Application::FaceBase < Puppet::Application # REVISIT: These should be configurable versions, through a global # '--version' option, but we don't implement that yet... --daniel 2011-03-29 - @type = self.class.name.to_s.sub(/.+:/, '').downcase.to_sym - @face = Puppet::Face[@type, :current] - @format = @face.default_format + @type = self.class.name.to_s.sub(/.+:/, '').downcase.to_sym + @face = Puppet::Face[@type, :current] # Now, walk the command line and identify the action. We skip over # arguments based on introspecting the action and all, and find the first @@ -94,16 +126,13 @@ class Puppet::Application::FaceBase < Puppet::Application raise OptionParser::InvalidOption.new(item.sub(/=.*$/, '')) end else - action = @face.get_action(item.to_sym) - if action.nil? then - raise OptionParser::InvalidArgument.new("#{@face} does not have an #{item} action") - end - @action = action + @action = @face.get_action(item.to_sym) end end - unless @action - raise OptionParser::MissingArgument.new("No action given on the command line") + if @action.nil? + @action = @face.get_default_action() + @is_default_action = true end # Now we can interact with the default option code to build behaviour @@ -111,7 +140,7 @@ class Puppet::Application::FaceBase < Puppet::Application @action.options.each do |option| option = @action.get_option(option) # make it the object. self.class.option(*option.optparse) # ...and make the CLI parse it. - end + end if @action # ...and invoke our parent to parse all the command line options. super @@ -138,7 +167,10 @@ class Puppet::Application::FaceBase < Puppet::Application # with it *always* being the first word of the remaining set of command # line arguments. So, strip that off when we construct the arguments to # pass down to the face action. --daniel 2011-04-04 - @arguments.delete_at(0) + # Of course, now that we have default actions, we should leave the + # "action" name on if we didn't actually consume it when we found our + # action. + @arguments.delete_at(0) unless @is_default_action # We copy all of the app options to the end of the call; This allows each # action to read in the options. This replaces the older model where we @@ -150,8 +182,17 @@ class Puppet::Application::FaceBase < Puppet::Application def main # Call the method associated with the provided action (e.g., 'find'). - if result = @face.send(@action.name, *arguments) - puts render(result) + if @action + result = @face.send(@action.name, *arguments) + puts render(result) unless result.nil? + else + if arguments.first.is_a? Hash + puts "#{@face} does not have a default action" + else + puts "#{@face} does not respond to action #{arguments.first}" + end + + puts Puppet::Face[:help, :current].help(@face.name, *arguments) end exit(exit_code) end diff --git a/lib/puppet/application/kick.rb b/lib/puppet/application/kick.rb index 536699442..4f3ed1802 100644 --- a/lib/puppet/application/kick.rb +++ b/lib/puppet/application/kick.rb @@ -76,31 +76,16 @@ copy things like LDAP settings. USAGE NOTES ----------- -'puppet kick' is useless unless 'puppet agent' is listening. See its -documentation for more information, but the gist is that you must enable -'listen' on the 'puppet agent' daemon, either using '--listen' on the -command line or adding 'listen = true' in its config file. In addition, -you need to set the daemons up to specifically allow connections by -creating the 'namespaceauth' file, normally at -'/etc/puppet/namespaceauth.conf'. This file specifies who has access to -each namespace; if you create the file you must add every namespace you -want any Puppet daemon to allow -- it is currently global to all Puppet -daemons. - -An example file looks like this: - - [fileserver] - allow *.madstop.com - - [puppetmaster] - allow *.madstop.com - - [puppetrunner] - allow culain.madstop.com - -This is what you would install on your Puppet master; non-master hosts -could leave off the 'fileserver' and 'puppetmaster' namespaces. - +Puppet kick is useless unless puppet agent is listening for incoming +connections and allowing access to the `run` endpoint. This entails +starting the agent with `listen = true` in its puppet.conf file, and +allowing access to the `/run` path in its auth.conf file; see +`http://docs.puppetlabs.com/guides/rest_auth_conf.html` for more +details. + +Additionally, due to a known bug, you must make sure a +namespaceauth.conf file exists in puppet agent's $confdir. This file +will not be consulted, and may be left empty. OPTIONS ------- diff --git a/lib/puppet/defaults.rb b/lib/puppet/defaults.rb index 680762b94..139c3c763 100644 --- a/lib/puppet/defaults.rb +++ b/lib/puppet/defaults.rb @@ -418,7 +418,7 @@ module Puppet :desc => "Where the puppet master web server logs." }, :masterport => [8140, "Which port puppet master listens on."], - :node_name => ["cert", "How the puppetmaster determines the client's identity + :node_name => ["cert", "How the puppet master determines the client's identity and sets the 'hostname', 'fqdn' and 'domain' facts for use in the manifest, in particular for determining which 'node' statement applies to the client. Possible values are 'cert' (use the subject's CN in the client's @@ -487,6 +487,11 @@ module Puppet This should match how often the hosts report back to the server."] ) + setdefaults(:device, + :devicedir => {:default => "$vardir/devices", :mode => "750", :desc => "The root directory of devices' $vardir"}, + :deviceconfig => ["$confdir/device.conf","Path to the device config file for puppet device"] + ) + setdefaults(:agent, :localconfig => { :default => "$statedir/localconfig", :owner => "root", @@ -523,10 +528,10 @@ module Puppet :runinterval => [1800, # 30 minutes "How often puppet agent applies the client configuration; in seconds."], :listen => [false, "Whether puppet agent should listen for - connections. If this is true, then by default only the - `runner` server is started, which allows remote authorized - and authenticated nodes to connect and trigger `puppet agent` - runs."], + connections. If this is true, then puppet agent will accept incoming + REST API requests, subject to the default ACLs and the ACLs set in + the `rest_authconfig` file. Puppet agent can respond usefully to + requests on the `run`, `facts`, `certificate`, and `resource` endpoints."], :ca_server => ["$server", "The server to use for certificate authority requests. It's a separate server because it cannot and does not need to horizontally scale."], @@ -623,8 +628,8 @@ module Puppet :graphdir => ["$statedir/graphs", "Where to store dot-outputted graphs."], :http_compression => [false, "Allow http compression in REST communication with the master. This setting might improve performance for agent -> master communications over slow WANs. - Your puppetmaster needs to support compression (usually by activating some settings in a reverse-proxy - in front of the puppetmaster, which rules out webrick). + Your puppet master needs to support compression (usually by activating some settings in a reverse-proxy + in front of the puppet master, which rules out webrick). It is harmless to activate this settings if your master doesn't support compression, but if it supports it, this setting might reduce performance on high-speed LANs."] ) diff --git a/lib/puppet/face/certificate.rb b/lib/puppet/face/certificate.rb index 77e80f099..4c2950fb3 100644 --- a/lib/puppet/face/certificate.rb +++ b/lib/puppet/face/certificate.rb @@ -2,24 +2,16 @@ require 'puppet/face/indirector' require 'puppet/ssl/host' Puppet::Face::Indirector.define(:certificate, '0.0.1') do - # REVISIT: This should use a pre-invoke hook to run the common code that - # needs to happen before we invoke any action; that would be much nicer than - # the "please repeat yourself" stuff found in here right now. - # - # option "--ca-location LOCATION" do - # type [:whatever, :location, :symbols] - # hook :before do |value| - # Puppet::SSL::Host.ca_location = value - # end - # end - # - # ...but should I pass the arguments as well? - # --daniel 2011-04-05 - option "--ca-location LOCATION" + option "--ca-location LOCATION" do + before_action do |action, args, options| + Puppet::SSL::Host.ca_location = options[:ca_location].to_sym + end + end action :generate do + summary "Generate a new Certificate Signing Request for HOST" + when_invoked do |name, options| - Puppet::SSL::Host.ca_location = options[:ca_location].to_sym host = Puppet::SSL::Host.new(name) host.generate_certificate_request host.certificate_request.class.indirection.save(host.certificate_request) @@ -27,8 +19,9 @@ Puppet::Face::Indirector.define(:certificate, '0.0.1') do end action :list do + summary "List all Certificate Signing Requests" + when_invoked do |options| - Puppet::SSL::Host.ca_location = options[:ca_location].to_sym Puppet::SSL::Host.indirection.search("*", { :for => :certificate_request, }).map { |h| h.inspect } @@ -36,8 +29,9 @@ Puppet::Face::Indirector.define(:certificate, '0.0.1') do end action :sign do + summary "Sign a Certificate Signing Request for HOST" + when_invoked do |name, options| - Puppet::SSL::Host.ca_location = options[:ca_location].to_sym host = Puppet::SSL::Host.new(name) host.desired_state = 'signed' Puppet::SSL::Host.indirection.save(host) diff --git a/lib/puppet/face/facts.rb b/lib/puppet/face/facts.rb index 8668b2531..04eab93a5 100644 --- a/lib/puppet/face/facts.rb +++ b/lib/puppet/face/facts.rb @@ -2,10 +2,10 @@ require 'puppet/face/indirector' require 'puppet/node/facts' Puppet::Face::Indirector.define(:facts, '0.0.1') do - set_default_format :yaml - # Upload our facts to the server action(:upload) do + render_as :yaml + when_invoked do |options| Puppet::Node::Facts.indirection.terminus_class = :facter facts = Puppet::Node::Facts.indirection.find(Puppet[:certname]) diff --git a/lib/puppet/face/help.rb b/lib/puppet/face/help.rb index 1c2da9e83..a762fb02e 100644 --- a/lib/puppet/face/help.rb +++ b/lib/puppet/face/help.rb @@ -13,12 +13,38 @@ Puppet::Face.define(:help, '0.0.1') do desc "Which version of the interface to show help for" end + default when_invoked do |*args| # Check our invocation, because we want varargs and can't do defaults # yet. REVISIT: when we do option defaults, and positional options, we # should rewrite this to use those. --daniel 2011-04-04 options = args.pop if options.nil? or args.length > 2 then + if args.select { |x| x == 'help' }.length > 2 then + c = "\n !\"'),-./7:;<GIJLST\\_`abcdefhiklmnoprstuwx|}".split('') + i = <<-'EOT'.gsub(/\s*/, '').to_i(36) + 2s7ytxy5vpj74kbab5xzf1ik2roinzlefaspjrzckiert5xbaxvwlku3a91w7y1rsd + nenp51gwpulmnrp54nwdil36fjgjarab801y0r5a9nh1hdfgi99arn5c5t3zhxbvzi + u6wx5r1tb7lun7pro69nrxunqqixsh6qmmv0ms0i0yycqw3pystyzmiita0lpxynqs + qkbjwadcx82n76wwpzbht8i8rgvqhqick8mk3cs3rvwdjookpgu0rxw4tcezned5sq + z5x8z9vntyyz0s4h6hjhtwtbytsmmu7ltvdftaixc7fkt276sqm48ab4yv0ot9y26n + z0xniy4pfl1x300lt6h9c8of49vf799ieuxwnoycsjlmtd4qntzit524j0tdn6n5aj + mq3z10apjuhkzprvmu53z1gnacymnoforrz5mbqto062kckgw5463pxwzg8liglub4 + ubnr0dln1s6iy3ummxuhim7m5a7yedl3gyy6ow4qqtmsigv27lysooau24zpsccsvx + ddwygjprqpbwon7i9s1279m1fpinvva8mfh6bgmotrpxsh1c8rc83l3u0utf5i200y + l7ui0ngcbcjyr4erzdee2tqk3fpjvb82t8xhncruhgn7j5dh2m914qzhb0gkoom47k + 6et7rp4tqjnrv0y2apk5qdl1x1hnbkkxup5ys6ip2ksmtpd3ipmrdtswxr5xwfiqtm + 60uyjr1v79irhnkrbbt4fwhgqjby1qflgwt9c1wpayzzucep6npgbn3f1k6cn4pug3 + 1u02wel4tald4hij8m5p49xr8u4ero1ucs5uht42o8nhpmpe7c7xf9t85i85m9m5kk + tgoqkgbu52gy5aoteyp8jkm3vri9fnkmwa5h60zt8otja72joxjb40p2rz2vp8f8q9 + nnggxt3x90pe5u4048ntyuha78q1oikhhpvw9j083yc3l00hz5ehv9c1au5gvctyap + zprub289qruve9qsyuh75j04wzkemqw3uhisrfs92u1ahv2qlqxmorgob16c1vbqkx + ttkoyp2agkt0v5l7lec25p0jqun9y39k41h67aeb5ihiqsftxc9azmg31hc73dk8ur + lj88vgbmgt8yln9rchw60whgxvnv9zn6cxbr482svctswc5a07atj + EOT + 607.times{i,x=i.divmod(1035);a,b=x.divmod(23);print(c[a]*b)} + raise ArgumentError, "Such panic is really not required." + end raise ArgumentError, "help only takes two (optional) arguments, a face name, and an action" end diff --git a/lib/puppet/face/indirector.rb b/lib/puppet/face/indirector.rb index f48611e4b..6c7708b51 100644 --- a/lib/puppet/face/indirector.rb +++ b/lib/puppet/face/indirector.rb @@ -5,6 +5,14 @@ class Puppet::Face::Indirector < Puppet::Face option "--terminus TERMINUS" do desc "REVISIT: You can select a terminus, which has some bigger effect that we should describe in this file somehow." + + before_action do |action, args, options| + set_terminus(options[:terminus]) + end + + after_action do |action, args, options| + indirection.reset_terminus_class + end end def self.indirections @@ -17,7 +25,6 @@ that we should describe in this file somehow." def call_indirection_method(method, *args) options = args.last - options.has_key?(:terminus) and set_terminus(options[:terminus]) begin result = indirection.__send__(method, *args) @@ -26,7 +33,6 @@ that we should describe in this file somehow." raise "Could not call '#{method}' on '#{indirection_name}': #{detail}" end - indirection.reset_terminus_class return result end @@ -49,16 +55,11 @@ that we should describe in this file somehow." # Print the configuration for the current terminus class action :info do when_invoked do |*args| - options = args.pop - options.has_key?(:terminus) and set_terminus(options[:terminus]) - if t = indirection.terminus_class puts "Run mode '#{Puppet.run_mode.name}': #{t}" else $stderr.puts "No default terminus class for run mode '#{Puppet.run_mode.name}'" end - - indirection.reset_terminus_class end end diff --git a/lib/puppet/face/node.rb b/lib/puppet/face/node.rb index fd1a548d6..12336df8f 100644 --- a/lib/puppet/face/node.rb +++ b/lib/puppet/face/node.rb @@ -1,5 +1,3 @@ require 'puppet/face/indirector' - Puppet::Face::Indirector.define(:node, '0.0.1') do - set_default_format :yaml end diff --git a/lib/puppet/face/parser.rb b/lib/puppet/face/parser.rb index c44810b99..d4aaaf043 100644 --- a/lib/puppet/face/parser.rb +++ b/lib/puppet/face/parser.rb @@ -6,7 +6,10 @@ Puppet::Face.define(:parser, '0.0.1') do when_invoked do |*args| args.pop files = args - files << Puppet[:manifest] if files.empty? + if files.empty? + files << Puppet[:manifest] + Puppet.notice "No manifest specified. Validating the default manifest #{Puppet[:manifest]}" + end files.each do |file| Puppet[:manifest] = file Puppet::Node::Environment.new(Puppet[:environment]).known_resource_types.clear diff --git a/lib/puppet/indirector/facts/network_device.rb b/lib/puppet/indirector/facts/network_device.rb new file mode 100644 index 000000000..c9bac803e --- /dev/null +++ b/lib/puppet/indirector/facts/network_device.rb @@ -0,0 +1,25 @@ +require 'puppet/node/facts' +require 'puppet/indirector/code' + +class Puppet::Node::Facts::NetworkDevice < Puppet::Indirector::Code + desc "Retrieve facts from a network device." + + # Look a device's facts up through the current device. + def find(request) + result = Puppet::Node::Facts.new(request.key, Puppet::Util::NetworkDevice.current.facts) + + result.add_local_facts + result.stringify + result.downcase_if_necessary + + result + end + + def destroy(facts) + raise Puppet::DevError, "You cannot destroy facts in the code store; it is only used for getting facts from a remote device" + end + + def save(facts) + raise Puppet::DevError, "You cannot save facts to the code store; it is only used for getting facts from a remote device" + end +end
\ No newline at end of file diff --git a/lib/puppet/indirector/queue.rb b/lib/puppet/indirector/queue.rb index fd089f431..85ffacacc 100644 --- a/lib/puppet/indirector/queue.rb +++ b/lib/puppet/indirector/queue.rb @@ -36,7 +36,7 @@ class Puppet::Indirector::Queue < Puppet::Indirector::Terminus def save(request) result = nil benchmark :info, "Queued #{indirection.name} for #{request.key}" do - result = client.send_message(queue, request.instance.render(:pson)) + result = client.publish_message(queue, request.instance.render(:pson)) end result rescue => detail diff --git a/lib/puppet/indirector/request.rb b/lib/puppet/indirector/request.rb index fd8d654dd..1918a3fb5 100644 --- a/lib/puppet/indirector/request.rb +++ b/lib/puppet/indirector/request.rb @@ -14,6 +14,51 @@ class Puppet::Indirector::Request OPTION_ATTRIBUTES = [:ip, :node, :authenticated, :ignore_terminus, :ignore_cache, :instance, :environment] + def self.from_pson(json) + raise ArgumentError, "No indirection name provided in json data" unless indirection_name = json['type'] + raise ArgumentError, "No method name provided in json data" unless method = json['method'] + raise ArgumentError, "No key provided in json data" unless key = json['key'] + + request = new(indirection_name, method, key, json['attributes']) + + if instance = json['instance'] + klass = Puppet::Indirector::Indirection.instance(request.indirection_name).model + if instance.is_a?(klass) + request.instance = instance + else + request.instance = klass.from_pson(instance) + end + end + + request + end + + def to_pson(*args) + result = { + 'document_type' => 'Puppet::Indirector::Request', + 'data' => { + 'type' => indirection_name, + 'method' => method, + 'key' => key + } + } + data = result['data'] + attributes = {} + OPTION_ATTRIBUTES.each do |key| + next unless value = send(key) + attributes[key] = value + end + + options.each do |opt, value| + attributes[opt] = value + end + + data['attributes'] = attributes unless attributes.empty? + data['instance'] = instance if instance + + result.to_pson(*args) + end + # Is this an authenticated request? def authenticated? # Double negative, so we just get true or false @@ -61,9 +106,11 @@ class Puppet::Indirector::Request self.indirection_name = indirection_name self.method = method + options = options.inject({}) { |hash, ary| hash[ary[0].to_sym] = ary[1]; hash } + set_attributes(options) - @options = options.inject({}) { |hash, ary| hash[ary[0].to_sym] = ary[1]; hash } + @options = options if key_or_instance.is_a?(String) || key_or_instance.is_a?(Symbol) key = key_or_instance @@ -153,7 +200,7 @@ class Puppet::Indirector::Request def set_attributes(options) OPTION_ATTRIBUTES.each do |attribute| - if options.include?(attribute) + if options.include?(attribute.to_sym) send(attribute.to_s + "=", options[attribute]) options.delete(attribute) end diff --git a/lib/puppet/interface.rb b/lib/puppet/interface.rb index 6570ebe46..5c8ade749 100644 --- a/lib/puppet/interface.rb +++ b/lib/puppet/interface.rb @@ -62,18 +62,35 @@ class Puppet::Interface end end - attr_accessor :default_format - def set_default_format(format) - self.default_format = format.to_sym + Puppet.warning("set_default_format is deprecated (and ineffective); use render_as on your actions instead.") end - attr_accessor :summary + ######################################################################## + # Documentation. We currently have to rewrite both getters because we share + # the same instance between build-time and the runtime instance. When that + # splits out this should merge into a module that both the action and face + # include. --daniel 2011-04-17 + attr_accessor :summary, :description def summary(value = nil) - @summary = value unless value.nil? + self.summary = value unless value.nil? @summary end + def summary=(value) + value = value.to_s + value =~ /\n/ and + raise ArgumentError, "Face summary should be a single line; put the long text in 'description' instead." + + @summary = value + end + + def description(value = nil) + self.description = value unless value.nil? + @description + end + + ######################################################################## attr_reader :name, :version def initialize(name, version, &block) @@ -83,33 +100,17 @@ class Puppet::Interface @name = Puppet::Interface::FaceCollection.underscorize(name) @version = version - @default_format = :pson instance_eval(&block) if block_given? end # Try to find actions defined in other files. def load_actions - path = "puppet/face/#{name}" - - loaded = [] - [path, "#{name}@#{version}/#{path}"].each do |path| - Puppet::Interface.autoloader.search_directories.each do |dir| - fdir = ::File.join(dir, path) - next unless FileTest.directory?(fdir) - - Dir.chdir(fdir) do - Dir.glob("*.rb").each do |file| - aname = file.sub(/\.rb/, '') - if loaded.include?(aname) - Puppet.debug "Not loading duplicate action '#{aname}' for '#{name}' from '#{fdir}/#{file}'" - next - end - loaded << aname - Puppet.debug "Loading action '#{aname}' for '#{name}' from '#{fdir}/#{file}'" - require "#{Dir.pwd}/#{aname}" - end - end + Puppet::Interface.autoloader.search_directories.each do |dir| + Dir.glob(File.join(dir, "puppet/face/#{name}", "*.rb")).each do |file| + action = file.sub(dir, '').sub(/^[\\\/]/, '').sub(/\.rb/, '') + Puppet.debug "Loading action '#{action}' for '#{name}' from '#{dir}/#{action}.rb'" + require(action) end end end @@ -117,4 +118,43 @@ class Puppet::Interface def to_s "Puppet::Face[#{name.inspect}, #{version.inspect}]" end + + ######################################################################## + # Action decoration, whee! You are not expected to care about this code, + # which exists to support face building and construction. I marked these + # private because the implementation is crude and ugly, and I don't yet know + # enough to work out how to make it clean. + # + # Once we have established that these methods will likely change radically, + # to be unrecognizable in the final outcome. At which point we will throw + # all this away, replace it with something nice, and work out if we should + # be making this visible to the outside world... --daniel 2011-04-14 + private + def __invoke_decorations(type, action, passed_args = [], passed_options = {}) + [:before, :after].member?(type) or fail "unknown decoration type #{type}" + + # Collect the decoration methods matching our pass. + methods = action.options.select do |name| + passed_options.has_key? name + end.map do |name| + action.get_option(name).__decoration_name(type) + end + + methods.each do |hook| + begin + respond_to? hook and self.__send__(hook, action, passed_args, passed_options) + rescue => e + Puppet.warning("invoking #{action} #{type} hook: #{e}") + end + end + end + + def __add_method(name, proc) + meta_def(name, &proc) + method(name).unbind + end + def self.__add_method(name, proc) + define_method(name, proc) + instance_method(name) + end end diff --git a/lib/puppet/interface/action.rb b/lib/puppet/interface/action.rb index db338e39e..23366b407 100644 --- a/lib/puppet/interface/action.rb +++ b/lib/puppet/interface/action.rb @@ -7,8 +7,10 @@ class Puppet::Interface::Action raise "#{name.inspect} is an invalid action name" unless name.to_s =~ /^[a-z]\w*$/ @face = face @name = name.to_sym - @options = {} attrs.each do |k, v| send("#{k}=", v) end + + @options = {} + @when_rendering = {} end # This is not nice, but it is the easiest way to make us behave like the @@ -20,11 +22,79 @@ class Puppet::Interface::Action return bound_version end - attr_reader :name def to_s() "#{@face}##{@name}" end + attr_reader :name + attr_accessor :default + def default? + !!@default + end + attr_accessor :summary + + ######################################################################## + # Support for rendering formats and all. + def when_rendering(type) + unless type.is_a? Symbol + raise ArgumentError, "The rendering format must be a symbol, not #{type.class.name}" + end + return unless @when_rendering.has_key? type + return @when_rendering[type].bind(@face) + end + def set_rendering_method_for(type, proc) + unless proc.is_a? Proc + msg = "The second argument to set_rendering_method_for must be a Proc" + msg += ", not #{proc.class.name}" unless proc.nil? + raise ArgumentError, msg + end + if proc.arity != 1 then + msg = "when_rendering methods take one argument, the result, not " + if proc.arity < 0 then + msg += "a variable number" + else + msg += proc.arity.to_s + end + raise ArgumentError, msg + end + unless type.is_a? Symbol + raise ArgumentError, "The rendering format must be a symbol, not #{type.class.name}" + end + if @when_rendering.has_key? type then + raise ArgumentError, "You can't define a rendering method for #{type} twice" + end + # Now, the ugly bit. We add the method to our interface object, and + # retrieve it, to rotate through the dance of getting a suitable method + # object out of the whole process. --daniel 2011-04-18 + @when_rendering[type] = + @face.__send__( :__add_method, __render_method_name_for(type), proc) + end + + def __render_method_name_for(type) + :"#{name}_when_rendering_#{type}" + end + private :__render_method_name_for + + + attr_accessor :render_as + def render_as=(value) + @render_as = value.to_sym + end + + + ######################################################################## + # Documentation stuff, whee! + attr_accessor :summary, :description + def summary=(value) + value = value.to_s + value =~ /\n/ and + raise ArgumentError, "Face summary should be a single line; put the long text in 'description' instead." + + @summary = value + end + + + ######################################################################## # Initially, this was defined to allow the @action.invoke pattern, which is # a very natural way to invoke behaviour given our introspection # capabilities. Heck, our initial plan was to have the faces delegate to @@ -82,13 +152,24 @@ class Puppet::Interface::Action # idea how motivated we were to make this cleaner. Sorry. --daniel 2011-03-31 internal_name = "#{@name} implementation, required on Ruby 1.8".to_sym - file = __FILE__ + "+eval" - line = __LINE__ + 1 - wrapper = "def #{@name}(*args, &block) - args << {} unless args.last.is_a? Hash - args << block if block_given? - self.__send__(#{internal_name.inspect}, *args) - end" + file = __FILE__ + "+eval" + line = __LINE__ + 1 + wrapper = <<WRAPPER +def #{@name}(*args) + if args.last.is_a? Hash then + options = args.last + else + args << (options = {}) + end + + action = get_action(#{name.inspect}) + action.validate_args(args) + __invoke_decorations(:before, action, args, options) + rval = self.__send__(#{internal_name.inspect}, *args) + __invoke_decorations(:after, action, args, options) + return rval +end +WRAPPER if @face.is_a?(Class) @face.class_eval do eval wrapper, nil, file, line end @@ -123,7 +204,28 @@ class Puppet::Interface::Action (@options.keys + @face.options).sort end - def get_option(name) - @options[name.to_sym] || @face.get_option(name) + def get_option(name, with_inherited_options = true) + option = @options[name.to_sym] + if option.nil? and with_inherited_options + option = @face.get_option(name) + end + option + end + + def validate_args(args) + required = options.map do |name| + get_option(name) + end.select(&:required?).collect(&:name) - args.last.keys + + return if required.empty? + raise ArgumentError, "missing required options (#{required.join(', ')})" + end + + ######################################################################## + # Support code for action decoration; see puppet/interface.rb for the gory + # details of why this is hidden away behind private. --daniel 2011-04-15 + private + def __add_method(name, proc) + @face.__send__ :__add_method, name, proc end end diff --git a/lib/puppet/interface/action_builder.rb b/lib/puppet/interface/action_builder.rb index 34bb3fa44..2ffa38709 100644 --- a/lib/puppet/interface/action_builder.rb +++ b/lib/puppet/interface/action_builder.rb @@ -20,16 +20,54 @@ class Puppet::Interface::ActionBuilder # method on the face would defer to it, but we can't get scope correct, so # we stick with this. --daniel 2011-03-24 def when_invoked(&block) - raise "when_invoked on an ActionBuilder with no corresponding Action" unless @action @action.when_invoked = block end + 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 + def option(*declaration, &block) option = Puppet::Interface::OptionBuilder.build(@action, *declaration, &block) @action.add_option(option) end - def summary(text) - @action.summary = text + def default(value = true) + @action.default = !!value + end + + def render_as(value = nil) + value.nil? and raise ArgumentError, "You must give a rendering format to render_as" + + formats = Puppet::Network::FormatHandler.formats << :for_humans + 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 =~ /^=/ + dsl = setter.sub(/=$/, '') + + unless private_instance_methods.include? dsl + # 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 #{dsl}(value) + @action.#{dsl} = value +end +METHOD + end end end diff --git a/lib/puppet/interface/action_manager.rb b/lib/puppet/interface/action_manager.rb index d75697afa..440be2d1b 100644 --- a/lib/puppet/interface/action_manager.rb +++ b/lib/puppet/interface/action_manager.rb @@ -5,8 +5,13 @@ module Puppet::Interface::ActionManager # the code to do so. def action(name, &block) @actions ||= {} + @default_action ||= nil raise "Action #{name} already defined for #{self}" if action?(name) action = Puppet::Interface::ActionBuilder.build(self, name, &block) + if action.default + raise "Actions #{@default_action.name} and #{name} cannot both be default" if @default_action + @default_action = action + end @actions[action.name] = action end @@ -50,6 +55,10 @@ module Puppet::Interface::ActionManager return result end + def get_default_action + @default_action + end + def action?(name) actions.include?(name.to_sym) end diff --git a/lib/puppet/interface/option.rb b/lib/puppet/interface/option.rb index ccc2fbba7..f4c56cb2c 100644 --- a/lib/puppet/interface/option.rb +++ b/lib/puppet/interface/option.rb @@ -1,19 +1,4 @@ -require 'puppet/interface' - class Puppet::Interface::Option - attr_reader :parent - attr_reader :name - attr_reader :aliases - attr_reader :optparse - attr_accessor :desc - - def takes_argument? - !!@argument - end - def optional_argument? - !!@optional_argument - end - def initialize(parent, *declaration, &block) @parent = parent @optparse = [] @@ -58,8 +43,9 @@ class Puppet::Interface::Option # Is our argument optional? The rules about consistency apply here, also, # just like they do to taking arguments at all. --daniel 2011-03-30 - @optional_argument = @optparse.any? { |o| o.include? "[" } - if @optional_argument and not @optparse.all? { |o| o.include? "[" } then + @optional_argument = @optparse.any? { |o| o=~/[ =]\[/ } + @optional_argument and raise ArgumentError, "Options with optional arguments are not supported" + if @optional_argument and not @optparse.all? { |o| o=~/[ =]\[/ } then raise ArgumentError, "Option #{@name} is inconsistent about the argument being optional" end end @@ -79,4 +65,40 @@ class Puppet::Interface::Option raise "#{name.inspect} is an invalid option name" unless name.to_s =~ /^[a-z]\w*$/ name.to_sym end + + + def takes_argument? + !!@argument + end + def optional_argument? + !!@optional_argument + end + def required? + !!@required + end + + attr_reader :parent, :name, :aliases, :optparse + attr_accessor :required, :desc + + attr_accessor :before_action + def before_action=(proc) + proc.is_a? Proc or raise ArgumentError, "before action hook for #{self} is a #{proc.class.name.inspect}, not a proc" + @before_action = + @parent.__send__(:__add_method, __decoration_name(:before), proc) + end + + attr_accessor :after_action + def after_action=(proc) + proc.is_a? Proc or raise ArgumentError, "after action hook for #{self} is a #{proc.class.name.inspect}, not a proc" + @after_action = + @parent.__send__(:__add_method, __decoration_name(:after), proc) + end + + def __decoration_name(type) + if @parent.is_a? Puppet::Interface::Action then + :"option #{name} from #{parent.name} #{type} decoration" + else + :"option #{name} #{type} decoration" + end + end end diff --git a/lib/puppet/interface/option_builder.rb b/lib/puppet/interface/option_builder.rb index 2240b3e4a..8f358c222 100644 --- a/lib/puppet/interface/option_builder.rb +++ b/lib/puppet/interface/option_builder.rb @@ -11,15 +11,44 @@ class Puppet::Interface::OptionBuilder def initialize(face, *declaration, &block) @face = face @option = Puppet::Interface::Option.new(face, *declaration) - block and instance_eval(&block) + instance_eval(&block) if block_given? @option end # Metaprogram the simple DSL from the option class. Puppet::Interface::Option.instance_methods.grep(/=$/).each do |setter| - next if setter =~ /^=/ # special case, darn it... + next if setter =~ /^=/ + dsl = setter.sub(/=$/, '') - dsl = setter.to_s.sub(/=$/, '') - define_method(dsl) do |value| @option.send(setter, value) end + unless private_instance_methods.include? dsl + define_method(dsl) do |value| @option.send(setter, value) end + end + end + + # Override some methods that deal in blocks, not objects. + def before_action(&block) + block or raise ArgumentError, "#{@option} before_action requires a block" + if @option.before_action + raise ArgumentError, "#{@option} already has a before_action set" + end + unless block.arity == 3 then + raise ArgumentError, "before_action takes three arguments, action, args, and options" + end + @option.before_action = block + end + + def after_action(&block) + block or raise ArgumentError, "#{@option} after_action requires a block" + if @option.after_action + raise ArgumentError, "#{@option} already has a after_action set" + end + unless block.arity == 3 then + raise ArgumentError, "after_action takes three arguments, action, args, and options" + end + @option.after_action = block + end + + def required(value = true) + @option.required = value end end diff --git a/lib/puppet/interface/option_manager.rb b/lib/puppet/interface/option_manager.rb index 56df9760f..d42359c07 100644 --- a/lib/puppet/interface/option_manager.rb +++ b/lib/puppet/interface/option_manager.rb @@ -37,10 +37,10 @@ module Puppet::Interface::OptionManager result.sort end - def get_option(name) + def get_option(name, with_inherited_options = true) @options ||= {} result = @options[name.to_sym] - unless result then + if result.nil? and with_inherited_options then if self.is_a?(Class) and superclass.respond_to?(:get_option) result = superclass.get_option(name) elsif self.class.respond_to?(:get_option) diff --git a/lib/puppet/network/http/handler.rb b/lib/puppet/network/http/handler.rb index 2b9e81b61..2c78a0283 100644 --- a/lib/puppet/network/http/handler.rb +++ b/lib/puppet/network/http/handler.rb @@ -122,10 +122,10 @@ module Puppet::Network::HTTP::Handler end # Execute our head. - def do_head(indirection_request, request, response) - unless indirection_request.model.head(indirection_request.key, indirection_request.to_hash) - Puppet.info("Could not find #{indirection_request.indirection_name} for '#{indirection_request.key}'") - return do_exception(response, "Could not find #{indirection_request.indirection_name} #{indirection_request.key}", 404) + def do_head(indirection_name, key, params, request, response) + unless self.model(indirection_name).indirection.head(key, params) + Puppet.info("Could not find #{indirection_name} for '#{key}'") + return do_exception(response, "Could not find #{indirection_name} #{key}", 404) end # No need to set a response because no response is expected from a diff --git a/lib/puppet/node.rb b/lib/puppet/node.rb index 5b0a98615..4bd4d1de6 100644 --- a/lib/puppet/node.rb +++ b/lib/puppet/node.rb @@ -20,6 +20,29 @@ class Puppet::Node attr_accessor :name, :classes, :source, :ipaddress, :parameters attr_reader :time + def self.from_pson(pson) + raise ArgumentError, "No name provided in pson data" unless name = pson['name'] + + node = new(name) + node.classes = pson['classes'] + node.parameters = pson['parameters'] + node.environment = pson['environment'] + node + end + + def to_pson(*args) + result = { + 'document_type' => "Puppet::Node", + 'data' => {} + } + result['data']['name'] = name + result['data']['classes'] = classes unless classes.empty? + result['data']['parameters'] = parameters unless parameters.empty? + result['data']['environment'] = environment.name + + result.to_pson(*args) + end + def environment return super if @environment diff --git a/lib/puppet/node/facts.rb b/lib/puppet/node/facts.rb index 577b62b62..2ff7156c8 100755 --- a/lib/puppet/node/facts.rb +++ b/lib/puppet/node/facts.rb @@ -61,18 +61,22 @@ class Puppet::Node::Facts def self.from_pson(data) result = new(data['name'], data['values']) - result.timestamp = Time.parse(data['timestamp']) - result.expiration = Time.parse(data['expiration']) + result.timestamp = Time.parse(data['timestamp']) if data['timestamp'] + result.expiration = Time.parse(data['expiration']) if data['expiration'] result end def to_pson(*args) - { - 'expiration' => expiration, - 'name' => name, - 'timestamp' => timestamp, - 'values' => strip_internal, - }.to_pson(*args) + result = { + 'document_type' => "Puppet::Node::Facts", + 'data' => {} + } + + result['data']['name'] = name + result['data']['expiration'] = expiration if expiration + result['data']['timestamp'] = timestamp if timestamp + result['data']['values'] = strip_internal + result.to_pson(*args) end # Add internal data to the facts for storage. diff --git a/lib/puppet/parser/compiler.rb b/lib/puppet/parser/compiler.rb index a891f1a11..613fcae74 100644 --- a/lib/puppet/parser/compiler.rb +++ b/lib/puppet/parser/compiler.rb @@ -56,23 +56,20 @@ class Puppet::Parser::Compiler # Note that this will fail if the resource is not unique. @catalog.add_resource(resource) + if resource.type.to_s.downcase != "class" && resource[:stage] + raise ArgumentError, "Only classes can set 'stage'; normal resources like #{resource} cannot change run stage" + end - # Add our container edge. If we're a class, then we get treated specially - we can - # control the stage that the class is applied in. Otherwise, we just - # get added to our parent container. + # 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.type.to_s.downcase == "stage" + # This adds a resource to the class it lexically appears in in the + # manifest. if resource.type.to_s.downcase != "class" - raise ArgumentError, "Only classes can set 'stage'; normal resources like #{resource} cannot change run stage" if resource[:stage] return @catalog.add_edge(scope.resource, resource) end - - unless stage = @catalog.resource(:stage, resource[:stage] || (scope && scope.resource && scope.resource[:stage]) || :main) - raise ArgumentError, "Could not find stage #{resource[:stage] || :main} specified by #{resource}" - end - - resource[:stage] ||= stage.title unless stage.title == :main - @catalog.add_edge(stage, resource) end # Do we use nodes found in the code, vs. the external node sources? diff --git a/lib/puppet/parser/resource.rb b/lib/puppet/parser/resource.rb index ace01bb4b..3bb5f8601 100644 --- a/lib/puppet/parser/resource.rb +++ b/lib/puppet/parser/resource.rb @@ -62,13 +62,30 @@ class Puppet::Parser::Resource < Puppet::Resource scope.environment end + # Process the stage metaparameter for a class. A containment edge + # is drawn from the class to the stage. The stage for containment + # defaults to main, if none is specified. + def add_edge_to_stage + return unless self.type.to_s.downcase == "class" + + unless stage = catalog.resource(:stage, self[:stage] || (scope && scope.resource && scope.resource[:stage]) || :main) + raise ArgumentError, "Could not find stage #{self[:stage] || :main} specified by #{self}" + end + + self[:stage] ||= stage.title unless stage.title == :main + catalog.add_edge(stage, self) + end + # Retrieve the associated definition and evaluate it. def evaluate return if evaluated? @evaluated = true if klass = resource_type and ! builtin_type? finish - return klass.evaluate_code(self) + evaluated_code = klass.evaluate_code(self) + add_edge_to_stage + + return evaluated_code elsif builtin? devfail "Cannot evaluate a builtin type (#{type})" else diff --git a/lib/puppet/parser/scope.rb b/lib/puppet/parser/scope.rb index 8de9d60b1..ed67cd141 100644 --- a/lib/puppet/parser/scope.rb +++ b/lib/puppet/parser/scope.rb @@ -101,7 +101,7 @@ class Puppet::Parser::Scope # Remove this when rebasing def environment - compiler.environment + compiler ? compiler.environment : nil end def find_hostclass(name) @@ -222,12 +222,12 @@ class Puppet::Parser::Scope private :qualified_scope - # Look up a variable. The simplest value search we do. + # Look up a variable. The simplest value search we do. def lookupvar(name, options = {}) table = ephemeral?(name) ? @ephemeral.last : @symtable # If the variable is qualified, then find the specified scope and look the variable up there instead. if name =~ /^(.*)::(.+)$/ - begin + begin qualified_scope($1).lookupvar($2,options) rescue RuntimeError => e location = (options[:file] && options[:line]) ? " at #{options[:file]}:#{options[:line]}" : '' @@ -238,7 +238,7 @@ class Puppet::Parser::Scope # We can't use "if table[name]" here because the value might be false if options[:dynamic] and self != compiler.topscope location = (options[:file] && options[:line]) ? " at #{options[:file]}:#{options[:line]}" : '' - Puppet.deprecation_warning "Dynamic lookup of $#{name}#{location} will not be supported in future versions. Use a fully-qualified variable name or parameterized classes." + Puppet.deprecation_warning "Dynamic lookup of $#{name}#{location} is deprecated. Support will be removed in Puppet 2.8. Use a fully-qualified variable name (e.g., $classname::variable) or parameterized classes." end table[name] elsif parent @@ -443,6 +443,6 @@ class Puppet::Parser::Scope def extend_with_functions_module extend Puppet::Parser::Functions.environment_module(Puppet::Node::Environment.root) - extend Puppet::Parser::Functions.environment_module(compiler ? environment : nil) + extend Puppet::Parser::Functions.environment_module(environment) end end diff --git a/lib/puppet/provider/cisco.rb b/lib/puppet/provider/cisco.rb new file mode 100644 index 000000000..918982f59 --- /dev/null +++ b/lib/puppet/provider/cisco.rb @@ -0,0 +1,9 @@ +require 'puppet/util/network_device/cisco/device' +require 'puppet/provider/network_device' + +# This is the base class of all prefetched cisco device providers +class Puppet::Provider::Cisco < Puppet::Provider::NetworkDevice + def self.device(url) + Puppet::Util::NetworkDevice::Cisco::Device.new(url) + end +end diff --git a/lib/puppet/provider/interface/cisco.rb b/lib/puppet/provider/interface/cisco.rb index f3bd202e9..795a7f1ac 100644 --- a/lib/puppet/provider/interface/cisco.rb +++ b/lib/puppet/provider/interface/cisco.rb @@ -1,22 +1,20 @@ -require 'puppet/util/network_device/cisco/device' -require 'puppet/provider/network_device' +require 'puppet/provider/cisco' -Puppet::Type.type(:interface).provide :cisco, :parent => Puppet::Provider::NetworkDevice do +Puppet::Type.type(:interface).provide :cisco, :parent => Puppet::Provider::Cisco do desc "Cisco switch/router provider for interface." mk_resource_methods - def self.lookup(url, name) + def self.lookup(device, name) interface = nil - network_gear = Puppet::Util::NetworkDevice::Cisco::Device.new(url) - network_gear.command do |ng| - interface = network_gear.interface(name) + device.command do |ng| + interface = device.interface(name) end interface end - def initialize(*args) + def initialize(device, *args) super end @@ -26,8 +24,4 @@ Puppet::Type.type(:interface).provide :cisco, :parent => Puppet::Provider::Netwo end super end - - def device - @device ||= Puppet::Util::NetworkDevice::Cisco::Device.new(resource[:device_url]) - end end diff --git a/lib/puppet/provider/network_device.rb b/lib/puppet/provider/network_device.rb index 58865fddc..46be27968 100644 --- a/lib/puppet/provider/network_device.rb +++ b/lib/puppet/provider/network_device.rb @@ -2,17 +2,22 @@ # This is the base class of all prefetched network device provider class Puppet::Provider::NetworkDevice < Puppet::Provider - def self.lookup(url, name) + def self.device(url) + raise "This provider doesn't implement the necessary device method" + end + + def self.lookup(device, name) raise "This provider doesn't implement the necessary lookup method" end def self.prefetch(resources) resources.each do |name, resource| - if result = lookup(resource[:device_url], name) + device = Puppet::Util::NetworkDevice.current || device(resource[:device_url]) + if result = lookup(device, name) result[:ensure] = :present - resource.provider = new(result) + resource.provider = new(device, result) else - resource.provider = new(:ensure => :absent) + resource.provider = new(device, :ensure => :absent) end end end @@ -21,8 +26,12 @@ class Puppet::Provider::NetworkDevice < Puppet::Provider @property_hash[:ensure] != :absent end - def initialize(*args) - super + attr_accessor :device + + def initialize(device, *args) + super(*args) + + @device = device # Make a duplicate, so that we have a copy for comparison # at the end. @@ -56,4 +65,4 @@ class Puppet::Provider::NetworkDevice < Puppet::Provider def properties @property_hash.dup end -end
\ No newline at end of file +end diff --git a/lib/puppet/provider/package/aptitude.rb b/lib/puppet/provider/package/aptitude.rb index 8bdf984e6..2eafd3ef8 100755 --- a/lib/puppet/provider/package/aptitude.rb +++ b/lib/puppet/provider/package/aptitude.rb @@ -12,6 +12,7 @@ Puppet::Type.type(:package).provide :aptitude, :parent => :apt, :source => :dpkg args.flatten! # Apparently aptitude hasn't always supported a -q flag. args.delete("-q") if args.include?("-q") + args.delete("--force-yes") if args.include?("--force-yes") output = aptitude(*args) # Yay, stupid aptitude doesn't throw an error when the package is missing. diff --git a/lib/puppet/provider/package/pkgutil.rb b/lib/puppet/provider/package/pkgutil.rb new file mode 100755 index 000000000..a1d844f73 --- /dev/null +++ b/lib/puppet/provider/package/pkgutil.rb @@ -0,0 +1,175 @@ +# Packaging using Peter Bonivart's pkgutil program. +Puppet::Type.type(:package).provide :pkgutil, :parent => :sun, :source => :sun do + desc "Package management using Peter Bonivart's ``pkgutil`` command on Solaris." + + pkgutil_bin = "pkgutil" + if FileTest.executable?("/opt/csw/bin/pkgutil") + pkgutil_bin = "/opt/csw/bin/pkgutil" + end + + confine :operatingsystem => :solaris + + commands :pkguti => pkgutil_bin + + def self.healthcheck() + unless FileTest.exists?("/var/opt/csw/pkgutil/admin") + Puppet.notice "It is highly recommended you create '/var/opt/csw/pkgutil/admin'." + Puppet.notice "See /var/opt/csw/pkgutil" + end + + correct_wgetopts = false + [ "/opt/csw/etc/pkgutil.conf", "/etc/opt/csw/pkgutil.conf" ].each do |confpath| + File.open(confpath) do |conf| + conf.each {|line| correct_wgetopts = true if line =~ /^\s*wgetopts\s*=.*(-nv|-q|--no-verbose|--quiet)/ } + end + end + if ! correct_wgetopts + Puppet.notice "It is highly recommended that you set 'wgetopts=-nv' in your pkgutil.conf." + end + end + + def self.instances(hash = {}) + healthcheck + + # Use the available pkg list (-a) to work out aliases + aliases = {} + availlist.each do |pkg| + aliases[pkg[:name]] = pkg[:alias] + end + + # The -c pkglist lists installed packages + pkginsts = [] + pkglist(hash).each do |pkg| + pkg.delete(:avail) + pkginsts << new(pkg) + + # Create a second instance with the alias if it's different + pkgalias = aliases[pkg[:name]] + if pkgalias and pkg[:name] != pkgalias + apkg = pkg.dup + apkg[:name] = pkgalias + pkginsts << new(apkg) + end + end + + pkginsts + end + + # Turns a pkgutil -a listing into hashes with the common alias, full + # package name and available version + def self.availlist + output = pkguti ["-a"] + + list = output.split("\n").collect do |line| + next if line =~ /^common\s+package/ # header of package list + next if noise?(line) + + if line =~ /\s*(\S+)\s+(\S+)\s+(.*)/ + { :alias => $1, :name => $2, :avail => $3 } + else + Puppet.warning "Cannot match %s" % line + end + end.reject { |h| h.nil? } + end + + # Turn our pkgutil -c listing into a bunch of hashes. + # Supports :justme => packagename, which uses the optimised --single arg + def self.pkglist(hash) + command = ["-c"] + + if hash[:justme] + # The --single option speeds up the execution, because it queries + # the package managament system for one package only. + command << "--single" + command << hash[:justme] + end + + output = pkguti(command).split("\n") + + if output[-1] == "Not in catalog" + Puppet.warning "Package not in pkgutil catalog: %s" % hash[:justme] + return nil + end + + list = output.collect do |line| + next if line =~ /installed\s+catalog/ # header of package list + next if noise?(line) + + pkgsplit(line) + end.reject { |h| h.nil? } + + if hash[:justme] + # Single queries may have been for an alias so return the name requested + if list.any? + list[-1][:name] = hash[:justme] + return list[-1] + end + else + list.reject! { |h| h[:ensure] == :absent } + return list + end + end + + # Identify common types of pkgutil noise as it downloads catalogs etc + def self.noise?(line) + true if line =~ /^#/ + true if line =~ /^Checking integrity / # use_gpg + true if line =~ /^gpg: / # gpg verification + true if line =~ /^=+> / # catalog fetch + true if line =~ /\d+:\d+:\d+ URL:/ # wget without -q + false + end + + # Split the different lines into hashes. + def self.pkgsplit(line) + if line =~ /\s*(\S+)\s+(\S+)\s+(.*)/ + hash = {} + hash[:name] = $1 + hash[:ensure] = if $2 == "notinst" + :absent + else + $2 + end + hash[:avail] = $3 + + if hash[:avail] =~ /^SAME\s*$/ + hash[:avail] = hash[:ensure] + end + + # Use the name method, so it works with subclasses. + hash[:provider] = self.name + + return hash + else + Puppet.warning "Cannot match %s" % line + return nil + end + end + + def install + pkguti "-y", "-i", @resource[:name] + end + + # Retrieve the version from the current package file. + def latest + hash = self.class.pkglist(:justme => @resource[:name]) + hash[:avail] if hash + end + + def query + if hash = self.class.pkglist(:justme => @resource[:name]) + hash + else + {:ensure => :absent} + end + end + + def update + pkguti "-y", "-u", @resource[:name] + end + + def uninstall + pkguti "-y", "-r", @resource[:name] + end +end + diff --git a/lib/puppet/provider/vlan/cisco.rb b/lib/puppet/provider/vlan/cisco.rb index 46e172c73..3421d35b0 100644 --- a/lib/puppet/provider/vlan/cisco.rb +++ b/lib/puppet/provider/vlan/cisco.rb @@ -1,22 +1,20 @@ -require 'puppet/util/network_device/cisco/device' -require 'puppet/provider/network_device' +require 'puppet/provider/cisco' -Puppet::Type.type(:vlan).provide :cisco, :parent => Puppet::Provider::NetworkDevice do +Puppet::Type.type(:vlan).provide :cisco, :parent => Puppet::Provider::Cisco do desc "Cisco switch/router provider for vlans." mk_resource_methods - def self.lookup(url, id) + def self.lookup(device, id) vlans = {} - device = Puppet::Util::NetworkDevice::Cisco::Device.new(url) device.command do |d| vlans = d.parse_vlans || {} end vlans[id] end - def initialize(*args) + def initialize(device, *args) super end @@ -27,8 +25,4 @@ Puppet::Type.type(:vlan).provide :cisco, :parent => Puppet::Provider::NetworkDev end super end - - def device - @device ||= Puppet::Util::NetworkDevice::Cisco::Device.new(resource[:device_url]) - end end diff --git a/lib/puppet/resource/catalog.rb b/lib/puppet/resource/catalog.rb index 98c29657e..a6cff9bdc 100644 --- a/lib/puppet/resource/catalog.rb +++ b/lib/puppet/resource/catalog.rb @@ -133,6 +133,7 @@ class Puppet::Resource::Catalog < Puppet::SimpleGraph transaction.report = options[:report] if options[:report] transaction.tags = options[:tags] if options[:tags] transaction.ignoreschedules = true if options[:ignoreschedules] + transaction.for_network_device = options[:network_device] transaction.add_times :config_retrieval => self.retrieval_duration || 0 diff --git a/lib/puppet/transaction.rb b/lib/puppet/transaction.rb index d7845fbc9..3728a2fff 100644 --- a/lib/puppet/transaction.rb +++ b/lib/puppet/transaction.rb @@ -12,7 +12,7 @@ class Puppet::Transaction require 'puppet/transaction/resource_harness' require 'puppet/resource/status' - attr_accessor :component, :catalog, :ignoreschedules + attr_accessor :component, :catalog, :ignoreschedules, :for_network_device attr_accessor :configurator # The report, once generated. @@ -339,6 +339,8 @@ class Puppet::Transaction resource.warning "Skipping because of failed dependencies" elsif resource.virtual? resource.debug "Skipping because virtual" + elsif resource.appliable_to_device? ^ for_network_device + resource.debug "Skipping #{resource.appliable_to_device? ? 'device' : 'host'} resources because running on a #{for_network_device ? 'device' : 'host'}" else return false end diff --git a/lib/puppet/type.rb b/lib/puppet/type.rb index c0e5d390b..656b8f264 100644 --- a/lib/puppet/type.rb +++ b/lib/puppet/type.rb @@ -116,6 +116,26 @@ class Type ens end + def self.apply_to_device + @apply_to = :device + end + + def self.apply_to_host + @apply_to = :host + end + + def self.apply_to_all + @apply_to = :both + end + + def self.apply_to + @apply_to ||= :host + end + + def self.can_apply_to(target) + [ target == :device ? :device : :host, :both ].include?(apply_to) + end + # Deal with any options passed into parameters. def self.handle_param_options(name, options) # If it's a boolean parameter, create a method to test the value easily @@ -1895,6 +1915,14 @@ class Type def virtual?; !!@virtual; end def exported?; !!@exported; end + + def appliable_to_device? + self.class.can_apply_to(:device) + end + + def appliable_to_host? + self.class.can_apply_to(:host) + end end end diff --git a/lib/puppet/type/interface.rb b/lib/puppet/type/interface.rb index 7560a0552..d3b5cb06f 100644 --- a/lib/puppet/type/interface.rb +++ b/lib/puppet/type/interface.rb @@ -10,6 +10,8 @@ Puppet::Type.newtype(:interface) do interface mode (access or trunking, native vlan and encapsulation), switchport characteristics (speed, duplex)." + apply_to_device + ensurable do defaultvalues diff --git a/lib/puppet/type/schedule.rb b/lib/puppet/type/schedule.rb index 5fb008f6f..f60f96fd2 100755 --- a/lib/puppet/type/schedule.rb +++ b/lib/puppet/type/schedule.rb @@ -43,6 +43,8 @@ module Puppet This will cause resources to be applied every 30 minutes by default. " + apply_to_all + newparam(:name) do desc "The name of the schedule. This name is used to retrieve the schedule when assigning it to an object: diff --git a/lib/puppet/type/vlan.rb b/lib/puppet/type/vlan.rb index 6708ea4f5..e39daf994 100644 --- a/lib/puppet/type/vlan.rb +++ b/lib/puppet/type/vlan.rb @@ -5,6 +5,8 @@ Puppet::Type.newtype(:vlan) do @doc = "This represents a router or switch vlan." + apply_to_device + ensurable newparam(:name) do diff --git a/lib/puppet/util/command_line.rb b/lib/puppet/util/command_line.rb index a884b8658..714d03f74 100644 --- a/lib/puppet/util/command_line.rb +++ b/lib/puppet/util/command_line.rb @@ -14,7 +14,8 @@ module Puppet 'queue' => 'puppetqd', 'resource' => 'ralsh', 'kick' => 'puppetrun', - 'master' => 'puppetmasterd' + 'master' => 'puppetmasterd', + 'device' => 'puppetdevice' ) def initialize(zero = $0, argv = ARGV, stdin = STDIN) diff --git a/lib/puppet/util/network_device.rb b/lib/puppet/util/network_device.rb index bca66016b..7fb8e2ff3 100644 --- a/lib/puppet/util/network_device.rb +++ b/lib/puppet/util/network_device.rb @@ -1,2 +1,17 @@ -module Puppet::Util::NetworkDevice -end
\ No newline at end of file +class Puppet::Util::NetworkDevice + class << self + attr_reader :current + end + + def self.init(device) + require "puppet/util/network_device/#{device.provider}/device" + @current = Puppet::Util::NetworkDevice.const_get(device.provider.capitalize).const_get(:Device).new(device.url) + rescue => detail + raise "Can't load #{device.provider} for #{device.name}: #{detail}" + end + + # Should only be used in tests + def self.teardown + @current = nil + end +end diff --git a/lib/puppet/util/network_device/base.rb b/lib/puppet/util/network_device/base.rb index ff96c8693..7d6c3fc44 100644 --- a/lib/puppet/util/network_device/base.rb +++ b/lib/puppet/util/network_device/base.rb @@ -3,27 +3,25 @@ require 'uri' require 'puppet/util/network_device/transport' require 'puppet/util/network_device/transport/base' -module Puppet::Util::NetworkDevice - class Base +class Puppet::Util::NetworkDevice::Base - attr_accessor :url, :transport + attr_accessor :url, :transport - def initialize(url) - @url = URI.parse(url) + def initialize(url) + @url = URI.parse(url) - @autoloader = Puppet::Util::Autoload.new( - self, - "puppet/util/network_device/transport", - :wrap => false - ) + @autoloader = Puppet::Util::Autoload.new( + self, + "puppet/util/network_device/transport", + :wrap => false + ) - if @autoloader.load(@url.scheme) - @transport = Puppet::Util::NetworkDevice::Transport.const_get(@url.scheme.capitalize).new - @transport.host = @url.host - @transport.port = @url.port || case @url.scheme ; when "ssh" ; 22 ; when "telnet" ; 23 ; end - @transport.user = @url.user - @transport.password = @url.password - end + if @autoloader.load(@url.scheme) + @transport = Puppet::Util::NetworkDevice::Transport.const_get(@url.scheme.capitalize).new + @transport.host = @url.host + @transport.port = @url.port || case @url.scheme ; when "ssh" ; 22 ; when "telnet" ; 23 ; end + @transport.user = @url.user + @transport.password = @url.password end end end
\ No newline at end of file diff --git a/lib/puppet/util/network_device/cisco/device.rb b/lib/puppet/util/network_device/cisco/device.rb index 1f350991a..005470e13 100644 --- a/lib/puppet/util/network_device/cisco/device.rb +++ b/lib/puppet/util/network_device/cisco/device.rb @@ -3,6 +3,7 @@ require 'puppet/util' require 'puppet/util/network_device/base' require 'puppet/util/network_device/ipcalc' require 'puppet/util/network_device/cisco/interface' +require 'puppet/util/network_device/cisco/facts' require 'ipaddr' class Puppet::Util::NetworkDevice::Cisco::Device < Puppet::Util::NetworkDevice::Base @@ -91,6 +92,15 @@ class Puppet::Util::NetworkDevice::Cisco::Device < Puppet::Util::NetworkDevice:: interface end + def facts + @facts ||= Puppet::Util::NetworkDevice::Cisco::Facts.new(transport) + facts = {} + command do |ng| + facts = @facts.retrieve + end + facts + end + def interface(name) ifname = canonalize_ifname(name) interface = parse_interface(ifname) diff --git a/lib/puppet/util/network_device/cisco/facts.rb b/lib/puppet/util/network_device/cisco/facts.rb new file mode 100644 index 000000000..40e2b37c2 --- /dev/null +++ b/lib/puppet/util/network_device/cisco/facts.rb @@ -0,0 +1,72 @@ + +require 'puppet/util/network_device/cisco' +require 'puppet/util/network_device/ipcalc' + +# this retrieves facts from a cisco device +class Puppet::Util::NetworkDevice::Cisco::Facts + + attr_reader :transport + + def initialize(transport) + @transport = transport + end + + def retrieve + facts = {} + facts.merge(parse_show_ver) + end + + def parse_show_ver + facts = {} + out = @transport.command("sh ver") + lines = out.split("\n") + lines.shift; lines.pop + lines.each do |l| + case l + # cisco WS-C2924C-XL (PowerPC403GA) processor (revision 0x11) with 8192K/1024K bytes of memory. + # Cisco 1841 (revision 5.0) with 355328K/37888K bytes of memory. + # Cisco 877 (MPC8272) processor (revision 0x200) with 118784K/12288K bytes of memory. + # cisco WS-C2960G-48TC-L (PowerPC405) processor (revision C0) with 61440K/4088K bytes of memory. + # cisco WS-C2950T-24 (RC32300) processor (revision R0) with 19959K bytes of memory. + when /[cC]isco ([\w-]+) (?:\(([\w-]+)\) processor )?\(revision (.+)\) with (\d+[KMG])(?:\/(\d+[KMG]))? bytes of memory\./ + facts[:hardwaremodel] = $1 + facts[:processor] = $2 if $2 + facts[:hardwarerevision] = $3 + facts[:memorysize] = $4 + # uptime + # Switch uptime is 1 year, 12 weeks, 6 days, 22 hours, 32 minutes + # c2950 uptime is 3 weeks, 1 day, 23 hours, 36 minutes + # c2960 uptime is 2 years, 27 weeks, 5 days, 21 hours, 30 minutes + # router uptime is 5 weeks, 1 day, 3 hours, 30 minutes + when /^\s*([\w-]+)\s+uptime is (.*?)$/ + facts[:hostname] = $1 + facts[:uptime] = $2 + facts[:uptime_seconds] = uptime_to_seconds($2) + facts[:uptime_days] = facts[:uptime_seconds] / 86400 + # "IOS (tm) C2900XL Software (C2900XL-C3H2S-M), Version 12.0(5)WC10, RELEASE SOFTWARE (fc1)"=> { :operatingsystem => "IOS", :operatingsystemrelease => "12.0(5)WC10", :operatingsystemmajrelease => "12.0", :operatingsystemfeature => "C3H2S"}, + # "IOS (tm) C2950 Software (C2950-I6K2L2Q4-M), Version 12.1(22)EA8a, RELEASE SOFTWARE (fc1)"=> { :operatingsystem => "IOS", :operatingsystemrelease => "12.1(22)EA8a", :operatingsystemmajrelease => "12.1", :operatingsystemfeature => "I6K2L2Q4"}, + # "Cisco IOS Software, C2960 Software (C2960-LANBASEK9-M), Version 12.2(44)SE, RELEASE SOFTWARE (fc1)"=>{ :operatingsystem => "IOS", :operatingsystemrelease => "12.2(44)SE", :operatingsystemmajrelease => "12.2", :operatingsystemfeature => "LANBASEK9"}, + # "Cisco IOS Software, C870 Software (C870-ADVIPSERVICESK9-M), Version 12.4(11)XJ4, RELEASE SOFTWARE (fc2)"=>{ :operatingsystem => "IOS", :operatingsystemrelease => "12.4(11)XJ40", :operatingsystemmajrelease => "12.4XJ", :operatingsystemfeature => "ADVIPSERVICESK9"}, + # "Cisco IOS Software, 1841 Software (C1841-ADVSECURITYK9-M), Version 12.4(24)T4, RELEASE SOFTWARE (fc2)" =>{ :operatingsystem => "IOS", :operatingsystemrelease => "12.4(24)T4", :operatingsystemmajrelease => "12.4T", :operatingsystemfeature => "ADVSECURITYK9"}, + when /(?:Cisco )?(IOS)\s*(?:\(tm\) |Software, )?(?:\w+)\s+Software\s+\(\w+-(\w+)-\w+\), Version ([0-9.()A-Za-z]+),/ + facts[:operatingsystem] = $1 + facts[:operatingsystemrelease] = $3 + facts[:operatingsystemmajrelease] = ios_major_version(facts[:operatingsystemrelease]) + facts[:operatingsystemfeature] = $2 + end + end + facts + end + + def ios_major_version(version) + version.gsub(/^(\d+)\.(\d+)\(.+\)([A-Z]+)([\da-z]+)?/, '\1.\2\3') + end + + def uptime_to_seconds(uptime) + captures = (uptime.match /^(?:(?:(?:(?:(\d+) years?,)?\s*(\d+) weeks?,)?\s*(\d+) days?,)?\s*(\d+) hours?,)?\s*(\d+) minutes?$/).captures + seconds = captures.zip([31536000, 604800, 86400, 3600, 60]).inject(0) do |total, (x,y)| + total + (x.nil? ? 0 : x.to_i * y) + end + end + +end
\ No newline at end of file diff --git a/lib/puppet/util/network_device/config.rb b/lib/puppet/util/network_device/config.rb new file mode 100644 index 000000000..17f4e254c --- /dev/null +++ b/lib/puppet/util/network_device/config.rb @@ -0,0 +1,93 @@ +require 'ostruct' +require 'puppet/util/loadedfile' + +class Puppet::Util::NetworkDevice::Config < Puppet::Util::LoadedFile + + def self.main + @main ||= self.new + end + + def self.devices + main.devices || [] + end + + attr_reader :devices + + def exists? + FileTest.exists?(@file) + end + + def initialize() + @file = Puppet[:deviceconfig] + + raise Puppet::DevError, "No device config file defined" unless @file + return unless self.exists? + super(@file) + @devices = {} + + read(true) # force reading at start + end + + # Read the configuration file. + def read(force = false) + return unless FileTest.exists?(@file) + + parse if force or changed? + end + + private + + def parse + begin + devices = {} + device = nil + File.open(@file) { |f| + count = 1 + f.each { |line| + case line + when /^\s*#/ # skip comments + count += 1 + next + when /^\s*$/ # skip blank lines + count += 1 + next + when /^\[([\w.]+)\]\s*$/ # [device.fqdn] + name = $1 + name.chomp! + raise ConfigurationError, "Duplicate device found at line #{count}, already found at #{device.line}" if devices.include?(name) + device = OpenStruct.new + device.name = name + device.line = count + Puppet.debug "found device: #{device.name} at #{device.line}" + devices[name] = device + when /^\s*(type|url)\s+(.+)$/ + parse_directive(device, $1, $2, count) + else + raise ConfigurationError, "Invalid line #{count}: #{line}" + end + count += 1 + } + } + rescue Errno::EACCES => detail + Puppet.err "Configuration error: Cannot read #{@file}; cannot serve" + #raise Puppet::Error, "Cannot read #{@config}" + rescue Errno::ENOENT => detail + Puppet.err "Configuration error: '#{@file}' does not exit; cannot serve" + end + + @devices = devices + end + + def parse_directive(device, var, value, count) + case var + when "type" + device.provider = value + when "url" + device.url = value + else + raise ConfigurationError, + "Invalid argument '#{var}' at line #{count}" + end + end + +end
\ No newline at end of file diff --git a/lib/puppet/util/network_device/transport.rb b/lib/puppet/util/network_device/transport.rb index e64fe9b69..cef8f3859 100644 --- a/lib/puppet/util/network_device/transport.rb +++ b/lib/puppet/util/network_device/transport.rb @@ -1,5 +1,3 @@ # stub -module Puppet::Util::NetworkDevice - module Transport - end +module Puppet::Util::NetworkDevice::Transport end
\ No newline at end of file diff --git a/lib/puppet/util/network_device/transport/ssh.rb b/lib/puppet/util/network_device/transport/ssh.rb index b3cf51b8a..bf0e7193c 100644 --- a/lib/puppet/util/network_device/transport/ssh.rb +++ b/lib/puppet/util/network_device/transport/ssh.rb @@ -31,6 +31,10 @@ class Puppet::Util::NetworkDevice::Transport::Ssh < Puppet::Util::NetworkDevice: @ssh = Net::SSH.start(host, user, :port => port, :password => password, :timeout => timeout) rescue TimeoutError raise TimeoutError, "timed out while opening an ssh connection to the host" + rescue Net::SSH::AuthenticationFailed + raise Puppet::Error, "SSH authentication failure connecting to #{host} as #{user}" + rescue Net::SSH::Exception => detail + raise Puppet::Error, "SSH connection failure to #{host}" end @buf = "" diff --git a/lib/puppet/util/queue.rb b/lib/puppet/util/queue.rb index 02357742a..636bdcf2e 100644 --- a/lib/puppet/util/queue.rb +++ b/lib/puppet/util/queue.rb @@ -30,7 +30,7 @@ require 'puppet/util/instance_loader' # # The client plugins are expected to implement an interface similar to that of Stomp::Client: # * <tt>new</tt> should return a connected, ready-to-go client instance. Note that no arguments are passed in. -# * <tt>send_message(queue, message)</tt> should send the _message_ to the specified _queue_. +# * <tt>publish_message(queue, message)</tt> should publish the _message_ to the specified _queue_. # * <tt>subscribe(queue)</tt> _block_ subscribes to _queue_ and executes _block_ upon receiving a message. # * _queue_ names are simple names independent of the message broker or client library. No "/queue/" prefixes like in Stomp::Client. module Puppet::Util::Queue diff --git a/lib/puppet/util/queue/stomp.rb b/lib/puppet/util/queue/stomp.rb index c18edae6a..cabc56627 100644 --- a/lib/puppet/util/queue/stomp.rb +++ b/lib/puppet/util/queue/stomp.rb @@ -28,8 +28,8 @@ class Puppet::Util::Queue::Stomp end end - def send_message(target, msg) - stomp_client.send(stompify_target(target), msg, :persistent => true) + def publish_message(target, msg) + stomp_client.publish(stompify_target(target), msg, :persistent => true) end def subscribe(target) diff --git a/spec/integration/indirector/catalog/queue_spec.rb b/spec/integration/indirector/catalog/queue_spec.rb index 569f096bf..940c8bab8 100755 --- a/spec/integration/indirector/catalog/queue_spec.rb +++ b/spec/integration/indirector/catalog/queue_spec.rb @@ -19,13 +19,13 @@ describe "Puppet::Resource::Catalog::Queue", :if => Puppet.features.pson? do after { Puppet.settings.clear } - it "should render catalogs to pson and send them via the queue client when catalogs are saved" do + it "should render catalogs to pson and publish them via the queue client when catalogs are saved" do terminus = Puppet::Resource::Catalog.indirection.terminus(:queue) client = mock 'client' terminus.stubs(:client).returns client - client.expects(:send_message).with(:catalog, @catalog.to_pson) + client.expects(:publish_message).with(:catalog, @catalog.to_pson) request = Puppet::Indirector::Request.new(:catalog, :save, "foo", :instance => @catalog) diff --git a/spec/integration/transaction_spec.rb b/spec/integration/transaction_spec.rb index 41a1ebac3..78d62fc51 100755 --- a/spec/integration/transaction_spec.rb +++ b/spec/integration/transaction_spec.rb @@ -75,6 +75,62 @@ describe Puppet::Transaction do transaction.evaluate end + it "should not apply device resources on normal host" do + catalog = Puppet::Resource::Catalog.new + resource = Puppet::Type.type(:interface).new :name => "FastEthernet 0/1" + catalog.add_resource resource + + transaction = Puppet::Transaction.new(catalog) + transaction.for_network_device = false + + transaction.expects(:apply).never.with(resource, nil) + + transaction.evaluate + transaction.resource_status(resource).should be_skipped + end + + it "should not apply host resources on device" do + catalog = Puppet::Resource::Catalog.new + resource = Puppet::Type.type(:file).new :path => "/foo/bar", :backup => false + catalog.add_resource resource + + transaction = Puppet::Transaction.new(catalog) + transaction.for_network_device = true + + transaction.expects(:apply).never.with(resource, nil) + + transaction.evaluate + transaction.resource_status(resource).should be_skipped + end + + it "should apply device resources on device" do + catalog = Puppet::Resource::Catalog.new + resource = Puppet::Type.type(:interface).new :name => "FastEthernet 0/1" + catalog.add_resource resource + + transaction = Puppet::Transaction.new(catalog) + transaction.for_network_device = true + + transaction.expects(:apply).with(resource, nil) + + transaction.evaluate + transaction.resource_status(resource).should_not be_skipped + end + + it "should apply resources appliable on host and device on a device" do + catalog = Puppet::Resource::Catalog.new + resource = Puppet::Type.type(:schedule).new :name => "test" + catalog.add_resource resource + + transaction = Puppet::Transaction.new(catalog) + transaction.for_network_device = true + + transaction.expects(:apply).with(resource, nil) + + transaction.evaluate + transaction.resource_status(resource).should_not be_skipped + end + # Verify that one component requiring another causes the contained # resources in the requiring component to get refreshed. it "should propagate events from a contained resource through its container to its dependent container's contained resources" do diff --git a/spec/lib/matchers/json.rb b/spec/lib/matchers/json.rb new file mode 100644 index 000000000..798e4cd21 --- /dev/null +++ b/spec/lib/matchers/json.rb @@ -0,0 +1,111 @@ +RSpec::Matchers.define :set_json_attribute do |*attributes| + def format + @format ||= Puppet::Network::FormatHandler.format('pson') + end + + chain :to do |value| + @value = value + end + + def json(instance) + PSON.parse(instance.to_pson) + end + + def attr_value(attrs, instance) + attrs = attrs.dup + hash = json(instance)['data'] + while attrs.length > 0 + name = attrs.shift + hash = hash[name] + end + hash + end + + match do |instance| + result = attr_value(attributes, instance) + if @value + result == @value + else + ! result.nil? + end + end + + failure_message_for_should do |instance| + if @value + "expected #{instance.inspect} to set #{attributes.inspect} to #{@value.inspect}; got #{attr_value(attributes, instance).inspect}" + else + "expected #{instance.inspect} to set #{attributes.inspect} but was nil" + end + end + + failure_message_for_should_not do |instance| + if @value + "expected #{instance.inspect} not to set #{attributes.inspect} to #{@value.inspect}" + else + "expected #{instance.inspect} not to set #{attributes.inspect} to nil" + end + end +end + +RSpec::Matchers.define :set_json_document_type_to do |type| + def format + @format ||= Puppet::Network::FormatHandler.format('pson') + end + + match do |instance| + json(instance)['document_type'] == type + end + + def json(instance) + PSON.parse(instance.to_pson) + end + + failure_message_for_should do |instance| + "expected #{instance.inspect} to set document_type to #{type.inspect}; got #{json(instance)['document_type'].inspect}" + end + + failure_message_for_should_not do |instance| + "expected #{instance.inspect} not to set document_type to #{type.inspect}" + end +end + +RSpec::Matchers.define :read_json_attribute do |attribute| + def format + @format ||= Puppet::Network::FormatHandler.format('pson') + end + + chain :from do |value| + @json = value + end + + chain :as do |as| + @value = as + end + + match do |klass| + raise "Must specify json with 'from'" unless @json + + @instance = format.intern(klass, @json) + if @value + @instance.send(attribute) == @value + else + ! @instance.send(attribute).nil? + end + end + + failure_message_for_should do |klass| + if @value + "expected #{klass} to read #{attribute} from #{@json} as #{@value.inspect}; got #{@instance.send(attribute).inspect}" + else + "expected #{klass} to read #{attribute} from #{@json} but was nil" + end + end + + failure_message_for_should_not do |klass| + if @value + "expected #{klass} not to set #{attribute} to #{@value}" + else + "expected #{klass} not to set #{attribute} to nil" + end + end +end diff --git a/spec/lib/puppet/face/basetest.rb b/spec/lib/puppet/face/basetest.rb index 00616f74f..e935161ae 100755 --- a/spec/lib/puppet/face/basetest.rb +++ b/spec/lib/puppet/face/basetest.rb @@ -1 +1,2 @@ +require 'puppet/face' Puppet::Face.define(:basetest, '0.0.1') diff --git a/spec/shared_behaviours/documentation_on_faces.rb b/spec/shared_behaviours/documentation_on_faces.rb new file mode 100644 index 000000000..41b4015c9 --- /dev/null +++ b/spec/shared_behaviours/documentation_on_faces.rb @@ -0,0 +1,35 @@ +# encoding: UTF-8 +shared_examples_for "documentation on faces" do + context "description" do + describe "#summary" do + it "should accept a summary" do + text = "this is my summary" + expect { subject.summary = text }.to_not raise_error + subject.summary.should == text + end + + it "should accept a long, long, long summary" do + text = "I never know when to stop with the word banana" + ("na" * 1000) + expect { subject.summary = text }.to_not raise_error + subject.summary.should == text + end + + it "should reject a summary with a newline" do + expect { subject.summary = "with\nnewlines" }. + to raise_error ArgumentError, /summary should be a single line/ + end + end + + describe "#description" do + it "should accept a description" do + subject.description = "hello" + subject.description.should == "hello" + end + + it "should accept a description with a newline" do + subject.description = "hello \n my \n fine \n friend" + subject.description.should == "hello \n my \n fine \n friend" + end + end + end +end diff --git a/spec/shared_behaviours/things_that_declare_options.rb b/spec/shared_behaviours/things_that_declare_options.rb index ac358eacd..5300a159a 100755 --- a/spec/shared_behaviours/things_that_declare_options.rb +++ b/spec/shared_behaviours/things_that_declare_options.rb @@ -1,45 +1,45 @@ # encoding: UTF-8 shared_examples_for "things that declare options" do it "should support options without arguments" do - subject = add_options_to { option "--bar" } - subject.should be_option :bar + thing = add_options_to { option "--bar" } + thing.should be_option :bar end it "should support options with an empty block" do - subject = add_options_to do + thing = add_options_to do option "--foo" do # this section deliberately left blank end end - subject.should be - subject.should be_option :foo + thing.should be + thing.should be_option :foo end { "--foo=" => :foo }.each do |input, option| it "should accept #{name.inspect}" do - subject = add_options_to { option input } - subject.should be_option option + thing = add_options_to { option input } + thing.should be_option option end end it "should support option documentation" do text = "Sturm und Drang (German pronunciation: [ˈʃtʊʁm ʊnt ˈdʁaŋ]) …" - subject = add_options_to do + thing = add_options_to do option "--foo" do desc text end end - subject.get_option(:foo).desc.should == text + thing.get_option(:foo).desc.should == text end it "should list all the options" do - subject = add_options_to do + thing = add_options_to do option "--foo" option "--bar" end - subject.options.should =~ [:foo, :bar] + thing.options.should =~ [:foo, :bar] end it "should detect conflicts in long options" do @@ -95,22 +95,24 @@ shared_examples_for "things that declare options" do should raise_error ArgumentError, /inconsistent about taking an argument/ end - it "should accept optional arguments" do - subject = add_options_to do option "--foo=[baz]", "--bar=[baz]" end - [:foo, :bar].each do |name| - subject.should be_option name - end + it "should not accept optional arguments" do + expect do + thing = add_options_to do option "--foo=[baz]", "--bar=[baz]" end + [:foo, :bar].each do |name| + thing.should be_option name + end + end.to raise_error(ArgumentError, /optional arguments are not supported/) end describe "#takes_argument?" do it "should detect an argument being absent" do - subject = add_options_to do option "--foo" end - subject.get_option(:foo).should_not be_takes_argument + thing = add_options_to do option "--foo" end + thing.get_option(:foo).should_not be_takes_argument end - ["=FOO", " FOO", "=[FOO]", " [FOO]"].each do |input| + ["=FOO", " FOO"].each do |input| it "should detect an argument given #{input.inspect}" do - subject = add_options_to do option "--foo#{input}" end - subject.get_option(:foo).should be_takes_argument + thing = add_options_to do option "--foo#{input}" end + thing.get_option(:foo).should be_takes_argument end end end @@ -131,10 +133,12 @@ shared_examples_for "things that declare options" do end ["=[FOO]", " [FOO]"].each do |input| - it "should be true if the argument is optional (like #{input.inspect})" do - option = add_options_to do option "--foo#{input}" end.get_option(:foo) - option.should be_takes_argument - option.should be_optional_argument + it "should fail if the argument is optional (like #{input.inspect})" do + expect do + option = add_options_to do option "--foo#{input}" end.get_option(:foo) + option.should be_takes_argument + option.should be_optional_argument + end.to raise_error(ArgumentError, /optional arguments are not supported/) end end end diff --git a/spec/unit/application/device_spec.rb b/spec/unit/application/device_spec.rb new file mode 100644 index 000000000..086e321e9 --- /dev/null +++ b/spec/unit/application/device_spec.rb @@ -0,0 +1,349 @@ +#!/usr/bin/env ruby + +require File.dirname(__FILE__) + '/../../spec_helper' + +require 'puppet/application/device' +require 'puppet/util/network_device/config' +require 'ostruct' +require 'puppet/configurer' + +describe Puppet::Application::Device do + before :each do + @device = Puppet::Application[:device] +# @device.stubs(:puts) + @device.preinit + Puppet::Util::Log.stubs(:newdestination) + Puppet::Util::Log.stubs(:level=) + + Puppet::Node.indirection.stubs(:terminus_class=) + Puppet::Node.indirection.stubs(:cache_class=) + Puppet::Node::Facts.indirection.stubs(:terminus_class=) + end + + it "should operate in agent run_mode" do + @device.class.run_mode.name.should == :agent + end + + it "should ask Puppet::Application to parse Puppet configuration file" do + @device.should_parse_config?.should be_true + end + + it "should declare a main command" do + @device.should respond_to(:main) + end + + it "should declare a preinit block" do + @device.should respond_to(:preinit) + end + + describe "in preinit" do + before :each do + @device.stubs(:trap) + end + + it "should catch INT" do + @device.expects(:trap).with { |arg,block| arg == :INT } + + @device.preinit + end + end + + describe "when handling options" do + before do + @device.command_line.stubs(:args).returns([]) + end + + [:centrallogging, :debug, :verbose,].each do |option| + it "should declare handle_#{option} method" do + @device.should respond_to("handle_#{option}".to_sym) + end + + it "should store argument value when calling handle_#{option}" do + @device.options.expects(:[]=).with(option, 'arg') + @device.send("handle_#{option}".to_sym, 'arg') + end + end + + it "should set waitforcert to 0 with --onetime and if --waitforcert wasn't given" do + Puppet[:onetime] = true + Puppet::SSL::Host.any_instance.expects(:wait_for_cert).with(0) + @device.setup_host + end + + it "should use supplied waitforcert when --onetime is specified" do + Puppet[:onetime] = true + @device.handle_waitforcert(60) + Puppet::SSL::Host.any_instance.expects(:wait_for_cert).with(60) + @device.setup_host + end + + it "should use a default value for waitforcert when --onetime and --waitforcert are not specified" do + Puppet::SSL::Host.any_instance.expects(:wait_for_cert).with(120) + @device.setup_host + end + + it "should set the log destination with --logdest" do + @device.options.stubs(:[]=).with { |opt,val| opt == :setdest } + Puppet::Log.expects(:newdestination).with("console") + + @device.handle_logdest("console") + end + + it "should put the setdest options to true" do + @device.options.expects(:[]=).with(:setdest,true) + + @device.handle_logdest("console") + end + + it "should parse the log destination from the command line" do + @device.command_line.stubs(:args).returns(%w{--logdest /my/file}) + + Puppet::Util::Log.expects(:newdestination).with("/my/file") + + @device.parse_options + end + + it "should store the waitforcert options with --waitforcert" do + @device.options.expects(:[]=).with(:waitforcert,42) + + @device.handle_waitforcert("42") + end + + it "should set args[:Port] with --port" do + @device.handle_port("42") + @device.args[:Port].should == "42" + end + + end + + describe "during setup" do + before :each do + @device.options.stubs(:[]) + Puppet.stubs(:info) + FileTest.stubs(:exists?).returns(true) + Puppet[:libdir] = "/dev/null/lib" + Puppet::SSL::Host.stubs(:ca_location=) + Puppet::Transaction::Report.indirection.stubs(:terminus_class=) + Puppet::Resource::Catalog.indirection.stubs(:terminus_class=) + Puppet::Resource::Catalog.indirection.stubs(:cache_class=) + Puppet::Node::Facts.indirection.stubs(:terminus_class=) + @host = stub_everything 'host' + Puppet::SSL::Host.stubs(:new).returns(@host) + Puppet.stubs(:settraps) + end + + it "should call setup_logs" do + @device.expects(:setup_logs) + @device.setup + end + + describe "when setting up logs" do + before :each do + Puppet::Util::Log.stubs(:newdestination) + end + + it "should set log level to debug if --debug was passed" do + @device.options.stubs(:[]).with(:debug).returns(true) + + Puppet::Util::Log.expects(:level=).with(:debug) + + @device.setup_logs + end + + it "should set log level to info if --verbose was passed" do + @device.options.stubs(:[]).with(:verbose).returns(true) + + Puppet::Util::Log.expects(:level=).with(:info) + + @device.setup_logs + end + + [:verbose, :debug].each do |level| + it "should set console as the log destination with level #{level}" do + @device.options.stubs(:[]).with(level).returns(true) + + Puppet::Util::Log.expects(:newdestination).with(:console) + + @device.setup_logs + end + end + + it "should set syslog as the log destination if no --logdest" do + @device.options.stubs(:[]).with(:setdest).returns(false) + + Puppet::Util::Log.expects(:newdestination).with(:syslog) + + @device.setup_logs + end + + end + + it "should set a central log destination with --centrallogs" do + @device.options.stubs(:[]).with(:centrallogs).returns(true) + Puppet[:server] = "puppet.reductivelabs.com" + Puppet::Util::Log.stubs(:newdestination).with(:syslog) + + Puppet::Util::Log.expects(:newdestination).with("puppet.reductivelabs.com") + + @device.setup + end + + it "should use :main, :agent, :device and :ssl config" do + Puppet.settings.expects(:use).with(:main, :agent, :device, :ssl) + + @device.setup + end + + it "should install a remote ca location" do + Puppet::SSL::Host.expects(:ca_location=).with(:remote) + + @device.setup + end + + it "should tell the report handler to use REST" do + Puppet::Transaction::Report.indirection.expects(:terminus_class=).with(:rest) + + @device.setup + end + + it "should change the catalog_terminus setting to 'rest'" do + Puppet[:catalog_terminus] = :foo + @device.setup + Puppet[:catalog_terminus].should == :rest + end + + it "should tell the catalog handler to use cache" do + Puppet::Resource::Catalog.indirection.expects(:cache_class=).with(:yaml) + + @device.setup + end + + it "should change the facts_terminus setting to 'network_device'" do + Puppet[:facts_terminus] = :foo + + @device.setup + Puppet[:facts_terminus].should == :network_device + end + end + + describe "when initializing each devices SSL" do + before(:each) do + @host = stub_everything 'host' + Puppet::SSL::Host.stubs(:new).returns(@host) + end + + it "should create a new ssl host" do + Puppet::SSL::Host.expects(:new).returns(@host) + @device.setup_host + end + + it "should wait for a certificate" do + @device.options.stubs(:[]).with(:waitforcert).returns(123) + @host.expects(:wait_for_cert).with(123) + + @device.setup_host + end + end + + + describe "when running" do + before :each do + @device.options.stubs(:[]).with(:fingerprint).returns(false) + Puppet.stubs(:notice) + @device.options.stubs(:[]).with(:client) + Puppet::Util::NetworkDevice::Config.stubs(:devices).returns({}) + end + + it "should dispatch to main" do + @device.stubs(:main) + @device.run_command + end + + it "should get the device list" do + device_hash = stub_everything 'device hash' + Puppet::Util::NetworkDevice::Config.expects(:devices).returns(device_hash) + @device.main + end + + it "should exit if the device list is empty" do + @device.expects(:exit).with(1) + @device.main + end + + describe "for each device" do + before(:each) do + Puppet[:vardir] = "/dummy" + Puppet[:confdir] = "/dummy" + Puppet[:certname] = "certname" + @device_hash = { + "device1" => OpenStruct.new(:name => "device1", :url => "url", :provider => "cisco"), + "device2" => OpenStruct.new(:name => "device2", :url => "url", :provider => "cisco"), + } + Puppet::Util::NetworkDevice::Config.stubs(:devices).returns(@device_hash) + Puppet.settings.stubs(:set_value) + Puppet.settings.stubs(:use) + @device.stubs(:setup_host) + Puppet::Util::NetworkDevice.stubs(:init) + @configurer = stub_everything 'configurer' + Puppet::Configurer.stubs(:new).returns(@configurer) + end + + it "should set vardir to the device vardir" do + Puppet.settings.expects(:set_value).with(:vardir, "/dummy/devices/device1", :cli) + @device.main + end + + it "should set confdir to the device confdir" do + Puppet.settings.expects(:set_value).with(:confdir, "/dummy/devices/device1", :cli) + @device.main + end + + it "should set certname to the device certname" do + Puppet.settings.expects(:set_value).with(:certname, "device1", :cli) + Puppet.settings.expects(:set_value).with(:certname, "device2", :cli) + @device.main + end + + it "should make sure all the required folders and files are created" do + Puppet.settings.expects(:use).with(:main, :agent, :ssl).twice + @device.main + end + + it "should initialize the device singleton" do + Puppet::Util::NetworkDevice.expects(:init).with(@device_hash["device1"]).then.with(@device_hash["device2"]) + @device.main + end + + it "should setup the SSL context" do + @device.expects(:setup_host).twice + @device.main + end + + it "should launch a configurer for this device" do + @configurer.expects(:run).twice + @device.main + end + + [:vardir, :confdir].each do |setting| + it "should cleanup the #{setting} setting after the run" do + configurer = states('configurer').starts_as('notrun') + Puppet.settings.expects(:set_value).with(setting, "/dummy/devices/device1", :cli).when(configurer.is('notrun')) + @configurer.expects(:run).twice.then(configurer.is('run')) + Puppet.settings.expects(:set_value).with(setting, "/dummy", :cli).when(configurer.is('run')) + + @device.main + end + end + + it "should cleanup the certname setting after the run" do + configurer = states('configurer').starts_as('notrun') + Puppet.settings.expects(:set_value).with(:certname, "device1", :cli).when(configurer.is('notrun')) + @configurer.expects(:run).twice.then(configurer.is('run')) + Puppet.settings.expects(:set_value).with(:certname, "certname", :cli).when(configurer.is('run')) + + @device.main + end + + end + end +end diff --git a/spec/unit/application/face_base_spec.rb b/spec/unit/application/face_base_spec.rb index 939712ef8..5403608cf 100755 --- a/spec/unit/application/face_base_spec.rb +++ b/spec/unit/application/face_base_spec.rb @@ -11,7 +11,6 @@ describe Puppet::Application::FaceBase do Puppet::Face.define(:basetest, '0.0.1') do option("--[no-]boolean") option("--mandatory MANDATORY") - option("--optional [OPTIONAL]") action :foo do option("--action") @@ -60,19 +59,39 @@ describe Puppet::Application::FaceBase do app.face.name.should == :basetest end - it "should set the format based on the face default" do - app.format.should == :pson - end - it "should find the action" do app.action.should be app.action.name.should == :foo end end - it "should fail if no action is given" do - expect { app.preinit; app.parse_options }. - to raise_error OptionParser::MissingArgument, /No action given/ + it "should use the default action if not given any arguments" do + app.command_line.stubs(:args).returns [] + action = stub(:options => []) + Puppet::Face[:basetest, '0.0.1'].expects(:get_default_action).returns(action) + app.stubs(:main) + app.run + app.action.should == action + app.arguments.should == [ { } ] + end + + it "should use the default action if not given a valid one" do + app.command_line.stubs(:args).returns %w{bar} + action = stub(:options => []) + Puppet::Face[:basetest, '0.0.1'].expects(:get_default_action).returns(action) + app.stubs(:main) + app.run + app.action.should == action + app.arguments.should == [ 'bar', { } ] + end + + it "should have no action if not given a valid one and there is no default action" do + app.command_line.stubs(:args).returns %w{bar} + Puppet::Face[:basetest, '0.0.1'].expects(:get_default_action).returns(nil) + app.stubs(:main) + app.run + app.action.should be_nil + app.arguments.should == [ 'bar', { } ] end it "should report a sensible error when options with = fail" do @@ -169,7 +188,6 @@ describe Puppet::Application::FaceBase do app.face = Puppet::Face[:basetest, '0.0.1'] app.action = app.face.get_action(:foo) - app.format = :pson app.arguments = ["myname", "myarg"] end @@ -178,10 +196,79 @@ describe Puppet::Application::FaceBase do app.main end + it "should lookup help when it cannot do anything else" do + app.action = nil + Puppet::Face[:help, :current].expects(:help).with(:basetest, *app.arguments) + app.stubs(:puts) # meh. Don't print nil, thanks. --daniel 2011-04-12 + app.main + end + it "should use its render method to render any result" do app.expects(:render).with(app.arguments.length + 1) app.stubs(:puts) # meh. Don't print nil, thanks. --daniel 2011-04-12 app.main end end + + describe "#render" do + before :each do + app.face = Puppet::Face[:basetest, '0.0.1'] + app.action = app.face.get_action(:foo) + end + + ["hello", 1, 1.0].each do |input| + it "should just return a #{input.class.name}" do + app.render(input).should == input + end + end + + [[1, 2], ["one"], [{ 1 => 1 }]].each do |input| + it "should render #{input.class} using the 'pp' library" do + app.render(input).should == input.pretty_inspect + end + end + + it "should render a non-trivially-keyed Hash with the 'pp' library" do + hash = { [1,2] => 3, [2,3] => 5, [3,4] => 7 } + app.render(hash).should == hash.pretty_inspect + end + + it "should render a {String,Numeric}-keyed Hash into a table" do + object = Object.new + hash = { "one" => 1, "two" => [], "three" => {}, "four" => object, + 5 => 5, 6.0 => 6 } + + # Gotta love ASCII-betical sort order. Hope your objects are better + # structured for display than my test one is. --daniel 2011-04-18 + app.render(hash).should == <<EOT +5 5 +6.0 6 +four #{object.pretty_inspect.chomp} +one 1 +three {} +two [] +EOT + end + + it "should render a hash nicely with a multi-line value" do + hash = { + "number" => { "1" => '1' * 40, "2" => '2' * 40, '3' => '3' * 40 }, + "text" => { "a" => 'a' * 40, 'b' => 'b' * 40, 'c' => 'c' * 40 } + } + app.render(hash).should == <<EOT +number {"1"=>"1111111111111111111111111111111111111111", + "2"=>"2222222222222222222222222222222222222222", + "3"=>"3333333333333333333333333333333333333333"} +text {"a"=>"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "b"=>"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "c"=>"cccccccccccccccccccccccccccccccccccccccc"} +EOT + end + + it "should invoke the action rendering hook while rendering" do + app.action.set_rendering_method_for(:for_humans, proc { |value| "bi-winning!" }) + app.action.render_as = :for_humans + app.render("bi-polar?").should == "bi-winning!" + end + end end diff --git a/spec/unit/application/indirection_base_spec.rb b/spec/unit/application/indirection_base_spec.rb index 63ab11eed..57740384a 100755 --- a/spec/unit/application/indirection_base_spec.rb +++ b/spec/unit/application/indirection_base_spec.rb @@ -23,12 +23,11 @@ describe Puppet::Application::IndirectionBase do it "should accept a terminus command line option" do # It would be nice not to have to stub this, but whatever... writing an # entire indirection stack would cause us more grief. --daniel 2011-03-31 - terminus = mock("test indirection terminus") + terminus = stub_everything("test indirection terminus") Puppet::Indirector::Indirection.expects(:instance). - with(:testindirection).twice.returns() + with(:testindirection).returns(terminus) - subject.command_line. - instance_variable_set('@args', %w{--terminus foo save}) + subject.command_line.instance_variable_set('@args', %w{--terminus foo save}) # Not a very nice thing. :( $stderr.stubs(:puts) diff --git a/spec/unit/face/certificate_spec.rb b/spec/unit/face/certificate_spec.rb index dbcc888ad..b0bbf1af6 100755 --- a/spec/unit/face/certificate_spec.rb +++ b/spec/unit/face/certificate_spec.rb @@ -6,9 +6,14 @@ describe Puppet::Face[:certificate, '0.0.1'] do end it "should set the ca location when invoked" do - pending "#6983: This is broken in the actual face..." Puppet::SSL::Host.expects(:ca_location=).with(:foo) Puppet::SSL::Host.indirection.expects(:save) - subject.sign :ca_location => :foo + subject.sign "hello, friend", :ca_location => :foo + end + + it "(#7059) should set the ca location when an inherited action is invoked" do + Puppet::SSL::Host.expects(:ca_location=).with(:foo) + subject.indirection.expects(:find) + subject.find "hello, friend", :ca_location => :foo end end diff --git a/spec/unit/face/facts_spec.rb b/spec/unit/face/facts_spec.rb index e6411f836..6ab6ad5be 100755 --- a/spec/unit/face/facts_spec.rb +++ b/spec/unit/face/facts_spec.rb @@ -2,14 +2,10 @@ require 'spec_helper' describe Puppet::Face[:facts, '0.0.1'] do - it "should define an 'upload' fact" do + it "should define an 'upload' action" do subject.should be_action(:upload) end - it "should set its default format to :yaml" do - subject.default_format.should == :yaml - end - describe "when uploading" do it "should set the terminus_class to :facter" diff --git a/spec/unit/face/help_spec.rb b/spec/unit/face/help_spec.rb index e67f29e07..faa5f9617 100755 --- a/spec/unit/face/help_spec.rb +++ b/spec/unit/face/help_spec.rb @@ -7,7 +7,7 @@ describe Puppet::Face[:help, '0.0.1'] do end it "should have a default action of help" do - pending "REVISIT: we don't support default actions yet" + subject.get_action('help').should be_default end it "should accept a call with no arguments" do @@ -55,19 +55,23 @@ describe Puppet::Face[:help, '0.0.1'] do end end - Puppet::Face.faces.each do |name| - face = Puppet::Face[name, :current] - summary = face.summary + it "should list all faces" do + Puppet::Face.faces.each do |name| + face = Puppet::Face[name, :current] + summary = face.summary - it { should =~ %r{ #{name} } } - it { should =~ %r{ #{name} +#{summary}} } if summary + subject.should =~ %r{ #{name} } + summary and subject.should =~ %r{ #{name} +#{summary}} + end end - Puppet::Face[:help, :current].legacy_applications.each do |appname| - it { should =~ %r{ #{appname} } } + it "should list all legacy applications" do + Puppet::Face[:help, :current].legacy_applications.each do |appname| + subject.should =~ %r{ #{appname} } - summary = Puppet::Face[:help, :current].horribly_extract_summary_from(appname) - summary and it { should =~ %r{ #{summary}\b} } + summary = Puppet::Face[:help, :current].horribly_extract_summary_from(appname) + summary and subject.should =~ %r{ #{summary}\b} + end end end diff --git a/spec/unit/face/node_spec.rb b/spec/unit/face/node_spec.rb index 90d258db9..d19312c58 100755 --- a/spec/unit/face/node_spec.rb +++ b/spec/unit/face/node_spec.rb @@ -2,7 +2,5 @@ require 'spec_helper' describe Puppet::Face[:node, '0.0.1'] do - it "should set its default format to :yaml" do - subject.default_format.should == :yaml - end + it "REVISIT: really should have some tests" end diff --git a/spec/unit/indirector/facts/network_device_spec.rb b/spec/unit/indirector/facts/network_device_spec.rb new file mode 100644 index 000000000..302a810e8 --- /dev/null +++ b/spec/unit/indirector/facts/network_device_spec.rb @@ -0,0 +1,89 @@ +#!/usr/bin/env ruby +# +# Created by Luke Kanies on 2007-9-23. +# Copyright (c) 2007. All rights reserved. + +require File.dirname(__FILE__) + '/../../../spec_helper' + +require 'puppet/indirector/facts/network_device' + +describe Puppet::Node::Facts::NetworkDevice do + it "should be a subclass of the Code terminus" do + Puppet::Node::Facts::NetworkDevice.superclass.should equal(Puppet::Indirector::Code) + end + + it "should have documentation" do + Puppet::Node::Facts::NetworkDevice.doc.should_not be_nil + end + + it "should be registered with the configuration store indirection" do + indirection = Puppet::Indirector::Indirection.instance(:facts) + Puppet::Node::Facts::NetworkDevice.indirection.should equal(indirection) + end + + it "should have its name set to :facter" do + Puppet::Node::Facts::NetworkDevice.name.should == :network_device + end +end + +describe Puppet::Node::Facts::NetworkDevice do + before :each do + @remote_device = stub 'remote_device', :facts => {} + Puppet::Util::NetworkDevice.stubs(:current).returns(@remote_device) + @device = Puppet::Node::Facts::NetworkDevice.new + @name = "me" + @request = stub 'request', :key => @name + end + + describe Puppet::Node::Facts::NetworkDevice, " when finding facts" do + it "should return a Facts instance" do + @device.find(@request).should be_instance_of(Puppet::Node::Facts) + end + + it "should return a Facts instance with the provided key as the name" do + @device.find(@request).name.should == @name + end + + it "should return the device facts as the values in the Facts instance" do + @remote_device.expects(:facts).returns("one" => "two") + facts = @device.find(@request) + facts.values["one"].should == "two" + end + + it "should add local facts" do + facts = Puppet::Node::Facts.new("foo") + Puppet::Node::Facts.expects(:new).returns facts + facts.expects(:add_local_facts) + + @device.find(@request) + end + + it "should convert all facts into strings" do + facts = Puppet::Node::Facts.new("foo") + Puppet::Node::Facts.expects(:new).returns facts + facts.expects(:stringify) + + @device.find(@request) + end + + it "should call the downcase hook" do + facts = Puppet::Node::Facts.new("foo") + Puppet::Node::Facts.expects(:new).returns facts + facts.expects(:downcase_if_necessary) + + @device.find(@request) + end + end + + describe Puppet::Node::Facts::NetworkDevice, " when saving facts" do + it "should fail" do + proc { @device.save(@facts) }.should raise_error(Puppet::DevError) + end + end + + describe Puppet::Node::Facts::NetworkDevice, " when destroying facts" do + it "should fail" do + proc { @device.destroy(@facts) }.should raise_error(Puppet::DevError) + end + end +end diff --git a/spec/unit/indirector/queue_spec.rb b/spec/unit/indirector/queue_spec.rb index b84ed2aea..eba136bbc 100755 --- a/spec/unit/indirector/queue_spec.rb +++ b/spec/unit/indirector/queue_spec.rb @@ -60,20 +60,20 @@ describe Puppet::Indirector::Queue, :if => Puppet.features.pson? do describe "when saving" do it 'should render the instance using pson' do @subject.expects(:render).with(:pson) - @store.client.stubs(:send_message) + @store.client.stubs(:publish_message) @store.save(@request) end - it "should send the rendered message to the appropriate queue on the client" do + it "should publish the rendered message to the appropriate queue on the client" do @subject.expects(:render).returns "mypson" - @store.client.expects(:send_message).with(:my_queue, "mypson") + @store.client.expects(:publish_message).with(:my_queue, "mypson") @store.save(@request) end it "should catch any exceptions raised" do - @store.client.expects(:send_message).raises ArgumentError + @store.client.expects(:publish_message).raises ArgumentError lambda { @store.save(@request) }.should raise_error(Puppet::Error) end diff --git a/spec/unit/indirector/request_spec.rb b/spec/unit/indirector/request_spec.rb index 965d54188..ba7dc815e 100755 --- a/spec/unit/indirector/request_spec.rb +++ b/spec/unit/indirector/request_spec.rb @@ -1,5 +1,6 @@ #!/usr/bin/env rspec require 'spec_helper' +require 'matchers/json' require 'puppet/indirector/request' describe Puppet::Indirector::Request do @@ -300,4 +301,99 @@ describe Puppet::Indirector::Request do lambda { @request.query_string }.should raise_error(ArgumentError) end end + + describe "when converting to json" do + before do + @request = Puppet::Indirector::Request.new(:facts, :find, "foo") + end + + it "should produce a hash with the document_type set to 'request'" do + @request.should set_json_document_type_to("Puppet::Indirector::Request") + end + + it "should set the 'key'" do + @request.should set_json_attribute("key").to("foo") + end + + it "should include an attribute for its indirection name" do + @request.should set_json_attribute("type").to("facts") + end + + it "should include a 'method' attribute set to its method" do + @request.should set_json_attribute("method").to("find") + end + + it "should add all attributes under the 'attributes' attribute" do + @request.ip = "127.0.0.1" + @request.should set_json_attribute("attributes", "ip").to("127.0.0.1") + end + + it "should add all options under the 'attributes' attribute" do + @request.options["opt"] = "value" + PSON.parse(@request.to_pson)["data"]['attributes']['opt'].should == "value" + end + + it "should include the instance if provided" do + facts = Puppet::Node::Facts.new("foo") + @request.instance = facts + PSON.parse(@request.to_pson)["data"]['instance'].should be_instance_of(Puppet::Node::Facts) + end + end + + describe "when converting from json" do + before do + @request = Puppet::Indirector::Request.new(:facts, :find, "foo") + @klass = Puppet::Indirector::Request + @format = Puppet::Network::FormatHandler.format('pson') + end + + def from_json(json) + @format.intern(Puppet::Indirector::Request, json) + end + + it "should set the 'key'" do + from_json(@request.to_pson).key.should == "foo" + end + + it "should fail if no key is provided" do + json = PSON.parse(@request.to_pson) + json['data'].delete("key") + lambda { from_json(json.to_pson) }.should raise_error(ArgumentError) + end + + it "should set its indirector name" do + from_json(@request.to_pson).indirection_name.should == :facts + end + + it "should fail if no type is provided" do + json = PSON.parse(@request.to_pson) + json['data'].delete("type") + lambda { from_json(json.to_pson) }.should raise_error(ArgumentError) + end + + it "should set its method" do + from_json(@request.to_pson).method.should == "find" + end + + it "should fail if no method is provided" do + json = PSON.parse(@request.to_pson) + json['data'].delete("method") + lambda { from_json(json.to_pson) }.should raise_error(ArgumentError) + end + + it "should initialize with all attributes and options" do + @request.ip = "127.0.0.1" + @request.options["opt"] = "value" + result = from_json(@request.to_pson) + result.options[:opt].should == "value" + result.ip.should == "127.0.0.1" + end + + it "should set its instance as an instance if one is provided" do + facts = Puppet::Node::Facts.new("foo") + @request.instance = facts + result = from_json(@request.to_pson) + result.instance.should be_instance_of(Puppet::Node::Facts) + end + end end diff --git a/spec/unit/indirector/rest_spec.rb b/spec/unit/indirector/rest_spec.rb index 513eb8352..5637080e8 100755 --- a/spec/unit/indirector/rest_spec.rb +++ b/spec/unit/indirector/rest_spec.rb @@ -233,6 +233,11 @@ describe Puppet::Indirector::REST do params[s] = 'foo' end + # The request special-cases this parameter, and it + # won't be passed on to the server, so we remove it here + # to avoid a failure. + params.delete('ip') + @request = Puppet::Indirector::Request.new(:foo, :find, "foo bar", params.merge(:environment => "myenv")) @connection.expects(:post).with do |uri, body| diff --git a/spec/unit/interface/action_builder_spec.rb b/spec/unit/interface/action_builder_spec.rb index 5b04df900..bf7afa74e 100755 --- a/spec/unit/interface/action_builder_spec.rb +++ b/spec/unit/interface/action_builder_spec.rb @@ -1,68 +1,192 @@ #!/usr/bin/env rspec require 'spec_helper' require 'puppet/interface/action_builder' +require 'puppet/network/format_handler' describe Puppet::Interface::ActionBuilder do - describe "::build" do - it "should build an action" do - action = Puppet::Interface::ActionBuilder.build(nil, :foo) do + let :face do Puppet::Interface.new(:puppet_interface_actionbuilder, '0.0.1') end + + it "should build an action" do + action = Puppet::Interface::ActionBuilder.build(nil, :foo) do + end + action.should be_a(Puppet::Interface::Action) + action.name.should == :foo + end + + it "should define a method on the face which invokes the action" do + face = Puppet::Interface.new(:action_builder_test_interface, '0.0.1') do + action(:foo) { when_invoked { "invoked the method" } } + end + + face.foo.should == "invoked the method" + end + + it "should require a block" do + expect { Puppet::Interface::ActionBuilder.build(nil, :foo) }. + should raise_error("Action :foo must specify a block") + end + + describe "when handling options" do + it "should have a #option DSL function" do + method = nil + Puppet::Interface::ActionBuilder.build(face, :foo) do + method = self.method(:option) + end + method.should be + end + + it "should define an option without a block" do + action = Puppet::Interface::ActionBuilder.build(face, :foo) do + option "--bar" end - action.should be_a(Puppet::Interface::Action) - action.name.should == :foo + action.should be_option :bar end - it "should define a method on the face which invokes the action" do - face = Puppet::Interface.new(:action_builder_test_interface, '0.0.1') + it "should accept an empty block" do action = Puppet::Interface::ActionBuilder.build(face, :foo) do - when_invoked do - "invoked the method" + option "--bar" do + # This space left deliberately blank. end end + action.should be_option :bar + end + end + + context "inline documentation" do + it "should set the summary" do + action = Puppet::Interface::ActionBuilder.build(face, :foo) do + summary "this is some text" + end + action.summary.should == "this is some text" + end + end - face.foo.should == "invoked the method" + context "action defaulting" do + it "should set the default to true" do + action = Puppet::Interface::ActionBuilder.build(face, :foo) do + default + end + action.default.should be_true end - it "should require a block" do - expect { Puppet::Interface::ActionBuilder.build(nil, :foo) }. - should raise_error("Action :foo must specify a block") + it "should not be default by, er, default. *cough*" do + action = Puppet::Interface::ActionBuilder.build(face, :foo) do end + action.default.should be_false end + end - describe "when handling options" do - let :face do Puppet::Interface.new(:option_handling, '0.0.1') end + context "#when_rendering" do + it "should fail if no rendering format is given" do + expect { + Puppet::Interface::ActionBuilder.build(face, :foo) do + when_rendering do true end + end + }.to raise_error ArgumentError, /must give a rendering format to when_rendering/ + end - it "should have a #option DSL function" do - method = nil + it "should fail if no block is given" do + expect { Puppet::Interface::ActionBuilder.build(face, :foo) do - method = self.method(:option) + when_rendering :json end - method.should be - end + }.to raise_error ArgumentError, /must give a block to when_rendering/ + end - it "should define an option without a block" do - action = Puppet::Interface::ActionBuilder.build(face, :foo) do - option "--bar" + it "should fail if the block takes no arguments" do + expect { + Puppet::Interface::ActionBuilder.build(face, :foo) do + when_rendering :json do true end end - action.should be_option :bar + }.to raise_error ArgumentError, /when_rendering methods take one argument, the result, not/ + end + + it "should fail if the block takes more than one argument" do + expect { + Puppet::Interface::ActionBuilder.build(face, :foo) do + when_rendering :json do |a, b, c| true end + end + }.to raise_error ArgumentError, /when_rendering methods take one argument, the result, not/ + end + + it "should fail if the block takes a variable number of arguments" do + expect { + Puppet::Interface::ActionBuilder.build(face, :foo) do + when_rendering :json do |*args| true end + end + }.to raise_error(ArgumentError, + /when_rendering methods take one argument, the result, not/) + end + + it "should stash a rendering block" do + action = Puppet::Interface::ActionBuilder.build(face, :foo) do + when_rendering :json do |a| true end end + action.when_rendering(:json).should be_an_instance_of Method + end - it "should accept an empty block" do - action = Puppet::Interface::ActionBuilder.build(face, :foo) do - option "--bar" do - # This space left deliberately blank. - end + it "should fail if you try to set the same rendering twice" do + expect { + Puppet::Interface::ActionBuilder.build(face, :foo) do + when_rendering :json do |a| true end + when_rendering :json do |a| true end end - action.should be_option :bar + }.to raise_error ArgumentError, /You can't define a rendering method for json twice/ + end + + it "should work if you set two different renderings" do + action = Puppet::Interface::ActionBuilder.build(face, :foo) do + when_rendering :json do |a| true end + when_rendering :yaml do |a| true end end + action.when_rendering(:json).should be_an_instance_of Method + action.when_rendering(:yaml).should be_an_instance_of Method end - context "inline documentation" do - let :face do Puppet::Interface.new(:inline_action_docs, '0.0.1') end + it "should be bound to the face when called" do + action = Puppet::Interface::ActionBuilder.build(face, :foo) do + when_rendering :json do |a| self end + end + action.when_rendering(:json).call(true).should == face + end + end + + context "#render_as" do + it "should default to nil (eg: based on context)" do + action = Puppet::Interface::ActionBuilder.build(face, :foo) do end + action.render_as.should be_nil + end + + it "should fail if not rendering format is given" do + expect { + Puppet::Interface::ActionBuilder.build(face, :foo) do + render_as + end + }.to raise_error ArgumentError, /must give a rendering format to render_as/ + end - it "should set the summary" do + Puppet::Network::FormatHandler.formats.each do |name| + it "should accept #{name.inspect} format" do action = Puppet::Interface::ActionBuilder.build(face, :foo) do - summary "this is some text" + render_as name end - action.summary.should == "this is some text" + action.render_as.should == name + end + end + + it "should accept :for_humans format" do + action = Puppet::Interface::ActionBuilder.build(face, :foo) do + render_as :for_humans + end + action.render_as.should == :for_humans + end + + [:if_you_define_this_format_you_frighten_me, "json", 12].each do |input| + it "should fail if given #{input.inspect}" do + expect { + Puppet::Interface::ActionBuilder.build(face, :foo) do + render_as input + end + }.to raise_error ArgumentError, /#{input.inspect} is not a valid rendering format/ end end end diff --git a/spec/unit/interface/action_manager_spec.rb b/spec/unit/interface/action_manager_spec.rb index c4b21eaac..07d517c18 100755 --- a/spec/unit/interface/action_manager_spec.rb +++ b/spec/unit/interface/action_manager_spec.rb @@ -3,6 +3,7 @@ require 'spec_helper' # This is entirely an internal class for Interface, so we have to load it instead of our class. require 'puppet/interface' +require 'puppet/face' class ActionManagerTester include Puppet::Interface::ActionManager @@ -103,6 +104,8 @@ describe Puppet::Interface::ActionManager do @klass = Class.new do include Puppet::Interface::ActionManager extend Puppet::Interface::ActionManager + def __invoke_decorations(*args) true end + def options() [] end end @instance = @klass.new end @@ -213,6 +216,23 @@ describe Puppet::Interface::ActionManager do end end + describe "#action" do + it 'should add an action' do + subject.action(:foo) { } + subject.get_action(:foo).should be_a Puppet::Interface::Action + end + + it 'should support default actions' do + subject.action(:foo) { default } + subject.get_default_action.should == subject.get_action(:foo) + end + + it 'should not support more than one default action' do + subject.action(:foo) { default } + expect { subject.action(:bar) { default } }.should raise_error + end + end + describe "#get_action" do let :parent_class do parent_class = Class.new(Puppet::Interface) diff --git a/spec/unit/interface/action_spec.rb b/spec/unit/interface/action_spec.rb index 8c6782976..0eb450ee2 100755 --- a/spec/unit/interface/action_spec.rb +++ b/spec/unit/interface/action_spec.rb @@ -66,8 +66,8 @@ describe Puppet::Interface::Action do let :face do Puppet::Interface.new(:ruby_api, '1.0.0') do action :bar do - when_invoked do |options| - options + when_invoked do |*args| + args.last end end end @@ -79,9 +79,14 @@ describe Puppet::Interface::Action do end it "should work when options are supplied" do - options = face.bar :bar => "beer" + options = face.bar(:bar => "beer") options.should == { :bar => "beer" } end + + it "should call #validate_args on the action when invoked" do + face.get_action(:bar).expects(:validate_args).with([1, :two, 'three', {}]) + face.bar 1, :two, 'three' + end end end @@ -168,5 +173,220 @@ describe Puppet::Interface::Action do end }.should raise_error ArgumentError, /Option foo conflicts with existing option foo/i end + + it "should fail when a required action option is not provided" do + face = Puppet::Interface.new(:required_action_option, '0.0.1') do + action(:bar) do + option('--foo') { required } + when_invoked { } + end + end + expect { face.bar }.to raise_error ArgumentError, /missing required options \(foo\)/ + end + + it "should fail when a required face option is not provided" do + face = Puppet::Interface.new(:required_face_option, '0.0.1') do + option('--foo') { required } + action(:bar) { when_invoked { } } + end + expect { face.bar }.to raise_error ArgumentError, /missing required options \(foo\)/ + end + end + + context "with action decorators" do + context "local only" do + let :face do + Puppet::Interface.new(:action_decorators, '0.0.1') do + action :bar do when_invoked do true end end + def report(arg) end + end + end + + it "should call action before decorators" do + face.action(:baz) do + option "--baz" do + before_action do |action, args, options| + report(:action_option) + end + end + when_invoked do true end + end + + face.expects(:report).with(:action_option) + face.baz :baz => true + end + + it "should call action after decorators" do + face.action(:baz) do + option "--baz" do + after_action do |action, args, options| + report(:action_option) + end + end + when_invoked do true end + end + + face.expects(:report).with(:action_option) + face.baz :baz => true + end + + it "should call local before decorators" do + face.option "--foo FOO" do + before_action do |action, args, options| + report(:before) + end + end + face.expects(:report).with(:before) + face.bar({:foo => 12}) + end + + it "should call local after decorators" do + face.option "--foo FOO" do + after_action do |action, args, options| report(:after) end + end + face.expects(:report).with(:after) + face.bar({:foo => 12}) + end + + context "with inactive decorators" do + it "should not invoke a decorator if the options are empty" do + face.option "--foo FOO" do + before_action do |action, args, options| + report :before_action + end + end + face.expects(:report).never # I am testing the negative. + face.bar + end + + context "with some decorators only" do + before :each do + face.option "--foo" do + before_action do |action, args, options| report :foo end + end + face.option "--bar" do + before_action do |action, args, options| report :bar end + end + end + + it "should work with the foo option" do + face.expects(:report).with(:foo) + face.bar(:foo => true) + end + + it "should work with the bar option" do + face.expects(:report).with(:bar) + face.bar(:bar => true) + end + + it "should work with both options" do + face.expects(:report).with(:foo) + face.expects(:report).with(:bar) + face.bar(:foo => true, :bar => true) + end + end + end + end + + context "with inherited decorators" do + let :parent do + parent = Class.new(Puppet::Interface) + parent.script :on_parent do :on_parent end + parent.define_method :report do |arg| arg end + parent + end + + let :child do + child = parent.new(:inherited_decorators, '0.0.1') do + script :on_child do :on_child end + end + end + + context "with a child decorator" do + subject do + child.option "--foo FOO" do + before_action do |action, args, options| + report(:child_before) + end + end + child.expects(:report).with(:child_before) + child + end + + it "child actions should invoke the decorator" do + subject.on_child({:foo => true, :bar => true}).should == :on_child + end + + it "parent actions should invoke the decorator" do + subject.on_parent({:foo => true, :bar => true}).should == :on_parent + end + end + + context "with a parent decorator" do + subject do + parent.option "--foo FOO" do + before_action do |action, args, options| + report(:parent_before) + end + end + child.expects(:report).with(:parent_before) + child + end + + it "child actions should invoke the decorator" do + subject.on_child({:foo => true, :bar => true}).should == :on_child + end + + it "parent actions should invoke the decorator" do + subject.on_parent({:foo => true, :bar => true}).should == :on_parent + end + end + + context "with child and parent decorators" do + subject do + parent.option "--foo FOO" do + before_action { |action, args, options| report(:parent_before) } + after_action { |action, args, options| report(:parent_after) } + end + child.option "--bar BAR" do + before_action { |action, args, options| report(:child_before) } + after_action { |action, args, options| report(:child_after) } + end + + child.expects(:report).with(:child_before) + child.expects(:report).with(:parent_before) + child.expects(:report).with(:parent_after) + child.expects(:report).with(:child_after) + + child + end + + it "child actions should invoke all the decorator" do + subject.on_child({:foo => true, :bar => true}).should == :on_child + end + + it "parent actions should invoke all the decorator" do + subject.on_parent({:foo => true, :bar => true}).should == :on_parent + end + end + end + end + + it_should_behave_like "documentation on faces" do + subject do + face = Puppet::Interface.new(:action_documentation, '0.0.1') do + action :documentation do end + end + face.get_action(:documentation) + end + end + + context "#when_rendering" do + it "should fail if no type is given when_rendering" + it "should accept a when_rendering block" + it "should accept multiple when_rendering blocks" + it "should fail if when_rendering gets a non-symbol identifier" + it "should fail if a second block is given for the same type" + it "should return the block if asked" end end diff --git a/spec/unit/interface/face_collection_spec.rb b/spec/unit/interface/face_collection_spec.rb index d1114dde7..f9498cbb8 100755 --- a/spec/unit/interface/face_collection_spec.rb +++ b/spec/unit/interface/face_collection_spec.rb @@ -24,10 +24,6 @@ describe Puppet::Interface::FaceCollection do $".clear ; @original_required.each do |item| $" << item end end - describe "::faces" do - it "REVISIT: should have some tests here, if we describe it" - end - describe "::validate_version" do it 'should permit three number versions' do subject.validate_version('10.10.10').should == true diff --git a/spec/unit/interface/option_builder_spec.rb b/spec/unit/interface/option_builder_spec.rb index fae48324e..e9346852c 100755 --- a/spec/unit/interface/option_builder_spec.rb +++ b/spec/unit/interface/option_builder_spec.rb @@ -8,22 +8,68 @@ describe Puppet::Interface::OptionBuilder do should be_an_instance_of Puppet::Interface::Option end - describe "when using the DSL block" do - it "should work with an empty block" do + it "should work with an empty block" do + option = Puppet::Interface::OptionBuilder.build(face, "--foo") do + # This block deliberately left blank. + end + + option.should be_an_instance_of Puppet::Interface::Option + end + + it "should support documentation declarations" do + text = "this is the description" + option = Puppet::Interface::OptionBuilder.build(face, "--foo") do + desc text + end + option.should be_an_instance_of Puppet::Interface::Option + option.desc.should == text + end + + context "before_action hook" do + it "should support a before_action hook" do option = Puppet::Interface::OptionBuilder.build(face, "--foo") do - # This block deliberately left blank. + before_action do |a,b,c| :whatever end end + option.before_action.should be_an_instance_of UnboundMethod + end - option.should be_an_instance_of Puppet::Interface::Option + it "should fail if the hook block takes too few arguments" do + expect do + Puppet::Interface::OptionBuilder.build(face, "--foo") do + before_action do |one, two| true end + end + end.to raise_error ArgumentError, /takes three arguments/ end - it "should support documentation declarations" do - text = "this is the description" - option = Puppet::Interface::OptionBuilder.build(face, "--foo") do - desc text + it "should fail if the hook block takes too many arguments" do + expect do + Puppet::Interface::OptionBuilder.build(face, "--foo") do + before_action do |one, two, three, four| true end + end + end.to raise_error ArgumentError, /takes three arguments/ + end + + it "should fail if the hook block takes a variable number of arguments" do + expect do + Puppet::Interface::OptionBuilder.build(face, "--foo") do + before_action do |*blah| true end + end + end.to raise_error ArgumentError, /takes three arguments/ + end + + it "should support simple required declarations" do + opt = Puppet::Interface::OptionBuilder.build(face, "--foo") do + required end - option.should be_an_instance_of Puppet::Interface::Option - option.desc.should == text + opt.should be_required end + + it "should support arguments to the required property" do + opt = Puppet::Interface::OptionBuilder.build(face, "--foo") do + required(false) + end + opt.should_not be_required + end + end end diff --git a/spec/unit/interface/option_spec.rb b/spec/unit/interface/option_spec.rb index 3bcd121e2..e77b46e79 100755 --- a/spec/unit/interface/option_spec.rb +++ b/spec/unit/interface/option_spec.rb @@ -1,3 +1,4 @@ +require 'puppet/interface' require 'puppet/interface/option' describe Puppet::Interface::Option do @@ -72,4 +73,28 @@ describe Puppet::Interface::Option do option.to_s.should == "foo-bar" end end + + %w{before after}.each do |side| + describe "#{side} hooks" do + subject { Puppet::Interface::Option.new(face, "--foo") } + let :proc do Proc.new do :from_proc end end + + it { should respond_to "#{side}_action" } + it { should respond_to "#{side}_action=" } + + it "should set the #{side}_action hook" do + subject.send("#{side}_action").should be_nil + subject.send("#{side}_action=", proc) + subject.send("#{side}_action").should be_an_instance_of UnboundMethod + end + + data = [1, "foo", :foo, Object.new, method(:hash), method(:hash).unbind] + data.each do |input| + it "should fail if a #{input.class} is added to the #{side} hooks" do + expect { subject.send("#{side}_action=", input) }. + to raise_error ArgumentError, /not a proc/ + end + end + end + end end diff --git a/spec/unit/interface_spec.rb b/spec/unit/interface_spec.rb index e52b45d8a..b4fef0307 100755 --- a/spec/unit/interface_spec.rb +++ b/spec/unit/interface_spec.rb @@ -33,7 +33,7 @@ describe Puppet::Interface do describe "#define" do it "should register the face" do - face = subject.define(:face_test_register, '0.0.1') + face = subject.define(:face_test_register, '0.0.1') face.should == subject[:face_test_register, '0.0.1'] end @@ -51,22 +51,16 @@ describe Puppet::Interface do subject.new(:foo, '1.0.0').should respond_to(:summary=).with(1).arguments end - it "should set the summary text" do - text = "hello, freddy, my little pal" - subject.define(:face_test_summary, '1.0.0') do - summary text - end - subject[:face_test_summary, '1.0.0'].summary.should == text - end - - it "should support mutating the summary" do - text = "hello, freddy, my little pal" - subject.define(:face_test_summary, '1.0.0') do - summary text + # Required documentation methods... + { :summary => "summary", + :description => "This is the description of the stuff\n\nWhee" + }.each do |attr, value| + it "should support #{attr} in the builder" do + face = subject.new(:builder, '1.0.0') do + self.send(attr, value) + end + face.send(attr).should == value end - subject[:face_test_summary, '1.0.0'].summary.should == text - subject[:face_test_summary, '1.0.0'].summary = text + text - subject[:face_test_summary, '1.0.0'].summary.should == text + text end end @@ -99,16 +93,6 @@ describe Puppet::Interface do subject.new(:me, '0.0.1').to_s.should =~ /\bme\b/ end - it "should allow overriding of the default format" do - face = subject.new(:me, '0.0.1') - face.set_default_format :foo - face.default_format.should == :foo - end - - it "should default to :pson for its format" do - subject.new(:me, '0.0.1').default_format.should == :pson - end - # Why? it "should create a class-level autoloader" do subject.autoloader.should be_instance_of(Puppet::Util::Autoload) @@ -204,4 +188,10 @@ describe Puppet::Interface do end end end + + it_should_behave_like "documentation on faces" do + subject do + Puppet::Interface.new(:face_documentation, '0.0.1') + end + end end diff --git a/spec/unit/network/http/handler_spec.rb b/spec/unit/network/http/handler_spec.rb index 83969c504..c709d82fe 100755 --- a/spec/unit/network/http/handler_spec.rb +++ b/spec/unit/network/http/handler_spec.rb @@ -240,34 +240,30 @@ describe Puppet::Network::HTTP::Handler do describe "when performing head operation" do before do - @irequest = stub 'indirection_request', :method => :head, :indirection_name => "my_handler", :to_hash => {}, :key => "my_result", :model => @model_class + @handler.stubs(:model).with("my_handler").returns(stub 'model', :indirection => @model_class) + @handler.stubs(:http_method).with(@request).returns("HEAD") + @handler.stubs(:path).with(@request).returns("/production/my_handler/my_result") + @handler.stubs(:params).with(@request).returns({}) @model_class.stubs(:head).returns true end - it "should use the indirection request to find the model class" do - @irequest.expects(:model).returns @model_class - - @handler.do_head(@irequest, @request, @response) - end - it "should use the escaped request key" do @model_class.expects(:head).with do |key, args| key == "my_result" end.returns true - @handler.do_head(@irequest, @request, @response) + @handler.process(@request, @response) end it "should not generate a response when a model head call succeeds" do @handler.expects(:set_response).never - @handler.do_head(@irequest, @request, @response) + @handler.process(@request, @response) end it "should return a 404 when the model head call returns false" do - @model_class.stubs(:name).returns "my name" @handler.expects(:set_response).with { |response, body, status| status == 404 } @model_class.stubs(:head).returns(false) - @handler.do_head(@irequest, @request, @response) + @handler.process(@request, @response) end end diff --git a/spec/unit/node/facts_spec.rb b/spec/unit/node/facts_spec.rb index a130ae3f8..07f5f7e1b 100755 --- a/spec/unit/node/facts_spec.rb +++ b/spec/unit/node/facts_spec.rb @@ -1,6 +1,6 @@ #!/usr/bin/env rspec require 'spec_helper' - +require 'matchers/json' require 'puppet/node/facts' describe Puppet::Node::Facts, "when indirecting" do @@ -110,7 +110,11 @@ describe Puppet::Node::Facts, "when indirecting" do end it "should accept properly formatted pson" do - pson = %Q({"name": "foo", "expiration": "#{@expiration}", "timestamp": "#{@timestamp}", "values": {"a": "1", "b": "2", "c": "3"}}) + facts = Puppet::Node::Facts.new("foo") + facts.values = {"a" => "1", "b" => "2", "c" => "3"} + facts.expiration = Time.now + #pson = %Q({"document_type": "Puppet::Node::Facts", "data: {"name": "foo", "expiration": "#{@expiration}", "timestamp": "#{@timestamp}", "values": {"a": "1", "b": "2", "c": "3"}}}) + pson = %Q({"data": {"name":"foo", "expiration":"#{@expiration}", "timestamp": "#{@timestamp}", "values":{"a":"1","b":"2","c":"3"}}, "document_type":"Puppet::Node::Facts"}) format = Puppet::Network::FormatHandler.format('pson') facts = format.intern(Puppet::Node::Facts,pson) facts.name.should == 'foo' @@ -122,8 +126,30 @@ describe Puppet::Node::Facts, "when indirecting" do Time.stubs(:now).returns(@timestamp) facts = Puppet::Node::Facts.new("foo", {'a' => 1, 'b' => 2, 'c' => 3}) facts.expiration = @expiration - pson = PSON.parse(facts.to_pson) - pson.should == {"name"=>"foo", "timestamp"=>@timestamp.to_s, "expiration"=>@expiration.to_s, "values"=>{"a"=>1, "b"=>2, "c"=>3}} + result = PSON.parse(facts.to_pson) + result.name.should == facts.name + result.values.should == facts.values + result.timestamp.should == facts.timestamp + result.expiration.should == facts.expiration + result.type.should == Puppet::Node::Facts + end + + it "should not include nil values" do + facts = Puppet::Node::Facts.new("foo", {'a' => 1, 'b' => 2, 'c' => 3}) + + # XXX:LAK For some reason this is resurrection the full instance, instead + # of just returning the hash. This code works, but I can't figure out what's + # going on. + newfacts = PSON.parse(facts.to_pson) + newfacts.expiration.should be_nil + end + + it "should be able to handle nil values" do + pson = %Q({"name": "foo", "values": {"a": "1", "b": "2", "c": "3"}}) + format = Puppet::Network::FormatHandler.format('pson') + facts = format.intern(Puppet::Node::Facts,pson) + facts.name.should == 'foo' + facts.expiration.should be_nil end end end diff --git a/spec/unit/node_spec.rb b/spec/unit/node_spec.rb index 7d813ba30..e8f826dca 100755 --- a/spec/unit/node_spec.rb +++ b/spec/unit/node_spec.rb @@ -1,5 +1,6 @@ #!/usr/bin/env rspec require 'spec_helper' +require 'matchers/json' describe Puppet::Node do describe "when managing its environment" do @@ -35,6 +36,69 @@ describe Puppet::Node do node.environment.name.should == :bar end end + + describe "when converting to json" do + before do + @node = Puppet::Node.new("mynode") + end + + it "should provide its name" do + @node.should set_json_attribute('name').to("mynode") + end + + it "should include the classes if set" do + @node.classes = %w{a b c} + @node.should set_json_attribute("classes").to(%w{a b c}) + end + + it "should not include the classes if there are none" do + @node.should_not set_json_attribute('classes') + end + + it "should include parameters if set" do + @node.parameters = {"a" => "b", "c" => "d"} + @node.should set_json_attribute('parameters').to({"a" => "b", "c" => "d"}) + end + + it "should not include the parameters if there are none" do + @node.should_not set_json_attribute('parameters') + end + + it "should include the environment" do + @node.environment = "production" + @node.should set_json_attribute('environment').to('production') + end + end + + describe "when converting from json" do + before do + @node = Puppet::Node.new("mynode") + @format = Puppet::Network::FormatHandler.format('pson') + end + + def from_json(json) + @format.intern(Puppet::Node, json) + end + + it "should set its name" do + Puppet::Node.should read_json_attribute('name').from(@node.to_pson).as("mynode") + end + + it "should include the classes if set" do + @node.classes = %w{a b c} + Puppet::Node.should read_json_attribute('classes').from(@node.to_pson).as(%w{a b c}) + end + + it "should include parameters if set" do + @node.parameters = {"a" => "b", "c" => "d"} + Puppet::Node.should read_json_attribute('parameters').from(@node.to_pson).as({"a" => "b", "c" => "d"}) + end + + it "should include the environment" do + @node.environment = "production" + Puppet::Node.should read_json_attribute('environment').from(@node.to_pson).as(Puppet::Node::Environment.new(:production)) + end + end end describe Puppet::Node, "when initializing" do diff --git a/spec/unit/parser/compiler_spec.rb b/spec/unit/parser/compiler_spec.rb index ced760b76..48aeb98dd 100755 --- a/spec/unit/parser/compiler_spec.rb +++ b/spec/unit/parser/compiler_spec.rb @@ -32,6 +32,14 @@ class CompilerTestResource def evaluate end + + def file + "/fake/file/goes/here" + end + + def line + "42" + end end describe Puppet::Parser::Compiler do @@ -413,52 +421,6 @@ describe Puppet::Parser::Compiler do @compiler.catalog.should be_edge(@scope.resource, resource) end - it "should add an edge to any specified stage for class resources" do - other_stage = resource(:stage, "other") - @compiler.add_resource(@scope, other_stage) - resource = resource(:class, "foo") - resource[:stage] = 'other' - - @compiler.add_resource(@scope, resource) - - @compiler.catalog.edge?(other_stage, resource).should be_true - end - - it "should fail if a non-class resource attempts to set a stage" do - other_stage = resource(:stage, "other") - @compiler.add_resource(@scope, other_stage) - resource = resource(:file, "foo") - resource[:stage] = 'other' - - lambda { @compiler.add_resource(@scope, resource) }.should raise_error(ArgumentError) - end - - it "should fail if an unknown stage is specified" do - resource = resource(:class, "foo") - resource[:stage] = 'other' - - lambda { @compiler.add_resource(@scope, resource) }.should raise_error(ArgumentError) - end - - it "should add edges from the class resources to the parent's stage if no stage is specified" do - main = @compiler.catalog.resource(:stage, :main) - foo_stage = resource(:stage, :foo_stage) - @compiler.add_resource(@scope, foo_stage) - resource = resource(:class, "foo") - @scope.stubs(:resource).returns(:stage => :foo_stage) - @compiler.add_resource(@scope, resource) - - @compiler.catalog.should be_edge(foo_stage, resource) - end - - it "should add edges from top-level class resources to the main stage if no stage is specified" do - main = @compiler.catalog.resource(:stage, :main) - resource = resource(:class, "foo") - @compiler.add_resource(@scope, resource) - - @compiler.catalog.should be_edge(main, resource) - end - it "should not add non-class resources that don't specify a stage to the 'main' stage" do main = @compiler.catalog.resource(:stage, :main) resource = resource(:file, "foo") diff --git a/spec/unit/parser/resource_spec.rb b/spec/unit/parser/resource_spec.rb index b03c18e5f..0d9cba60b 100755 --- a/spec/unit/parser/resource_spec.rb +++ b/spec/unit/parser/resource_spec.rb @@ -131,9 +131,19 @@ describe Puppet::Parser::Resource do end describe "when evaluating" do + before do + @node = Puppet::Node.new "test-node" + @compiler = Puppet::Parser::Compiler.new @node + @catalog = Puppet::Resource::Catalog.new + source = stub('source') + source.stubs(:module_name) + @scope = Puppet::Parser::Scope.new(:compiler => @compiler, :source => source) + @catalog.add_resource(Puppet::Parser::Resource.new("stage", :main, :scope => @scope)) + end + it "should evaluate the associated AST definition" do definition = newdefine "mydefine" - res = Puppet::Parser::Resource.new("mydefine", "whatever", :scope => @scope, :source => @source) + res = Puppet::Parser::Resource.new("mydefine", "whatever", :scope => @scope, :source => @source, :catalog => @catalog) definition.expects(:evaluate_code).with(res) res.evaluate @@ -141,17 +151,65 @@ describe Puppet::Parser::Resource do it "should evaluate the associated AST class" do @class = newclass "myclass" - res = Puppet::Parser::Resource.new("class", "myclass", :scope => @scope, :source => @source) + res = Puppet::Parser::Resource.new("class", "myclass", :scope => @scope, :source => @source, :catalog => @catalog) @class.expects(:evaluate_code).with(res) res.evaluate end it "should evaluate the associated AST node" do nodedef = newnode("mynode") - res = Puppet::Parser::Resource.new("node", "mynode", :scope => @scope, :source => @source) + res = Puppet::Parser::Resource.new("node", "mynode", :scope => @scope, :source => @source, :catalog => @catalog) nodedef.expects(:evaluate_code).with(res) res.evaluate end + + it "should add an edge to any specified stage for class resources" do + @compiler.known_resource_types.add Puppet::Resource::Type.new(:hostclass, "foo", '') + + other_stage = Puppet::Parser::Resource.new(:stage, "other", :scope => @scope, :catalog => @catalog) + @compiler.add_resource(@scope, other_stage) + resource = Puppet::Parser::Resource.new(:class, "foo", :scope => @scope, :catalog => @catalog) + resource[:stage] = 'other' + @compiler.add_resource(@scope, resource) + + resource.evaluate + + @compiler.catalog.edge?(other_stage, resource).should be_true + end + + it "should fail if an unknown stage is specified" do + @compiler.known_resource_types.add Puppet::Resource::Type.new(:hostclass, "foo", '') + + resource = Puppet::Parser::Resource.new(:class, "foo", :scope => @scope, :catalog => @catalog) + resource[:stage] = 'other' + + lambda { resource.evaluate }.should raise_error(ArgumentError, /Could not find stage other specified by/) + end + + it "should add edges from the class resources to the parent's stage if no stage is specified" do + main = @compiler.catalog.resource(:stage, :main) + foo_stage = Puppet::Parser::Resource.new(:stage, :foo_stage, :scope => @scope, :catalog => @catalog) + @compiler.add_resource(@scope, foo_stage) + @compiler.known_resource_types.add Puppet::Resource::Type.new(:hostclass, "foo", '') + resource = Puppet::Parser::Resource.new(:class, "foo", :scope => @scope, :catalog => @catalog) + resource[:stage] = 'foo_stage' + @compiler.add_resource(@scope, resource) + + resource.evaluate + + @compiler.catalog.should be_edge(foo_stage, resource) + end + + it "should add edges from top-level class resources to the main stage if no stage is specified" do + main = @compiler.catalog.resource(:stage, :main) + @compiler.known_resource_types.add Puppet::Resource::Type.new(:hostclass, "foo", '') + resource = Puppet::Parser::Resource.new(:class, "foo", :scope => @scope, :catalog => @catalog) + @compiler.add_resource(@scope, resource) + + resource.evaluate + + @compiler.catalog.should be_edge(main, resource) + end end describe "when finishing" do diff --git a/spec/unit/parser/scope_spec.rb b/spec/unit/parser/scope_spec.rb index bf4d1e29e..5308856ed 100755 --- a/spec/unit/parser/scope_spec.rb +++ b/spec/unit/parser/scope_spec.rb @@ -121,7 +121,11 @@ describe Puppet::Parser::Scope do def create_class_scope(name) klass = newclass(name) - Puppet::Parser::Resource.new("class", name, :scope => @scope, :source => mock('source')).evaluate + + catalog = Puppet::Resource::Catalog.new + catalog.add_resource(Puppet::Parser::Resource.new("stage", :main, :scope => Puppet::Parser::Scope.new)) + + Puppet::Parser::Resource.new("class", name, :scope => @scope, :source => mock('source'), :catalog => catalog).evaluate @scope.class_scope(klass) end diff --git a/spec/unit/provider/cisco_spec.rb b/spec/unit/provider/cisco_spec.rb new file mode 100644 index 000000000..08320731c --- /dev/null +++ b/spec/unit/provider/cisco_spec.rb @@ -0,0 +1,16 @@ +#!/usr/bin/env ruby + +require File.dirname(__FILE__) + '/../../spec_helper' + +require 'puppet/provider/cisco' + +describe Puppet::Provider::Cisco do + it "should implement a device class method" do + Puppet::Provider::Cisco.should respond_to(:device) + end + + it "should create a cisco device instance" do + Puppet::Util::NetworkDevice::Cisco::Device.expects(:new).returns :device + Puppet::Provider::Cisco.device(:url).should == :device + end +end
\ No newline at end of file diff --git a/spec/unit/provider/interface/cisco_spec.rb b/spec/unit/provider/interface/cisco_spec.rb index d1f70609f..c18f87cf8 100755 --- a/spec/unit/provider/interface/cisco_spec.rb +++ b/spec/unit/provider/interface/cisco_spec.rb @@ -8,12 +8,13 @@ provider_class = Puppet::Type.type(:interface).provider(:cisco) describe provider_class do before do + @device = stub_everything 'device' @resource = stub("resource", :name => "Fa0/1") - @provider = provider_class.new(@resource) + @provider = provider_class.new(@device, @resource) end - it "should have a parent of Puppet::Provider::NetworkDevice" do - provider_class.should < Puppet::Provider::NetworkDevice + it "should have a parent of Puppet::Provider::Cisco" do + provider_class.should < Puppet::Provider::Cisco end it "should have an instances method" do @@ -22,31 +23,24 @@ describe provider_class do describe "when looking up instances at prefetch" do before do - @device = stub_everything 'device' - Puppet::Util::NetworkDevice::Cisco::Device.stubs(:new).returns(@device) @device.stubs(:command).yields(@device) end - it "should initialize the network device with the given url" do - Puppet::Util::NetworkDevice::Cisco::Device.expects(:new).with(:url).returns(@device) - provider_class.lookup(:url, "Fa0/1") - end - it "should delegate to the device interface fetcher" do @device.expects(:interface) - provider_class.lookup("", "Fa0/1") + provider_class.lookup(@device, "Fa0/1") end it "should return the given interface data" do @device.expects(:interface).returns({ :description => "thisone", :mode => :access}) - provider_class.lookup("", "Fa0").should == {:description => "thisone", :mode => :access } + provider_class.lookup(@device, "Fa0").should == {:description => "thisone", :mode => :access } end end describe "when an instance is being flushed" do it "should call the device interface update method with current and past properties" do - @instance = provider_class.new(:ensure => :present, :name => "Fa0/1", :description => "myinterface") + @instance = provider_class.new(@device, :ensure => :present, :name => "Fa0/1", :description => "myinterface") @instance.description = "newdesc" @instance.resource = @resource @resource.stubs(:[]).with(:name).returns("Fa0/1") diff --git a/spec/unit/provider/network_device_spec.rb b/spec/unit/provider/network_device_spec.rb index 83d2bdc01..e2a87cf4e 100755 --- a/spec/unit/provider/network_device_spec.rb +++ b/spec/unit/provider/network_device_spec.rb @@ -3,10 +3,15 @@ require File.dirname(__FILE__) + '/../../spec_helper' require 'puppet/provider/network_device' +require 'ostruct' Puppet::Type.type(:vlan).provide :test, :parent => Puppet::Provider::NetworkDevice do mk_resource_methods - def self.lookup(device_url, name) + def self.lookup(device, name) + end + + def self.device(url) + :device end end @@ -34,7 +39,7 @@ describe provider_class do end it "should lookup an entry for each passed resource" do - provider_class.expects(:lookup).with(nil, "200").returns nil + provider_class.expects(:lookup).with{ |device,value| value == "200" }.returns nil provider_class.stubs(:new) @resource.stubs(:provider=) @@ -44,7 +49,7 @@ describe provider_class do describe "resources that do not exist" do it "should create a provider with :ensure => :absent" do provider_class.stubs(:lookup).returns(nil) - provider_class.expects(:new).with(:ensure => :absent).returns "myprovider" + provider_class.expects(:new).with(:device, :ensure => :absent).returns "myprovider" @resource.expects(:provider=).with("myprovider") provider_class.prefetch(@resources) end @@ -54,7 +59,7 @@ describe provider_class do it "should create a provider with the results of the find and ensure at present" do provider_class.stubs(:lookup).returns({ :name => "200", :description => "myvlan"}) - provider_class.expects(:new).with(:name => "200", :description => "myvlan", :ensure => :present).returns "myprovider" + provider_class.expects(:new).with(:device, :name => "200", :description => "myvlan", :ensure => :present).returns "myprovider" @resource.expects(:provider=).with("myprovider") provider_class.prefetch(@resources) @@ -74,7 +79,7 @@ describe provider_class do end it "should store a copy of the hash as its vlan_properties" do - instance = provider_class.new(:one => :two) + instance = provider_class.new(:device, :one => :two) instance.former_properties.should == {:one => :two} end end @@ -82,7 +87,7 @@ describe provider_class do describe "when an instance" do before do - @instance = provider_class.new + @instance = provider_class.new(:device) @property_class = stub 'property_class', :array_matching => :all, :superclass => Puppet::Property @resource_class = stub 'resource_class', :attrclass => @property_class, :valid_parameter? => true, :validproperties => [:description] @@ -98,12 +103,12 @@ describe provider_class do end it "should indicate when the instance already exists" do - @instance = provider_class.new(:ensure => :present) + @instance = provider_class.new(:device, :ensure => :present) @instance.exists?.should be_true end it "should indicate when the instance does not exist" do - @instance = provider_class.new(:ensure => :absent) + @instance = provider_class.new(:device, :ensure => :absent) @instance.exists?.should be_false end diff --git a/spec/unit/provider/package/pkgutil_spec.rb b/spec/unit/provider/package/pkgutil_spec.rb new file mode 100755 index 000000000..5549b3f6d --- /dev/null +++ b/spec/unit/provider/package/pkgutil_spec.rb @@ -0,0 +1,182 @@ +#!/usr/bin/env ruby + +require File.dirname(__FILE__) + '/../../../spec_helper' + +provider = Puppet::Type.type(:package).provider(:pkgutil) + +describe provider do + before(:each) do + @resource = Puppet::Type.type(:package).new( + :name => "TESTpkg", + :ensure => :present, + :provider => :pkgutil + ) + @provider = provider.new(@resource) + + # Stub all file and config tests + provider.stubs(:healthcheck) + end + + it "should have an install method" do + @provider.should respond_to(:install) + end + + it "should have a latest method" do + @provider.should respond_to(:uninstall) + end + + it "should have an update method" do + @provider.should respond_to(:update) + end + + it "should have a latest method" do + @provider.should respond_to(:latest) + end + + describe "when installing" do + it "should use a command without versioned package" do + @resource[:ensure] = :latest + @provider.expects(:pkguti).with('-y', '-i', 'TESTpkg') + @provider.install + end + end + + describe "when updating" do + it "should use a command without versioned package" do + @provider.expects(:pkguti).with('-y', '-u', 'TESTpkg') + @provider.update + end + end + + describe "when uninstalling" do + it "should call the remove operation" do + @provider.expects(:pkguti).with('-y', '-r', 'TESTpkg') + @provider.uninstall + end + end + + describe "when getting latest version" do + it "should return TESTpkg's version string" do + fake_data = " +noisy output here +TESTpkg 1.4.5,REV=2007.11.18 1.4.5,REV=2007.11.20" + provider.expects(:pkguti).with(['-c', '--single', 'TESTpkg']).returns fake_data + @provider.latest.should == "1.4.5,REV=2007.11.20" + end + + it "should handle TESTpkg's 'SAME' version string" do + fake_data = " +noisy output here +TESTpkg 1.4.5,REV=2007.11.18 SAME" + provider.expects(:pkguti).with(['-c', '--single', 'TESTpkg']).returns fake_data + @provider.latest.should == "1.4.5,REV=2007.11.18" + end + + it "should handle a non-existent package" do + fake_data = "noisy output here +Not in catalog" + provider.expects(:pkguti).with(['-c', '--single', 'TESTpkg']).returns fake_data + @provider.latest.should == nil + end + + it "should warn on unknown pkgutil noise" do + provider.expects(:pkguti).with(['-c', '--single', 'TESTpkg']).returns("testingnoise") + @provider.latest.should == nil + end + + it "should ignore pkgutil noise/headers to find TESTpkg" do + fake_data = "# stuff +=> Fetching new catalog and descriptions (http://mirror.opencsw.org/opencsw/unstable/i386/5.11) if available ... +2011-02-19 23:05:46 URL:http://mirror.opencsw.org/opencsw/unstable/i386/5.11/catalog [534635/534635] -> \"/var/opt/csw/pkgutil/catalog.mirror.opencsw.org_opencsw_unstable_i386_5.11.tmp\" [1] +Checking integrity of /var/opt/csw/pkgutil/catalog.mirror.opencsw.org_opencsw_unstable_i386_5.11 with gpg. +gpg: Signature made February 17, 2011 05:27:53 PM GMT using DSA key ID E12E9D2F +gpg: Good signature from \"Distribution Manager <dm@blastwave.org>\" +==> 2770 packages loaded from /var/opt/csw/pkgutil/catalog.mirror.opencsw.org_opencsw_unstable_i386_5.11 +package installed catalog +TESTpkg 1.4.5,REV=2007.11.18 1.4.5,REV=2007.11.20" + provider.expects(:pkguti).with(['-c', '--single', 'TESTpkg']).returns fake_data + @provider.latest.should == "1.4.5,REV=2007.11.20" + end + + it "should find REALpkg via an alias (TESTpkg)" do + fake_data = " +noisy output here +REALpkg 1.4.5,REV=2007.11.18 1.4.5,REV=2007.11.20" + provider.expects(:pkguti).with(['-c', '--single', 'TESTpkg']).returns fake_data + @provider.query[:name].should == "TESTpkg" + end + end + + describe "when querying current version" do + it "should return TESTpkg's version string" do + fake_data = "TESTpkg 1.4.5,REV=2007.11.18 1.4.5,REV=2007.11.20" + provider.expects(:pkguti).with(['-c', '--single', 'TESTpkg']).returns fake_data + @provider.query[:ensure].should == "1.4.5,REV=2007.11.18" + end + + it "should handle a package that isn't installed" do + fake_data = "TESTpkg notinst 1.4.5,REV=2007.11.20" + provider.expects(:pkguti).with(['-c', '--single', 'TESTpkg']).returns fake_data + @provider.query[:ensure].should == :absent + end + + it "should handle a non-existent package" do + fake_data = "noisy output here +Not in catalog" + provider.expects(:pkguti).with(['-c', '--single', 'TESTpkg']).returns fake_data + @provider.query[:ensure].should == :absent + end + end + + describe "when querying current instances" do + it "should warn on unknown pkgutil noise" do + provider.expects(:pkguti).with(['-a']).returns("testingnoise") + provider.expects(:pkguti).with(['-c']).returns("testingnoise") + Puppet.expects(:warning).times(2) + provider.expects(:new).never + provider.instances.should == [] + end + + it "should return TESTpkg's version string" do + fake_data = "TESTpkg TESTpkg 1.4.5,REV=2007.11.20" + provider.expects(:pkguti).with(['-a']).returns fake_data + + fake_data = "TESTpkg 1.4.5,REV=2007.11.18 1.4.5,REV=2007.11.20" + provider.expects(:pkguti).with(['-c']).returns fake_data + + testpkg = mock 'pkg1' + provider.expects(:new).with(:ensure => "1.4.5,REV=2007.11.18", :name => "TESTpkg", :provider => :pkgutil).returns testpkg + provider.instances.should == [testpkg] + end + + it "should also return both TESTpkg and mypkg alias instances" do + fake_data = "mypkg TESTpkg 1.4.5,REV=2007.11.20" + provider.expects(:pkguti).with(['-a']).returns fake_data + + fake_data = "TESTpkg 1.4.5,REV=2007.11.18 1.4.5,REV=2007.11.20" + provider.expects(:pkguti).with(['-c']).returns fake_data + + testpkg = mock 'pkg1' + provider.expects(:new).with(:ensure => "1.4.5,REV=2007.11.18", :name => "TESTpkg", :provider => :pkgutil).returns testpkg + + aliaspkg = mock 'pkg2' + provider.expects(:new).with(:ensure => "1.4.5,REV=2007.11.18", :name => "mypkg", :provider => :pkgutil).returns aliaspkg + + provider.instances.should == [testpkg,aliaspkg] + end + + it "shouldn't mind noise in the -a output" do + fake_data = "noisy output here" + provider.expects(:pkguti).with(['-a']).returns fake_data + + fake_data = "TESTpkg 1.4.5,REV=2007.11.18 1.4.5,REV=2007.11.20" + provider.expects(:pkguti).with(['-c']).returns fake_data + + testpkg = mock 'pkg1' + provider.expects(:new).with(:ensure => "1.4.5,REV=2007.11.18", :name => "TESTpkg", :provider => :pkgutil).returns testpkg + + provider.instances.should == [testpkg] + end + end + +end diff --git a/spec/unit/provider/vlan/cisco_spec.rb b/spec/unit/provider/vlan/cisco_spec.rb index bb243a75e..a67290eb4 100755 --- a/spec/unit/provider/vlan/cisco_spec.rb +++ b/spec/unit/provider/vlan/cisco_spec.rb @@ -8,12 +8,13 @@ provider_class = Puppet::Type.type(:vlan).provider(:cisco) describe provider_class do before do + @device = stub_everything 'device' @resource = stub("resource", :name => "200") - @provider = provider_class.new(@resource) + @provider = provider_class.new(@device, @resource) end - it "should have a parent of Puppet::Provider::NetworkDevice" do - provider_class.should < Puppet::Provider::NetworkDevice + it "should have a parent of Puppet::Provider::Cisco" do + provider_class.should < Puppet::Provider::Cisco end it "should have an instances method" do @@ -22,31 +23,24 @@ describe provider_class do describe "when looking up instances at prefetch" do before do - @device = stub_everything 'device' - Puppet::Util::NetworkDevice::Cisco::Device.stubs(:new).returns(@device) @device.stubs(:command).yields(@device) end - it "should initialize the network device with the given url" do - Puppet::Util::NetworkDevice::Cisco::Device.expects(:new).with(:url).returns(@device) - provider_class.lookup(:url, "200") - end - it "should delegate to the device vlans" do @device.expects(:parse_vlans) - provider_class.lookup("", "200") + provider_class.lookup(@device, "200") end it "should return only the given vlan" do @device.expects(:parse_vlans).returns({"200" => { :description => "thisone" }, "1" => { :description => "nothisone" }}) - provider_class.lookup("", "200").should == {:description => "thisone" } + provider_class.lookup(@device, "200").should == {:description => "thisone" } end end describe "when an instance is being flushed" do it "should call the device update_vlan method with its vlan id, current attributes, and desired attributes" do - @instance = provider_class.new(:ensure => :present, :name => "200", :description => "myvlan") + @instance = provider_class.new(@device, :ensure => :present, :name => "200", :description => "myvlan") @instance.description = "myvlan2" @instance.resource = @resource @resource.stubs(:[]).with(:name).returns("200") diff --git a/spec/unit/resource/catalog_spec.rb b/spec/unit/resource/catalog_spec.rb index ae65aa91a..ebf523eab 100755 --- a/spec/unit/resource/catalog_spec.rb +++ b/spec/unit/resource/catalog_spec.rb @@ -592,6 +592,7 @@ describe Puppet::Resource::Catalog, "when compiling" do Puppet::Transaction.stubs(:new).returns(@transaction) @transaction.stubs(:evaluate) @transaction.stubs(:add_times) + @transaction.stubs(:for_network_device=) Puppet.settings.stubs(:use) end diff --git a/spec/unit/transaction_spec.rb b/spec/unit/transaction_spec.rb index 4157e58ac..d7788c06c 100755 --- a/spec/unit/transaction_spec.rb +++ b/spec/unit/transaction_spec.rb @@ -270,6 +270,24 @@ describe Puppet::Transaction do @resource.stubs(:virtual?).returns true @transaction.should be_skip(@resource) end + + it "should skip device only resouce on normal host" do + @resource.stubs(:appliable_to_device?).returns true + @transaction.for_network_device = false + @transaction.should be_skip(@resource) + end + + it "should not skip device only resouce on remote device" do + @resource.stubs(:appliable_to_device?).returns true + @transaction.for_network_device = true + @transaction.should_not be_skip(@resource) + end + + it "should skip host resouce on device" do + @resource.stubs(:appliable_to_device?).returns false + @transaction.for_network_device = true + @transaction.should be_skip(@resource) + end end describe "when determining if tags are missing" do diff --git a/spec/unit/type/interface_spec.rb b/spec/unit/type/interface_spec.rb index 68f4c765f..12ba225d9 100755 --- a/spec/unit/type/interface_spec.rb +++ b/spec/unit/type/interface_spec.rb @@ -3,6 +3,7 @@ require File.dirname(__FILE__) + '/../../spec_helper' describe Puppet::Type.type(:interface) do + it "should have a 'name' parameter'" do Puppet::Type.type(:interface).new(:name => "FastEthernet 0/1")[:name].should == "FastEthernet 0/1" end @@ -15,6 +16,10 @@ describe Puppet::Type.type(:interface) do Puppet::Type.type(:interface).attrtype(:ensure).should == :property end + it "should be applied on device" do + Puppet::Type.type(:interface).new(:name => "FastEthernet 0/1").should be_appliable_to_device + end + [:description, :speed, :duplex, :native_vlan, :encapsulation, :mode, :allowed_trunk_vlans, :etherchannel, :ipaddress].each do |p| it "should have a #{p} property" do Puppet::Type.type(:interface).attrtype(p).should == :property diff --git a/spec/unit/type/schedule_spec.rb b/spec/unit/type/schedule_spec.rb index 7599411e4..33064ca6f 100755 --- a/spec/unit/type/schedule_spec.rb +++ b/spec/unit/type/schedule_spec.rb @@ -36,6 +36,14 @@ describe Puppet::Type.type(:schedule) do describe Puppet::Type.type(:schedule) do include ScheduleTesting + it "should apply to device" do + @schedule.should be_appliable_to_device + end + + it "should apply to host" do + @schedule.should be_appliable_to_host + end + it "should default to :distance for period-matching" do @schedule[:periodmatch].should == :distance end diff --git a/spec/unit/type/vlan_spec.rb b/spec/unit/type/vlan_spec.rb index 2983a58e9..3bee14bbd 100755 --- a/spec/unit/type/vlan_spec.rb +++ b/spec/unit/type/vlan_spec.rb @@ -3,6 +3,7 @@ require File.dirname(__FILE__) + '/../../spec_helper' describe Puppet::Type.type(:vlan) do + it "should have a 'name' parameter'" do Puppet::Type.type(:vlan).new(:name => "200")[:name].should == "200" end @@ -11,6 +12,10 @@ describe Puppet::Type.type(:vlan) do Puppet::Type.type(:vlan).new(:name => "200", :device_url => :device)[:device_url].should == :device end + it "should be applied on device" do + Puppet::Type.type(:vlan).new(:name => "200").should be_appliable_to_device + end + it "should have an ensure property" do Puppet::Type.type(:vlan).attrtype(:ensure).should == :property end diff --git a/spec/unit/util/network_device/cisco/device_spec.rb b/spec/unit/util/network_device/cisco/device_spec.rb index 82b0666e6..33242a1ab 100755 --- a/spec/unit/util/network_device/cisco/device_spec.rb +++ b/spec/unit/util/network_device/cisco/device_spec.rb @@ -392,130 +392,17 @@ eos @cisco.parse_interface_config("Gi0/17").should == {:etherchannel=>"1"} end end + + describe "when finding device facts" do + it "should delegate to the cisco facts entity" do + facts = stub 'facts' + Puppet::Util::NetworkDevice::Cisco::Facts.expects(:new).returns(facts) + + facts.expects(:retrieve).returns(:facts) + + @cisco.facts.should == :facts + end + end + end -# static access -# Switch#sh interfaces FastEthernet 0/1 switchport -# Name: Fa0/1 -# Switchport: Enabled -# Administrative mode: static access -# Operational Mode: static access -# Administrative Trunking Encapsulation: isl -# Operational Trunking Encapsulation: isl -# Negotiation of Trunking: Disabled -# Access Mode VLAN: 100 (SHDSL) -# Trunking Native Mode VLAN: 1 (default) -# Trunking VLANs Enabled: NONE -# Pruning VLANs Enabled: NONE -# -# Priority for untagged frames: 0 -# Override vlan tag priority: FALSE -# Voice VLAN: none -# Appliance trust: none -# Self Loopback: No -# Switch# - -# c2960#sh interfaces GigabitEthernet 0/1 switchport -# Name: Gi0/1 -# Switchport: Enabled -# Administrative Mode: trunk -# Operational Mode: trunk -# Administrative Trunking Encapsulation: dot1q -# Operational Trunking Encapsulation: dot1q -# Negotiation of Trunking: On -# Access Mode VLAN: 1 (default) -# Trunking Native Mode VLAN: 1 (default) -# Administrative Native VLAN tagging: enabled -# Voice VLAN: none -# Administrative private-vlan host-association: none -# Administrative private-vlan mapping: none -# Administrative private-vlan trunk native VLAN: none -# Administrative private-vlan trunk Native VLAN tagging: enabled -# Administrative private-vlan trunk encapsulation: dot1q -# Administrative private-vlan trunk normal VLANs: none -# Administrative private-vlan trunk associations: none -# Administrative private-vlan trunk mappings: none -# Operational private-vlan: none -# Trunking VLANs Enabled: 1,99 -# Pruning VLANs Enabled: 2-1001 -# Capture Mode Disabled -# Capture VLANs Allowed: ALL -# -# Protected: false -# Unknown unicast blocked: disabled -# Unknown multicast blocked: disabled -# Appliance trust: none -# c2960# - -# c2960#sh interfaces GigabitEthernet 0/2 switchport -# Name: Gi0/2 -# Switchport: Enabled -# Administrative Mode: static access -# Operational Mode: static access -# Administrative Trunking Encapsulation: dot1q -# Operational Trunking Encapsulation: native -# Negotiation of Trunking: Off -# Access Mode VLAN: 99 (MGMT) -# Trunking Native Mode VLAN: 1 (default) -# Administrative Native VLAN tagging: enabled -# Voice VLAN: none -# Administrative private-vlan host-association: none -# Administrative private-vlan mapping: none -# Administrative private-vlan trunk native VLAN: none -# Administrative private-vlan trunk Native VLAN tagging: enabled -# Administrative private-vlan trunk encapsulation: dot1q -# Administrative private-vlan trunk normal VLANs: none -# Administrative private-vlan trunk associations: none -# Administrative private-vlan trunk mappings: none -# Operational private-vlan: none -# Trunking VLANs Enabled: ALL -# Pruning VLANs Enabled: 2-1001 -# Capture Mode Disabled -# Capture VLANs Allowed: ALL -# -# Protected: false -# Unknown unicast blocked: disabled -# Unknown multicast blocked: disabled -# Appliance trust: none -# c2960# - -# c877#sh interfaces FastEthernet 1 switchport -# Name: Fa1 -# Switchport: Enabled -# Administrative Mode: trunk -# Operational Mode: trunk -# Administrative Trunking Encapsulation: dot1q -# Operational Trunking Encapsulation: dot1q -# Negotiation of Trunking: Disabled -# Access Mode VLAN: 0 ((Inactive)) -# Trunking Native Mode VLAN: 1 (default) -# Trunking VLANs Enabled: ALL -# Trunking VLANs Active: 1 -# Protected: false -# Priority for untagged frames: 0 -# Override vlan tag priority: FALSE -# Voice VLAN: none -# Appliance trust: none - - -# c2960#sh etherchannel summary -# Flags: D - down P - bundled in port-channel -# I - stand-alone s - suspended -# H - Hot-standby (LACP only) -# R - Layer3 S - Layer2 -# U - in use f - failed to allocate aggregator -# -# M - not in use, minimum links not met -# u - unsuitable for bundling -# w - waiting to be aggregated -# d - default port -# -# -# Number of channel-groups in use: 1 -# Number of aggregators: 1 -# -# Group Port-channel Protocol Ports -# ------+-------------+-----------+----------------------------------------------- -# 1 Po1(SU) LACP Gi0/17(P) Gi0/18(P) -# -# c2960# diff --git a/spec/unit/util/network_device/cisco/facts_spec.rb b/spec/unit/util/network_device/cisco/facts_spec.rb new file mode 100644 index 000000000..bb29ac292 --- /dev/null +++ b/spec/unit/util/network_device/cisco/facts_spec.rb @@ -0,0 +1,63 @@ +#!/usr/bin/env ruby + +require File.dirname(__FILE__) + '/../../../../spec_helper' + +require 'puppet/util/network_device' +require 'puppet/util/network_device/cisco/facts' + +describe Puppet::Util::NetworkDevice::Cisco::Facts do + before(:each) do + @transport = stub_everything 'transport' + @facts = Puppet::Util::NetworkDevice::Cisco::Facts.new(@transport) + end + + { + "cisco WS-C2924C-XL (PowerPC403GA) processor (revision 0x11) with 8192K/1024K bytes of memory." => {:hardwaremodel => "WS-C2924C-XL", :memorysize => "8192K", :processor => "PowerPC403GA", :hardwarerevision => "0x11" }, + "Cisco 1841 (revision 5.0) with 355328K/37888K bytes of memory." => {:hardwaremodel=>"1841", :memorysize => "355328K", :hardwarerevision => "5.0" }, + "Cisco 877 (MPC8272) processor (revision 0x200) with 118784K/12288K bytes of memory." => {:hardwaremodel=>"877", :memorysize => "118784K", :processor => "MPC8272", :hardwarerevision => "0x200" }, + "cisco WS-C2960G-48TC-L (PowerPC405) processor (revision C0) with 61440K/4088K bytes of memory." => {:hardwaremodel=>"WS-C2960G-48TC-L", :memorysize => "61440K", :processor => "PowerPC405", :hardwarerevision => "C0" }, + "cisco WS-C2950T-24 (RC32300) processor (revision R0) with 19959K bytes of memory." => {:hardwaremodel=>"WS-C2950T-24", :memorysize => "19959K", :processor => "RC32300", :hardwarerevision => "R0" } + }.each do |ver, expected| + it "should parse show ver output for hardware device facts" do + @transport.stubs(:command).with("sh ver").returns(<<eos) +Switch>sh ver +#{ver} +Switch> +eos + @facts.parse_show_ver.should == expected + end + end + + { +"Switch uptime is 1 year, 12 weeks, 6 days, 22 hours, 32 minutes" => { :hostname => "Switch", :uptime => "1 year, 12 weeks, 6 days, 22 hours, 32 minutes", :uptime_seconds => 39393120, :uptime_days => 455 }, +"c2950 uptime is 3 weeks, 1 day, 23 hours, 36 minutes" => { :hostname => "c2950", :uptime => "3 weeks, 1 day, 23 hours, 36 minutes", :uptime_days => 22, :uptime_seconds => 1985760}, +"router uptime is 5 weeks, 1 day, 3 hours, 30 minutes" => { :hostname => "router", :uptime => "5 weeks, 1 day, 3 hours, 30 minutes", :uptime_days => 36, :uptime_seconds => 3123000 }, +"c2950 uptime is 1 minute" => { :hostname => "c2950", :uptime => "1 minute", :uptime_days => 0, :uptime_seconds => 60 } + }.each do |ver, expected| + it "should parse show ver output for device uptime facts" do + @transport.stubs(:command).with("sh ver").returns(<<eos) +Switch>sh ver +#{ver} +Switch> +eos + @facts.parse_show_ver.should == expected + end + end + + { +"IOS (tm) C2900XL Software (C2900XL-C3H2S-M), Version 12.0(5)WC10, RELEASE SOFTWARE (fc1)"=> { :operatingsystem => "IOS", :operatingsystemrelease => "12.0(5)WC10", :operatingsystemmajrelease => "12.0WC", :operatingsystemfeature => "C3H2S"}, +"IOS (tm) C2950 Software (C2950-I6K2L2Q4-M), Version 12.1(22)EA8a, RELEASE SOFTWARE (fc1)"=> { :operatingsystem => "IOS", :operatingsystemrelease => "12.1(22)EA8a", :operatingsystemmajrelease => "12.1EA", :operatingsystemfeature => "I6K2L2Q4"}, +"Cisco IOS Software, C2960 Software (C2960-LANBASEK9-M), Version 12.2(44)SE, RELEASE SOFTWARE (fc1)"=>{ :operatingsystem => "IOS", :operatingsystemrelease => "12.2(44)SE", :operatingsystemmajrelease => "12.2SE", :operatingsystemfeature => "LANBASEK9"}, +"Cisco IOS Software, C870 Software (C870-ADVIPSERVICESK9-M), Version 12.4(11)XJ4, RELEASE SOFTWARE (fc2)"=>{ :operatingsystem => "IOS", :operatingsystemrelease => "12.4(11)XJ4", :operatingsystemmajrelease => "12.4XJ", :operatingsystemfeature => "ADVIPSERVICESK9"}, +"Cisco IOS Software, 1841 Software (C1841-ADVSECURITYK9-M), Version 12.4(24)T4, RELEASE SOFTWARE (fc2)" =>{ :operatingsystem => "IOS", :operatingsystemrelease => "12.4(24)T4", :operatingsystemmajrelease => "12.4T", :operatingsystemfeature => "ADVSECURITYK9"}, + }.each do |ver, expected| + it "should parse show ver output for device software version facts" do + @transport.stubs(:command).with("sh ver").returns(<<eos) +Switch>sh ver +#{ver} +Switch> +eos + @facts.parse_show_ver.should == expected + end + end +end diff --git a/spec/unit/util/network_device/config_spec.rb b/spec/unit/util/network_device/config_spec.rb new file mode 100644 index 000000000..52796f30b --- /dev/null +++ b/spec/unit/util/network_device/config_spec.rb @@ -0,0 +1,102 @@ +#!/usr/bin/env ruby + +require File.dirname(__FILE__) + '/../../../spec_helper' + +require 'puppet/util/network_device/config' + +describe Puppet::Util::NetworkDevice::Config do + before(:each) do + Puppet[:deviceconfig] = "/dummy" + FileTest.stubs(:exists?).with("/dummy").returns(true) + end + + describe "when initializing" do + before :each do + Puppet::Util::NetworkDevice::Config.any_instance.stubs(:read) + end + + it "should use the deviceconfig setting as pathname" do + Puppet.expects(:[]).with(:deviceconfig).returns("/dummy") + + Puppet::Util::NetworkDevice::Config.new + end + + it "should raise an error if no file is defined finally" do + Puppet.expects(:[]).with(:deviceconfig).returns(nil) + + lambda { Puppet::Util::NetworkDevice::Config.new }.should raise_error(Puppet::DevError) + end + + it "should read and parse the file" do + Puppet::Util::NetworkDevice::Config.any_instance.expects(:read) + + Puppet::Util::NetworkDevice::Config.new + end + end + + describe "when parsing device" do + before :each do + @config = Puppet::Util::NetworkDevice::Config.new + @config.stubs(:changed?).returns(true) + @fd = stub 'fd' + File.stubs(:open).yields(@fd) + end + + it "should skip comments" do + @fd.stubs(:each).yields(' # comment') + + OpenStruct.expects(:new).never + + @config.read + end + + it "should increment line number even on commented lines" do + @fd.stubs(:each).multiple_yields(' # comment','[router.puppetlabs.com]') + + @config.read + @config.devices.should be_include('router.puppetlabs.com') + end + + it "should skip blank lines" do + @fd.stubs(:each).yields(' ') + + @config.read + @config.devices.should be_empty + end + + it "should produce the correct line number" do + @fd.stubs(:each).multiple_yields(' ', '[router.puppetlabs.com]') + + @config.read + @config.devices['router.puppetlabs.com'].line.should == 2 + end + + it "should throw an error if the current device already exists" do + @fd.stubs(:each).multiple_yields('[router.puppetlabs.com]', '[router.puppetlabs.com]') + + lambda { @config.read }.should raise_error + end + + it "should create a new device for each found device line" do + @fd.stubs(:each).multiple_yields('[router.puppetlabs.com]', '[swith.puppetlabs.com]') + + @config.read + @config.devices.size.should == 2 + end + + it "should parse the device type" do + @fd.stubs(:each).multiple_yields('[router.puppetlabs.com]', 'type cisco') + + @config.read + @config.devices['router.puppetlabs.com'].provider.should == 'cisco' + end + + it "should parse the device url" do + @fd.stubs(:each).multiple_yields('[router.puppetlabs.com]', 'type cisco', 'url ssh://test/') + + @config.read + @config.devices['router.puppetlabs.com'].url.should == 'ssh://test/' + end + end + +end
\ No newline at end of file diff --git a/spec/unit/util/network_device/transport/ssh_spec.rb b/spec/unit/util/network_device/transport/ssh_spec.rb index 0e91ed9f9..18d22a953 100755 --- a/spec/unit/util/network_device/transport/ssh_spec.rb +++ b/spec/unit/util/network_device/transport/ssh_spec.rb @@ -30,6 +30,14 @@ describe Puppet::Util::NetworkDevice::Transport::Ssh, :if => Puppet.features.ssh @transport.connect end + it "should raise a Puppet::Error when encountering an authentication failure" do + Net::SSH.expects(:start).raises Net::SSH::AuthenticationFailed + @transport.host = "localhost" + @transport.user = "user" + + lambda { @transport.connect }.should raise_error Puppet::Error + end + describe "when connected" do before(:each) do @ssh = stub_everything 'ssh' diff --git a/spec/unit/util/network_device_spec.rb b/spec/unit/util/network_device_spec.rb new file mode 100644 index 000000000..0f7c6036b --- /dev/null +++ b/spec/unit/util/network_device_spec.rb @@ -0,0 +1,50 @@ +#!/usr/bin/env rspec +require 'spec_helper' + +require 'ostruct' +require 'puppet/util/network_device' + +describe Puppet::Util::NetworkDevice do + + before(:each) do + @device = OpenStruct.new(:name => "name", :provider => "test") + end + + after(:each) do + Puppet::Util::NetworkDevice.teardown + end + + class Puppet::Util::NetworkDevice::Test + class Device + def initialize(device) + end + end + end + + describe "when initializing the remote network device singleton" do + it "should load the network device code" do + Puppet::Util::NetworkDevice.expects(:require) + Puppet::Util::NetworkDevice.init(@device) + end + + it "should create a network device instance" do + Puppet::Util::NetworkDevice.stubs(:require) + Puppet::Util::NetworkDevice::Test::Device.expects(:new) + Puppet::Util::NetworkDevice.init(@device) + end + + it "should raise an error if the remote device instance can't be created" do + Puppet::Util::NetworkDevice.stubs(:require).raises("error") + lambda { Puppet::Util::NetworkDevice.init(@device) }.should raise_error + end + + it "should let caller to access the singleton device" do + device = stub 'device' + Puppet::Util::NetworkDevice.stubs(:require) + Puppet::Util::NetworkDevice::Test::Device.expects(:new).returns(device) + Puppet::Util::NetworkDevice.init(@device) + + Puppet::Util::NetworkDevice.current.should == device + end + end +end diff --git a/spec/unit/util/queue/stomp_spec.rb b/spec/unit/util/queue/stomp_spec.rb index f67189cf5..99c77d0b4 100755 --- a/spec/unit/util/queue/stomp_spec.rb +++ b/spec/unit/util/queue/stomp_spec.rb @@ -63,26 +63,26 @@ describe 'Puppet::Util::Queue::Stomp', :if => Puppet.features.stomp? do end end - describe "when sending a message" do + describe "when publishing a message" do before do @client = stub 'client' Stomp::Client.stubs(:new).returns @client @queue = Puppet::Util::Queue::Stomp.new end - it "should send it to the queue client instance" do - @client.expects(:send).with { |queue, msg, options| msg == "Smite!" } - @queue.send_message('fooqueue', 'Smite!') + it "should publish it to the queue client instance" do + @client.expects(:publish).with { |queue, msg, options| msg == "Smite!" } + @queue.publish_message('fooqueue', 'Smite!') end - it "should send it to the transformed queue name" do - @client.expects(:send).with { |queue, msg, options| queue == "/queue/fooqueue" } - @queue.send_message('fooqueue', 'Smite!') + it "should publish it to the transformed queue name" do + @client.expects(:publish).with { |queue, msg, options| queue == "/queue/fooqueue" } + @queue.publish_message('fooqueue', 'Smite!') end - it "should send it as a persistent message" do - @client.expects(:send).with { |queue, msg, options| options[:persistent] == true } - @queue.send_message('fooqueue', 'Smite!') + it "should publish it as a persistent message" do + @client.expects(:publish).with { |queue, msg, options| options[:persistent] == true } + @queue.publish_message('fooqueue', 'Smite!') end end diff --git a/spec/watchr.rb b/spec/watchr.rb index 26919d1a1..c0f1d0257 100755 --- a/spec/watchr.rb +++ b/spec/watchr.rb @@ -85,7 +85,11 @@ def run_spec_files(files) else opts = File.readlines('spec/spec.opts').collect { |l| l.chomp }.join(" ") end - run_spec("rspec #{opts} --tty #{files.join(' ')}") + begin + run_spec("rspec #{opts} --tty #{files.join(' ')}") + rescue => detail + puts "Failed to load #{files}: #{detail}" + end end def run_all_tests diff --git a/test/lib/puppettest/railstesting.rb b/test/lib/puppettest/railstesting.rb index e05511e3b..f5666f2c4 100644 --- a/test/lib/puppettest/railstesting.rb +++ b/test/lib/puppettest/railstesting.rb @@ -14,39 +14,5 @@ module PuppetTest::RailsTesting def railsinit Puppet::Rails.init end - - def railsteardown - Puppet::Rails.teardown if Puppet[:dbadapter] != "sqlite3" - end - - def railsresource(type = "file", title = "/tmp/testing", params = {}) - railsteardown - railsinit - - # We need a host for resources - #host = Puppet::Rails::Host.new(:name => Facter.value("hostname")) - - # Now build a resource - resources = [] - - resources << mkresource( - :type => type, :title => title, :exported => true, - - :parameters => params) - - # Now collect our facts - facts = Facter.to_hash - - # Now try storing our crap - host = nil - node = mknode(facts["hostname"]) - node.parameters = facts - assert_nothing_raised { - host = Puppet::Rails::Host.store(node, resources) - } - - # Now save the whole thing - host.save - end end |
