ManageIQ/manageiq

View on GitHub
app/models/miq_request_workflow.rb

Summary

Maintainability
F
3 days
Test Coverage
D
64%
require 'ostruct'

class MiqRequestWorkflow
  include Vmdb::Logging
  include DialogFieldValidation

  # We rely on MiqRequestWorkflow's descendants to be comprehensive
  singleton_class.send(:prepend, DescendantLoader::ArDescendantsWithLoader)

  attr_accessor :requester, :values, :last_vm_id
  attr_writer :dialogs

  def self.automate_dialog_request
    nil
  end

  def self.default_dialog_file
    nil
  end

  def self.default_pre_dialog_file
    nil
  end

  def self.encrypted_options_fields
    []
  end

  def self.encrypted_options_field_regs
    encrypted_options_fields.map { |f| /\[:#{f}\]/ } + [/password::/]
  end

  def self.all_encrypted_options_fields
    descendants.flat_map(&:encrypted_options_fields).uniq
  end

  def self.update_requester_from_parameters(data, user)
    return user if data[:user_name].blank?

    new_user = User.lookup_by_identity(data[:user_name])

    unless new_user
      _log.error("requested not changed to <#{data[:user_name]}> due to a lookup failure")
      raise ActiveRecord::RecordNotFound
    end

    _log.warn("requested changed to <#{new_user.userid}>")
    new_user
  end

  def initialize(values, requester, options = {})
    instance_var_init(values, requester, options)

    unless options[:skip_dialog_load] == true
      # If this is the first time we are called the values hash will be empty
      # Also skip if we are being called from a web-service
      if @dialogs.nil?
        @dialogs = get_dialogs
        normalize_numeric_fields
      else
        @running_pre_dialog = true if options[:use_pre_dialog] != false
      end
    end

    unless options[:skip_dialog_load] == true
      set_default_values
      update_field_visibility
    end
  end

  def instance_var_init(values, requester, options)
    @values       = values
    @filters      = {}
    @requester    = requester.kind_of?(User) ? requester : User.lookup_by_identity(requester)
    group_description = values[:requester_group]
    if group_description && group_description != @requester.miq_group_description
      @requester = @requester.clone
      @requester.current_group_by_description = group_description
    end
    @values.merge!(options) if options.present?
  end

  # Helper method when not using workflow
  def make_request(request, values, _requester = nil, auto_approve = false)
    return false unless validate(values)

    password_helper(values, true)
    # Ensure that tags selected in the pre-dialog get applied to the request
    values[:vm_tags] = (Array.wrap(values[:vm_tags]) + @values[:pre_dialog_vm_tags]).uniq if @values.try(:[], :pre_dialog_vm_tags).present?

    set_request_values(values)
    if request
      MiqRequest.update_request(request, values, @requester)
    else
      req = request_class.new(:options => values, :requester => @requester, :request_type => request_type.to_s)
      return req unless req.valid? # TODO: CatalogController#atomic_req_submit is the only one that enumerates over the errors

      values[:__request_type__] = request_type.to_s.presence # Pass this along to MiqRequest#create_request
      request_class.create_request(values, @requester, auto_approve)
    end
  end

  def init_from_dialog(init_values)
    @dialogs[:dialogs].keys.each do |dialog_name|
      get_all_fields(dialog_name).each_pair do |field_name, field_values|
        next unless init_values[field_name].nil?
        next if field_values[:display] == :ignore

        if !field_values[:default].nil?
          val = field_values[:default]
        end

        if field_values[:values]
          if field_values[:values].kind_of?(Hash)
            # Save [value, description], skip for timezones array
            init_values[field_name] = [val, field_values[:values][val]]
          else
            field_values[:values].each do |tz|
              next unless tz.kind_of?(Array)

              if tz[1].to_i_with_method == val.to_i_with_method
                # Save [value, description] for timezones array
                init_values[field_name] = [val, tz[0]]
              end
            end
          end
        else
          # Set to default value
          init_values[field_name] = val
        end
      end
    end
  end

  def validate(values)
    # => Input - A hash keyed by field name with entered values
    # => Output - true || false
    #
    # Update @dialogs adding error keys to fields that don't validate
    valid = true

    get_all_dialogs(false).each do |d, dlg|
      # Check if the entire dialog is ignored or disabled and check while processing the fields
      dialog_disabled = !dialog_active?(d, dlg, values)

      get_all_fields(d, false).each do |f, fld|
        fld[:error] = nil

        # Check the disabled flag here so we reset the "error" value on each field
        next if dialog_disabled || fld[:display] == :hide

        value = /array_/.match?(fld[:data_type]) ? values[f] : get_value(values[f])

        if fld[:required] == true
          # If :required_method is defined let it determine if the field is value
          if fld[:required_method].nil?
            default_require_method = "default_require_#{f}".to_sym
            if respond_to?(default_require_method)
              fld[:error] = send(default_require_method, f, values, dlg, fld, value)
              unless fld[:error].nil?
                valid = false
                next
              end
            else
              if value.blank?
                fld[:error] = "#{required_description(dlg, fld)} is required"
                valid = false
                next
              end
            end
          else
            Array.wrap(fld[:required_method]).each do |method|
              fld[:error] = send(method, f, values, dlg, fld, value)
              # Bail out early if we see an error
              break unless fld[:error].nil?
            end

            unless fld[:error].nil?
              valid = false
              next
            end
          end
        end

        if fld[:validation_method] && respond_to?(fld[:validation_method])
          if (fld[:error] = send(fld[:validation_method], f, values, dlg, fld, value))
            valid = false
            next
          end
        end

        next if value.blank?

        msg = "'#{fld[:description]}' in dialog #{dlg[:description]} must be of type #{fld[:data_type]}"
        validate_data_types(value, fld, msg, valid)
      end
    end

    valid
  end

  def validate_data_types(value, fld, msg, valid)
    case fld[:data_type]
    when :integer
      unless is_integer?(value)
        fld[:error] = msg
        valid = false
      end
    when :float
      unless is_numeric?(value)
        fld[:error] = msg
        valid = false
      end
    when :boolean
      # TODO: do we need validation for boolean
    when :button
      # Ignore
    when :array_integer
      unless value.kind_of?(Array)
        fld[:error] = msg
        valid = false
      end
    else
      data_type = Object.const_get(fld[:data_type].to_s.camelize)
      unless value.kind_of?(data_type)
        fld[:error] = msg
        valid = false
      end
    end
    [valid, fld]
  end

  def get_dialog_order
    @dialogs[:dialog_order]
  end

  def get_buttons
    @dialogs[:buttons] || [:submit, :cancel]
  end

  def provisioning_tab_list
    dialog_names = @dialogs[:dialog_order].collect(&:to_s)
    dialog_descriptions = dialog_names.collect do |dialog_name|
      @dialogs.fetch_path(:dialogs, dialog_name.to_sym, :description)
    end
    dialog_display = dialog_names.collect do |dialog_name|
      @dialogs.fetch_path(:dialogs, dialog_name.to_sym, :display)
    end

    tab_list = []
    dialog_names.each_with_index do |dialog_name, index|
      next if dialog_display[index] == :hide || dialog_display[index] == :ignore

      tab_list << {
        :name        => dialog_name,
        :description => dialog_descriptions[index]
      }
    end

    tab_list
  end

  def get_all_dialogs(refresh_values = true)
    @dialogs[:dialogs].each_key { |d| get_dialog(d, refresh_values) }
    @dialogs[:dialogs]
  end

  def get_dialog(dialog_name, refresh_values = true)
    dialog = @dialogs.fetch_path(:dialogs, dialog_name.to_sym)
    return {} unless dialog

    get_all_fields(dialog_name, refresh_values)
    dialog
  end

  def get_all_fields(dialog_name, refresh_values = true)
    dialog = @dialogs.fetch_path(:dialogs, dialog_name.to_sym)
    return {} unless dialog

    dialog[:fields].each_key { |f| get_field(f, dialog_name, refresh_values) }
    dialog[:fields]
  end

  def get_field(field_name, dialog_name = nil, refresh_values = true)
    field_name = field_name.to_sym
    dialog_name = find_dialog_from_field_name(field_name) if dialog_name.nil?
    field = @dialogs.fetch_path(:dialogs, dialog_name.to_sym, :fields, field_name)
    return {} unless field

    if field.key?(:values_from) && refresh_values
      options = field[:values_from][:options] || {}
      options[:prov_field_name] = field_name
      field[:values] = send(field[:values_from][:method], options)

      # Reset then currently selected item if it no longer appears in the available values
      if field[:values].kind_of?(Hash)
        if field[:values].length == 1
          unless field[:auto_select_single] == false
            @values[field_name] = field[:values].to_a.first
          end
        else
          currently_selected = get_value(@values[field_name])
          unless currently_selected.nil? || field[:values].key?(currently_selected)
            @values[field_name] = [nil, nil]
          end
        end
      end
    end
    field
  end

  # TODO: Return list in defined ordered
  def dialogs(&block)
    @dialogs[:dialogs].each_pair(&block)
  end

  def fields(dialog = nil)
    dialog = [*dialog] unless dialog.nil?
    @dialogs[:dialogs].each_pair do |dn, d|
      next unless dialog.blank? || dialog.include?(dn)

      d[:fields].each_pair do |fn, f|
        yield(fn, f, dn, d)
      end
    end
  end

  def normalize_numeric_fields
    fields do |_fn, f, _dn, _d|
      if f[:data_type] == :integer
        f[:default] = f[:default].to_i_with_method if f[:default].present?
        if f[:values].present?
          keys = f[:values].keys.dup
          keys.each { |k| f[:values][k.to_i_with_method] = f[:values].delete(k) }
        end
      end
    end
  end

  # Helper method to write message to the rails log (production.log) for debugging
  def rails_logger(_name, _start)
    # Rails.logger.warn("#{name} #{start.zero? ? 'start' : 'end'}")
  end

  def parse_ws_string(text_input, options = {})
    self.class.parse_ws_string(text_input, options)
  end

  def self.parse_ws_string(text_input, options = {})
    return parse_request_parameter_hash(text_input, options) if text_input.kind_of?(Hash)
    return {} unless text_input.kind_of?(String)

    deprecated_warn = "method: parse_ws_string, arg Type => String"
    solution = "arg should be a hash"
    MiqAeMethodService::Deprecation.deprecation_warning(deprecated_warn, solution)

    result = {}
    text_input.split('|').each do |value|
      next if value.blank?

      idx = value.index('=')
      next if idx.nil?

      key = options[:modify_key_name] == false ? value[0, idx].strip : value[0, idx].strip.downcase.to_sym
      result[key] = value[idx + 1..-1].strip
    end
    result
  end

  def self.parse_request_parameter_hash(parameter_hash, options = {})
    parameter_hash.each_with_object({}) do |param, hash|
      key, value = param
      key = key.strip.downcase.to_sym unless options[:modify_key_name] == false
      hash[key] = value
    end
  end

  def ws_tags(tag_string, parser = :parse_ws_string)
    # Tags are passed as category|value.  Example: cc|001|environment|test
    ws_tags = send(parser, tag_string)

    tags = allowed_tags.each_with_object({}) do |h, atags|
      atags[h[:name]] = h[:children].each_with_object({}) { |(k, v), tc| tc[v[:name]] = k }
    end

    ws_tags.collect { |cat, tag| tags.fetch_path(cat.to_s.downcase, tag.downcase) }.compact
  end

  # @param parser [:parse_ws_string|:parse_ws_string_v1]
  # @param additional_values [String] values of the form cc=001|environment=test
  def ws_values(additional_values, parser = :parse_ws_string, parser_options = {})
    parsed_values = send(parser, additional_values, parser_options)

    parsed_values.each_with_object({}) { |(k, v), ws_values| ws_values[k.to_sym] = v }
  end

  def parse_ws_string_v1(values, _options = {})
    na = []
    values.to_s.split("|").each_slice(2) do |k, v|
      next if v.nil?

      na << [k.strip, v.strip]
    end
    na
  end

  def find_dialog_from_field_name(field_name)
    @dialogs[:dialogs].each_key do |dialog_name|
      return dialog_name if @dialogs[:dialogs][dialog_name][:fields].key?(field_name.to_sym)
    end
    nil
  end

  def get_value(data)
    data.kind_of?(Array) ? data.first : data
  end

  def set_or_default_field_values(values)
    field_names = values.keys
    fields do |fn, f, _dn, _d|
      if field_names.include?(fn)
        if f.key?(:values)
          selected_key = nil
          if f[:values].key?(values[fn])
            selected_key = values[fn]
          elsif f.key?(:default) && f[:values].key?(f[:default])
            selected_key = f[:default]
          else
            if f[:values].present?
              sorted_values = f[:values].sort
              selected_key = sorted_values.first.first
            end
          end
          @values[fn] = [selected_key, f[:values][selected_key]] unless selected_key.nil?
        else
          @values[fn] = values[fn]
        end
      end
    end
  end

  def clear_field_values(field_names)
    fields do |fn, f, _dn, _d|
      if field_names.include?(fn)
        @values[fn] = f.key?(:values) ? [nil, nil] : nil
      end
    end
  end

  def set_value_from_list(fn, f, value, values = nil, partial_key = false)
    @values[fn] = [nil, nil]
    values = f[:values] if values.nil?
    unless value.nil?
      @values[fn] = values.to_a.detect do |v|
        if partial_key
          _log.warn("comparing [#{v[0]}] to [#{value}]")
          v[0].to_s.downcase.include?(value.to_s.downcase)
        else
          v.include?(value)
        end
      end
      if @values[fn].nil?
        _log.info("set_value_from_list did not matched an item") if partial_key
        @values[fn] = [nil, nil]
      else
        _log.info("set_value_from_list matched item value:[#{value}] to item:[#{@values[fn][0]}]") if partial_key
      end
    end
  end

  def show_dialog(dialog_name, show_flag, enabled_flag = nil)
    dialog = @dialogs.fetch_path(:dialogs, dialog_name.to_sym)
    unless dialog.nil?
      dialog[:display_init] = dialog[:display] if dialog[:display_init].nil?
      # If the initial dialog is not set to show then do not modify it here.
      return if dialog[:display_init] != :show

      dialog[:display] = show_flag
      @values["#{dialog_name}_enabled".to_sym] = [enabled_flag] unless enabled_flag.nil?
    end
  end

  def required_description(dlg, fld)
    "'#{dlg[:description]}/#{fld[:required_description] || fld[:description]}'"
  end

  def allowed_filters(options = {})
    model_name = options[:category]
    return @filters[model_name] unless @filters[model_name].nil?

    rails_logger("allowed_filters - #{model_name}", 0)
    @filters[model_name] = @requester.get_expressions(model_name).invert
    rails_logger("allowed_filters - #{model_name}", 1)
    @filters[model_name]
  end

  def dialog_active?(name, config, values)
    return false if config[:display] == :ignore

    enabled_field = "#{name}_enabled".to_sym
    # Check if the fields hash contains a <dialog_name>_enabled field
    enabled = get_value(values[enabled_field])
    return false if [false, "disabled"].include?(enabled)

    true
  end

  def show_fields(display_flag, field_names, display_field = :display)
    fields do |fn, f, _dn, _d|
      if field_names.include?(fn)
        flag = (f[:display_override].presence || display_flag)
        f[display_field] = flag
      end
    end
  end

  def retrieve_ldap(_options = {})
    email = get_value(@values[:owner_email])
    if email.present?
      l = MiqLdap.new
      if l.bind_with_default == true
        raise _("No information returned for %{email}") % {:email => email} if (d = l.get_user_info(email, "mail")).nil?

        [:first_name, :last_name, :address, :city, :state, :zip, :country, :title, :company,
         :department, :office, :phone, :phone_mobile, :manager, :manager_mail, :manager_phone].each do |prop|
          @values["owner_#{prop}".to_sym] = d[prop].try(:dup)
        end
        @values[:sysprep_organization] = d[:company].try(:dup)
      end
    end
  end

  def default_schedule_time(options = {})
    # TODO: Added support for "default_from", like values_from, that gets called once after dialog creation
    # Update VM description
    fields do |fn, f, _dn, _d|
      if fn == :schedule_time
        f[:default] = Time.now + options[:offset].to_i_with_method if f[:default].nil?
        break
      end
    end
  end

  def values_less_then(options)
    results = options[:values].transform_keys(&:to_i_with_method)
    field, include_equals = options[:field], options[:include_equals]
    max_value = field.nil? ? options[:value].to_i_with_method : get_value(@values[field]).to_i_with_method
    return results if max_value <= 0

    results.reject { |k, _v| include_equals == true ? max_value < k : max_value <= k }
  end

  def tags
    vm_tags = @values[:vm_tags]
    return unless vm_tags.kind_of?(Array)

    vm_tags.each do |tag_id|
      tag = Classification.find(tag_id)
      yield(tag.name, tag.parent.name) # yield the tag's name and category
    end
  end

  def get_tags
    tag_string = ''
    tags do |tag, cat|
      tag_string << ':' unless tag_string.empty?
      tag_string << "#{cat}/#{tag}"
    end
    tag_string
  end

  def allowed_tags(options = {})
    return @tags unless @tags.nil?

    region_number = options.delete(:region_number)

    # TODO: Call allowed_tags properly from controller - it is currently hard-coded with no options passed
    field_options = @dialogs.fetch_path(:dialogs, :purpose, :fields, :vm_tags, :options)
    options = field_options unless field_options.nil?

    rails_logger('allowed_tags', 0)
    st = Time.now
    @tags = {}

    exclude_list  = options[:exclude].blank? ? [] : options[:exclude].collect(&:to_s)
    include_list  = options[:include].blank? ? [] : options[:include].collect(&:to_s)
    single_select = options[:single_select].blank? ? [] : options[:single_select].collect(&:to_s)

    cats = Classification.visible.writeable.managed
    cats = cats.in_region(region_number) if region_number
    cats.each do |t|
      next if exclude_list.include?(t.name)
      next unless include_list.blank? || include_list.include?(t.name)

      # Force passed tags to be single select
      single_value = single_select.include?(t.name) ? true : t.single_value?
      @tags[t.id] = {:name => t.name, :description => t.description, :single_value => single_value, :children => {}, :id => t.id}
    end

    ents = Classification.visible.writeable.parent_ids(@tags.keys).with_tag_name
    ents = ents.in_region(region_number) if region_number
    ents.each do |t|
      full_tag_name = "#{@tags[t.parent_id][:name]}/#{t.name}"
      next if exclude_list.include?(full_tag_name)

      @tags[t.parent_id][:children][t.id] = {:name => t.name, :description => t.description}
    end

    @tags.delete_if { |_k, v| v[:children].empty? }

    # Now sort the tags based on the order passed options.  All remaining tags not defined in the order
    # will be sorted by description and appended to the other sorted tags
    tag_results, tags_to_sort = [], []
    sort_order = options[:order].blank? ? [] : options[:order].collect(&:to_s)
    @tags.each do |_k, v|
      (idx = sort_order.index(v[:name])).nil? ? tags_to_sort << v : tag_results[idx] = v
    end

    tags_to_sort = tags_to_sort.sort_by { |a| a[:description] }
    @tags = tag_results.compact + tags_to_sort

    @tags.each do |tag|
      tag[:children] = if /^\d/.match?(tag[:children].first.last[:name])
                         tag[:children].sort_by { |_k, v| v[:name].to_i }
                       else
                         tag[:children].sort_by { |_k, v| v[:description] }
                       end
    end

    rails_logger('allowed_tags', 1)
    _log.info("allowed_tags returned [#{@tags.length}] objects in [#{Time.now - st}] seconds")
    @tags
  end

  def allowed_tags_and_pre_tags
    pre_tags = Array.wrap(@values[:pre_dialog_vm_tags])
    return allowed_tags if pre_tags.blank?

    tag_cats = allowed_tags.dup
    tag_cat_names = tag_cats.collect { |cat| cat[:name] }

    Classification.where(:id => pre_tags).each do |tag|
      parent = tag.parent
      next if tag_cat_names.include?(parent.name)

      new_cat = {:name => parent.name, :description => parent.description, :single_value => parent.single_value?, :children => {}, :id => parent.id}
      parent.children.each { |c| new_cat[:children][c.id] = {:name => c.name, :description => c.description} }
      tag_cats << new_cat
      tag_cat_names << new_cat[:name]
    end

    tag_cats
  end

  def tag_symbol
    :tag_ids
  end

  def build_ci_hash_struct(ci, props)
    nh = OpenStruct.new(:id => ci.id, :evm_object_class => ci.class.base_class.name.to_sym)
    props.each { |p| nh.send(:"#{p}=", ci.send(p)) }
    nh
  end

  def get_dialogs
    @values[:miq_request_dialog_name] ||= @values[:provision_dialog_name] || dialog_name_from_automate || self.class.default_dialog_file
    dp = @values[:miq_request_dialog_name] = File.basename(@values[:miq_request_dialog_name], ".rb")
    _log.info("Loading dialogs <#{dp}> for user <#{@requester.userid}>")
    d = MiqDialog.find_by("lower(name) = ? and dialog_type = ?", dp.downcase, self.class.base_model.name)
    if d.nil?
      raise MiqException::Error,
            "Dialog cannot be found.  Name:[%{name}]  Type:[%{type}]" % {:name => @values[:miq_request_dialog_name],
                                                                         :type => self.class.base_model.name}
    end
    d.content
  end

  def get_pre_dialogs
    pre_dialogs = nil
    pre_dialog_name = dialog_name_from_automate('get_pre_dialog_name')
    if pre_dialog_name.present?
      pre_dialog_name = File.basename(pre_dialog_name, ".rb")
      d = MiqDialog.find_by(:name => pre_dialog_name, :dialog_type => self.class.base_model.name)
      unless d.nil?
        _log.info("Loading pre-dialogs <#{pre_dialog_name}> for user <#{@requester.userid}>")
        pre_dialogs = d.content
      end
    end

    pre_dialogs
  end

  def dialog_name_from_automate(message = 'get_dialog_name', input_fields = [:request_type], extra_attrs = {})
    return nil if self.class.automate_dialog_request.nil?

    _log.info("Querying Automate Profile for dialog name")
    attrs = {'request' => self.class.automate_dialog_request, 'message' => message}
    extra_attrs.each { |k, v| attrs[k] = v }

    @values.each_key do |k|
      key = "dialog_input_#{k.to_s.downcase}"
      if attrs.key?(key)
        _log.info("Skipping key=<#{key}> because already set to <#{attrs[key]}>")
      else
        value = (k == :vm_tags) ? get_tags : get_value(@values[k]).to_s
        _log.info("Setting attrs[#{key}]=<#{value}>")
        attrs[key] = value
      end
    end

    input_fields.each { |k| attrs["dialog_input_#{k.to_s.downcase}"] = send(k).to_s }

    ws = MiqAeEngine.resolve_automation_object("REQUEST", @requester, attrs, :vmdb_object => @requester)

    if ws && ws.root
      dialog_option_prefix = 'dialog_option_'
      dialog_option_prefix_length = dialog_option_prefix.length
      ws.root.attributes.each do |key, value|
        next unless key.downcase.starts_with?(dialog_option_prefix)
        next unless key.length > dialog_option_prefix_length

        key = key[dialog_option_prefix_length..-1].downcase
        _log.info("Setting @values[#{key}]=<#{value}>")
        @values[key.to_sym] = value
      end

      name = ws.root("dialog_name")
      return name.presence
    end

    nil
  end

  def self.request_type(type)
    type.presence.try(:to_sym) || request_class.request_types.first
  end

  def request_type
    self.class.request_type(get_value(@values[:request_type]))
  end

  def request_class
    req_class = self.class.request_class
    return req_class unless get_value(@values[:service_template_request]) == true

    (req_class.name + "Template").constantize
  end

  def self.request_class
    @workflow_class ||= name.underscore.gsub(/_workflow$/, "_request").camelize.constantize
  end

  def set_default_values
    set_default_user_info rescue nil
  end

  def set_default_user_info
    return if get_dialog(:requester).blank?

    if get_value(@values[:owner_email]).blank? && @requester.email.present?
      @values[:owner_email] = @requester.email
      retrieve_ldap if MiqLdap.using_ldap?
    end

    show_flag = MiqLdap.using_ldap? ? :show : :hide
    show_fields(show_flag, [:owner_load_ldap])
  end

  def set_request_values(values)
    values[:requester_group] ||= @requester.current_group.description
    email = values[:owner_email]
    if email.present? && values[:owner_group].blank?
      values[:owner_group] = User.lookup_by_lower_email(email, @requester).try(:miq_group_description)
    end
  end

  def password_helper(values = @values, encrypt = true)
    self.class.encrypted_options_fields.each do |pwd_key|
      next if values[pwd_key].blank?

      if encrypt
        values[pwd_key].replace(ManageIQ::Password.try_encrypt(values[pwd_key]))
      else
        values[pwd_key].replace(ManageIQ::Password.try_decrypt(values[pwd_key]))
      end
    end
  end

  def update_field_visibility
  end

  # Subclasses should define this as appropriate.
  #
  def get_source_and_targets(_refresh = false)
    raise NotImplementedError, _("get_source_and_targets must be implemented in a subclass")
  end

  def refresh_field_values(values)
    st = Time.now

    @values = values

    get_source_and_targets(true)

    # @values gets modified during this call
    get_all_dialogs

    values.merge!(@values)

    # Update the display flag for fields based on current settings
    update_field_visibility

    _log.info("refresh completed in [#{Time.now - st}] seconds")
  rescue => err
    $log.log_backtrace(err)
    raise
  end

  # Run the relationship methods and perform set intersections on the returned values.
  # Optional starting set of results maybe passed in.
  def allowed_ci(ci, relats, sources, filtered_ids = nil)
    result = nil
    relats.each do |rsc_type|
      rails_logger("allowed_ci - #{rsc_type}_to_#{ci}", 0)
      rc = send(:"#{rsc_type}_to_#{ci}", sources)
      rails_logger("allowed_ci - #{rsc_type}_to_#{ci}", 1)
      unless rc.nil?
        rc = rc.to_a
        result = result.nil? ? rc : result & rc
      end
    end
    result = [] if result.nil?
    result.reject! { |k, _v| !filtered_ids.include?(k) } unless filtered_ids.nil?
    result.each_with_object({}) { |s, hash| hash[s[0]] = s[1] }
  end

  def process_filter(filter_prop, ci_klass, targets)
    rails_logger("process_filter - [#{ci_klass}]", 0)
    filter_id = get_value(@values[filter_prop]).to_i
    MiqSearch.filtered(filter_id, ci_klass, targets,
                       :user      => @requester,
                       :miq_group => @requester.current_group
                      ).tap { rails_logger("process_filter - [#{ci_klass}]", 1) }
  end

  def find_all_ems_of_type(klass, src = nil)
    result = []
    each_ems_metadata(src, klass) { |ci| result << ci }
    result
  end

  def find_hosts_under_ci(item)
    find_classes_under_ci(item, Host)
  end

  def find_respools_under_ci(item)
    find_classes_under_ci(item, ResourcePool)
  end

  def find_classes_under_ci(item, klass)
    results = []
    return results if item.nil?

    @_find_classes_under_ci_prefix ||= _log.prefix
    node = load_ems_node(item, @_find_classes_under_ci_prefix)
    each_ems_metadata(node.attributes[:object], klass) { |ci| results << ci } unless node.nil?
    results
  end

  def load_ems_node(item, log_header)
    @ems_xml_nodes ||= {}
    klass_name = if item.kind_of?(OpenStruct)
                   Object.const_get(item.evm_object_class)
                 else
                   item.class.base_class
                 end
    node = @ems_xml_nodes.fetch(klass_name, {})[item.id]
    $log.error("#{log_header} Resource <#{klass_name}_#{item.id} - #{item.name}> not found in cached resource tree.") if node.nil?
    node
  end

  def ems_has_clusters?
    found = each_ems_metadata(nil, EmsCluster) { |ci| break(ci) }
    return found.evm_object_class == :EmsCluster if found.kind_of?(OpenStruct)

    false
  end

  def get_ems_folders(folder, dh = {}, full_path = "")
    path = full_path
    if folder.evm_object_class == :EmsFolder
      if folder.hidden
        return dh if folder.name != 'vm'
      else
        path = full_path.dup
        if path.blank?
          path << folder.name.to_s
        else
          path << " / ".freeze << folder.name
        end
        dh[folder.id] = path unless folder.datacenter?
      end
    end

    # Process child folders
    @_get_ems_folders_prefix ||= _log.prefix
    node = load_ems_node(folder, @_get_ems_folders_prefix)
    node.children.each { |child| get_ems_folders(child.attributes[:object], dh, path) } unless node.nil?

    dh
  end

  def get_ems_respool(node, dh = {}, full_path = "")
    return if node.nil?

    if node.kind_of?(XmlHash::Element)
      folder = node.attributes[:object]
      if node.name == :ResourcePool
        full_path += full_path.blank? ? folder.name.to_s : " / #{folder.name}"
        dh[folder.id] = full_path
      end
    end

    # Process child folders
    node.children.each { |child| get_ems_respool(child, dh, full_path) }

    dh
  end

  def find_datacenter_for_ci(item, ems_src = nil)
    find_class_above_ci(item, EmsFolder, ems_src, true)
  end

  def find_hosts_for_respool(item, ems_src = nil)
    hosts = find_class_above_ci(item, Host, ems_src)
    return [hosts] if hosts.present?

    cluster = find_cluster_above_ci(item)
    find_hosts_under_ci(cluster)
  end

  def find_cluster_above_ci(item, ems_src = nil)
    find_class_above_ci(item, EmsCluster, ems_src)
  end

  def find_class_above_ci(item, klass, _ems_src = nil, datacenter = false)
    result = nil
    @_find_class_above_ci_prefix ||= _log.prefix
    node = load_ems_node(item, @_find_class_above_ci_prefix)
    klass_name = klass.name.to_sym
    # Walk the xml document parents to find the requested class
    while node.kind_of?(XmlHash::Element)
      ci = node.attributes[:object]
      if node.name == klass_name && (datacenter == false || datacenter == true && ci.datacenter?)
        result = ci
        break
      end
      node = node.parent
    end

    result
  end

  def each_ems_metadata(ems_ci = nil, klass = nil, &_blk)
    if ems_ci.nil?
      src = get_source_and_targets
      ems_xml = get_ems_metadata_tree(src)
      ems_node = ems_xml.try(:root)
    else
      @_each_ems_metadata_prefix ||= _log.prefix
      ems_node = load_ems_node(ems_ci, @_each_ems_metadata_prefix)
    end
    klass_name = klass.name.to_sym unless klass.nil?
    unless ems_node.nil?
      ems_node.each_recursive { |node| yield(node.attributes[:object]) if klass.nil? || klass_name == node.name }
    end
  end

  def get_ems_metadata_tree(src)
    @ems_metadata_tree ||= begin
      return if src[:ems].nil?

      st = Time.zone.now
      result = load_ar_obj(src[:ems]).fulltree_arranged(:except_type => "VmOrTemplate")
      ems_metadata_tree_add_hosts_under_clusters!(result)
      @ems_xml_nodes = {}
      xml = MiqXml.newDoc(:xmlhash)
      convert_to_xml(xml, result)
      _log.info("EMS metadata collection completed in [#{Time.zone.now - st}] seconds")
      xml
    end
  end

  def ems_metadata_tree_add_hosts_under_clusters!(result)
    result.each do |obj, children|
      ems_metadata_tree_add_hosts_under_clusters!(children)
      obj.hosts.each { |h| children[h] = {} } if obj.kind_of?(EmsCluster)
    end
  end

  def convert_to_xml(xml, result)
    result.each do |obj, children|
      @ems_xml_nodes[obj.class.base_class] ||= {}
      @ems_xml_nodes[obj.class.base_class][obj.id] = node = xml.add_element(obj.class.base_class.name, :object => ci_to_hash_struct(obj))
      convert_to_xml(node, children)
    end
  end

  def add_target(dialog_key, key, klass, result)
    key_id = "#{key}_id".to_sym
    result[key_id] = get_value(@values[dialog_key])
    result[key_id] = nil if result[key_id] == 0
    result[key] = ci_to_hash_struct(klass.find_by(:id => result[key_id])) unless result[key_id].nil?
  end

  def ci_to_hash_struct(ci)
    return if ci.nil?
    return ci.collect { |c| ci_to_hash_struct(c) } if ci.respond_to?(:collect)

    method_name = "#{ci.class.base_class.name.underscore}_to_hash_struct".to_sym
    return send(method_name, ci) if respond_to?(method_name, true)

    default_ci_to_hash_struct(ci)
  end

  def host_to_hash_struct(ci)
    build_ci_hash_struct(ci, [:name, :vmm_product, :vmm_version, :state, :v_total_vms, :maintenance])
  end

  def vm_or_template_to_hash_struct(ci)
    v = build_ci_hash_struct(ci, [:name, :platform])
    v.snapshots = ci.snapshots.collect { |si| ci_to_hash_struct(si) }
    v
  end

  def ems_folder_to_hash_struct(ci)
    build_ci_hash_struct(ci, [:name, :type, :hidden]).tap do |nh|
      nh.send(:"datacenter?=", ci.kind_of?(Datacenter))
    end
  end

  def storage_to_hash_struct(ci)
    storage_clusters = ci.storage_clusters.blank? ? nil : ci.storage_clusters.collect(&:name).join(', ')
    build_ci_hash_struct(ci, [:name, :free_space, :total_space, :storage_domain_type]).tap do |hs|
      hs.storage_clusters = storage_clusters
    end
  end

  def snapshot_to_hash_struct(ci)
    build_ci_hash_struct(ci, [:name, :current?])
  end

  def customization_spec_to_hash_struct(ci)
    build_ci_hash_struct(ci, [:name, :typ, :description, :last_update_time, :is_sysprep_spec?])
  end

  def load_ar_obj(ci)
    return load_ar_objs(ci) if ci.kind_of?(Array)
    return ci unless ci.kind_of?(OpenStruct)

    ci.evm_object_class.to_s.camelize.constantize.find_by(:id => ci.id)
  end

  def load_ar_objs(ci)
    ci.collect { |i| load_ar_obj(i) }
  end

  # Return empty hash if we are selecting placement automatically so we do not
  # spend time determining all the available resources
  def resources_for_ui
    get_source_and_targets
  end

  def allowed_hosts_obj(options = {})
    return [] if (src = resources_for_ui).blank? || src[:ems].nil?

    datacenter = src[:datacenter] || options[:datacenter]
    rails_logger('allowed_hosts_obj', 0)
    st = Time.now
    hosts_ids = find_all_ems_of_type(Host).collect(&:id)
    hosts_ids &= load_ar_obj(src[:storage]).hosts.collect(&:id) unless src[:storage].nil?
    if datacenter
      @_allowed_hosts_obj_prefix ||= _log.prefix
      dc_node = load_ems_node(datacenter, @_allowed_hosts_obj_prefix)
      hosts_ids &= find_hosts_under_ci(dc_node.attributes[:object]).collect(&:id)
    end
    return [] if hosts_ids.blank?

    # Remove any hosts that are no longer in the list
    all_hosts = load_ar_obj(src[:ems]).hosts.where(:id => hosts_ids)
    allowed_hosts_obj_cache = process_filter(:host_filter, Host, all_hosts)
    _log.info("allowed_hosts_obj returned [#{allowed_hosts_obj_cache.length}] objects in [#{Time.now - st}] seconds")
    rails_logger('allowed_hosts_obj', 1)
    allowed_hosts_obj_cache
  end

  def allowed_storages(_options = {})
    return [] if (src = resources_for_ui).blank? || src[:ems].nil?

    hosts = src[:host].nil? ? allowed_hosts_obj({}) : [load_ar_obj(src[:host])]
    return [] if hosts.blank?

    rails_logger('allowed_storages', 0)
    st = Time.now

    selected_storage_profile_id = get_value(@values[:placement_storage_profile])
    storages = selected_storage_profile_id ? StorageProfile.find(selected_storage_profile_id).storages : Storage.all
    storages = storages.where(:id => HostStorage.writable_accessible.where(:host => hosts).select(:storage_id))
    allowed_storages_cache = process_filter(:ds_filter, Storage, storages).collect do |s|
      ci_to_hash_struct(s)
    end

    _log.info("allowed_storages returned [#{allowed_storages_cache.length}] objects in [#{Time.now - st}] seconds")
    rails_logger('allowed_storages', 1)
    allowed_storages_cache
  end

  def allowed_hosts(_options = {})
    hosts = allowed_hosts_obj
    hosts_ids = hosts.collect(&:id)
    result_hosts_hash = allowed_ci(:host, [:cluster, :respool, :folder], hosts_ids)

    host_ids = result_hosts_hash.to_a.transpose.first
    return [] if host_ids.nil?

    find_all_ems_of_type(Host).collect { |h| h if host_ids.include?(h.id) }.compact
  end

  def allowed_datacenters(_options = {})
    allowed_ci(:datacenter, [:cluster, :respool, :host, :folder])
  end

  def allowed_clusters(_options = {})
    all_clusters     = EmsCluster.where(:ems_id => get_source_and_targets[:ems].try(:id))
    filtered_targets = process_filter(:cluster_filter, EmsCluster, all_clusters)
    allowed_ci(:cluster, [:respool, :host, :folder], filtered_targets.collect(&:id))
  end

  def allowed_respools(_options = {})
    all_resource_pools = ResourcePool.where(:ems_id => get_source_and_targets[:ems].try(:id))
    filtered_targets   = process_filter(:rp_filter, ResourcePool, all_resource_pools)
    allowed_ci(:respool, [:cluster, :host, :folder], filtered_targets.collect(&:id))
  end
  alias_method :allowed_resource_pools, :allowed_respools

  def allowed_folders(_options = {})
    allowed_ci(:folder, [:cluster, :host, :respool])
  end

  def cluster_to_datacenter(src)
    return nil unless ems_has_clusters?

    ci_to_datacenter(src, :cluster, EmsCluster)
  end

  def respool_to_datacenter(src)
    ci_to_datacenter(src, :respool, ResourcePool)
  end

  def host_to_datacenter(src)
    ci_to_datacenter(src, :host, Host)
  end

  def folder_to_datacenter(src)
    return nil if src[:folder].nil?

    ci_to_datacenter(src, :folder, EmsFolder)
  end

  def ci_to_datacenter(src, ci, ci_type)
    sources = src[ci].nil? ? find_all_ems_of_type(ci_type) : [src[ci]]
    sources.collect { |c| find_datacenter_for_ci(c) }.compact.uniq.each_with_object({}) { |c, r| r[c.id] = c.name }
  end

  def respool_to_cluster(src)
    return nil unless ems_has_clusters?

    sources = src[:respool].nil? ? find_all_ems_of_type(ResourcePool) : [src[:respool]]
    build_id_to_name_hash(sources.collect { |rp| find_cluster_above_ci(rp) }.compact)
  end

  def host_to_cluster(src)
    return nil unless ems_has_clusters?

    sources = src[:host].nil? ? allowed_hosts_obj : [src[:host]]
    build_id_to_name_hash(sources.collect { |h| find_cluster_above_ci(h) }.compact)
  end

  def folder_to_cluster(src)
    return nil unless ems_has_clusters?

    source = find_all_ems_of_type(EmsCluster)
    build_id_to_name_hash(filter_to_objects_in_same_datacenter(source, src))
  end

  def cluster_to_respool(src)
    return nil unless ems_has_clusters?

    targets = src[:cluster].nil? ? find_all_ems_of_type(ResourcePool) : find_respools_under_ci(src[:cluster])
    res_pool_with_path = get_ems_respool(get_ems_metadata_tree(src))
    targets.each_with_object({}) { |rp, r| r[rp.id] = res_pool_with_path[rp.id] }
  end

  def folder_to_respool(src)
    return nil if src[:folder].nil?

    datacenter = find_datacenter_for_ci(src[:folder])
    targets = find_respools_under_ci(datacenter)
    res_pool_with_path = get_ems_respool(get_ems_metadata_tree(src))
    targets.each_with_object({}) { |rp, r| r[rp.id] = res_pool_with_path[rp.id] }
  end

  def host_to_respool(src)
    hosts = src[:host].nil? ? allowed_hosts_obj : [src[:host]]
    targets = hosts.collect do |h|
      cluster = find_cluster_above_ci(h)
      source = cluster.nil? ? h : cluster
      find_respools_under_ci(source)
    end.flatten
    res_pool_with_path = get_ems_respool(get_ems_metadata_tree(src))
    targets.each_with_object({}) { |rp, r| r[rp.id] = res_pool_with_path[rp.id] }
  end

  def cluster_to_host(src)
    return nil unless ems_has_clusters?

    hosts = src[:cluster].nil? ? find_all_ems_of_type(Host) : find_hosts_under_ci(src[:cluster])
    build_id_to_name_hash(hosts)
  end

  def respool_to_host(src)
    hosts = src[:respool].nil? ? find_all_ems_of_type(Host) : find_hosts_for_respool(src[:respool])
    build_id_to_name_hash(hosts)
  end

  def folder_to_host(src)
    source = find_all_ems_of_type(Host)
    build_id_to_name_hash(filter_to_objects_in_same_datacenter(source, src))
  end

  def host_to_folder(src)
    sources = src[:host].nil? ? allowed_hosts_obj : [src[:host]]
    datacenters = sources.collect do |h|
      rails_logger("host_to_folder for host #{h.name}", 0)
      result = find_datacenter_for_ci(h)
      rails_logger("host_to_folder for host #{h.name}", 1)
      result
    end.compact.uniq
    datacenters.each_with_object({}) do |dc, folders|
      rails_logger("host_to_folder for dc #{dc.name}", 0)
      folders.merge!(get_ems_folders(dc))
      rails_logger("host_to_folder for dc #{dc.name}", 1)
    end
  end

  def cluster_to_folder(src)
    return nil unless ems_has_clusters?
    return nil if src[:cluster].nil?

    sources = [src[:cluster]]
    datacenters = sources.collect { |h| find_datacenter_for_ci(h) }.compact
    datacenters.each_with_object({}) { |dc, folders| folders.merge!(get_ems_folders(dc)) }
  end

  def respool_to_folder(src)
    return nil if src[:respool].nil?

    sources = [src[:respool]]
    datacenters = sources.collect { |h| find_datacenter_for_ci(h) }.compact
    datacenters.each_with_object({}) { |dc, folders| folders.merge!(get_ems_folders(dc)) }
  end

  def set_ws_field_value(values, key, data, dialog_name, dlg_fields)
    value = data.delete(key)

    dlg_field = dlg_fields[key]
    data_type = dlg_field[:data_type]
    set_value = cast_value(value, data_type)

    result = nil
    if dlg_field.key?(:values)
      get_source_and_targets(true)
      get_field(key, dialog_name)
      field_values = dlg_field[:values]
      _log.info("processing key <#{dialog_name}:#{key}(#{data_type})> with values <#{field_values.inspect}>")
      if field_values.present?
        result = if field_values.first.kind_of?(OpenStruct)
                   found = field_values.detect { |v| v.id == set_value }
                   [found.id, found.name] if found
                 elsif data_type == :array_integer
                   field_values.keys & set_value
                 else
                   [set_value, field_values[set_value]] if field_values.key?(set_value)
                 end

        set_value = apply_result(result, data_type)
      end
    end

    _log.warn("Unable to find value for key <#{dialog_name}:#{key}(#{data_type})> with input value <#{set_value.inspect}>.  No matching item found.") if result.nil?
    _log.info("setting key <#{dialog_name}:#{key}(#{data_type})> to value <#{set_value.inspect}>")
    values[key] = set_value
  end

  def cast_value(value, data_type)
    case data_type
    when :integer         then value.to_i_with_method
    when :float           then value.to_f
    when :boolean         then value.to_s.downcase.in?(%w[true t])
    when :time            then Time.zone.parse(value)
    when :button          then value # Ignore
    when :array_integer   then Array.wrap(value).map!(&:to_i)
    else value # Ignore
    end
  end

  def set_ws_field_value_by_display_name(values, key, data, dialog_name, dlg_fields, obj_key = :name)
    value = data.delete(key)

    dlg_field = dlg_fields[key]
    data_type = dlg_field[:data_type]
    find_value = value.to_s.downcase

    if dlg_field.key?(:values)
      field_values = dlg_field[:values]
      _log.info("processing key <#{dialog_name}:#{key}(#{data_type})> with values <#{field_values.inspect}>")
      if field_values.present?
        result = if field_values.first.kind_of?(OpenStruct)
                   found = field_values.detect { |v| v.send(obj_key).to_s.downcase == find_value }
                   [found.id, found.send(obj_key)] if found
                 else
                   field_values.detect { |_k, v| v.to_s.downcase == find_value }
                 end

        if result.nil?
          _log.warn("Unable to set key <#{dialog_name}:#{key}(#{data_type})> to value <#{find_value.inspect}>.  No matching item found.")
        else
          set_value = [result.first, result.last]
          _log.info("setting key <#{dialog_name}:#{key}(#{data_type})> to value <#{set_value.inspect}>")
          values[key] = set_value
        end
      end
    end
  end

  def set_ws_field_value_by_id_or_name(values, dlg_field, data, dialog_name, dlg_fields, data_key = nil, id_klass = nil)
    data_key = dlg_field if data_key.blank?
    if data.key?(data_key)
      data[data_key] = "#{id_klass}::#{data[data_key]}" if id_klass.present?
      data[dlg_field] = data.delete(data_key)
      set_ws_field_value(values, dlg_field, data, dialog_name, dlg_fields)
    else
      data_key_without_id = data_key.to_s.chomp('_id').to_sym
      if data.key?(data_key_without_id)
        data[data_key] = data.delete(data_key_without_id)
        data[dlg_field] = data.delete(data_key)
        set_ws_field_value_by_display_name(values, dlg_field, data, dialog_name, dlg_fields, :name)
      end
    end
  end

  def get_ws_dialog_fields(dialog_name)
    dlg_fields = @dialogs.fetch_path(:dialogs, dialog_name, :fields)
    _log.info("<#{dialog_name}> dialog not found in dialogs.  Field updates will be skipped.") if dlg_fields.nil?
    dlg_fields
  end

  def allowed_customization_templates(_options = {})
    result = []
    customization_template_id = get_value(@values[:customization_template_id])
    @values[:customization_template_script] = nil if customization_template_id.nil?
    prov_typ = "vm"
    image = supports_iso? ? get_iso_image : get_pxe_image
    unless image.nil?
      result = image.customization_templates.collect do |c|
        # filter customizationtemplates
        if c.pxe_image_type.provision_type.blank? || c.pxe_image_type.provision_type == prov_typ
          @values[:customization_template_script] = c.script if c.id == customization_template_id
          build_ci_hash_struct(c, [:name, :description, :updated_at])
        end
      end.compact
    end

    @values[:customization_template_script] = nil if result.blank?
    result
  end

  def get_iso_image
    get_image_by_type(:iso_image_id)
  end

  def get_pxe_image
    get_image_by_type(:pxe_image_id)
  end

  def get_image_by_type(image_type)
    klass, id = get_value(@values[image_type]).to_s.split('::')
    return nil if id.blank?

    klass.constantize.find_by(:id => id)
  end

  def get_pxe_server
    PxeServer.find_by(:id => get_value(@values[:pxe_server_id]))
  end

  def allowed_pxe_servers(_options = {})
    PxeServer.all.each_with_object({}) { |p, h| h[p.id] = p.name }
  end

  def allowed_pxe_images(_options = {})
    pxe_server = get_pxe_server
    return [] if pxe_server.nil?

    prov_typ = "vm"

    pxe_server.pxe_images.collect do |p|
      next if p.pxe_image_type.nil? || p.default_for_windows

      # filter pxe images by provision_type to show vm/any or host/any
      build_ci_hash_struct(p, [:name, :description]) if p.pxe_image_type.provision_type.blank? || p.pxe_image_type.provision_type == prov_typ
    end.compact
  end

  def allowed_windows_images(_options = {})
    pxe_server = get_pxe_server
    return [] if pxe_server.nil?

    pxe_server.windows_images.collect do |p|
      build_ci_hash_struct(p, [:name, :description])
    end.compact
  end

  def allowed_images(options = {})
    result = allowed_pxe_images(options) + allowed_windows_images(options)
    # Change the ID to contain the class name since this is a mix class type
    result.each { |ci| ci.id = "#{ci.evm_object_class}::#{ci.id}" }
    result
  end

  def get_iso_images
    template = VmOrTemplate.find_by(:id => get_value(@values[:src_vm_id]))
    template.try(:ext_management_system).try(:iso_images) || []
  end

  def allowed_iso_images(_options = {})
    result = get_iso_images.collect do |p|
      build_ci_hash_struct(p, [:name])
    end.compact
    # Change the ID to contain the class name since this is a mix class type
    result.each { |ci| ci.id = "#{ci.evm_object_class}::#{ci.id}" }
    result
  end

  def ws_requester_fields(values, fields)
    dialog_name = :requester
    dlg_fields = @dialogs.fetch_path(:dialogs, :requester, :fields)
    if dlg_fields.nil?
      _log.info("<#{dialog_name}> dialog not found in dialogs.  Field updates be skipped.")
      return
    end

    data = parse_ws_string(fields)
    _log.info("data:<#{data.inspect}>")
    values[:auto_approve] = data.delete(:auto_approve) == 'true'
    data.delete(:user_name)

    # get owner values from LDAP if configured
    if data[:owner_email].present? && MiqLdap.using_ldap?
      email = data[:owner_email]
      unless email.include?('@')
        email = "#{email}@#{::Settings.authentication.user_suffix}"
      end
      values[:owner_email] = email
      retrieve_ldap rescue nil
    end

    dlg_keys = dlg_fields.keys
    data.keys.each do |key|
      if dlg_keys.include?(key)
        _log.info("processing key <#{dialog_name}:#{key}> with value <#{data[key].inspect}>")
        values[key] = data[key]
      else
        _log.warn("Skipping key <#{dialog_name}:#{key}>.  Key name not found in dialog")
      end
    end
  end

  def ws_schedule_fields(values, _fields, data)
    return if (dlg_fields = get_ws_dialog_fields(dialog_name = :schedule)).nil?

    if data[:schedule_time].present?
      values[:schedule_type] = 'schedule'
      [:schedule_time, :retirement_time].each do |key|
        data_type = :time
        time_value = data.delete(key)
        set_value = time_value.blank? ? nil : Time.parse(time_value)
        _log.info("setting key <#{dialog_name}:#{key}(#{data_type})> to value <#{set_value.inspect}>")
        values[key] = set_value
      end
    end

    dlg_keys = dlg_fields.keys
    data.keys.each { |key| set_ws_field_value(values, key, data, dialog_name, dlg_fields) if dlg_keys.include?(key) }
  end

  def raise_validate_errors
    errors = []
    fields { |_fn, f, _dn, _d| errors << f[:error] unless f[:error].nil? }
    err_text = "Provision failed for the following reasons:\n#{errors.join("\n")}"
    _log.error("<#{err_text}>")
    raise _("Provision failed for the following reasons:\n%{errors}") % {:errors => errors.join("\n")}
  end

  private

  def apply_result(result, data_type)
    return result if data_type == :array_integer

    [result.first, result.last] unless result.nil?
  end

  def build_id_to_name_hash(array)
    array.each_with_object({}) { |i, h| h[i.id] = i.name }
  end

  def default_ci_to_hash_struct(ci)
    attributes = []
    attributes << :name if ci.respond_to?(:name)
    build_ci_hash_struct(ci, attributes)
  end

  def filter_to_objects_in_same_datacenter(array, source)
    # If a folder is selected, reduce the host/cluster list to only hosts/clusters in the same datacenter as the folder
    source[:datacenter] ? array.reject { |i| find_datacenter_for_ci(i).id != source[:datacenter].id } : array
  end
end