Skip to content

Commit

Permalink
Fixes #17527 - Add support for hostgroup facets
Browse files Browse the repository at this point in the history
  • Loading branch information
ShimShtein authored and tbrisker committed Dec 13, 2018
1 parent addd8e4 commit 3927dc2
Show file tree
Hide file tree
Showing 15 changed files with 608 additions and 214 deletions.
6 changes: 3 additions & 3 deletions app/controllers/api/v2/hosts_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -115,10 +115,10 @@ def show
param :compute_attributes, Hash, :desc => N_("Additional compute resource specific attributes.")

Facets.registered_facets.values.each do |facet_config|
next unless facet_config.api_param_group && facet_config.api_controller
next unless facet_config.host_configuration.api_param_group && facet_config.host_configuration.api_controller
param "#{facet_config.name}_attributes".to_sym, Hash, :desc => facet_config.api_param_group_description || (N_("Parameters for host's %s facet") % facet_config.name) do
facet_config.load_api_controller
param_group facet_config.api_param_group, facet_config.api_controller
facet_config.host_configuration.load_api_controller
param_group facet_config.host_configuration.api_param_group, facet_config.host_configuration.api_controller
end
end
end
Expand Down
5 changes: 5 additions & 0 deletions app/models/concerns/facets/base.rb
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,11 @@ def provisioning_template_options
module ClassMethods
# Change attributes that will be sent to an facet based on inherited values from the hostgroup.
def inherited_attributes(hostgroup, facet_attributes)
_, facet_config = Facets.find_facet_by_class(self, :host)

if facet_config.has_hostgroup_configuration? && hostgroup
facet_attributes = hostgroup.inherited_facet_attributes(facet_config)
end
facet_attributes
end

Expand Down
86 changes: 3 additions & 83 deletions app/models/concerns/facets/base_host_extensions.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,88 +3,17 @@
module Facets
module BaseHostExtensions
extend ActiveSupport::Concern
include Facets::ModelExtensionsBase

included do
Facets::BaseHostExtensions.refresh_facet_relations(self)

def attributes
hash = super

# include all facet attributes by default
facets_with_definitions.each do |facet, facet_definition|
hash["#{facet_definition.name}_attributes"] = facet.attributes.reject { |key| %w(created_at updated_at).include? key }
end
hash
end
end

class << self
def refresh_facet_relations(klass)
Facets.registered_facets.values.each do |facet_config|
self.register_facet_relation(klass, facet_config)
end
end

# This method is used to add all relation objects necessary for accessing facet from the host object.
# It:
# 1. Adds active record one to one association
# 2. Adds the ability to set facet's attributes via Host#attributes= method
# 3. Extends Host::Managed model with extension module defined by facet's configuration
# 4. Includes facet in host's cloning mechanism
# 5. Adds compatibility properties forwarders so old property calls will still work after moving them to a facet:
# host.foo # => will call Host.my_facet.foo
def register_facet_relation(klass, facet_config)
klass.class_eval do
has_one facet_config.name, :class_name => facet_config.model.name, :foreign_key => :host_id, :inverse_of => :host, :dependent => facet_config.dependent
accepts_nested_attributes_for facet_config.name, :update_only => true, :reject_if => :all_blank
if Foreman.in_setup_db_rake?
# To prevent running into issues in old migrations when new facet is defined but not migrated yet.
# We define it only when in migration to avoid this unnecessary checks outside for the migration
@facet_relation_db_migrate_extensions ||= {} # prevent duplicates
unless @facet_relation_db_migrate_extensions.key?(facet_config.name)
@facet_relation_db_migrate_extensions[facet_config.name] = Module.new do
define_method(facet_config.name) do
if facet_config.model.table_exists?
super()
else
logger.warn("Table for #{facet_config.name} not defined yet: skipping the facet data")
nil
end
end
end
prepend @facet_relation_db_migrate_extensions[facet_config.name]
end
end
alias_method "#{facet_config.name}_attributes", facet_config.name

include facet_config.extension if facet_config.extension

facet_config.compatibility_properties&.each do |prop|
define_method(prop) { |*args| forward_property_call(prop, args, facet_config.name) }
end
end
end
end

def facets
facets_with_definitions.keys
end

