Skip to content

Commit 47d1648

Browse files
committed
Fixes #38948 - Add power state bulk action to new host overview page
1 parent d61cf5a commit 47d1648

File tree

13 files changed

+588
-124
lines changed

13 files changed

+588
-124
lines changed

app/controllers/api/v2/hosts_bulk_actions_controller.rb

Lines changed: 78 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@ class HostsBulkActionsController < V2::BaseController
55
include Api::V2::BulkHostsExtension
66

77
before_action :find_deletable_hosts, :only => [:bulk_destroy]
8-
before_action :find_editable_hosts, :only => [:build, :reassign_hostgroup, :change_owner, :disassociate]
8+
before_action :find_editable_hosts, :only => [:build, :reassign_hostgroup, :change_owner, :disassociate, :change_power_state]
9+
before_action :validate_power_action, :only => [:change_power_state]
910

1011
def_param_group :bulk_host_ids do
1112
param :organization_id, :number, :required => true, :desc => N_("ID of the organization")
@@ -63,6 +64,56 @@ def build
6364
end
6465
end
6566

67+
api :PUT, "/hosts/bulk/change_power_state", N_("Change power state")
68+
param_group :bulk_host_ids
69+
param :power, String, :required => true, :desc => N_("Power action to perform (start, stop, poweroff, reboot, reset, soft, cycle)")
70+
def change_power_state
71+
action = params[:power]
72+
73+
manager = BulkHostsManager.new(hosts: @hosts)
74+
result = manager.change_power_state(action)
75+
76+
failed_hosts = result[:failed_hosts] || []
77+
failed_host_ids = result[:failed_host_ids] || []
78+
unsupported_hosts = result[:unsupported_hosts] || []
79+
unsupported_host_ids = result[:unsupported_host_ids] || []
80+
81+
if failed_hosts.empty? && unsupported_hosts.empty?
82+
render json: {
83+
message: _('The power state of the selected hosts will be set to %s') % _(action),
84+
}, status: :ok
85+
else
86+
total_failed = failed_hosts.size
87+
total_unsupported = unsupported_hosts.size
88+
89+
parts = []
90+
if total_failed > 0
91+
parts << n_(
92+
"Failed to set power state for %s host.",
93+
"Failed to set power state for %s hosts.",
94+
total_failed
95+
) % total_failed
96+
end
97+
98+
if total_unsupported > 0
99+
parts << n_(
100+
"%s host does not support power management.",
101+
"%s hosts do not support power management.",
102+
total_unsupported
103+
) % total_unsupported
104+
end
105+
106+
render json: {
107+
error: {
108+
message: parts.join(' '),
109+
failed_host_ids: (failed_host_ids + unsupported_host_ids).uniq,
110+
failed_hosts: failed_hosts,
111+
unsupported_hosts: unsupported_hosts,
112+
},
113+
}, status: :unprocessable_entity
114+
end
115+
end
116+
66117
api :PUT, "/hosts/bulk/reassign_hostgroups", N_("Reassign hostgroups")
67118
param_group :bulk_host_ids
68119
param :hostgroup_id, :number, :desc => N_("ID of the hostgroup to reassign the hosts to")
@@ -126,7 +177,7 @@ def assign_location
126177

127178
def action_permission
128179
case params[:action]
129-
when 'build'
180+
when 'build', 'change_power_state'
130181
'edit'
131182
else
132183
super
@@ -173,6 +224,31 @@ def rebuild_config
173224
)
174225
end
175226
end
227+
228+
def validate_power_action
229+
action = params[:power]
230+
host_ids = @hosts&.map(&:id) || []
231+
232+
return true if action.present? && PowerManager::REAL_ACTIONS.include?(action)
233+
234+
if action.blank?
235+
render json: {
236+
error: {
237+
message: _("Power action is required"),
238+
failed_host_ids: host_ids,
239+
},
240+
}, status: :unprocessable_entity
241+
else
242+
render json: {
243+
error: {
244+
message: _("Invalid power action"),
245+
valid_power_actions: PowerManager::REAL_ACTIONS,
246+
failed_host_ids: host_ids,
247+
},
248+
}, status: :unprocessable_entity
249+
end
250+
false
251+
end
176252
end
177253
end
178254
end

app/services/bulk_hosts_manager.rb

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,4 +66,36 @@ def assign_taxonomy(taxonomy, optimistic_import)
6666
raise _("Cannot update %{type} to %{name} because of mismatch in settings") % {type: taxonomy.type.downcase, name: taxonomy.name}
6767
end
6868
end
69+
70+
def change_power_state(action)
71+
failed_hosts = []
72+
unsupported_hosts = []
73+
74+
@hosts.each do |host|
75+
unless host.supports_power?
76+
unsupported_hosts << {
77+
id: host.id,
78+
error: _('Power management not available for this host'),
79+
}
80+
next
81+
end
82+
83+
begin
84+
host.power.send(action.to_sym)
85+
rescue => error
86+
Foreman::Logging.exception("Failed to set power state for #{host}.", error)
87+
failed_hosts << {
88+
id: host.id,
89+
error: error.message,
90+
}
91+
end
92+
end
93+
94+
{
95+
failed_hosts: failed_hosts,
96+
failed_host_ids: failed_hosts.map { |h| h[:id] },
97+
unsupported_hosts: unsupported_hosts,
98+
unsupported_host_ids: unsupported_hosts.map { |h| h[:id] },
99+
}
100+
end
69101
end

