Project

General

Profile

« Previous | Next » 

Revision 715d097c

Added by Shimon Shtein over 6 years ago

Fixes #15409 - Separated puppet facts from core

View differences:

app/controllers/api/v2/hosts_controller.rb
param :type, String, :desc => N_("optional: the STI type of host to create")
def facts
@host = detect_host_type.import_host params[:name], params[:facts][:_type] || 'puppet', params[:certname], detected_proxy.try(:id)
state = @host.import_facts(params[:facts].to_unsafe_h)
@host = detect_host_type.import_host params[:name], params[:certname]
state = @host.import_facts(params[:facts].to_unsafe_h, detected_proxy)
process_response state
rescue ::Foreman::Exception => e
render_message(e.to_s, :status => :unprocessable_entity)
app/models/concerns/facets/base.rb
# Use this method to populate host's fields based on fact values exposed by the importer.
# You can populate fields in the associated host's facets too.
def populate_fields_from_facts(host, importer, type, proxy_id)
def populate_fields_from_facts(host, parser, type, proxy)
end
end
end
app/models/concerns/facets/managed_host_extensions.rb
attributes
end
def populate_facet_fields(parser, type, source_proxy)
Facets.registered_facets.values.each do |facet_config|
facet_config.model.populate_fields_from_facts(self, parser, type, source_proxy)
end
end
private
def forward_property_call(property, args, facet)
app/models/concerns/puppet_host_extensions.rb
module PuppetHostExtensions
def populate_fields_from_facts(parser, type, source_proxy)
super
type ||= 'puppet'
return unless type == 'puppet'
if Setting[:update_environment_from_facts]
set_non_empty_values parser, [:environment]
else
self.environment ||= parser.environment unless parser.environment.blank?
end
# if proxy authentication is enabled and we have no puppet proxy set and the upload came from puppet,
# use it as puppet proxy.
self.puppet_proxy ||= source_proxy
end
end
app/models/host/base.rb
super - [ inheritance_column ]
end
def self.import_host(hostname, certname = nil, deprecated_proxy = nil)
raise(::Foreman::Exception.new("Invalid Hostname, must be a String")) unless hostname.is_a?(String)
Foreman::Deprecation.deprecation_warning("1.19", "proxy parameter is deprecated, please use import_facts to set it") if deprecated_proxy
# downcase everything
hostname.try(:downcase!)
certname.try(:downcase!)
host = Host.find_by_certname(certname) if certname.present?
host ||= Host.find_by_name(hostname)
host ||= self.new(:name => hostname) # if no host was found, build a new one
# if we were given a certname but found the Host by hostname we should update the certname
# this also sets certname for newly created hosts
host.certname = certname if certname.present?
host
end
def create_new_host_when_facts_are_uploaded?
Setting[:create_new_host_when_facts_are_uploaded]
end
# expect a facts hash
def import_facts(facts)
def import_facts(facts, source_proxy = nil)
return false if !create_new_host_when_facts_are_uploaded? && new_record?
# we are not importing facts for hosts in build state (e.g. waiting for a re-installation)
......
facts[:domain] = facts[:domain].downcase if facts[:domain].present?
type = facts.delete(:_type)
importer = FactImporter.importer_for(type).new(self, facts)
importer.import!
save(:validate => false)
parse_facts facts, type, source_proxy
end
def parse_facts(facts, type, source_proxy)
time = facts[:_timestamp]
time = time.to_time if time.is_a?(String)
self.last_compile = time if time
type = facts.delete(:_type) || 'puppet'
importer = FactImporter.importer_for(type).new(self, facts)
importer.import!
unless build?
parser = FactParser.parser_for(type).new(facts)
populate_fields_from_facts(parser, type, source_proxy)
end
save(:validate => false)
set_taxonomies(facts)
populate_fields_from_facts(facts, type)
# we are saving here with no validations, as we want this process to be as fast
# as possible, assuming we already have all the right settings in Foreman.
......
[ :model ]
end
def populate_fields_from_facts(facts = self.facts_hash, type = 'puppet')
# we don't import facts for host in build mode
return if build?
parser = FactParser.parser_for(type).new(facts)
def populate_fields_from_facts(parser, type, source_proxy)
# we must create interface if it's missing so we can store domain
build_required_interfaces(:managed => false)
set_non_empty_values(parser, attributes_to_import_from_facts)
set_interfaces(parser) if parser.parse_interfaces?
parser
end
def set_non_empty_values(parser, methods)
app/models/host/managed.rb
setAutosign
end
def import_facts(facts)
def import_facts(facts, source_proxy = nil)
# Facts come from 'existing' attributes/infrastructure. We skip triggering
# the orchestration of this infrastructure when we create a host this way.
skip_orchestration! if SETTINGS[:unattended]
super(facts)
super
ensure
enable_orchestration! if SETTINGS[:unattended]
end
......
all_puppetclasses.collect {|c| c.name}
end
def self.import_host(hostname, import_type, certname = nil, proxy_id = nil)
raise(::Foreman::Exception.new("Invalid Hostname, must be a String")) unless hostname.is_a?(String)
# downcase everything
hostname.try(:downcase!)
certname.try(:downcase!)
host = Host.find_by_certname(certname) if certname.present?
host ||= Host.find_by_name(hostname)
host ||= Host.new(:name => hostname) # if no host was found, build a new one
# if we were given a certname but found the Host by hostname we should update the certname
# this also sets certname for newly created hosts
host.certname = certname if certname.present?
# if proxy authentication is enabled and we have no puppet proxy set and the upload came from puppet,
# use it as puppet proxy.
host.puppet_proxy_id ||= proxy_id if import_type == 'puppet'
host
end
def attributes_to_import_from_facts
attrs = [:architecture, :hostgroup]
if !Setting[:ignore_facts_for_operatingsystem] || (Setting[:ignore_facts_for_operatingsystem] && operatingsystem.blank?)
......
super + attrs
end
def populate_fields_from_facts(facts = self.facts_hash, type = 'puppet')
importer = super
if Setting[:update_environment_from_facts]
set_non_empty_values importer, [:environment]
else
self.environment ||= importer.environment unless importer.environment.blank?
end
def populate_fields_from_facts(parser, type, source_proxy)
super
operatingsystem.architectures << architecture if operatingsystem && architecture && !operatingsystem.architectures.include?(architecture)
self.save(:validate => false)
populate_facet_fields(parser, type, source_proxy)
end
# Called by build link in the list
app/services/fact_importer.rb
attr_reader :counters
def self.importer_for(type)
importers[type.to_s] || importers[:puppet]
importers[type.to_s]
end
def self.importers
@importers ||= { :puppet => PuppetFactImporter }.with_indifferent_access
@importers ||= {}.with_indifferent_access
end
def self.register_fact_importer(key, klass)
def self.register_fact_importer(key, klass, default = false)
importers.default = klass if default
importers[key.to_sym] = klass
end
app/services/fact_parser.rb
VIRTUAL_NAMES = /#{VIRTUAL}|#{BRIDGES}|#{BONDS}/
def self.parser_for(type)
parsers[type.to_s] || parsers[:puppet]
parsers[type.to_s]
end
def self.parsers
@parsers ||= { :puppet => PuppetFactParser }.with_indifferent_access
@parsers ||= {}.with_indifferent_access
end
def self.register_fact_parser(key, klass)
def self.register_fact_parser(key, klass, default = false)
parsers.default = klass if default
parsers[key.to_sym] = klass
end
config/initializers/puppet.rb
:partial => "hosts/puppet/main_tab_fields",
:priority => 100
end
FactImporter.register_fact_importer :puppet, PuppetFactImporter, true
FactParser.register_fact_parser :puppet, PuppetFactParser, true
# The module should be included after the class is constructed,
# since it tries to alias_method_chain a method that is defined
# in the class itself.
Host::Managed.send :prepend, PuppetHostExtensions
test/models/host_test.rb
test "should populate primary interface attributes even without existing interface" do
host = FactoryGirl.create(:host, :managed => false)
host.interfaces = []
host.populate_fields_from_facts(:domain => 'example.com',
:operatingsystem => 'RedHat',
:operatingsystemrelease => '6.2',
:macaddress_eth0 => '00:00:11:22:11:22',
:ipaddress_eth0 => '192.168.0.1',
:ipaddress6_eth0 => '2001:db8::1',
:interfaces => 'eth0')
host.populate_fields_from_facts(
mock_parser(:domain => 'example.com',
:operatingsystem => 'RedHat',
:operatingsystemrelease => '6.2',
:macaddress_eth0 => '00:00:11:22:11:22',
:ipaddress_eth0 => '192.168.0.1',
:ipaddress6_eth0 => '2001:db8::1',
:interfaces => 'eth0'),
'puppet',
nil)
assert_equal 'example.com', host.domain.name
assert_equal '2001:db8::1', host.primary_interface.ip6
refute host.primary_interface.managed?
......
assert_difference 'Host.count' do
Setting[:ignore_facts_for_domain] = true
raw = read_json_fixture('facts/facts_with_certname.json')
host = Host.import_host(raw['name'], 'puppet', raw['certname'])
host = Host.import_host(raw['name'], raw['certname'])
assert host.import_facts(raw['facts'])
assert Host.find_by_name('sinn1636.lan')
assert host.domain
......
test 'should downcase hostname parameter from json of a new host' do
raw = read_json_fixture('facts/facts_with_caps.json')
host = Host.import_host(raw['name'], 'puppet')
host = Host.import_host(raw['name'])
assert host.import_facts(raw['facts'])
assert Host.find_by_name('sinn1636.lan')
end
test 'should downcase domain parameter from json of a new host' do
raw = read_json_fixture('facts/facts_with_caps.json')
host = Host.import_host(raw['name'], 'puppet')
host = Host.import_host(raw['name'])
assert host.import_facts(raw['facts'])
assert_equal raw['facts']['domain'].downcase, Host.find_by_name('sinn1636.lan').facts_hash['domain']
end
test 'should import facts idempotently' do
raw = read_json_fixture('facts/facts_with_caps.json')
host = Host.import_host(raw['name'], 'puppet')
host = Host.import_host(raw['name'])
assert host.import_facts(raw['facts'])
value_ids = Host.find_by_name('sinn1636.lan').fact_values.map(&:id)
assert host.import_facts(raw['facts'])
......
# hostname in the json is sinn1636.lan, so if the facts have been updated for
# this host, it's a successful identification by certname
raw = read_json_fixture('facts/facts_with_certname.json')
host = Host.import_host(raw['name'], 'puppet', raw['certname'])
host = Host.import_host(raw['name'], raw['certname'])
assert host.import_facts(raw['facts'])
host = Host.find_by_name('sinn1636.fail')
assert_equal '10.35.27.2', host.interfaces.find_by_identifier('br180').ip
......
Host.new(:name => 'sinn1636.lan', :certname => 'sinn1636.cert.fail').save(:validate => false)
assert_equal 'sinn1636.cert.fail', Host.find_by_name('sinn1636.lan').certname
raw = read_json_fixture('facts/facts_with_certname.json')
host = Host.import_host(raw['name'], 'puppet', raw['certname'])
host = Host.import_host(raw['name'], raw['certname'])
assert host.import_facts(raw['facts'])
assert_equal 'sinn1636.lan.cert', Host.find_by_name('sinn1636.lan').certname
end
......
assert_difference 'Host.count' do
Setting[:create_new_host_when_facts_are_uploaded] = true
raw = read_json_fixture('facts/facts_with_certname.json')
host = Host.import_host(raw['name'], 'puppet', raw['certname'])
host = Host.import_host(raw['name'], raw['certname'])
assert host.import_facts(raw['facts'])
assert Host.find_by_name('sinn1636.lan')
Setting[:create_new_host_when_facts_are_uploaded] =
......
Setting[:create_new_host_when_facts_are_uploaded] = false
refute Setting[:create_new_host_when_facts_are_uploaded]
raw = read_json_fixture('facts/facts_with_certname.json')
host = Host.import_host(raw['name'], 'puppet', raw['certname'])
host = Host.import_host(raw['name'], raw['certname'])
refute host.import_facts(raw['facts'])
host = Host.find_by_name('sinn1636.lan')
Setting[:create_new_host_when_facts_are_uploaded] =
......
Setting[:create_new_host_when_facts_are_uploaded] = false
refute Setting[:create_new_host_when_facts_are_uploaded]
raw = read_json_fixture('facts/facts_with_certname.json')
host = Host.import_host(raw['name'], 'puppet', raw['certname'])
host = Host.import_host(raw['name'], raw['certname'])
assert host.import_facts(raw['facts'])
end
test 'host taxonomies are set to a default when uploading facts' do
Setting[:create_new_host_when_facts_are_uploaded] = true
raw = read_json_fixture('facts/facts.json')
host = Host.import_host(raw['name'], 'puppet')
host = Host.import_host(raw['name'])
assert host.import_facts(raw['facts'])
assert_equal Setting[:default_location], Host.find_by_name('sinn1636.lan').location.title
......
raw = read_json_fixture('facts/facts.json')
raw['facts']['foreman_location'] = 'Location 2'
raw['facts']['foreman_organization'] = 'Organization 2'
host = Host.import_host(raw['name'], 'puppet')
host = Host.import_host(raw['name'])
assert host.import_facts(raw['facts'])
assert_equal 'Location 2', Host.find_by_name('sinn1636.lan').location.title
......
test 'default taxonomies are not assigned to hosts with taxonomies' do
Setting[:default_location] = taxonomies(:location1).title
raw = read_json_fixture('facts/facts.json')
host = Host.import_host(raw['name'], 'puppet')
host = Host.import_host(raw['name'])
assert host.import_facts(raw['facts'])
Host.find_by_name('sinn1636.lan').update_attribute(:location, taxonomies(:location2))
Host.find_by_name('sinn1636.lan').import_facts(raw['facts'])
......
raw = read_json_fixture('facts/facts.json')
raw['facts']['foreman_location'] = 'Location 2'
host = Host.import_host(raw['name'], 'puppet')
host = Host.import_host(raw['name'])
assert host.import_facts(raw['facts'])
Host.find_by_name('sinn1636.lan').update_attribute(:location, taxonomies(:location1))
......
end
test 'operatingsystem updated from facts' do
host = Host.import_host('host', 'puppet')
host = Host.import_host('host')
assert host.import_facts(:lsbdistrelease => '6.7', :operatingsystem => 'CentOS')
assert_equal 'CentOS 6.7', host.operatingsystem.to_s
end
test 'operatingsystem not updated from facts when ignore_facts_for_operatingsystem false' do
host = Host.import_host('host', 'puppet')
host = Host.import_host('host')
assert host.import_facts(:lsbdistrelease => '6.7', :operatingsystem => 'CentOS')
Setting[:ignore_facts_for_operatingsystem] = true
assert host.import_facts(:lsbdistrelease => '6.8', :operatingsystem => 'CentOS')
......
assert_difference 'Host.count' do
Setting[:ignore_facts_for_operatingsystem] = true
raw = read_json_fixture('facts/facts_with_certname.json')
host = Host.import_host(raw['name'], 'puppet', raw['certname'])
host = Host.import_host(raw['name'], raw['certname'])
assert host.import_facts(raw['facts'])
assert Host.find_by_name('sinn1636.lan')
assert host.operatingsystem
......
:fact_name => FactoryGirl.create(:fact_name, :name => 'kernelversion'))
assert_difference('Model.count') do
facts = read_json_fixture('facts/facts.json')
h.populate_fields_from_facts facts['facts']
h.populate_fields_from_facts(
mock_parser(facts['facts']),
'puppet',
nil)
end
end
......
test "should update puppet_proxy_id to the id of the validated proxy" do
sp = smart_proxies(:puppetmaster)
raw = read_json_fixture('facts/facts_with_caps.json')
host = Host.import_host(raw['name'], 'puppet', nil, sp.id)
assert host.import_facts(raw['facts'])
host = Host.import_host(raw['name'], nil)
assert host.import_facts(raw['facts'], sp)
assert_equal sp.id, Host.find_by_name('sinn1636.lan').puppet_proxy_id
end
test "should not update puppet_proxy_id if it was not puppet upload" do
sp = smart_proxies(:puppetmaster)
raw = read_json_fixture('facts/facts_with_caps.json')
host = Host.import_host(raw['name'], 'chef', nil, sp.id)
host = Host.import_host(raw['name'])
assert host.import_facts(raw['facts'].merge(:_type => 'chef'), sp)
assert_nil host.puppet_proxy_id
end
......
Host.new(:name => 'sinn1636.lan', :puppet_proxy_id => smart_proxies(:puppetmaster).id).save(:validate => false)
sp = smart_proxies(:puppetmaster)
raw = read_json_fixture('facts/facts_with_certname.json')
host = Host.import_host(raw['name'], 'puppet', nil, sp.id)
assert host.import_facts(raw['facts'])
host = Host.import_host(raw['name'])
assert host.import_facts(raw['facts'], sp)
assert_equal smart_proxies(:puppetmaster).id, Host.find_by_name('sinn1636.lan').puppet_proxy_id
end
......
test 'check operatingsystem and architecture association' do
host = FactoryGirl.build(:host, :interfaces => [FactoryGirl.build(:nic_primary_and_provision)])
assert_nil Operatingsystem.find_by_name('RedHat-test'), "operatingsystem already exist"
host.populate_fields_from_facts(:architecture => "x86_64", :operatingsystem => 'RedHat-test', :operatingsystemrelease => '6.2')
host.populate_fields_from_facts(
mock_parser(:architecture => "x86_64", :operatingsystem => 'RedHat-test', :operatingsystemrelease => '6.2'),
'puppet',
nil)
assert host.operatingsystem.architectures.include?(host.architecture), "no association between operatingsystem and architecture"
end
......
parser = stub(:ipmi_interface => hash, :interfaces => {}, :suggested_primary_interface => [ primary.identifier, {:macaddress => primary.mac, :ipaddress => primary.ip} ])
[host, parser]
end
def mock_parser(properties)
PuppetFactParser.new(properties)
end
end
test/unit/facet_test.rb
@host.apply_inherited_attributes(attributes)
end
test 'facts are parsed by facets too' do
TestFacet.expects(:populate_fields_from_facts)
@host.stubs(:save)
facts_json = read_json_fixture('facts/brslc022.facts.json')
@host.parse_facts(facts_json['facts'], nil, nil)
end
end
context "managed host facet behavior" do
test/unit/fact_parser_test.rb
test ".register_custom_parser" do
chef_parser = Struct.new(:my_method)
FactParser.register_fact_parser :chef, chef_parser
assert_equal chef_parser, FactParser.parser_for(:chef)
begin
assert_equal chef_parser, FactParser.parser_for(:chef)
ensure
FactParser.parsers.delete :chef
end
end
test "#parse_interfaces? should answer based on current setttings" do

Also available in: Unified diff