# This method will return a hash of facets for a specific host including the coresponding definitions.
# The output should look like this:
# { host.puppet_aspect => Facets.registered_facets[:puppet_aspect] }
def facets_with_definitions
Hash[(Facets.registered_facets.values.map do |facet_config|
facet = send(facet_config.name)
[facet, facet_config] if facet
end).compact]
refresh_facet_relations
end

# This method will return attributes list augmented with attributes that are
# set by the facet. Each registered facet will get opportunity to add its
# own attributes to the list.
def apply_facet_attributes(hostgroup, attributes)
Facets.registered_facets.values.map do |facet_config|
Facets.registered_facets(:host).values.map(&:host_configuration).map do |facet_config|
facet_attributes = attributes["#{facet_config.name}_attributes"] || {}
facet_attributes = facet_config.model.inherited_attributes(hostgroup, facet_attributes)
attributes["#{facet_config.name}_attributes"] = facet_attributes unless facet_attributes.empty?
Expand All @@ -97,14 +26,5 @@ def populate_facet_fields(parser, type, source_proxy)
facet_config.model.populate_fields_from_facts(self, parser, type, source_proxy)
end
end

private

def forward_property_call(property, args, facet)
facet_instance = send(facet)
return nil unless facet_instance

facet_instance.send(property, *args)
end
end
end
35 changes: 35 additions & 0 deletions app/models/concerns/facets/hostgroup_extensions.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
require 'facets'

module Facets
module HostgroupExtensions
extend ActiveSupport::Concern
include Facets::ModelExtensionsBase

included do
configure_facet(:hostgroup, :hostgroup, :hostgroup_id)

Facets.after_entry_created do |entry|
register_facet_relation(entry) if entry.has_hostgroup_configuration?
end
end

def hostgroup_ancestry_cache
@hostgroup_ancestry_cache ||= begin
hostgroup_facets = Facets.registered_facets.select { |_, facet| facet.has_hostgroup_configuration? }
# return sorted list of ancestors with all facets in place
ancestors.includes(hostgroup_facets.keys)
end
end

def inherited_facet_attributes(facet_config)
inherited_attributes = send(facet_config.name).inherited_attributes
hostgroup_ancestry_cache.reverse_each do |hostgroup|
hg_facet = hostgroup.send(facet_config.name)
next unless hg_facet
inherited_attributes.merge!(hg_facet.inherited_attributes) { |_, left, right| left || right }
end

inherited_attributes
end
end
end
32 changes: 32 additions & 0 deletions app/models/concerns/facets/hostgroup_facet.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
require 'facets'

module Facets
module HostgroupFacet
extend ActiveSupport::Concern

included do
belongs_to :hostgroup, class_name: "::Hostgroup", foreign_key: :hostgroup_id
end

module ClassMethods
def inherit_attributes(*attributes)
attributes_to_inherit.concat(attributes).uniq!
end

def attributes_to_inherit
@attributes_to_inherit ||= begin
_, facet_config = Facets.find_facet_by_class(self, :hostgroup)
if facet_config.has_host_configuration? && facet_config.host_configuration.model == self
attribute_names - ['id', 'created_at', 'updated_at']
else
[]
end
end
end
end

def inherited_attributes
attributes.slice(*self.class.attributes_to_inherit)
end
end
end
22 changes: 5 additions & 17 deletions app/models/concerns/facets/managed_host_extensions.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,25 +6,13 @@ module ManagedHostExtensions

included do
include Facets::BaseHostExtensions
Facets::ManagedHostExtensions.refresh_facet_relations(self)
end

class << self
def refresh_facet_relations(klass)
Facets.registered_facets.values.each do |facet_config|
self.register_facet_relation(klass, facet_config)
end
configure_facet(:host, :host, :host_id) do |facet_config|
include_in_clone facet_config.name
end
refresh_facet_relations

# This method is used to add all relation objects necessary for accessing facet from the host object.
# It:
# 1. Includes facet in host's cloning mechanism
def register_facet_relation(klass, facet_config)
Facets::BaseHostExtensions.register_facet_relation(klass, facet_config)

klass.class_eval do
include_in_clone facet_config.name
end
Facets.after_entry_created do |entry|
register_facet_relation(entry) if entry.has_host_configuration?
end
end
end
Expand Down
135 changes: 135 additions & 0 deletions app/models/concerns/facets/model_extensions_base.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
require 'facets'

