summaryrefslogtreecommitdiffstats
path: root/lib/puppet/string
diff options
context:
space:
mode:
Diffstat (limited to 'lib/puppet/string')
-rw-r--r--lib/puppet/string/action.rb117
-rw-r--r--lib/puppet/string/action_builder.rb11
-rw-r--r--lib/puppet/string/action_manager.rb18
-rw-r--r--lib/puppet/string/catalog.rb4
-rw-r--r--lib/puppet/string/catalog/select.rb2
-rw-r--r--lib/puppet/string/certificate.rb6
-rw-r--r--lib/puppet/string/config.rb1
-rw-r--r--lib/puppet/string/configurer.rb2
-rw-r--r--lib/puppet/string/facts.rb2
-rw-r--r--lib/puppet/string/indirector.rb41
-rw-r--r--lib/puppet/string/option.rb80
-rw-r--r--lib/puppet/string/option_builder.rb25
-rw-r--r--lib/puppet/string/option_manager.rb56
-rw-r--r--lib/puppet/string/report.rb2
14 files changed, 323 insertions, 44 deletions
diff --git a/lib/puppet/string/action.rb b/lib/puppet/string/action.rb
index 4db9e97e2..ee3b2991b 100644
--- a/lib/puppet/string/action.rb
+++ b/lib/puppet/string/action.rb
@@ -1,26 +1,121 @@
+# -*- coding: utf-8 -*-
require 'puppet/string'
+require 'puppet/string/option'
class Puppet::String::Action
attr_reader :name
- def initialize(string, name, attrs = {})
- name = name.to_s
- raise "'#{name}' is an invalid action name" unless name =~ /^[a-z]\w*$/
-
- @string = string
- @name = name
- attrs.each do |k,v| send("#{k}=", v) end
+ def to_s
+ "#{@string}##{@name}"
end
- def invoke(*args, &block)
- @string.method(name).call(*args,&block)
+ def initialize(string, name, attrs = {})
+ raise "#{name.inspect} is an invalid action name" unless name.to_s =~ /^[a-z]\w*$/
+ @string = string
+ @name = name.to_sym
+ @options = {}
+ attrs.each do |k, v| send("#{k}=", v) end
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 string delegate to
+ # the action object for invocation and all.
+ #
+ # It turns out that we have a binding problem to solve: @string 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 = @string.get_action(name)
+ # @action.invoke(arg1, arg2, arg3)
+ #
+ # ...with...
+ #
+ # @action = @string.get_action(name)
+ # @string.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)
+ # @string.send(name, *args, &block)
+ # end
+
def invoke=(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 @string.is_a?(Class)
- @string.define_method(@name, &block)
+ @string.class_eval do eval wrapper, nil, file, line end
+ @string.define_method(internal_name, &block)
else
- @string.meta_def(@name, &block)
+ @string.instance_eval do eval wrapper, nil, file, line end
+ @string.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 = @string.get_option(name) then
+ raise ArgumentError, "Option #{option} conflicts with existing option #{conflict} on #{@string}"
+ 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 + @string.options).sort
+ end
+
+ def get_option(name)
+ @options[name.to_sym] || @string.get_option(name)
end
end
diff --git a/lib/puppet/string/action_builder.rb b/lib/puppet/string/action_builder.rb
index b3db51104..e76044470 100644
--- a/lib/puppet/string/action_builder.rb
+++ b/lib/puppet/string/action_builder.rb
@@ -5,10 +5,8 @@ class Puppet::String::ActionBuilder
attr_reader :action
def self.build(string, name, &block)
- name = name.to_s
- raise "Action '#{name}' must specify a block" unless block
- builder = new(string, name, &block)
- builder.action
+ raise "Action #{name.inspect} must specify a block" unless block
+ new(string, name, &block).action
end
def initialize(string, name, &block)
@@ -24,4 +22,9 @@ class Puppet::String::ActionBuilder
raise "Invoke called on an ActionBuilder with no corresponding Action" unless @action
@action.invoke = block
end
+
+ def option(*declaration, &block)
+ option = Puppet::String::OptionBuilder.build(@action, *declaration, &block)
+ @action.add_option(option)
+ end
end
diff --git a/lib/puppet/string/action_manager.rb b/lib/puppet/string/action_manager.rb
index c29dbf454..7d22a0c52 100644
--- a/lib/puppet/string/action_manager.rb
+++ b/lib/puppet/string/action_manager.rb
@@ -5,20 +5,15 @@ module Puppet::String::ActionManager
# the code to do so.
def action(name, &block)
@actions ||= {}
- name = name.to_s.downcase.to_sym
-
raise "Action #{name} already defined for #{self}" if action?(name)
-
action = Puppet::String::ActionBuilder.build(self, name, &block)
-
- @actions[name] = action
+ @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 ||= {}
- name = name.to_s.downcase.to_sym
raise "Action #{name} already defined for #{self}" if action?(name)
@actions[name] = Puppet::String::Action.new(self, name, :invoke => block)
end
@@ -36,7 +31,16 @@ module Puppet::String::ActionManager
end
def get_action(name)
- @actions[name].dup
+ @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)
diff --git a/lib/puppet/string/catalog.rb b/lib/puppet/string/catalog.rb
index 0ddd83176..c6de47708 100644
--- a/lib/puppet/string/catalog.rb
+++ b/lib/puppet/string/catalog.rb
@@ -2,7 +2,7 @@ require 'puppet/string/indirector'
Puppet::String::Indirector.define(:catalog, '0.0.1') do
action(:apply) do
- invoke do |catalog|
+ invoke do |catalog, options|
report = Puppet::Transaction::Report.new("apply")
report.configuration_version = catalog.version
@@ -23,7 +23,7 @@ Puppet::String::Indirector.define(:catalog, '0.0.1') do
end
action(:download) do
- invoke do |certname,facts|
+ invoke do |certname, facts, options|
Puppet::Resource::Catalog.terminus_class = :rest
facts_to_upload = {:facts_format => :b64_zlib_yaml, :facts => CGI.escape(facts.render(:b64_zlib_yaml))}
catalog = nil
diff --git a/lib/puppet/string/catalog/select.rb b/lib/puppet/string/catalog/select.rb
index 52c77d3ce..a8f4480cd 100644
--- a/lib/puppet/string/catalog/select.rb
+++ b/lib/puppet/string/catalog/select.rb
@@ -1,7 +1,7 @@
# Select and show a list of resources of a given type.
Puppet::String.define(:catalog, '0.0.1') do
action :select do
- invoke do |host,type|
+ invoke do |host, type, options|
catalog = Puppet::Resource::Catalog.indirection.find(host)
catalog.resources.reject { |res| res.type != type }.each { |res| puts res }
diff --git a/lib/puppet/string/certificate.rb b/lib/puppet/string/certificate.rb
index 7b2e5f397..53f731e81 100644
--- a/lib/puppet/string/certificate.rb
+++ b/lib/puppet/string/certificate.rb
@@ -4,7 +4,7 @@ require 'puppet/ssl/host'
Puppet::String::Indirector.define(:certificate, '0.0.1') do
action :generate do
- invoke do |name|
+ invoke do |name, options|
host = Puppet::SSL::Host.new(name)
host.generate_certificate_request
host.certificate_request.class.indirection.save(host.certificate_request)
@@ -12,7 +12,7 @@ Puppet::String::Indirector.define(:certificate, '0.0.1') do
end
action :list do
- invoke do
+ invoke do |options|
Puppet::SSL::Host.indirection.search("*", {
:for => :certificate_request,
}).map { |h| h.inspect }
@@ -20,7 +20,7 @@ Puppet::String::Indirector.define(:certificate, '0.0.1') do
end
action :sign do
- invoke do |name|
+ invoke do |name, options|
Puppet::SSL::Host.indirection.save(Puppet::SSL::Host.new(name))
end
end
diff --git a/lib/puppet/string/config.rb b/lib/puppet/string/config.rb
index ae1a408cf..49a1688fc 100644
--- a/lib/puppet/string/config.rb
+++ b/lib/puppet/string/config.rb
@@ -3,6 +3,7 @@ require 'puppet/string'
Puppet::String.define(:config, '0.0.1') do
action(:print) do
invoke do |*args|
+ options = args.pop
Puppet.settings[:configprint] = args.join(",")
Puppet.settings.print_config_options
nil
diff --git a/lib/puppet/string/configurer.rb b/lib/puppet/string/configurer.rb
index a6ea74b6a..2520d4188 100644
--- a/lib/puppet/string/configurer.rb
+++ b/lib/puppet/string/configurer.rb
@@ -2,7 +2,7 @@ require 'puppet/string'
Puppet::String.define(:configurer, '0.0.1') do
action(:synchronize) do
- invoke do |certname|
+ invoke do |certname, options|
facts = Puppet::String[:facts, '0.0.1'].find(certname)
catalog = Puppet::String[:catalog, '0.0.1'].download(certname, facts)
report = Puppet::String[:catalog, '0.0.1'].apply(catalog)
diff --git a/lib/puppet/string/facts.rb b/lib/puppet/string/facts.rb
index 73acb0df6..31298813b 100644
--- a/lib/puppet/string/facts.rb
+++ b/lib/puppet/string/facts.rb
@@ -6,7 +6,7 @@ Puppet::String::Indirector.define(:facts, '0.0.1') do
# Upload our facts to the server
action(:upload) do
- invoke do |*args|
+ invoke do |options|
Puppet::Node::Facts.indirection.terminus_class = :facter
facts = Puppet::Node::Facts.indirection.find(Puppet[:certname])
Puppet::Node::Facts.indirection.terminus_class = :rest
diff --git a/lib/puppet/string/indirector.rb b/lib/puppet/string/indirector.rb
index 15984e39e..bb081533f 100644
--- a/lib/puppet/string/indirector.rb
+++ b/lib/puppet/string/indirector.rb
@@ -2,6 +2,11 @@ require 'puppet'
require 'puppet/string'
class Puppet::String::Indirector < Puppet::String
+ 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
@@ -10,6 +15,21 @@ class Puppet::String::Indirector < Puppet::String
Puppet::Indirector::Terminus.terminus_classes(indirection.to_sym).collect { |t| t.to_s }.sort
end
+ def call_indirection_method(method, *args)
+ options = args.pop
+ 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
invoke { |*args| call_indirection_method(:destroy, *args) }
end
@@ -29,11 +49,16 @@ class Puppet::String::Indirector < Puppet::String
# Print the configuration for the current terminus class
action :info do
invoke 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
@@ -53,7 +78,8 @@ class Puppet::String::Indirector < Puppet::String
# One usually does.
def indirection
unless @indirection
- Puppet.info("Could not find terminus for #{indirection_name}") unless @indirection = Puppet::Indirector::Indirection.instance(indirection_name)
+ @indirection = Puppet::Indirector::Indirection.instance(indirection_name)
+ @indirection or raise "Could not find terminus for #{indirection_name}"
end
@indirection
end
@@ -62,18 +88,7 @@ class Puppet::String::Indirector < Puppet::String
begin
indirection.terminus_class = from
rescue => detail
- raise "Could not set '#{indirection.name}' terminus to '#{from}' (#{detail}); valid terminus types are #{terminus_classes(indirection.name).join(", ") }"
+ raise "Could not set '#{indirection.name}' terminus to '#{from}' (#{detail}); valid terminus types are #{self.class.terminus_classes(indirection.name).join(", ") }"
end
end
-
- def call_indirection_method(method, *args)
- begin
- result = indirection.send(method, *args)
- rescue => detail
- puts detail.backtrace if Puppet[:trace]
- raise "Could not call '#{method}' on '#{indirection_name}': #{detail}"
- end
-
- result
- end
end
diff --git a/lib/puppet/string/option.rb b/lib/puppet/string/option.rb
new file mode 100644
index 000000000..f499e4b95
--- /dev/null
+++ b/lib/puppet/string/option.rb
@@ -0,0 +1,80 @@
+class Puppet::String::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 strings to name symbols, and vice-versa. This isn't a full
+ # bidirectional transformation though.
+ def to_s
+ @name.to_s.tr('_', '-')
+ end
+
+ def optparse_to_name(declaration)
+ unless found = declaration.match(/^-+([^= ]+)/) or found.length != 1 then
+ raise ArgumentError, "Can't find a name in the declaration #{declaration.inspect}"
+ end
+ name = found.captures.first.sub('[no-]', '').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/string/option_builder.rb b/lib/puppet/string/option_builder.rb
new file mode 100644
index 000000000..da0d213fb
--- /dev/null
+++ b/lib/puppet/string/option_builder.rb
@@ -0,0 +1,25 @@
+require 'puppet/string/option'
+
+class Puppet::String::OptionBuilder
+ attr_reader :option
+
+ def self.build(string, *declaration, &block)
+ new(string, *declaration, &block).option
+ end
+
+ private
+ def initialize(string, *declaration, &block)
+ @string = string
+ @option = Puppet::String::Option.new(string, *declaration)
+ block and instance_eval(&block)
+ @option
+ end
+
+ # Metaprogram the simple DSL from the option class.
+ Puppet::String::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/string/option_manager.rb b/lib/puppet/string/option_manager.rb
new file mode 100644
index 000000000..f952ad4f0
--- /dev/null
+++ b/lib/puppet/string/option_manager.rb
@@ -0,0 +1,56 @@
+require 'puppet/string/option_builder'
+
+module Puppet::String::OptionManager
+ # Declare that this app can take a specific option, and provide
+ # the code to do so.
+ def option(*declaration, &block)
+ add_option Puppet::String::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/lib/puppet/string/report.rb b/lib/puppet/string/report.rb
index 55a008533..5b617e49e 100644
--- a/lib/puppet/string/report.rb
+++ b/lib/puppet/string/report.rb
@@ -2,7 +2,7 @@ require 'puppet/string/indirector'
Puppet::String::Indirector.define(:report, '0.0.1') do
action(:submit) do
- invoke do |report|
+ invoke do |report, options|
begin
Puppet::Transaction::Report.terminus_class = :rest
report.save