Skip to content

Commit 6073a1f

Browse files
authored
Merge branch 'main' into fix/direct-dep-features
2 parents d295098 + d3ae0b5 commit 6073a1f

26 files changed

Lines changed: 1457 additions & 265 deletions

File tree

crates/agent/src/ethernet_virtualization.rs

Lines changed: 60 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ use carbide_network::ip::prefix::Ipv4Net;
3434
use carbide_network::virtualization::{VpcVirtualizationType, build_dual_stack_list};
3535
use eyre::WrapErr;
3636
use mac_address::MacAddress;
37+
use nvue_client::client::NvueClientError;
3738
use nvue_client::{NvueClient, NvueConfig};
3839
use serde::Deserialize;
3940
use tokio::process::Command as TokioCommand;
@@ -145,8 +146,54 @@ struct PostAction {
145146
}
146147

147148
pub enum NvueUpdateFlavor<'a> {
148-
StartupFile { hbn_root: &'a Path, skip_post: bool },
149-
RestApi { nvue_client: &'a NvueClient },
149+
StartupFile {
150+
hbn_root: &'a Path,
151+
skip_post: bool,
152+
},
153+
RestApi {
154+
nvue_context: &'a mut NvueClientContext,
155+
},
156+
}
157+
158+
/// The NVUE client and other information associated with it.
159+
pub struct NvueClientContext {
160+
pub nvue_client: NvueClient,
161+
pub last_applied_hash: Option<u64>,
162+
}
163+
164+
impl NvueClientContext {
165+
pub fn new(nvue_client: NvueClient) -> Self {
166+
let last_applied_hash = None;
167+
Self {
168+
nvue_client,
169+
last_applied_hash,
170+
}
171+
}
172+
173+
// Wrap the inner nvue_client's `push_config()` and try to avoid re-applying
174+
// a configuration we're already using. Returns Ok(Some(revision_id)) on
175+
// a change, Ok(None) if the config was unchanged, and otherwise passes
176+
// through errors from the inner client.
177+
pub async fn update_config(
178+
&mut self,
179+
config: &NvueConfig,
180+
) -> Result<Option<String>, NvueClientError> {
181+
let new_hash = config.u64_hash();
182+
183+
if let Some(last_applied_hash) = self.last_applied_hash
184+
&& new_hash == last_applied_hash
185+
{
186+
Ok(None)
187+
} else {
188+
self.nvue_client
189+
.push_config(config)
190+
.await
191+
.map(|revision_id| {
192+
self.last_applied_hash.replace(new_hash);
193+
Some(revision_id)
194+
})
195+
}
196+
}
150197
}
151198

152199
/// Converts an RPC routing profile into the NVUE renderer model.
@@ -197,7 +244,8 @@ pub async fn update_nvue(
197244
) -> eyre::Result<bool> {
198245
let hbn_version = match update_flavor {
199246
NvueUpdateFlavor::StartupFile { .. } => hbn::read_version().await?,
200-
NvueUpdateFlavor::RestApi { nvue_client } => nvue_client
247+
NvueUpdateFlavor::RestApi { ref nvue_context } => nvue_context
248+
.nvue_client
201249
.system_build_info()
202250
.await
203251
.map_err(|e| eyre::eyre!("Couldn't get HBN version from NVUE: {e}"))
@@ -570,15 +618,19 @@ pub async fn update_nvue(
570618
}
571619
Ok(true)
572620
}
573-
NvueUpdateFlavor::RestApi { nvue_client } => {
621+
NvueUpdateFlavor::RestApi { nvue_context } => {
574622
let config = NvueConfig::from_yaml(&next_contents)
575623
.map_err(|e| eyre::eyre!("Couldn't parse NVUE config as YAML: {e}"))?;
576-
let revision_id = nvue_client
577-
.push_config(&config)
624+
let revision_id = nvue_context
625+
.update_config(&config)
578626
.await
579627
.map_err(|e| eyre::eyre!("Couldn't push new config to NVUE server: {e}"))?;
580-
tracing::debug!(revision_id, "Applied NVUE config via REST API");
581-
Ok(true)
628+
if let Some(revision_id) = revision_id {
629+
tracing::debug!(revision_id, "Applied NVUE config via REST API");
630+
Ok(true)
631+
} else {
632+
Ok(false)
633+
}
582634
}
583635
}
584636
}

crates/agent/src/main_loop.rs

Lines changed: 15 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ use crate::dpu::interface::Interface;
4949
use crate::dpu::route::{DpuRoutePlan, IpRoute, Route};
5050
use crate::duppet::{SummaryFormat, SyncOptions};
5151
use crate::ethernet_virtualization::{
52-
InterfaceTranslationMode, NvueUpdateFlavor, ServiceAddresses,
52+
InterfaceTranslationMode, NvueClientContext, NvueUpdateFlavor, ServiceAddresses,
5353
};
5454
use crate::fmds_client::FmdsUpdater;
5555
use crate::health::HealthCheckParams;
@@ -221,11 +221,12 @@ pub async fn setup_and_run(
221221
// We have eight cores. Letting ovs_vswitchd have one is OK.
222222
};
223223

224-
let nvue_client = match options.hbn_config_mode {
224+
let nvue_context = match options.hbn_config_mode {
225225
HbnConfigMode::ContainerExec => None,
226226
HbnConfigMode::NvueRest => {
227227
let nvue_client = nvue_client::NvueClient::new_https_from_env()?;
228-
Some(nvue_client)
228+
let nvue_context = NvueClientContext::new(nvue_client);
229+
Some(nvue_context)
229230
}
230231
};
231232

@@ -376,7 +377,7 @@ pub async fn setup_and_run(
376377
close_sender,
377378
network_monitor_handle,
378379
extension_service_manager,
379-
nvue_client,
380+
nvue_context,
380381
dhcp_interface_translation_mode,
381382
};
382383

@@ -409,7 +410,7 @@ struct MainLoop {
409410
network_monitor_handle: Option<JoinHandle<()>>,
410411
close_sender: watch::Sender<bool>,
411412
extension_service_manager: extension_services::ExtensionServiceManager,
412-
nvue_client: Option<nvue_client::NvueClient>,
413+
nvue_context: Option<NvueClientContext>,
413414
dhcp_interface_translation_mode: Option<InterfaceTranslationMode>,
414415
}
415416

@@ -558,10 +559,11 @@ impl MainLoop {
558559
if self.is_hbn_up {
559560
// First thing is to read the existing HBN version and properly set the hbn device names
560561
// associated with that version.
561-
let hbn_version = match self.nvue_client.as_mut() {
562+
let hbn_version = match self.nvue_context.as_mut() {
562563
None => hbn::read_version().await?,
563-
Some(nvue_client) => {
564-
let nvue_system_build = nvue_client.system_build_info().await?;
564+
Some(nvue_context) => {
565+
let nvue_system_build =
566+
nvue_context.nvue_client.system_build_info().await?;
565567
match nvue_system_build.strip_prefix("HBN ") {
566568
Some(hbn_version) => Ok(hbn_version.into()),
567569
None => Err(eyre::format_err!(
@@ -680,8 +682,8 @@ impl MainLoop {
680682
};
681683

682684
if bridging_result.is_ok() {
683-
let update_flavor = match self.nvue_client.as_ref() {
684-
Some(nvue_client) => NvueUpdateFlavor::RestApi { nvue_client },
685+
let update_flavor = match self.nvue_context.as_mut() {
686+
Some(nvue_context) => NvueUpdateFlavor::RestApi { nvue_context },
685687
None => NvueUpdateFlavor::StartupFile {
686688
hbn_root: &self.agent_config.hbn.root_dir,
687689
skip_post: self.agent_config.hbn.skip_reload,
@@ -783,7 +785,7 @@ impl MainLoop {
783785
match ethernet_virtualization::interfaces(
784786
&conf,
785787
self.factory_mac_address,
786-
self.nvue_client.as_ref(),
788+
self.nvue_context.as_ref().map(|c| &c.nvue_client),
787789
)
788790
.await
789791
{
@@ -826,7 +828,7 @@ impl MainLoop {
826828
current_instance_config_version = status_out.instance_config_version.clone();
827829
current_instance_id = status_out.instance_id.as_ref().map(|id| id.to_string());
828830

829-
let health_report = match self.nvue_client.as_ref() {
831+
let health_report = match self.nvue_context.as_ref() {
830832
None => {
831833
health::health_check(HealthCheckParams {
832834
hbn_root: &self.agent_config.hbn.root_dir,
@@ -840,7 +842,7 @@ impl MainLoop {
840842
})
841843
.await
842844
}
843-
Some(nvue_client) => health::nvue_api_health(nvue_client).await,
845+
Some(nvue_context) => health::nvue_api_health(&nvue_context.nvue_client).await,
844846
};
845847
is_healthy = !health_report.successes.is_empty() && health_report.alerts.is_empty();
846848
self.is_hbn_up = health::is_up(&health_report);

crates/api-db/src/attestation/spdm.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -220,6 +220,13 @@ pub async fn get_attestation_status_for_machine_id(
220220
.await
221221
.map_err(|e| DatabaseError::query(query, e))?;
222222

223+
if attestation_status_rows.is_empty() {
224+
return Err(DatabaseError::NotFoundError {
225+
kind: "SPDM Attestation Record",
226+
id: machine_id.to_string(),
227+
});
228+
}
229+
223230
// if all passed - PASSED
224231
// if all cancelled - CANCELLED
225232
// if any failed && none not in progress - FAILED

crates/api-model/src/attestation.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ pub mod spdm {
5454
use itertools::Itertools;
5555
use nras::{NrasError, NrasVerifierClient, ProcessedAttestationOutcome, RawAttestationOutcome};
5656
use serde::{Deserialize, Serialize};
57+
use sha2::{Digest, Sha256};
5758
use sqlx::Row;
5859
use sqlx::postgres::PgRow;
5960

@@ -96,6 +97,12 @@ pub mod spdm {
9697
pub completed_at: Option<DateTime<Utc>>,
9798
}
9899

100+
impl SpdmDeviceAttestation {
101+
pub fn nonce_hex(&self) -> String {
102+
hex::encode(Sha256::digest(self.nonce.as_bytes()))
103+
}
104+
}
105+
99106
/// Major state, associated with Machine.
100107
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
101108
pub enum SpdmAttestationState {

crates/api-model/src/machine/mod.rs

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2687,7 +2687,7 @@ pub enum HardwareHealthReportsConfig {
26872687
pub fn dpf_based_dpu_provisioning_possible(
26882688
state: &ManagedHostStateSnapshot,
26892689
dpf_enabled_at_site: bool,
2690-
reprovisioing_case: bool,
2690+
reprovisioning_case: bool,
26912691
) -> bool {
26922692
// DPF is disabled at site.
26932693
if !dpf_enabled_at_site {
@@ -2703,11 +2703,19 @@ pub fn dpf_based_dpu_provisioning_possible(
27032703
return false;
27042704
}
27052705

2706-
// if it is reprovisioing case, initial ingestion should be done with dpf to continue
2707-
// reprovision.
2708-
if reprovisioing_case && !state.host_snapshot.dpf.used_for_ingestion {
2706+
// if it is reprovisioning case, initial ingestion should be done with dpf
2707+
// to continue or we should be trying to reprovision all the dpus (switching
2708+
// to DPF). Reprovisioning only a subset of DPUs cannot flip the host to DPF.
2709+
if reprovisioning_case
2710+
&& !state.host_snapshot.dpf.used_for_ingestion
2711+
&& !state
2712+
.dpu_snapshots
2713+
.iter()
2714+
.all(|dpu| dpu.reprovision_requested.is_some())
2715+
{
27092716
tracing::info!(
2710-
"DPF based DPU reprovisioning is not possible because initial ingestion is not done with DPF - host {}.",
2717+
"DPF based DPU reprovisioning is not possible for host {} because initial ingestion is not done with DPF \
2718+
and not all DPUs are being reprovisioned.",
27112719
state.host_snapshot.id
27122720
);
27132721
return false;

crates/api-model/src/machine/slas.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ pub const MEASUREMENT_WAIT_FOR_MEASUREMENT: Duration = Duration::from_secs(30 *
4343

4444
pub const SPDM_ATTESTATION_TRIGGER: Duration = Duration::from_secs(30);
4545

46-
pub const SPDM_ATTESTATION_RESULT_POLL: Duration = Duration::from_secs(30 * 60);
46+
pub const SPDM_ATTESTATION_RESULT_POLL: Duration = Duration::from_secs(10 * 60);
4747

4848
pub const START_ASSIGNMENT_CYCLE: Duration = Duration::from_secs(60);
4949

crates/api/src/cfg/file.rs

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -643,6 +643,13 @@ pub struct CarbideConfig {
643643
/// hidden when the list is empty.
644644
#[serde(default)]
645645
pub web_ui_sidebar_tools: Vec<ToolLink>,
646+
647+
/// In-memory log history for the admin web live log viewer
648+
/// (`/admin/logs`): how much recent log data to keep for
649+
/// replay-on-connect and scrollback, and how many lines to send
650+
/// per page to the browser.
651+
#[serde(default)]
652+
pub log_history: LogHistoryConfig,
646653
}
647654

648655
impl CarbideConfig {
@@ -678,6 +685,40 @@ pub struct ToolLink {
678685
pub url: String,
679686
}
680687

688+
/// In-memory log history for the admin web live log viewer
689+
/// (`crate::web::logs`). Bounds memory use and the page size served
690+
/// to the browser.
691+
#[derive(Clone, Debug, Serialize, Deserialize)]
692+
pub struct LogHistoryConfig {
693+
/// Maximum amount of recent log history to retain in memory, in
694+
/// MiB. Oldest lines are evicted once the budget is exceeded.
695+
/// Default 128.
696+
#[serde(default = "default_log_history_max_megabytes")]
697+
pub max_megabytes: usize,
698+
699+
/// Number of lines sent in the initial view and in each
700+
/// scrollback page. Default 500.
701+
#[serde(default = "default_log_history_page_size")]
702+
pub page_size: usize,
703+
}
704+
705+
impl Default for LogHistoryConfig {
706+
fn default() -> Self {
707+
Self {
708+
max_megabytes: default_log_history_max_megabytes(),
709+
page_size: default_log_history_page_size(),
710+
}
711+
}
712+
}
713+
714+
fn default_log_history_max_megabytes() -> usize {
715+
128
716+
}
717+
718+
fn default_log_history_page_size() -> usize {
719+
500
720+
}
721+
681722
#[derive(Clone, Debug, Default, Deserialize, Serialize, PartialEq)]
682723
pub enum BgpLeafSessionPassword {
683724
/// Use a defined site-wide password.

crates/api/src/logging/setup.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@ pub fn setup_logging(
7777
debug: u8,
7878
extra_logfmt_event_fields: Vec<String>,
7979
override_logging_subscriber: Option<impl SubscriberInitExt>,
80+
log_history_max_bytes: usize,
8081
) -> eyre::Result<Logging> {
8182
// This configures emission of logs in LogFmt syntax
8283
// and emission of metrics
@@ -107,7 +108,7 @@ pub fn setup_logging(
107108

108109
// Used as part of a layer for collecting + brodcasting
109110
// log events to the admin web UI.
110-
let log_stream = LogStream::default();
111+
let log_stream = LogStream::with_max_bytes(log_history_max_bytes);
111112

112113
// == Dynamic filter for tracing enabled/disabled ==
113114
// This doesn't track levels but instead just enabled/disabled (when we want tracing enabled, we

0 commit comments

Comments
 (0)