summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--README.strings115
-rw-r--r--lib/puppet/application/catalog.rb4
-rw-r--r--lib/puppet/application/certificate.rb18
-rw-r--r--lib/puppet/application/certificate_request.rb4
-rw-r--r--lib/puppet/application/certificate_revocation_list.rb4
-rw-r--r--lib/puppet/application/config.rb4
-rw-r--r--lib/puppet/application/configurer.rb23
-rw-r--r--lib/puppet/application/faces.rb95
-rw-r--r--lib/puppet/application/faces_base.rb150
-rw-r--r--lib/puppet/application/facts.rb4
-rw-r--r--lib/puppet/application/file.rb4
-rw-r--r--lib/puppet/application/indirection_base.rb4
-rw-r--r--lib/puppet/application/key.rb4
-rw-r--r--lib/puppet/application/node.rb4
-rw-r--r--lib/puppet/application/report.rb4
-rw-r--r--lib/puppet/application/resource_type.rb4
-rw-r--r--lib/puppet/application/status.rb4
-rw-r--r--lib/puppet/faces.rb12
-rw-r--r--lib/puppet/faces/catalog.rb40
-rw-r--r--lib/puppet/faces/catalog/select.rb10
-rw-r--r--lib/puppet/faces/certificate.rb46
-rw-r--r--lib/puppet/faces/certificate_request.rb4
-rw-r--r--lib/puppet/faces/certificate_revocation_list.rb4
-rw-r--r--lib/puppet/faces/config.rb12
-rw-r--r--lib/puppet/faces/configurer.rb12
-rw-r--r--lib/puppet/faces/facts.rb18
-rw-r--r--lib/puppet/faces/file.rb5
-rw-r--r--lib/puppet/faces/indirector.rb94
-rw-r--r--lib/puppet/faces/key.rb4
-rw-r--r--lib/puppet/faces/node.rb5
-rw-r--r--lib/puppet/faces/report.rb15
-rw-r--r--lib/puppet/faces/resource.rb4
-rw-r--r--lib/puppet/faces/resource_type.rb4
-rw-r--r--lib/puppet/faces/status.rb4
-rw-r--r--lib/puppet/interface.rb115
-rw-r--r--lib/puppet/interface/action.rb119
-rw-r--r--lib/puppet/interface/action_builder.rb31
-rw-r--r--lib/puppet/interface/action_manager.rb49
-rw-r--r--lib/puppet/interface/face_collection.rb125
-rw-r--r--lib/puppet/interface/option.rb82
-rw-r--r--lib/puppet/interface/option_builder.rb25
-rw-r--r--lib/puppet/interface/option_manager.rb56
-rw-r--r--spec/lib/puppet/faces/huzzah.rb4
-rw-r--r--spec/shared_behaviours/things_that_declare_options.rb134
-rw-r--r--spec/spec_helper.rb3
-rwxr-xr-xspec/unit/application/certificate_spec.rb17
-rwxr-xr-xspec/unit/application/config_spec.rb10
-rwxr-xr-xspec/unit/application/configurer_spec.rb31
-rwxr-xr-xspec/unit/application/faces_base_spec.rb185
-rwxr-xr-xspec/unit/application/faces_spec.rb10
-rwxr-xr-xspec/unit/application/indirection_base_spec.rb39
-rwxr-xr-xspec/unit/faces/catalog_spec.rb4
-rwxr-xr-xspec/unit/faces/certificate_request_spec.rb3
-rwxr-xr-xspec/unit/faces/certificate_revocation_list_spec.rb3
-rwxr-xr-xspec/unit/faces/certificate_spec.rb14
-rwxr-xr-xspec/unit/faces/config_spec.rb24
-rwxr-xr-xspec/unit/faces/configurer_spec.rb24
-rwxr-xr-xspec/unit/faces/facts_spec.rb21
-rwxr-xr-xspec/unit/faces/file_spec.rb3
-rwxr-xr-xspec/unit/faces/indirector_spec.rb60
-rwxr-xr-xspec/unit/faces/key_spec.rb3
-rwxr-xr-xspec/unit/faces/node_spec.rb9
-rwxr-xr-xspec/unit/faces/report_spec.rb3
-rwxr-xr-xspec/unit/faces/resource_spec.rb3
-rwxr-xr-xspec/unit/faces/resource_type_spec.rb3
-rwxr-xr-xspec/unit/interface/action_builder_spec.rb59
-rwxr-xr-xspec/unit/interface/action_manager_spec.rb233
-rwxr-xr-xspec/unit/interface/action_spec.rb173
-rwxr-xr-xspec/unit/interface/face_collection_spec.rb175
-rw-r--r--spec/unit/interface/option_builder_spec.rb29
-rw-r--r--spec/unit/interface/option_spec.rb75
-rwxr-xr-xspec/unit/interface_spec.rb147
-rw-r--r--spec/unit/puppet/provider/README.markdown4
-rw-r--r--spec/unit/puppet/type/README.markdown4
74 files changed, 2853 insertions, 0 deletions
diff --git a/README.strings b/README.strings
new file mode 100644
index 000000000..28289ee10
--- /dev/null
+++ b/README.strings
@@ -0,0 +1,115 @@
+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/lib/puppet/application/catalog.rb b/lib/puppet/application/catalog.rb
new file mode 100644
index 000000000..10ce05be7
--- /dev/null
+++ b/lib/puppet/application/catalog.rb
@@ -0,0 +1,4 @@
+require 'puppet/application/indirection_base'
+
+class Puppet::Application::Catalog < Puppet::Application::IndirectionBase
+end
diff --git a/lib/puppet/application/certificate.rb b/lib/puppet/application/certificate.rb
new file mode 100644
index 000000000..eacb830b2
--- /dev/null
+++ b/lib/puppet/application/certificate.rb
@@ -0,0 +1,18 @@
+require 'puppet/application/indirection_base'
+
+class Puppet::Application::Certificate < Puppet::Application::IndirectionBase
+ def setup
+ unless options[:ca_location]
+ raise ArgumentError, "You must have a CA location specified;\n" +
+ "use --ca-location to specify the location (remote, local, only)"
+ end
+
+ location = Puppet::SSL::Host.ca_location
+ if location == :local && !Puppet::SSL::CertificateAuthority.ca?
+ self.class.run_mode("master")
+ self.set_run_mode self.class.run_mode
+ end
+
+ super
+ end
+end
diff --git a/lib/puppet/application/certificate_request.rb b/lib/puppet/application/certificate_request.rb
new file mode 100644
index 000000000..1b1b0830c
--- /dev/null
+++ b/lib/puppet/application/certificate_request.rb
@@ -0,0 +1,4 @@
+require 'puppet/application/indirection_base'
+
+class Puppet::Application::Certificate_request < Puppet::Application::IndirectionBase
+end
diff --git a/lib/puppet/application/certificate_revocation_list.rb b/lib/puppet/application/certificate_revocation_list.rb
new file mode 100644
index 000000000..60b9d97d6
--- /dev/null
+++ b/lib/puppet/application/certificate_revocation_list.rb
@@ -0,0 +1,4 @@
+require 'puppet/application/indirection_base'
+
+class Puppet::Application::Certificate_revocation_list < Puppet::Application::IndirectionBase
+end
diff --git a/lib/puppet/application/config.rb b/lib/puppet/application/config.rb
new file mode 100644
index 000000000..41a46c339
--- /dev/null
+++ b/lib/puppet/application/config.rb
@@ -0,0 +1,4 @@
+require 'puppet/application/faces_base'
+
+class Puppet::Application::Config < Puppet::Application::FacesBase
+end
diff --git a/lib/puppet/application/configurer.rb b/lib/puppet/application/configurer.rb
new file mode 100644
index 000000000..751e6b4d7
--- /dev/null
+++ b/lib/puppet/application/configurer.rb
@@ -0,0 +1,23 @@
+require 'puppet/application'
+require 'puppet/faces'
+
+class Puppet::Application::Configurer < Puppet::Application
+ should_parse_config
+ run_mode :agent
+
+ option("--debug", "-d")
+ option("--verbose", "-v")
+
+ def setup
+ if options[:debug] or options[:verbose]
+ Puppet::Util::Log.level = options[:debug] ? :debug : :info
+ end
+
+ Puppet::Util::Log.newdestination(:console)
+ end
+
+ def run_command
+ report = Puppet::Faces[:configurer, '0.0.1'].synchronize(Puppet[:certname])
+ Puppet::Faces[:report, '0.0.1'].submit(report)
+ end
+end
diff --git a/lib/puppet/application/faces.rb b/lib/puppet/application/faces.rb
new file mode 100644
index 000000000..904a0cccc
--- /dev/null
+++ b/lib/puppet/application/faces.rb
@@ -0,0 +1,95 @@
+require 'puppet/application'
+require 'puppet/faces'
+
+class Puppet::Application::Faces < Puppet::Application
+
+ should_parse_config
+ run_mode :agent
+
+ option("--debug", "-d") do |arg|
+ Puppet::Util::Log.level = :debug
+ end
+
+ option("--verbose", "-v") do
+ Puppet::Util::Log.level = :info
+ end
+
+ def list(*arguments)
+ if arguments.empty?
+ arguments = %w{terminuses actions}
+ end
+ faces.each do |name|
+ str = "#{name}:\n"
+ if arguments.include?("terminuses")
+ begin
+ terms = terminus_classes(name.to_sym)
+ str << "\tTerminuses: #{terms.join(", ")}\n"
+ rescue => detail
+ puts detail.backtrace if Puppet[:trace]
+ $stderr.puts "Could not load terminuses for #{name}: #{detail}"
+ end
+ end
+
+ if arguments.include?("actions")
+ begin
+ actions = actions(name.to_sym)
+ str << "\tActions: #{actions.join(", ")}\n"
+ rescue => detail
+ puts detail.backtrace if Puppet[:trace]
+ $stderr.puts "Could not load actions for #{name}: #{detail}"
+ end
+ end
+
+ print str
+ end
+ end
+
+ attr_accessor :verb, :name, :arguments
+
+ def main
+ # Call the method associated with the provided action (e.g., 'find').
+ send(verb, *arguments)
+ end
+
+ def setup
+ Puppet::Util::Log.newdestination :console
+
+ load_applications # Call this to load all of the apps
+
+ @verb, @arguments = command_line.args
+ @arguments ||= []
+
+ validate
+ end
+
+ def validate
+ unless verb
+ raise "You must specify 'find', 'search', 'save', or 'destroy' as a verb; 'save' probably does not work right now"
+ end
+
+ unless respond_to?(verb)
+ raise "Command '#{verb}' not found for 'faces'"
+ end
+ end
+
+ def faces
+ Puppet::Faces.faces
+ end
+
+ def terminus_classes(indirection)
+ Puppet::Indirector::Terminus.terminus_classes(indirection).collect { |t| t.to_s }.sort
+ end
+
+ def actions(indirection)
+ return [] unless faces = Puppet::Faces[indirection, '0.0.1']
+ faces.load_actions
+ return faces.actions.sort { |a, b| a.to_s <=> b.to_s }
+ end
+
+ def load_applications
+ command_line.available_subcommands.each do |app|
+ command_line.require_application app
+ end
+ end
+end
+
diff --git a/lib/puppet/application/faces_base.rb b/lib/puppet/application/faces_base.rb
new file mode 100644
index 000000000..288b50048
--- /dev/null
+++ b/lib/puppet/application/faces_base.rb
@@ -0,0 +1,150 @@
+require 'puppet/application'
+require 'puppet/faces'
+
+class Puppet::Application::FacesBase < Puppet::Application
+ should_parse_config
+ run_mode :agent
+
+ option("--debug", "-d") do |arg|
+ Puppet::Util::Log.level = :debug
+ end
+
+ option("--verbose", "-v") do
+ Puppet::Util::Log.level = :info
+ end
+
+ option("--format FORMAT") do |arg|
+ @format = arg.to_sym
+ end
+
+ option("--mode RUNMODE", "-r") do |arg|
+ raise "Invalid run mode #{arg}; supported modes are user, agent, master" unless %w{user agent master}.include?(arg)
+ self.class.run_mode(arg.to_sym)
+ set_run_mode self.class.run_mode
+ end
+
+
+ attr_accessor :face, :action, :type, :arguments, :format
+ attr_writer :exit_code
+
+ # This allows you to set the exit code if you don't want to just exit
+ # immediately but you need to indicate a failure.
+ def exit_code
+ @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)
+ else
+ result.send(render_method)
+ end
+ end
+
+ def preinit
+ super
+ Signal.trap(:INT) do
+ $stderr.puts "Cancelling Face"
+ exit(0)
+ end
+
+ # We need to parse enough of the command line out early, to identify what
+ # the action is, so that we can obtain the full set of options to parse.
+
+ # TODO: 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::Faces[@type, :current]
+ @format = @face.default_format
+
+ # Now, walk the command line and identify the action. We skip over
+ # arguments based on introspecting the action and all, and find the first
+ # non-option word to use as the action.
+ action = nil
+ index = -1
+ until @action or (index += 1) >= command_line.args.length do
+ item = command_line.args[index]
+ if item =~ /^-/ then
+ option = @face.options.find do |name|
+ item =~ /^-+#{name.to_s.gsub(/[-_]/, '[-_]')}(?:[ =].*)?$/
+ end
+ if option then
+ option = @face.get_option(option)
+ # If we have an inline argument, just carry on. We don't need to
+ # care about optional vs mandatory in that case because we do a real
+ # parse later, and that will totally take care of raising the error
+ # when we get there. --daniel 2011-04-04
+ if option.takes_argument? and !item.index('=') then
+ index += 1 unless
+ (option.optional_argument? and command_line.args[index + 1] =~ /^-/)
+ end
+ elsif option = find_global_settings_argument(item) then
+ unless Puppet.settings.boolean? option.name then
+ # As far as I can tell, we treat non-bool options as always having
+ # a mandatory argument. --daniel 2011-04-05
+ index += 1 # ...so skip the argument.
+ end
+ else
+ raise ArgumentError, "Unknown option #{item.sub(/=.*$/, '').inspect}"
+ end
+ else
+ action = @face.get_action(item.to_sym)
+ if action.nil? then
+ raise ArgumentError, "#{@face} does not have an #{item.inspect} action!"
+ end
+ @action = action
+ end
+ end
+
+ @action or raise ArgumentError, "No action given on the command line!"
+
+ # Finally, we can interact with the default option code to build behaviour
+ # around the full set of options we now know we support.
+ @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
+
+ def find_global_settings_argument(item)
+ Puppet.settings.each do |name, object|
+ object.optparse_args.each do |arg|
+ next unless arg =~ /^-/
+ # sadly, we have to emulate some of optparse here...
+ pattern = /^#{arg.sub('[no-]', '').sub(/[ =].*$/, '')}(?:[ =].*)?$/
+ pattern.match item and return object
+ end
+ end
+ return nil # nothing found.
+ end
+
+ def setup
+ Puppet::Util::Log.newdestination :console
+
+ @arguments = command_line.args
+
+ # Note: because of our definition of where the action is set, we end up
+ # 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)
+
+ # 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
+ # would invoke the action with options set as global state in the
+ # interface object. --daniel 2011-03-28
+ @arguments << options
+ end
+
+
+ def main
+ # Call the method associated with the provided action (e.g., 'find').
+ if result = @face.send(@action.name, *arguments)
+ puts render(result)
+ end
+ exit(exit_code)
+ end
+end
diff --git a/lib/puppet/application/facts.rb b/lib/puppet/application/facts.rb
new file mode 100644
index 000000000..d18b21ea7
--- /dev/null
+++ b/lib/puppet/application/facts.rb
@@ -0,0 +1,4 @@
+require 'puppet/application/indirection_base'
+
+class Puppet::Application::Facts < Puppet::Application::IndirectionBase
+end
diff --git a/lib/puppet/application/file.rb b/lib/puppet/application/file.rb
new file mode 100644
index 000000000..32a81c7c6
--- /dev/null
+++ b/lib/puppet/application/file.rb
@@ -0,0 +1,4 @@
+require 'puppet/application/indirection_base'
+
+class Puppet::Application::File < Puppet::Application::IndirectionBase
+end
diff --git a/lib/puppet/application/indirection_base.rb b/lib/puppet/application/indirection_base.rb
new file mode 100644
index 000000000..7455ebedf
--- /dev/null
+++ b/lib/puppet/application/indirection_base.rb
@@ -0,0 +1,4 @@
+require 'puppet/application/faces_base'
+
+class Puppet::Application::IndirectionBase < Puppet::Application::FacesBase
+end
diff --git a/lib/puppet/application/key.rb b/lib/puppet/application/key.rb
new file mode 100644
index 000000000..57835b627
--- /dev/null
+++ b/lib/puppet/application/key.rb
@@ -0,0 +1,4 @@
+require 'puppet/application/indirection_base'
+
+class Puppet::Application::Key < Puppet::Application::IndirectionBase
+end
diff --git a/lib/puppet/application/node.rb b/lib/puppet/application/node.rb
new file mode 100644
index 000000000..38c1f8610
--- /dev/null
+++ b/lib/puppet/application/node.rb
@@ -0,0 +1,4 @@
+require 'puppet/application/indirection_base'
+
+class Puppet::Application::Node < Puppet::Application::IndirectionBase
+end
diff --git a/lib/puppet/application/report.rb b/lib/puppet/application/report.rb
new file mode 100644
index 000000000..f7f961edd
--- /dev/null
+++ b/lib/puppet/application/report.rb
@@ -0,0 +1,4 @@
+require 'puppet/application/indirection_base'
+
+class Puppet::Application::Report < Puppet::Application::IndirectionBase
+end
diff --git a/lib/puppet/application/resource_type.rb b/lib/puppet/application/resource_type.rb
new file mode 100644
index 000000000..59594262c
--- /dev/null
+++ b/lib/puppet/application/resource_type.rb
@@ -0,0 +1,4 @@
+require 'puppet/application/indirection_base'
+
+class Puppet::Application::Resource_type < Puppet::Application::IndirectionBase
+end
diff --git a/lib/puppet/application/status.rb b/lib/puppet/application/status.rb
new file mode 100644
index 000000000..1c3ca054e
--- /dev/null
+++ b/lib/puppet/application/status.rb
@@ -0,0 +1,4 @@
+require 'puppet/application/indirection_base'
+
+class Puppet::Application::Status < Puppet::Application::IndirectionBase
+end
diff --git a/lib/puppet/faces.rb b/lib/puppet/faces.rb
new file mode 100644
index 000000000..947eecf24
--- /dev/null
+++ b/lib/puppet/faces.rb
@@ -0,0 +1,12 @@
+# The public name of this feature is 'faces', but we have hidden all the
+# plumbing over in the 'interfaces' namespace to make clear the distinction
+# between the two.
+#
+# This file exists to ensure that the public name is usable without revealing
+# the details of the implementation; you really only need go look at anything
+# under Interfaces if you are looking to extend the implementation.
+#
+# It isn't hidden to gratuitously hide things, just to make it easier to
+# separate out the interests people will have. --daniel 2011-04-07
+require 'puppet/interface'
+Puppet::Faces = Puppet::Interface
diff --git a/lib/puppet/faces/catalog.rb b/lib/puppet/faces/catalog.rb
new file mode 100644
index 000000000..3353d5d04
--- /dev/null
+++ b/lib/puppet/faces/catalog.rb
@@ -0,0 +1,40 @@
+require 'puppet/faces/indirector'
+
+Puppet::Faces::Indirector.define(:catalog, '0.0.1') do
+ action(:apply) do
+ when_invoked do |catalog, options|
+ report = Puppet::Transaction::Report.new("apply")
+ report.configuration_version = catalog.version
+
+ Puppet::Util::Log.newdestination(report)
+
+ begin
+ benchmark(:notice, "Finished catalog run") do
+ catalog.apply(:report => report)
+ end
+ rescue => detail
+ puts detail.backtrace if Puppet[:trace]
+ Puppet.err "Failed to apply catalog: #{detail}"
+ end
+
+ report.finalize_report
+ report
+ end
+ end
+
+ action(:download) do
+ when_invoked do |certname, facts, options|
+ Puppet::Resource::Catalog.indirection.terminus_class = :rest
+ facts_to_upload = {:facts_format => :b64_zlib_yaml, :facts => CGI.escape(facts.render(:b64_zlib_yaml))}
+ catalog = nil
+ retrieval_duration = thinmark do
+ catalog = Puppet::Faces[:catalog, '0.0.1'].find(certname, facts_to_upload)
+ end
+ catalog = catalog.to_ral
+ catalog.finalize
+ catalog.retrieval_duration = retrieval_duration
+ catalog.write_class_file
+ catalog
+ end
+ end
+end
diff --git a/lib/puppet/faces/catalog/select.rb b/lib/puppet/faces/catalog/select.rb
new file mode 100644
index 000000000..e29d19970
--- /dev/null
+++ b/lib/puppet/faces/catalog/select.rb
@@ -0,0 +1,10 @@
+# Select and show a list of resources of a given type.
+Puppet::Faces.define(:catalog, '0.0.1') do
+ action :select do
+ when_invoked do |host, type, options|
+ catalog = Puppet::Resource::Catalog.indirection.find(host)
+
+ catalog.resources.reject { |res| res.type != type }.each { |res| puts res }
+ end
+ end
+end
diff --git a/lib/puppet/faces/certificate.rb b/lib/puppet/faces/certificate.rb
new file mode 100644
index 000000000..b10bee579
--- /dev/null
+++ b/lib/puppet/faces/certificate.rb
@@ -0,0 +1,46 @@
+require 'puppet/faces/indirector'
+require 'puppet/ssl/host'
+
+Puppet::Faces::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"
+
+ action :generate do
+ 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)
+ end
+ end
+
+ action :list do
+ 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 }
+ end
+ end
+
+ action :sign do
+ 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)
+ end
+ end
+end
diff --git a/lib/puppet/faces/certificate_request.rb b/lib/puppet/faces/certificate_request.rb
new file mode 100644
index 000000000..5e91bdb7f
--- /dev/null
+++ b/lib/puppet/faces/certificate_request.rb
@@ -0,0 +1,4 @@
+require 'puppet/faces/indirector'
+
+Puppet::Faces::Indirector.define(:certificate_request, '0.0.1') do
+end
diff --git a/lib/puppet/faces/certificate_revocation_list.rb b/lib/puppet/faces/certificate_revocation_list.rb
new file mode 100644
index 000000000..2f2d72874
--- /dev/null
+++ b/lib/puppet/faces/certificate_revocation_list.rb
@@ -0,0 +1,4 @@
+require 'puppet/faces/indirector'
+
+Puppet::Faces::Indirector.define(:certificate_revocation_list, '0.0.1') do
+end
diff --git a/lib/puppet/faces/config.rb b/lib/puppet/faces/config.rb
new file mode 100644
index 000000000..647bf5052
--- /dev/null
+++ b/lib/puppet/faces/config.rb
@@ -0,0 +1,12 @@
+require 'puppet/faces'
+
+Puppet::Faces.define(:config, '0.0.1') do
+ action(:print) do
+ when_invoked do |*args|
+ options = args.pop
+ Puppet.settings[:configprint] = args.join(",")
+ Puppet.settings.print_config_options
+ nil
+ end
+ end
+end
diff --git a/lib/puppet/faces/configurer.rb b/lib/puppet/faces/configurer.rb
new file mode 100644
index 000000000..d40987697
--- /dev/null
+++ b/lib/puppet/faces/configurer.rb
@@ -0,0 +1,12 @@
+require 'puppet/faces'
+
+Puppet::Faces.define(:configurer, '0.0.1') do
+ action(:synchronize) do
+ when_invoked do |certname, options|
+ facts = Puppet::Faces[:facts, '0.0.1'].find(certname)
+ catalog = Puppet::Faces[:catalog, '0.0.1'].download(certname, facts)
+ report = Puppet::Faces[:catalog, '0.0.1'].apply(catalog)
+ report
+ end
+ end
+end
diff --git a/lib/puppet/faces/facts.rb b/lib/puppet/faces/facts.rb
new file mode 100644
index 000000000..33eacef38
--- /dev/null
+++ b/lib/puppet/faces/facts.rb
@@ -0,0 +1,18 @@
+require 'puppet/faces/indirector'
+require 'puppet/node/facts'
+
+Puppet::Faces::Indirector.define(:facts, '0.0.1') do
+ set_default_format :yaml
+
+ # Upload our facts to the server
+ action(:upload) do
+ when_invoked do |options|
+ Puppet::Node::Facts.indirection.terminus_class = :facter
+ facts = Puppet::Node::Facts.indirection.find(Puppet[:certname])
+ Puppet::Node::Facts.indirection.terminus_class = :rest
+ Puppet::Node::Facts.indirection.save(facts)
+ Puppet.notice "Uploaded facts for '#{Puppet[:certname]}'"
+ nil
+ end
+ end
+end
diff --git a/lib/puppet/faces/file.rb b/lib/puppet/faces/file.rb
new file mode 100644
index 000000000..e8ad18c17
--- /dev/null
+++ b/lib/puppet/faces/file.rb
@@ -0,0 +1,5 @@
+require 'puppet/faces/indirector'
+
+Puppet::Faces::Indirector.define(:file, '0.0.1') do
+ set_indirection_name :file_bucket_file
+end
diff --git a/lib/puppet/faces/indirector.rb b/lib/puppet/faces/indirector.rb
new file mode 100644
index 000000000..7e4e0f00f
--- /dev/null
+++ b/lib/puppet/faces/indirector.rb
@@ -0,0 +1,94 @@
+require 'puppet'
+require 'puppet/faces'
+
+class Puppet::Faces::Indirector < Puppet::Faces
+ option "--terminus TERMINUS" do
+ desc "REVISIT: You can select a terminus, which has some bigger effect
+that we should describe in this file somehow."
+ end
+
+ def self.indirections
+ Puppet::Indirector::Indirection.instances.collect { |t| t.to_s }.sort
+ end
+
+ def self.terminus_classes(indirection)
+ Puppet::Indirector::Terminus.terminus_classes(indirection.to_sym).collect { |t| t.to_s }.sort
+ end
+
+ def call_indirection_method(method, *args)
+ options = args.last
+ options.has_key?(:terminus) and set_terminus(options[:terminus])
+
+ begin
+ result = indirection.__send__(method, *args)
+ rescue => detail
+ puts detail.backtrace if Puppet[:trace]
+ raise "Could not call '#{method}' on '#{indirection_name}': #{detail}"
+ end
+
+ indirection.reset_terminus_class
+ return result
+ end
+
+ action :destroy do
+ when_invoked { |*args| call_indirection_method(:destroy, *args) }
+ end
+
+ action :find do
+ when_invoked { |*args| call_indirection_method(:find, *args) }
+ end
+
+ action :save do
+ when_invoked { |*args| call_indirection_method(:save, *args) }
+ end
+
+ action :search do
+ when_invoked { |*args| call_indirection_method(:search, *args) }
+ end
+
+ # 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
+
+ attr_accessor :from
+
+ def indirection_name
+ @indirection_name || name.to_sym
+ end
+
+ # Here's your opportunity to override the indirection name. By default it
+ # will be the same name as the face.
+ def set_indirection_name(name)
+ @indirection_name = name
+ end
+
+ # Return an indirection associated with a face, if one exists;
+ # One usually does.
+ def indirection
+ unless @indirection
+ @indirection = Puppet::Indirector::Indirection.instance(indirection_name)
+ @indirection or raise "Could not find terminus for #{indirection_name}"
+ end
+ @indirection
+ end
+
+ def set_terminus(from)
+ begin
+ indirection.terminus_class = from
+ rescue => detail
+ raise "Could not set '#{indirection.name}' terminus to '#{from}' (#{detail}); valid terminus types are #{self.class.terminus_classes(indirection.name).join(", ") }"
+ end
+ end
+end
diff --git a/lib/puppet/faces/key.rb b/lib/puppet/faces/key.rb
new file mode 100644
index 000000000..7b6ad52ac
--- /dev/null
+++ b/lib/puppet/faces/key.rb
@@ -0,0 +1,4 @@
+require 'puppet/faces/indirector'
+
+Puppet::Faces::Indirector.define(:key, '0.0.1') do
+end
diff --git a/lib/puppet/faces/node.rb b/lib/puppet/faces/node.rb
new file mode 100644
index 000000000..7eed0df91
--- /dev/null
+++ b/lib/puppet/faces/node.rb
@@ -0,0 +1,5 @@
+require 'puppet/faces/indirector'
+
+Puppet::Faces::Indirector.define(:node, '0.0.1') do
+ set_default_format :yaml
+end
diff --git a/lib/puppet/faces/report.rb b/lib/puppet/faces/report.rb
new file mode 100644
index 000000000..23a518981
--- /dev/null
+++ b/lib/puppet/faces/report.rb
@@ -0,0 +1,15 @@
+require 'puppet/faces/indirector'
+
+Puppet::Faces::Indirector.define(:report, '0.0.1') do
+ action(:submit) do
+ when_invoked do |report, options|
+ begin
+ Puppet::Transaction::Report.terminus_class = :rest
+ report.save
+ rescue => detail
+ puts detail.backtrace if Puppet[:trace]
+ Puppet.err "Could not send report: #{detail}"
+ end
+ end
+ end
+end
diff --git a/lib/puppet/faces/resource.rb b/lib/puppet/faces/resource.rb
new file mode 100644
index 000000000..60b0d94db
--- /dev/null
+++ b/lib/puppet/faces/resource.rb
@@ -0,0 +1,4 @@
+require 'puppet/faces/indirector'
+
+Puppet::Faces::Indirector.define(:resource, '0.0.1') do
+end
diff --git a/lib/puppet/faces/resource_type.rb b/lib/puppet/faces/resource_type.rb
new file mode 100644
index 000000000..4321d65e7
--- /dev/null
+++ b/lib/puppet/faces/resource_type.rb
@@ -0,0 +1,4 @@
+require 'puppet/faces/indirector'
+
+Puppet::Faces::Indirector.define(:resource_type, '0.0.1') do
+end
diff --git a/lib/puppet/faces/status.rb b/lib/puppet/faces/status.rb
new file mode 100644
index 000000000..e035f281f
--- /dev/null
+++ b/lib/puppet/faces/status.rb
@@ -0,0 +1,4 @@
+require 'puppet/faces/indirector'
+
+Puppet::Faces::Indirector.define(:status, '0.0.1') do
+end
diff --git a/lib/puppet/interface.rb b/lib/puppet/interface.rb
new file mode 100644
index 000000000..07e27efa8
--- /dev/null
+++ b/lib/puppet/interface.rb
@@ -0,0 +1,115 @@
+require 'puppet'
+require 'puppet/util/autoload'
+
+class Puppet::Interface
+ require 'puppet/interface/face_collection'
+
+ require 'puppet/interface/action_manager'
+ include Puppet::Interface::ActionManager
+ extend Puppet::Interface::ActionManager
+
+ require 'puppet/interface/option_manager'
+ include Puppet::Interface::OptionManager
+ extend Puppet::Interface::OptionManager
+
+ include Puppet::Util
+
+ class << self
+ # This is just so we can search for actions. We only use its
+ # list of directories to search.
+ # Can't we utilize an external autoloader, or simply use the $LOAD_PATH? -pvb
+ def autoloader
+ @autoloader ||= Puppet::Util::Autoload.new(:application, "puppet/faces")
+ end
+
+ def faces
+ Puppet::Interface::FaceCollection.faces
+ end
+
+ def face?(name, version)
+ Puppet::Interface::FaceCollection.face?(name, version)
+ end
+
+ def register(instance)
+ Puppet::Interface::FaceCollection.register(instance)
+ end
+
+ def define(name, version, &block)
+ if face?(name, version)
+ face = Puppet::Interface::FaceCollection[name, version]
+ else
+ face = self.new(name, version)
+ Puppet::Interface::FaceCollection.register(face)
+ # REVISIT: Shouldn't this be delayed until *after* we evaluate the
+ # current block, not done before? --daniel 2011-04-07
+ face.load_actions
+ end
+
+ face.instance_eval(&block) if block_given?
+
+ return face
+ end
+
+ def [](name, version)
+ unless face = Puppet::Interface::FaceCollection[name, version]
+ if current = Puppet::Interface::FaceCollection[name, :current]
+ raise Puppet::Error, "Could not find version #{version} of #{current}"
+ else
+ raise Puppet::Error, "Could not find Puppet Face #{name.inspect}"
+ end
+ end
+ face
+ end
+ end
+
+ attr_accessor :default_format
+
+ def set_default_format(format)
+ self.default_format = format.to_sym
+ end
+
+ attr_accessor :type, :verb, :version, :arguments
+ attr_reader :name
+
+ def initialize(name, version, &block)
+ unless Puppet::Interface::FaceCollection.validate_version(version)
+ raise ArgumentError, "Cannot create face #{name.inspect} with invalid version number '#{version}'!"
+ end
+
+ @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/faces/#{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
+ end
+ end
+ end
+
+ def to_s
+ "Puppet::Faces[#{name.inspect}, #{version.inspect}]"
+ end
+end
diff --git a/lib/puppet/interface/action.rb b/lib/puppet/interface/action.rb
new file mode 100644
index 000000000..e4a37a1f7
--- /dev/null
+++ b/lib/puppet/interface/action.rb
@@ -0,0 +1,119 @@
+# -*- coding: utf-8 -*-
+require 'puppet/interface'
+require 'puppet/interface/option'
+
+class Puppet::Interface::Action
+ def initialize(face, name, attrs = {})
+ 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
+ end
+
+ attr_reader :name
+ def to_s() "#{@face}##{@name}" 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
+ # the action object for invocation and all.
+ #
+ # It turns out that we have a binding problem to solve: @face was bound to
+ # the parent class, not the subclass instance, and we don't pass the
+ # appropriate context or change the binding enough to make this work.
+ #
+ # We could hack around it, by either mandating that you pass the context in
+ # to invoke, or try to get the binding right, but that has probably got
+ # subtleties that we don't instantly think of – especially around threads.
+ #
+ # So, we are pulling this method for now, and will return it to life when we
+ # have the time to resolve the problem. For now, you should replace...
+ #
+ # @action = @face.get_action(name)
+ # @action.invoke(arg1, arg2, arg3)
+ #
+ # ...with...
+ #
+ # @action = @face.get_action(name)
+ # @face.send(@action.name, arg1, arg2, arg3)
+ #
+ # I understand that is somewhat cumbersome, but it functions as desired.
+ # --daniel 2011-03-31
+ #
+ # PS: This code is left present, but commented, to support this chunk of
+ # documentation, for the benefit of the reader.
+ #
+ # def invoke(*args, &block)
+ # @face.send(name, *args, &block)
+ # end
+
+ def when_invoked=(block)
+ # We need to build an instance method as a wrapper, using normal code, to
+ # be able to expose argument defaulting between the caller and definer in
+ # the Ruby API. An extra method is, sadly, required for Ruby 1.8 to work.
+ #
+ # In future this also gives us a place to hook in additional behaviour
+ # such as calling out to the action instance to validate and coerce
+ # parameters, which avoids any exciting context switching and all.
+ #
+ # Hopefully we can improve this when we finally shuffle off the last of
+ # Ruby 1.8 support, but that looks to be a few "enterprise" release eras
+ # away, so we are pretty stuck with this for now.
+ #
+ # Patches to make this work more nicely with Ruby 1.9 using runtime
+ # version checking and all are welcome, but they can't actually help if
+ # the results are not totally hidden away in here.
+ #
+ # Incidentally, we though about vendoring evil-ruby and actually adjusting
+ # the internal C structure implementation details under the hood to make
+ # this stuff work, because it would have been cleaner. Which gives you an
+ # 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"
+
+ if @face.is_a?(Class)
+ @face.class_eval do eval wrapper, nil, file, line end
+ @face.define_method(internal_name, &block)
+ else
+ @face.instance_eval do eval wrapper, nil, file, line end
+ @face.meta_def(internal_name, &block)
+ end
+ end
+
+ def add_option(option)
+ option.aliases.each do |name|
+ if conflict = get_option(name) then
+ raise ArgumentError, "Option #{option} conflicts with existing option #{conflict}"
+ elsif conflict = @face.get_option(name) then
+ raise ArgumentError, "Option #{option} conflicts with existing option #{conflict} on #{@face}"
+ end
+ end
+
+ option.aliases.each do |name|
+ @options[name] = option
+ end
+
+ option
+ end
+
+ def option?(name)
+ @options.include? name.to_sym
+ end
+
+ def options
+ (@options.keys + @face.options).sort
+ end
+
+ def get_option(name)
+ @options[name.to_sym] || @face.get_option(name)
+ end
+end
diff --git a/lib/puppet/interface/action_builder.rb b/lib/puppet/interface/action_builder.rb
new file mode 100644
index 000000000..b08c3d023
--- /dev/null
+++ b/lib/puppet/interface/action_builder.rb
@@ -0,0 +1,31 @@
+require 'puppet/interface'
+require 'puppet/interface/action'
+
+class Puppet::Interface::ActionBuilder
+ attr_reader :action
+
+ def self.build(face, name, &block)
+ raise "Action #{name.inspect} must specify a block" unless block
+ new(face, name, &block).action
+ end
+
+ private
+ def initialize(face, name, &block)
+ @face = face
+ @action = Puppet::Interface::Action.new(face, name)
+ instance_eval(&block)
+ end
+
+ # Ideally the method we're defining here would be added to the action, and a
+ # method on the face would defer to it, but we can't get scope correct, so
+ # we stick with this. --daniel 2011-03-24
+ def when_invoked(&block)
+ raise "when_invoked on an ActionBuilder with no corresponding Action" unless @action
+ @action.when_invoked = block
+ end
+
+ def option(*declaration, &block)
+ option = Puppet::Interface::OptionBuilder.build(@action, *declaration, &block)
+ @action.add_option(option)
+ end
+end
diff --git a/lib/puppet/interface/action_manager.rb b/lib/puppet/interface/action_manager.rb
new file mode 100644
index 000000000..bb0e5bf57
--- /dev/null
+++ b/lib/puppet/interface/action_manager.rb
@@ -0,0 +1,49 @@
+require 'puppet/interface/action_builder'
+
+module Puppet::Interface::ActionManager
+ # Declare that this app can take a specific action, and provide
+ # the code to do so.
+ def action(name, &block)
+ @actions ||= {}
+ raise "Action #{name} already defined for #{self}" if action?(name)
+ action = Puppet::Interface::ActionBuilder.build(self, name, &block)
+ @actions[action.name] = action
+ end
+
+ # This is the short-form of an action definition; it doesn't use the
+ # builder, just creates the action directly from the block.
+ def script(name, &block)
+ @actions ||= {}
+ raise "Action #{name} already defined for #{self}" if action?(name)
+ @actions[name] = Puppet::Interface::Action.new(self, name, :when_invoked => block)
+ end
+
+ def actions
+ @actions ||= {}
+ result = @actions.keys
+
+ if self.is_a?(Class) and superclass.respond_to?(:actions)
+ result += superclass.actions
+ elsif self.class.respond_to?(:actions)
+ result += self.class.actions
+ end
+ result.sort
+ end
+
+ def get_action(name)
+ @actions ||= {}
+ result = @actions[name.to_sym]
+ if result.nil?
+ if self.is_a?(Class) and superclass.respond_to?(:get_action)
+ result = superclass.get_action(name)
+ elsif self.class.respond_to?(:get_action)
+ result = self.class.get_action(name)
+ end
+ end
+ return result
+ end
+
+ def action?(name)
+ actions.include?(name.to_sym)
+ end
+end
diff --git a/lib/puppet/interface/face_collection.rb b/lib/puppet/interface/face_collection.rb
new file mode 100644
index 000000000..9f7a499c2
--- /dev/null
+++ b/lib/puppet/interface/face_collection.rb
@@ -0,0 +1,125 @@
+# -*- coding: utf-8 -*-
+require 'puppet/interface'
+
+module Puppet::Interface::FaceCollection
+ SEMVER_VERSION = /^(\d+)\.(\d+)\.(\d+)([A-Za-z][0-9A-Za-z-]*|)$/
+
+ @faces = Hash.new { |hash, key| hash[key] = {} }
+
+ def self.faces
+ unless @loaded
+ @loaded = true
+ $LOAD_PATH.each do |dir|
+ next unless FileTest.directory?(dir)
+ Dir.chdir(dir) do
+ # REVISIT: This is wrong!!!! We don't name files like that ever,
+ # so we should no longer match things like this. Damnit!!! --daniel 2011-04-07
+ Dir.glob("puppet/faces/v*/*.rb").collect { |f| f.sub(/\.rb/, '') }.each do |file|
+ iname = file.sub(/\.rb/, '')
+ begin
+ require iname
+ rescue Exception => detail
+ puts detail.backtrace if Puppet[:trace]
+ raise "Could not load #{iname} from #{dir}/#{file}: #{detail}"
+ end
+ end
+ end
+ end
+ end
+ return @faces.keys
+ end
+
+ def self.validate_version(version)
+ !!(SEMVER_VERSION =~ version.to_s)
+ end
+
+ def self.cmp_semver(a, b)
+ a, b = [a, b].map do |x|
+ parts = SEMVER_VERSION.match(x).to_a[1..4]
+ parts[0..2] = parts[0..2].map { |e| e.to_i }
+ parts
+ end
+
+ cmp = a[0..2] <=> b[0..2]
+ if cmp == 0
+ cmp = a[3] <=> b[3]
+ cmp = +1 if a[3].empty? && !b[3].empty?
+ cmp = -1 if b[3].empty? && !a[3].empty?
+ end
+ cmp
+ end
+
+ def self.[](name, version)
+ @faces[underscorize(name)][version] if face?(name, version)
+ end
+
+ def self.face?(name, version)
+ name = underscorize(name)
+ return true if @faces[name].has_key?(version)
+
+ # We always load the current version file; the common case is that we have
+ # the expected version and any compatibility versions in the same file,
+ # the default. Which means that this is almost always the case.
+ #
+ # We use require to avoid executing the code multiple times, like any
+ # other Ruby library that we might want to use. --daniel 2011-04-06
+ begin
+ require "puppet/faces/#{name}"
+
+ # If we wanted :current, we need to index to find that; direct version
+ # requests just work™ as they go. --daniel 2011-04-06
+ if version == :current then
+ # We need to find current out of this. This is the largest version
+ # number that doesn't have a dedicated on-disk file present; those
+ # represent "experimental" versions of faces, which we don't fully
+ # support yet.
+ #
+ # We walk the versions from highest to lowest and take the first version
+ # that is not defined in an explicitly versioned file on disk as the
+ # current version.
+ #
+ # This constrains us to only ship experimental versions with *one*
+ # version in the file, not multiple, but given you can't reliably load
+ # them except by side-effect when you ignore that rule this seems safe
+ # enough...
+ #
+ # Given those constraints, and that we are not going to ship a versioned
+ # interface that is not :current in this release, we are going to leave
+ # these thoughts in place, and just punt on the actual versioning.
+ #
+ # When we upgrade the core to support multiple versions we can solve the
+ # problems then; as lazy as possible.
+ #
+ # We do support multiple versions in the same file, though, so we sort
+ # versions here and return the last item in that set.
+ #
+ # --daniel 2011-04-06
+ latest_ver = @faces[name].keys.sort {|a, b| cmp_semver(a, b) }.last
+ @faces[name][:current] = @faces[name][latest_ver]
+ end
+ rescue LoadError => e
+ raise unless e.message =~ %r{-- puppet/faces/#{name}$}
+ # ...guess we didn't find the file; return a much better problem.
+ end
+
+ # Now, either we have the version in our set of faces, or we didn't find
+ # the version they were looking for. In the future we will support
+ # loading versioned stuff from some look-aside part of the Ruby load path,
+ # but we don't need that right now.
+ #
+ # So, this comment is a place-holder for that. --daniel 2011-04-06
+ return !! @faces[name].has_key?(version)
+ end
+
+ def self.register(face)
+ @faces[underscorize(face.name)][face.version] = face
+ end
+
+ def self.underscorize(name)
+ unless name.to_s =~ /^[-_a-z]+$/i then
+ raise ArgumentError, "#{name.inspect} (#{name.class}) is not a valid face name"
+ end
+
+ name.to_s.downcase.split(/[-_]/).join('_').to_sym
+ end
+end
diff --git a/lib/puppet/interface/option.rb b/lib/puppet/interface/option.rb
new file mode 100644
index 000000000..ccc2fbba7
--- /dev/null
+++ b/lib/puppet/interface/option.rb
@@ -0,0 +1,82 @@
+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 = []
+
+ # Collect and sort the arguments in the declaration.
+ dups = {}
+ declaration.each do |item|
+ if item.is_a? String and item.to_s =~ /^-/ then
+ unless item =~ /^-[a-z]\b/ or item =~ /^--[^-]/ then
+ raise ArgumentError, "#{item.inspect}: long options need two dashes (--)"
+ end
+ @optparse << item
+
+ # Duplicate checking...
+ name = optparse_to_name(item)
+ if dup = dups[name] then
+ raise ArgumentError, "#{item.inspect}: duplicates existing alias #{dup.inspect} in #{@parent}"
+ else
+ dups[name] = item
+ end
+ else
+ raise ArgumentError, "#{item.inspect} is not valid for an option argument"
+ end
+ end
+
+ if @optparse.empty? then
+ raise ArgumentError, "No option declarations found while building"
+ end
+
+ # Now, infer the name from the options; we prefer the first long option as
+ # the name, rather than just the first option.
+ @name = optparse_to_name(@optparse.find do |a| a =~ /^--/ end || @optparse.first)
+ @aliases = @optparse.map { |o| optparse_to_name(o) }
+
+ # Do we take an argument? If so, are we consistent about it, because
+ # incoherence here makes our life super-difficult, and we can more easily
+ # relax this rule later if we find a valid use case for it. --daniel 2011-03-30
+ @argument = @optparse.any? { |o| o =~ /[ =]/ }
+ if @argument and not @optparse.all? { |o| o =~ /[ =]/ } then
+ raise ArgumentError, "Option #{@name} is inconsistent about taking an argument"
+ end
+
+ # 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
+ raise ArgumentError, "Option #{@name} is inconsistent about the argument being optional"
+ end
+ end
+
+ # to_s and optparse_to_name are roughly mirrored, because they are used to
+ # transform options to name symbols, and vice-versa. This isn't a full
+ # bidirectional transformation though. --daniel 2011-04-07
+ def to_s
+ @name.to_s.tr('_', '-')
+ end
+
+ def optparse_to_name(declaration)
+ unless found = declaration.match(/^-+(?:\[no-\])?([^ =]+)/) then
+ raise ArgumentError, "Can't find a name in the declaration #{declaration.inspect}"
+ end
+ name = found.captures.first.tr('-', '_')
+ raise "#{name.inspect} is an invalid option name" unless name.to_s =~ /^[a-z]\w*$/
+ name.to_sym
+ end
+end
diff --git a/lib/puppet/interface/option_builder.rb b/lib/puppet/interface/option_builder.rb
new file mode 100644
index 000000000..83a1906b0
--- /dev/null
+++ b/lib/puppet/interface/option_builder.rb
@@ -0,0 +1,25 @@
+require 'puppet/interface/option'
+
+class Puppet::Interface::OptionBuilder
+ attr_reader :option
+
+ def self.build(face, *declaration, &block)
+ new(face, *declaration, &block).option
+ end
+
+ private
+ def initialize(face, *declaration, &block)
+ @face = face
+ @option = Puppet::Interface::Option.new(face, *declaration)
+ block and instance_eval(&block)
+ @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...
+
+ dsl = setter.sub(/=$/, '')
+ define_method(dsl) do |value| @option.send(setter, value) end
+ end
+end
diff --git a/lib/puppet/interface/option_manager.rb b/lib/puppet/interface/option_manager.rb
new file mode 100644
index 000000000..56df9760f
--- /dev/null
+++ b/lib/puppet/interface/option_manager.rb
@@ -0,0 +1,56 @@
+require 'puppet/interface/option_builder'
+
+module Puppet::Interface::OptionManager
+ # Declare that this app can take a specific option, and provide
+ # the code to do so.
+ def option(*declaration, &block)
+ add_option Puppet::Interface::OptionBuilder.build(self, *declaration, &block)
+ end
+
+ def add_option(option)
+ option.aliases.each do |name|
+ if conflict = get_option(name) then
+ raise ArgumentError, "Option #{option} conflicts with existing option #{conflict}"
+ end
+
+ actions.each do |action|
+ action = get_action(action)
+ if conflict = action.get_option(name) then
+ raise ArgumentError, "Option #{option} conflicts with existing option #{conflict} on #{action}"
+ end
+ end
+ end
+
+ option.aliases.each { |name| @options[name] = option }
+ option
+ end
+
+ def options
+ @options ||= {}
+ result = @options.keys
+
+ if self.is_a?(Class) and superclass.respond_to?(:options)
+ result += superclass.options
+ elsif self.class.respond_to?(:options)
+ result += self.class.options
+ end
+ result.sort
+ end
+
+ def get_option(name)
+ @options ||= {}
+ result = @options[name.to_sym]
+ unless result then
+ if self.is_a?(Class) and superclass.respond_to?(:get_option)
+ result = superclass.get_option(name)
+ elsif self.class.respond_to?(:get_option)
+ result = self.class.get_option(name)
+ end
+ end
+ return result
+ end
+
+ def option?(name)
+ options.include? name.to_sym
+ end
+end
diff --git a/spec/lib/puppet/faces/huzzah.rb b/spec/lib/puppet/faces/huzzah.rb
new file mode 100644
index 000000000..735004475
--- /dev/null
+++ b/spec/lib/puppet/faces/huzzah.rb
@@ -0,0 +1,4 @@
+require 'puppet/faces'
+Puppet::Faces.define(:huzzah, '2.0.1') do
+ action :bar do "is where beer comes from" end
+end
diff --git a/spec/shared_behaviours/things_that_declare_options.rb b/spec/shared_behaviours/things_that_declare_options.rb
new file mode 100644
index 000000000..1b41c2279
--- /dev/null
+++ b/spec/shared_behaviours/things_that_declare_options.rb
@@ -0,0 +1,134 @@
+# -*- coding: 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
+ end
+
+ it "should support options with an empty block" do
+ subject = add_options_to do
+ option "--foo" do
+ # this section deliberately left blank
+ end
+ end
+ subject.should be
+ subject.should be_option :foo
+ end
+
+ it "should support option documentation" do
+ text = "Sturm und Drang (German pronunciation: [ˈʃtʊʁm ʊnt ˈdʁaŋ]) …"
+
+ subject = add_options_to do
+ option "--foo" do
+ desc text
+ end
+ end
+
+ subject.get_option(:foo).desc.should == text
+ end
+
+ it "should list all the options" do
+ subject = add_options_to do
+ option "--foo"
+ option "--bar"
+ end
+ subject.options.should =~ [:foo, :bar]
+ end
+
+ it "should detect conflicts in long options" do
+ expect {
+ add_options_to do
+ option "--foo"
+ option "--foo"
+ end
+ }.should raise_error ArgumentError, /Option foo conflicts with existing option foo/i
+ end
+
+ it "should detect conflicts in short options" do
+ expect {
+ add_options_to do
+ option "-f"
+ option "-f"
+ end
+ }.should raise_error ArgumentError, /Option f conflicts with existing option f/
+ end
+
+ ["-f", "--foo"].each do |option|
+ ["", " FOO", "=FOO", " [FOO]", "=[FOO]"].each do |argument|
+ input = option + argument
+ it "should detect conflicts within a single option like #{input.inspect}" do
+ expect {
+ add_options_to do
+ option input, input
+ end
+ }.should raise_error ArgumentError, /duplicates existing alias/
+ end
+ end
+ end
+
+
+ # Verify the range of interesting conflicts to check for ordering causing
+ # the behaviour to change, or anything exciting like that.
+ [ %w{--foo}, %w{-f}, %w{-f --foo}, %w{--baz -f},
+ %w{-f --baz}, %w{-b --foo}, %w{--foo -b}
+ ].each do |conflict|
+ base = %w{--foo -f}
+ it "should detect conflicts between #{base.inspect} and #{conflict.inspect}" do
+ expect {
+ add_options_to do
+ option *base
+ option *conflict
+ end
+ }.should raise_error ArgumentError, /conflicts with existing option/
+ end
+ end
+
+ it "should fail if we are not consistent about taking an argument" do
+ expect { add_options_to do option "--foo=bar", "--bar" end }.
+ 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
+ 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
+ end
+ ["=FOO", " FOO", "=[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
+ end
+ end
+ end
+
+ describe "#optional_argument?" do
+ it "should be false if no argument is present" do
+ option = add_options_to do option "--foo" end.get_option(:foo)
+ option.should_not be_takes_argument
+ option.should_not be_optional_argument
+ end
+
+ ["=FOO", " FOO"].each do |input|
+ it "should be false if the argument is mandatory (like #{input.inspect})" do
+ option = add_options_to do option "--foo#{input}" end.get_option(:foo)
+ option.should be_takes_argument
+ option.should_not be_optional_argument
+ end
+ 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
+ end
+ end
+ end
+end
diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb
index 13470428e..fc63c6d19 100644
--- a/spec/spec_helper.rb
+++ b/spec/spec_helper.rb
@@ -20,6 +20,8 @@ module PuppetSpec
end
require 'pathname'
+require 'tmpdir'
+
require 'lib/puppet_spec/verbose'
require 'lib/puppet_spec/files'
require 'lib/puppet_spec/fixtures'
@@ -66,4 +68,5 @@ RSpec.configure do |config|
end
end
+# close of the "don't evaluate twice" mess.
end
diff --git a/spec/unit/application/certificate_spec.rb b/spec/unit/application/certificate_spec.rb
new file mode 100755
index 000000000..6153d9538
--- /dev/null
+++ b/spec/unit/application/certificate_spec.rb
@@ -0,0 +1,17 @@
+require 'puppet/application/certificate'
+
+describe Puppet::Application::Certificate do
+ it "should have a 'ca-location' option" do
+ # REVISIT: This is delegated from the face, and we will have a test there,
+ # so is this actually a valuable test? --daniel 2011-04-07
+ subject.command_line.stubs(:args).returns %w{list}
+ subject.preinit
+ subject.should respond_to(:handle_ca_location)
+ end
+
+ it "should accept the ca-location option" do
+ subject.command_line.stubs(:args).returns %w{--ca-location local list}
+ subject.preinit and subject.parse_options and subject.setup
+ subject.arguments.should == [{ :ca_location => "local" }]
+ end
+end
diff --git a/spec/unit/application/config_spec.rb b/spec/unit/application/config_spec.rb
new file mode 100755
index 000000000..066df6a51
--- /dev/null
+++ b/spec/unit/application/config_spec.rb
@@ -0,0 +1,10 @@
+#!/usr/bin/env ruby
+
+require File.expand_path(File.dirname(__FILE__) + '/../../spec_helper.rb')
+require 'puppet/application/config'
+
+describe Puppet::Application::Config do
+ it "should be a subclass of Puppet::Application::FacesBase" do
+ Puppet::Application::Config.superclass.should equal(Puppet::Application::FacesBase)
+ end
+end
diff --git a/spec/unit/application/configurer_spec.rb b/spec/unit/application/configurer_spec.rb
new file mode 100755
index 000000000..621039bcc
--- /dev/null
+++ b/spec/unit/application/configurer_spec.rb
@@ -0,0 +1,31 @@
+#!/usr/bin/env ruby
+
+require File.expand_path(File.dirname(__FILE__) + '/../../spec_helper.rb')
+require 'puppet/application/configurer'
+require 'puppet/indirector/catalog/rest'
+require 'puppet/indirector/report/rest'
+require 'tempfile'
+
+describe "Puppet::Application::Configurer" do
+ it "should retrieve and apply a catalog and submit a report" do
+ dirname = Dir.mktmpdir("puppetdir")
+ Puppet[:vardir] = dirname
+ Puppet[:confdir] = dirname
+ Puppet[:certname] = "foo"
+ @catalog = Puppet::Resource::Catalog.new
+ @file = Puppet::Resource.new(:file, File.join(dirname, "tmp_dir_resource"), :parameters => {:ensure => :present})
+ @catalog.add_resource(@file)
+
+ @report = Puppet::Transaction::Report.new("apply")
+ Puppet::Transaction::Report.stubs(:new).returns(@report)
+
+ Puppet::Resource::Catalog::Rest.any_instance.stubs(:find).returns(@catalog)
+ @report.expects(:save)
+
+ Puppet::Util::Log.stubs(:newdestination)
+
+ Puppet::Application::Configurer.new.run
+
+ @report.status.should == "changed"
+ end
+end
diff --git a/spec/unit/application/faces_base_spec.rb b/spec/unit/application/faces_base_spec.rb
new file mode 100755
index 000000000..6d8815f44
--- /dev/null
+++ b/spec/unit/application/faces_base_spec.rb
@@ -0,0 +1,185 @@
+#!/usr/bin/env ruby
+
+require File.expand_path(File.dirname(__FILE__) + '/../../spec_helper.rb')
+require 'puppet/application/faces_base'
+require 'tmpdir'
+
+class Puppet::Application::FacesBase::Basetest < Puppet::Application::FacesBase
+end
+
+describe Puppet::Application::FacesBase do
+ before :all do
+ @dir = Dir.mktmpdir
+ $LOAD_PATH.push(@dir)
+ FileUtils.mkdir_p(File.join @dir, 'puppet', 'faces')
+ File.open(File.join(@dir, 'puppet', 'faces', 'basetest.rb'), 'w') do |f|
+ f.puts "Puppet::Faces.define(:basetest, '0.0.1')"
+ end
+
+ Puppet::Faces.define(:basetest, '0.0.1') do
+ option("--[no-]boolean")
+ option("--mandatory MANDATORY")
+ option("--optional [OPTIONAL]")
+
+ action :foo do
+ option("--action")
+ when_invoked { |*args| args.length }
+ end
+ end
+ end
+
+ after :all do
+ FileUtils.remove_entry_secure @dir
+ $LOAD_PATH.pop
+ end
+
+ let :app do
+ app = Puppet::Application::FacesBase::Basetest.new
+ app.stubs(:exit)
+ app.stubs(:puts)
+ app.command_line.stubs(:subcommand_name).returns 'subcommand'
+ Puppet::Util::Log.stubs(:newdestination)
+ app
+ end
+
+ describe "#find_global_settings_argument" do
+ it "should not match --ca to --ca-location" do
+ option = mock('ca option', :optparse_args => ["--ca"])
+ Puppet.settings.expects(:each).yields(:ca, option)
+
+ app.find_global_settings_argument("--ca-location").should be_nil
+ end
+ end
+
+ describe "#preinit" do
+ before :each do
+ app.command_line.stubs(:args).returns %w{}
+ end
+
+ describe "parsing the command line" do
+ context "with just an action" do
+ before :all do
+ app.command_line.stubs(:args).returns %w{foo}
+ app.preinit
+ end
+
+ it "should set the faces based on the type" do
+ app.face.name.should == :basetest
+ end
+
+ it "should set the format based on the faces 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 }.
+ should raise_error ArgumentError, /No action given/
+ end
+
+ it "should report a sensible error when options with = fail" do
+ app.command_line.stubs(:args).returns %w{--action=bar foo}
+ expect { app.preinit }.
+ should raise_error ArgumentError, /Unknown option "--action"/
+ end
+
+ it "should fail if an action option is before the action" do
+ app.command_line.stubs(:args).returns %w{--action foo}
+ expect { app.preinit }.
+ should raise_error ArgumentError, /Unknown option "--action"/
+ end
+
+ it "should fail if an unknown option is before the action" do
+ app.command_line.stubs(:args).returns %w{--bar foo}
+ expect { app.preinit }.
+ should raise_error ArgumentError, /Unknown option "--bar"/
+ end
+
+ it "should not fail if an unknown option is after the action" do
+ app.command_line.stubs(:args).returns %w{foo --bar}
+ app.preinit
+ app.action.name.should == :foo
+ app.face.should_not be_option :bar
+ app.action.should_not be_option :bar
+ end
+
+ it "should accept --bar as an argument to a mandatory option after action" do
+ app.command_line.stubs(:args).returns %w{foo --mandatory --bar}
+ app.preinit and app.parse_options
+ app.action.name.should == :foo
+ app.options.should == { :mandatory => "--bar" }
+ end
+
+ it "should accept --bar as an argument to a mandatory option before action" do
+ app.command_line.stubs(:args).returns %w{--mandatory --bar foo}
+ app.preinit and app.parse_options
+ app.action.name.should == :foo
+ app.options.should == { :mandatory => "--bar" }
+ end
+
+ it "should not skip when --foo=bar is given" do
+ app.command_line.stubs(:args).returns %w{--mandatory=bar --bar foo}
+ expect { app.preinit }.
+ should raise_error ArgumentError, /Unknown option "--bar"/
+ end
+
+ { "boolean options before" => %w{--trace foo},
+ "boolean options after" => %w{foo --trace}
+ }.each do |name, args|
+ it "should accept global boolean settings #{name} the action" do
+ app.command_line.stubs(:args).returns args
+ app.preinit && app.parse_options
+ Puppet[:trace].should be_true
+ end
+ end
+
+ { "before" => %w{--syslogfacility user1 foo},
+ " after" => %w{foo --syslogfacility user1}
+ }.each do |name, args|
+ it "should accept global settings with arguments #{name} the action" do
+ app.command_line.stubs(:args).returns args
+ app.preinit && app.parse_options
+ Puppet[:syslogfacility].should == "user1"
+ end
+ end
+ end
+ end
+
+ describe "#setup" do
+ it "should remove the action name from the arguments" do
+ app.command_line.stubs(:args).returns %w{--mandatory --bar foo}
+ app.preinit and app.parse_options and app.setup
+ app.arguments.should == [{ :mandatory => "--bar" }]
+ end
+
+ it "should pass positional arguments" do
+ app.command_line.stubs(:args).returns %w{--mandatory --bar foo bar baz quux}
+ app.preinit and app.parse_options and app.setup
+ app.arguments.should == ['bar', 'baz', 'quux', { :mandatory => "--bar" }]
+ end
+ end
+
+ describe "#main" do
+ before do
+ app.face = Puppet::Faces[:basetest, '0.0.1']
+ app.action = app.face.get_action(:foo)
+ app.format = :pson
+ app.arguments = ["myname", "myarg"]
+ end
+
+ it "should send the specified verb and name to the faces" do
+ app.face.expects(:foo).with(*app.arguments)
+ app.main
+ end
+
+ it "should use its render method to render any result" do
+ app.expects(:render).with(app.arguments.length + 1)
+ app.main
+ end
+ end
+end
diff --git a/spec/unit/application/faces_spec.rb b/spec/unit/application/faces_spec.rb
new file mode 100755
index 000000000..d945c40b5
--- /dev/null
+++ b/spec/unit/application/faces_spec.rb
@@ -0,0 +1,10 @@
+#!/usr/bin/env ruby
+
+require File.expand_path(File.dirname(__FILE__) + '/../../spec_helper.rb')
+require 'puppet/application/faces'
+
+describe Puppet::Application::Faces do
+ it "should be an application" do
+ Puppet::Application::Faces.superclass.should equal(Puppet::Application)
+ end
+end
diff --git a/spec/unit/application/indirection_base_spec.rb b/spec/unit/application/indirection_base_spec.rb
new file mode 100755
index 000000000..a73cf4fca
--- /dev/null
+++ b/spec/unit/application/indirection_base_spec.rb
@@ -0,0 +1,39 @@
+#!/usr/bin/env ruby
+
+require File.expand_path(File.dirname(__FILE__) + '/../../spec_helper.rb')
+require 'puppet/application/indirection_base'
+require 'puppet/faces/indirector'
+
+########################################################################
+# Stub for testing; the names are critical, sadly. --daniel 2011-03-30
+class Puppet::Application::TestIndirection < Puppet::Application::IndirectionBase
+end
+
+face = Puppet::Faces::Indirector.define(:testindirection, '0.0.1') do
+end
+# REVISIT: This horror is required because we don't allow anything to be
+# :current except for if it lives on, and is loaded from, disk. --daniel 2011-03-29
+face.version = :current
+Puppet::Faces.register(face)
+########################################################################
+
+
+describe Puppet::Application::IndirectionBase do
+ subject { Puppet::Application::TestIndirection.new }
+
+ 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")
+ Puppet::Indirector::Indirection.expects(:instance).
+ with(:testindirection).twice.returns()
+
+ subject.command_line.
+ instance_variable_set('@args', %w{--terminus foo save})
+
+ # Not a very nice thing. :(
+ $stderr.stubs(:puts)
+
+ expect { subject.run }.should raise_error SystemExit
+ end
+end
diff --git a/spec/unit/faces/catalog_spec.rb b/spec/unit/faces/catalog_spec.rb
new file mode 100755
index 000000000..e0a771d10
--- /dev/null
+++ b/spec/unit/faces/catalog_spec.rb
@@ -0,0 +1,4 @@
+require 'puppet/faces'
+describe Puppet::Faces[:catalog, '0.0.1'] do
+ it "should actually have some testing..."
+end
diff --git a/spec/unit/faces/certificate_request_spec.rb b/spec/unit/faces/certificate_request_spec.rb
new file mode 100755
index 000000000..1a71a8379
--- /dev/null
+++ b/spec/unit/faces/certificate_request_spec.rb
@@ -0,0 +1,3 @@
+describe Puppet::Faces[:certificate_request, '0.0.1'] do
+ it "should actually have some tests..."
+end
diff --git a/spec/unit/faces/certificate_revocation_list_spec.rb b/spec/unit/faces/certificate_revocation_list_spec.rb
new file mode 100755
index 000000000..4f41edef6
--- /dev/null
+++ b/spec/unit/faces/certificate_revocation_list_spec.rb
@@ -0,0 +1,3 @@
+describe Puppet::Faces[:certificate_revocation_list, '0.0.1'] do
+ it "should actually have some tests..."
+end
diff --git a/spec/unit/faces/certificate_spec.rb b/spec/unit/faces/certificate_spec.rb
new file mode 100755
index 000000000..ba264f967
--- /dev/null
+++ b/spec/unit/faces/certificate_spec.rb
@@ -0,0 +1,14 @@
+require 'puppet/ssl/host'
+
+describe Puppet::Faces[:certificate, '0.0.1'] do
+ it "should have a ca-location option" do
+ subject.should be_option :ca_location
+ end
+
+ it "should set the ca location when invoked" do
+ pending "#6983: This is broken in the actual faces..."
+ Puppet::SSL::Host.expects(:ca_location=).with(:foo)
+ Puppet::SSL::Host.indirection.expects(:save)
+ subject.sign :ca_location => :foo
+ end
+end
diff --git a/spec/unit/faces/config_spec.rb b/spec/unit/faces/config_spec.rb
new file mode 100755
index 000000000..2eb04a81b
--- /dev/null
+++ b/spec/unit/faces/config_spec.rb
@@ -0,0 +1,24 @@
+#!/usr/bin/env ruby
+
+require File.expand_path(File.dirname(__FILE__) + '/../../spec_helper.rb')
+
+describe Puppet::Faces[:config, '0.0.1'] do
+ it "should use Settings#print_config_options when asked to print" do
+ Puppet.settings.stubs(:puts)
+ Puppet.settings.expects(:print_config_options)
+ subject.print
+ end
+
+ it "should set 'configprint' to all desired values and call print_config_options when a specific value is provided" do
+ Puppet.settings.stubs(:puts)
+ Puppet.settings.expects(:print_config_options)
+ subject.print("libdir", "ssldir")
+ Puppet.settings[:configprint].should == "libdir,ssldir"
+ end
+
+ it "should always return nil" do
+ Puppet.settings.stubs(:puts)
+ Puppet.settings.expects(:print_config_options)
+ subject.print("libdir").should be_nil
+ end
+end
diff --git a/spec/unit/faces/configurer_spec.rb b/spec/unit/faces/configurer_spec.rb
new file mode 100755
index 000000000..270888ca1
--- /dev/null
+++ b/spec/unit/faces/configurer_spec.rb
@@ -0,0 +1,24 @@
+#!/usr/bin/env ruby
+
+require File.expand_path(File.dirname(__FILE__) + '/../../spec_helper.rb')
+require 'puppet/indirector/catalog/rest'
+require 'tempfile'
+
+describe Puppet::Faces[:configurer, '0.0.1'] do
+ describe "#synchronize" do
+ it "should retrieve and apply a catalog and return a report" do
+ dirname = Dir.mktmpdir("puppetdir")
+ Puppet[:vardir] = dirname
+ Puppet[:confdir] = dirname
+ @catalog = Puppet::Resource::Catalog.new
+ @file = Puppet::Resource.new(:file, File.join(dirname, "tmp_dir_resource"), :parameters => {:ensure => :present})
+ @catalog.add_resource(@file)
+ Puppet::Resource::Catalog::Rest.any_instance.stubs(:find).returns(@catalog)
+
+ report = subject.synchronize("foo")
+
+ report.kind.should == "apply"
+ report.status.should == "changed"
+ end
+ end
+end
diff --git a/spec/unit/faces/facts_spec.rb b/spec/unit/faces/facts_spec.rb
new file mode 100755
index 000000000..480f463e4
--- /dev/null
+++ b/spec/unit/faces/facts_spec.rb
@@ -0,0 +1,21 @@
+#!/usr/bin/env ruby
+
+require File.expand_path(File.dirname(__FILE__) + '/../../spec_helper.rb')
+
+describe Puppet::Faces[:facts, '0.0.1'] do
+ it "should define an 'upload' fact" 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"
+
+ it "should set the cach_eclass to :rest"
+
+ it "should find the current certname"
+ end
+end
diff --git a/spec/unit/faces/file_spec.rb b/spec/unit/faces/file_spec.rb
new file mode 100755
index 000000000..fcb52c67e
--- /dev/null
+++ b/spec/unit/faces/file_spec.rb
@@ -0,0 +1,3 @@
+describe Puppet::Faces[:file, '0.0.1'] do
+ it "should actually have some tests..."
+end
diff --git a/spec/unit/faces/indirector_spec.rb b/spec/unit/faces/indirector_spec.rb
new file mode 100755
index 000000000..3ed64bc01
--- /dev/null
+++ b/spec/unit/faces/indirector_spec.rb
@@ -0,0 +1,60 @@
+#!/usr/bin/env ruby
+
+require File.expand_path(File.dirname(__FILE__) + '/../../spec_helper.rb')
+require 'puppet/faces/indirector'
+
+describe Puppet::Faces::Indirector do
+ subject do
+ instance = Puppet::Faces::Indirector.new(:test, '0.0.1')
+ indirection = stub('indirection',
+ :name => :stub_indirection,
+ :reset_terminus_class => nil)
+ instance.stubs(:indirection).returns indirection
+ instance
+ end
+
+ it "should be able to return a list of indirections" do
+ Puppet::Faces::Indirector.indirections.should be_include("catalog")
+ end
+
+ it "should be able to return a list of terminuses for a given indirection" do
+ Puppet::Faces::Indirector.terminus_classes(:catalog).should be_include("compiler")
+ end
+
+ describe "as an instance" do
+ it "should be able to determine its indirection" do
+ # Loading actions here an get, um, complicated
+ Puppet::Faces.stubs(:load_actions)
+ Puppet::Faces::Indirector.new(:catalog, '0.0.1').indirection.should equal(Puppet::Resource::Catalog.indirection)
+ end
+ end
+
+ [:find, :search, :save, :destroy].each do |method|
+ it "should define a '#{method}' action" do
+ Puppet::Faces::Indirector.should be_action(method)
+ end
+
+ it "should call the indirection method with options when the '#{method}' action is invoked" do
+ subject.indirection.expects(method).with(:test, "myargs", {})
+ subject.send(method, :test, "myargs")
+ end
+ it "should forward passed options" do
+ subject.indirection.expects(method).with(:test, "action", {'one'=>'1'})
+ subject.send(method, :test, 'action', {'one'=>'1'})
+ end
+ end
+
+ it "should be able to override its indirection name" do
+ subject.set_indirection_name :foo
+ subject.indirection_name.should == :foo
+ end
+
+ it "should be able to set its terminus class" do
+ subject.indirection.expects(:terminus_class=).with(:myterm)
+ subject.set_terminus(:myterm)
+ end
+
+ it "should define a class-level 'info' action" do
+ Puppet::Faces::Indirector.should be_action(:info)
+ end
+end
diff --git a/spec/unit/faces/key_spec.rb b/spec/unit/faces/key_spec.rb
new file mode 100755
index 000000000..9b7a58706
--- /dev/null
+++ b/spec/unit/faces/key_spec.rb
@@ -0,0 +1,3 @@
+describe Puppet::Faces[:key, '0.0.1'] do
+ it "should actually have some tests..."
+end
diff --git a/spec/unit/faces/node_spec.rb b/spec/unit/faces/node_spec.rb
new file mode 100755
index 000000000..4639bdf63
--- /dev/null
+++ b/spec/unit/faces/node_spec.rb
@@ -0,0 +1,9 @@
+#!/usr/bin/env ruby
+
+require File.expand_path(File.dirname(__FILE__) + '/../../spec_helper.rb')
+
+describe Puppet::Faces[:node, '0.0.1'] do
+ it "should set its default format to :yaml" do
+ subject.default_format.should == :yaml
+ end
+end
diff --git a/spec/unit/faces/report_spec.rb b/spec/unit/faces/report_spec.rb
new file mode 100755
index 000000000..30897d5e7
--- /dev/null
+++ b/spec/unit/faces/report_spec.rb
@@ -0,0 +1,3 @@
+describe Puppet::Faces[:report, '0.0.1'] do
+ it "should actually have some tests..."
+end
diff --git a/spec/unit/faces/resource_spec.rb b/spec/unit/faces/resource_spec.rb
new file mode 100755
index 000000000..e3f2e1c62
--- /dev/null
+++ b/spec/unit/faces/resource_spec.rb
@@ -0,0 +1,3 @@
+describe Puppet::Faces[:resource, '0.0.1'] do
+ it "should actually have some tests..."
+end
diff --git a/spec/unit/faces/resource_type_spec.rb b/spec/unit/faces/resource_type_spec.rb
new file mode 100755
index 000000000..fcbf07520
--- /dev/null
+++ b/spec/unit/faces/resource_type_spec.rb
@@ -0,0 +1,3 @@
+describe Puppet::Faces[:resource_type, '0.0.1'] do
+ it "should actually have some tests..."
+end
diff --git a/spec/unit/interface/action_builder_spec.rb b/spec/unit/interface/action_builder_spec.rb
new file mode 100755
index 000000000..ae9cc83d4
--- /dev/null
+++ b/spec/unit/interface/action_builder_spec.rb
@@ -0,0 +1,59 @@
+#!/usr/bin/env ruby
+
+require File.expand_path(File.dirname(__FILE__) + '/../../spec_helper.rb')
+require 'puppet/interface/action_builder'
+
+describe Puppet::Interface::ActionBuilder do
+ describe "::build" do
+ 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')
+ action = Puppet::Interface::ActionBuilder.build(face, :foo) do
+ when_invoked do
+ "invoked the method"
+ end
+ 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
+ let :face do Puppet::Interface.new(:option_handling, '0.0.1') end
+
+ 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_option :bar
+ 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
+ end
+ action.should be_option :bar
+ end
+ end
+ end
+end
diff --git a/spec/unit/interface/action_manager_spec.rb b/spec/unit/interface/action_manager_spec.rb
new file mode 100755
index 000000000..50bea5f89
--- /dev/null
+++ b/spec/unit/interface/action_manager_spec.rb
@@ -0,0 +1,233 @@
+#!/usr/bin/env ruby
+
+require File.expand_path(File.dirname(__FILE__) + '/../../spec_helper.rb')
+
+# This is entirely an internal class for Interface, so we have to load it instead of our class.
+require 'puppet/interface'
+
+class ActionManagerTester
+ include Puppet::Interface::ActionManager
+end
+
+describe Puppet::Interface::ActionManager do
+ subject { ActionManagerTester.new }
+
+ describe "when included in a class" do
+ it "should be able to define an action" do
+ subject.action(:foo) do
+ when_invoked { "something "}
+ end
+ end
+
+ it "should be able to define a 'script' style action" do
+ subject.script :bar do
+ "a bar is where beer is found"
+ end
+ end
+
+ it "should be able to list defined actions" do
+ subject.action(:foo) do
+ when_invoked { "something" }
+ end
+ subject.action(:bar) do
+ when_invoked { "something" }
+ end
+
+ subject.actions.should =~ [:foo, :bar]
+ end
+
+ it "should list 'script' actions" do
+ subject.script :foo do "foo" end
+ subject.actions.should =~ [:foo]
+ end
+
+ it "should list both script and normal actions" do
+ subject.action :foo do
+ when_invoked do "foo" end
+ end
+ subject.script :bar do "a bar is where beer is found" end
+
+ subject.actions.should =~ [:foo, :bar]
+ end
+
+ it "should be able to indicate when an action is defined" do
+ subject.action(:foo) do
+ when_invoked { "something" }
+ end
+
+ subject.should be_action(:foo)
+ end
+
+ it "should indicate an action is defined for script actions" do
+ subject.script :foo do "foo" end
+ subject.should be_action :foo
+ end
+
+ it "should correctly treat action names specified as strings" do
+ subject.action(:foo) do
+ when_invoked { "something" }
+ end
+
+ subject.should be_action("foo")
+ end
+ end
+
+ describe "when used to extend a class" do
+ subject { Class.new.extend(Puppet::Interface::ActionManager) }
+
+ it "should be able to define an action" do
+ subject.action(:foo) do
+ when_invoked { "something "}
+ end
+ end
+
+ it "should be able to list defined actions" do
+ subject.action(:foo) do
+ when_invoked { "something" }
+ end
+ subject.action(:bar) do
+ when_invoked { "something" }
+ end
+
+ subject.actions.should include(:bar)
+ subject.actions.should include(:foo)
+ end
+
+ it "should be able to indicate when an action is defined" do
+ subject.action(:foo) { "something" }
+ subject.should be_action(:foo)
+ end
+ end
+
+ describe "when used both at the class and instance level" do
+ before do
+ @klass = Class.new do
+ include Puppet::Interface::ActionManager
+ extend Puppet::Interface::ActionManager
+ end
+ @instance = @klass.new
+ end
+
+ it "should be able to define an action at the class level" do
+ @klass.action(:foo) do
+ when_invoked { "something "}
+ end
+ end
+
+ it "should create an instance method when an action is defined at the class level" do
+ @klass.action(:foo) do
+ when_invoked { "something" }
+ end
+ @instance.foo.should == "something"
+ end
+
+ it "should be able to define an action at the instance level" do
+ @instance.action(:foo) do
+ when_invoked { "something "}
+ end
+ end
+
+ it "should create an instance method when an action is defined at the instance level" do
+ @instance.action(:foo) do
+ when_invoked { "something" }
+ end
+ @instance.foo.should == "something"
+ end
+
+ it "should be able to list actions defined at the class level" do
+ @klass.action(:foo) do
+ when_invoked { "something" }
+ end
+ @klass.action(:bar) do
+ when_invoked { "something" }
+ end
+
+ @klass.actions.should include(:bar)
+ @klass.actions.should include(:foo)
+ end
+
+ it "should be able to list actions defined at the instance level" do
+ @instance.action(:foo) do
+ when_invoked { "something" }
+ end
+ @instance.action(:bar) do
+ when_invoked { "something" }
+ end
+
+ @instance.actions.should include(:bar)
+ @instance.actions.should include(:foo)
+ end
+
+ it "should be able to list actions defined at both instance and class level" do
+ @klass.action(:foo) do
+ when_invoked { "something" }
+ end
+ @instance.action(:bar) do
+ when_invoked { "something" }
+ end
+
+ @instance.actions.should include(:bar)
+ @instance.actions.should include(:foo)
+ end
+
+ it "should be able to indicate when an action is defined at the class level" do
+ @klass.action(:foo) do
+ when_invoked { "something" }
+ end
+ @instance.should be_action(:foo)
+ end
+
+ it "should be able to indicate when an action is defined at the instance level" do
+ @klass.action(:foo) do
+ when_invoked { "something" }
+ end
+ @instance.should be_action(:foo)
+ end
+
+ it "should list actions defined in superclasses" do
+ @subclass = Class.new(@klass)
+ @instance = @subclass.new
+
+ @klass.action(:parent) do
+ when_invoked { "a" }
+ end
+ @subclass.action(:sub) do
+ when_invoked { "a" }
+ end
+ @instance.action(:instance) do
+ when_invoked { "a" }
+ end
+
+ @instance.should be_action(:parent)
+ @instance.should be_action(:sub)
+ @instance.should be_action(:instance)
+ end
+
+ it "should create an instance method when an action is defined in a superclass" do
+ @subclass = Class.new(@klass)
+ @instance = @subclass.new
+
+ @klass.action(:foo) do
+ when_invoked { "something" }
+ end
+ @instance.foo.should == "something"
+ end
+ end
+
+ describe "#get_action" do
+ let :parent_class do
+ parent_class = Class.new(Puppet::Interface)
+ parent_class.action(:foo) {}
+ parent_class
+ end
+
+ it "should check that we can find inherited actions when we are a class" do
+ Class.new(parent_class).get_action(:foo).name.should == :foo
+ end
+
+ it "should check that we can find inherited actions when we are an instance" do
+ instance = parent_class.new(:foo, '0.0.0')
+ instance.get_action(:foo).name.should == :foo
+ end
+ end
+end
diff --git a/spec/unit/interface/action_spec.rb b/spec/unit/interface/action_spec.rb
new file mode 100755
index 000000000..4801a3cc8
--- /dev/null
+++ b/spec/unit/interface/action_spec.rb
@@ -0,0 +1,173 @@
+#!/usr/bin/env ruby
+
+require File.expand_path(File.dirname(__FILE__) + '/../../spec_helper.rb')
+require 'puppet/interface/action'
+
+describe Puppet::Interface::Action do
+ describe "when validating the action name" do
+ [nil, '', 'foo bar', '-foobar'].each do |input|
+ it "should treat #{input.inspect} as an invalid name" do
+ expect { Puppet::Interface::Action.new(nil, input) }.
+ should raise_error(/is an invalid action name/)
+ end
+ end
+ end
+
+ describe "when invoking" do
+ it "should be able to call other actions on the same object" do
+ face = Puppet::Interface.new(:my_face, '0.0.1') do
+ action(:foo) do
+ when_invoked { 25 }
+ end
+
+ action(:bar) do
+ when_invoked { "the value of foo is '#{foo}'" }
+ end
+ end
+ face.foo.should == 25
+ face.bar.should == "the value of foo is '25'"
+ end
+
+ # bar is a class action calling a class action
+ # quux is a class action calling an instance action
+ # baz is an instance action calling a class action
+ # qux is an instance action calling an instance action
+ it "should be able to call other actions on the same object when defined on a class" do
+ class Puppet::Interface::MyInterfaceBaseClass < Puppet::Interface
+ action(:foo) do
+ when_invoked { 25 }
+ end
+
+ action(:bar) do
+ when_invoked { "the value of foo is '#{foo}'" }
+ end
+
+ action(:quux) do
+ when_invoked { "qux told me #{qux}" }
+ end
+ end
+
+ face = Puppet::Interface::MyInterfaceBaseClass.new(:my_inherited_face, '0.0.1') do
+ action(:baz) do
+ when_invoked { "the value of foo in baz is '#{foo}'" }
+ end
+
+ action(:qux) do
+ when_invoked { baz }
+ end
+ end
+ face.foo.should == 25
+ face.bar.should == "the value of foo is '25'"
+ face.quux.should == "qux told me the value of foo in baz is '25'"
+ face.baz.should == "the value of foo in baz is '25'"
+ face.qux.should == "the value of foo in baz is '25'"
+ end
+
+ context "when calling the Ruby API" do
+ let :face do
+ Puppet::Interface.new(:ruby_api, '1.0.0') do
+ action :bar do
+ when_invoked do |options|
+ options
+ end
+ end
+ end
+ end
+
+ it "should work when no options are supplied" do
+ options = face.bar
+ options.should == {}
+ end
+
+ it "should work when options are supplied" do
+ options = face.bar :bar => "beer"
+ options.should == { :bar => "beer" }
+ end
+ end
+ end
+
+ describe "with action-level options" do
+ it "should support options with an empty block" do
+ face = Puppet::Interface.new(:action_level_options, '0.0.1') do
+ action :foo do
+ option "--bar" do
+ # this line left deliberately blank
+ end
+ end
+ end
+
+ face.should_not be_option :bar
+ face.get_action(:foo).should be_option :bar
+ end
+
+ it "should return only action level options when there are no face options" do
+ face = Puppet::Interface.new(:action_level_options, '0.0.1') do
+ action :foo do option "--bar" end
+ end
+
+ face.get_action(:foo).options.should =~ [:bar]
+ end
+
+ describe "with both face and action options" do
+ let :face do
+ Puppet::Interface.new(:action_level_options, '0.0.1') do
+ action :foo do option "--bar" end
+ action :baz do option "--bim" end
+ option "--quux"
+ end
+ end
+
+ it "should return combined face and action options" do
+ face.get_action(:foo).options.should =~ [:bar, :quux]
+ end
+
+ it "should fetch options that the face inherited" do
+ parent = Class.new(Puppet::Interface)
+ parent.option "--foo"
+ child = parent.new(:inherited_options, '0.0.1') do
+ option "--bar"
+ action :action do option "--baz" end
+ end
+
+ action = child.get_action(:action)
+ action.should be
+
+ [:baz, :bar, :foo].each do |name|
+ action.get_option(name).should be_an_instance_of Puppet::Interface::Option
+ end
+ end
+
+ it "should get an action option when asked" do
+ face.get_action(:foo).get_option(:bar).
+ should be_an_instance_of Puppet::Interface::Option
+ end
+
+ it "should get a face option when asked" do
+ face.get_action(:foo).get_option(:quux).
+ should be_an_instance_of Puppet::Interface::Option
+ end
+
+ it "should return options only for this action" do
+ face.get_action(:baz).options.should =~ [:bim, :quux]
+ end
+ end
+
+ it_should_behave_like "things that declare options" do
+ def add_options_to(&block)
+ face = Puppet::Interface.new(:with_options, '0.0.1') do
+ action(:foo, &block)
+ end
+ face.get_action(:foo)
+ end
+ end
+
+ it "should fail when a face option duplicates an action option" do
+ expect {
+ Puppet::Interface.new(:action_level_options, '0.0.1') do
+ option "--foo"
+ action :bar do option "--foo" end
+ end
+ }.should raise_error ArgumentError, /Option foo conflicts with existing option foo/i
+ end
+ end
+end
diff --git a/spec/unit/interface/face_collection_spec.rb b/spec/unit/interface/face_collection_spec.rb
new file mode 100755
index 000000000..bf3801efd
--- /dev/null
+++ b/spec/unit/interface/face_collection_spec.rb
@@ -0,0 +1,175 @@
+#!/usr/bin/env ruby
+require File.expand_path(File.dirname(__FILE__) + '/../../spec_helper.rb')
+
+require 'tmpdir'
+require 'puppet/interface/face_collection'
+
+describe Puppet::Interface::FaceCollection do
+ # To avoid cross-pollution we have to save and restore both the hash
+ # containing all the interface data, and the array used by require. Restoring
+ # both means that we don't leak side-effects across the code. --daniel 2011-04-06
+ before :each do
+ @original_faces = subject.instance_variable_get("@faces").dup
+ @original_required = $".dup
+ subject.instance_variable_get("@faces").clear
+ end
+
+ after :each do
+ subject.instance_variable_set("@faces", @original_faces)
+ $".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
+ end
+
+ it 'should permit versions with appended descriptions' do
+ subject.validate_version('10.10.10beta').should == true
+ end
+
+ it 'should not permit versions with more than three numbers' do
+ subject.validate_version('1.2.3.4').should == false
+ end
+
+ it 'should not permit versions with only two numbers' do
+ subject.validate_version('10.10').should == false
+ end
+
+ it 'should not permit versions with only one number' do
+ subject.validate_version('123').should == false
+ end
+
+ it 'should not permit versions with text in any position but at the end' do
+ subject.validate_version('v1.1.1').should == false
+ end
+ end
+
+ describe "::[]" do
+ before :each do
+ subject.instance_variable_get("@faces")[:foo]['0.0.1'] = 10
+ end
+
+ before :each do
+ @dir = Dir.mktmpdir
+ @lib = FileUtils.mkdir_p(File.join @dir, 'puppet', 'faces')
+ $LOAD_PATH.push(@dir)
+ end
+
+ after :each do
+ FileUtils.remove_entry_secure @dir
+ $LOAD_PATH.pop
+ end
+
+ it "should return the faces with the given name" do
+ subject["foo", '0.0.1'].should == 10
+ end
+
+ it "should attempt to load the faces if it isn't found" do
+ subject.expects(:require).with('puppet/faces/bar')
+ subject["bar", '0.0.1']
+ end
+
+ it "should attempt to load the default faces for the specified version :current" do
+ subject.expects(:require).never # except...
+ subject.expects(:require).with('puppet/faces/fozzie')
+ subject['fozzie', :current]
+ end
+ end
+
+ describe "::face?" do
+ before :each do
+ subject.instance_variable_get("@faces")[:foo]['0.0.1'] = 10
+ end
+
+ it "should return true if the faces specified is registered" do
+ subject.face?("foo", '0.0.1').should == true
+ end
+
+ it "should attempt to require the faces if it is not registered" do
+ subject.expects(:require).with do |file|
+ subject.instance_variable_get("@faces")[:bar]['0.0.1'] = true
+ file == 'puppet/faces/bar'
+ end
+ subject.face?("bar", '0.0.1').should == true
+ end
+
+ it "should return true if requiring the faces registered it" do
+ subject.stubs(:require).with do
+ subject.instance_variable_get("@faces")[:bar]['0.0.1'] = 20
+ end
+ end
+
+ it "should return false if the faces is not registered" do
+ subject.stubs(:require).returns(true)
+ subject.face?("bar", '0.0.1').should be_false
+ end
+
+ it "should return false if the faces file itself is missing" do
+ subject.stubs(:require).
+ raises(LoadError, 'no such file to load -- puppet/faces/bar')
+ subject.face?("bar", '0.0.1').should be_false
+ end
+
+ it "should register the version loaded by `:current` as `:current`" do
+ subject.expects(:require).with do |file|
+ subject.instance_variable_get("@faces")[:huzzah]['2.0.1'] = :huzzah_faces
+ file == 'puppet/faces/huzzah'
+ end
+ subject.face?("huzzah", :current)
+ subject.instance_variable_get("@faces")[:huzzah][:current].should == :huzzah_faces
+ end
+
+ context "with something on disk" do
+ it "should register the version loaded from `puppet/faces/{name}` as `:current`" do
+ subject.should be_face "huzzah", '2.0.1'
+ subject.should be_face "huzzah", :current
+ Puppet::Faces[:huzzah, '2.0.1'].should == Puppet::Faces[:huzzah, :current]
+ end
+
+ it "should index :current when the code was pre-required" do
+ subject.instance_variable_get("@faces")[:huzzah].should_not be_key :current
+ require 'puppet/faces/huzzah'
+ subject.face?(:huzzah, :current).should be_true
+ end
+ end
+ end
+
+ describe "::register" do
+ it "should store the faces by name" do
+ faces = Puppet::Faces.new(:my_faces, '0.0.1')
+ subject.register(faces)
+ subject.instance_variable_get("@faces").should == {:my_faces => {'0.0.1' => faces}}
+ end
+ end
+
+ describe "::underscorize" do
+ faulty = [1, "#foo", "$bar", "sturm und drang", :"sturm und drang"]
+ valid = {
+ "Foo" => :foo,
+ :Foo => :foo,
+ "foo_bar" => :foo_bar,
+ :foo_bar => :foo_bar,
+ "foo-bar" => :foo_bar,
+ :"foo-bar" => :foo_bar,
+ }
+
+ valid.each do |input, expect|
+ it "should map #{input.inspect} to #{expect.inspect}" do
+ result = subject.underscorize(input)
+ result.should == expect
+ end
+ end
+
+ faulty.each do |input|
+ it "should fail when presented with #{input.inspect} (#{input.class})" do
+ expect { subject.underscorize(input) }.
+ should raise_error ArgumentError, /not a valid face name/
+ end
+ end
+ end
+end
diff --git a/spec/unit/interface/option_builder_spec.rb b/spec/unit/interface/option_builder_spec.rb
new file mode 100644
index 000000000..fae48324e
--- /dev/null
+++ b/spec/unit/interface/option_builder_spec.rb
@@ -0,0 +1,29 @@
+require 'puppet/interface/option_builder'
+
+describe Puppet::Interface::OptionBuilder do
+ let :face do Puppet::Interface.new(:option_builder_testing, '0.0.1') end
+
+ it "should be able to construct an option without a block" do
+ Puppet::Interface::OptionBuilder.build(face, "--foo").
+ should be_an_instance_of Puppet::Interface::Option
+ end
+
+ describe "when using the DSL 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
+ end
+end
diff --git a/spec/unit/interface/option_spec.rb b/spec/unit/interface/option_spec.rb
new file mode 100644
index 000000000..3bcd121e2
--- /dev/null
+++ b/spec/unit/interface/option_spec.rb
@@ -0,0 +1,75 @@
+require 'puppet/interface/option'
+
+describe Puppet::Interface::Option do
+ let :face do Puppet::Interface.new(:option_testing, '0.0.1') end
+
+ describe "#optparse_to_name" do
+ ["", "=BAR", " BAR", "=bar", " bar"].each do |postfix|
+ { "--foo" => :foo, "-f" => :f }.each do |base, expect|
+ input = base + postfix
+ it "should map #{input.inspect} to #{expect.inspect}" do
+ option = Puppet::Interface::Option.new(face, input)
+ option.name.should == expect
+ end
+ end
+ end
+
+ [:foo, 12, nil, {}, []].each do |input|
+ it "should fail sensible when given #{input.inspect}" do
+ expect { Puppet::Interface::Option.new(face, input) }.
+ should raise_error ArgumentError, /is not valid for an option argument/
+ end
+ end
+
+ ["-foo", "-foo=BAR", "-foo BAR"].each do |input|
+ it "should fail with a single dash for long option #{input.inspect}" do
+ expect { Puppet::Interface::Option.new(face, input) }.
+ should raise_error ArgumentError, /long options need two dashes \(--\)/
+ end
+ end
+ end
+
+ it "requires a face when created" do
+ expect { Puppet::Interface::Option.new }.
+ should raise_error ArgumentError, /wrong number of arguments/
+ end
+
+ it "also requires some declaration arguments when created" do
+ expect { Puppet::Interface::Option.new(face) }.
+ should raise_error ArgumentError, /No option declarations found/
+ end
+
+ it "should infer the name from an optparse string" do
+ option = Puppet::Interface::Option.new(face, "--foo")
+ option.name.should == :foo
+ end
+
+ it "should infer the name when multiple optparse string are given" do
+ option = Puppet::Interface::Option.new(face, "--foo", "-f")
+ option.name.should == :foo
+ end
+
+ it "should prefer the first long option name over a short option name" do
+ option = Puppet::Interface::Option.new(face, "-f", "--foo")
+ option.name.should == :foo
+ end
+
+ it "should create an instance when given a face and name" do
+ Puppet::Interface::Option.new(face, "--foo").
+ should be_instance_of Puppet::Interface::Option
+ end
+
+ describe "#to_s" do
+ it "should transform a symbol into a string" do
+ option = Puppet::Interface::Option.new(face, "--foo")
+ option.name.should == :foo
+ option.to_s.should == "foo"
+ end
+
+ it "should use - rather than _ to separate words in strings but not symbols" do
+ option = Puppet::Interface::Option.new(face, "--foo-bar")
+ option.name.should == :foo_bar
+ option.to_s.should == "foo-bar"
+ end
+ end
+end
diff --git a/spec/unit/interface_spec.rb b/spec/unit/interface_spec.rb
new file mode 100755
index 000000000..ea11b21ba
--- /dev/null
+++ b/spec/unit/interface_spec.rb
@@ -0,0 +1,147 @@
+require 'puppet/faces'
+require 'puppet/interface'
+
+describe Puppet::Interface do
+ subject { Puppet::Interface }
+
+ before :all do
+ @faces = Puppet::Interface::FaceCollection.instance_variable_get("@faces").dup
+ end
+
+ before :each do
+ Puppet::Interface::FaceCollection.instance_variable_get("@faces").clear
+ end
+
+ after :all do
+ Puppet::Interface::FaceCollection.instance_variable_set("@faces", @faces)
+ end
+
+ describe "#define" do
+ it "should register the face" do
+ face = subject.define(:face_test_register, '0.0.1')
+ face.should == subject[:face_test_register, '0.0.1']
+ end
+
+ it "should load actions" do
+ subject.any_instance.expects(:load_actions)
+ subject.define(:face_test_load_actions, '0.0.1')
+ end
+
+ it "should require a version number" do
+ expect { subject.define(:no_version) }.should raise_error ArgumentError
+ end
+ end
+
+ describe "#initialize" do
+ it "should require a version number" do
+ expect { subject.new(:no_version) }.should raise_error ArgumentError
+ end
+
+ it "should require a valid version number" do
+ expect { subject.new(:bad_version, 'Rasins') }.
+ should raise_error ArgumentError
+ end
+
+ it "should instance-eval any provided block" do
+ face = subject.new(:face_test_block, '0.0.1') do
+ action(:something) do
+ when_invoked { "foo" }
+ end
+ end
+
+ face.something.should == "foo"
+ end
+ end
+
+ it "should have a name" do
+ subject.new(:me, '0.0.1').name.should == :me
+ end
+
+ it "should stringify with its own name" 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)
+ end
+
+ it "should try to require faces that are not known" do
+ pending "mocking require causes random stack overflow"
+ subject::FaceCollection.expects(:require).with "puppet/faces/foo"
+ subject[:foo, '0.0.1']
+ end
+
+ it "should be able to load all actions in all search paths"
+
+
+ it_should_behave_like "things that declare options" do
+ def add_options_to(&block)
+ subject.new(:with_options, '0.0.1', &block)
+ end
+ end
+
+ describe "with face-level options" do
+ it "should not return any action-level options" do
+ face = subject.new(:with_options, '0.0.1') do
+ option "--foo"
+ option "--bar"
+ action :baz do
+ option "--quux"
+ end
+ end
+ face.options.should =~ [:foo, :bar]
+ end
+
+ it "should fail when a face option duplicates an action option" do
+ expect {
+ subject.new(:action_level_options, '0.0.1') do
+ action :bar do option "--foo" end
+ option "--foo"
+ end
+ }.should raise_error ArgumentError, /Option foo conflicts with existing option foo on/i
+ end
+
+ it "should work when two actions have the same option" do
+ face = subject.new(:with_options, '0.0.1') do
+ action :foo do option "--quux" end
+ action :bar do option "--quux" end
+ end
+
+ face.get_action(:foo).options.should =~ [:quux]
+ face.get_action(:bar).options.should =~ [:quux]
+ end
+ end
+
+ describe "with inherited options" do
+ let :face do
+ parent = Class.new(subject)
+ parent.option("--inherited")
+ face = parent.new(:example, '0.2.1')
+ face.option("--local")
+ face
+ end
+
+ describe "#options" do
+ it "should list inherited options" do
+ face.options.should =~ [:inherited, :local]
+ end
+ end
+
+ describe "#get_option" do
+ it "should return an inherited option object" do
+ face.get_option(:inherited).should be_an_instance_of subject::Option
+ end
+ end
+ end
+end
diff --git a/spec/unit/puppet/provider/README.markdown b/spec/unit/puppet/provider/README.markdown
new file mode 100644
index 000000000..702585021
--- /dev/null
+++ b/spec/unit/puppet/provider/README.markdown
@@ -0,0 +1,4 @@
+Provider Specs
+==============
+
+Define specs for your providers under this directory.
diff --git a/spec/unit/puppet/type/README.markdown b/spec/unit/puppet/type/README.markdown
new file mode 100644
index 000000000..1ee19ac84
--- /dev/null
+++ b/spec/unit/puppet/type/README.markdown
@@ -0,0 +1,4 @@
+Resource Type Specs
+===================
+
+Define specs for your resource types in this directory.