Skip to content

Commit 8f39a38

Browse files
committed
Fixes #39067 - rebuild only if host powered off
1 parent e8ff59d commit 8f39a38

File tree

5 files changed

+103
-2
lines changed

5 files changed

+103
-2
lines changed

app/models/host/managed.rb

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -333,6 +333,7 @@ class Jail < ::Safemode::Jail
333333
before_validation :set_compute_attributes, :on => :create, :if => proc { compute_attributes_empty? }
334334
validate :check_if_provision_method_changed, :on => :update, :if => proc { |host| host.managed }
335335
validates :uuid, uniqueness: { :allow_blank => true }
336+
validate :check_if_rebuild_allowed, :if => proc { |host| host.build_changed?(from: false, to: true) }
336337

337338
before_validation :set_hostgroup_defaults, :set_ip_address
338339
after_validation :ensure_associations
@@ -500,6 +501,23 @@ def self.registered_provision_methods
500501
Foreman::Plugin.all.map(&:provision_methods).inject(:merge) || {}
501502
end
502503

504+
def check_if_rebuild_allowed
505+
return true unless rebuild_requires_poweroff && power.ready?
506+
507+
errors.add(:build, N_('The host must be powered off to build.'))
508+
false
509+
rescue Foreman::Exception => _e
510+
logger.info "Could not read power state of #{name}. Allowing to build the host."
511+
true
512+
end
513+
514+
def rebuild_requires_poweroff
515+
method_name = "#{provision_method}_rebuild_requires_poweroff"
516+
return false unless respond_to?(method_name)
517+
518+
public_send(method_name)
519+
end
520+
503521
def self.valid_rebuild_only_values
504522
if Host::Managed.respond_to?(:rebuild_methods)
505523
Nic::Managed.rebuild_methods.values + Host::Managed.rebuild_methods.values

app/views/api/v2/hosts/main.json.rabl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ attributes :ip, :ip6, :last_report, :mac, :realm_id, :realm_name,
1313
:sp_mac, :sp_ip, :sp_name, :domain_id, :domain_name, :architecture_id, :architecture_name, :operatingsystem_id, :operatingsystem_name,
1414
:subnet_id, :subnet_name, :subnet6_id, :subnet6_name, :sp_subnet_id, :ptable_id, :ptable_name, :medium_id, :medium_name, :pxe_loader,
1515
:build, :comment, :disk, :initiated_at, :installed_at, :model_id, :hostgroup_id, :owner_id, :owner_name, :owner_type, :creator_id, :creator,
16-
:enabled, :managed, :use_image, :image_file, :uuid,
16+
:enabled, :managed, :use_image, :image_file, :uuid, :rebuild_requires_poweroff,
1717
:compute_resource_id, :compute_resource_name,
1818
:compute_profile_id, :compute_profile_name, :capabilities, :provision_method,
1919
:certname, :image_id, :image_name, :created_at, :updated_at,

test/models/host_test.rb

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -549,6 +549,62 @@ class HostTest < ActiveSupport::TestCase
549549
refute_equal original_status, new_status
550550
end
551551

