Project

General

Profile

« Previous | Next » 

Revision 1ce43f91

Added by Ohad Levy almost 12 years ago

  • ID 1ce43f91fee51a7539cd3751799fe0d3d560d743

Export arguments of parameterized classes

Use puppet/parser for a first class analysis of the class definitions.
Using regexes would have been a nightmare.

Exports a "params" sub-object whose keys are the parameter names and
values are a best-effort convertion from AST leaves to native ruby
types, before being exported as native JSON values.

Compatible with Puppet 2.6 and 2.7, at least.

Tests:
- Manifest must now parse in order to extract classes from it,
hence the fix in the test.
- Functional tests

View differences:

lib/proxy/puppet/puppet_class.rb
require 'puppet/parser'
module Proxy::Puppet
class PuppetClass
......
# scans a given directory and its sub directory for puppet classes
# returns an array of PuppetClass objects.
def scan_directory directory
# Get a Puppet Parser to parse the manifest source
parser = Puppet::Parser::Parser.new Puppet::Node::Environment.new
Dir.glob("#{directory}/*/manifests/**/*.pp").map do |manifest|
scan_manifest File.read(manifest)
scan_manifest File.read(manifest), manifest, parser
end.compact.flatten
end
def scan_manifest manifest
def scan_manifest manifest, filename = '', parser = nil
klasses = []
manifest.each_line do |line|
if line.match(/^\s*class\s+([\w:-]*)/)
klasses << new($1) unless $1 == ""
# Get a Puppet Parser to parse the manifest source
parser ||= Puppet::Parser::Parser.new(Puppet::Node::Environment.new)
already_seen = Set.new parser.known_resource_types.hostclasses.keys
already_seen << '' # Prevent the toplevel "main" class from matching
ast = parser.parse manifest
# Get the parsed representation of the top most objects
hostclasses = ast.respond_to?(:instantiate) ? ast.instantiate('') : ast.hostclasses.values
hostclasses.each do |klass|
# Only look at classes
if klass.type == :hostclass and not already_seen.include? klass.namespace
params = {}
# Get parameters and eventual default values
klass.arguments.each do |name, value|
params[name] = ast_to_value(value) rescue nil
end
klasses << new(klass.namespace, params)
end
end
klasses
rescue => e
puts "Error while parsing #{filename}: #{e}"
klasses
end
private
def ast_to_value value
unless value.class.name.start_with? "Puppet::Parser::AST::"
# Native Ruby types
case value
# Supported with exact JSON equivalent
when NilClass, String, Numeric, Array, Hash, FalseClass, TrueClass
value
when Struct
value.hash
when Enumerable
value.to_a
# Stringified
when Regexp # /(?:stringified)/
"/#{value.to_s}/"
when Symbol # stringified
value.to_s
else
raise TypeError
end
else
# Parser types
case value
# Supported with exact JSON equivalent
when Puppet::Parser::AST::Boolean, Puppet::Parser::AST::String
value.evaluate nil
# Supported with stringification
when Puppet::Parser::AST::Concat
# Note1: only simple content are supported, plus variables whose raw name is taken
# Note2: The variable substitution WON'T be done by Puppet from the ENC YAML output
value.value.map do |v|
case v
when Puppet::Parser::AST::String
v.evaluate nil
when Puppet::Parser::AST::Variable
"${#{v.value}}"
else
raise TypeError
end
end.join rescue nil
when Puppet::Parser::AST::Type
value.value
when Puppet::Parser::AST::Name
(Puppet::Parser::Scope.number?(value.value) or value.value)
when Puppet::Parser::AST::Undef # equivalent of nil
nil
# Depends on content
when Puppet::Parser::AST::ASTArray
value.inject([]) { |arr, v| (arr << ast_to_value(v)) rescue arr }
when Puppet::Parser::AST::ASTHash
Hash[value.value.each.inject([]) { |arr, (k,v)| (arr << [ast_to_value(k), ast_to_value(v)]) rescue arr }]
# Let's see if a raw evaluation works with no scope for any other type
else
if value.respond_to? :evaluate
# Can probably work for: (depending on the actual content)
# - Puppet::Parser::AST::ArithmeticOperator
# - Puppet::Parser::AST::ComparisonOperator
# - Puppet::Parser::AST::BooleanOperator
# - Puppet::Parser::AST::Minus
# - Puppet::Parser::AST::Not
# May work for:
# - Puppet::Parser::AST::InOperator
# - Puppet::Parser::AST::MatchOperator
# - Puppet::Parser::AST::Selector
# Probably won't work for
# - Puppet::Parser::AST::Variable
# - Puppet::Parser::AST::HashOrArrayAccess
# - Puppet::Parser::AST::ResourceReference
# - Puppet::Parser::AST::Function
value.evaluate nil
else
raise TypeError
end
end
end
end
end
def initialize name
@klass = name || raise("Must provide puppet class name")
def initialize name, params = {}
@klass = name || raise("Must provide puppet class name")
@params = params
end
def to_s
......
has_module?(klass) ? klass[(klass.index("::")+2)..-1] : klass
end
attr_reader :params
private
attr_reader :klass
lib/puppet_api.rb
begin
env = Proxy::Puppet::Environment.find(params[:environment])
log_halt 404, "Not found" unless env
env.classes.map{|k| {k.to_s => { :name => k.name, :module => k.module} } }.to_json
env.classes.map{|k| {k.to_s => { :name => k.name, :module => k.module, :params => k.params} } }.to_json
rescue => e
log_halt 406, "Failed to show puppet classes: #{e}"
end
end
end
end
test/puppet_class_test.rb
class PuppetClassTest < Test::Unit::TestCase
def setup
Puppet::Node::Environment.clear
end
def test_should_have_a_logger
assert_respond_to Proxy::Puppet, :logger
end
......
manifest = <<-EOF
class foreman::install {
include 'x::y'
}
EOF
klasses = Proxy::Puppet::PuppetClass.scan_manifest(manifest)
assert_kind_of Array, klasses
......
klasses = Proxy::Puppet::PuppetClass.scan_manifest(manifest)
assert klasses.empty?
end
def test_should_find_multiple_class_in_a_manifest
def test_should_find_multiple_class_in_a_manifest
manifest = <<-EOF
class foreman::install {
include 'x::y'
......
klasses = Proxy::Puppet::PuppetClass.scan_manifest(manifest)
assert_kind_of Array, klasses
assert_equal 2, klasses.size
klasses.sort! { |k1,k2| k1.name <=> k2.name }
klass = klasses.first
assert_equal "install", klass.name
assert_equal "foreman", klass.module
klass = klasses.last
assert_equal "params", klass.name
assert_equal "foreman", klass.module
end
end
def test_should_scan_a_dir
klasses = Proxy::Puppet::PuppetClass.scan_directory('/tmp/no_such_dir')
......
assert klasses.empty?
end
def test_should_extract_parameters__no_param_parenthesis
manifest = <<-EOF
class foreman::install {
}
EOF
klasses = Proxy::Puppet::PuppetClass.scan_manifest(manifest)
assert_kind_of Array, klasses
assert_equal 1, klasses.size
klass = klasses.first
assert_equal({}, klass.params)
end
def test_should_extract_parameters__empty_param_parenthesis
manifest = <<-EOF
class foreman::install () {
}
EOF
klasses = Proxy::Puppet::PuppetClass.scan_manifest(manifest)
assert_kind_of Array, klasses
assert_equal 1, klasses.size
klass = klasses.first
assert_equal({}, klass.params)
end
def test_should_extract_parameters__single_param_no_value
manifest = <<-EOF
class foreman::install ($mandatory) {
}
EOF
klasses = Proxy::Puppet::PuppetClass.scan_manifest(manifest)
assert_kind_of Array, klasses
assert_equal 1, klasses.size
klass = klasses.first
assert_equal({'mandatory' => nil}, klass.params)
end
def test_should_extract_parameters__type_coverage
# Note that all keys are string in Puppet
manifest = <<-EOF
class foreman::install (
$mandatory,
$emptyString = '',
$emptyStringDq = "",
$string = "foo",
$integer = 42,
$float = 3.14,
$array = ['', "", "foo", 42, 3.14],
$hash = { unquoted => '', "quoted" => "", 42 => "integer", 3.14 => "float", '' => 'empty' },
$complex = { array => ['','foo',42,3.14], hash => {foo=>"bar"}, mixed => [{foo=>bar},{bar=>"baz"}] }
) {
}
EOF
klasses = Proxy::Puppet::PuppetClass.scan_manifest(manifest)
assert_kind_of Array, klasses
assert_equal 1, klasses.size
klass = klasses.first
assert_equal({
'mandatory' => nil,
'emptyString' => '',
'emptyStringDq' => '',
'string' => 'foo',
'integer' => 42,
'float' => 3.14,
'array' => ['', '', 'foo', 42, 3.14],
# All keys must be strings
'hash' => { 'unquoted' => '', 'quoted' => '', '42' => 'integer', '3.14' => 'float', '' => 'empty' },
'complex' => { 'array' => ['','foo',42,3.14], 'hash' => {'foo'=>'bar'}, 'mixed' => [{'foo'=>'bar'},{'bar'=>'baz'}] }
}, klass.params)
end
#TODO add scans to a real puppet directory with modules
end

Also available in: Unified diff