diff --git a/app/models/manageiq/providers/vmware/infra_manager.rb b/app/models/manageiq/providers/vmware/infra_manager.rb index dff40b9ee..a2c8178fa 100644 --- a/app/models/manageiq/providers/vmware/infra_manager.rb +++ b/app/models/manageiq/providers/vmware/infra_manager.rb @@ -214,7 +214,7 @@ def supports_authentication?(authtype) end def self.catalog_types - {"vmware" => N_("VMware")} + {"vmware" => N_("VMware"), "generic_ovf_template" => N_("VMware Content Libary OVF Template")} end def streaming_refresh_enabled? diff --git a/app/models/manageiq/providers/vmware/infra_manager/ovf_service.rb b/app/models/manageiq/providers/vmware/infra_manager/ovf_service.rb new file mode 100644 index 000000000..a4efef6f2 --- /dev/null +++ b/app/models/manageiq/providers/vmware/infra_manager/ovf_service.rb @@ -0,0 +1,140 @@ +class ManageIQ::Providers::Vmware::InfraManager::OvfService < ServiceGeneric + delegate :ovf_template, :manager, :to => :service_template, :allow_nil => true + + # A chance for taking options from automate script to override options from a service dialog + def preprocess(action, new_options = {}) + return unless action == ResourceAction::PROVISION + + if new_options.present? + _log.info("Override with new options:\n#{new_options}") + end + + save_action_options(action, new_options) + end + + def execute(action) + return unless action == ResourceAction::PROVISION + + deploy_library_item_queue + end + + def deploy_library_item_queue + task_options = {:action => "Deploying VMware Content Library Item", :userid => "system"} + queue_options = { + :class_name => self.class.name, + :instance_id => id, + :method_name => "deploy_library_item", + :args => {}, + :role => "ems_operations", + :queue_name => manager.queue_name_for_ems_operations, + :zone => manager.my_zone + } + + task_id = MiqTask.generic_action_with_callback(task_options, queue_options) + update(:options => options.merge(:deploy_task_id => task_id)) + end + + def deploy_library_item(_options) + _log.info("OVF template provisioning with template ID: [#{ovf_template.id}] name:[#{ovf_template.name}] was initiated.") + opts = provision_options + _log.info("VMware Content Library OVF Tempalte provisioning with options:\n#{opts}") + + @deploy_response = ovf_template.deploy(opts).to_hash + _log.info("Content Library request response: #{@deploy_response}") + update(:options => options.merge(:deploy_response => @deploy_response)) + rescue VSphereAutomation::ApiError => e + _log.error("Failed to deploy content library template(#{ovf_template.name}), error: #{e}") + raise MiqException::MiqOrchestrationProvisionError, "Content library OVF template deployment failed: #{e}" + end + + def check_completed(action) + return [true, 'not supported'] unless action == ResourceAction::PROVISION + return [false, nil] if deploy_task.state != "Finished" + + message = deploy_response&.dig(:value, :succeeded) ? nil : deploy_response&.dig(:value, :error).to_json || deploy_response&.to_json || deploy_task.message + [true, message] + end + + def refresh(_action) + end + + def check_refreshed(action) + return [true, nil] unless deploy_response&.dig(:value, :succeeded) + + dest = find_destination_in_vmdb + if dest + add_resource!(dest, :name => action) + + if dest.kind_of?(ResourcePool) + dest.vms.each { |vm| add_resource!(vm, :name => action) } + end + + [true, nil] + else + [false, nil] + end + end + + private + + def deploy_response + @deploy_response ||= options[:deploy_response] + end + + def deploy_task + @deploy_task ||= MiqTask.find_by(:id => options[:deploy_task_id]) + end + + def find_destination_in_vmdb + target_model_class.find_by(:ems_id => manager.id, :ems_ref => deploy_response.dig(:value, :resource_id, :id)) + end + + def target_model_class + case deploy_response.dig(:value, :resource_id, :type) + when "VirtualMachine" + manager.class::Vm + when "VirtualApp" + manager.class::ResourcePool + end + end + + def get_action_options(action) + options[action_option_key(action)] + end + + def provision_options + @provision_options ||= get_action_options(ResourceAction::PROVISION) + end + + def save_action_options(action, overrides) + return unless action == ResourceAction::PROVISION + + action_options = options.fetch_path(:config_info, action.downcase.to_sym).with_indifferent_access + %w[resource_pool ems_folder host].each do |r| + next if action_options[r].blank? + + action_options["#{r}_id"] = action_options.delete(r).split.first.to_i + end + + action_options.deep_merge!(parse_dialog_options) + action_options.deep_merge!(overrides) + + options[action_option_key(action)] = action_options + save! + end + + def parse_dialog_options + dialog_options = options[:dialog] || {} + options = {:vm_name => dialog_options['dialog_vm_name']} + options[:accept_all_EULA] = dialog_options['dialog_accept_all_EULA'] == 't' + + %w[resource_pool ems_folder host storage].each do |r| + options["#{r}_id"] = dialog_options["dialog_#{r}"].split.first.to_i if dialog_options["dialog_#{r}"].present? + end + options + end + + def action_option_key(action) + "#{action.downcase}_options".to_sym + end +end diff --git a/app/models/manageiq/providers/vmware/infra_manager/ovf_service_template.rb b/app/models/manageiq/providers/vmware/infra_manager/ovf_service_template.rb new file mode 100644 index 000000000..5dda2d58e --- /dev/null +++ b/app/models/manageiq/providers/vmware/infra_manager/ovf_service_template.rb @@ -0,0 +1,81 @@ +class ManageIQ::Providers::Vmware::InfraManager::OvfServiceTemplate < ServiceTemplateGeneric + def self.default_provisioning_entry_point(_service_type) + '/Service/Generic/StateMachines/GenericLifecycle/provision' + end + + # create ServiceTemplate and supporting ServiceResources and ResourceActions + # options + # :name + # :description + # :service_template_catalog_id + # :config_info + # :provision + # :dialog_id or :dialog + # :ovf_template_id or :ovf_template + # + def self.create_catalog_item(options, _auth_user) + options = options.merge(:service_type => 'atomic', :prov_type => 'generic_ovf_template') + config_info = validate_config_info(options[:config_info]) + enhanced_config = config_info.deep_merge( + :provision => { + :configuration_template => ovf_template_from_config_info(config_info) + } + ) + + transaction do + create_from_options(options).tap do |service_template| + service_template.create_resource_actions(enhanced_config) + end + end + end + + def self.validate_config_info(info) + info[:provision][:fqname] ||= default_provisioning_entry_point(SERVICE_TYPE_ATOMIC) if info.key?(:provision) + + # TODO: Add more validation for required fields + info + end + private_class_method :validate_config_info + + def self.ovf_template_from_config_info(info) + ovf_template_id = info[:provision][:ovf_template_id] + ovf_template_id ? OrchestrationTemplate.find(ovf_template_id) : info[:provision][:ovf_template] + end + private_class_method :ovf_template_from_config_info + + def ovf_template + @ovf_template ||= resource_actions.find_by(:action => "Provision").try(:configuration_template) + end + + def manager + @manager ||= ovf_template.try(:ext_management_system) + end + + def update_catalog_item(options) + config_info = validate_update_config_info(options) + config_info[:provision][:configuration_template] ||= ovf_template_from_config_info(config_info) if config_info.key?(:provision) + options[:config_info] = config_info + + super + end + + private + + def ovf_template_from_config_info(info) + self.class.send(:ovf_template_from_config_info, info) + end + + def validate_update_config_info(options) + opts = super + self.class.send(:validate_config_info, opts) + end + + def update_service_resources(_config_info, _auth_user = nil) + # do nothing since no service resources for this template + end + + def update_from_options(params) + options[:config_info] = Hash[params[:config_info].collect { |k, v| [k, v.except(:configuration_template)] }] + update!(params.except(:config_info)) + end +end diff --git a/spec/factories/service.rb b/spec/factories/service.rb new file mode 100644 index 000000000..0e8260188 --- /dev/null +++ b/spec/factories/service.rb @@ -0,0 +1,3 @@ +FactoryBot.define do + factory :service_ovf, :class => "ManageIQ::Providers::Vmware::InfraManager::OvfService", :parent => :service +end diff --git a/spec/factories/service_template.rb b/spec/factories/service_template.rb new file mode 100644 index 000000000..5a90857bd --- /dev/null +++ b/spec/factories/service_template.rb @@ -0,0 +1,3 @@ +FactoryBot.define do + factory :service_template_ovf, :class => "ManageIQ::Providers::Vmware::InfraManager::OvfServiceTemplate", :parent => :service_template +end diff --git a/spec/models/manageiq/providers/vmware/infra_manager/ovf_service_spec.rb b/spec/models/manageiq/providers/vmware/infra_manager/ovf_service_spec.rb new file mode 100644 index 000000000..93f5809a0 --- /dev/null +++ b/spec/models/manageiq/providers/vmware/infra_manager/ovf_service_spec.rb @@ -0,0 +1,128 @@ +require 'vsphere-automation-vcenter' + +describe(ManageIQ::Providers::Vmware::InfraManager::OvfService) do + let(:action) { ResourceAction::PROVISION } + let(:ovf_template) { FactoryBot.create(:orchestration_template_vmware_infra) } + + let(:ems) { FactoryBot.create(:ems_vmware) } + + let(:service) do + FactoryBot.create(:service_ovf, :options => config_info_options.merge(dialog_options)).tap do |svc| + allow(svc).to receive(:manager).and_return(ems) + end + end + + let(:loaded_service) do + service_template = FactoryBot.create(:service_template_ovf).tap do |st| + allow(st).to receive(:manager).and_return(ems) + end + + FactoryBot.create(:service_ovf, + :options => {:provision_options => provision_options}.merge(config_info_options), + :service_template => service_template).tap do |svc| + allow(svc).to receive(:ovf_template).and_return(ovf_template) + end + end + + let(:dialog_options) do + { + :dialog => { + "dialog_vm_name" => "dialog_vm_name", + "dialog_resource_pool" => "5 test resource pool", + "dialog_ems_folder" => "30 lucy", + } + } + end + + let(:config_info_options) do + { + :config_info => { + :provision => { + :dialog_id => "2", + :ovf_template_id => ovf_template.id, + :vm_name => "template_vm_name", + :accept_all_EULA => true, + :resource_pool => "2 Default for Cluster dev-vc67-cluster", + :ems_folder => "3 test_folder", + :host => "1 test_host" + } + } + } + end + + let(:override_options) { {:vm_name => 'override_vm_name'} } + + let(:provision_options) do + { + "ovf_template_id" => ovf_template.id, + "dialog_id" => "2", + "vm_name" => "dialog_vm_name", + "accept_all_EULA" => false, + "resource_pool_id" => 5, + "ems_folder_id" => 30 + } + end + + let(:failed_response ) { {:value => {:succeeded => false, :error=>{:errors=>[{:category=>"SERVER", :error=>{:@class=>"com.vmware.vapi.std.errors.already_exists", :messages=>[{:args=>["VirtualMachine", "lucy-api-vm-2"], :default_message=>"An object of type \"VirtualMachine\" named \"lucy-api-vm-2\" already exists.", :id=>"com.vmware.vdcs.util.duplicate_name"}]}}], :warnings=>[], :information=>[]}}} } + + let(:deploy_task) { FactoryBot.create(:miq_task, :state => "Active")} + + describe '#preprocess' do + it 'prepares job options from dialog' do + service.preprocess(action) + expect(service.options[:provision_options]).to match a_hash_including(provision_options) + end + + it 'prepares job options combined from dialog and overrides' do + service.preprocess(action, override_options) + expect(service.options[:provision_options]).to match a_hash_including( + "vm_name" => override_options[:vm_name] + ) + end + end + + describe '#deploy_library_item' do + it 'Provisions with an ovf template' do + expect(ovf_template).to receive(:deploy) do |options| + expect(options).to eq(provision_options) + failed_response + end + loaded_service.deploy_library_item(action) + end + end + + describe '#check_completed' do + it 'created VM ends in VMDB' do + deploy_task.update(:state => "Finished") + loaded_service.update(:options => loaded_service.options.merge(:deploy_task_id => deploy_task.id)) + loaded_service.update(:options => loaded_service.options.merge(:deploy_response => failed_response)) + expect(loaded_service.check_completed(action)).to eq([true, failed_response.dig(:value, :error).to_json]) + end + + it 'created VM not ends in VMDB yet' do + loaded_service.update(:options => loaded_service.options.merge(:deploy_task_id => deploy_task.id)) + expect(loaded_service.check_completed(action)).to eq([false, nil]) + end + end + + describe '#check_refreshed' do + it 'successful deployment response ' do + response = {:value => {:succeeded => true, :resource_id=>{:type=>"VirtualMachine", :id=>"vm-934"}}} + loaded_service.update(:options => loaded_service.options.merge(:deploy_response => response)) + expect(loaded_service.check_refreshed(action)).to eq([false, nil]) + + FactoryBot.create(:vm_vmware, :ems_ref_type => "VirtualMachine", :ems_ref => "vm-934", :ems_id => ems.id) + expect(loaded_service.check_refreshed(action)).to eq([true, nil]) + end + + it 'no successful deployment response ' do + response = {:value => {:succeeded => false}} + loaded_service.update(:options => loaded_service.options.merge(:deploy_response => response)) + expect(loaded_service.check_refreshed(action)).to eq([true, nil]) + end + + it 'no deployment response' do + expect(loaded_service.check_refreshed(action)).to eq([true, nil]) + end + end +end