552+
test "rebuild_requires_poweroff is validated and prevents to re-build a running host" do
553+
power = mock("power")
554+
power.stubs(:ready?).returns(true)
555+
556+
host = FactoryBot.create(:host, :managed)
557+
host.stubs(:power).returns(power)
558+
host.save!
559+
560+
host.expects(:build_rebuild_requires_poweroff).returns(true)
561+
host.build = true
562+
refute host.valid?
563+
assert host.errors[:build].include?("The host must be powered off to build.")
564+
end
565+
566+
test "rebuild_requires_poweroff is validated and allows to re-build the turned-off host" do
567+
power = mock("power")
568+
power.stubs(:ready?).returns(false)
569+
570+
host = FactoryBot.create(:host, :managed)
571+
host.stubs(:power).returns(power)
572+
host.save!
573+
574+
host.expects(:build_rebuild_requires_poweroff).returns(true)
575+
host.build = true
576+
assert host.valid?
577+
refute host.errors[:build].any?
578+
end
579+
580+
test "rebuild_requires_poweroff is validated and allows to re-build the host because provision_method allows it" do
581+
power = mock("power")
582+
power.stubs(:ready?).returns(true)
583+
584+
host = FactoryBot.create(:host, :managed)
585+
host.stubs(:power).returns(power)
586+
host.save!
587+
588+
host.expects(:build_rebuild_requires_poweroff).returns(false)
589+
host.build = true
590+
assert host.valid?
591+
refute host.errors[:build].any?
592+
end
593+
594+
test "rebuild_requires_poweroff allows rebuild when power state check fails" do
595+
power = mock("power")
596+
power.stubs(:ready?).raises(Foreman::Exception.new("boom"))
597+
598+
host = FactoryBot.create(:host, :managed)
599+
host.stubs(:power).returns(power)
600+
host.save!
601+
602+
host.expects(:build_rebuild_requires_poweroff).returns(true)
603+
host.build = true
604+
assert host.valid?
605+
refute host.errors[:build].any?
606+
end
607+
552608
context 'host assigned to location and organization' do
553609
setup do
554610
@host = FactoryBot.create(:host, :managed => false)

webpack/assets/javascripts/react_app/components/HostDetails/ActionsBar/index.js

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ const ActionsBar = ({
4545
hostName,
4646
computeId,
4747
isBuild,
48+
buildRequiresPowerOff,
4849
permissions: {
4950
destroy_hosts: canDestroy,
5051
create_hosts: canCreate,
@@ -67,6 +68,8 @@ const ActionsBar = ({
6768
);
6869

6970
const isConsoleDisabled = !(computeId && isHostActive);
71+
const isHostActiveButRequiresPowerOff =
72+
buildRequiresPowerOff && isHostActive;
7073
const determineTooltip = () => {
7174
if (isConsoleDisabled) {
7275
if (computeId) {
@@ -76,6 +79,26 @@ const ActionsBar = ({
7679
}
7780
return undefined;
7881
};
82+
const determineBuildDescription = () => {
83+
if (!isBuild) {
84+
if (!canBuild) {
85+
return __("No permission to start build for this host.");
86+
}
87+
if (isHostActiveButRequiresPowerOff) {
88+
return __('The host must be powered off to build.');
89+
}
90+
return __('Start build for this host.');
91+
} else {
92+
if (!canBuild) {
93+
return __("No permission to cancel the build.");
94+
}
95+
return __('Cancel the current build.');
96+
}
97+
};
98+
// Build action is disabled if user doesn't have permissions to build or
99+
// if the host is active but requires power off before build
100+
const isBuildDisabled =
101+
!canBuild || (!isBuild && isHostActiveButRequiresPowerOff);
79102
const buildHandler = () => {
80103
if (isBuild) {
81104
dispatch(cancelBuild(hostId, hostName));
@@ -90,7 +113,8 @@ const ActionsBar = ({
90113
onClick={buildHandler}
91114
key="build"
92115
component="button"
93-
isDisabled={!canBuild}
116+
description={determineBuildDescription()}
117+
isDisabled={isBuildDisabled}
94118
icon={
95119
<Icon>
96120
<BuildIcon />
@@ -232,6 +256,7 @@ ActionsBar.propTypes = {
232256
computeId: PropTypes.number,
233257
permissions: PropTypes.object,
234258
isBuild: PropTypes.bool,
259+
buildRequiresPowerOff: PropTypes.bool,
235260
};
236261
ActionsBar.defaultProps = {
237262
hostId: undefined,
@@ -245,6 +270,7 @@ ActionsBar.defaultProps = {
245270
build_hosts: false,
246271
},
247272
isBuild: false,
273+
buildRequiresPowerOff: false,
248274
};
249275

250276
export default ActionsBar;

webpack/assets/javascripts/react_app/components/HostDetails/index.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -216,6 +216,7 @@ const HostDetails = ({
216216
hostName={response.name}
217217
permissions={response.permissions}
218218
isBuild={response.build}
219+
buildRequiresPowerOff={response.rebuild_requires_poweroff}
219220
/>
220221
</FlexItem>
221222
</Flex>

0 commit comments

Comments
 (0)