Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[sled-agent] address EarlyNetworkConfig issues from 20231130 dogfood mupdate #4636

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
104 changes: 70 additions & 34 deletions sled-agent/src/bootstrap/early_networking.rs
Original file line number Diff line number Diff line change
Expand Up @@ -682,6 +682,65 @@ pub struct EarlyNetworkConfig {
pub body: EarlyNetworkConfigBody,
}

impl EarlyNetworkConfig {
// Note: This currently only converts between v0 and v1 or deserializes v1 of
// `EarlyNetworkConfig`.
pub fn deserialize_bootstore_config(
log: &Logger,
config: &bootstore::NetworkConfig,
) -> Result<Self, serde_json::Error> {
// Try to deserialize the latest version of the data structure (v1). If
// that succeeds we are done.
let v1_error =
match serde_json::from_slice::<EarlyNetworkConfig>(&config.blob) {
Ok(val) => return Ok(val),
Err(error) => {
// Log this error and continue trying to deserialize older
// versions.
warn!(
log,
"Failed to deserialize EarlyNetworkConfig \
as v1, trying next as v0: {}",
error,
);
error
}
};

match serde_json::from_slice::<EarlyNetworkConfigV0>(&config.blob) {
Ok(val) => {
// Convert from v0 to v1
return Ok(EarlyNetworkConfig {
generation: val.generation,
schema_version: 1,
body: EarlyNetworkConfigBody {
ntp_servers: val.ntp_servers,
rack_network_config: val.rack_network_config.map(
|v0_config| {
RackNetworkConfigV0::to_v1(
val.rack_subnet,
v0_config,
)
},
),
},
});
}
Err(error) => {
// Log this error.
warn!(
log,
"Failed to deserialize EarlyNetworkConfig as v0: {}", error,
);
}
};

// Return the v1 error preferentially over the v0 error as it's more
// likely to be useful.
Err(v1_error)
}
}

/// This is the actual configuration of EarlyNetworking.
///
/// We nest it below the "header" of `generation` and `schema_version` so that
Expand Down Expand Up @@ -711,39 +770,6 @@ impl From<EarlyNetworkConfig> for bootstore::NetworkConfig {
}
}

// Note: This currently only converts between v0 and v1 or deserializes v1 of
// `EarlyNetworkConfig`.
impl TryFrom<bootstore::NetworkConfig> for EarlyNetworkConfig {
type Error = serde_json::Error;

fn try_from(
value: bootstore::NetworkConfig,
) -> std::result::Result<Self, Self::Error> {
// Try to deserialize the latest version of the data structure (v1). If
// that succeeds we are done.
if let Ok(val) =
serde_json::from_slice::<EarlyNetworkConfig>(&value.blob)
{
return Ok(val);
}

// We don't have the latest version. Try to deserialize v0 and then
// convert it to the latest version.
let v0 = serde_json::from_slice::<EarlyNetworkConfigV0>(&value.blob)?;

Ok(EarlyNetworkConfig {
generation: v0.generation,
schema_version: 1,
body: EarlyNetworkConfigBody {
ntp_servers: v0.ntp_servers,
rack_network_config: v0.rack_network_config.map(|v0_config| {
RackNetworkConfigV0::to_v1(v0.rack_subnet, v0_config)
}),
},
})
}
}

