theforeman/foreman

View on GitHub
app/models/smart_proxy.rb

Summary

Maintainability
A
55 mins
Test Coverage
class SmartProxy < ApplicationRecord
  audited
  include Authorizable
  extend FriendlyId
  friendly_id :name
  include Taxonomix
  include Parameterizable::ByIdName

  validates_lengths_from_database
  before_destroy EnsureNotUsedBy.new(:hosts, :hostgroups, :subnets, :domains, [:puppet_ca_hosts, :hosts], [:puppet_ca_hostgroups, :hostgroups], :realms)
  # TODO check if there is a way to look into the tftp_id too
  # maybe with a predefined sql
  has_many :smart_proxy_features, :dependent => :destroy
  has_many :features, :through => :smart_proxy_features
  has_many :subnets,                                          :foreign_key => 'dhcp_id'
  has_many :domains,                                          :foreign_key => 'dns_id'
  has_many_hosts                                              :foreign_key => 'puppet_proxy_id'
  has_many :hostgroups,                                       :foreign_key => 'puppet_proxy_id'
  has_many :puppet_ca_hosts, :class_name => 'Host::Managed',  :foreign_key => 'puppet_ca_proxy_id'
  has_many :puppet_ca_hostgroups, :class_name => 'Hostgroup', :foreign_key => 'puppet_ca_proxy_id'
  has_many :realms,                                           :foreign_key => 'realm_proxy_id'
  validates :name, :uniqueness => true, :presence => true
  validates :url, :presence => true, :url_schema => ['http', 'https'],
    :uniqueness => { :message => N_('Only one declaration of a proxy is allowed') }
  has_many :infrastructure_host_facets, :class_name => '::HostFacets::InfrastructureFacet', :dependent => :nullify
  has_many :smart_proxy_hosts, :through => :infrastructure_host_facets, :source => :host

  # There should be no problem with associating features before the proxy is saved as the whole operation is in a transaction
  before_save :sanitize_url, :associate_features

  scoped_search :on => :id, :complete_enabled => false, :only_explicit => true, :validator => ScopedSearch::Validators::INTEGER
  scoped_search :on => :name, :complete_value => :true
  scoped_search :on => :url, :complete_value => :true
  scoped_search :relation => :features, :on => :name, :rename => :feature, :complete_value => :true

  # with proc support, default_scope can no longer be chained
  # include all default scoping here
  default_scope lambda {
    with_taxonomy_scope do
      order('smart_proxies.name')
    end
  }

  scope :with_features, ->(*feature_names) { where(:features => { :name => feature_names }).joins(:features) if feature_names.any? }

  def hostname
    URI(url).host
  end

  def to_s
    hostname
  end

  def hosts_count
    Host::Managed.search_for("smart_proxy = #{name}").count
  end

  def refresh
    statuses.values.each { |status| status.revoke_cache! }
    associate_features
    errors
  end

  def ping
    begin
      reply = get_features
      unless reply.is_a?(Hash)
        logger.debug("Invalid response from proxy #{name}: Expected Hash of features, got #{reply}.")
        errors.add(:base, _('An invalid response was received while requesting available features from this proxy'))
      end
    rescue => e
      errors.add(:base, _('Unable to communicate with the proxy: %s') % e)
    end
    !errors.any?
  end

  def taxonomy_foreign_conditions
    conditions = {}
    if has_feature?('Puppet') && has_feature?('Puppet CA')
      conditions = "puppet_proxy_id = #{id} OR puppet_ca_proxy_id = #{id}"
    elsif has_feature?('Puppet')
      conditions[:puppet_proxy_id] = id
    elsif has_feature?('Puppet CA')
      conditions[:puppet_ca_proxy_id] = id
    end
    conditions
  end

  def smart_proxy_feature_by_name(feature_name)
    feature_id = Feature.find_by(:name => feature_name).try(:id)
    # loop through the in memory object to work on unsaved objects
    smart_proxy_features.find { |spf| spf.feature_id == feature_id }
  end

  def has_feature?(feature_name)
    feature_ids = Feature.where(:name => feature_name).pluck(:id)
    smart_proxy_features.any? { |proxy_feature| feature_ids.include?(proxy_feature.feature_id) }
  end

  def capabilities(feature)
    smart_proxy_feature_by_name(feature).try(:capabilities)
  end

  def has_capability?(feature, capability)
    capabilities(feature)&.include?(capability.to_s)
  end

  def setting(feature, setting)
    smart_proxy_feature_by_name(feature).try(:settings).try(:[], setting)
  end

  def httpboot_http_port
    setting(:HTTPBoot, 'http_port')
  end

  def httpboot_http_port!
    httpboot_http_port || raise(::Foreman::Exception.new(N_("HTTP boot requires proxy with httpboot feature and http_port exposed setting")))
  end

  def httpboot_https_port
    setting(:HTTPBoot, 'https_port')
  end

  def httpboot_https_port!
    httpboot_https_port || raise(::Foreman::Exception.new(N_("HTTPS boot requires proxy with httpboot feature and https_port exposed setting")))
  end

  def statuses
    return @statuses if @statuses
    @statuses = {}
    features.each do |feature|
      name = feature.name.delete(' ')
      if (status = ProxyStatus.find_status_by_humanized_name(name))
        @statuses[name.downcase.to_sym] = status.new(self)
      end
    end
    @statuses[:version] = ProxyStatus::Version.new(self)

    @statuses
  end

  def feature_details
    smart_proxy_features.includes(:feature).each_with_object({}) do |smart_proxy_feature, hash|
      hash[smart_proxy_feature.feature.name] = smart_proxy_feature.details
    end
  end

  private

  def sanitize_url
    self.url = url.chomp('/') unless url.empty?
  end

  def associate_features
    begin
      reply = get_features
      unless reply.is_a?(Hash)
        logger.debug("Invalid response from proxy #{name}: Expected Hash or Array of features, got #{reply}.")
        errors.add(:base, _('An invalid response was received while requesting available features from this proxy'))
        throw :abort
      end

      feature_name_map = Feature.name_map
      valid_features = reply.select { |feature, options| feature_name_map.key?(feature) }

      if valid_features.any?
        SmartProxyFeature.import_features(self, valid_features)
      else
        smart_proxy_features.clear
        if reply.any?
          errors.add :base, _('Features "%s" in this proxy are not recognized by Foreman. '\
                              'If these features come from a Smart Proxy plugin, make sure Foreman has the plugin installed too.') % reply.keys.to_sentence
        else
          errors.add :base, _('No features found on this proxy, please make sure you enable at least one feature')
        end
      end
    rescue => e
      errors.add(:base, _('Unable to communicate with the proxy: %s') % e)
      errors.add(:base, _('Please check the proxy is configured and running on the host.'))
    end
    throw :abort if smart_proxy_features.empty?
  end

  def get_features
    begin
      reply = ProxyAPI::V2::Features.new(:url => url).features.with_indifferent_access
      reply.reject! { |name| reply[name]['state'] != 'running' }
    rescue NotImplementedError
      reply = ProxyAPI::Features.new(:url => url).features
    end

    if reply.is_a?(Array)
      Hash[reply.collect { |f| [f, {}] }]
    else
      reply
    end
  end

  apipie :class do
    name 'Smart Proxy'
    refs 'SmartProxy'
    sections only: %w[all additional]
    prop_group :basic_model_props, ApplicationRecord, meta: { friendly_name: 'Smart Proxy' }
    property :hostname, String, desc: 'Returns name of the host with proxy'
    property :httpboot_http_port, Integer, desc: 'Returns proxy port for HTTP boot'
    property :httpboot_http_port!, Integer, desc: 'Same as httpboot_http_port, but raises Foreman::Exception if no port is set'
    property :httpboot_https_port, Integer, desc: 'Returns proxy port for HTTPS boot'
    property :httpboot_https_port!, Integer, desc: 'Same as httpboot_https_port, but raises Foreman::Exception if no port is set'
  end
  class Jail < ::Safemode::Jail
    allow :id, :name, :hostname, :httpboot_http_port, :httpboot_https_port, :httpboot_http_port!, :httpboot_https_port!, :url
  end
end