-
Notifications
You must be signed in to change notification settings - Fork 998
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Fixes #32518 - Moving Ansible fact parser
Moving Ansible fact parser from Ansible plugin to Core for better refactoring of parsers logic
- Loading branch information
Showing
11 changed files
with
1,187 additions
and
0 deletions.
There are no files selected for viewing
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
# frozen_string_literal: true | ||
|
||
module ForemanAnsible | ||
# Define the class that fact names that come from Ansible should have | ||
# It allows us to filter facts by origin, and also to display the origin | ||
# in the fact values table (/fact_values) | ||
class FactName < ::FactName | ||
def origin | ||
'Ansible' | ||
end | ||
|
||
def icon_path | ||
'Ansible.png' | ||
end | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,126 @@ | ||
# frozen_string_literal: true | ||
|
||
# Override methods from Foreman app/services/fact_parser so that facts | ||
# representing host properties are understood when they come from Ansible. | ||
class AnsibleFactParser < FactParser | ||
include ForemanAnsible::OperatingSystemParser | ||
attr_reader :facts | ||
|
||
def initialize(facts) | ||
@facts = HashWithIndifferentAccess.new(facts[:ansible_facts]) | ||
end | ||
|
||
# Don't do anything as there's no env in Ansible | ||
def environment | ||
end | ||
|
||
def architecture | ||
name = facts[:ansible_architecture] || facts[:facter_architecture] | ||
Architecture.where(:name => name).first_or_create if name.present? | ||
end | ||
|
||
def model | ||
name = detect_fact([:ansible_product_name, :facter_virtual, | ||
:facter_productname, :facter_model, :model]) | ||
Model.where(:name => name.strip).first_or_create if name.present? | ||
end | ||
|
||
def domain | ||
name = detect_fact([:ansible_domain, :facter_domain, | ||
:ohai_domain, :domain]) | ||
Domain.where(:name => name).first_or_create if name.present? | ||
end | ||
|
||
def support_interfaces_parsing? | ||
true | ||
end | ||
|
||
# Move ansible's default interface first in the list of interfaces since | ||
# Foreman picks the first one that is usable. If ansible has no | ||
# preference otherwise at least sort the list. | ||
# | ||
# This method overrides app/services/fact_parser.rb on Foreman and returns | ||
# an array of interface names, ['eth0', 'wlan1', etc...] | ||
def get_interfaces | ||
pref = facts[:ansible_default_ipv4] && | ||
facts[:ansible_default_ipv4]['interface'] | ||
if pref.present? | ||
(facts[:ansible_interfaces] - [pref]).unshift(pref) | ||
else | ||
ansible_interfaces | ||
end | ||
end | ||
|
||
def get_facts_for_interface(iface_name) | ||
interface = iface_name.tr('-', '_') # virbr1-nic -> virbr1_nic | ||
interface_facts = facts[:"ansible_#{interface}"] | ||
ipaddress = ip_from_interface(interface) | ||
ipaddress6 = ipv6_from_interface(interface) | ||
macaddress = mac_from_interface(interface) | ||
iface_facts = HashWithIndifferentAccess[ | ||
interface_facts.merge(:ipaddress => ipaddress, | ||
:ipaddress6 => ipaddress6, | ||
:macaddress => macaddress) | ||
] | ||
logger.debug { "Ansible interface #{interface} facts: #{iface_facts.inspect}" } | ||
iface_facts | ||
end | ||
|
||
def ipmi_interface | ||
end | ||
|
||
def boot_timestamp | ||
Time.zone.now.to_i - facts['ansible_uptime_seconds'].to_i | ||
end | ||
|
||
def virtual | ||
facts['ansible_virtualization_role'] == 'guest' | ||
end | ||
|
||
def ram | ||
facts['ansible_memtotal_mb'].to_i | ||
end | ||
|
||
def sockets | ||
facts['ansible_processor_count'].to_i | ||
end | ||
|
||
def cores | ||
facts['ansible_processor_cores'].to_i | ||
end | ||
|
||
private | ||
|
||
def ansible_interfaces | ||
return [] if facts[:ansible_interfaces].blank? | ||
facts[:ansible_interfaces].sort | ||
end | ||
|
||
def mac_from_interface(interface) | ||
facts[:"ansible_#{interface}"]['perm_macaddress'].presence || facts[:"ansible_#{interface}"]['macaddress'] | ||
end | ||
|
||
def ip_from_interface(interface) | ||
return if facts[:"ansible_#{interface}"]['ipv4'].blank? | ||
if facts[:"ansible_#{interface}"]['ipv4'].is_a?(Array) | ||
facts[:"ansible_#{interface}"]['ipv4'][0]['address'] | ||
else | ||
facts[:"ansible_#{interface}"]['ipv4']['address'] | ||
end | ||
end | ||
|
||
def ipv6_from_interface(interface) | ||
return if facts[:"ansible_#{interface}"]['ipv6'].blank? | ||
|
||
facts[:"ansible_#{interface}"]['ipv6'].first['address'] | ||
end | ||
|
||
# Returns first non-empty fact. Needed to check for empty strings. | ||
def detect_fact(fact_names) | ||
facts[ | ||
fact_names.detect do |fact_name| | ||
facts[fact_name].present? | ||
end | ||
] | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,37 @@ | ||
# frozen_string_literal: true | ||
|
||
module ForemanAnsible | ||
# See sparse and unsparse documentation | ||
class FactSparser | ||
class << self | ||
# Sparses facts, so that it converts a facts hash | ||
# { operatingsystem : { major: 20, name : 'fedora' } | ||
# into | ||
# { operatingsystem::major: 20, | ||
# operatingsystem::name: 'fedora' } | ||
def sparse(hash, options = {}) | ||
hash.map do |k, v| | ||
prefix = options.fetch(:prefix, []) + [k] | ||
next sparse(v, options.merge(:prefix => prefix)) if v.is_a? Hash | ||
{ prefix.join(options.fetch(:separator, FactName::SEPARATOR)) => v } | ||
end.reduce(:merge) || {} | ||
end | ||
|
||
# Unsparses facts, so that it converts a hash with facts | ||
# { operatingsystem::major: 20, | ||
# operatingsystem::name: 'fedora' } | ||
# into | ||
# { operatingsystem : { major: 20, name: 'fedora' } } | ||
def unsparse(facts_hash) | ||
ret = {} | ||
sparse(facts_hash).each do |full_name, value| | ||
current = ret | ||
fact_name = full_name.to_s.split(FactName::SEPARATOR) | ||
current = (current[fact_name.shift] ||= {}) until fact_name.size <= 1 | ||
current[fact_name.first] = value | ||
end | ||
ret | ||
end | ||
end | ||
end | ||
end |
102 changes: 102 additions & 0 deletions
102
app/services/foreman_ansible/operating_system_parser.rb
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,102 @@ | ||
# frozen_string_literal: true | ||
|
||
module ForemanAnsible | ||
# Methods to parse facts related to the OS | ||
module OperatingSystemParser | ||
def operatingsystem | ||
args = { :name => os_name, :major => os_major, :minor => os_minor } | ||
args[:release_name] = os_release_name if os_name == 'Debian' || os_name == 'Ubuntu' | ||
return @local_os if local_os(args).present? | ||
return @new_os if new_os(args).present? | ||
logger.debug do | ||
'Ansible facts parser: No OS could be created with '\ | ||
"os_name='#{os_name}' os_major='#{os_major}' "\ | ||
"os_minor='#{os_minor}': "\ | ||
"#{@new_os.errors if @new_os.present?}" | ||
end | ||
nil | ||
end | ||
|
||
def local_os(args) | ||
@local_os = Operatingsystem.where(args).first | ||
end | ||
|
||
def new_os(args) | ||
return @new_os if @new_os.present? | ||
@new_os = Operatingsystem.new(args.merge(:description => os_description)) | ||
@new_os if @new_os.valid? && @new_os.save | ||
end | ||
|
||
def debian_os_major_sid | ||
case facts[:ansible_distribution_major_version] | ||
when /wheezy/i | ||
'7' | ||
when /jessie/i | ||
'8' | ||
when /stretch/i | ||
'9' | ||
when /buster/i | ||
'10' | ||
end | ||
end | ||
|
||
def os_release_name | ||
return '' if os_name != 'Debian' && os_name != 'Ubuntu' | ||
facts[:ansible_distribution_release] | ||
end | ||
|
||
def os_major | ||
if os_name == 'Debian' && | ||
facts[:ansible_distribution_major_version][%r{\/sid}i] | ||
debian_os_major_sid | ||
else | ||
facts[:ansible_distribution_major_version] || | ||
facts[:ansible_lsb] && facts[:ansible_lsb]['major_release'] || | ||
(facts[:version].split('R')[0] if os_name == 'junos') | ||
end | ||
end | ||
|
||
def os_release | ||
facts[:ansible_distribution_version] || | ||
facts[:ansible_lsb] && facts[:ansible_lsb]['release'] | ||
end | ||
|
||
def os_minor | ||
_, minor = os_release&.split('.', 2) || | ||
(facts[:version].split('R') if os_name == 'junos') | ||
# Until Foreman supports os.minor as something that's not a number, | ||
# we should remove the extra dots in the version. E.g: | ||
# '6.1.7601.65536' becomes '6.1.760165536' | ||
if facts[:ansible_os_family] == 'Windows' | ||
minor, patch = minor.split('.', 2) | ||
patch.tr!('.', '') | ||
minor = "#{minor}.#{patch}" | ||
end | ||
minor || '' | ||
end | ||
|
||
def os_name | ||
if facts[:ansible_os_family] == 'Windows' | ||
facts[:ansible_os_name].tr(" \n\t", '') || | ||
facts[:ansible_distribution].tr(" \n\t", '') | ||
else | ||
distribution = facts[:ansible_lsb].try(:[], 'id') || facts[:ansible_distribution] | ||
|
||
if distribution == 'RedHat' && | ||
facts[:ansible_lsb].try(:[], 'id') == 'RedHatEnterpriseWorkstation' | ||
distribution += '_Workstation' | ||
end | ||
|
||
distribution | ||
end | ||
end | ||
|
||
def os_description | ||
if facts[:ansible_os_family] == 'Windows' | ||
facts[:ansible_os_name].strip || facts[:ansible_distribution].strip | ||
else | ||
facts[:ansible_lsb] && facts[:ansible_lsb]['description'] | ||
end | ||
end | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,25 @@ | ||
# frozen_string_literal: true | ||
|
||
module ForemanAnsible | ||
# On 1.13+ , use the parser for structured facts (like Facter 2) that comes | ||
# from core | ||
class StructuredFactImporter < ::StructuredFactImporter | ||
def fact_name_class | ||
ForemanAnsible::FactName | ||
end | ||
|
||
def self.authorized_smart_proxy_features | ||
'Ansible' | ||
end | ||
|
||
def initialize(host, facts = {}) | ||
# Try to assign these facts to the correct host as per the facts say | ||
# If that host isn't created yet, the host parameter will contain it | ||
@host = Host.find_by(:name => facts[:ansible_facts][:ansible_fqdn] || | ||
facts[:ansible_facts][:fqdn]) || | ||
host | ||
@facts = normalize(facts[:ansible_facts]) | ||
@counters = {} | ||
end | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
class ParserRegistrator | ||
def self.register_fact_parser(type, parser) | ||
if (old_parser = FactParser.parser_for(type)) && old_parser != FactParser.parsers.default | ||
Rails.logger.warn("WARNING: Parser #{old_parser} for type #{type} is replaced with #{parser}") | ||
end | ||
|
||
FactParser.register_fact_parser(type, parser) | ||
end | ||
end | ||
|
||
# Ansible | ||
Foreman::Plugin.fact_importer_registry.register(:ansible, ForemanAnsible::StructuredFactImporter, false) | ||
ParserRegistrator.register_fact_parser(:ansible, AnsibleFactParser) |
Oops, something went wrong.