summaryrefslogtreecommitdiffstats
path: root/lib/puppet/interface
diff options
context:
space:
mode:
authorMax Martin <max@puppetlabs.com>2011-04-13 17:30:44 -0700
committerMax Martin <max@puppetlabs.com>2011-04-13 17:30:44 -0700
commit3dde838ac992571e13262ea29ba3a0eb8152e753 (patch)
tree90520cf62bfa2f1bb9c992bbfe1bc47ae10471f2 /lib/puppet/interface
parentfe45c2417af580597cd39adec96a30a05a7cd66a (diff)
parent3ab44c7ce01ab86a995deb66228f5be95239c92a (diff)
Merge branch 'next'
* next: (204 commits) Revert "(#6928) Removed --ignoreimport" Updated CHANGELOG for 2.6.8rc1 (#6928) Removed --ignoreimport (#6928) Remove --parseonly (#6928) Add a Parser face with Validate action (#6830) Fix sha1 to digest/sha1 require issue for Ruby 1.9 (#6830) Fix UTF-8 encoding issue for Ruby 1.9 (#6830) Fix string method sub call on a symbol for Ruby 1.9 (#2331) Remove darwinports pkg provider, replace with rewritten macports provider (#7059) handle inherited action binding scope maint: ensure we handle '-foo=' options correctly in faces. (#2150) Fix File const lookup when configuring routes Fixed #7082 - Added system support for groups maint: install erb templates under lib/ maint: clean up the spec test headers in bulk. (#7056) Use 'face' rather than 'faces' in the production code. maint: eliminate deprecated since 2008 code from Puppet. (#6117) Add POST support to indirector requests (#6962) Move option handling into #parse_options, not #preinit. maint: whitespace cleanup for puppet/util/command_line. ...
Diffstat (limited to 'lib/puppet/interface')
-rw-r--r--lib/puppet/interface/action.rb129
-rw-r--r--lib/puppet/interface/action_builder.rb35
-rw-r--r--lib/puppet/interface/action_manager.rb56
-rw-r--r--lib/puppet/interface/face_collection.rb131
-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
7 files changed, 514 insertions, 0 deletions
diff --git a/lib/puppet/interface/action.rb b/lib/puppet/interface/action.rb
new file mode 100644
index 000000000..db338e39e
--- /dev/null
+++ b/lib/puppet/interface/action.rb
@@ -0,0 +1,129 @@
+# -*- 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
+
+ # This is not nice, but it is the easiest way to make us behave like the
+ # Ruby Method object rather than UnboundMethod. Duplication is vaguely
+ # annoying, but at least we are a shallow clone. --daniel 2011-04-12
+ def __dup_and_rebind_to(to)
+ bound_version = self.dup
+ bound_version.instance_variable_set(:@face, to)
+ return bound_version
+ end
+
+ attr_reader :name
+ def to_s() "#{@face}##{@name}" end
+
+ attr_accessor :summary
+
+ # 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..34bb3fa44
--- /dev/null
+++ b/lib/puppet/interface/action_builder.rb
@@ -0,0 +1,35 @@
+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
+
+ def summary(text)
+ @action.summary = text
+ end
+end
diff --git a/lib/puppet/interface/action_manager.rb b/lib/puppet/interface/action_manager.rb
new file mode 100644
index 000000000..d75697afa
--- /dev/null
+++ b/lib/puppet/interface/action_manager.rb
@@ -0,0 +1,56 @@
+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)
+ found = superclass.get_action(name)
+ elsif self.class.respond_to?(:get_action)
+ found = self.class.get_action(name)
+ end
+
+ if found then
+ # This is not the nicest way to make action equivalent to the Ruby
+ # Method object, rather than UnboundMethod, but it will do for now,
+ # and we only have to make this change in *one* place. --daniel 2011-04-12
+ result = @actions[name.to_sym] = found.__dup_and_rebind_to(self)
+ 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..591471d4b
--- /dev/null
+++ b/lib/puppet/interface/face_collection.rb
@@ -0,0 +1,131 @@
+# -*- 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
+ Dir.glob("puppet/face/*.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)
+
+ # Note: be careful not to accidentally create the top level key, either,
+ # because it will result in confusion when people try to enumerate the
+ # list of valid faces later. --daniel 2011-04-11
+ return true if @faces.has_key?(name) and @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/face/#{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/face/#{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
+ #
+ # Note: be careful not to accidentally create the top level key, either,
+ # because it will result in confusion when people try to enumerate the
+ # list of valid faces later. --daniel 2011-04-11
+ return !! (@faces.has_key?(name) and @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..2240b3e4a
--- /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.to_s.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