Revision 1ce43f91
Added by Ohad Levy almost 12 years ago
- ID 1ce43f91fee51a7539cd3751799fe0d3d560d743
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
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