diff --git a/app/controllers/api/v2/hosts_controller.rb b/app/controllers/api/v2/hosts_controller.rb
index 0dd4a381634..7beb3ed9414 100644
--- a/app/controllers/api/v2/hosts_controller.rb
+++ b/app/controllers/api/v2/hosts_controller.rb
@@ -280,6 +280,38 @@ def boot
render_exception(e, :status => :unprocessable_entity)
end
+ api :PUT, "/hosts/:id/wol", N_("Send Wake on LAN request to host")
+ param :id, :identifier_dottable, :required => true
+
+ def wol
+ primary_interface = @host.primary_interface
+
+ unless primary_interface
+ return render_error :custom_error, :status => :unprocessable_entity, :locals => { :message => _('Host has no primary interface') }
+ end
+
+ unless primary_interface.mac.present?
+ return render_error :custom_error, :status => :unprocessable_entity, :locals => { :message => _('Primary interface has no MAC address') }
+ end
+
+ unless primary_interface.subnet&.dhcp?
+ return render_error :custom_error, :status => :unprocessable_entity, :locals => { :message => _('Primary interface subnet has no DHCP proxy configured') }
+ end
+
+ dhcp_proxy = primary_interface.subnet.dhcp
+ unless dhcp_proxy.has_feature?('WOL')
+ return render_error :custom_error, :status => :unprocessable_entity, :locals => { :message => _('DHCP proxy does not have WOL feature enabled') }
+ end
+
+ begin
+ wol_api = ProxyAPI::Wol.new(:url => dhcp_proxy.url)
+ result = wol_api.wake(primary_interface.mac)
+ render :json => { :wol => result }, :status => :ok
+ rescue => e
+ render_error :custom_error, :status => :unprocessable_entity, :locals => { :message => e.message }
+ end
+ end
+
api :POST, "/hosts/facts", N_("Upload facts for a host, creating the host if required")
param :name, String, :required => true, :desc => N_("hostname of the host")
param :facts, Hash, :required => true, :desc => N_("hash containing the facts for the host")
@@ -382,6 +414,8 @@ def action_permission
:power
when 'boot'
:ipmi_boot
+ when 'wol'
+ :power
when 'console'
:console
when 'disassociate', 'forget_status'
@@ -397,7 +431,7 @@ def action_permission
def parent_permission(child_permission)
case child_permission.to_s
- when 'power', 'boot', 'console', 'vm_compute_attributes', 'get_status', 'template', 'enc', 'rebuild_config', 'inherited_parameters'
+ when 'power', 'boot', 'wol', 'console', 'vm_compute_attributes', 'get_status', 'template', 'enc', 'rebuild_config', 'inherited_parameters'
'view'
when 'disassociate'
'edit'
diff --git a/app/services/proxy_api/wol.rb b/app/services/proxy_api/wol.rb
new file mode 100644
index 00000000000..7c8c15b213c
--- /dev/null
+++ b/app/services/proxy_api/wol.rb
@@ -0,0 +1,20 @@
+module ProxyAPI
+ class Wol < ProxyAPI::Resource
+ def initialize(args)
+ @url = args[:url] + "/wol"
+ super args
+ end
+
+ # Send Wake on LAN request
+ # [+mac+] : MAC address in coloned sextuplet format
+ # Returns : Boolean status
+ def wake(mac)
+ raise "Must define a MAC address" if mac.blank?
+
+ params = { :mac_address => mac }
+ parse post(params)
+ rescue => e
+ raise ProxyException.new(url, e, N_("Unable to send Wake on LAN request for MAC %s"), mac)
+ end
+ end
+end
diff --git a/config/initializers/f_foreman_permissions.rb b/config/initializers/f_foreman_permissions.rb
index 6d29d1a8f11..e1a4c22c466 100644
--- a/config/initializers/f_foreman_permissions.rb
+++ b/config/initializers/f_foreman_permissions.rb
@@ -285,7 +285,7 @@
:"api/v2/hosts" => [:rebuild_config],
}
map.permission :power_hosts, {:hosts => [:power],
- :"api/v2/hosts" => [:power, :power_status] }
+ :"api/v2/hosts" => [:power, :power_status, :wol] }
map.permission :console_hosts, {:hosts => [:console] }
map.permission :ipmi_boot_hosts, { :hosts => [:ipmi_boot],
:"api/v2/hosts" => [:boot] }
diff --git a/config/routes/api/v2.rb b/config/routes/api/v2.rb
index 74ce286d37c..688b9bc67c4 100644
--- a/config/routes/api/v2.rb
+++ b/config/routes/api/v2.rb
@@ -319,6 +319,7 @@
put :boot, :on => :member
get :power, :on => :member, :action => :power_status
put :power, :on => :member
+ put :wol, :on => :member
put :rebuild_config, :on => :member
get :inherited_parameters, :on => :member
post :facts, :on => :collection
diff --git a/db/seeds.d/110-smart_proxy_features.rb b/db/seeds.d/110-smart_proxy_features.rb
index f89624b49eb..07c3703be16 100644
--- a/db/seeds.d/110-smart_proxy_features.rb
+++ b/db/seeds.d/110-smart_proxy_features.rb
@@ -1,6 +1,6 @@
# Proxy features
proxy_features = ["Templates", "TFTP", "DNS", "DHCP", "Puppet CA", "BMC", "Realm", "Facts", "Logs", "HTTPBoot", "External IPAM",
- "Registration"]
+ "Registration", "WOL"]
proxy_features.each do |input|
f = Feature.where(:name => input).first_or_create
diff --git a/test/controllers/api/v2/hosts_controller_test.rb b/test/controllers/api/v2/hosts_controller_test.rb
index fdcae6d8111..1cec127e39a 100644
--- a/test/controllers/api/v2/hosts_controller_test.rb
+++ b/test/controllers/api/v2/hosts_controller_test.rb
@@ -1427,4 +1427,127 @@ def setup
assert_equal json_response['facet_param'], 'bar'
end
end
+
+ context 'Wake-on-LAN (WOL) tests' do
+ setup do
+ @dhcp_proxy = FactoryBot.create(:dhcp_smart_proxy)
+ @dhcp_proxy.features << FactoryBot.create(:feature, :name => 'WOL')
+ @subnet = FactoryBot.create(:subnet_ipv4, :dhcp => @dhcp_proxy)
+ @host = FactoryBot.create(:host, :managed)
+ @primary_interface = FactoryBot.create(:nic_primary_and_provision,
+ :host => @host,
+ :subnet => @subnet,
+ :mac => '00:61:23:18:48:a5')
+ @host.interfaces = [@primary_interface]
+ @host.save!
+ User.current = users(:apiadmin)
+ end
+
+ test 'should send WOL request successfully' do
+ ProxyAPI::Wol.any_instance.expects(:wake).with('00:61:23:18:48:a5').returns(true)
+
+ put :wol, params: { :id => @host.to_param }
+
+ assert_response :success
+ response_body = JSON.parse(@response.body)
+ assert_equal true, response_body['wol']
+ end
+
+ test 'should return error when host has no primary interface' do
+ @host.interfaces = []
+ @host.save!
+
+ put :wol, params: { :id => @host.to_param }
+
+ assert_response :unprocessable_entity
+ response_body = JSON.parse(@response.body)
+ assert_match(/Host has no primary interface/, response_body['error']['message'])
+ end
+
+ test 'should return error when primary interface has no MAC address' do
+ @primary_interface.update_attribute(:mac, nil)
+
+ put :wol, params: { :id => @host.to_param }
+
+ assert_response :unprocessable_entity
+ response_body = JSON.parse(@response.body)
+ assert_match(/Primary interface has no MAC address/, response_body['error']['message'])
+ end
+
+ test 'should return error when primary interface has empty MAC address' do
+ @primary_interface.update_attribute(:mac, '')
+
+ put :wol, params: { :id => @host.to_param }
+
+ assert_response :unprocessable_entity
+ response_body = JSON.parse(@response.body)
+ assert_match(/Primary interface has no MAC address/, response_body['error']['message'])
+ end
+
+ test 'should return error when primary interface subnet has no DHCP proxy' do
+ @primary_interface.subnet.update_attribute(:dhcp, nil)
+
+ put :wol, params: { :id => @host.to_param }
+
+ assert_response :unprocessable_entity
+ response_body = JSON.parse(@response.body)
+ assert_match(/Primary interface subnet has no DHCP proxy configured/, response_body['error']['message'])
+ end
+
+ test 'should return error when DHCP proxy does not have WOL feature' do
+ @dhcp_proxy.features.delete_all
+
+ put :wol, params: { :id => @host.to_param }
+
+ assert_response :unprocessable_entity
+ response_body = JSON.parse(@response.body)
+ assert_match(/DHCP proxy does not have WOL feature enabled/, response_body['error']['message'])
+ end
+
+ test 'should handle ProxyException with proper error message' do
+ proxy_exception = ProxyException.new('http://proxy.example.com/wol',
+ StandardError.new('Network error'),
+ N_("Unable to send Wake on LAN request for MAC %s"),
+ '00:61:23:18:48:a5')
+ ProxyAPI::Wol.any_instance.expects(:wake).with('00:61:23:18:48:a5').raises(proxy_exception)
+
+ put :wol, params: { :id => @host.to_param }
+
+ assert_response :unprocessable_entity
+ response_body = JSON.parse(@response.body)
+ assert_match(/Unable to send Wake on LAN request/, response_body['error']['message'])
+ end
+
+ test 'should return 404 for non-existent host' do
+ put :wol, params: { :id => 'non-existent-host' }
+
+ assert_response :not_found
+ end
+
+ context 'permissions' do
+ setup do
+ setup_user 'view', 'hosts'
+ setup_user 'power', 'hosts'
+ end
+
+ test 'should allow user with power permission to send WOL request' do
+ ProxyAPI::Wol.any_instance.expects(:wake).with('00:61:23:18:48:a5').returns(true)
+
+ put :wol, params: { :id => @host.to_param }, session: set_session_user.merge(:user => @one.id)
+
+ assert_response :success
+ response_body = JSON.parse(@response.body)
+ assert_equal true, response_body['wol']
+ end
+
+ test 'should deny user without power permission' do
+ setup_user 'view', 'hosts'
+ # Don't add power permission
+
+ put :wol, params: { :id => @host.to_param }, session: set_session_user.merge(:user => @one.id)
+
+ assert_response :forbidden
+ end
+ end
+ end
end
diff --git a/test/unit/proxy_api/wol_test.rb b/test/unit/proxy_api/wol_test.rb
new file mode 100644
index 00000000000..64ca801381b
--- /dev/null
+++ b/test/unit/proxy_api/wol_test.rb
@@ -0,0 +1,69 @@
+require 'test_helper'
+
+class ProxyApiWolTest < ActiveSupport::TestCase
+ def setup
+ @url = "http://dummyproxy.theforeman.org:8443"
+ @wol_proxy = ProxyAPI::Wol.new({:url => @url})
+ end
+
+ test "base url should equal /wol" do
+ expected = @url + "/wol"
+ assert_equal(expected, @wol_proxy.url)
+ end
+
+ test "wake should raise exception when MAC address is nil" do
+ exception = assert_raise ProxyAPI::ProxyException do
+ @wol_proxy.wake(nil)
+ end
+ assert_includes(exception.message, "Must define a MAC address")
+ end
+
+ test "wake should handle proxy communication errors" do
+ mac = "00:11:22:33:44:55"
+ error = StandardError.new("Connection timeout")
+
+ @wol_proxy.stubs(:post).raises(error)
+
+ exception = assert_raise ProxyAPI::ProxyException do
+ @wol_proxy.wake(mac)
+ end
+
+ assert_match(/Unable to send Wake on LAN request for MAC/, exception.message)
+ assert_match(/#{mac}/, exception.message)
+ end
+
+ test "wake should handle JSON parsing errors" do
+ mac = "00:11:22:33:44:55"
+ expected_params = { :mac_address => mac }
+
+ @wol_proxy.stubs(:post).with(expected_params).returns(fake_rest_client_response("invalid json"))
+ @wol_proxy.stubs(:parse).raises(JSON::ParserError.new("Invalid JSON"))
+
+ exception = assert_raise ProxyAPI::ProxyException do
+ @wol_proxy.wake(mac)
+ end
+
+ assert_match(/Unable to send Wake on LAN request for MAC/, exception.message)
+ assert_match(/#{mac}/, exception.message)
+ end
+
+ test "wake should return parsed response on success" do
+ mac = "00:11:22:33:44:55"
+ expected_params = { :mac_address => mac }
+ response_data = { "status" => "success", "result" => true }
+
+ @wol_proxy.stubs(:post).with(expected_params).returns(fake_rest_client_response(response_data))
+ @wol_proxy.stubs(:parse).returns(response_data)
+
+ result = @wol_proxy.wake(mac)
+ assert_equal(response_data, result)
+ end
+
+ private
+
+ def fake_rest_client_response(data)
+ response = mock('RestClient::Response')
+ response.stubs(:body).returns(data.to_json)
+ response
+ end
+end
diff --git a/test/unit/shared/access_permissions_test_base.rb b/test/unit/shared/access_permissions_test_base.rb
index d4cfd0ecbbd..56e142bd805 100644
--- a/test/unit/shared/access_permissions_test_base.rb
+++ b/test/unit/shared/access_permissions_test_base.rb
@@ -27,7 +27,7 @@ def check_routes(app_routes, skipped_actions, skip_patterns: [/^(api\/v2\/)?(dum
# Pass if the controller deliberately skips login requirement
next if controller < ApplicationController && filters.select { |f| f.filter == :require_login }.empty?
- assert_not_empty Foreman::AccessControl.permissions.select { |p| p.actions.include? path }, "permission for #{path} not found, check access_permissions.rb"
+ assert_not_empty Foreman::AccessControl.permissions.select { |p| p.actions.include? path }, "permission for #{path} not found, check config/initializers/f_foreman_permissions.rb.rb"
end
end
end
diff --git a/webpack/assets/javascripts/react_app/components/HostDetails/ActionsBar/__tests__/ActionsBar.fixtures.js b/webpack/assets/javascripts/react_app/components/HostDetails/ActionsBar/__tests__/ActionsBar.fixtures.js
new file mode 100644
index 00000000000..9ae58873ca9
--- /dev/null
+++ b/webpack/assets/javascripts/react_app/components/HostDetails/ActionsBar/__tests__/ActionsBar.fixtures.js
@@ -0,0 +1,43 @@
+export const mockHost = {
+ hostId: 1,
+ hostFriendlyId: 'test-host.example.com',
+ hostName: 'test-host.example.com',
+ computeId: 123,
+ isBuild: false,
+};
+
+export const basePermissions = {
+ destroy_hosts: true,
+ create_hosts: true,
+ edit_hosts: true,
+ build_hosts: true,
+ power_hosts: true,
+};
+
+export const noPermissions = {
+ destroy_hosts: false,
+ create_hosts: false,
+ edit_hosts: false,
+ build_hosts: false,
+ power_hosts: false,
+};
+
+export const noPowerPermissions = {
+ ...basePermissions,
+ power_hosts: false,
+};
+
+export const baseProps = {
+ ...mockHost,
+ permissions: basePermissions,
+};
+
+export const noPowerProps = {
+ ...mockHost,
+ permissions: noPowerPermissions,
+};
+
+export const noPermissionProps = {
+ ...mockHost,
+ permissions: noPermissions,
+};
diff --git a/webpack/assets/javascripts/react_app/components/HostDetails/ActionsBar/__tests__/ActionsBar.test.js b/webpack/assets/javascripts/react_app/components/HostDetails/ActionsBar/__tests__/ActionsBar.test.js
new file mode 100644
index 00000000000..2c03f0727f0
--- /dev/null
+++ b/webpack/assets/javascripts/react_app/components/HostDetails/ActionsBar/__tests__/ActionsBar.test.js
@@ -0,0 +1,40 @@
+import { shallowRenderComponentWithFixtures } from '../../../../common/testHelpers';
+import ActionsBar from '../index';
+import {
+ baseProps,
+ noPowerProps,
+ noPermissionProps
+} from './ActionsBar.fixtures';
+
+// Mock the required external dependencies
+jest.mock('../../../../../foreman_navigation', () => ({
+ visit: jest.fn(),
+}));
+
+jest.mock('../../../../Root/Context/ForemanContext', () => ({
+ useForemanSettings: () => ({ destroyVmOnHostDelete: false }),
+ useForemanHostsPageUrl: () => '/hosts',
+}));
+
+jest.mock('react-redux', () => ({
+ useSelector: jest.fn(() => []),
+ useDispatch: () => jest.fn(),
+ connect: jest.fn(() => (component) => component),
+}));
+
+const fixtures = {
+ 'renders ActionsBar with all permissions (including power)': baseProps,
+ 'renders ActionsBar without power permissions': noPowerProps,
+ 'renders ActionsBar without any permissions': noPermissionProps,
+};
+
+describe('ActionsBar', () => {
+ describe('rendering', () => {
+ const components = shallowRenderComponentWithFixtures(ActionsBar, fixtures);
+ components.forEach(({ description, component }) => {
+ it(description, () => {
+ expect(component).toMatchSnapshot();
+ });
+ });
+ });
+});
diff --git a/webpack/assets/javascripts/react_app/components/HostDetails/ActionsBar/__tests__/__snapshots__/ActionsBar.test.js.snap b/webpack/assets/javascripts/react_app/components/HostDetails/ActionsBar/__tests__/__snapshots__/ActionsBar.test.js.snap
new file mode 100644
index 00000000000..4096f222345
--- /dev/null
+++ b/webpack/assets/javascripts/react_app/components/HostDetails/ActionsBar/__tests__/__snapshots__/ActionsBar.test.js.snap
@@ -0,0 +1,373 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`ActionsBar rendering renders ActionsBar with all permissions (including power) 1`] = `
+
+
+
+
+
+
+
+
+ }
+ isDisabled={false}
+ onClick={[Function]}
+ ouiaId="build-dropdown-item"
+ >
+ Build
+ ,
+ }
+ isDisabled={false}
+ onClick={[Function]}
+ ouiaId="clone-dropdown-item"
+ >
+ Clone
+ ,
+ }
+ isDisabled={false}
+ onClick={[Function]}
+ ouiaId="delete-dropdown-item"
+ >
+ Delete
+ ,
+ ,
+ }
+ isAriaDisabled={false}
+ onClick={[Function]}
+ ouiaId="console-dropdown-item"
+ >
+ Console
+ ,
+ }
+ onClick={[Function]}
+ ouiaId="fact-dropdown-item"
+ >
+ Facts
+ ,
+ }
+ isDisabled={false}
+ onClick={[Function]}
+ ouiaId="wol-dropdown-item"
+ >
+ Wake on LAN
+ ,
+ ,
+ }
+ ouiaId="pre-version-dropdown-item"
+ >
+ Legacy UI
+ ,
+ ]
+ }
+ isOpen={false}
+ isPlain={true}
+ ouiaId="kebab-dropdown"
+ toggle={
+
+ }
+ />
+
+
+
+
+`;
+
+exports[`ActionsBar rendering renders ActionsBar without any permissions 1`] = `
+
+
+
+
+
+
+
+
+ }
+ isDisabled={true}
+ onClick={[Function]}
+ ouiaId="build-dropdown-item"
+ >
+ Build
+ ,
+ }
+ isDisabled={true}
+ onClick={[Function]}
+ ouiaId="clone-dropdown-item"
+ >
+ Clone
+ ,
+ }
+ isDisabled={true}
+ onClick={[Function]}
+ ouiaId="delete-dropdown-item"
+ >
+ Delete
+ ,
+ ,
+ }
+ isAriaDisabled={false}
+ onClick={[Function]}
+ ouiaId="console-dropdown-item"
+ >
+ Console
+ ,
+ }
+ onClick={[Function]}
+ ouiaId="fact-dropdown-item"
+ >
+ Facts
+ ,
+ }
+ isDisabled={true}
+ onClick={[Function]}
+ ouiaId="wol-dropdown-item"
+ >
+ Wake on LAN
+ ,
+ ,
+ }
+ ouiaId="pre-version-dropdown-item"
+ >
+ Legacy UI
+ ,
+ ]
+ }
+ isOpen={false}
+ isPlain={true}
+ ouiaId="kebab-dropdown"
+ toggle={
+
+ }
+ />
+
+
+
+
+`;
+
+exports[`ActionsBar rendering renders ActionsBar without power permissions 1`] = `
+
+
+
+
+
+
+
+
+ }
+ isDisabled={false}
+ onClick={[Function]}
+ ouiaId="build-dropdown-item"
+ >
+ Build
+ ,
+ }
+ isDisabled={false}
+ onClick={[Function]}
+ ouiaId="clone-dropdown-item"
+ >
+ Clone
+ ,
+ }
+ isDisabled={false}
+ onClick={[Function]}
+ ouiaId="delete-dropdown-item"
+ >
+ Delete
+ ,
+ ,
+ }
+ isAriaDisabled={false}
+ onClick={[Function]}
+ ouiaId="console-dropdown-item"
+ >
+ Console
+ ,
+ }
+ onClick={[Function]}
+ ouiaId="fact-dropdown-item"
+ >
+ Facts
+ ,
+ }
+ isDisabled={true}
+ onClick={[Function]}
+ ouiaId="wol-dropdown-item"
+ >
+ Wake on LAN
+ ,
+ ,
+ }
+ ouiaId="pre-version-dropdown-item"
+ >
+ Legacy UI
+ ,
+ ]
+ }
+ isOpen={false}
+ isPlain={true}
+ ouiaId="kebab-dropdown"
+ toggle={
+
+ }
+ />
+
+
+
+
+`;
diff --git a/webpack/assets/javascripts/react_app/components/HostDetails/ActionsBar/__tests__/__snapshots__/actions.test.js.snap b/webpack/assets/javascripts/react_app/components/HostDetails/ActionsBar/__tests__/__snapshots__/actions.test.js.snap
new file mode 100644
index 00000000000..db6f6ea0427
--- /dev/null
+++ b/webpack/assets/javascripts/react_app/components/HostDetails/ActionsBar/__tests__/__snapshots__/actions.test.js.snap
@@ -0,0 +1,33 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`ActionsBar actions wakeOnLan should call Wake on LAN API action 1`] = `
+Array [
+ Array [
+ Object {
+ "payload": Object {
+ "errorToast": [Function],
+ "key": "1_WOL",
+ "successToast": [Function],
+ "url": "/api/hosts/1/wol",
+ },
+ "type": "API_PUT",
+ },
+ ],
+]
+`;
+
+exports[`ActionsBar actions wakeOnLan should call Wake on LAN API action with different host 1`] = `
+Array [
+ Array [
+ Object {
+ "payload": Object {
+ "errorToast": [Function],
+ "key": "42_WOL",
+ "successToast": [Function],
+ "url": "/api/hosts/42/wol",
+ },
+ "type": "API_PUT",
+ },
+ ],
+]
+`;
diff --git a/webpack/assets/javascripts/react_app/components/HostDetails/ActionsBar/__tests__/actions.test.js b/webpack/assets/javascripts/react_app/components/HostDetails/ActionsBar/__tests__/actions.test.js
new file mode 100644
index 00000000000..2e8c93ef625
--- /dev/null
+++ b/webpack/assets/javascripts/react_app/components/HostDetails/ActionsBar/__tests__/actions.test.js
@@ -0,0 +1,13 @@
+import { testActionSnapshotWithFixtures } from '../../../../common/testHelpers';
+import { wakeOnLan } from '../actions';
+
+const fixtures = {
+ 'should call Wake on LAN API action': () => wakeOnLan(1, 'test-host.example.com'),
+ 'should call Wake on LAN API action with different host': () => wakeOnLan(42, 'another-host.example.com'),
+};
+
+describe('ActionsBar actions', () => {
+ describe('wakeOnLan', () => {
+ testActionSnapshotWithFixtures(fixtures);
+ });
+});
diff --git a/webpack/assets/javascripts/react_app/components/HostDetails/ActionsBar/actions.js b/webpack/assets/javascripts/react_app/components/HostDetails/ActionsBar/actions.js
index c362380c2d3..135e42af681 100644
--- a/webpack/assets/javascripts/react_app/components/HostDetails/ActionsBar/actions.js
+++ b/webpack/assets/javascripts/react_app/components/HostDetails/ActionsBar/actions.js
@@ -110,3 +110,19 @@ export const isHostTurnOn = store => {
const { state } = selectAPIResponse(store, POWER_REQURST_KEY);
return state === 'on';
};
+
+export const wakeOnLan = (hostId, hostName) => dispatch => {
+ const successToast = () => __('Wake on LAN signal has been sent');
+ const errorToast = ({ message }) =>
+ message || __('Sending Wake on LAN signal has failed');
+ const url = foremanUrl(`/api/hosts/${hostId}/wol`);
+
+ dispatch(
+ APIActions.put({
+ url,
+ key: `${hostId}_WOL`,
+ successToast,
+ errorToast,
+ })
+ );
+};
diff --git a/webpack/assets/javascripts/react_app/components/HostDetails/ActionsBar/index.js b/webpack/assets/javascripts/react_app/components/HostDetails/ActionsBar/index.js
index c3b4622d4f6..2d020e00f57 100644
--- a/webpack/assets/javascripts/react_app/components/HostDetails/ActionsBar/index.js
+++ b/webpack/assets/javascripts/react_app/components/HostDetails/ActionsBar/index.js
@@ -15,12 +15,13 @@ import {
UndoIcon,
BuildIcon,
TerminalIcon,
+ PlayIcon,
} from '@patternfly/react-icons';
import { visit } from '../../../../foreman_navigation';
import { translate as __ } from '../../../common/I18n';
import { selectKebabItems } from './Selectors';
import { foremanUrl } from '../../../common/helpers';
-import { cancelBuild, deleteHost, isHostTurnOn } from './actions';
+import { cancelBuild, deleteHost, isHostTurnOn, wakeOnLan } from './actions';
import {
useForemanSettings,
useForemanHostsPageUrl,
@@ -46,6 +47,7 @@ const ActionsBar = ({
create_hosts: canCreate,
edit_hosts: canEdit,
build_hosts: canBuild,
+ power_hosts: canPower,
},
}) => {
const [kebabIsOpen, setKebab] = useState(false);
@@ -62,6 +64,11 @@ const ActionsBar = ({
deleteHost(hostName, computeId, destroyVmOnHostDelete, hostsIndexUrl)
);
+ const wakeOnLanHandler = () => {
+ dispatch(wakeOnLan(hostId, hostName));
+ setKebab(false);
+ };
+
const isConsoleDisabled = !(computeId && isHostActive);
const determineTooltip = () => {
if (isConsoleDisabled) {
@@ -132,6 +139,16 @@ const ActionsBar = ({
>
{__('Facts')}
,
+ }
+ >
+ {__('Wake on LAN')}
+ ,
,