Project

General

Profile

« Previous | Next » 

Revision 69f6b3e5

Added by Dmitri Dolguikh almost 10 years ago

fixes #6306: removed possible namespace collisions with puppet and chef

View differences:

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
... This diff was truncated because it exceeds the maximum size that can be displayed.

Also available in: Unified diff