diff --git a/crates/api/src/lib.rs b/crates/api/src/lib.rs index fd96e69ad..36b587655 100644 --- a/crates/api/src/lib.rs +++ b/crates/api/src/lib.rs @@ -353,14 +353,14 @@ pub struct ZoneRemoveResult { #[derive(Deserialize, Serialize, Debug, Clone)] pub enum ZoneRemoveError { NotFound, - MidRestoration, + NotInMaintenanceMode, } impl fmt::Display for ZoneRemoveError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.write_str(match self { Self::NotFound => "no such zone was found", - Self::MidRestoration => "the zone is being restored from disk", + Self::NotInMaintenanceMode => "the zone is not in maintenance mode", }) } } diff --git a/crates/cli/src/commands/zone.rs b/crates/cli/src/commands/zone.rs index 42f136460..1f181e5b4 100644 --- a/crates/cli/src/commands/zone.rs +++ b/crates/cli/src/commands/zone.rs @@ -57,6 +57,8 @@ pub enum ZoneCommand { }, /// Remove a zone + /// + /// The zone must be in maintenance mode. #[command(name = "remove")] Remove { name: ZoneName }, diff --git a/doc/manual/build/man/cascade-zone.1 b/doc/manual/build/man/cascade-zone.1 index ae9e644f0..430ea3e39 100644 --- a/doc/manual/build/man/cascade-zone.1 +++ b/doc/manual/build/man/cascade-zone.1 @@ -54,7 +54,7 @@ cascade-zone \- Manage zones .sp \fBcascade\fP \fB[GLOBAL OPTIONS]\fP zone \fI\%history\fP \fB\fP .sp -\fBcascade\fP \fB[GLOBAL OPTIONS]\fP zone \fBmaintenance\fP \fB\fP \fB\fP +\fBcascade\fP \fB[GLOBAL OPTIONS]\fP zone \fI\%maintenance\fP \fB\fP \fB\fP .SH DESCRIPTION .sp Manage Cascade\(aqs zones. @@ -73,6 +73,9 @@ Add a new zone. .B remove Remove a zone. .sp +Maintenance mode must be enabled (see \fBzone maintenance\fP). The +zone must be passive (with no ongoing operations) or in a hard\-halt state. +.sp \fBNOTE:\fP .INDENT 7.0 .INDENT 3.5 @@ -124,6 +127,25 @@ Reset the pipeline for a zone to get it out of a halted state. .B history Get the history of a single zone. .UNINDENT +.INDENT 0.0 +.TP +.B maintenance +Enable or disable maintenance mode for the zone. +.sp +In maintenance mode, Cascade will not act on the zone autonomously (e.g. to +load new zone data or refresh signatures). This quiet state is helpful for +debugging and changing zone configuration. +.sp +\fBNOTE:\fP +.INDENT 7.0 +.INDENT 3.5 +If maintenance mode is enabled while a new instance of the zone +is being built (i.e. loading, signing, review, etc. is ongoing), +it will not be canceled; maintenance mode will go into effect once +the operation completes. +.UNINDENT +.UNINDENT +.UNINDENT .SH OPTIONS FOR ZONE ADD .INDENT 0.0 .TP diff --git a/doc/manual/source/man/cascade-zone.rst b/doc/manual/source/man/cascade-zone.rst index 78adcc01f..e85629a1c 100644 --- a/doc/manual/source/man/cascade-zone.rst +++ b/doc/manual/source/man/cascade-zone.rst @@ -50,6 +50,9 @@ Commands Remove a zone. + Maintenance mode must be enabled (see :subcmd:`zone maintenance`). The + zone must be passive (with no ongoing operations) or in a hard-halt state. + .. note:: Once removed, downstream servers will no longer be able to fetch the zone! @@ -88,6 +91,19 @@ Commands Get the history of a single zone. +.. subcmd:: maintenance + + Enable or disable maintenance mode for the zone. + + In maintenance mode, Cascade will not act on the zone autonomously (e.g. to + load new zone data or refresh signatures). This quiet state is helpful for + debugging and changing zone configuration. + + .. note:: If maintenance mode is enabled while a new instance of the zone + is being built (i.e. loading, signing, review, etc. is ongoing), + it will not be canceled; maintenance mode will go into effect once + the operation completes. + Options for :subcmd:`zone add` ------------------------------ diff --git a/integration-tests/incremental-signing/scripts/tests-change-denial.sh b/integration-tests/incremental-signing/scripts/tests-change-denial.sh index e77e54b42..bcd43c41b 100755 --- a/integration-tests/incremental-signing/scripts/tests-change-denial.sh +++ b/integration-tests/incremental-signing/scripts/tests-change-denial.sh @@ -67,6 +67,7 @@ do $CASCADE policy reload $CASCADE zone reload example + $CASCADE zone maintanence enable example for i in 1 2 3 4 5 6 7 8 9 10 do dig @127.0.0.1 -p 8053 example soa | diff --git a/integration-tests/incremental-signing/scripts/tests.sh b/integration-tests/incremental-signing/scripts/tests.sh index 0c73a8892..cca90ab16 100755 --- a/integration-tests/incremental-signing/scripts/tests.sh +++ b/integration-tests/incremental-signing/scripts/tests.sh @@ -39,6 +39,7 @@ do } cp zones/incremental-signing-test${test}-input2.zone example.in $CASCADE zone reload example + $CASCADE zone maintenance enable example for i in 1 2 3 4 5 6 7 8 9 10 do dig @127.0.0.1 -p 8053 example soa | diff --git a/integration-tests/review-unsigned-zone2/scripts/tests.sh b/integration-tests/review-unsigned-zone2/scripts/tests.sh index 58726f4ac..4d9d2c8c7 100755 --- a/integration-tests/review-unsigned-zone2/scripts/tests.sh +++ b/integration-tests/review-unsigned-zone2/scripts/tests.sh @@ -9,21 +9,22 @@ do cp zones/test${test}.zone example.in $CASCADE zone add --source $PWD/example.in --policy review-test example --import-csk-file $KEY + $CASCADE zone maintenance enable example + serial="$test$test$test$test$test" # Wait for the zone to be signed. for i in 1 2 3 4 5 6 7 8 9 10 do - dig @127.0.0.1 -p 8053 example soa | - grep $serial && break - echo zone is not signed yet, sleeping - sleep 1 + dig @127.0.0.1 -p 8053 example soa | + grep $serial && break + echo zone is not signed yet, sleeping + sleep 1 done dig @127.0.0.1 -p 8053 example soa | - grep $serial || - { - echo zone is not signed yet, giving up - exit 1 + grep $serial || { + echo zone is not signed yet, giving up + exit 1 } $CASCADE zone remove example done diff --git a/integration-tests/tests/persist-zone/action.yml b/integration-tests/tests/persist-zone/action.yml index 18e63344f..9b2fd1aa2 100644 --- a/integration-tests/tests/persist-zone/action.yml +++ b/integration-tests/tests/persist-zone/action.yml @@ -101,6 +101,7 @@ runs: - name: Delete the zone run: | + cascade zone maintenance enable i.dont.exist cascade zone remove i.dont.exist # ------------------------------------------------------------------------ diff --git a/integration-tests/tests/remove-zone/action.yml b/integration-tests/tests/remove-zone/action.yml index 734f9a462..4b6069686 100644 --- a/integration-tests/tests/remove-zone/action.yml +++ b/integration-tests/tests/remove-zone/action.yml @@ -45,6 +45,9 @@ runs: sleep 1 done + - name: Enable maintenance mode + run: cascade zone maintenance enable example.test + - name: Check zone status run: | timeout=10 # seconds diff --git a/integration-tests/tests/upstream-tsig/action.yml b/integration-tests/tests/upstream-tsig/action.yml index 2e16a74a5..96d1ca92d 100644 --- a/integration-tests/tests/upstream-tsig/action.yml +++ b/integration-tests/tests/upstream-tsig/action.yml @@ -37,6 +37,7 @@ runs: - name: Add zone without the required TSIG key. run: | cascade zone add --policy default --source "127.0.0.1:1055" example-tsig.test + cascade zone maintenance enable example-tsig.test - name: Check zone status run: | @@ -71,6 +72,7 @@ runs: esac cascade zone add --policy default --source "127.0.0.1:1055^tsig-key" example-tsig.test + cascade zone maintenance enable example-tsig.test - name: Check zone status run: | diff --git a/src/center.rs b/src/center.rs index 7909445f6..cae2dd5df 100644 --- a/src/center.rs +++ b/src/center.rs @@ -264,9 +264,13 @@ pub fn remove_zone(center: &Arc
, name: Name) -> Result<(), ZoneRe let ZoneByName(zone) = state.zones.get(&name).ok_or(ZoneRemoveError::NotFound)?; - // TODO(#871): support removing a zone during restoration. - if zone.read().storage.is_restoring() { - return Err(ZoneRemoveError::MidRestoration); + { + let zone = zone.read(); + // The zone must be in maintenance mode, and passive/halted. + // TODO(#871): support removing a zone during restoration. + if !zone.maintenance_mode || !zone.machine.is_waiting() && !zone.machine.is_halted() { + return Err(ZoneRemoveError::NotInMaintenanceMode); + } } let ZoneByName(zone) = state @@ -518,8 +522,8 @@ pub enum ZoneRemoveError { /// No such name could be found. NotFound, - /// The zone is being restored from disk. - MidRestoration, + /// The zone is not in maintenance mode. + NotInMaintenanceMode, } impl std::error::Error for ZoneRemoveError {} @@ -528,7 +532,7 @@ impl fmt::Display for ZoneRemoveError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.write_str(match self { Self::NotFound => "no such zone was found", - Self::MidRestoration => "the zone is being restored from disk", + Self::NotInMaintenanceMode => "the zone is not in maintenance mode", }) } } @@ -537,7 +541,7 @@ impl From for api::ZoneRemoveError { fn from(value: ZoneRemoveError) -> Self { match value { ZoneRemoveError::NotFound => Self::NotFound, - ZoneRemoveError::MidRestoration => Self::MidRestoration, + ZoneRemoveError::NotInMaintenanceMode => Self::NotInMaintenanceMode, } } } diff --git a/src/zone/machine.rs b/src/zone/machine.rs index d4e1f8698..ea1cb5d16 100644 --- a/src/zone/machine.rs +++ b/src/zone/machine.rs @@ -86,6 +86,10 @@ pub enum ZoneStateMachine { } impl ZoneStateMachine { + pub fn is_waiting(&self) -> bool { + matches!(self, Self::Waiting(_)) + } + pub fn is_halted(&self) -> bool { matches!( self,