diff --git a/app/controllers/api/v2/bulk_snapshots_controller.rb b/app/controllers/api/v2/bulk_snapshots_controller.rb index 8446f5a..6c8afed 100644 --- a/app/controllers/api/v2/bulk_snapshots_controller.rb +++ b/app/controllers/api/v2/bulk_snapshots_controller.rb @@ -3,12 +3,27 @@ module Api module V2 class BulkSnapshotsController < V2::BaseController - SNAPSHOT_MODES = %w[full include_ram quiesce].freeze + include ::Api::V2::BulkHostsExtension - before_action :validate_hosts, :validate_snapshot, :validate_mode, only: :create + SNAPSHOT_MODES = ForemanSnapshotManagement::Extensions::BulkHostsManager::SNAPSHOT_MODES + + before_action :validate_snapshot, :validate_mode, :find_snapshotable_hosts, only: :create api :POST, '/snapshots/bulk/create_snapshot', N_('Create snapshots for multiple hosts') - param :host_ids, Array, of: Integer, desc: N_('Array of host IDs to create snapshots for'), required: true + + param :organization_id, :number, required: true, desc: N_('ID of the organization') + + param :included, Hash, required: true, action_aware: true do + param :search, String, required: false, + desc: N_('Search string for hosts to perform an action on') + param :ids, Array, required: false, + desc: N_('List of host ids to perform an action on') + end + + param :excluded, Hash, required: true, action_aware: true do + param :ids, Array, required: false, + desc: N_('List of host ids to exclude and not run an action on') + end param :snapshot, Hash, desc: N_('Snapshot parameters'), required: true do param :name, String, desc: N_('Name of the snapshot'), required: true @@ -21,17 +36,21 @@ class BulkSnapshotsController < V2::BaseController required: false def create - include_ram, quiesce = resolve_mode(params[:mode]) - - results = @hosts.map do |host| - process_host(host, @snapshot_name, @snapshot_description, include_ram, quiesce) - end + results = ::BulkHostsManager.new(hosts: @hosts).create_snapshots( + name: @snapshot_name, + description: @snapshot_description, + mode: params[:mode] + ) render_results(results) end private + def find_snapshotable_hosts + find_bulk_hosts(:create_snapshots, params) + end + def validate_mode mode = params[:mode] return true if mode.blank? || SNAPSHOT_MODES.include?(mode) @@ -46,44 +65,6 @@ def validate_mode false end - def validate_hosts - host_ids = params[:host_ids] - - if host_ids.blank? - render( - json: { error: _('host_ids parameter is required and cannot be empty') }, - status: :unprocessable_entity - ) - return false - end - - unless host_ids.is_a?(Array) - render( - json: { error: _('host_ids must be an array') }, - status: :unprocessable_entity - ) - return false - end - - @requested_host_ids = host_ids.map(&:to_i) - resource_base = Host.authorized("#{action_permission}_snapshots".to_sym, Host).friendly - @hosts = resource_base.where(id: @requested_host_ids) - - found_ids = @hosts.pluck(:id) - missing_ids = @requested_host_ids - found_ids - - return true if missing_ids.empty? - - render( - json: { - error: _('Some host_ids are invalid'), - invalid_ids: missing_ids, - }, - status: :unprocessable_entity - ) - false - end - def validate_snapshot snap = params[:snapshot] || {} @snapshot_name = snap[:name].to_s.strip @@ -101,13 +82,7 @@ def validate_snapshot def render_results(results) failed_hosts = results.select { |r| r[:status] == 'failed' } failed_count = failed_hosts.length - - status = - if failed_count.zero? - :ok - else - :unprocessable_entity - end + status = failed_count.zero? ? :ok : :unprocessable_entity render( json: { @@ -121,66 +96,6 @@ def render_results(results) ) end - def resolve_mode(mode) - case mode - when 'include_ram' - [true, false] - when 'quiesce' - [false, true] - else - [false, false] - end - end - - def process_host(host, name, description, include_ram, quiesce) - snapshot = ForemanSnapshotManagement::Snapshot.new( - name: name, - description: description, - include_ram: include_ram, - quiesce: quiesce, - host: host - ) - - if snapshot.create - { - host_id: host.id, - host_name: host.name, - status: 'success', - } - else - errors = snapshot.errors.full_messages - errors << quiesce_error_message if quiesce - - { - host_id: host.id, - host_name: host.name, - status: 'failed', - errors: errors, - } - end - rescue StandardError => e - Foreman::Logging.exception( - "Failed to create snapshot for host #{host.name} (ID: #{host.id})", - e - ) - { - host_id: host.id, - host_name: host.name, - status: 'failed', - errors: [_('Snapshot creation failed: %s') % e.message], - } - end - - def quiesce_error_message - _( - 'Unable to create VMWare Snapshot with Quiesce. Check Power and VMWare Tools status.' - ) - end - - def action_permission - :create - end - def resource_class ::ForemanSnapshotManagement::Snapshot end diff --git a/app/models/foreman_snapshot_management/extensions/bulk_hosts_manager.rb b/app/models/foreman_snapshot_management/extensions/bulk_hosts_manager.rb new file mode 100644 index 0000000..05cc182 --- /dev/null +++ b/app/models/foreman_snapshot_management/extensions/bulk_hosts_manager.rb @@ -0,0 +1,123 @@ +# frozen_string_literal: true + +module ForemanSnapshotManagement + module Extensions + module BulkHostsManager + extend ActiveSupport::Concern + + SNAPSHOT_MODES = %w[full include_ram quiesce].freeze + QUIESCE_ERROR_MESSAGE = + _('Unable to create VMWare Snapshot with Quiesce. Check Power and VMWare Tools status.').freeze + + def create_snapshots(name:, description:, mode:) + include_ram, quiesce = resolve_mode(mode) + + @hosts.map do |host| + process_snapshot_for_host( + host: host, + name: name, + description: description, + include_ram: include_ram, + quiesce: quiesce + ) + end + end + + private + + def resolve_mode(mode) + case mode + when 'include_ram' + [true, false] + when 'quiesce' + [false, true] + else + [false, false] + end + end + + def process_snapshot_for_host(host:, name:, description:, include_ram:, quiesce:) + validation_error = validate_host(host) + return validation_error if validation_error + + create_snapshot_for_host(host, name, description, include_ram, quiesce) + rescue ActiveRecord::RecordNotFound => e + handle_record_not_found(host, e) + rescue StandardError => e + handle_snapshot_error(host, e) + end + + def validate_host(host) + if host.compute_resource.nil? + return { + host_id: host.id, + host_name: host.name, + status: 'failed', + errors: [_('Host has no compute resource, cannot create snapshot.')], + } + end + + return if host.uuid.present? + + { + host_id: host.id, + host_name: host.name, + status: 'failed', + errors: [_('Host has no UUID, cannot create snapshot.')], + } + end + + def create_snapshot_for_host(host, name, description, include_ram, quiesce) + snapshot = ::ForemanSnapshotManagement::Snapshot.new( + name: name, + description: description, + include_ram: include_ram, + quiesce: quiesce, + host: host + ) + + if snapshot.create + { host_id: host.id, host_name: host.name, status: 'success' } + else + errors = snapshot.errors.full_messages + errors << QUIESCE_ERROR_MESSAGE if quiesce + { host_id: host.id, host_name: host.name, status: 'failed', errors: errors } + end + end + + def handle_record_not_found(host, exception) + ::Foreman::Logging.exception( + "Failed to create snapshot for host #{host.name} (ID: #{host.id})", + exception + ) + + cr_name = host.compute_resource&.name + cr_name = "#{cr_name} " if cr_name.present? + + msg = + _('VM details could not be retrieved from compute resource %sThe VM may be missing/deleted or inaccessible.') % cr_name + + { + host_id: host.id, + host_name: host.name, + status: 'failed', + errors: [msg], + } + end + + def handle_snapshot_error(host, exception) + ::Foreman::Logging.exception( + "Failed to create snapshot for host #{host.name} (ID: #{host.id})", + exception + ) + + { + host_id: host.id, + host_name: host.name, + status: 'failed', + errors: [_('Snapshot creation failed: %s') % exception.message], + } + end + end + end +end diff --git a/lib/foreman_snapshot_management/engine.rb b/lib/foreman_snapshot_management/engine.rb index 3d02de0..a177794 100644 --- a/lib/foreman_snapshot_management/engine.rb +++ b/lib/foreman_snapshot_management/engine.rb @@ -91,6 +91,8 @@ class Engine < ::Rails::Engine # Include concerns in this config.to_prepare block config.to_prepare do begin + ::BulkHostsManager.include ForemanSnapshotManagement::Extensions::BulkHostsManager + ::ForemanFogProxmox::Proxmox.prepend ForemanSnapshotManagement::ProxmoxExtensions # Load Fog extensions diff --git a/test/controllers/api/v2/bulk_snapshots_controller_test.rb b/test/controllers/api/v2/bulk_snapshots_controller_test.rb index 34a1429..0a1a8a0 100644 --- a/test/controllers/api/v2/bulk_snapshots_controller_test.rb +++ b/test/controllers/api/v2/bulk_snapshots_controller_test.rb @@ -55,7 +55,15 @@ class Api::V2::BulkSnapshotsControllerTest < ActionController::TestCase setup { ::Fog.mock! } teardown { ::Fog.unmock! } - context 'view user without create permissions' do + def bulk_params(ids:) + { + :organization_id => tax_organization.id, + :included => { :ids => ids }, + :excluded => { :ids => [] }, + } + end + + context 'user without edit_hosts permissions (bulk selection gate)' do setup do host1 host2 @@ -67,11 +75,10 @@ class Api::V2::BulkSnapshotsControllerTest < ActionController::TestCase User.current = @orig_user end - test 'should refute create bulk snapshots without permission' do - post :create, params: { - :host_ids => [host1.id, host2.id], - :snapshot => { :name => 'test' }, - } + test 'should forbid bulk snapshot create when user cannot bulk-select hosts' do + post :create, params: bulk_params(ids: [host1.id, host2.id]).merge( + :snapshot => { :name => 'test' } + ) assert_response :forbidden end end @@ -83,6 +90,8 @@ class Api::V2::BulkSnapshotsControllerTest < ActionController::TestCase host_without_compute @orig_user = User.current User.current = manager_user + + ForemanSnapshotManagement::Snapshot.any_instance.stubs(:create).returns(true) end teardown do @@ -90,97 +99,44 @@ class Api::V2::BulkSnapshotsControllerTest < ActionController::TestCase end test 'should return 422 when snapshot parameter is missing' do - post :create, params: { :host_ids => [host1.id, host2.id] } + post :create, params: bulk_params(ids: [host1.id, host2.id]) assert_response :unprocessable_entity body = ActiveSupport::JSON.decode(@response.body) assert_includes body['error'], 'snapshot.name is required' end test 'should return 422 when snapshot name is missing' do - post :create, params: { - :host_ids => [host1.id, host2.id], - :snapshot => { :description => 'test description' }, - } + post :create, params: bulk_params(ids: [host1.id, host2.id]).merge( + :snapshot => { :description => 'test description' } + ) assert_response :unprocessable_entity body = ActiveSupport::JSON.decode(@response.body) assert_includes body['error'], 'snapshot.name is required' end test 'should return 422 when snapshot name is empty string' do - post :create, params: { - :host_ids => [host1.id, host2.id], - :snapshot => { :name => '' }, - } + post :create, params: bulk_params(ids: [host1.id, host2.id]).merge( + :snapshot => { :name => '' } + ) assert_response :unprocessable_entity body = ActiveSupport::JSON.decode(@response.body) assert_includes body['error'], 'snapshot.name is required' end test 'should return 422 when snapshot name is only whitespace' do - post :create, params: { - :host_ids => [host1.id, host2.id], - :snapshot => { :name => ' ' }, - } + post :create, params: bulk_params(ids: [host1.id, host2.id]).merge( + :snapshot => { :name => ' ' } + ) assert_response :unprocessable_entity body = ActiveSupport::JSON.decode(@response.body) assert_includes body['error'], 'snapshot.name is required' end - test 'should return error when host_ids parameter is missing' do - post :create, params: { :snapshot => { :name => 'test' } } - assert_response :unprocessable_entity - body = ActiveSupport::JSON.decode(@response.body) - assert_includes body['error'], 'host_ids' - end - - test 'should return error when host_ids is empty array' do - post :create, params: { - :host_ids => [], - :snapshot => { :name => 'test' }, - } - assert_response :unprocessable_entity - body = ActiveSupport::JSON.decode(@response.body) - assert_includes body['error'], 'host_ids' - end - - test 'should return error when host_ids contains non-existent IDs' do - post :create, params: { - :host_ids => [99_999, 88_888], - :snapshot => { :name => 'test' }, - } - assert_response :unprocessable_entity - body = ActiveSupport::JSON.decode(@response.body) - assert_includes body['error'], 'host_ids' - end - - test 'should return error when some host_ids are invalid' do - post :create, params: { - :host_ids => [host1.id, 99_999], - :snapshot => { :name => 'test' }, - } - - assert_response :unprocessable_entity - body = ActiveSupport::JSON.decode(@response.body) - assert_includes body['error'], 'Some host_ids are invalid' - assert_equal [99_999], body['invalid_ids'] - end - - test 'should return error when host_ids is not an array' do - post :create, params: { - :host_ids => 'not-an-array', - :snapshot => { :name => 'test' }, - } - assert_response :unprocessable_entity - body = ActiveSupport::JSON.decode(@response.body) - assert_includes body['error'], 'host_ids' - end - test 'should create snapshots with include_ram mode' do - post :create, params: { - :host_ids => [host1.id], + post :create, params: bulk_params(ids: [host1.id]).merge( :snapshot => { :name => 'test_with_ram' }, - :mode => 'include_ram', - } + :mode => 'include_ram' + ) assert_response :ok body = ActiveSupport::JSON.decode(@response.body) assert_equal 1, body['total'] @@ -188,11 +144,10 @@ class Api::V2::BulkSnapshotsControllerTest < ActionController::TestCase end test 'should create snapshots with full mode' do - post :create, params: { - :host_ids => [host1.id], + post :create, params: bulk_params(ids: [host1.id]).merge( :snapshot => { :name => 'test_without_ram' }, - :mode => 'full', - } + :mode => 'full' + ) assert_response :ok body = ActiveSupport::JSON.decode(@response.body) assert_equal 1, body['total'] @@ -200,11 +155,10 @@ class Api::V2::BulkSnapshotsControllerTest < ActionController::TestCase end test 'should create snapshots with quiesce mode' do - post :create, params: { - :host_ids => [host1.id], + post :create, params: bulk_params(ids: [host1.id]).merge( :snapshot => { :name => 'test_quiesce' }, - :mode => 'quiesce', - } + :mode => 'quiesce' + ) assert_response :ok body = ActiveSupport::JSON.decode(@response.body) assert_equal 1, body['total'] @@ -212,23 +166,17 @@ class Api::V2::BulkSnapshotsControllerTest < ActionController::TestCase end test 'should return 422 when mode is invalid' do - post :create, params: { - :host_ids => [host1.id], + post :create, params: bulk_params(ids: [host1.id]).merge( :snapshot => { :name => 'invalid_mode_snapshot' }, - :mode => 'invalid_mode', - } - + :mode => 'invalid_mode' + ) assert_response :unprocessable_entity end test 'should successfully create snapshot for single host' do - post :create, params: { - :host_ids => [host1.id], - :snapshot => { - :name => 'successful_snapshot', - :description => 'Test description', - }, - } + post :create, params: bulk_params(ids: [host1.id]).merge( + :snapshot => { :name => 'successful_snapshot', :description => 'Test description' } + ) assert_response :ok body = ActiveSupport::JSON.decode(@response.body) assert_equal 1, body['total'] @@ -241,41 +189,31 @@ class Api::V2::BulkSnapshotsControllerTest < ActionController::TestCase end test 'should successfully create snapshots for multiple hosts' do - post :create, params: { - :host_ids => [host1.id, host2.id], - :snapshot => { - :name => 'bulk_snapshot', - :description => 'Bulk test', - }, - } + post :create, params: bulk_params(ids: [host1.id, host2.id]).merge( + :snapshot => { :name => 'bulk_snapshot', :description => 'Bulk test' } + ) assert_response :ok body = ActiveSupport::JSON.decode(@response.body) assert_equal 2, body['total'] assert_equal 2, body['success_count'] assert_equal 0, body['failed_count'] - statuses = body['results'].map { |r| r['status'] } assert_equal ['success', 'success'], statuses end test 'should include snapshot description in request' do - post :create, params: { - :host_ids => [host1.id], - :snapshot => { - :name => 'test', - :description => 'Detailed description here', - }, - } + post :create, params: bulk_params(ids: [host1.id]).merge( + :snapshot => { :name => 'test', :description => 'Detailed description here' } + ) assert_response :ok body = ActiveSupport::JSON.decode(@response.body) assert_equal 1, body['success_count'] end test 'should handle snapshot without description' do - post :create, params: { - :host_ids => [host1.id], - :snapshot => { :name => 'test' }, - } + post :create, params: bulk_params(ids: [host1.id]).merge( + :snapshot => { :name => 'test' } + ) assert_response :ok body = ActiveSupport::JSON.decode(@response.body) assert_equal 1, body['success_count'] @@ -287,10 +225,9 @@ class Api::V2::BulkSnapshotsControllerTest < ActionController::TestCase OpenStruct.new(full_messages: ['Snapshot creation failed']) ) - post :create, params: { - :host_ids => [host1.id], - :snapshot => { :name => 'failing_snapshot' }, - } + post :create, params: bulk_params(ids: [host1.id]).merge( + :snapshot => { :name => 'failing_snapshot' } + ) assert_response :unprocessable_entity body = ActiveSupport::JSON.decode(@response.body) assert_equal 1, body['total'] @@ -305,10 +242,9 @@ class Api::V2::BulkSnapshotsControllerTest < ActionController::TestCase StandardError.new('Unexpected error') ) - post :create, params: { - :host_ids => [host1.id], - :snapshot => { :name => 'exception_snapshot' }, - } + post :create, params: bulk_params(ids: [host1.id]).merge( + :snapshot => { :name => 'exception_snapshot' } + ) assert_response :unprocessable_entity body = ActiveSupport::JSON.decode(@response.body) assert_equal 1, body['total'] @@ -323,10 +259,9 @@ class Api::V2::BulkSnapshotsControllerTest < ActionController::TestCase OpenStruct.new(full_messages: ['Error message']) ) - post :create, params: { - :host_ids => [host1.id], - :snapshot => { :name => 'test' }, - } + post :create, params: bulk_params(ids: [host1.id]).merge( + :snapshot => { :name => 'test' } + ) body = ActiveSupport::JSON.decode(@response.body) assert_nil body['results'].first['errors'] @@ -335,15 +270,14 @@ class Api::V2::BulkSnapshotsControllerTest < ActionController::TestCase end test 'should handle mixed success and failure results' do - ForemanSnapshotManagement::Snapshot.any_instance.stubs(:create).returns(true).then.returns(false) + ForemanSnapshotManagement::Snapshot.any_instance.stubs(:create).returns(true, false) ForemanSnapshotManagement::Snapshot.any_instance.stubs(:errors).returns( OpenStruct.new(full_messages: ['Failed for this host']) ) - post :create, params: { - :host_ids => [host1.id, host2.id], - :snapshot => { :name => 'mixed_results' }, - } + post :create, params: bulk_params(ids: [host1.id, host2.id]).merge( + :snapshot => { :name => 'mixed_results' } + ) assert_response :unprocessable_entity body = ActiveSupport::JSON.decode(@response.body) assert_equal 2, body['total'] @@ -355,26 +289,23 @@ class Api::V2::BulkSnapshotsControllerTest < ActionController::TestCase test 'should return 422 when any host fails' do ForemanSnapshotManagement::Snapshot.any_instance.stubs(:create).returns(false) - post :create, params: { - :host_ids => [host1.id, host2.id], - :snapshot => { :name => 'test' }, - } + post :create, params: bulk_params(ids: [host1.id, host2.id]).merge( + :snapshot => { :name => 'test' } + ) assert_response :unprocessable_entity end test 'should return 200 when all hosts succeed' do - post :create, params: { - :host_ids => [host1.id, host2.id], - :snapshot => { :name => 'test' }, - } + post :create, params: bulk_params(ids: [host1.id, host2.id]).merge( + :snapshot => { :name => 'test' } + ) assert_response :ok end test 'should handle host without compute resource' do - post :create, params: { - :host_ids => [host_without_compute.id], - :snapshot => { :name => 'test' }, - } + post :create, params: bulk_params(ids: [host_without_compute.id]).merge( + :snapshot => { :name => 'test' } + ) assert_response :unprocessable_entity body = ActiveSupport::JSON.decode(@response.body) assert_equal 1, body['total'] @@ -383,10 +314,9 @@ class Api::V2::BulkSnapshotsControllerTest < ActionController::TestCase end test 'should handle mix of hosts with and without compute resources' do - post :create, params: { - :host_ids => [host1.id, host_without_compute.id], - :snapshot => { :name => 'test' }, - } + post :create, params: bulk_params(ids: [host1.id, host_without_compute.id]).merge( + :snapshot => { :name => 'test' } + ) assert_response :unprocessable_entity body = ActiveSupport::JSON.decode(@response.body) assert_equal 2, body['total'] @@ -399,11 +329,10 @@ class Api::V2::BulkSnapshotsControllerTest < ActionController::TestCase OpenStruct.new(full_messages: ['Underlying quiesce failure']) ) - post :create, params: { - :host_ids => [host1.id, host2.id], + post :create, params: bulk_params(ids: [host1.id, host2.id]).merge( :snapshot => { :name => 'test_quiesce_fail' }, - :mode => 'quiesce', - } + :mode => 'quiesce' + ) assert_response :unprocessable_entity body = ActiveSupport::JSON.decode(@response.body) diff --git a/webpack/components/SnapshotManagement/SnapshotManagementActions.js b/webpack/components/SnapshotManagement/SnapshotManagementActions.js index f4566e0..b0b51dd 100644 --- a/webpack/components/SnapshotManagement/SnapshotManagementActions.js +++ b/webpack/components/SnapshotManagement/SnapshotManagementActions.js @@ -213,6 +213,54 @@ export const snapshotRollbackAction = (host, rowData) => async dispatch => { } }; +const buildHostsSearchHref = query => + `/new/hosts?search=${encodeURIComponent(query)}`; + +const failedHostsToastParams = ({ message, failedHostIds, key }) => { + const { FAILURE } = actionTypeGenerator(key); + + const toastParams = { + type: 'danger', + message, + key: FAILURE, + }; + + if (failedHostIds?.length) { + const query = `id ^ (${failedHostIds.join(',')})`; + toastParams.link = { + children: __('Failed hosts'), + href: buildHostsSearchHref(query), + }; + } + + return toastParams; +}; + +const buildEnhancedErrorMessage = ({ + successCount, + failedCount, + failedHosts, +}) => { + const base = sprintf( + successCount === 1 + ? __('%d snapshot succeeded, %d failed.') + : __('%d snapshots succeeded, %d failed.'), + successCount, + failedCount + ); + + const uniqueErrors = [ + ...new Set( + (failedHosts || []).flatMap(h => h.errors || []).filter(Boolean) + ), + ]; + + const extra = uniqueErrors.length + ? ` ${uniqueErrors.slice(0, 2).join(' ')}` + : ''; + return `${base}${extra}`; +}; + export const bulkCreateSnapshotsAction = payload => async dispatch => { const { REQUEST, SUCCESS, FAILURE } = actionTypeGenerator( SNAPSHOT_BULK_CREATE @@ -231,28 +279,34 @@ export const bulkCreateSnapshotsAction = payload => async dispatch => { }) ); + if (payload.hostId) { + dispatch(loadSnapshotList(payload.hostId)); + } + return dispatch({ type: SUCCESS, payload, response: data }); } catch (error) { - const data = error?.response?.data; - const failedCount = data?.['failed_count'] || 0; - const successCount = data?.['success_count'] || 0; + const data = error?.response?.data || {}; + + const failedHosts = data.failed_hosts || []; + const failedHostIds = failedHosts.map(h => h.host_id).filter(Boolean); + + const failedCount = data.failed_count || failedHosts.length || 0; + const successCount = data.success_count || 0; + + const message = buildEnhancedErrorMessage({ + successCount, + failedCount, + failedHosts, + }); dispatch( - addToast({ - type: 'error', - message: sprintf( - successCount === 1 - ? __( - '%d snapshot succeeded, %d failed. Check production logs for details.' - ) - : __( - '%d snapshots succeeded, %d failed. Check production logs for details.' - ), - successCount, - failedCount - ), - key: FAILURE, - }) + addToast( + failedHostsToastParams({ + message, + failedHostIds, + key: SNAPSHOT_BULK_CREATE, + }) + ) ); return dispatch({ diff --git a/webpack/components/SnapshotManagement/components/BulkActions/BulkSnapshotModalScene/BulkSnapshotModalScene.js b/webpack/components/SnapshotManagement/components/BulkActions/BulkSnapshotModalScene/BulkSnapshotModalScene.js index fad8936..9779dc7 100644 --- a/webpack/components/SnapshotManagement/components/BulkActions/BulkSnapshotModalScene/BulkSnapshotModalScene.js +++ b/webpack/components/SnapshotManagement/components/BulkActions/BulkSnapshotModalScene/BulkSnapshotModalScene.js @@ -4,14 +4,24 @@ import WrappedSnapshotFormModal from '../../SnapshotFormModal'; import useSnapshotSubmit from '../../hooks/useSnapshotSubmit'; const BulkSnapshotModalScene = () => { - const { selectedResults = [], selectedCount = 0 } = - useContext(ForemanActionsBarContext) || {}; + const { + selectedResults = [], + selectedCount = 0, + fetchBulkParams, + exclusionSet, + selectAllMode, + response = {}, + } = useContext(ForemanActionsBarContext) || {}; const { handleSubmit } = useSnapshotSubmit(); return ( ); diff --git a/webpack/components/SnapshotManagement/components/SnapshotForm/SnapshotForm.js b/webpack/components/SnapshotManagement/components/SnapshotForm/SnapshotForm.js index 0630b9d..53d7e5d 100644 --- a/webpack/components/SnapshotManagement/components/SnapshotForm/SnapshotForm.js +++ b/webpack/components/SnapshotManagement/components/SnapshotForm/SnapshotForm.js @@ -30,7 +30,11 @@ const SnapshotForm = ({ onSubmit, selectedHosts, setModalClosed, + fetchBulkParams, host, + selectAllMode, + exclusionSet, + allHosts, }) => { let nameValidation = Yup.string().max(80, 'Too Long!'); if (capabilities.limitSnapshotNameFormat) @@ -55,9 +59,13 @@ const SnapshotForm = ({ const [snapshotMode, setSnapshotMode] = useState(''); const [isSelectOpen, setIsSelectOpen] = useState(false); + const hostsToCheck = selectAllMode + ? allHosts.filter(h => !exclusionSet.has(h.id)) + : selectedHosts; + const allSelectedSupportQuiesce = - selectedHosts?.length > 0 && - selectedHosts.every(h => + hostsToCheck?.length > 0 && + hostsToCheck.every(h => h.capabilities?.includes('snapshot_include_quiesce') ); @@ -101,25 +109,15 @@ const SnapshotForm = ({ }, }; - const hostsIds = - Array.isArray(selectedHosts) && selectedHosts.length > 0 - ? selectedHosts.map(h => h.id) - : []; - const singleHostId = host?.id; - let ids = []; - if (hostsIds.length > 0) { - ids = hostsIds; - } else if (singleHostId) { - ids = [singleHostId]; - } - - if (ids.length === 0) { - actions.setSubmitting(false); - return; - } - const payload = { host_ids: ids, ...submitValues }; + const payload = { + included: { + search: host ? `id = ${singleHostId}` : fetchBulkParams(), + }, + ...(singleHostId && { hostId: singleHostId }), + ...submitValues, + }; await onSubmit(payload, actions); } catch (error) { if (actions.setStatus) { @@ -298,6 +296,10 @@ SnapshotForm.propTypes = { quiesceOption: PropTypes.bool, }), }), + fetchBulkParams: PropTypes.func.isRequired, + selectAllMode: PropTypes.bool, + exclusionSet: PropTypes.instanceOf(Set), + allHosts: PropTypes.array, }; SnapshotForm.defaultProps = { @@ -314,6 +316,9 @@ SnapshotForm.defaultProps = { }, selectedHosts: [], host: null, + selectAllMode: false, + exclusionSet: new Set(), + allHosts: [], }; export default SnapshotForm; diff --git a/webpack/components/SnapshotManagement/components/SnapshotForm/__tests__/SnapshotForm.test.js b/webpack/components/SnapshotManagement/components/SnapshotForm/__tests__/SnapshotForm.test.js index a30345b..215c9f5 100644 --- a/webpack/components/SnapshotManagement/components/SnapshotForm/__tests__/SnapshotForm.test.js +++ b/webpack/components/SnapshotManagement/components/SnapshotForm/__tests__/SnapshotForm.test.js @@ -2,16 +2,20 @@ import { testComponentSnapshotsWithFixtures } from 'react-redux-test-utils'; import SnapshotForm from '../SnapshotForm'; +const fetchBulkParams = () => null; + const fixtures = { - render: { hostId: 42, onSubmit: () => null }, + render: { hostId: 42, onSubmit: () => null, fetchBulkParams }, 'render with limitSnapshotNameFormat capability': { hostId: 42, onSubmit: () => null, + fetchBulkParams, capabilities: { limitSnapshotNameFormat: true }, }, 'render with optional Props': { hostId: 42, onSubmit: () => null, + fetchBulkParams, initialValues: { name: 'Snapshot1', description: 'Hello World', diff --git a/webpack/components/SnapshotManagement/components/SnapshotFormModal/SnapshotFormModal.js b/webpack/components/SnapshotManagement/components/SnapshotFormModal/SnapshotFormModal.js index 0e54974..58b01bc 100644 --- a/webpack/components/SnapshotManagement/components/SnapshotFormModal/SnapshotFormModal.js +++ b/webpack/components/SnapshotManagement/components/SnapshotFormModal/SnapshotFormModal.js @@ -11,6 +11,7 @@ const SnapshotFormModal = ({ selectedHostsCount, setModalClosed, host, + fetchBulkParams, ...props }) => { const displayCount = selectedHostsCount === 0 ? 1 : selectedHostsCount; @@ -50,6 +51,7 @@ const SnapshotFormModal = ({ setModalClosed={setModalClosed} selectedHosts={selectedHosts} host={host} + fetchBulkParams={fetchBulkParams} {...props} /> )} @@ -76,6 +78,7 @@ SnapshotFormModal.propTypes = { quiesceOption: PropTypes.bool, }), }), + fetchBulkParams: PropTypes.func.isRequired, }; SnapshotFormModal.defaultProps = { diff --git a/webpack/components/SnapshotManagement/components/SnapshotFormModal/__tests__/SnapshotFormModal.test.js b/webpack/components/SnapshotManagement/components/SnapshotFormModal/__tests__/SnapshotFormModal.test.js index ec9dd9f..f0c0b4c 100644 --- a/webpack/components/SnapshotManagement/components/SnapshotFormModal/__tests__/SnapshotFormModal.test.js +++ b/webpack/components/SnapshotManagement/components/SnapshotFormModal/__tests__/SnapshotFormModal.test.js @@ -9,8 +9,13 @@ import SnapshotFormModal from '../SnapshotFormModal'; // const mockStore = configureStore([]); const setModalClosed = () => null; +const fetchBulkParams = () => null; const fixtures = { - normal: { host: { id: 42, name: 'deep.thought' }, setModalClosed }, + normal: { + host: { id: 42, name: 'deep.thought' }, + setModalClosed, + fetchBulkParams, + }, }; describe('SnapshotFormModal', () => { diff --git a/webpack/components/SnapshotManagement/components/SnapshotFormModal/__tests__/__snapshots__/SnapshotFormModal.test.js.snap b/webpack/components/SnapshotManagement/components/SnapshotFormModal/__tests__/__snapshots__/SnapshotFormModal.test.js.snap index fc32887..fe2a4e1 100644 --- a/webpack/components/SnapshotManagement/components/SnapshotFormModal/__tests__/__snapshots__/SnapshotFormModal.test.js.snap +++ b/webpack/components/SnapshotManagement/components/SnapshotFormModal/__tests__/__snapshots__/SnapshotFormModal.test.js.snap @@ -30,6 +30,7 @@ exports[`SnapshotFormModal renders normal 1`] = `