Skip to content

Commit

Permalink
[π˜€π—½π—Ώ] initial version
Browse files Browse the repository at this point in the history
Created using spr 1.3.5
  • Loading branch information
sunshowers committed Dec 6, 2023
1 parent bcd7ac5 commit df737fa
Show file tree
Hide file tree
Showing 6 changed files with 238 additions and 37 deletions.
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"]}]}}}
156 changes: 156 additions & 0 deletions sled-agent/tests/integration_tests/early_network.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
// 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;

/// Test that previous and current versions of `EarlyNetworkConfig` blobs
/// deserialize correctly.
#[test]
fn early_network_blobs_deserialize() {
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("tests/data/early_network_blobs.txt")
.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(
"tests/data/early_network_blobs.txt",
&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;

0 comments on commit df737fa

Please sign in to comment.