diff --git a/app/controllers/api/v2/hosts_bulk_actions_controller.rb b/app/controllers/api/v2/hosts_bulk_actions_controller.rb
index 872c0d1e92..501229ab9f 100644
--- a/app/controllers/api/v2/hosts_bulk_actions_controller.rb
+++ b/app/controllers/api/v2/hosts_bulk_actions_controller.rb
@@ -94,6 +94,34 @@ def disassociate
"Updated hosts: Disassociated from compute resource", @hosts.count)})
end
+ api :PUT, "/hosts/bulk/assign_organization", N_("Assign organization")
+ param_group :bulk_host_ids
+ param :id, :number, :required => true, :desc => N_("The organization ID to assign the hosts to")
+ param :mismatch_setting, :bool, :required => true, :desc => N_("Fix organization on mismatch")
+ def assign_organization
+ without_taxonomy do
+ find_editable_hosts
+ taxonomy = Organization.find(params[:id])
+ BulkHostsManager.new(hosts: @hosts).assign_taxonomy(taxonomy, params[:mismatch_setting])
+ message = _("Organization is set to %s") % taxonomy.name
+ process_response(true, { :message => n_("Updated host: #{message}", "Updated hosts: #{message}", @hosts.count)})
+ end
+ end
+
+ api :PUT, "/hosts/bulk/assign_location", N_("Assign location")
+ param_group :bulk_host_ids
+ param :id, :number, :required => true, :desc => N_("The location ID to assign the hosts to")
+ param :mismatch_setting, :bool, :required => true, :desc => N_("Fix location on mismatch")
+ def assign_location
+ without_taxonomy do
+ find_editable_hosts
+ taxonomy = Location.find(params[:id])
+ BulkHostsManager.new(hosts: @hosts).assign_taxonomy(taxonomy, params[:mismatch_setting])
+ message = _("Location is set to %s") % taxonomy.name
+ process_response(true, { :message => n_("Updated host: #{message}", "Updated hosts: #{message}", @hosts.count)})
+ end
+ end
+
protected
def action_permission
@@ -115,6 +143,16 @@ def find_editable_hosts
find_bulk_hosts(:edit_hosts, params)
end
+ def without_taxonomy
+ context = Foreman::ThreadSession::Context.get
+ Foreman::ThreadSession::Context.set(user: context[:user])
+ yield
+ rescue => e
+ render_error(:custom_error, :status => :unprocessable_entity, :locals => { :message => e.message})
+ ensure
+ Foreman::ThreadSession::Context.set(**context) if context
+ end
+
def rebuild_config
all_fails = BulkHostsManager.new(hosts: @hosts).rebuild_configuration
failed_host_ids = all_fails.flat_map { |_key, values| values&.map(&:id) }
diff --git a/app/controllers/concerns/foreman/controller/taxonomy_multiple.rb b/app/controllers/concerns/foreman/controller/taxonomy_multiple.rb
index f812a0dcef..94e2f0efc2 100644
--- a/app/controllers/concerns/foreman/controller/taxonomy_multiple.rb
+++ b/app/controllers/concerns/foreman/controller/taxonomy_multiple.rb
@@ -36,21 +36,12 @@ def update_multiple_taxonomies(type)
end
taxonomy = Taxonomy.find_by_id(id)
-
- if params[type][:optimistic_import] == 'yes'
- @hosts.update_all("#{type}_id".to_sym => taxonomy.id)
- # hosts location needs to be updated before import missing ids
- taxonomy.import_missing_ids
- else
- if taxonomy.need_to_be_selected_ids.count == 0
- @hosts.update_all("#{type}_id".to_sym => taxonomy.id)
- else
- error "Cannot update #{taxonomy.type} to #{taxonomy.name} because of mismatch in settings"
- redirect_back_or_to helpers.current_hosts_path
- return
- end
+ begin
+ BulkHostsManager.new(hosts: @hosts).assign_taxonomy(taxonomy, params[type][:optimistic_import] == 'yes')
+ success "Updated hosts: Changed #{type.to_s.classify}"
+ rescue => e
+ error e.message
end
- success "Updated hosts: Changed #{type.to_s.classify}"
redirect_back_or_to helpers.current_hosts_path
end
end
diff --git a/app/services/bulk_hosts_manager.rb b/app/services/bulk_hosts_manager.rb
index 5ebadc1d47..d439dc5ced 100644
--- a/app/services/bulk_hosts_manager.rb
+++ b/app/services/bulk_hosts_manager.rb
@@ -51,4 +51,19 @@ def disassociate
host.disassociate!
end
end
+
+ # @param [Boolean] optimistic_import
+ # either fix on mismatch or fail on mismatch
+ def assign_taxonomy(taxonomy, optimistic_import)
+ tax_type = taxonomy.type.downcase
+ if optimistic_import
+ @hosts.update_all("#{tax_type}_id".to_sym => taxonomy.id)
+ # hosts location needs to be updated before import missing ids
+ taxonomy.import_missing_ids
+ elsif taxonomy.need_to_be_selected_ids.empty?
+ @hosts.update_all("#{tax_type}_id".to_sym => taxonomy.id)
+ else
+ raise _("Cannot update %{type} to %{name} because of mismatch in settings") % {type: taxonomy.type.downcase, name: taxonomy.name}
+ end
+ end
end
diff --git a/config/initializers/f_foreman_permissions.rb b/config/initializers/f_foreman_permissions.rb
index 4925c1dbe6..67d83d5ccf 100644
--- a/config/initializers/f_foreman_permissions.rb
+++ b/config/initializers/f_foreman_permissions.rb
@@ -271,7 +271,7 @@
:"api/v2/hosts" => [:update, :disassociate, :forget_status],
:"api/v2/interfaces" => [:create, :update, :destroy],
:"api/v2/compute_resources" => [:associate],
- :"api/v2/hosts_bulk_actions" => [:build, :reassign_hostgroup, :change_owner, :disassociate],
+ :"api/v2/hosts_bulk_actions" => [:assign_organization, :assign_location, :build, :reassign_hostgroup, :change_owner, :disassociate],
}
map.permission :destroy_hosts, {:hosts => [:destroy, :multiple_actions, :reset_multiple, :multiple_destroy, :submit_multiple_destroy],
:"api/v2/hosts" => [:destroy],
diff --git a/config/routes/api/v2.rb b/config/routes/api/v2.rb
index e66d0abc82..664e570e54 100644
--- a/config/routes/api/v2.rb
+++ b/config/routes/api/v2.rb
@@ -4,6 +4,8 @@
# new v2 routes that point to v2
scope "(:apiv)", :module => :v2, :defaults => {:apiv => 'v2'}, :apiv => /v2/, :constraints => ApiConstraints.new(:version => 2, :default => true) do
match 'hosts/bulk', :to => 'hosts_bulk_actions#bulk_destroy', :via => [:delete]
+ put 'hosts/bulk/assign_organization', :to => 'hosts_bulk_actions#assign_organization'
+ put 'hosts/bulk/assign_location', :to => 'hosts_bulk_actions#assign_location'
match 'hosts/bulk/build', :to => 'hosts_bulk_actions#build', :via => [:put]
match 'hosts/bulk/change_owner', :to => 'hosts_bulk_actions#change_owner', :via => [:put]
put 'hosts/bulk/disassociate', :to => 'hosts_bulk_actions#disassociate'
diff --git a/test/controllers/hosts_controller_test.rb b/test/controllers/hosts_controller_test.rb
index 149b83b96f..139e4c01c6 100644
--- a/test/controllers/hosts_controller_test.rb
+++ b/test/controllers/hosts_controller_test.rb
@@ -812,7 +812,7 @@ def test_unset_manage
:host_ids => Host.pluck('hosts.id'),
}, session: set_session_user
assert_redirected_to new_hosts_index_page_path
- assert flash[:error] == "Cannot update Location to Location 1 because of mismatch in settings"
+ assert_equal "Cannot update location to Location 1 because of mismatch in settings", flash[:error]
end
test "update multiple location does not update location of hosts if fails on pessimistic import" do
@request.env['HTTP_REFERER'] = current_hosts_path
@@ -871,7 +871,7 @@ def test_unset_manage
:host_ids => Host.pluck('hosts.id'),
}, session: set_session_user
assert_redirected_to new_hosts_index_page_path
- assert_equal "Cannot update Organization to Organization 1 because of mismatch in settings", flash[:error]
+ assert_equal "Cannot update organization to Organization 1 because of mismatch in settings", flash[:error]
end
test "update multiple organization does not update organization of hosts if fails on pessimistic import" do
@request.env['HTTP_REFERER'] = current_hosts_path
diff --git a/webpack/assets/javascripts/react_app/components/HostsIndex/BulkActions/assignTaxonomy/BulkAssignTaxonomyConstants.js b/webpack/assets/javascripts/react_app/components/HostsIndex/BulkActions/assignTaxonomy/BulkAssignTaxonomyConstants.js
new file mode 100644
index 0000000000..bfcf75fd00
--- /dev/null
+++ b/webpack/assets/javascripts/react_app/components/HostsIndex/BulkActions/assignTaxonomy/BulkAssignTaxonomyConstants.js
@@ -0,0 +1,8 @@
+export const BULK_ASSIGN_ORGANIZATION_KEY = 'BULK_ASSIGN_ORGANIZATION';
+export const BULK_ASSIGN_LOCATION_KEY = 'BULK_ASSIGN_LOCATION';
+export const ORGANIZATION_KEY = 'ORGANIZATION';
+export const LOCATION_KEY = 'LOCATION';
+export const MODAL_TYPES = {
+ ORGANIZATION: 'ORGANIZATION',
+ LOCATION: 'LOCATION',
+};
diff --git a/webpack/assets/javascripts/react_app/components/HostsIndex/BulkActions/assignTaxonomy/BulkAssignTaxonomyModal.js b/webpack/assets/javascripts/react_app/components/HostsIndex/BulkActions/assignTaxonomy/BulkAssignTaxonomyModal.js
new file mode 100644
index 0000000000..5bc8273709
--- /dev/null
+++ b/webpack/assets/javascripts/react_app/components/HostsIndex/BulkActions/assignTaxonomy/BulkAssignTaxonomyModal.js
@@ -0,0 +1,252 @@
+import React, { useState, useEffect } from 'react';
+import PropTypes from 'prop-types';
+import { useDispatch, useSelector } from 'react-redux';
+import { FormattedMessage } from 'react-intl';
+import {
+ Modal,
+ Button,
+ MenuToggle,
+ SelectOption,
+ TextContent,
+ Text,
+ TreeView,
+} from '@patternfly/react-core';
+import { addToast } from '../../../ToastsList/slice';
+import { translate as __ } from '../../../../common/I18n';
+import { STATUS } from '../../../../constants';
+import {
+ selectAPIStatus,
+ selectAPIResponse,
+} from '../../../../redux/API/APISelectors';
+import {
+ bulkAssignOrganization,
+ bulkAssignLocation,
+ fetchOrganizations,
+ fetchLocations,
+} from './actions';
+import {
+ BULK_ASSIGN_ORGANIZATION_KEY,
+ BULK_ASSIGN_LOCATION_KEY,
+ ORGANIZATION_KEY,
+ LOCATION_KEY,
+ MODAL_TYPES,
+} from './BulkAssignTaxonomyConstants';
+import { foremanUrl } from '../../../../common/helpers';
+import { APIActions } from '../../../../redux/API';
+import {
+ HOSTS_API_PATH,
+ API_REQUEST_KEY,
+} from '../../../../routes/Hosts/constants';
+import TaxonomySelect from './TaxonomySelect';
+
+export const BulkAssignOrganizationModal = props => (
+