ManageIQ/manageiq

View on GitHub
app/models/orchestration_template.rb

Summary

Maintainability
A
25 mins
Test Coverage
A
95%
require 'digest/md5'
class OrchestrationTemplate < ApplicationRecord
  include NewWithTypeStiMixin

  acts_as_miq_taggable

  has_many :stacks, :class_name => "OrchestrationStack"
  has_one :picture, :dependent => :destroy, :as => :resource, :autosave => true

  default_value_for :draft, false
  default_value_for :orderable, true

  validates :md5,
            :uniqueness_when_changed => {:scope => :draft, :message => "of content already exists (content must be unique)"},
            :if                      => :unique_md5?
  validates_presence_of :name

  scope :orderable, -> { where(:orderable => true) }

  before_destroy :check_not_in_use

  attr_accessor :remote_proxy
  alias remote_proxy? remote_proxy

  # available templates for ordering an orchestration service
  def self.available
    where(:draft => false, :orderable => true)
  end

  def self.find_with_content(template_content)
    where(:draft => false).find_by(:md5 => calc_md5(with_universal_newline(template_content)))
  end

  # Find only by template content. Here we only compare md5 considering the table is expected
  # to be small and the chance of md5 collision is minimal.
  #
  def self.find_or_create_by_contents(hashes)
    hashes = [hashes] unless hashes.kind_of?(Array)
    md5s = []
    hashes = hashes.reject do |hash|
      if hash[:draft]
        create!(hash.except(:md5)) # always create a new template if it is a draft
        true
      else
        klass = hash[:type].present? ? hash[:type].constantize : self
        md5s << klass.calc_md5(with_universal_newline(hash[:content]))
        false
      end
    end

    existing_templates = where(:draft => false, :md5 => md5s).index_by(&:md5)

    hashes.zip(md5s).collect do |hash, md5|
      template = existing_templates[md5]
      unless template
        template = create!(hash.except(:md5))
        existing_templates[md5] = template
      end
      template
    end
  end

  def content=(text)
    super(with_universal_newline(text))
    self.md5 = calc_md5(content)
  end

  # Determines if validation for md5 uniqueness is done
  def unique_md5?
    !draft?
  end

  # Check whether a template has been referenced by any stack. A template that is in use should be
  # considered read only
  def in_use?
    !stacks.empty?
  end

  # Find all in use and read-only templates
  def self.in_use
    joins(:stacks).distinct
  end

  def valid_service_orchestration_resource
    true
  end

  # Find all not in use thus editable templates
  def self.not_in_use
    includes(:stacks).where(:orchestration_stacks => {:orchestration_template_id => nil})
  end

  def tabs
    [
      {
        :title        => "Basic Information",
        :stack_group  => deployment_options,
        :param_groups => parameter_groups
      }
    ]
  end

  def parameter_groups
    raise NotImplementedError, _("parameter_groups must be implemented in subclass")
  end

  # Basic options for all templates, each subclass should add more type/provider specific deployment options
  # Return array of OrchestrationParameters. (Deployment options are different from parameters, but they use same class)
  def deployment_options(_manager_class = nil)
    stack_name_opt = OrchestrationTemplate::OrchestrationParameter.new(
      :name           => "stack_name",
      :label          => "Stack Name",
      :data_type      => "string",
      :description    => "Name of the stack",
      :required       => true,
      :reconfigurable => false,
      :constraints    => [
        OrchestrationTemplate::OrchestrationParameterPattern.new(
          :pattern => '^[A-Za-z][A-Za-z0-9\-]*$'
        )
      ]
    )
    [stack_name_opt]
  end

  # Typically the provider's cloud or infra manager class name. Providers that need
  # something more advanced should define this within their respective subclass.
  #
  def self.eligible_manager_types
    raise NotImplementedError, _("eligible_manager_types must be implemented in subclass")
  end

  # List managers that may be able to deploy this template
  def self.eligible_managers
    Rbac::Filterer.filtered(ExtManagementSystem, :named_scope => [[:with_eligible_manager_types, eligible_manager_types]])
  end

  delegate :eligible_managers, :to => :class

  def self.stack_type
    "OrchestrationStack"
  end

  delegate :stack_type, :to => :class

  # return the validation error message; otherwise nil
  def validate_content(manager = nil)
    test_managers = manager.nil? ? eligible_managers : [manager]
    test_managers.each do |mgr|
      return mgr.orchestration_template_validate(self) rescue nil
    end
    "No provider is capable to validate the template"
  end

  def validate_format
    raise NotImplementedError, _("validate_format must be implemented in subclass")
  end

  # use cases for md5 conflict:
  # draft: always save, new or existing
  # existing:
  #   discovered duplicate: take over stacks and delete the discovered one
  #   orderable duplicate: raise error through save! validation
  # new:
  #   discovered duplicate: promote discovered to orderable
  #   orderable duplicate: raise error through save! validation)
  def save_as_orderable!
    error_msg = validate_format unless draft
    raise MiqException::MiqParsingError, error_msg if error_msg

    self.orderable = true
    return save! if draft?

    old_template = self.class.find_with_content(content)
    return save! if old_template.nil? || old_template.orderable || old_template.id == id

    new_record? ? replace_with_old_template(old_template) : transfer_stacks(old_template)
  end

  private

  # This is an unsaved template. Replace with an existing one after it is updated
  def replace_with_old_template(old_template)
    old_template.update(:name => name, :description => description, :orderable => true)
    self.id = old_template.id
    reload
    true
  end

  # Take over stacks belongs to the old template and delete the old template
  def transfer_stacks(old_template)
    old_template.stacks.update_all(:orchestration_template_id => id)
    old_template.delete
    save!
  end

  def md5=(_md5)
    super
  end

  def self.calc_md5(text)
    Digest::MD5.hexdigest(text) if text
  end

  def calc_md5(text)
    self.class.calc_md5(text)
  end

  def self.with_universal_newline(text)
    # ensure universal new lines and content ending with a new line
    text.encode(:universal_newline => true).chomp.concat("\n")
  end

  def with_universal_newline(text)
    self.class.with_universal_newline(text)
  end

  def check_not_in_use
    return true unless in_use?
    errors.add(:base, "Cannot delete the template while it is used by some orchestration stacks")
    throw :abort
  end
end