Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion app/controllers/api/v2/hosts_bulk_actions_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ class HostsBulkActionsController < V2::BaseController
include Api::V2::BulkHostsExtension

before_action :find_deletable_hosts, :only => [:bulk_destroy]
before_action :find_editable_hosts, :only => [:build, :reassign_hostgroup, :change_owner]
before_action :find_editable_hosts, :only => [:build, :reassign_hostgroup, :change_owner, :disassociate]

def_param_group :bulk_host_ids do
param :organization_id, :number, :required => true, :desc => N_("ID of the organization")
Expand Down Expand Up @@ -86,6 +86,14 @@ def change_owner
process_response(true, { :message => n_("Updated host: changed owner", "Updated hosts: changed owner", @hosts.count)})
end

api :PUT, "/hosts/bulk/disassociate", N_("Disassociate compute resources")
param_group :bulk_host_ids
def disassociate
BulkHostsManager.new(hosts: @hosts).disassociate
process_response(true, { :message => n_("Updated host: Disassociated from compute resource",
"Updated hosts: Disassociated from compute resource", @hosts.count)})
end

protected

def action_permission
Expand Down
4 changes: 1 addition & 3 deletions app/controllers/hosts_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -551,9 +551,7 @@ def multiple_disassociate
end

def update_multiple_disassociate
@hosts.each do |host|
host.disassociate!
end
BulkHostsManager.new(hosts: @hosts).disassociate
success _('Updated hosts: Disassociated from VM')
redirect_back_or_to helpers.current_hosts_path
end
Expand Down
6 changes: 6 additions & 0 deletions app/services/bulk_hosts_manager.rb
Original file line number Diff line number Diff line change
Expand Up @@ -45,4 +45,10 @@ def change_owner(owner_id)
host.save(:validate => false)
end
end

def disassociate
@hosts.each do |host|
host.disassociate!
end
Comment on lines 50 to 52

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm going to note this down for my own sanity, because I've been looking at it multiple times.

The method disassociate! is defined here:

def disassociate!
self.uuid = nil
self.compute_resource_id = nil
save!(:validate => false) # don't want to trigger callbacks
end

Now the comment says it doesn't trigger callbacks, but not what https://api.rubyonrails.org/v8.0.2/classes/ActiveRecord/Persistence.html#method-i-save-21 states it does run callbacks. Callbacks triggers audits and hooks, possibly more. That means you can't replace it with update_all since that uses direct SQL to update them all in bulk.

If we already accept that we need to save each individual object then there's still the question of counting the affected rows.

save!() will always return true or raise an exception so there's no way of knowing if it actually modified any records. That means you need to count.

Last note: that I found while looking this up: there is MyClass.in_batches.each_record to avoid instantiating all objects. For large collections of records this can save with memory consumption. That's probably something more for a general BulkHostsManager optimization.

Short summary is that for now this is probably the best we can do.

end
end
2 changes: 1 addition & 1 deletion config/initializers/f_foreman_permissions.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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],
:"api/v2/hosts_bulk_actions" => [: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],
Expand Down
1 change: 1 addition & 0 deletions config/routes/api/v2.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
match 'hosts/bulk', :to => 'hosts_bulk_actions#bulk_destroy', :via => [:delete]
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'
match 'hosts/bulk/reassign_hostgroup', :to => 'hosts_bulk_actions#reassign_hostgroup', :via => [:put]

resources :architectures, :except => [:new, :edit] do
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,199 @@
import React from 'react';
import PropTypes from 'prop-types';
import { useDispatch } from 'react-redux';
import {
Modal,
Alert,
Button,
TextContent,
Text,
TreeView,
} from '@patternfly/react-core';
import { addToast } from '../../../ToastsList/slice';
import { foremanUrl } from '../../../../common/helpers';
import { translate as __ } from '../../../../common/I18n';
import { BULK_DISASSOCIATE_KEY, bulkDisassociate } from './actions';
import { APIActions } from '../../../../redux/API';
import {
HOSTS_API_PATH,
API_REQUEST_KEY,
} from '../../../../routes/Hosts/constants';
import { failedHostsToastParams } from '../helpers';