module Facets
module ModelExtensionsBase
extend ActiveSupport::Concern

included do
include InstanceMethods

refresh_facet_relations
end

module ClassMethods
def configure_facet(facet_type, base_model_symbol, base_model_id_field, &block)
config = OpenStruct.new(
facet_type: facet_type,
base_model_symbol: base_model_symbol,
base_model_id_field: base_model_id_field)
config.callback = block

facet_configurations << config
end

def facet_configurations
@facet_configurations ||= []
end

def refresh_facet_relations
Facets.registered_facets.values.each do |facet_config|
register_facet_relation(facet_config)
end
end

# This method is used to add all relation objects necessary for accessing facet from the host object.
# It:
# 1. Adds active record one to one association
# 2. Adds the ability to set facet's attributes via Host#attributes= method
# 3. Extends Host::Managed model with extension module defined by facet's configuration
# 4. Includes facet in host's cloning mechanism
# 5. Adds compatibility properties forwarders so old property calls will still work after moving them to a facet:
# host.foo # => will call Host.my_facet.foo
def register_facet_relation(facet_config)
facet_configurations.each do |extension_config|
return unless facet_config.has_configuration(extension_config.facet_type)
type_config = facet_config.send "#{extension_config.facet_type}_configuration"
facet_name = facet_config.name

extend_model_attributes(type_config, facet_name, extension_config)
extend_model(type_config, facet_name)
handle_migrations(type_config, facet_name)

extension_config.callback&.call(facet_config)
end
end

def handle_migrations(type_config, facet_name)
return unless Foreman.in_setup_db_rake?
# To prevent running into issues in old migrations when new facet is defined but not migrated yet.
# We define it only when in migration to avoid this unnecessary checks outside for the migration
@facet_relation_db_migrate_extensions ||= {} # prevent duplicates
return if @facet_relation_db_migrate_extensions.key?(facet_name)
@facet_relation_db_migrate_extensions[facet_name] = Module.new do
define_method(facet_name) do
if type_config.model.table_exists?
super()
else
logger.warn("Table for #{facet_name} not defined yet: skipping the facet data")
nil
end
end
end
prepend @facet_relation_db_migrate_extensions[facet_name]
end

def extend_model(type_config, facet_name)
include type_config.extension if type_config.extension

type_config.compatibility_properties&.each do |prop|
define_method(prop) { |*args| forward_property_call(prop, args, facet_name) }
end
end

def extend_model_attributes(type_config, facet_name, extension_config)
has_one facet_name,
:class_name => type_config.model.name,
:foreign_key => extension_config.base_model_id_field,
:inverse_of => extension_config.base_model_symbol,
:dependent => type_config.dependent
accepts_nested_attributes_for facet_name, :update_only => true, :reject_if => :all_blank

alias_method "#{facet_name}_attributes", facet_name
end
end

# define instance methods in a module, so they will be set
# even for models that do not include this module directly
# like in case Hostgroup -> HostgroupExtensions -> ModelExtensionsBase
module InstanceMethods
def attributes
hash = super

# include all facet attributes by default
facets_with_definitions.each do |facet, facet_definition|
hash["#{facet_definition.name}_attributes"] = facet.attributes.reject { |key| %w(created_at updated_at).include? key }
end
hash
end

def facets
facets_with_definitions.keys
end

# This method will return a hash of facets for a specific host including the coresponding definitions.
# The output should look like this:
# { host.puppet_aspect => Facets.registered_facets[:puppet_aspect] }
def facets_with_definitions
facet_types = self.class.facet_configurations.map(&:facet_type)
tuples = Facets.registered_facets(facet_types).values.map do |facet_config|
facet = send(facet_config.name)
[facet, facet_config] if facet
end
Hash[tuples.compact]
end
end

private

def forward_property_call(property, args, facet)
facet_instance = send(facet)
return nil unless facet_instance

facet_instance.send(property, *args)
end
end
end
2 changes: 2 additions & 0 deletions app/models/hostgroup.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ class Hostgroup < ApplicationRecord
include NestedAncestryCommon
include NestedAncestryCommon::Search

include Facets::HostgroupExtensions

validates :name, :presence => true, :uniqueness => {:scope => :ancestry, :case_sensitive => false}

validate :validate_subnet_types
Expand Down
Loading

0 comments on commit 3927dc2

Please sign in to comment.