Revision 69f6b3e5
Added by Dmitri Dolguikh almost 10 years ago
lib/smart_proxy.rb | ||
---|---|---|
require 'tftp/tftp'
|
||
require 'dhcp/dhcp'
|
||
require 'puppetca/puppetca'
|
||
require 'puppet/puppet'
|
||
require 'puppet_proxy/puppet'
|
||
require 'bmc/bmc'
|
||
require 'chef/chef'
|
||
require 'chef_proxy/chef'
|
||
require "realm/realm"
|
||
|
||
def self.version
|
modules/chef/authentication.rb | ||
---|---|---|
module Proxy::Chef
|
||
class Authentication
|
||
require 'chef'
|
||
require 'digest/sha2'
|
||
require 'base64'
|
||
require 'openssl'
|
||
|
||
def verify_signature_request(client_name,signature,body)
|
||
#We need to retrieve node public key
|
||
#to verify signature
|
||
chefurl = Proxy::Chef::Plugin.settings.chef_server_url
|
||
chef_smartproxy_clientname = Proxy::Chef::Plugin.settings.chef_smartproxy_clientname
|
||
key = Proxy::Chef::Plugin.settings.chef_smartproxy_privatekey
|
||
rest = ::Chef::REST.new(chefurl,chef_smartproxy_clientname,key)
|
||
begin
|
||
public_key = OpenSSL::PKey::RSA.new(rest.get_rest("/clients/#{client_name}").public_key)
|
||
rescue Timeout::Error, Errno::EINVAL, Errno::ECONNRESET, EOFError,
|
||
Net::HTTPBadResponse, Net::HTTPHeaderSyntaxError, Net::ProtocolError,
|
||
Errno::ECONNREFUSED, OpenSSL::SSL::SSLError => e
|
||
raise Proxy::Error::Unauthorized, "Failed to authenticate node : "+e.message
|
||
end
|
||
|
||
#signature is base64 encoded
|
||
decoded_signature = Base64.decode64(signature)
|
||
hash_body = Digest::SHA256.hexdigest(body)
|
||
public_key.verify(OpenSSL::Digest::SHA256.new,decoded_signature,hash_body)
|
||
end
|
||
|
||
def authenticated(request, &block)
|
||
content = request.env["rack.input"].read
|
||
|
||
auth = true
|
||
if Proxy::Chef::Plugin.settings.chef_authenticate_nodes
|
||
client_name = request.env['HTTP_X_FOREMAN_CLIENT']
|
||
signature = request.env['HTTP_X_FOREMAN_SIGNATURE']
|
||
|
||
raise Proxy::Error::Unauthorized, "Failed to authenticate node #{client_name}. Missing some headers" if client_name.nil? or signature.nil?
|
||
auth = verify_signature_request(client_name,signature,content)
|
||
end
|
||
|
||
if auth
|
||
raise Proxy::Error::BadRequest, "Body is empty for node #{client_name}" if content.nil?
|
||
block.call(content)
|
||
else
|
||
raise Proxy::Error::Unauthorized, "Failed to authenticate node #{client_name}"
|
||
end
|
||
end
|
||
end
|
||
end
|
modules/chef/chef.rb | ||
---|---|---|
require 'chef/chef_plugin'
|
||
module Proxy::Chef; end
|
modules/chef/chef_api.rb | ||
---|---|---|
require 'chef/chef_request'
|
||
require 'chef/authentication'
|
||
|
||
module Proxy::Chef
|
||
class Api < ::Sinatra::Base
|
||
helpers ::Proxy::Helpers
|
||
|
||
error Proxy::Error::BadRequest do
|
||
log_halt(400, "Bad request : " + env['sinatra.error'].message )
|
||
end
|
||
|
||
error Proxy::Error::Unauthorized do
|
||
log_halt(401, "Unauthorized : " + env['sinatra.error'].message )
|
||
end
|
||
|
||
post "/hosts/facts" do
|
||
Proxy::Chef::Authentication.new.authenticated(request) do |content|
|
||
Proxy::Chef::Facts.new.post_facts(content)
|
||
end
|
||
end
|
||
|
||
post "/reports" do
|
||
Proxy::Chef::Authentication.new.authenticated(request) do |content|
|
||
Proxy::Chef::Reports.new.post_report(content)
|
||
end
|
||
end
|
||
end
|
||
end
|
modules/chef/chef_plugin.rb | ||
---|---|---|
module Proxy::Chef
|
||
class Plugin < Proxy::Plugin
|
||
http_rackup_path File.expand_path("http_config.ru", File.expand_path("../", __FILE__))
|
||
https_rackup_path File.expand_path("http_config.ru", File.expand_path("../", __FILE__))
|
||
|
||
settings_file "chef.yml"
|
||
plugin :chefproxy, ::Proxy::VERSION
|
||
end
|
||
end
|
modules/chef/chef_request.rb | ||
---|---|---|
require 'net/http'
|
||
require 'net/https'
|
||
require 'uri'
|
||
|
||
# TODO: need settings validation on startup, otherwise we get a 500 error due to missing/wrong config settings when api is accessed
|
||
# TODO: shouldn't SSL settings use ssl_certificate, ssl_ca_file, and ssl_private_key as opposed to foreman_ssl_ca, foreman_ssl_cert, and foreman_ssl_key?
|
||
|
||
module Proxy::Chef
|
||
class ForemanRequest
|
||
def send_request(path, body)
|
||
uri = URI.parse(Proxy::SETTINGS.foreman_url.to_s)
|
||
http = Net::HTTP.new(uri.host, uri.port)
|
||
http.use_ssl = uri.scheme == 'https'
|
||
http.verify_mode = OpenSSL::SSL::VERIFY_NONE
|
||
|
||
if http.use_ssl?
|
||
if Proxy::Chef::Plugin.settings.foreman_ssl_ca && !Proxy::Chef::Plugin.settings.foreman_ssl_ca.to_s.empty?
|
||
http.ca_file = Proxy::Chef::Plugin.settings.foreman_ssl_ca
|
||
http.verify_mode = OpenSSL::SSL::VERIFY_PEER
|
||
end
|
||
|
||
if Proxy::Chef::Plugin.settings.foreman_ssl_cert && !Proxy::Chef::Plugin.settings.foreman_ssl_cert.to_s.empty? && Proxy::Chef::Plugin.settings.foreman_ssl_key && !Proxy::Chef::Plugin.settings.foreman_ssl_key.to_s.empty?
|
||
http.cert = OpenSSL::X509::Certificate.new(File.read(Proxy::Chef::Plugin.settings.foreman_ssl_cert))
|
||
http.key = OpenSSL::PKey::RSA.new(File.read(Proxy::Chef::Plugin.settings.foreman_ssl_key), nil)
|
||
end
|
||
end
|
||
|
||
path = [uri.path, path].join('/') unless uri.path.empty?
|
||
req = Net::HTTP::Post.new(URI.join(uri.to_s, path).path)
|
||
req.add_field('Accept', 'application/json,version=2')
|
||
req.content_type = 'application/json'
|
||
req.body = body
|
||
response = http.request(req)
|
||
end
|
||
end
|
||
|
||
class Facts < ForemanRequest
|
||
def post_facts(facts)
|
||
send_request('/api/hosts/facts',facts)
|
||
end
|
||
end
|
||
|
||
class Reports < ForemanRequest
|
||
def post_report(report)
|
||
send_request('/api/reports',report)
|
||
end
|
||
end
|
||
end
|
modules/chef/http_config.ru | ||
---|---|---|
require 'chef/chef_api'
|
||
|
||
map "/api" do
|
||
run Proxy::Chef::Api
|
||
end
|
modules/chef_proxy/authentication.rb | ||
---|---|---|
module Proxy::Chef
|
||
class Authentication
|
||
require 'chef'
|
||
require 'digest/sha2'
|
||
require 'base64'
|
||
require 'openssl'
|
||
|
||
def verify_signature_request(client_name,signature,body)
|
||
#We need to retrieve node public key
|
||
#to verify signature
|
||
chefurl = Proxy::Chef::Plugin.settings.chef_server_url
|
||
chef_smartproxy_clientname = Proxy::Chef::Plugin.settings.chef_smartproxy_clientname
|
||
key = Proxy::Chef::Plugin.settings.chef_smartproxy_privatekey
|
||
rest = ::Chef::REST.new(chefurl,chef_smartproxy_clientname,key)
|
||
begin
|
||
public_key = OpenSSL::PKey::RSA.new(rest.get_rest("/clients/#{client_name}").public_key)
|
||
rescue Timeout::Error, Errno::EINVAL, Errno::ECONNRESET, EOFError,
|
||
Net::HTTPBadResponse, Net::HTTPHeaderSyntaxError, Net::ProtocolError,
|
||
Errno::ECONNREFUSED, OpenSSL::SSL::SSLError => e
|
||
raise Proxy::Error::Unauthorized, "Failed to authenticate node : "+e.message
|
||
end
|
||
|
||
#signature is base64 encoded
|
||
decoded_signature = Base64.decode64(signature)
|
||
hash_body = Digest::SHA256.hexdigest(body)
|
||
public_key.verify(OpenSSL::Digest::SHA256.new,decoded_signature,hash_body)
|
||
end
|
||
|
||
def authenticated(request, &block)
|
||
content = request.env["rack.input"].read
|
||
|
||
auth = true
|
||
if Proxy::Chef::Plugin.settings.chef_authenticate_nodes
|
||
client_name = request.env['HTTP_X_FOREMAN_CLIENT']
|
||
signature = request.env['HTTP_X_FOREMAN_SIGNATURE']
|
||
|
||
raise Proxy::Error::Unauthorized, "Failed to authenticate node #{client_name}. Missing some headers" if client_name.nil? or signature.nil?
|
||
auth = verify_signature_request(client_name,signature,content)
|
||
end
|
||
|
||
if auth
|
||
raise Proxy::Error::BadRequest, "Body is empty for node #{client_name}" if content.nil?
|
||
block.call(content)
|
||
else
|
||
raise Proxy::Error::Unauthorized, "Failed to authenticate node #{client_name}"
|
||
end
|
||
end
|
||
end
|
||
end
|
modules/chef_proxy/chef.rb | ||
---|---|---|
require 'chef_proxy/chef_plugin'
|
||
module Proxy::Chef; end
|
modules/chef_proxy/chef_api.rb | ||
---|---|---|
require 'chef_proxy/chef_request'
|
||
require 'chef_proxy/authentication'
|
||
|
||
module Proxy::Chef
|
||
class Api < ::Sinatra::Base
|
||
helpers ::Proxy::Helpers
|
||
|
||
error Proxy::Error::BadRequest do
|
||
log_halt(400, "Bad request : " + env['sinatra.error'].message )
|
||
end
|
||
|
||
error Proxy::Error::Unauthorized do
|
||
log_halt(401, "Unauthorized : " + env['sinatra.error'].message )
|
||
end
|
||
|
||
post "/hosts/facts" do
|
||
Proxy::Chef::Authentication.new.authenticated(request) do |content|
|
||
Proxy::Chef::Facts.new.post_facts(content)
|
||
end
|
||
end
|
||
|
||
post "/reports" do
|
||
Proxy::Chef::Authentication.new.authenticated(request) do |content|
|
||
Proxy::Chef::Reports.new.post_report(content)
|
||
end
|
||
end
|
||
end
|
||
end
|
modules/chef_proxy/chef_plugin.rb | ||
---|---|---|
module Proxy::Chef
|
||
class Plugin < Proxy::Plugin
|
||
http_rackup_path File.expand_path("http_config.ru", File.expand_path("../", __FILE__))
|
||
https_rackup_path File.expand_path("http_config.ru", File.expand_path("../", __FILE__))
|
||
|
||
settings_file "chef.yml"
|
||
plugin :chefproxy, ::Proxy::VERSION
|
||
end
|
||
end
|
modules/chef_proxy/chef_request.rb | ||
---|---|---|
require 'net/http'
|
||
require 'net/https'
|
||
require 'uri'
|
||
|
||
# TODO: need settings validation on startup, otherwise we get a 500 error due to missing/wrong config settings when api is accessed
|
||
# TODO: shouldn't SSL settings use ssl_certificate, ssl_ca_file, and ssl_private_key as opposed to foreman_ssl_ca, foreman_ssl_cert, and foreman_ssl_key?
|
||
|
||
module Proxy::Chef
|
||
class ForemanRequest
|
||
def send_request(path, body)
|
||
uri = URI.parse(Proxy::SETTINGS.foreman_url.to_s)
|
||
http = Net::HTTP.new(uri.host, uri.port)
|
||
http.use_ssl = uri.scheme == 'https'
|
||
http.verify_mode = OpenSSL::SSL::VERIFY_NONE
|
||
|
||
if http.use_ssl?
|
||
if Proxy::Chef::Plugin.settings.foreman_ssl_ca && !Proxy::Chef::Plugin.settings.foreman_ssl_ca.to_s.empty?
|
||
http.ca_file = Proxy::Chef::Plugin.settings.foreman_ssl_ca
|
||
http.verify_mode = OpenSSL::SSL::VERIFY_PEER
|
||
end
|
||
|
||
if Proxy::Chef::Plugin.settings.foreman_ssl_cert && !Proxy::Chef::Plugin.settings.foreman_ssl_cert.to_s.empty? && Proxy::Chef::Plugin.settings.foreman_ssl_key && !Proxy::Chef::Plugin.settings.foreman_ssl_key.to_s.empty?
|
||
http.cert = OpenSSL::X509::Certificate.new(File.read(Proxy::Chef::Plugin.settings.foreman_ssl_cert))
|
||
http.key = OpenSSL::PKey::RSA.new(File.read(Proxy::Chef::Plugin.settings.foreman_ssl_key), nil)
|
||
end
|
||
end
|
||
|
||
path = [uri.path, path].join('/') unless uri.path.empty?
|
||
req = Net::HTTP::Post.new(URI.join(uri.to_s, path).path)
|
||
req.add_field('Accept', 'application/json,version=2')
|
||
req.content_type = 'application/json'
|
||
req.body = body
|
||
response = http.request(req)
|
||
end
|
||
end
|
||
|
||
class Facts < ForemanRequest
|
||
def post_facts(facts)
|
||
send_request('/api/hosts/facts',facts)
|
||
end
|
||
end
|
||
|
||
class Reports < ForemanRequest
|
||
def post_report(report)
|
||
send_request('/api/reports',report)
|
||
end
|
||
end
|
||
end
|
modules/chef_proxy/http_config.ru | ||
---|---|---|
require 'chef_proxy/chef_api'
|
||
|
||
map "/api" do
|
||
run Proxy::Chef::Api
|
||
end
|
modules/puppet/class_scanner.rb | ||
---|---|---|
require 'puppet/puppet_class'
|
||
|
||
module Proxy::Puppet
|
||
class ClassScanner
|
||
class << self
|
||
# scans a given directory and its sub directory for puppet classes
|
||
# returns an array of PuppetClass objects.
|
||
def scan_directory directory
|
||
|
||
parser = Puppet::Parser::Parser.new Puppet::Node::Environment.new
|
||
|
||
Dir.glob("#{directory}/*/manifests/**/*.pp").map do |filename|
|
||
scan_manifest File.read(filename), parser, filename
|
||
end.compact.flatten
|
||
end
|
||
|
||
def scan_manifest manifest, parser, filename = ''
|
||
klasses = []
|
||
|
||
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 << PuppetClass.new(klass.namespace, params)
|
||
end
|
||
end
|
||
klasses
|
||
rescue => e
|
||
puts "Error while parsing #{filename}: #{e}"
|
||
klasses
|
||
end
|
||
|
||
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
|
||
# This is the case when two params are concatenated together ,e.g. "param_${key}_something"
|
||
# 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::Variable
|
||
"${#{value}}"
|
||
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, but optional
|
||
""
|
||
# 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 }]
|
||
when Puppet::Parser::AST::Function
|
||
value.to_s
|
||
# 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
|
||
value.evaluate nil
|
||
else
|
||
raise TypeError
|
||
end
|
||
end
|
||
end
|
||
end
|
||
end
|
||
end
|
||
end
|
modules/puppet/class_scanner_eparser.rb | ||
---|---|---|
require 'puppet/puppet_class'
|
||
require 'puppet'
|
||
|
||
if Puppet::PUPPETVERSION.to_f >= 3.2
|
||
require 'puppet/pops'
|
||
|
||
module Proxy::Puppet
|
||
class ClassScannerEParser
|
||
class << self
|
||
# scans a given directory and its sub directory for puppet classes
|
||
# returns an array of PuppetClass objects.
|
||
def scan_directory directory
|
||
|
||
parser = Puppet::Pops::Parser::Parser.new
|
||
Dir.glob("#{directory}/*/manifests/**/*.pp").map do |filename|
|
||
scan_manifest File.read(filename), parser, filename
|
||
end.compact.flatten
|
||
end
|
||
|
||
def scan_manifest manifest, parser, filename = ''
|
||
klasses = []
|
||
|
||
already_seen = Set.new
|
||
already_seen << '' # Prevent the toplevel "main" class from matching
|
||
ast = parser.parse_string manifest
|
||
class_finder = ClassFinder.new
|
||
|
||
class_finder.do_find ast.current
|
||
klasses = class_finder.klasses
|
||
|
||
klasses
|
||
rescue => e
|
||
puts "Error while parsing #{filename}: #{e}"
|
||
klasses
|
||
end
|
||
end
|
||
end
|
||
|
||
class ClassFinder
|
||
|
||
@@finder_visitor ||= Puppet::Pops::Visitor.new(nil,'find',0,0)
|
||
|
||
attr_reader :klasses
|
||
|
||
def initialize
|
||
@klasses = []
|
||
end
|
||
|
||
def do_find ast
|
||
@@finder_visitor.visit_this(self, ast)
|
||
end
|
||
|
||
def find_HostClassDefinition o
|
||
params = {}
|
||
o.parameters.each do |param|
|
||
params[param.name] = ast_to_value_new(param.value) rescue nil
|
||
end
|
||
@klasses << PuppetClass.new(o.name, params)
|
||
|
||
if o.body
|
||
do_find(o.body)
|
||
end
|
||
end
|
||
|
||
def find_BlockExpression o
|
||
o.statements.collect {|x| do_find(x) }
|
||
end
|
||
|
||
def find_CallNamedFunctionExpression o
|
||
if o.lambda
|
||
do_find(o.lambda)
|
||
end
|
||
end
|
||
|
||
def find_Program o
|
||
if o.body
|
||
do_find(o.body)
|
||
end
|
||
end
|
||
|
||
def find_Object o
|
||
#puts "Unhandled object:#{o}"
|
||
end
|
||
|
||
def ast_to_value_new value
|
||
unless value.class.name.start_with? "Puppet::Pops::Model::"
|
||
# 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::Pops::Model::BooleanExpression, Puppet::Pops::Model::LiteralString, Puppet::Pops::Model::LiteralNumber, Puppet::Pops::Model::QualifiedName
|
||
(Puppet::Parser::Scope.number?(value.value) or value.value)
|
||
when Puppet::Pops::Model::UnaryMinusExpression
|
||
- ast_to_value_new(value.expr)
|
||
when Puppet::Pops::Model::ArithmeticExpression
|
||
ast_to_value_new(value.left_expr).send(value.operator, ast_to_value_new(value.right_expr))
|
||
# Supported with stringification
|
||
when Puppet::Pops::Model::ConcatenatedString
|
||
# This is the case when two params are concatenated together ,e.g. "param_${key}_something"
|
||
# 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.segments.map {|v| ast_to_value_new v}.join rescue nil
|
||
when Puppet::Pops::Model::TextExpression
|
||
ast_to_value_new value.expr
|
||
when Puppet::Pops::Model::VariableExpression
|
||
"${#{ast_to_value_new value.expr}}"
|
||
when (Puppet::Pops::Model::TypeReference rescue nil)
|
||
value.value
|
||
when Puppet::Pops::Model::LiteralUndef
|
||
""
|
||
# Depends on content
|
||
when Puppet::Pops::Model::LiteralList
|
||
value.values.inject([]) { |arr, v| (arr << ast_to_value_new(v)) rescue arr }
|
||
when Puppet::Pops::Model::LiteralHash
|
||
# Note that all keys are string in Puppet
|
||
Hash[value.entries.inject([]) { |arr, entry| (arr << [ast_to_value_new(entry.key).to_s, ast_to_value_new(entry.value)]) rescue arr }]
|
||
when Puppet::Pops::Model::NamedFunctionDefinition
|
||
value.to_s
|
||
# Let's see if a raw evaluation works with no scope for any other type
|
||
else
|
||
if value.respond_to? :value
|
||
value.value
|
||
elsif value.respond_to? :expr
|
||
ast_to_value_new value.expr
|
||
else
|
||
raise TypeError
|
||
end
|
||
end
|
||
end
|
||
end
|
||
end
|
||
end
|
||
end
|
modules/puppet/config_reader.rb | ||
---|---|---|
require 'augeas'
|
||
|
||
module Proxy::Puppet
|
||
class ConfigReader
|
||
attr_reader :config
|
||
|
||
def initialize(config)
|
||
raise "Puppet config at #{config} was not found" unless File.exist?(config)
|
||
@config = config
|
||
end
|
||
|
||
def get
|
||
return @config_hash if @config_hash
|
||
|
||
aug = nil
|
||
begin
|
||
aug = ::Augeas::open(nil, nil, ::Augeas::NO_MODL_AUTOLOAD)
|
||
aug.set('/augeas/load/Puppet/lens', 'Puppet.lns')
|
||
aug.set('/augeas/load/Puppet/incl', config)
|
||
aug.load
|
||
|
||
@config_hash = Hash.new { |h,k| h[k] = {} }
|
||
aug.match("/files#{config}/*/*[label() != '#comment']").each do |path|
|
||
(section, key) = path.split('/')[-2..-1].map(&:to_sym)
|
||
@config_hash[section][key] = aug.get(path)
|
||
end
|
||
ensure
|
||
aug.close if aug
|
||
end
|
||
@config_hash
|
||
end
|
||
end
|
||
end
|
modules/puppet/customrun.rb | ||
---|---|---|
require 'puppet/runner'
|
||
|
||
class Proxy::Puppet::CustomRun < Proxy::Puppet::Runner
|
||
def run
|
||
cmd = Proxy::Puppet::Plugin.settings.customrun_cmd
|
||
unless File.exists?( cmd )
|
||
logger.warn "#{cmd} not found."
|
||
return false
|
||
end
|
||
|
||
shell_command( [ escape_for_shell(cmd), Proxy::Puppet::Plugin.settings.customrun_args, shell_escaped_nodes ] )
|
||
end
|
||
end
|
modules/puppet/environment.rb | ||
---|---|---|
require 'puppet'
|
||
require 'puppet/initializer'
|
||
require 'puppet/config_reader'
|
||
require 'puppet/puppet_class'
|
||
|
||
class Proxy::Puppet::Environment
|
||
extend Proxy::Log
|
||
|
||
class << self
|
||
# return a list of all puppet environments
|
||
def all
|
||
puppet_environments.map { |env, path| new(:name => env, :paths => path.split(":")) }
|
||
end
|
||
|
||
def find name
|
||
all.each { |e| return e if e.name == name }
|
||
nil
|
||
end
|
||
|
||
private
|
||
|
||
def puppet_environments
|
||
Proxy::Puppet::Initializer.load
|
||
conf = Proxy::Puppet::ConfigReader.new(Proxy::Puppet::Initializer.config).get
|
||
|
||
env = { }
|
||
# query for the environments variable
|
||
if conf[:main][:environments].nil?
|
||
# 0.25 and newer doesn't require the environments variable anymore, scanning for modulepath
|
||
conf.keys.each { |p| env[p] = conf[p][:modulepath] unless conf[p][:modulepath].nil? }
|
||
# puppetmaster section "might" also returns the modulepath
|
||
env.delete :main
|
||
env.delete :puppetmasterd if env.size > 1
|
||
else
|
||
conf[:main][:environments].split(",").each { |e| env[e.to_sym] = conf[e.to_sym][:modulepath] unless conf[e.to_sym][:modulepath].nil? }
|
||
end
|
||
if env.values.compact.size == 0
|
||
# fall back to defaults - we probably don't use environments
|
||
env[:production] = conf[:main][:modulepath] || conf[:master][:modulepath] || '/etc/puppet/modules'
|
||
logger.warn "No environments found - falling back to defaults (production - #{env[:production]})"
|
||
end
|
||
if env.size == 1 and env.keys.first == :master and !env.values.first.include?('$environment')
|
||
# If we only have an entry in [master] it should really be called production
|
||
logger.warn "Re-writing single 'master' environment as 'production'"
|
||
env[:production] = env[:master]
|
||
env.delete :master
|
||
end
|
||
|
||
new_env = env.clone
|
||
# are we using dynamic puppet environments?
|
||
env.each do|environment, modulepath|
|
||
next unless modulepath
|
||
|
||
# expand $confdir if defined and used in modulepath
|
||
if modulepath.include?("$confdir")
|
||
if conf[:main][:confdir]
|
||
modulepath.gsub!("$confdir", conf[:main][:confdir])
|
||
else
|
||
# /etc/puppet is the default if $confdir is not defined
|
||
modulepath.gsub!("$confdir", "/etc/puppet")
|
||
end
|
||
end
|
||
|
||
# parting modulepath into static and dynamic paths
|
||
staticpath = modulepath.split(":")
|
||
dynamicpath = modulepath.split(":")
|
||
|
||
modulepath.split(":").each do |base_dir|
|
||
if base_dir.include?("$environment")
|
||
# remove this entry from the static paths
|
||
staticpath.delete base_dir
|
||
else
|
||
# remove this entry from the dynamic paths
|
||
dynamicpath.delete base_dir
|
||
end
|
||
end
|
||
|
||
# remove or add static environment
|
||
if staticpath.empty?
|
||
new_env.delete environment
|
||
else
|
||
new_env[environment] = staticpath.join(':')
|
||
end
|
||
|
||
# create dynamic environments and modulepaths (array of hash)
|
||
unless dynamicpath.empty?
|
||
temp_environment = []
|
||
|
||
dynamicpath.each do |base_dir|
|
||
# Dynamic environments - get every directory under the modulepath
|
||
Dir.glob("#{base_dir.gsub(/\$environment(.*)/,"/")}/*").grep(/\/[A-Za-z0-9_]+$/) do |dir|
|
||
e = dir.split("/").last
|
||
temp_environment.push({e => base_dir.gsub("$environment", e)})
|
||
end
|
||
end
|
||
|
||
# group array of hashes, join values (modulepaths) and create dynamic environment => modulepath
|
||
dynamic_environment = temp_environment.group_by(&:keys).map{|k, v| {k.first => v.flatten.map(&:values).join(':')}}
|
||
|
||
dynamic_environment.each do |h|
|
||
h.each do |k,v|
|
||
new_env[k.to_sym] = v
|
||
end
|
||
end
|
||
end
|
||
end
|
||
|
||
new_env.reject { |k, v| k.nil? or v.nil? }
|
||
end
|
||
end
|
||
|
||
attr_reader :name, :paths
|
||
|
||
def initialize args
|
||
@name = args[:name].to_s || raise("Must provide a name")
|
||
@paths= args[:paths] || raise("Must provide a path")
|
||
end
|
||
|
||
def to_s
|
||
name
|
||
end
|
||
|
||
def classes
|
||
::Proxy::Puppet::Initializer.load
|
||
conf = ::Proxy::Puppet::ConfigReader.new(::Proxy::Puppet::Initializer.config).get
|
||
eparser = (conf[:main] && conf[:main][:parser] == 'future') || (conf[:master] && conf[:master][:parser] == 'future')
|
||
|
||
paths.map {|path| ::Proxy::Puppet::PuppetClass.scan_directory path, eparser}.flatten
|
||
end
|
||
end
|
modules/puppet/http_config.ru | ||
---|---|---|
require 'puppet/puppet_api'
|
||
|
||
map "/puppet" do
|
||
run Proxy::Puppet::Api
|
||
end
|
modules/puppet/initializer.rb | ||
---|---|---|
require 'puppet'
|
||
|
||
module Proxy::Puppet
|
||
class Initializer
|
||
extend Proxy::Log
|
||
|
||
class << self
|
||
def load
|
||
Puppet.clear
|
||
if Puppet::PUPPETVERSION.to_i >= 3
|
||
# Used on Puppet 3.0, private method that clears the "initialized or
|
||
# not" state too, so a full config reload takes place and we pick up
|
||
# new environments
|
||
Puppet.settings.send(:clear_everything_for_tests)
|
||
end
|
||
|
||
Puppet[:config] = config
|
||
raise("Cannot read #{File.expand_path(config)}") unless File.exist?(config)
|
||
logger.info "Initializing from Puppet config file: #{config}"
|
||
|
||
if Puppet::PUPPETVERSION.to_i >= 3
|
||
Puppet.initialize_settings
|
||
else
|
||
Puppet.parse_config
|
||
end
|
||
|
||
# Don't follow imports, the proxy scans for .pp files itself
|
||
Puppet[:ignoreimport] = true
|
||
end
|
||
|
||
def config
|
||
Proxy::Puppet::Plugin.settings.puppet_conf || File.join(Proxy::Puppet::Plugin.settings.puppetdir, 'puppet.conf')
|
||
end
|
||
end
|
||
end
|
||
end
|
modules/puppet/mcollective.rb | ||
---|---|---|
require 'puppet/runner'
|
||
|
||
class Proxy::Puppet::MCollective < Proxy::Puppet::Runner
|
||
def run
|
||
cmd = []
|
||
cmd.push(which("sudo"))
|
||
|
||
if Proxy::Puppet::Plugin.settings.puppet_user
|
||
cmd.push("-u", Proxy::Puppet::Plugin.settings.puppet_user)
|
||
end
|
||
|
||
cmd.push(which("mco", "/opt/puppet/bin"))
|
||
|
||
if cmd.include?(false)
|
||
logger.warn "sudo or the mco binary is missing."
|
||
return false
|
||
end
|
||
|
||
shell_command(cmd + ["puppet", "runonce", "-I"] + shell_escaped_nodes)
|
||
end
|
||
end
|
modules/puppet/puppet.rb | ||
---|---|---|
require 'puppet/puppet_plugin'
|
||
module Proxy::Puppet; end
|
modules/puppet/puppet_api.rb | ||
---|---|---|
require 'puppet/environment'
|
||
|
||
class Proxy::Puppet::Api < ::Sinatra::Base
|
||
helpers ::Proxy::Helpers
|
||
|
||
def puppet_setup(opts = {})
|
||
raise "Smart Proxy is not configured to support Puppet runs" unless Proxy::Puppet::Plugin.settings.enabled
|
||
case Proxy::Puppet::Plugin.settings.puppet_provider
|
||
when "puppetrun"
|
||
require 'proxy/puppet/puppetrun'
|
||
@server = Proxy::Puppet::PuppetRun.new(opts)
|
||
when "mcollective"
|
||
require 'proxy/puppet/mcollective'
|
||
@server = Proxy::Puppet::MCollective.new(opts)
|
||
when "puppetssh"
|
||
require 'proxy/puppet/puppet_ssh'
|
||
@server = Proxy::Puppet::PuppetSSH.new(opts)
|
||
when "salt"
|
||
require 'proxy/puppet/salt'
|
||
@server = Proxy::Puppet::Salt.new(opts)
|
||
when "customrun"
|
||
require 'proxy/puppet/customrun'
|
||
@server = Proxy::Puppet::CustomRun.new(opts)
|
||
else
|
||
log_halt 400, "Unrecognized or missing puppet_provider: #{Proxy::Puppet::Plugin.settings.puppet_provider || "MISSING"}"
|
||
end
|
||
rescue => e
|
||
log_halt 400, e
|
||
end
|
||
|
||
post "/run" do
|
||
nodes = params[:nodes]
|
||
begin
|
||
log_halt 400, "Failed puppet run: No nodes defined" unless nodes
|
||
log_halt 500, "Failed puppet run: Check Log files" unless puppet_setup(:nodes => [nodes].flatten).run
|
||
rescue => e
|
||
log_halt 500, "Failed puppet run: #{e}"
|
||
end
|
||
end
|
||
|
||
get "/environments" do
|
||
content_type :json
|
||
begin
|
||
Proxy::Puppet::Environment.all.map(&:name).to_json
|
||
rescue => e
|
||
log_halt 406, "Failed to list puppet environments: #{e}"
|
||
end
|
||
end
|
||
|
||
get "/environments/:environment" do
|
||
content_type :json
|
||
begin
|
||
env = Proxy::Puppet::Environment.find(params[:environment])
|
||
log_halt 404, "Not found" unless env
|
||
{:name => env.name, :paths => env.paths}.to_json
|
||
rescue => e
|
||
log_halt 406, "Failed to show puppet environment: #{e}"
|
||
end
|
||
end
|
||
|
||
get "/environments/:environment/classes" do
|
||
content_type :json
|
||
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, :params => k.params} } }.to_json
|
||
rescue => e
|
||
log_halt 406, "Failed to show puppet classes: #{e}"
|
||
end
|
||
end
|
||
end
|
modules/puppet/puppet_class.rb | ||
---|---|---|
require 'puppet/class_scanner'
|
||
require 'puppet/class_scanner_eparser'
|
||
|
||
class Proxy::Puppet::PuppetClass
|
||
class << self
|
||
# scans a given directory and its sub directory for puppet classes
|
||
# returns an array of PuppetClass objects.
|
||
def scan_directory directory, eparser = false
|
||
# Get a Puppet Parser to parse the manifest source
|
||
Proxy::Puppet::Initializer.load
|
||
|
||
if eparser
|
||
Proxy::Puppet::ClassScannerEParser.scan_directory directory
|
||
else
|
||
Proxy::Puppet::ClassScanner.scan_directory directory
|
||
end
|
||
end
|
||
end
|
||
|
||
def initialize name, params = {}
|
||
@klass = name || raise("Must provide puppet class name")
|
||
@params = params
|
||
end
|
||
|
||
def to_s
|
||
self.module.nil? ? name : "#{self.module}::#{name}"
|
||
end
|
||
|
||
# returns module name (excluding of the class name)
|
||
def module
|
||
klass[0..(klass.index("::")-1)] if has_module?(klass)
|
||
end
|
||
|
||
# returns class name (excluding of the module name)
|
||
def name
|
||
has_module?(klass) ? klass[(klass.index("::")+2)..-1] : klass
|
||
end
|
||
|
||
attr_reader :params
|
||
|
||
private
|
||
attr_reader :klass
|
||
|
||
def has_module?(klass)
|
||
!!klass.index("::")
|
||
end
|
||
end
|
modules/puppet/puppet_plugin.rb | ||
---|---|---|
module Proxy::Puppet
|
||
class Plugin < Proxy::Plugin
|
||
http_rackup_path File.expand_path("http_config.ru", File.expand_path("../", __FILE__))
|
||
https_rackup_path File.expand_path("http_config.ru", File.expand_path("../", __FILE__))
|
||
|
||
default_settings :puppet_provider => 'puppetrun', :puppetdir => '/etc/puppet'
|
||
plugin :puppet, ::Proxy::VERSION
|
||
end
|
||
end
|
modules/puppet/puppet_ssh.rb | ||
---|---|---|
require 'puppet/runner'
|
||
|
||
class Proxy::Puppet::PuppetSSH < Proxy::Puppet::Runner
|
||
def run
|
||
cmd = []
|
||
cmd.push(which('sudo')) if Proxy::Puppet::Plugin.settings.puppetssh_sudo
|
||
cmd.push(which('ssh'))
|
||
cmd.push("-l", "#{Proxy::Puppet::Plugin.settings.puppetssh_user}") if Proxy::Puppet::Plugin.settings.puppetssh_user
|
||
if (file = Proxy::Puppet::Plugin.settings.puppetssh_keyfile)
|
||
if File.exists?(file)
|
||
cmd.push("-i", "#{file}")
|
||
else
|
||
logger.warn("Unable to access SSH private key:#{file}, ignoring...")
|
||
end
|
||
end
|
||
|
||
if cmd.include?(false)
|
||
logger.warn 'sudo or the ssh binary is missing.'
|
||
return false
|
||
end
|
||
|
||
ssh_command = escape_for_shell(Proxy::Puppet::Plugin.settings.puppetssh_command || 'puppet agent --onetime --no-usecacheonfailure')
|
||
nodes.each do |node|
|
||
shell_command(cmd + [escape_for_shell(node), ssh_command], false)
|
||
end
|
||
end
|
||
end
|
modules/puppet/puppetrun.rb | ||
---|---|---|
require 'puppet/runner'
|
||
|
||
class Proxy::Puppet::PuppetRun < ::Proxy::Puppet::Runner
|
||
def run
|
||
# Search in /opt/ for puppet enterprise users
|
||
default_path = "/opt/puppet/bin"
|
||
# search for puppet for users using puppet 2.6+
|
||
cmd = []
|
||
cmd.push(which("sudo"))
|
||
|
||
if Proxy::Puppet::Plugin.settings.puppet_user
|
||
cmd.push("-u", Proxy::Puppet::Plugin.settings.puppet_user)
|
||
end
|
||
|
||
cmd.push(which("puppetrun", default_path) || which("puppet", default_path))
|
||
|
||
if cmd.include?(false)
|
||
logger.warn "sudo or puppetrun binary was not found - aborting"
|
||
return false
|
||
end
|
||
|
||
# Append kick to the puppet command if we are not using the old puppetca command
|
||
cmd.push("kick") if cmd.any? { |part| part.end_with?('puppet') }
|
||
shell_command(cmd + (shell_escaped_nodes.map {|n| ["--host", n] }).flatten)
|
||
end
|
||
end
|
modules/puppet/runner.rb | ||
---|---|---|
module Proxy::Puppet
|
||
class Runner
|
||
include Proxy::Log
|
||
include Proxy::Util
|
||
|
||
def initialize(opts)
|
||
@nodes = opts[:nodes]
|
||
end
|
||
|
||
protected
|
||
attr_reader :nodes
|
||
|
||
def shell_escaped_nodes
|
||
nodes.collect { |n| escape_for_shell(n) }
|
||
end
|
||
|
||
def shell_command(cmd, wait = true)
|
||
begin
|
||
c = popen(cmd)
|
||
unless wait
|
||
Process.detach(c.pid)
|
||
return 0
|
||
end
|
||
Process.wait(c.pid)
|
||
rescue Exception => e
|
||
logger.error("Exception '#{e}' when executing '#{cmd}'")
|
||
return false
|
||
end
|
||
logger.warn("Non-null exit code when executing '#{cmd}'") if $?.exitstatus != 0
|
||
$?.exitstatus == 0
|
||
end
|
||
|
||
def popen(cmd)
|
||
# 1.8.7 note: this assumes that cli options are space-separated
|
||
cmd = cmd.join(' ') unless RUBY_VERSION > '1.8.7'
|
||
logger.debug("about to execute: #{cmd}")
|
||
IO.popen(cmd)
|
||
end
|
||
end
|
||
end
|
modules/puppet/salt.rb | ||
---|---|---|
require 'puppet/runner'
|
||
|
||
class Proxy::Puppet::Salt < Proxy::Puppet::Runner
|
||
def run
|
||
cmd = []
|
||
cmd.push(which('sudo'))
|
||
cmd.push(which('salt'))
|
||
|
||
if cmd.include?(false)
|
||
logger.warn 'sudo or the salt binary is missing.'
|
||
return false
|
||
end
|
||
|
||
cmd.push('-L')
|
||
cmd.push(shell_escaped_nodes.join(','))
|
||
cmd.push('puppet.run')
|
||
|
||
shell_command(cmd)
|
||
end
|
||
end
|
modules/puppet_proxy/class_scanner.rb | ||
---|---|---|
require 'puppet_proxy/puppet_class'
|
||
|
||
module Proxy::Puppet
|
||
class ClassScanner
|
||
class << self
|
||
# scans a given directory and its sub directory for puppet classes
|
||
# returns an array of PuppetClass objects.
|
||
def scan_directory directory
|
||
|
||
parser = Puppet::Parser::Parser.new Puppet::Node::Environment.new
|
||
|
||
Dir.glob("#{directory}/*/manifests/**/*.pp").map do |filename|
|
||
scan_manifest File.read(filename), parser, filename
|
||
end.compact.flatten
|
||
end
|
||
|
||
def scan_manifest manifest, parser, filename = ''
|
||
klasses = []
|
||
|
||
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 << PuppetClass.new(klass.namespace, params)
|
||
end
|
||
end
|
||
klasses
|
||
rescue => e
|
||
puts "Error while parsing #{filename}: #{e}"
|
||
klasses
|
||
end
|
||
|
||
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
|
||
# This is the case when two params are concatenated together ,e.g. "param_${key}_something"
|
||
# 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::Variable
|
||
"${#{value}}"
|
||
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, but optional
|
||
""
|
||
# 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 }]
|
||
when Puppet::Parser::AST::Function
|
||
value.to_s
|
||
# 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
|
||
value.evaluate nil
|
||
else
|
||
raise TypeError
|
||
end
|
||
end
|
||
end
|
||
end
|
||
end
|
||
end
|
||
end
|
modules/puppet_proxy/class_scanner_eparser.rb | ||
---|---|---|
require 'puppet_proxy/puppet_class'
|
||
require 'puppet'
|
||
|
||
if Puppet::PUPPETVERSION.to_f >= 3.2
|
||
require 'puppet/pops'
|
||
|
||
module Proxy::Puppet
|
||
class ClassScannerEParser
|
||
class << self
|
||
# scans a given directory and its sub directory for puppet classes
|
||
# returns an array of PuppetClass objects.
|
||
def scan_directory directory
|
||
|
||
parser = Puppet::Pops::Parser::Parser.new
|
||
Dir.glob("#{directory}/*/manifests/**/*.pp").map do |filename|
|
||
scan_manifest File.read(filename), parser, filename
|
||
end.compact.flatten
|
||
end
|
||
|
||
def scan_manifest manifest, parser, filename = ''
|
||
klasses = []
|
||
|
||
already_seen = Set.new
|
||
already_seen << '' # Prevent the toplevel "main" class from matching
|
||
ast = parser.parse_string manifest
|
||
class_finder = ClassFinder.new
|
||
|
||
class_finder.do_find ast.current
|
||
klasses = class_finder.klasses
|
||
|
||
klasses
|
||
rescue => e
|
||
puts "Error while parsing #{filename}: #{e}"
|
||
klasses
|
||
end
|
||
end
|
||
end
|
||
|
||
class ClassFinder
|
||
|
||
@@finder_visitor ||= Puppet::Pops::Visitor.new(nil,'find',0,0)
|
||
|
||
attr_reader :klasses
|
||
|
||
def initialize
|
||
@klasses = []
|
||
end
|
||
|
||
def do_find ast
|
||
@@finder_visitor.visit_this(self, ast)
|
||
end
|
||
|
||
def find_HostClassDefinition o
|
||
params = {}
|
||
o.parameters.each do |param|
|
||
params[param.name] = ast_to_value_new(param.value) rescue nil
|
||
end
|
||
@klasses << PuppetClass.new(o.name, params)
|
||
|
||
if o.body
|
||
do_find(o.body)
|
||
end
|
||
end
|
||
|
||
def find_BlockExpression o
|
||
o.statements.collect {|x| do_find(x) }
|
||
end
|
||
|
||
def find_CallNamedFunctionExpression o
|
||
if o.lambda
|
||
do_find(o.lambda)
|
||
end
|
||
end
|
||
|
||
def find_Program o
|
||
if o.body
|
||
do_find(o.body)
|
||
end
|
||
end
|
||
|
||
def find_Object o
|
||
#puts "Unhandled object:#{o}"
|
||
end
|
||
|
||
def ast_to_value_new value
|
||
unless value.class.name.start_with? "Puppet::Pops::Model::"
|
||
# 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::Pops::Model::BooleanExpression, Puppet::Pops::Model::LiteralString, Puppet::Pops::Model::LiteralNumber, Puppet::Pops::Model::QualifiedName
|
||
(Puppet::Parser::Scope.number?(value.value) or value.value)
|
||
when Puppet::Pops::Model::UnaryMinusExpression
|
||
- ast_to_value_new(value.expr)
|
||
when Puppet::Pops::Model::ArithmeticExpression
|
||
ast_to_value_new(value.left_expr).send(value.operator, ast_to_value_new(value.right_expr))
|
||
# Supported with stringification
|
||
when Puppet::Pops::Model::ConcatenatedString
|
||
# This is the case when two params are concatenated together ,e.g. "param_${key}_something"
|
||
# 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.segments.map {|v| ast_to_value_new v}.join rescue nil
|
||
when Puppet::Pops::Model::TextExpression
|
||
ast_to_value_new value.expr
|
||
when Puppet::Pops::Model::VariableExpression
|
||
"${#{ast_to_value_new value.expr}}"
|
||
when (Puppet::Pops::Model::TypeReference rescue nil)
|
||
value.value
|
||
when Puppet::Pops::Model::LiteralUndef
|
||
""
|
||
# Depends on content
|
||
when Puppet::Pops::Model::LiteralList
|
||
value.values.inject([]) { |arr, v| (arr << ast_to_value_new(v)) rescue arr }
|
||
when Puppet::Pops::Model::LiteralHash
|
||
# Note that all keys are string in Puppet
|
||
Hash[value.entries.inject([]) { |arr, entry| (arr << [ast_to_value_new(entry.key).to_s, ast_to_value_new(entry.value)]) rescue arr }]
|
||
when Puppet::Pops::Model::NamedFunctionDefinition
|
||
value.to_s
|
Also available in: Unified diff
fixes #6306: removed possible namespace collisions with puppet and chef