/// Deprecated, use `RackNetworkConfig` instead. Cannot actually deprecate due to
/// <https://github.com/serde-rs/serde/issues/2195>
///
Expand Down Expand Up @@ -815,9 +841,13 @@ fn convert_fec(fec: &PortFec) -> dpd_client::types::PortFec {
mod tests {
use super::*;
use omicron_common::api::internal::shared::RouteConfig;
use omicron_test_utils::dev::test_setup_log;

#[test]
fn serialized_early_network_config_v0_to_v1_conversion() {
let logctx = test_setup_log(
"serialized_early_network_config_v0_to_v1_conversion",
);
let v0 = EarlyNetworkConfigV0 {
generation: 1,
rack_subnet: Ipv6Addr::UNSPECIFIED,
Expand All @@ -841,7 +871,11 @@ mod tests {
let bootstore_conf =
bootstore::NetworkConfig { generation: 1, blob: v0_serialized };

let v1 = EarlyNetworkConfig::try_from(bootstore_conf).unwrap();
let v1 = EarlyNetworkConfig::deserialize_bootstore_config(
&logctx.log,
&bootstore_conf,
)
.unwrap();
let v0_rack_network_config = v0.rack_network_config.unwrap();
let uplink = v0_rack_network_config.uplinks[0].clone();
let expected = EarlyNetworkConfig {
Expand Down Expand Up @@ -872,5 +906,7 @@ mod tests {
};

assert_eq!(expected, v1);

logctx.cleanup_successful();
}
}
5 changes: 4 additions & 1 deletion sled-agent/src/http_entrypoints.rs
Original file line number Diff line number Diff line change
Expand Up @@ -659,7 +659,10 @@ async fn read_network_bootstore_config_cache(
})?;

let config = match config {
Some(config) => EarlyNetworkConfig::try_from(config).map_err(|e| {
Some(config) => EarlyNetworkConfig::deserialize_bootstore_config(
&rqctx.log, &config,
)
.map_err(|e| {
HttpError::for_internal_error(format!(
"deserialize early network config: {e}"
))
Expand Down
7 changes: 5 additions & 2 deletions sled-agent/src/sled_agent.rs
Original file line number Diff line number Diff line change
Expand Up @@ -445,8 +445,11 @@ impl SledAgent {
})?;

let early_network_config =
EarlyNetworkConfig::try_from(serialized_config)
.map_err(|err| BackoffError::transient(err.to_string()))?;
EarlyNetworkConfig::deserialize_bootstore_config(
&log,
&serialized_config,
)
.map_err(|err| BackoffError::transient(err.to_string()))?;

Ok(early_network_config.body.rack_network_config)
};
Expand Down
2 changes: 2 additions & 0 deletions sled-agent/tests/data/early_network_blobs.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
2023-11-30 mupdate failing blob,{"generation":15,"schema_version":1,"body":{"ntp_servers":[],"rack_network_config":{"rack_subnet":"fd00:1122:3344:100::/56","infra_ip_first":"0.0.0.0","infra_ip_last":"0.0.0.0","ports":[{"routes":[],"addresses":[],"switch":"switch1","port":"qsfp0","uplink_port_speed":"speed100_g","uplink_port_fec":"none","bgp_peers":[]},{"routes":[],"addresses":["172.20.15.53/29"],"switch":"switch1","port":"qsfp18","uplink_port_speed":"speed100_g","uplink_port_fec":"rs","bgp_peers":[{"asn":65002,"port":"qsfp18","addr":"172.20.15.51","hold_time":6,"idle_hold_time":6,"delay_open":0,"connect_retry":3,"keepalive":2}]},{"routes":[],"addresses":["172.20.15.45/29"],"switch":"switch0","port":"qsfp18","uplink_port_speed":"speed100_g","uplink_port_fec":"rs","bgp_peers":[{"asn":65002,"port":"qsfp18","addr":"172.20.15.43","hold_time":6,"idle_hold_time":6,"delay_open":0,"connect_retry":3,"keepalive":2}]},{"routes":[],"addresses":[],"switch":"switch0","port":"qsfp0","uplink_port_speed":"speed100_g","uplink_port_fec":"none","bgp_peers":[]}],"bgp":[{"asn":65002,"originate":["172.20.26.0/24"]},{"asn":65002,"originate":["172.20.26.0/24"]}]}}}
2023-12-06 config,{"generation":20,"schema_version":1,"body":{"ntp_servers":["ntp.example.com"],"rack_network_config":{"rack_subnet":"ff01::/32","infra_ip_first":"127.0.0.1","infra_ip_last":"127.1.0.1","ports":[{"routes":[{"destination":"10.1.9.32/16","nexthop":"10.1.9.32"}],"addresses":["2001:db8::/96"],"switch":"switch0","port":"foo","uplink_port_speed":"speed200_g","uplink_port_fec":"firecode","bgp_peers":[{"asn":65000,"port":"bar","addr":"1.2.3.4","hold_time":20,"idle_hold_time":50,"delay_open":null,"connect_retry":30,"keepalive":10}],"autoneg":true}],"bgp":[{"asn":20000,"originate":["192.168.0.0/24"]}]}}}
154 changes: 154 additions & 0 deletions sled-agent/tests/integration_tests/early_network.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at https://mozilla.org/MPL/2.0/.

//! Tests that EarlyNetworkConfig deserializes across versions.

use std::net::Ipv4Addr;

use bootstore::schemes::v0 as bootstore;
use omicron_common::api::{
external::SwitchLocation,
internal::shared::{
BgpConfig, BgpPeerConfig, PortConfigV1, PortFec, PortSpeed,
RackNetworkConfig, RouteConfig,
},
};
use omicron_sled_agent::bootstrap::early_networking::{
EarlyNetworkConfig, EarlyNetworkConfigBody,
};
use omicron_test_utils::dev::test_setup_log;

const BLOB_PATH: &str = "tests/data/early_network_blobs.txt";

/// Test that previous and current versions of `EarlyNetworkConfig` blobs
/// deserialize correctly.
#[test]
fn early_network_blobs_deserialize() {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FWIW: for little parsers like this, I really like using nom.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah definitely if it's a bit more complex than this. In this case I figured it was just splitting by comma.

let logctx = test_setup_log("early_network_blobs_deserialize");

let (current_desc, current_config) = current_config_example();
assert!(
!current_desc.contains(',') && !current_desc.contains('\n'),
"current_desc must not contain commas or newlines"
);

// Read old blobs as newline-delimited JSON.
let mut known_blobs = std::fs::read_to_string(BLOB_PATH)
.expect("error reading early_network_blobs.txt");
let mut current_blob_is_known = false;
for (blob_idx, line) in known_blobs.lines().enumerate() {
let blob_lineno = blob_idx + 1;
let (blob_desc, blob_json) =
line.split_once(',').unwrap_or_else(|| {
panic!(
"error parsing early_network_blobs.txt \
line {blob_lineno}: missing comma",
);
});

// Attempt to deserialize this blob.
let config = serde_json::from_str::<EarlyNetworkConfig>(blob_json)
.unwrap_or_else(|error| {
panic!(
"error deserializing early_network_blobs.txt \
\"{blob_desc}\" (line {blob_lineno}): {error}",
);
});

// Does this config match the current config?
if blob_desc == current_desc {
assert_eq!(
config, current_config,
"early_network_blobs.txt line {}: {} does not match current config",
blob_lineno, blob_desc
);
current_blob_is_known = true;
}

// Now attempt to put this blob into a bootstore config, and deserialize that.
let network_config = bootstore::NetworkConfig {
generation: config.generation,
blob: blob_json.to_owned().into(),
};
let config2 = EarlyNetworkConfig::deserialize_bootstore_config(
&logctx.log,
&network_config,
).unwrap_or_else(|error| {
panic!(
"error deserializing early_network_blobs.txt \
\"{blob_desc}\" (line {blob_lineno}) as bootstore config: {error}",
);
});

assert_eq!(
config, config2,
"early_network_blobs.txt line {}: {} does not match deserialization \
as bootstore config",
blob_lineno, blob_desc
);
}

// If the current blob was not covered, add it to the list of known blobs.
if !current_blob_is_known {
let current_blob_json = serde_json::to_string(&current_config).unwrap();
let current_blob = format!("{},{}", current_desc, current_blob_json);
known_blobs.push_str(&current_blob);
known_blobs.push('\n');
}

expectorate::assert_contents(BLOB_PATH, &known_blobs);

logctx.cleanup_successful();
}

/// Returns a current version of the EarlyNetworkConfig blob, along with a
/// short description of the current version. The values can be arbitrary, but
/// this should be a nontrivial blob where no vectors are empty.
///
/// The goal is that if the definition of `EarlyNetworkConfig` changes in the
/// future, older blobs can still be deserialized correctly.
fn current_config_example() -> (&'static str, EarlyNetworkConfig) {
// NOTE: the description must not contain commas or newlines.
let description = "2023-12-06 config";
let config = EarlyNetworkConfig {
generation: 20,
schema_version: 1,
body: EarlyNetworkConfigBody {
ntp_servers: vec!["ntp.example.com".to_owned()],
rack_network_config: Some(RackNetworkConfig {
rack_subnet: "ff01::0/32".parse().unwrap(),
infra_ip_first: Ipv4Addr::new(127, 0, 0, 1),
infra_ip_last: Ipv4Addr::new(127, 1, 0, 1),
ports: vec![PortConfigV1 {
routes: vec![RouteConfig {
destination: "10.1.9.32/16".parse().unwrap(),
nexthop: "10.1.9.32".parse().unwrap(),
}],
addresses: vec!["2001:db8::/96".parse().unwrap()],
switch: SwitchLocation::Switch0,
port: "foo".to_owned(),
uplink_port_speed: PortSpeed::Speed200G,
uplink_port_fec: PortFec::Firecode,
bgp_peers: vec![BgpPeerConfig {
asn: 65000,
port: "bar".to_owned(),
addr: Ipv4Addr::new(1, 2, 3, 4),
hold_time: Some(20),
idle_hold_time: Some(50),
delay_open: None,
connect_retry: Some(30),
keepalive: Some(10),
}],
autoneg: true,
}],
bgp: vec![BgpConfig {
asn: 20000,
originate: vec!["192.168.0.0/24".parse().unwrap()],
}],
}),
},
};

(description, config)
}
1 change: 1 addition & 0 deletions sled-agent/tests/integration_tests/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@
// file, You can obtain one at https://mozilla.org/MPL/2.0/.

mod commands;
mod early_network;
Loading