const BulkDisassociateModal = ({
isOpen,
closeModal,
selectAllHostsMode,
selectedCount,
selectedResults,
fetchBulkParams,
}) => {
const dispatch = useDispatch();
const hostsWithComputeResource = selectedResults?.filter(
h => h.compute_resource_id && h.uuid
);
const hostsWithoutComputeResource = selectedResults?.filter(
h => !hostsWithComputeResource.includes(h)
);
const selectedResultsEmpty = selectedResults?.length === 0;

const selectedTreeViewData = [
{
name: __('Selected hosts'),
id: 'selected-hosts-tree-view-title',
customBadgeContent: selectAllHostsMode ? 'All' : selectedCount,
},
];
const applicableTreeViewData = [
{
name: __('Hosts associated to compute resources'),
id: 'applicable-hosts-tree-view-title',
customBadgeContent: hostsWithComputeResource?.length,
},
];
const excludedTreeViewData = [
{
name: __('Excluded hosts'),
id: 'excluded-hosts-tree-view-title',
customBadgeContent: hostsWithoutComputeResource?.length,
},
];

const handleError = response => {
closeModal();
dispatch(
addToast(
failedHostsToastParams({
...response.data.error,
key: BULK_DISASSOCIATE_KEY,
})
)
);
};

const handleSuccess = response => {
dispatch(
addToast({
type: 'success',
message: response.data.message,
})
);
dispatch(
APIActions.get({
key: API_REQUEST_KEY,
url: foremanUrl(HOSTS_API_PATH),
})
);
closeModal();
};

const handleConfirm = () => {
const queryString = selectedResultsEmpty
? fetchBulkParams()
: `id ^ (${hostsWithComputeResource.map(h => h.id).join(',')})`;
const requestBody = {
included: {
search: queryString,
},
};

dispatch(bulkDisassociate(requestBody, handleSuccess, handleError));
};

const modalActions = [
<Button
key="add"
ouiaId="bulk-disassociate-modal-add-button"
variant="primary"
onClick={handleConfirm}
isDisabled={
hostsWithComputeResource?.length === 0 && !selectedResultsEmpty
}
>
{__('Disassociate')}
</Button>,
<Button
key="cancel"
ouiaId="bulk-disassociate-modal-cancel-button"
variant="link"
onClick={closeModal}
>
{__('Cancel')}
</Button>,
];

return (
<Modal
isOpen={isOpen}
onClose={closeModal}
onEscapePress={closeModal}
title={__('Disassociate hosts')}
width="50%"
position="top"
actions={modalActions}
id="bulk-disassociate-modal"
key="bulk-disassociate-modal"
ouiaId="bulk-disassociate-modal"
>
<TextContent>
<Text ouiaId="bulk-disassociate-options">
{__(
'This will disassociate the host in Foreman from its compute resource.'
)}
<br />
{__(
'After disassociating, a host can be deleted from Foreman without affecting its virtual machine.'
)}
</Text>
</TextContent>
<Alert
style={{ marginTop: '2rem', marginBottom: '1rem' }}
variant="warning"
isInline
isPlain
title={__('Hosts without a compute resource will be excluded.')}
ouiaId="warning-alert"
/>
<div style={{ width: '70%', maxHeight: '50%', marginLeft: '-1rem' }}>
{selectedResultsEmpty && (
<TreeView
Comment thread
ekohl marked this conversation as resolved.
Outdated
data={selectedTreeViewData}
aria-label={__('Selected hosts')}
hasBadges
/>
)}
{!selectedResultsEmpty && (
<>
<TreeView
data={applicableTreeViewData}
aria-label={__('Hosts associated to compute resources')}
hasBadges
/>
<TreeView
data={excludedTreeViewData}
aria-label={__('Excluded hosts')}
hasBadges
/>
</>
)}
</div>
</Modal>
);
};

