Skip to content

Commit a62fc86

Browse files
authored
[MGS] Reject SP reset requests to our own sled (#8830)
Fixes #8768 by implementing "option 1" from that issue's proposed solutions: * Extends MGS's config file slightly to specify which _sled_ it's running on (once it determines which scrimlet position it's in). * Adds a config option to disallow SP reset requests to the local scrimlet. * Adds a custom dropshot error type for `sp_component_reset()` that can cleanly report this particular kind of failure. This is definitely the first custom dropshot error in MGS (although we need some others: #8013, #8014, #8350), and maybe the first in omicron more generally? The server side is pretty straightforward, I think, but the client side ended up being a little messy. I'm not sure how much of that is because of the rework I had to do to `try_all_serially()` vs the inherent changes (e.g., the fact that we have a different progenitor error type for this one endpoint). I'll definitely put this on a racklette before merging, but this should be close enough to review, and I'm very interested in feedback about integrating the custom error type.
1 parent fc12607 commit a62fc86

23 files changed

Lines changed: 549 additions & 129 deletions

File tree

Cargo.lock

Lines changed: 3 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

gateway-api/src/lib.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ use gateway_types::{
2323
task_dump::TaskDump,
2424
update::{
2525
HostPhase2Progress, HostPhase2RecoveryImageId, InstallinatorImageId,
26-
SpUpdateStatus,
26+
SpComponentResetError, SpUpdateStatus,
2727
},
2828
};
2929
use schemars::JsonSchema;
@@ -219,7 +219,7 @@ pub trait GatewayApi {
219219
async fn sp_component_reset(
220220
rqctx: RequestContext<Self::Context>,
221221
path: Path<PathSpComponent>,
222-
) -> Result<HttpResponseUpdatedNoContent, HttpError>;
222+
) -> Result<HttpResponseUpdatedNoContent, SpComponentResetError>;
223223

224224
/// Update an SP component
225225
///

gateway-test-utils/configs/config.test.toml

Lines changed: 27 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -33,11 +33,32 @@ max_attempts_reset = 10
3333
# sleep time between UDP RPC resends (up to `max_attempts_*`)
3434
per_attempt_timeout_millis = 1000
3535

36-
[switch.location]
37-
# possible locations where MGS could be running; these names appear in logs and
38-
# are used in the remainder of the `[switch.*]` configuration to define port
39-
# mappings
40-
names = ["switch0", "switch1"]
36+
# Possible locations where MGS could be running.
37+
#
38+
# The `name` of each location description will appear in logs and in the
39+
# remainder of the `[switch.*]` configuration to define port mappings.
40+
#
41+
# The `local_sled` of each location description specifies the `slot` of the
42+
# `SpIdentifier` for the sled on which this MGS is running. (The
43+
# `SpIdentifier::typ` value is implicitly `SpType::Sled`.)
44+
#
45+
# `allow_local_sled_sp_reset` determines whether MGS will accept a request to
46+
# perform an SP reset on its own local sled. This is dangerous during SP
47+
# updates, because the "reset" operation involves a watchdog that requires MGS
48+
# to send a "disarm the watchdog" message _after_ the reset, which it can't do
49+
# if it just powered itself off. In production, we set this to `false` for both
50+
# sleds; this means a scrimlet's SP can only be reset via MGS on the _other_
51+
# scrimlet. (We allow this to be `true` in tests and other dev environments
52+
# where rejecting reset attempts is too restrictive.)
53+
[[switch.location.description]]
54+
name = "switch0"
55+
local_sled = 0
56+
allow_local_sled_sp_reset = true
57+
58+
[[switch.location.description]]
59+
name = "switch1"
60+
local_sled = 1
61+
allow_local_sled_sp_reset = true
4162

4263
# `[[switch.location.determination]]` is a list of switch ports we should
4364
# contact in order to determine our location; each port defines a subset of
@@ -57,7 +78,7 @@ sp_port_2 = ["switch1"]
5778
# `[[switch.port.*]]` defines the local data link address (in RFD 250 terms, the
5879
# interface configured to use VLAN tag assigned to the given port) and the
5980
# logical ID of the remote SP ("sled 7", "switch 1", etc.), which must have an
60-
# entry for each member of `[switch.location]` above.
81+
# entry for each member of `[[switch.location.description]]` above.
6182
#
6283
# TODO This section has some concessions to local testing: ultimately we will
6384
# use a single multicast address, target port, and source port, but for now all

gateway-test-utils/src/setup.rs

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -127,19 +127,23 @@ fn expected_location(
127127
sp_port: SpPort,
128128
) -> String {
129129
let config = &config.switch.location;
130-
let mut locations = config.names.iter().cloned().collect::<HashSet<_>>();
130+
let mut locations = config
131+
.description
132+
.iter()
133+
.map(|d| d.name.as_str())
134+
.collect::<HashSet<_>>();
131135

132136
for determination in &config.determination {
133137
let refined = match sp_port {
134138
SpPort::One => &determination.sp_port_1,
135139
SpPort::Two => &determination.sp_port_2,
136140
};
137141

138-
locations.retain(|name| refined.contains(name));
142+
locations.retain(|name| refined.iter().any(|s| s == name));
139143
}
140144

141145
assert_eq!(locations.len(), 1);
142-
locations.into_iter().next().unwrap()
146+
locations.into_iter().next().unwrap().to_string()
143147
}
144148

145149
pub async fn test_setup_with_config(

gateway-types/Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ workspace = true
99

1010
[dependencies]
1111
daft.workspace = true
12+
dropshot.workspace = true
1213
gateway-messages.workspace = true
1314
# Avoid depending on gateway-sp-comms! It is a pretty heavy dependency and
1415
# would only be used for From impls anyway. We put those impls in
@@ -19,5 +20,6 @@ omicron-uuid-kinds.workspace = true
1920
omicron-workspace-hack.workspace = true
2021
schemars.workspace = true
2122
serde.workspace = true
23+
thiserror.workspace = true
2224
tufaceous-artifact.workspace = true
2325
uuid.workspace = true

gateway-types/src/update.rs

Lines changed: 50 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,63 @@
22
// License, v. 2.0. If a copy of the MPL was not distributed with this
33
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
44

5-
use std::time::Duration;
6-
5+
use dropshot::{ErrorStatusCode, HttpError, HttpResponseError};
76
use gateway_messages::UpdateStatus;
87
use omicron_uuid_kinds::MupdateUuid;
98
use schemars::JsonSchema;
109
use serde::{Deserialize, Serialize};
10+
use std::time::Duration;
1111
use tufaceous_artifact::ArtifactHash;
1212
use uuid::Uuid;
1313

14+
/// The error type returned by the `sp_component_reset()` MGS endpoint.
15+
#[derive(
16+
Debug, Clone, PartialEq, Eq, Serialize, JsonSchema, thiserror::Error,
17+
)]
18+
#[serde(tag = "state", rename_all = "snake_case")]
19+
pub enum SpComponentResetError {
20+
/// MGS refuses to reset its own sled's SP.
21+
#[error("cannot reset SP of the sled hosting MGS")]
22+
ResetSpOfLocalSled,
23+
24+
/// Other dropshot errors.
25+
#[error("{internal_message}")]
26+
Other {
27+
message: String,
28+
error_code: Option<String>,
29+
30+
// Skip serializing these fields, as they are used for the
31+
// `fmt::Display` implementation and for determining the status code,
32+
// respectively, rather than included in the response body:
33+
#[serde(skip)]
34+
internal_message: String,
35+
#[serde(skip)]
36+
status: ErrorStatusCode,
37+
},
38+
}
39+
40+
impl HttpResponseError for SpComponentResetError {
41+
fn status_code(&self) -> ErrorStatusCode {
42+
match self {
43+
SpComponentResetError::ResetSpOfLocalSled => {
44+
ErrorStatusCode::BAD_REQUEST
45+
}
46+
SpComponentResetError::Other { status, .. } => *status,
47+
}
48+
}
49+
}
50+
51+
impl From<HttpError> for SpComponentResetError {
52+
fn from(err: HttpError) -> Self {
53+
Self::Other {
54+
message: err.external_message,
55+
error_code: err.error_code,
56+
internal_message: err.internal_message,
57+
status: err.status_code,
58+
}
59+
}
60+
}
61+
1462
#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize, JsonSchema)]
1563
#[serde(tag = "state", rename_all = "snake_case")]
1664
pub enum SpUpdateStatus {

gateway/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ gateway-types.workspace = true
2323
hex.workspace = true
2424
http.workspace = true
2525
hyper.workspace = true
26+
iddqd.workspace = true
2627
illumos-utils.workspace = true
2728
ipcc.workspace = true
2829
omicron-common.workspace = true

gateway/examples/config.toml

Lines changed: 27 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -28,11 +28,32 @@ max_attempts_reset = 30
2828
# sleep time between UDP RPC resends (up to `max_attempts_*`)
2929
per_attempt_timeout_millis = 2000
3030

31-
[switch.location]
32-
# possible locations where MGS could be running; these names appear in logs and
33-
# are used in the remainder of the `[switch.*]` configuration to define port
34-
# mappings
35-
names = ["switch0", "switch1"]
31+
# Possible locations where MGS could be running.
32+
#
33+
# The `name` of each location description will appear in logs and in the
34+
# remainder of the `[switch.*]` configuration to define port mappings.
35+
#
36+
# The `local_sled` of each location description specifies the `slot` of the
37+
# `SpIdentifier` for the sled on which this MGS is running. (The
38+
# `SpIdentifier::typ` value is implicitly `SpType::Sled`.)
39+
#
40+
# `allow_local_sled_sp_reset` determines whether MGS will accept a request to
41+
# perform an SP reset on its own local sled. This is dangerous during SP
42+
# updates, because the "reset" operation involves a watchdog that requires MGS
43+
# to send a "disarm the watchdog" message _after_ the reset, which it can't do
44+
# if it just powered itself off. In production, we set this to `false` for both
45+
# sleds; this means a scrimlet's SP can only be reset via MGS on the _other_
46+
# scrimlet. (We allow this to be `true` in tests and other dev environments
47+
# where rejecting reset attempts is too restrictive.)
48+
[[switch.location.description]]
49+
name = "switch0"
50+
local_sled = 0
51+
allow_local_sled_sp_reset = true
52+
53+
[[switch.location.description]]
54+
name = "switch1"
55+
local_sled = 1
56+
allow_local_sled_sp_reset = true
3657

3758
# `[[switch.location.determination]]` is a list of switch ports we should
3859
# contact in order to determine our location; each port defines a subset of
@@ -52,7 +73,7 @@ sp_port_2 = ["switch1"]
5273
# `[[switch.port.*]]` defines the local data link address (in RFD 250 terms, the
5374
# interface configured to use VLAN tag assigned to the given port) and the
5475
# logical ID of the remote SP ("sled 7", "switch 1", etc.), which must have an
55-
# entry for each member of `[switch.location]` above.
76+
# entry for each member of `[[switch.location.description]]` above.
5677
[[switch.port]]
5778
kind = "simulated"
5879
fake-interface = "fake-switch0"

gateway/src/http_entrypoints.rs

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ use gateway_types::task_dump::TaskDump;
5050
use gateway_types::update::HostPhase2Progress;
5151
use gateway_types::update::HostPhase2RecoveryImageId;
5252
use gateway_types::update::InstallinatorImageId;
53+
use gateway_types::update::SpComponentResetError;
5354
use gateway_types::update::SpUpdateStatus;
5455
use omicron_uuid_kinds::GenericUuid;
5556
use std::io::Cursor;
@@ -467,23 +468,43 @@ impl GatewayApi for GatewayImpl {
467468
async fn sp_component_reset(
468469
rqctx: RequestContext<Self::Context>,
469470
path: Path<PathSpComponent>,
470-
) -> Result<HttpResponseUpdatedNoContent, HttpError> {
471+
) -> Result<HttpResponseUpdatedNoContent, SpComponentResetError> {
471472
let apictx = rqctx.context();
472473
let PathSpComponent { sp, component } = path.into_inner();
473474
let sp_id = sp.into();
474475
let handler = async {
475-
let sp = apictx.mgmt_switch.sp(sp_id)?;
476+
let sp = apictx.mgmt_switch.sp(sp_id).map_err(HttpError::from)?;
476477
let component = component_from_str(&component)?;
477478

479+
// Our config may specifically disallow resetting the SP of our
480+
// local sled. This is because resetting our local SP after an
481+
// update doesn't work: `reset_component_trigger` below will issue a
482+
// "reset with watchdog", wait for the SP to come back, then send a
483+
// "disarm the watchdog" message. But if we've reset our own local
484+
// sled, we won't be alive to disarm the watchdog, which will result
485+
// in the SP (erroneously) rolling back the update. (In production
486+
// we always disallow this; it's a config option to support dev/test
487+
// environments that don't need this.)
488+
if component == SpComponent::SP_ITSELF
489+
&& !apictx
490+
.mgmt_switch
491+
.allowed_to_reset_sp(sp_id)
492+
.map_err(HttpError::from)?
493+
{
494+
return Err(SpComponentResetError::ResetSpOfLocalSled);
495+
}
496+
478497
sp.reset_component_prepare(component)
479498
// We always want to run with the watchdog when resetting as
480-
// disabling the watchdog should be considered a debug only feature
499+
// disabling the watchdog should be considered a debug only
500+
// feature
481501
.and_then(|()| sp.reset_component_trigger(component, false))
482502
.await
483503
.map_err(|err| SpCommsError::SpCommunicationFailed {
484504
sp: sp_id,
485505
err,
486-
})?;
506+
})
507+
.map_err(HttpError::from)?;
487508

488509
Ok(HttpResponseUpdatedNoContent {})
489510
};

gateway/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ use futures::StreamExt;
2020
use futures::stream::FuturesUnordered;
2121
use gateway_sp_comms::InMemoryHostPhase2Provider;
2222
pub use management_switch::LocationConfig;
23+
pub use management_switch::LocationDescriptionConfig;
2324
pub use management_switch::LocationDeterminationConfig;
2425
pub use management_switch::ManagementSwitch;
2526
pub use management_switch::RetryConfig;

0 commit comments

Comments
 (0)