config/initializers/f_foreman_permissions.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -271,7 +271,7 @@
271271
:"api/v2/hosts" => [:update, :disassociate, :forget_status],
272272
:"api/v2/interfaces" => [:create, :update, :destroy],
273273
:"api/v2/compute_resources" => [:associate],
274-
:"api/v2/hosts_bulk_actions" => [:assign_organization, :assign_location, :build, :reassign_hostgroup, :change_owner, :disassociate],
274+
:"api/v2/hosts_bulk_actions" => [:assign_organization, :assign_location, :build, :reassign_hostgroup, :change_owner, :disassociate, :change_power_state],
275275
}
276276
map.permission :destroy_hosts, {:hosts => [:destroy, :multiple_actions, :reset_multiple, :multiple_destroy, :submit_multiple_destroy],
277277
:"api/v2/hosts" => [:destroy],

config/routes/api/v2.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
put 'hosts/bulk/assign_location', :to => 'hosts_bulk_actions#assign_location'
99
match 'hosts/bulk/build', :to => 'hosts_bulk_actions#build', :via => [:put]
1010
match 'hosts/bulk/change_owner', :to => 'hosts_bulk_actions#change_owner', :via => [:put]
11+
put 'hosts/bulk/change_power_state', :to => 'hosts_bulk_actions#change_power_state'
1112
put 'hosts/bulk/disassociate', :to => 'hosts_bulk_actions#disassociate'
1213
match 'hosts/bulk/reassign_hostgroup', :to => 'hosts_bulk_actions#reassign_hostgroup', :via => [:put]
1314

test/controllers/api/v2/hosts_bulk_actions_controller_test.rb

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,10 @@ def valid_bulk_params(host_ids = @host_ids)
2626
}
2727
end
2828

29+
def valid_power_params(host_ids = @host_ids, action = 'start')
30+
valid_bulk_params(host_ids).merge(:power => action)
31+
end
32+
2933
test "should change owner with user id" do
3034
put :change_owner, params: valid_bulk_params.merge(:owner_id => @user.id_and_type)
3135

@@ -103,6 +107,88 @@ def valid_bulk_params(host_ids = @host_ids)
103107
end
104108
end
105109

110+
context "change_power_state" do
111+
test "successfully changes power state for all hosts" do
112+
Host.any_instance.stubs(:supports_power?).returns(true)
113+
power_mock = mock('power')
114+
Host.any_instance.stubs(:power).returns(power_mock)
115+
power_mock.expects(:send).with(:start).times(@host_ids.size)
116+
117+
put :change_power_state, params: valid_power_params(@host_ids, 'start')
118+
119+
assert_response :success
120+
body = ActiveSupport::JSON.decode(@response.body)
121+
assert_match(/The power state of the selected hosts will be set to start/, body['message'])
122+
end
123+
124+
test "returns failed_host_ids when all hosts fail" do
125+
Host.any_instance.stubs(:supports_power?).returns(true)
126+
power_mock = mock('power')
127+
Host.any_instance.stubs(:power).returns(power_mock)
128+
power_mock.stubs(:send).with(:start).raises(StandardError.new('Power operation failed'))
129+
130+
put :change_power_state, params: valid_power_params(@host_ids, 'start')
131+
132+
assert_response :unprocessable_entity
133+
body = ActiveSupport::JSON.decode(@response.body)
134+
assert_match(/Failed to set power state for 3 hosts/, body['error']['message'])
135+
assert_equal @host_ids.sort, body['error']['failed_host_ids'].sort
136+
end
137+
138+
test "returns error when power param is missing" do
139+
Host.any_instance.stubs(:supports_power?).returns(true)
140+
power_mock = mock('power')
141+
Host.any_instance.stubs(:power).returns(power_mock)
142+
power_mock.stubs(:send).raises(StandardError.new('Power operation failed'))
143+
144+
put :change_power_state, params: valid_bulk_params(@host_ids)
145+
146+
assert_response :unprocessable_entity
147+
body = ActiveSupport::JSON.decode(@response.body)
148+
assert_equal "Power action is required", body['error']['message']
149+
assert_equal @host_ids.sort, body['error']['failed_host_ids'].sort
150+
end
151+
152+
test "returns error when power action is invalid" do
153+
put :change_power_state, params: valid_power_params(@host_ids, 'invalid')
154+
155+
assert_response :unprocessable_entity
156+
body = ActiveSupport::JSON.decode(@response.body)
157+
assert_equal "Invalid power action", body['error']['message']
158+
assert_equal PowerManager::REAL_ACTIONS, body['error']['valid_power_actions']
159+
assert_equal @host_ids.sort, body['error']['failed_host_ids'].sort
160+
end
161+
162+
test "handles mixed hosts with no power support, failure, and success" do
163+
Host.any_instance.stubs(:supports_power?)
164+
.returns(false)
165+
.then.returns(true)
166+
.then.returns(true)
167+
168+
power_fail = mock('power_fail')
169+
power_ok = mock('power_ok')
170+
171+
Host.any_instance.stubs(:power)
172+
.returns(power_fail)
173+
.then.returns(power_ok)
174+
175+
power_fail.expects(:send).with(:start).raises(StandardError.new('Power operation failed'))
176+
power_ok.expects(:send).with(:start)
177+
178+
put :change_power_state, params: valid_power_params(@host_ids, 'start')
179+
180+
assert_response :unprocessable_entity
181+
body = ActiveSupport::JSON.decode(@response.body)
182+
# Message should contain both failure types
183+
assert_match(/Failed to set power state for 1 host/, body['error']['message'])
184+
assert_match(/1 host does not support power management/, body['error']['message'])
185+
assert_equal 2, body['error']['failed_host_ids'].size
186+
body['error']['failed_host_ids'].each do |id|
187+
assert_includes @host_ids, id
188+
end
189+
end
190+
end
191+
106192
private
107193

108194
def set_session_user

0 commit comments

Comments
 (0)