BulkDisassociateModal.propTypes = {
isOpen: PropTypes.bool,
closeModal: PropTypes.func,
selectedResults: PropTypes.array,
fetchBulkParams: PropTypes.func.isRequired,
selectedCount: PropTypes.number.isRequired,
selectAllHostsMode: PropTypes.bool.isRequired,
};

BulkDisassociateModal.defaultProps = {
isOpen: false,
closeModal: () => {},
selectedResults: [],
};

export default BulkDisassociateModal;
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { APIActions } from '../../../../redux/API';
import { foremanUrl } from '../../../../common/helpers';

export const BULK_DISASSOCIATE_KEY = 'BULK_DISASSOCIATE';
export const bulkDisassociate = (params, handleSuccess, handleError) => {
const url = foremanUrl(`/api/v2/hosts/bulk/disassociate`);
return APIActions.put({
key: BULK_DISASSOCIATE_KEY,
url,
handleSuccess,
handleError,
params,
});
};

export default bulkDisassociate;
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import React, { useContext } from 'react';
import { ForemanActionsBarContext } from '../../../../components/HostDetails/ActionsBar';
import { useForemanModal } from '../../../../components/ForemanModal/ForemanModalHooks';
import BulkDisassociateModal from './BulkDisassociateModal';

const BulkDisassociateModalScene = () => {
const {
selectAllHostsMode,
selectedCount,
selectedResults,
fetchBulkParams,
} = useContext(ForemanActionsBarContext);
const { modalOpen, setModalClosed } = useForemanModal({
id: 'bulk-disassociate-modal',
});
return (
<BulkDisassociateModal
key="bulk-disassociate-modal"
selectAllHostsMode={selectAllHostsMode}
selectedCount={selectedCount}
selectedResults={selectedResults}
fetchBulkParams={fetchBulkParams}
isOpen={modalOpen}
closeModal={setModalClosed}
/>
);
};

export default BulkDisassociateModalScene;
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ import { bulkDeleteHosts } from './BulkActions/bulkDelete';
import BulkBuildHostModal from './BulkActions/buildHosts';
import BulkReassignHostgroupModal from './BulkActions/reassignHostGroup';
import BulkChangeOwnerModal from './BulkActions/changeOwner';
import BulkDisassociateModal from './BulkActions/disassociate';
import { foremanUrl } from '../../common/helpers';
import Slot from '../common/Slot';
import forceSingleton from '../../common/forceSingleton';
Expand Down Expand Up @@ -223,6 +224,11 @@ const HostsIndex = () => {
id: 'bulk-change-owner-modal',
})
);
dispatch(
addModal({
id: 'bulk-disassociate-modal',
})
);
}, [dispatch]);

const { setModalOpen: setHgModalOpen } = useForemanModal({
Expand All @@ -234,6 +240,9 @@ const HostsIndex = () => {
const { setModalOpen: setChangeOwnerModalOpen } = useForemanModal({
id: 'bulk-change-owner-modal',
});
const { setModalOpen: setDisassociateModalOpen } = useForemanModal({
id: 'bulk-disassociate-modal',
});

const dropdownItems = [
<MenuItem
Expand All @@ -260,6 +269,14 @@ const HostsIndex = () => {
>
{__('Change owner')}
</MenuItem>,
<MenuItem
itemId="disassociate-dropdown-item"
key="disassociate-dropdown-item"
onClick={setDisassociateModalOpen}
isDisabled={selectedCount === 0}
>
{__('Disassociate hosts')}
</MenuItem>,
];

const dangerZoneItems = [
Expand Down Expand Up @@ -468,6 +485,7 @@ const HostsIndex = () => {
<BulkBuildHostModal key="bulk-build-hosts-modal" />
<BulkReassignHostgroupModal key="bulk-reassign-hg-modal" />
<BulkChangeOwnerModal key="bulk-change-owner-modal" />
<BulkDisassociateModal key="bulk-disassociate-modal" />
<Slot id="_all-hosts-modals" multi />
</ForemanActionsBarContext.Provider>
</TableIndexPage>
Expand Down
Loading